diff --git a/.husky/pre-commit b/.husky/pre-commit index 3a89582..7ca736d 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,2 +1,4 @@ -npm run version -lint-staged \ No newline at end of file +#! /bin/sh + +[ -n "$(node scripts/sync-version.js)" ] && git add src/hemi.tokenlist.json +lint-staged diff --git a/package-lock.json b/package-lock.json index 40a0cdf..750e3f0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@hemilabs/token-list", - "version": "1.6.0", + "version": "1.7.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@hemilabs/token-list", - "version": "1.6.0", + "version": "1.7.0", "license": "MIT", "devDependencies": { "@commitlint/cli": "19.5.0", @@ -21,6 +21,9 @@ "lint-staged": "15.2.10", "prettier": "3.3.3", "viem": "^2.21.51" + }, + "engines": { + "node": ">=20" } }, "node_modules/@adraffy/ens-normalize": { diff --git a/package.json b/package.json index 08af94c..349c129 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@hemilabs/token-list", - "version": "1.6.0", + "version": "1.7.0", "description": "List of ERC-20 tokens in the Hemi Network chains", "bugs": { "url": "https://github.com/hemilabs/token-list/issues" @@ -24,8 +24,7 @@ "prepare": "husky", "schema:validate": "node scripts/validate-list.js", "pretest": "npm run schema:validate", - "test": "node --test", - "version": "node scripts/sync-version.js && git add ." + "test": "node --test" }, "devDependencies": { "@commitlint/cli": "19.5.0", @@ -41,5 +40,8 @@ "prettier": "3.3.3", "viem": "^2.21.51" }, + "engines": { + "node": ">=20" + }, "type": "module" } diff --git a/scripts/add-token.js b/scripts/add-token.js index b64684d..fefe30c 100644 --- a/scripts/add-token.js +++ b/scripts/add-token.js @@ -3,36 +3,34 @@ import { erc20Abi, getAddress as toChecksum, http, - isAddress, isAddressEqual, } from "viem"; import { hemi, hemiSepolia } from "hemi-viem"; import fs from "node:fs"; -// eslint fails to parse "with { type: "json" }" -// See https://github.com/eslint/eslint/discussions/15305 -const tokenList = JSON.parse(fs.readFileSync("./src/hemi.tokenlist.json")); +const tokenList = JSON.parse( + fs.readFileSync("./src/hemi.tokenlist.json", "utf-8"), +); async function addToken() { - const [chainIdStr, address] = process.argv.slice(2); + const [chainIdStr, addressGiven] = process.argv.slice(2); const chainId = Number.parseInt(chainIdStr); + const address = toChecksum(addressGiven); - if (!isAddress(address)) { - throw new Error("Invalid address"); - } - - if ( - tokenList.tokens.find( - (token) => - isAddressEqual(token.address, address) && token.chainId === chainId, - ) - ) { + const found = tokenList.tokens.find( + (t) => isAddressEqual(t.address, address) && t.chainId === chainId, + ); + if (found) { console.log("Token already present"); return; } try { const chain = [hemi, hemiSepolia].find((c) => c.id === chainId); + if (!chain) { + throw new Error("Unsupported chain"); + } + const client = createPublicClient({ chain, transport: http(), @@ -51,18 +49,57 @@ async function addToken() { ) ); + // The remote token address should be read from the "REMOTE_TOKEN" function + // as "l1Token" and "remoteToken" are deprecated as per the comments in + // OptimismMintableERC20. + // And if there is an error getting the remote address, just ignore it. + // See: https://github.com/ethereum-optimism/optimism/blob/ca5855220fb2264aa32c882d056dd98da21ac47a/packages/contracts-bedrock/src/universal/OptimismMintableERC20.sol#L23 + const remoteTokenAddress = await client + .readContract({ + abi: [ + { + inputs: [], + name: "REMOTE_TOKEN", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + ], + address, + args: [], + functionName: "REMOTE_TOKEN", + }) + .catch(() => undefined); + const filename = symbol.toLowerCase().replace(".e", ""); const repoUrl = "https://raw.githubusercontent.com/hemilabs/token-list"; const logoURI = `${repoUrl}/master/src/logos/${filename}.svg`; tokenList.tokens.push({ - address: toChecksum(address), + address, chainId, decimals, + extensions: remoteTokenAddress && { + bridgeInfo: { + [chain.sourceId]: { + tokenAddress: remoteTokenAddress, + }, + }, + }, logoURI, name, symbol, }); + tokenList.tokens + .sort((a, b) => a.address.toLowerCase() - b.address.toLowerCase()) + .sort((a, b) => a.chainId - b.chainId); + fs.writeFileSync( "src/hemi.tokenlist.json", JSON.stringify(tokenList, null, 2), diff --git a/scripts/sync-version.js b/scripts/sync-version.js index e46a0e2..0a10bef 100644 --- a/scripts/sync-version.js +++ b/scripts/sync-version.js @@ -1,15 +1,28 @@ import fs from "node:fs"; -// eslint fails to parse "with { type: "json" }" -// See https://github.com/eslint/eslint/discussions/15305 -const packageJson = JSON.parse(fs.readFileSync("./package.json")); -const tokenList = JSON.parse(fs.readFileSync("./src/hemi.tokenlist.json")); +const packageJson = JSON.parse(fs.readFileSync("./package.json", "utf-8")); +const tokenList = JSON.parse( + fs.readFileSync("./src/hemi.tokenlist.json", "utf-8"), +); -const [major, minor, patch] = packageJson.version.split("."); -tokenList.version.major = parseInt(major); -tokenList.version.minor = parseInt(minor); -tokenList.version.patch = parseInt(patch); +const currentVersion = [ + tokenList.version.major, + tokenList.version.minor, + tokenList.version.patch, +].join("."); -tokenList.timestamp = new Date().toISOString(); +if (currentVersion !== packageJson.version) { + const [major, minor, patch] = packageJson.version.split("."); + tokenList.version.major = parseInt(major); + tokenList.version.minor = parseInt(minor); + tokenList.version.patch = parseInt(patch); -fs.writeFileSync("src/hemi.tokenlist.json", JSON.stringify(tokenList, null, 2)); + tokenList.timestamp = new Date().toISOString(); + + fs.writeFileSync( + "src/hemi.tokenlist.json", + JSON.stringify(tokenList, null, 2), + ); + + console.log("Version updated"); +} diff --git a/scripts/validate-list.js b/scripts/validate-list.js index 4cf8be9..e243bb6 100644 --- a/scripts/validate-list.js +++ b/scripts/validate-list.js @@ -3,9 +3,9 @@ import addFormats from "ajv-formats"; import fs from "node:fs"; import { exit } from "node:process"; -// eslint fails to parse "with { type: "json" }" -// See https://github.com/eslint/eslint/discussions/15305 -const tokenList = JSON.parse(fs.readFileSync("./src/hemi.tokenlist.json")); +const tokenList = JSON.parse( + fs.readFileSync("./src/hemi.tokenlist.json", "utf-8"), +); const schemaUrl = "https://raw.githubusercontent.com/Uniswap/token-lists/main/src/tokenlist.schema.json"; diff --git a/src/hemi.tokenlist.json b/src/hemi.tokenlist.json index 30d98c7..71aeeba 100644 --- a/src/hemi.tokenlist.json +++ b/src/hemi.tokenlist.json @@ -1,9 +1,9 @@ { "name": "Hemi Token List", - "timestamp": "2025-01-27T18:08:23.402Z", + "timestamp": "2025-01-27T20:27:42.430Z", "version": { "major": 1, - "minor": 6, + "minor": 7, "patch": 0 }, "tokens": [ @@ -42,7 +42,7 @@ "birthBlock": 575834, "bridgeInfo": { "11155111": { - "tokenAddress": "0xaa8e23fb1079ea71e0a56f48a2aa51851d8433d0" + "tokenAddress": "0xaA8E23Fb1079EA71e0a56F48a2aA51851D8433D0" } } }, @@ -287,6 +287,7 @@ "chainId": 43111, "decimals": 18, "extensions": { + "birthBlock": 363002, "bridgeInfo": { "1": { "tokenAddress": "0x936Ab482d6bd111910a42849D3A51Ff80BB0A711" @@ -302,6 +303,7 @@ "chainId": 43111, "decimals": 18, "extensions": { + "birthBlock": 782510, "bridgeInfo": { "1": { "tokenAddress": "0x18084fbA666a33d37592fA2633fD49a74DD93a88" @@ -317,11 +319,8 @@ "chainId": 43111, "decimals": 6, "extensions": { - "bridgeInfo": { - "1": { - "tokenAddress": "0xdAC17F958D2ee523a2206206994597C13D831ec7" - } - } + "birthBlock": 666946, + "tunnel": "stargate" }, "logoURI": "https://raw.githubusercontent.com/hemilabs/token-list/master/src/logos/usdt.svg", "name": "USDT", @@ -332,11 +331,8 @@ "chainId": 43111, "decimals": 6, "extensions": { - "bridgeInfo": { - "1": { - "tokenAddress": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" - } - } + "birthBlock": 623077, + "tunnel": "stargate" }, "logoURI": "https://raw.githubusercontent.com/hemilabs/token-list/master/src/logos/usdc.svg", "name": "Bridged USDC (Stargate)", diff --git a/test/hemi.tokenlist.test.js b/test/hemi.tokenlist.test.js index 03006b8..301eb37 100644 --- a/test/hemi.tokenlist.test.js +++ b/test/hemi.tokenlist.test.js @@ -5,10 +5,10 @@ import { hemi, hemiSepolia } from "hemi-viem"; import assert from "node:assert/strict"; import fs from "node:fs"; -// eslint fails to parse "with { type: "json" }" -// See https://github.com/eslint/eslint/discussions/15305 -const packageJson = JSON.parse(fs.readFileSync("./package.json")); -const tokenList = JSON.parse(fs.readFileSync("./src/hemi.tokenlist.json")); +const packageJson = JSON.parse(fs.readFileSync("./package.json", "utf-8")); +const tokenList = JSON.parse( + fs.readFileSync("./src/hemi.tokenlist.json", "utf-8"), +); const clients = Object.fromEntries( [hemi, hemiSepolia].map((chain) => [ @@ -49,7 +49,7 @@ describe("List of tokens", function () { symbol, } = token; - describe(`Token ${chainId}:${symbol}`, function () { + describe(`Token ${chainId}:${address} (${symbol})`, function () { it("should have all its addresses in the checksum format", function () { // viem's isAddress checks for checksum format assert.ok(isAddress(address)); @@ -81,6 +81,48 @@ describe("List of tokens", function () { assert.equal(logoURI, `${repoUrl}/master/src/logos/${filename}.svg`); fs.accessSync(`src/logos/${filename}.svg`); }); + + it("should have a valid birth block number", function () { + const birthBlock = extensions?.birthBlock; + if (!birthBlock) { + this.skip(); + return; + } + + assert.ok(Number.isInteger(birthBlock)); + }); + + it("should have the correct remote token address", async function () { + const client = clients[chainId]; + const tokenAddress = + extensions?.bridgeInfo?.[client.chain.sourceId].tokenAddress; + if (!tokenAddress) { + this.skip(); + return; + } + + const remoteTokenAddress = await client.readContract({ + abi: [ + { + inputs: [], + name: "REMOTE_TOKEN", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + ], + address, + args: [], + functionName: "REMOTE_TOKEN", + }); + assert.equal(remoteTokenAddress, tokenAddress); + }); }); }); });