Skip to content

Commit

Permalink
chore: use js script
Browse files Browse the repository at this point in the history
  • Loading branch information
SGiaccobasso committed Sep 23, 2024
1 parent f926ea1 commit c595dc9
Show file tree
Hide file tree
Showing 3 changed files with 313 additions and 9 deletions.
9 changes: 1 addition & 8 deletions .github/workflows/validate-tokens.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,15 @@ jobs:

- name: Install dependencies
run: |
npm install -g typescript
npm install ethers@5.7.2
npm install viem
npm install axios
npm install fs
npm install @types/node
- name: Compile TypeScript
run: tsc validate-token-configs.ts
- name: Extract and validate new tokens
run: |
DIFF=$(git diff origin/${{ github.base_ref }} -- registry/mainnet/interchain/squid.tokenlist.json | grep '^+' | grep -v '+++')
NEW_TOKENS=$(echo "$DIFF" | sed -n '/"0x/,/]/{/]/q;p}' | jq -s 'reduce .[] as $item ({}; . + ($item | fromjson))')
echo "$NEW_TOKENS" > new_tokens.json
node validate-token-configs.ts
node scripts/validate-token-configs.js
- name: Check validation results
run: |
Expand Down
311 changes: 311 additions & 0 deletions scripts/validate-token-configs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,311 @@
const fs = require("fs");
const ethers = require("ethers");
const axios = require("axios");

/*
* =============================
* Section: Constants
* =============================
*/
const tokenManagerTypes = [
"nativeInterchainToken",
"mintBurnFrom",
"lockUnlock",
"lockUnlockFee",
"mintBurn",
"gateway",
];
const ITSAddress = "0xB5FB4BE02232B1bBA4dC8f81dc24C26980dE9e3C";
const TOKEN_FILE_ROUTE = "./new_tokens.json";
const COINGECKO_API_KEY = "CG-3VGxh1K3Qk7jAvpt4DJA3LvB";
const COINGECKO_URL = "https://api.coingecko.com/api/v3/coins/";
const CHAIN_CONFIGS_URL =
"https://axelar-mainnet.s3.us-east-2.amazonaws.com/configs/mainnet-config-1.x.json";
const ERC20ABI = [
{
constant: true,
inputs: [],
name: "name",
outputs: [{ name: "", type: "string" }],
type: "function",
},
{
constant: true,
inputs: [],
name: "symbol",
outputs: [{ name: "", type: "string" }],
type: "function",
},
{
constant: true,
inputs: [],
name: "decimals",
outputs: [{ name: "", type: "uint8" }],
type: "function",
},
];
const ITSABI = [
{
inputs: [
{ internalType: "address", name: "sender", type: "address" },
{ internalType: "bytes32", name: "salt", type: "bytes32" },
],
name: "interchainTokenId",
outputs: [{ internalType: "bytes32", name: "tokenId", type: "bytes32" }],
stateMutability: "pure",
type: "function",
},
];
const tokenManagerABI = [
{
inputs: [],
name: "tokenAddress",
outputs: [{ internalType: "address", name: "", type: "address" }],
stateMutability: "view",
type: "function",
},
{
inputs: [],
name: "implementationType",
outputs: [
{
internalType: "uint256",
name: "",
type: "uint256",
},
],
stateMutability: "view",
type: "function",
},
];

/*
* =============================
* Section: Helper Functions
* =============================
*/
async function getAxelarChains() {
const { data } = await axios.get(CHAIN_CONFIGS_URL);
return data.chains;
}

async function getRpcUrl(axelarChainId) {
try {
const chains = await getAxelarChains();
return chains[axelarChainId].config.rpc[0];
} catch (error) {
throw new Error(
`Error fetching chain configs for chain '${axelarChainId}':\n ${error.message}`
);
}
}

function exitWithError(errorMessage) {
console.error(errorMessage);
fs.writeFileSync("validation_errors.txt", errorMessage);
process.exit(1);
}

/*
* =============================
* Section: Validation Functions
* =============================
*/
async function validateTokenInfo(tokenInfo) {
for (const [tokenId, info] of Object.entries(tokenInfo)) {
console.log(`\nValidating token: ${tokenId}...`);
try {
await validateTokenId(tokenId, info);
await validateCoinGeckoId(tokenId, info);
await validateChains(info);
await validateOriginChain(info);
await validateDeployerAndSalt(tokenId, info);
} catch (error) {
exitWithError(error.message);
}
}
}

async function validateTokenId(tokenId, info) {
if (tokenId !== info.tokenId) {
throw new Error(`Mismatch in tokenId: ${tokenId} vs ${info.tokenId}`);
}
}

async function validateCoinGeckoId(tokenId, { coinGeckoId, prettySymbol }) {
if (!coinGeckoId)
throw new Error(`CoinGecko ID is missing for token ${tokenId}`);

try {
const response = await axios.get(COINGECKO_URL + coinGeckoId, {
headers: {
"x-cg-demo-api-key": COINGECKO_API_KEY,
},
});

if (response.status !== 200)
throw new Error(
`CoinGecko API returned status ${response.status} for token ${tokenId}`
);

const coinData = response.data;
if (coinData.symbol.toLowerCase() !== prettySymbol.toLowerCase()) {
throw new Error(
`CoinGecko symbol (${coinData.symbol}) does not match prettySymbol (${prettySymbol}) for token ${tokenId}`
);
}
} catch (error) {
if (axios.isAxiosError(error) && error.response?.status === 404) {
throw new Error(
`CoinGecko ID ${coinGeckoId} not found for token ${tokenId}`
);
}
throw new Error(
`Error fetching data from CoinGecko for token ${tokenId}: ${error.message}`
);
}
}

