Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 66 additions & 3 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -88,12 +88,75 @@ jobs:
# Copy base file
cp tokens/token-list.json dist/token-list.json

# Copy all historical versions
# Copy all historical versions and create major.minor and major aliases
if [ -d "versions" ]; then
cp versions/*.json dist/ 2>/dev/null || true
echo "Copied historical versions from versions/ directory"
# Declare associative arrays to track highest versions
declare -A highest_patch # tracks highest patch for each major.minor
declare -A highest_minor # tracks highest minor.patch for each major

for version_file in versions/*.json; do
if [ -f "$version_file" ]; then
# Copy the full version file (e.g., v1.2.0.json)
cp "$version_file" dist/

# Extract version components from the file content
FILE_MAJOR=$(jq -r '.version.major' "$version_file")
FILE_MINOR=$(jq -r '.version.minor' "$version_file")
FILE_PATCH=$(jq -r '.version.patch' "$version_file")

# Skip if this is the current version's major (already created above)
if [ "$FILE_MAJOR" = "$MAJOR" ]; then
echo "Skipping major alias v${FILE_MAJOR}.json - current version takes precedence"
# But still process minor alias if different minor version
if [ "$FILE_MINOR" = "$MINOR" ]; then
echo "Skipping minor alias v${FILE_MAJOR}.${FILE_MINOR}.json - current version takes precedence"
continue
fi
fi

# Create/update major.minor alias (e.g., v1.2.json)
MINOR_KEY="${FILE_MAJOR}.${FILE_MINOR}"
MINOR_ALIAS="v${MINOR_KEY}.json"
CURRENT_PATCH=${highest_patch[$MINOR_KEY]:-"-1"}
if [ "$FILE_PATCH" -gt "$CURRENT_PATCH" ]; then
cp "$version_file" "dist/${MINOR_ALIAS}"
highest_patch[$MINOR_KEY]=$FILE_PATCH
echo "Created/updated alias ${MINOR_ALIAS} from $(basename $version_file)"
fi

# Create/update major alias (e.g., v1.json) - only for non-current majors
if [ "$FILE_MAJOR" != "$MAJOR" ]; then
MAJOR_ALIAS="v${FILE_MAJOR}.json"
# Compare as "minor.patch" to find highest version within major
CURRENT_MINOR_PATCH=${highest_minor[$FILE_MAJOR]:-"-1.-1"}
CURRENT_M=$(echo $CURRENT_MINOR_PATCH | cut -d. -f1)
CURRENT_P=$(echo $CURRENT_MINOR_PATCH | cut -d. -f2)
if [ "$FILE_MINOR" -gt "$CURRENT_M" ] || ([ "$FILE_MINOR" = "$CURRENT_M" ] && [ "$FILE_PATCH" -gt "$CURRENT_P" ]); then
cp "$version_file" "dist/${MAJOR_ALIAS}"
highest_minor[$FILE_MAJOR]="${FILE_MINOR}.${FILE_PATCH}"
echo "Created/updated alias ${MAJOR_ALIAS} from $(basename $version_file)"
fi
fi
fi
done
echo "Copied historical versions and created aliases"
fi

# Create index.html that redirects to latest.json
cat > dist/index.html << 'EOF'
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Request Network Token List</title>
<meta http-equiv="refresh" content="0; url=latest.json">
</head>
<body>
<p>Redirecting to <a href="latest.json">latest.json</a>...</p>
</body>
</html>
EOF

echo "Created versioned files: v${VERSION}.json, v${MAJOR}.${MINOR}.json, v${MAJOR}.json, latest.json"

- name: Deploy to GitHub Pages
Expand Down
7 changes: 5 additions & 2 deletions src/schemas/token-list-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@
},
"timestamp": {
"type": "string",
"format": "date-time",
"description": "The timestamp of when this list was last updated"
"description": "The timestamp of when this list was last updated (ISO 8601 format)",
"oneOf": [
{ "format": "date-time" },
{ "const": "Set automatically during deployment" }
]
},
"version": {
"type": "object",
Expand Down
6 changes: 6 additions & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ export enum NetworkType {
FIAT = "fiat",
}

/**
* Placeholder value for timestamp in the source token list file.
* The actual timestamp is set during deployment by the GitHub Actions workflow.
*/
export const TIMESTAMP_PLACEHOLDER = "Set automatically during deployment";

export const CHAIN_IDS: Record<string, number> = {
mainnet: 1,
matic: 137,
Expand Down
18 changes: 16 additions & 2 deletions src/validation/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
NetworkType,
TokenType,
CHAIN_IDS,
TIMESTAMP_PLACEHOLDER,
} from "../types";
import schema from "../schemas/token-list-schema.json";

Expand Down Expand Up @@ -112,9 +113,22 @@ function isValidVersion(version: {
);
}

