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": [
{