diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 343f29a..840b730 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -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' + + + + + Request Network Token List + + + +

Redirecting to latest.json...

+ + + EOF + echo "Created versioned files: v${VERSION}.json, v${MAJOR}.${MINOR}.json, v${MAJOR}.json, latest.json" - name: Deploy to GitHub Pages diff --git a/src/schemas/token-list-schema.json b/src/schemas/token-list-schema.json index 0cc9afc..751a1db 100644 --- a/src/schemas/token-list-schema.json +++ b/src/schemas/token-list-schema.json @@ -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", diff --git a/src/types/index.ts b/src/types/index.ts index 163b87a..9ad4063 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -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 = { mainnet: 1, matic: 137, diff --git a/src/validation/validate.ts b/src/validation/validate.ts index e60365e..741d556 100644 --- a/src/validation/validate.ts +++ b/src/validation/validate.ts @@ -8,6 +8,7 @@ import { NetworkType, TokenType, CHAIN_IDS, + TIMESTAMP_PLACEHOLDER, } from "../types"; import schema from "../schemas/token-list-schema.json"; @@ -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 { diff --git a/tests/validation.test.ts b/tests/validation.test.ts index ac727f5..50069ba 100644 --- a/tests/validation.test.ts +++ b/tests/validation.test.ts @@ -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 = { @@ -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], }; @@ -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: [ { @@ -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, @@ -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: [ { @@ -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], }; @@ -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: [ { @@ -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: [ { @@ -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", @@ -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: [ { @@ -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: [ { diff --git a/tokens/token-list.json b/tokens/token-list.json index 0091c17..d0bb979 100644 --- a/tokens/token-list.json +++ b/tokens/token-list.json @@ -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": [ {