diff --git a/.github/workflows/generate-tokenlists.yml b/.github/workflows/generate-tokenlists.yml index bad9fb5..27e1a0f 100644 --- a/.github/workflows/generate-tokenlists.yml +++ b/.github/workflows/generate-tokenlists.yml @@ -11,21 +11,20 @@ jobs: runs-on: ubuntu-latest permissions: - # Give the default GITHUB_TOKEN write permission to commit and push the - # added or changed files to the repository. contents: write + id-token: write steps: - name: Checkout code - uses: actions/checkout@v5 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install uv - uses: astral-sh/setup-uv@v4 + uses: astral-sh/setup-uv@38f3f104447c67c051c4a08e39b64a148898af3a # v4.2.0 with: enable-cache: true @@ -37,7 +36,50 @@ jobs: # Commit all changed files back to the repository - name: Commit changes - uses: stefanzweifel/git-auto-commit-action@v5 + uses: stefanzweifel/git-auto-commit-action@b863ae1933cb653a53c021fe36dbb774e1fb9403 # v5.2.0 with: commit_message: "Update tokenlist-mainnet.json" file_pattern: "tokenlist-mainnet.json" + + # ---- npm package build & publish ---- + + - name: Set up Node.js + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: "20" + registry-url: "https://registry.npmjs.org" + + - name: Install npm dependencies + working-directory: npm + run: npm ci --ignore-scripts + + - name: Audit dependencies + working-directory: npm + run: npm audit --audit-level=high + + - name: Build package + working-directory: npm + run: npm run build + + - name: Validate package contents + working-directory: npm + run: npm pack --dry-run 2>&1 + + - name: Check if version already published + id: check + working-directory: npm + run: | + VERSION=$(node -p "require('./package.json').version") + CURRENT=$(npm view @monad-crypto/token-list version 2>/dev/null || echo "0.0.0") + if [ "$CURRENT" = "$VERSION" ]; then + echo "skip=true" >> "$GITHUB_OUTPUT" + else + echo "skip=false" >> "$GITHUB_OUTPUT" + fi + + - name: Publish to npm + if: steps.check.outputs.skip != 'true' + working-directory: npm + run: npm publish --access public --provenance + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.gitignore b/.gitignore index 3323b34..3430f9f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,9 @@ .env .DS_Store + +# npm package build artifacts +npm/src/mainnet.ts +npm/src/testnet.ts +npm/src/index.ts +npm/dist/ +npm/node_modules/ diff --git a/npm/package-lock.json b/npm/package-lock.json new file mode 100644 index 0000000..35092cf --- /dev/null +++ b/npm/package-lock.json @@ -0,0 +1,30 @@ +{ + "name": "@monad-crypto/token-list", + "version": "2.27.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@monad-crypto/token-list", + "version": "2.27.0", + "license": "MIT", + "devDependencies": { + "typescript": "~5.7.0" + } + }, + "node_modules/typescript": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + } + } +} diff --git a/npm/package.json b/npm/package.json new file mode 100644 index 0000000..469599f --- /dev/null +++ b/npm/package.json @@ -0,0 +1,91 @@ +{ + "name": "@monad-crypto/token-list", + "version": "2.27.0", + "description": "Official Monad token list with full TypeScript types", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/monad-crypto/token-list.git", + "directory": "npm" + }, + "homepage": "https://github.com/monad-crypto/token-list/tree/main/npm#readme", + "bugs": { + "url": "https://github.com/monad-crypto/token-list/issues" + }, + "keywords": [ + "monad", + "token-list", + "erc20", + "crypto", + "ethereum" + ], + "type": "module", + "main": "./dist/cjs/index.js", + "module": "./dist/esm/index.js", + "types": "./dist/esm/index.d.ts", + "exports": { + ".": { + "import": { + "types": "./dist/esm/index.d.ts", + "default": "./dist/esm/index.js" + }, + "require": { + "types": "./dist/cjs/index.d.ts", + "default": "./dist/cjs/index.js" + } + }, + "./mainnet": { + "import": { + "types": "./dist/esm/mainnet.d.ts", + "default": "./dist/esm/mainnet.js" + }, + "require": { + "types": "./dist/cjs/mainnet.d.ts", + "default": "./dist/cjs/mainnet.js" + } + }, + "./testnet": { + "import": { + "types": "./dist/esm/testnet.d.ts", + "default": "./dist/esm/testnet.js" + }, + "require": { + "types": "./dist/cjs/testnet.d.ts", + "default": "./dist/cjs/testnet.js" + } + }, + "./types": { + "import": { + "types": "./dist/esm/types.d.ts", + "default": "./dist/esm/types.js" + }, + "require": { + "types": "./dist/cjs/types.d.ts", + "default": "./dist/cjs/types.js" + } + }, + "./package.json": "./package.json" + }, + "files": [ + "dist" + ], + "scripts": { + "clean": "rm -rf dist", + "codegen": "node scripts/codegen.mjs", + "build:esm": "tsc -p tsconfig.json", + "build:cjs": "tsc -p tsconfig.cjs.json", + "build": "npm run clean && npm run codegen && npm run build:esm && npm run build:cjs", + "prepublishOnly": "npm run build" + }, + "devDependencies": { + "typescript": "~5.7.0" + }, + "sideEffects": false, + "engines": { + "node": ">=18" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org" + } +} diff --git a/npm/scripts/codegen.mjs b/npm/scripts/codegen.mjs new file mode 100644 index 0000000..e9ec9c2 --- /dev/null +++ b/npm/scripts/codegen.mjs @@ -0,0 +1,130 @@ +#!/usr/bin/env node + +/** + * Codegen script: reads the generated token list JSON files and produces + * TypeScript source files that embed the data with full type information. + */ + +import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const ROOT = join(__dirname, "..", ".."); +const SRC = join(__dirname, "..", "src"); +const CJS_DIR = join(__dirname, "..", "dist", "cjs"); + +mkdirSync(SRC, { recursive: true }); +mkdirSync(CJS_DIR, { recursive: true }); + +// Generate mainnet.ts + +const mainnetJson = readFileSync( + join(ROOT, "tokenlist-mainnet.json"), + "utf-8", +); + +let mainnetData; +try { + mainnetData = JSON.parse(mainnetJson); +} catch (err) { + console.error("codegen: tokenlist-mainnet.json is not valid JSON:", err.message); + process.exit(1); +} + +writeFileSync( + join(SRC, "mainnet.ts"), + [ + "// Auto-generated by codegen.mjs -- do not edit", + 'import type { MainnetTokenList } from "./types.js";', + "", + `export const mainnetTokenList = ${mainnetJson.trimEnd()} as const satisfies MainnetTokenList;`, + "", + ].join("\n"), +); + +// Generate testnet.ts + +const testnetJson = readFileSync( + join(ROOT, "tokenlist-testnet.json"), + "utf-8", +); + +try { + JSON.parse(testnetJson); +} catch (err) { + console.error("codegen: tokenlist-testnet.json is not valid JSON:", err.message); + process.exit(1); +} + +writeFileSync( + join(SRC, "testnet.ts"), + [ + "// Auto-generated by codegen.mjs -- do not edit", + 'import type { TestnetTokenList } from "./types.js";', + "", + `export const testnetTokenList = ${testnetJson.trimEnd()} as const satisfies TestnetTokenList;`, + "", + ].join("\n"), +); + +// Generate index.ts (barrel re-export) + +writeFileSync( + join(SRC, "index.ts"), + [ + "// Auto-generated by codegen.mjs -- do not edit", + 'export { mainnetTokenList } from "./mainnet.js";', + 'export { testnetTokenList } from "./testnet.js";', + "export type {", + " Address,", + " MonadMainnetChainId,", + " MonadTestnetChainId,", + " MonadChainId,", + " CrossChainId,", + " BridgeProtocol,", + " CrossChainAddressEntry,", + " CrossChainAddresses,", + " BridgeInfo,", + " TokenExtensions,", + " Token,", + " MainnetToken,", + " TestnetToken,", + " Version,", + " TokenList,", + " MainnetTokenList,", + " TestnetTokenList,", + '} from "./types.js";', + "", + ].join("\n"), +); + +// Generate dist/cjs/package.json (CommonJS marker) + +writeFileSync( + join(CJS_DIR, "package.json"), + `${JSON.stringify({ type: "commonjs" }, null, 2)}\n`, +); + +// Sync package.json and package-lock.json versions from the token list + +const NPM_DIR = join(__dirname, ".."); +const { major, minor, patch } = mainnetData.version; +const version = `${major}.${minor}.${patch}`; + +const pkgPath = join(NPM_DIR, "package.json"); +const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")); +pkg.version = version; +writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`); + +const lockPath = join(NPM_DIR, "package-lock.json"); +const lock = JSON.parse(readFileSync(lockPath, "utf-8")); +lock.version = version; +if (lock.packages?.[""]) { + lock.packages[""].version = version; +} +writeFileSync(lockPath, `${JSON.stringify(lock, null, 2)}\n`); + +console.log( + `codegen: wrote src/mainnet.ts, src/testnet.ts, src/index.ts, dist/cjs/package.json, package.json (v${version}), package-lock.json (v${version})`, +); diff --git a/npm/src/types.ts b/npm/src/types.ts new file mode 100644 index 0000000..de9cb08 --- /dev/null +++ b/npm/src/types.ts @@ -0,0 +1,131 @@ +/** + * TypeScript type definitions for the Monad token list. + * + * @module @monad-crypto/token-list + */ + +// --------------------------------------------------------------------------- +// Address type +// --------------------------------------------------------------------------- + +/** A hex-prefixed address. */ +export type Address = `0x${string}`; + +// --------------------------------------------------------------------------- +// Chain IDs +// --------------------------------------------------------------------------- + +/** Monad Mainnet chain ID. */ +export type MonadMainnetChainId = 143; + +/** Monad Testnet chain ID. */ +export type MonadTestnetChainId = 10143; + +/** Any Monad chain ID. */ +export type MonadChainId = MonadMainnetChainId | MonadTestnetChainId; + +/** Known cross-chain EVM chain IDs. */ +export type CrossChainId = + | "1" // Ethereum + | "10" // Optimism + | "56" // BSC + | "137" // Polygon + | "999" // HyperEVM + | "8453" // Base + | "9745" // Plasma + | "42161" // Arbitrum + | "43114"; // Avalanche + +// --------------------------------------------------------------------------- +// Bridge protocol +// --------------------------------------------------------------------------- + +/** Valid bridge protocol identifiers. */ +export type BridgeProtocol = + | "Chainlink CCIP" + | "Circle CCTP" + | "Hyperlane Warp Route" + | "LayerZero OFT" + | "Wormhole" + | "Wormhole NTT"; + +// --------------------------------------------------------------------------- +// Token extensions +// --------------------------------------------------------------------------- + +/** An address entry on a remote chain. */ +export interface CrossChainAddressEntry { + readonly address: Address; + readonly symbol?: string; + readonly decimals?: number; +} + +/** Cross-chain address mapping, keyed by chain ID string. */ +export type CrossChainAddresses = { + readonly [K in CrossChainId]?: CrossChainAddressEntry; +}; + +/** Bridge information for a bridged token. */ +export interface BridgeInfo { + readonly protocol: BridgeProtocol; + readonly bridgeAddress: Address; +} + +/** Optional metadata extensions on a token. */ +export interface TokenExtensions { + readonly coinGeckoId?: string; + readonly bridgeInfo?: BridgeInfo; + readonly crossChainAddresses?: CrossChainAddresses; +} + +// --------------------------------------------------------------------------- +// Token +// --------------------------------------------------------------------------- + +/** A single token entry in the token list. */ +export interface Token { + readonly chainId: ChainId; + readonly address: Address; + readonly name: string; + readonly symbol: string; + readonly decimals: number; + readonly logoURI: string; + readonly extensions?: TokenExtensions; +} + +/** A mainnet token (chainId is always 143). */ +export type MainnetToken = Token; + +/** A testnet token (chainId is always 10143). */ +export type TestnetToken = Token; + +// --------------------------------------------------------------------------- +// Version +// --------------------------------------------------------------------------- + +/** Semantic version object. */ +export interface Version { + readonly major: number; + readonly minor: number; + readonly patch: number; +} + +// --------------------------------------------------------------------------- +// Token list +// --------------------------------------------------------------------------- + +/** Full token list structure. */ +export interface TokenList { + readonly name: string; + readonly logoURI: string; + readonly keywords: readonly string[]; + readonly timestamp: string; + readonly tokens: readonly T[]; + readonly version: Version; +} + +/** The mainnet token list type. */ +export type MainnetTokenList = TokenList; + +/** The testnet token list type. */ +export type TestnetTokenList = TokenList; diff --git a/npm/tsconfig.cjs.json b/npm/tsconfig.cjs.json new file mode 100644 index 0000000..75a988f --- /dev/null +++ b/npm/tsconfig.cjs.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "CommonJS", + "moduleResolution": "node", + "outDir": "dist/cjs" + } +} diff --git a/npm/tsconfig.json b/npm/tsconfig.json new file mode 100644 index 0000000..b1b4e22 --- /dev/null +++ b/npm/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "Node16", + "moduleResolution": "Node16", + "declaration": true, + "declarationMap": false, + "sourceMap": false, + "outDir": "dist/esm", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true + }, + "include": ["src"] +}