From 4c94c79b19b5e27fe4913b184aa3d8a666a9a790 Mon Sep 17 00:00:00 2001 From: Gabriel Montes Date: Mon, 2 Dec 2024 13:47:46 -0500 Subject: [PATCH] Fix list, test, add tokens (#16) * Fix the list to match the on-chain data * Add tests * 1.2.1 * Add script to add tokens * Fix workflow --- .github/workflows/js-checks.yml | 28 +++- .husky/pre-commit | 1 + lint-staged.config.js | 6 +- package-lock.json | 219 +++++++++++++++++++++++++++++++- package.json | 10 +- scripts/add-token.js | 63 +++++++++ scripts/sync-version.js | 24 ++-- src/hemi.tokenlist.json | 24 ++-- src/logos/hdai.svg | 16 +++ test/hemi.tokenlist.test.js | 70 ++++++++++ 10 files changed, 421 insertions(+), 40 deletions(-) create mode 100644 scripts/add-token.js create mode 100644 src/logos/hdai.svg create mode 100644 test/hemi.tokenlist.test.js diff --git a/.github/workflows/js-checks.yml b/.github/workflows/js-checks.yml index 18db088..78d7576 100644 --- a/.github/workflows/js-checks.yml +++ b/.github/workflows/js-checks.yml @@ -8,8 +8,30 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }} cancel-in-progress: true +# The reusable workflow at bloq/actions/.github/workflows/js-checks.yml@v1 +# cannot be used as environment variables must be sent to the test command to +# overwrite the EVM RPC URL of Hemi mainnet. When this is not needed anymore, +# the custom workflow should be replaced by the reusable one. + jobs: js-checks: - uses: hemilabs/actions/.github/workflows/js-checks.yml@main - with: - node-versions: '["16", "18", "20", "22"]' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: bloq/actions/setup-node-env@v1 + with: + cache: npm + - run: npm run --if-present format:check + - run: npm run --if-present lint + - run: npm run --if-present deps:check + run-tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: bloq/actions/setup-node-env@v1 + with: + cache: npm + - run: npm run test + env: + EVM_RPC_URL_43111: ${{ secrets.EVM_RPC_URL_43111 }} + EVM_RPC_URL_743111: ${{ secrets.WEB3_RPC_743111 }} diff --git a/.husky/pre-commit b/.husky/pre-commit index af5adff..3a89582 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1 +1,2 @@ +npm run version lint-staged \ No newline at end of file diff --git a/lint-staged.config.js b/lint-staged.config.js index 58a75b8..b67622d 100644 --- a/lint-staged.config.js +++ b/lint-staged.config.js @@ -1,9 +1,7 @@ const formatFiles = "prettier --ignore-unknown --write"; const sortPackageJson = "better-sort-package-json"; -module.exports = { - "!(*.{js,json,md,svg,ts,tsx,yml}|package.json)": [formatFiles], - "*.{js,md,svg,ts,tsx,yml}": [formatFiles], - "src/*.json": [formatFiles], +export default { + "!package.json": [formatFiles], "package.json": [sortPackageJson, formatFiles], }; diff --git a/package-lock.json b/package-lock.json index 6185188..20e587e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,22 +1,30 @@ { "name": "@hemilabs/token-list", - "version": "1.2.0", + "version": "1.2.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@hemilabs/token-list", - "version": "1.2.0", + "version": "1.2.1", "license": "MIT", "devDependencies": { "@commitlint/cli": "19.5.0", "better-sort-package-json": "1.1.0", "commitlint-config-bloq": "1.1.0", + "hemi-viem": "^1.6.1", "husky": "9.1.6", "lint-staged": "15.2.10", - "prettier": "3.3.3" + "prettier": "3.3.3", + "viem": "^2.21.51" } }, + "node_modules/@adraffy/ens-normalize": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.11.0.tgz", + "integrity": "sha512-/3DDPKHqqIqxUULp8yP4zODUY1i+2xvVWsv8A79xGWdCAG+8sb0hRh0Rk2QyOJUnnbyPUAZYcpBuRe3nS2OIUg==", + "dev": true + }, "node_modules/@babel/code-frame": { "version": "7.26.2", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", @@ -267,6 +275,69 @@ "node": ">=v18" } }, + "node_modules/@noble/curves": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.6.0.tgz", + "integrity": "sha512-TlaHRXDehJuRNR9TfZDNQ45mMEd5dwUwmicsafcIX4SsNiqnCHKjE/1alYPd/lDRVhxdhUAlv8uEhMCI5zjIJQ==", + "dev": true, + "dependencies": { + "@noble/hashes": "1.5.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.5.0.tgz", + "integrity": "sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA==", + "dev": true, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/base": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.9.tgz", + "integrity": "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==", + "dev": true, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.5.0.tgz", + "integrity": "sha512-8EnFYkqEQdnkuGBVpCzKxyIwDCBLDVj3oiX0EKUFre/tOjL/Hqba1D6n/8RcmaQy4f95qQFrO2A8Sr6ybh4NRw==", + "dev": true, + "dependencies": { + "@noble/curves": "~1.6.0", + "@noble/hashes": "~1.5.0", + "@scure/base": "~1.1.7" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.4.0.tgz", + "integrity": "sha512-BEEm6p8IueV/ZTfQLp/0vhw4NPnT9oWf5+28nvmeUICjP99f4vr2d+qc7AVGDDtwRep6ifR43Yed9ERVmiITzw==", + "dev": true, + "dependencies": { + "@noble/hashes": "~1.5.0", + "@scure/base": "~1.1.8" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@types/conventional-commits-parser": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@types/conventional-commits-parser/-/conventional-commits-parser-5.0.0.tgz", @@ -285,6 +356,27 @@ "undici-types": "~6.19.8" } }, + "node_modules/abitype": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.0.6.tgz", + "integrity": "sha512-MMSqYh4+C/aVqI2RQaWqbvI4Kxo5cQV40WQ4QFtDnNzCkqChm8MuENhElmynZlO0qUy/ObkEUaXtKqYnx1Kp3A==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/wevm" + }, + "peerDependencies": { + "typescript": ">=5.0.4", + "zod": "^3 >=3.22.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, "node_modules/ajv": { "version": "8.17.1", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", @@ -1035,6 +1127,15 @@ "node": ">= 0.4" } }, + "node_modules/hemi-viem": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/hemi-viem/-/hemi-viem-1.6.1.tgz", + "integrity": "sha512-5gDb0ZWWNferl2O5nEJKNyq3QqV74EzHWB+dKOrRGpAY1zF01XWv7HcWlDxWF7/LRLIuqhemnBGW5SLnQQg/FQ==", + "dev": true, + "peerDependencies": { + "viem": "^2.x" + } + }, "node_modules/human-signals": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", @@ -1175,6 +1276,21 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, + "node_modules/isows": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.6.tgz", + "integrity": "sha512-lPHCayd40oW98/I0uvgaHKWCSvkzY27LjWLbtzOm64yQ+G3Q5npjjbdppU65iZXkK1Zt+kH9pfegli0AYfwYYw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "peerDependencies": { + "ws": "*" + } + }, "node_modules/jiti": { "version": "1.21.6", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz", @@ -1568,6 +1684,35 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/ox": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ox/-/ox-0.1.2.tgz", + "integrity": "sha512-ak/8K0Rtphg9vnRJlbOdaX9R7cmxD2MiSthjWGaQdMk3D7hrAlDoM+6Lxn7hN52Za3vrXfZ7enfke/5WjolDww==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "dependencies": { + "@adraffy/ens-normalize": "^1.10.1", + "@noble/curves": "^1.6.0", + "@noble/hashes": "^1.5.0", + "@scure/bip32": "^1.5.0", + "@scure/bip39": "^1.4.0", + "abitype": "^1.0.6", + "eventemitter3": "5.0.1" + }, + "peerDependencies": { + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/p-limit": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", @@ -1963,6 +2108,53 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/viem": { + "version": "2.21.51", + "resolved": "https://registry.npmjs.org/viem/-/viem-2.21.51.tgz", + "integrity": "sha512-IBZTFoo9cZvMBkFqaJq5G8Ori4IeEDe9AHE5CmOlvNw7ytkC3vdVrJ/APL+V3H4d/5i1FiV331UsckIqQLIM0w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "dependencies": { + "@noble/curves": "1.6.0", + "@noble/hashes": "1.5.0", + "@scure/bip32": "1.5.0", + "@scure/bip39": "1.4.0", + "abitype": "1.0.6", + "isows": "1.0.6", + "ox": "0.1.2", + "webauthn-p256": "0.0.10", + "ws": "8.18.0" + }, + "peerDependencies": { + "typescript": ">=5.0.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/webauthn-p256": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/webauthn-p256/-/webauthn-p256-0.0.10.tgz", + "integrity": "sha512-EeYD+gmIT80YkSIDb2iWq0lq2zbHo1CxHlQTeJ+KkCILWpVy3zASH3ByD4bopzfk0uCwXxLqKGLqp2W4O28VFA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "dependencies": { + "@noble/curves": "^1.4.0", + "@noble/hashes": "^1.4.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -1995,6 +2187,27 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index dbea87d..e082e57 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@hemilabs/token-list", - "version": "1.2.0", + "version": "1.2.1", "description": "List of ERC-20 tokens in the Hemi Network chains", "bugs": { "url": "https://github.com/hemilabs/token-list/issues" @@ -21,14 +21,18 @@ "scripts": { "format:check": "prettier --check .", "prepare": "husky", + "test": "node --test", "version": "node scripts/sync-version.js && git add ." }, "devDependencies": { "@commitlint/cli": "19.5.0", "better-sort-package-json": "1.1.0", "commitlint-config-bloq": "1.1.0", + "hemi-viem": "^1.6.1", "husky": "9.1.6", "lint-staged": "15.2.10", - "prettier": "3.3.3" - } + "prettier": "3.3.3", + "viem": "^2.21.51" + }, + "type": "module" } diff --git a/scripts/add-token.js b/scripts/add-token.js new file mode 100644 index 0000000..8255dde --- /dev/null +++ b/scripts/add-token.js @@ -0,0 +1,63 @@ +import { createPublicClient, http } from "viem"; +import { erc20Abi } from "viem"; +import { hemi, hemiSepolia } from "hemi-viem"; +import fs from "node:fs"; + +import tokenList from "../src/hemi.tokenlist.json" with { type: "json" }; + +try { + process.loadEnvFile(); +} catch (e) {} + +const [chainIdStr, address] = process.argv.slice(2); +const chainId = Number.parseInt(chainIdStr); + +if ( + tokenList.tokens.find( + (token) => token.address === address && token.chainId === chainId, + ) +) { + console.log("Token already present"); + process.exit(0); +} + +try { + const client = createPublicClient({ + chain: [hemi, hemiSepolia].find((chain) => chain.id == chainId), + transport: http(process.env[`EVM_RPC_URL_${chainId}`]), + }); + + const [decimals, symbol, name] = /** @type {[Number,String,string]} */ ( + await Promise.all( + ["decimals", "symbol", "name"].map((method) => + client.readContract({ + abi: erc20Abi, + address: /** @type {`0x${string}`} */ (address), + args: [], + functionName: /** @type {'decimals'|'symbol'|'name'} */ (method), + }), + ), + ) + ); + + 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, + chainId, + decimals, + logoURI, + name, + symbol, + }); + + fs.writeFileSync( + "src/hemi.tokenlist.json", + JSON.stringify(tokenList, null, 2), + ); + + console.log("Token added"); +} catch (err) { + console.error("Could not add token:", err.message); +} diff --git a/scripts/sync-version.js b/scripts/sync-version.js index 9e4dad5..3eae4d1 100644 --- a/scripts/sync-version.js +++ b/scripts/sync-version.js @@ -1,19 +1,13 @@ -// Syncs the version of the metadata file with the version set in the -// package.json file. -// -// This script is executed just before committing the changes -// done by the "npm version" command. +import fs from "node:fs"; -"use strict"; - -const fs = require("fs"); - -const packageJson = require("../package.json"); -const metadata = require("../src/hemi.tokenlist.json"); +import packageJson from "../package.json" with { type: "json" }; +import tokenList from "../src/hemi.tokenlist.json" with { type: "json" }; const [major, minor, patch] = packageJson.version.split("."); -metadata.version.major = parseInt(major); -metadata.version.minor = parseInt(minor); -metadata.version.patch = parseInt(patch); +tokenList.version.major = parseInt(major); +tokenList.version.minor = parseInt(minor); +tokenList.version.patch = parseInt(patch); + +tokenList.timestamp = new Date().toISOString(); -fs.writeFileSync("src/hemi.tokenlist.json", JSON.stringify(metadata, null, 2)); +fs.writeFileSync("src/hemi.tokenlist.json", JSON.stringify(tokenList, null, 2)); diff --git a/src/hemi.tokenlist.json b/src/hemi.tokenlist.json index 4176a7a..d364cda 100644 --- a/src/hemi.tokenlist.json +++ b/src/hemi.tokenlist.json @@ -1,19 +1,19 @@ { "name": "Hemi Token List", - "timestamp": "2024-05-07T22:03:02.686Z", + "timestamp": "2024-11-27T20:27:00.112Z", "version": { "major": 1, "minor": 2, - "patch": 0 + "patch": 1 }, "tokens": [ { "address": "0xec46E0EFB2EA8152da0327a5Eb3FF9a43956F13e", "chainId": 743111, "decimals": 18, - "logoURI": "https://raw.githubusercontent.com/hemilabs/token-list/master/src/logos/dai.svg", - "name": "Dai", - "symbol": "DAI", + "logoURI": "https://raw.githubusercontent.com/hemilabs/token-list/master/src/logos/hdai.svg", + "name": "Hemi Tunneled DAI", + "symbol": "hDAI", "extensions": { "birthBlock": 71589, "bridgeInfo": { @@ -47,7 +47,7 @@ } }, "logoURI": "https://raw.githubusercontent.com/hemilabs/token-list/master/src/logos/usdt.svg", - "name": "Tether", + "name": "USDT.e", "symbol": "USDT.e" }, { @@ -63,7 +63,7 @@ } }, "logoURI": "https://raw.githubusercontent.com/hemilabs/token-list/master/src/logos/usdc.svg", - "name": "USD Coin", + "name": "USDC.e", "symbol": "USDC.e" }, { @@ -79,7 +79,7 @@ } }, "logoURI": "https://raw.githubusercontent.com/hemilabs/token-list/master/src/logos/vsp.svg", - "name": "Vesper", + "name": "VesperToken", "symbol": "VSP" }, { @@ -95,7 +95,7 @@ } }, "logoURI": "https://raw.githubusercontent.com/hemilabs/token-list/master/src/logos/met.svg", - "name": "Metronome", + "name": "Metronome2", "symbol": "MET" }, { @@ -127,7 +127,7 @@ } }, "logoURI": "https://raw.githubusercontent.com/hemilabs/token-list/master/src/logos/dai.svg", - "name": "Dai", + "name": "Dai Stablecoin", "symbol": "DAI" }, { @@ -143,7 +143,7 @@ } }, "logoURI": "https://raw.githubusercontent.com/hemilabs/token-list/master/src/logos/wbtc.svg", - "name": "wBTC", + "name": "Wrapped BTC", "symbol": "WBTC" }, { @@ -175,7 +175,7 @@ } }, "logoURI": "https://raw.githubusercontent.com/hemilabs/token-list/master/src/logos/link.svg", - "name": "Chainlink", + "name": "ChainLink Token", "symbol": "LINK" }, { diff --git a/src/logos/hdai.svg b/src/logos/hdai.svg new file mode 100644 index 0000000..16bbae4 --- /dev/null +++ b/src/logos/hdai.svg @@ -0,0 +1,16 @@ + + + + diff --git a/test/hemi.tokenlist.test.js b/test/hemi.tokenlist.test.js new file mode 100644 index 0000000..e7fdeca --- /dev/null +++ b/test/hemi.tokenlist.test.js @@ -0,0 +1,70 @@ +import { createPublicClient, http } from "viem"; +import { describe, it } from "node:test"; +import { erc20Abi } from "viem"; +import { hemi, hemiSepolia } from "hemi-viem"; +import assert from "node:assert/strict"; +import fs from "node:fs"; + +import packageJson from "../package.json" with { type: "json" }; +import tokenList from "../src/hemi.tokenlist.json" with { type: "json" }; + +try { + process.loadEnvFile(); +} catch (e) {} + +const clients = Object.fromEntries( + [hemi, hemiSepolia].map((chain) => [ + chain.id, + createPublicClient({ + chain, + transport: http(process.env[`EVM_RPC_URL_${chain.id}`]), + }), + ]), +); + +describe("Version", function () { + it("should match the package version", function () { + const { major, minor, patch } = tokenList.version; + const versionString = `${major}.${minor}.${patch}`; + assert.equal(versionString, packageJson.version); + }); +}); + +describe("List of tokens", function () { + it("should not be any duplicates", function () { + assert.equal( + new Set( + tokenList.tokens.map(({ chainId, symbol }) => `${chainId}:${symbol}`), + ).size, + tokenList.tokens.length, + ); + }); + + tokenList.tokens.map(function (token) { + const { address, chainId, decimals, logoURI, name, symbol } = token; + + describe(`Token ${chainId}:${symbol}`, function () { + it("should be a valid ERC20", async function () { + const client = clients[chainId]; + const props = await Promise.all( + ["decimals", "symbol", "name"].map((method) => + client.readContract({ + abi: erc20Abi, + address: /** @type {`0x${string}`} */ (address), + args: [], + functionName: /** @type {'decimals'|'symbol'|'name'} */ (method), + }), + ), + ); + assert.deepEqual(props, [decimals, symbol, name]); + }); + + it("image URL and file should be valid", function () { + const repoUrl = "https://raw.githubusercontent.com/hemilabs/token-list"; + const filename = symbol.toLowerCase().replace(".e", ""); + assert.equal(logoURI, `${repoUrl}/master/src/logos/${filename}.svg`); + fs.accessSync(`src/logos/${filename}.svg`); + }); + }); + }); +});