async function validateChains(info) {
for (const chain of info.chains) {
console.log(`Validating for ${chain.axelarChainId}...`);

const rpcUrl = await getRpcUrl(chain.axelarChainId);
const provider = new ethers.JsonRpcProvider(rpcUrl);

await validateTokenAddress(chain, provider);
await validateTokenDetails(chain, info, provider);
await validateTokenManager(chain, provider);
}
}

async function validateTokenAddress(chain, provider) {
const tokenCode = await provider.getCode(chain.tokenAddress);
if (tokenCode === "0x")
throw new Error(
`Token address ${chain.tokenAddress} does not exist on chain ${chain.axelarChainId}`
);
}

async function validateTokenDetails(
chain,
{ originAxelarChainId, prettySymbol, decimals },
provider
) {
const tokenContract = new ethers.Contract(
chain.tokenAddress,
ERC20ABI,
provider
);
const tokenName = await tokenContract.name();
const tokenSymbol = await tokenContract.symbol();
const tokenDecimals = await tokenContract.decimals();
const decimalsFromContract = Number(tokenDecimals);

if (tokenName.toLowerCase() !== chain.name.toLowerCase())
throw new Error(
`Token name mismatch on chain ${chain.axelarChainId}: expected ${chain.name}, got ${tokenName}`
);
if (tokenSymbol.toLowerCase() !== chain.symbol.toLowerCase())
throw new Error(
`Token symbol mismatch on chain ${chain.axelarChainId}: expected ${chain.symbol}, got ${tokenSymbol}`
);
if (originAxelarChainId === chain.axelarChainId) {
if (tokenSymbol.toLowerCase() !== prettySymbol.toLowerCase())
throw new Error(
`Token symbol mismatch on chain ${chain.axelarChainId}: expected ${prettySymbol}, got ${tokenSymbol}`
);
if (decimalsFromContract !== decimals)
throw new Error(
`Token decimals mismatch on chain ${chain.axelarChainId}: expected ${decimals}, got ${decimalsFromContract}`
);
}
}

async function validateTokenManager(chain, provider) {
const managerCode = await provider.getCode(chain.tokenManager);
if (managerCode === "0x") {
throw new Error(
`Token manager ${chain.tokenManager} does not exist on chain ${chain.axelarChainId}`
);
}

const tokenManagerContract = new ethers.Contract(
chain.tokenManager,
tokenManagerABI,
provider
);
const managedTokenAddress = await tokenManagerContract.tokenAddress();
if (managedTokenAddress.toLowerCase() !== chain.tokenAddress.toLowerCase())
throw new Error(
`Token manager ${chain.tokenManager} on chain ${chain.axelarChainId} does not manage the specified token address ${chain.tokenAddress}`
);

const implementationType = await tokenManagerContract.implementationType();
if (
Number(implementationType) !==
tokenManagerTypes.indexOf(chain.tokenManagerType)
)
throw new Error(
`Token manager on chain ${
chain.axelarChainId
} has incorrect implementation type: expected '${
tokenManagerTypes[Number(implementationType)] || "Unknown"
}', got '${chain.tokenManagerType}'`
);
}

async function validateOriginChain({ chains, originAxelarChainId }) {
const originChain = chains.find(
(chain) => chain.axelarChainId === originAxelarChainId
);
if (!originChain)
throw new Error(
`Origin chain ${originAxelarChainId} not found in chains list`
);
}

async function validateDeployerAndSalt(
tokenId,
{ originAxelarChainId, deployer, deploySalt }
) {
const rpcUrl = await getRpcUrl(originAxelarChainId);
const provider = new ethers.JsonRpcProvider(rpcUrl);
const itsContract = new ethers.Contract(ITSAddress, ITSABI, provider);

if (!ethers.isAddress(deployer))
throw new Error(`Invalid deployer address: ${deployer}`);

const calculatedTokenId = await itsContract.interchainTokenId(
deployer,
deploySalt
);

if (calculatedTokenId.toLowerCase() !== tokenId.toLowerCase())
throw new Error(
`Mismatch in interchainTokenId for token ${tokenId}:\n` +
`Deployer or Deploy Salt could be incorrect\n` +
` Expected: ${tokenId}\n` +
` Calculated: ${calculatedTokenId}\n` +
` Deployer: ${deployer}\n` +
` Deploy Salt: ${deploySalt}`
);
}

/*
* =============================
* Section: Main
* =============================
*/
async function main() {
try {
// Read new token configurations from file
const newTokens = JSON.parse(fs.readFileSync(TOKEN_FILE_ROUTE, "utf8"));
await validateTokenInfo(newTokens);
} catch (error) {
exitWithError(error.message);
}
console.log("Validation successful!");
}

main();
2 changes: 1 addition & 1 deletion scripts/validate-token-configs.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import fs from "fs";
import * as fs from "fs";
import { ethers } from "ethers";
import axios from "axios";
/*
Expand Down

0 comments on commit c595dc9

Please sign in to comment.