/**
* Validates the timestamp field.
*
* Note: The JSON schema allows both date-time format and the placeholder string
* so that consumers can use the schema to validate deployed token lists.
* However, THIS validation script enforces the placeholder because it runs
* on the source file (tokens/token-list.json) during CI. The actual timestamp
* is set during deployment by the GitHub Actions workflow.
*/
function isValidTimestamp(timestamp: string): boolean {
const date = new Date(timestamp);
return date.toString() !== "Invalid Date";
// Source file must use placeholder - actual timestamp is set during deployment
if (timestamp !== TIMESTAMP_PLACEHOLDER) {
console.error(`Timestamp must be '${TIMESTAMP_PLACEHOLDER}' - actual timestamp is set during deployment`);
return false;
}
return true;
}

function isValidDecimals(decimals: number): boolean {
Expand Down
33 changes: 22 additions & 11 deletions tests/validation.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, it, expect } from "vitest";
import { validateTokenList } from "../src/validation/validate";
import { NetworkType, TokenList, TokenType, CHAIN_IDS } from "../src/types";
import { NetworkType, TokenList, TokenType, CHAIN_IDS, TIMESTAMP_PLACEHOLDER } from "../src/types";

describe("Token List Validation", () => {
const validToken = {
Expand All @@ -17,7 +17,7 @@ describe("Token List Validation", () => {
it("should validate a correct token list", async () => {
const validList: TokenList = {
name: "Test Token List",
timestamp: new Date().toISOString(),
timestamp: TIMESTAMP_PLACEHOLDER,
version: { major: 1, minor: 0, patch: 0 },
tokens: [validToken],
};
Expand All @@ -28,7 +28,7 @@ describe("Token List Validation", () => {
it("should reject invalid token addresses", async () => {
const invalidList: TokenList = {
name: "Test Token List",
timestamp: new Date().toISOString(),
timestamp: TIMESTAMP_PLACEHOLDER,
version: { major: 1, minor: 0, patch: 0 },
tokens: [
{
Expand All @@ -44,7 +44,7 @@ describe("Token List Validation", () => {
it("should reject duplicate token IDs", async () => {
const duplicateList: TokenList = {
name: "Test Token List",
timestamp: new Date().toISOString(),
timestamp: TIMESTAMP_PLACEHOLDER,
version: { major: 1, minor: 0, patch: 0 },
tokens: [
validToken,
Expand All @@ -61,7 +61,7 @@ describe("Token List Validation", () => {
it("should reject invalid decimals", async () => {
const invalidList: TokenList = {
name: "Test Token List",
timestamp: new Date().toISOString(),
timestamp: TIMESTAMP_PLACEHOLDER,
version: { major: 1, minor: 0, patch: 0 },
tokens: [
{
Expand All @@ -77,7 +77,7 @@ describe("Token List Validation", () => {
it("should validate version format", async () => {
const invalidList: TokenList = {
name: "Test Token List",
timestamp: new Date().toISOString(),
timestamp: TIMESTAMP_PLACEHOLDER,
version: { major: -1, minor: 0, patch: 0 },
tokens: [validToken],
};
Expand All @@ -88,7 +88,7 @@ describe("Token List Validation", () => {
it("should validate network type", async () => {
const invalidList: TokenList = {
name: "Test Token List",
timestamp: new Date().toISOString(),
timestamp: TIMESTAMP_PLACEHOLDER,
version: { major: 1, minor: 0, patch: 0 },
tokens: [
{
Expand All @@ -104,7 +104,7 @@ describe("Token List Validation", () => {
it("should validate token type", async () => {
const invalidList: TokenList = {
name: "Test Token List",
timestamp: new Date().toISOString(),
timestamp: TIMESTAMP_PLACEHOLDER,
version: { major: 1, minor: 0, patch: 0 },
tokens: [
{
Expand All @@ -117,7 +117,18 @@ describe("Token List Validation", () => {
expect(await validateTokenList(invalidList)).toBe(false);
});

it("should validate timestamp format", async () => {
it("should reject real timestamps (must use placeholder)", async () => {
const invalidList: TokenList = {
name: "Test Token List",
timestamp: new Date().toISOString(),
version: { major: 1, minor: 0, patch: 0 },
tokens: [validToken],
};

expect(await validateTokenList(invalidList)).toBe(false);
});

it("should reject invalid timestamp format", async () => {
const invalidList: TokenList = {
name: "Test Token List",
timestamp: "invalid-date",
Expand All @@ -131,7 +142,7 @@ describe("Token List Validation", () => {
it("should reject invalid chainId for network", async () => {
const invalidList: TokenList = {
name: "Test Token List",
timestamp: new Date().toISOString(),
timestamp: TIMESTAMP_PLACEHOLDER,
version: { major: 1, minor: 0, patch: 0 },
tokens: [
{
Expand All @@ -156,7 +167,7 @@ describe("Token List Validation", () => {
for (const { network, chainId } of networks) {
const validList: TokenList = {
name: "Test Token List",
timestamp: new Date().toISOString(),
timestamp: TIMESTAMP_PLACEHOLDER,
version: { major: 1, minor: 0, patch: 0 },
tokens: [
{
Expand Down
4 changes: 2 additions & 2 deletions tokens/token-list.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
{
"name": "Request Network Token List",
"timestamp": "2025-12-02T15:43:26.000Z",
"timestamp": "Set automatically during deployment",
"version": {
"major": 1,
"minor": 3,
"patch": 0
"patch": 1
},
"tokens": [
{
Expand Down