Skip to content

Commit

Permalink
feat: update token ownership request script (#271)
Browse files Browse the repository at this point in the history
Co-authored-by: Talal Ashraf <[email protected]>
  • Loading branch information
blockchainguyy and talalashraf authored Aug 2, 2024
1 parent 0ba1321 commit 79c9c5c
Show file tree
Hide file tree
Showing 5 changed files with 255 additions and 626 deletions.
9 changes: 8 additions & 1 deletion evm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,14 @@ Verify TokenManagerProxy contract for ITS. `--tokenId` must be specified and `--
node evm/verify-contract.js -e [env] -n [chain] -c TokenManagerProxy --dir /path/to/interchain-token-service --tokenId [tokenId]
```

Verify AxelarAmplifierGateway contract. `--address` can be optionally specified (otherwise will default to the value from config).
## Verify Token Ownership requests

Download the pending requests [spreadsheet](https://docs.google.com/spreadsheets/d/1zKH1DINTiz83iXbbZRNRurxxZTaU0r5JS4A1c8b9-9A/edit?resourcekey=&gid=1705825087#gid=1705825087) into a csv format.

`node evm/check-ownership-requests.js -f sheet_path.csv`

## Verify AxelarAmplifierGateway contract.
`--address` can be optionally specified (otherwise will default to the value from config).

1. First clone the `axelar-gmp-sdk-solidity` repo: `git clone [email protected]:axelarnetwork/axelar-gmp-sdk-solidity.git`
2. Checkout the branch or commit from where the contract was deployed: `git checkout <branch_name>`
Expand Down
285 changes: 134 additions & 151 deletions evm/check-ownership-request.js
Original file line number Diff line number Diff line change
@@ -1,181 +1,179 @@
'use strict';

require('dotenv').config();

const axios = require('axios');
const { Command, Option } = require('commander');
const csv = require('csv-parser');
const { writeFile, createReadStream } = require('fs');
const { ethers } = require('hardhat');
const { Contract, getDefaultProvider } = ethers;

const { readJSON } = require(`${__dirname}/../axelar-chains-config`);
const keys = readJSON(`${__dirname}/../keys.json`);
const {
loadConfig,
validateParameters,
printError,
getContractJSON,
printInfo,
loadConfig,
copyObject,
printWarn,
printObj,
isValidAddress,
isStringArray,
printError,
getDeploymentTx,
printInfo,
} = require('./utils');

const interchainTokenFactoryABI = getContractJSON('InterchainTokenFactory').abi;
const interchainTokenABI = getContractJSON('InterchainToken').abi;
const interchainTokenServiceABI = getContractJSON('InterchainTokenService').abi;
const erc20ABI = getContractJSON('IERC20Named').abi;

async function processCommand(config, options) {
try {
const { deployer, address, its, rpc, api } = options;
let { source, destination } = options;
const { file, startingIndex } = options;

validateParameters({ isValidAddress: { address }, isNonEmptyString: { source, destination } });
if (startingIndex) {
validateParameters({ isValidNumber: { startingIndex } });
}

const sourceChain = config.chains[source.toLowerCase()];
const data = await loadCsvFile(file, startingIndex);
const finalData = copyObject(data);
let totalRowsRemoved = 0;

if (!sourceChain) {
throw new Error(`Chain ${source} is not defined in the info file`);
}
for (let i = 0; i < data.length; ++i) {
const row = data[i];
const tokenAddress = row[0];
const destinationChainsRaw = row[2].split(',');
const destinationChains = destinationChainsRaw.map((chain) => chain.trim().toLowerCase()).filter((chain) => chain);
const dustTx = row[4];

try {
destination = JSON.parse(destination);
} catch (error) {
throw new Error(`Unable to parse destination chains: ${error}`);
}
validateParameters({ isValidAddress: { tokenAddress } });

const invalidDestinationChains = await verifyChains(config, tokenAddress, destinationChains);
const validDestinationChains = destinationChains.filter((chain) => !invalidDestinationChains.includes(chain));

if (!isStringArray(destination)) {
throw new Error(`Invalid destination chains type, expected string`);
if (validDestinationChains.length > 0) {
finalData[i - totalRowsRemoved][2] =
validDestinationChains.length === 1 ? `${validDestinationChains[0]}` : `"${validDestinationChains.join(', ')}"`;
} else {
finalData.splice(i - totalRowsRemoved, 1);
++totalRowsRemoved;
continue;
}

const invalidDestinations = destination.filter((chain) => !config.chains[chain.toLowerCase()]);
const chain = validDestinationChains[0];
const apiUrl = config.chains[chain].explorer.api;
const apiKey = keys.chains[chain].api;
let deploymentTx, isValidDustx;

try {
deploymentTx = await getDeploymentTx(apiUrl, apiKey, tokenAddress);
isValidDustx = await verifyDustTx(deploymentTx, dustTx, config.chains);
} catch {}

if (invalidDestinations.length > 0) {
throw new Error(`Chains ${invalidDestinations.join(', ')} are not defined in the info file`);
if (!isValidDustx) {
finalData.splice(i - totalRowsRemoved, 1);
++totalRowsRemoved;
}
}

const provider = getDefaultProvider(rpc || sourceChain.rpc);
let itsAddress;
await createCsvFile('pending_ownership_requests.csv', finalData);
}

if (its) {
if (isValidAddress(its)) {
itsAddress = its;
} else {
throw new Error(`Invalid ITS address: ${its}`);
}
} else {
itsAddress = sourceChain.contracts.InterchainTokenService?.address;
}
async function verifyDustTx(deploymentTx, dustTx, chains) {
const senderDeploymentTx = await getSenderDeploymentTx(deploymentTx);
const senderDustTx = await getSenderDustTx(dustTx, chains);

if (deployer === 'gateway') {
const gatewayTokens = await fetchGatewayTokens(address, sourceChain.name.toLowerCase(), destination, provider, api);
printInfo(`Gateway Tokens on destination chains`);
printObj(gatewayTokens);
return;
}
return senderDeploymentTx === senderDustTx;
}

if (await isTokenCanonical(address, itsAddress, provider)) {
printInfo(`Provided address ${address} is a canonical token`);
return;
}
async function getSenderDeploymentTx(deploymentTx) {
try {
const response = await axios.get('https://api.axelarscan.io/gmp/searchGMP', {
params: { txHash: deploymentTx },
headers: { 'Content-Type': 'application/json' },
});

const interchainToken = new Contract(address, interchainTokenABI, provider);
const tokenId = await isNativeInterchainToken(interchainToken);
const interchainTokens = await fetchNativeInterchainTokens(address, config, tokenId, destination, its);
printInfo(`Native Interchain Tokens on destination chains`);
printObj(interchainTokens);
const data = response.data.data[0];
return data.call.receipt.from.toLowerCase();
} catch (error) {
printError('Error', error.message);
throw new Error('Error fetching sender from deploymentTx: ', error);
}
}

async function isTokenCanonical(address, itsAddress, provider) {
let isCanonicalToken;
const its = new Contract(itsAddress, interchainTokenServiceABI, provider);
const itsFactory = new Contract(await its.interchainTokenFactory(), interchainTokenFactoryABI, provider);
const canonicalTokenId = await itsFactory.canonicalInterchainTokenId(address);
async function getSenderDustTx(dustTx, chains) {
if (!dustTx.startsWith('https') && !dustTx.startsWith('0x')) {
throw new Error('Invalid dustTx format. It must start with "https" or "0x".');
}

try {
const validCanonicalAddress = await its.validTokenAddress(canonicalTokenId);
isCanonicalToken = address.toLowerCase() === validCanonicalAddress.toLowerCase();
} catch {}
const txHash = dustTx.startsWith('https') ? dustTx.split('/').pop() : dustTx;

for (const chainName in chains) {
const chain = chains[chainName];

return isCanonicalToken;
if (chain.id.toLowerCase().includes('axelar')) continue;

try {
const provider = getDefaultProvider(chain.rpc);
const tx = await provider.getTransaction(txHash);
if (tx) return tx.from.toLowerCase();
} catch {}
}

throw new Error(`Transaction ${dustTx} not found on any chain`);
}

async function fetchNativeInterchainTokens(address, config, tokenId, destination, itsAddress) {
const interchainTokens = [];
async function verifyChains(config, tokenAddress, destinationChains) {
const invalidDestinationChains = [];

try {
for (const chain of destination) {
for (const chain of destinationChains) {
try {
const chainConfig = config.chains[chain];
itsAddress = itsAddress || chainConfig.contracts.InterchainTokenService?.address;
const provider = getDefaultProvider(chainConfig.rpc);
const its = new Contract(itsAddress, interchainTokenServiceABI, provider);

if ((await its.validTokenAddress(tokenId)).toLowerCase() === address.toLowerCase()) {
interchainTokens.push({ [chain]: address });
} else {
printWarn(`No native Interchain token found for tokenId ${tokenId} on chain ${chain}`);
}
}
const token = new Contract(tokenAddress, interchainTokenABI, provider);
const tokenId = await token.interchainTokenId();

if (destination.length !== interchainTokens.length) {
printError('Native Interchain tokens not found on all destination chains');
validateParameters({ isValidTokenId: { tokenId } });
} catch {
// printWarn(`No Interchain token found for address ${tokenAddress} on chain ${chain}`);
invalidDestinationChains.push(chain);
}

return interchainTokens;
} catch (error) {
throw new Error('Unable to fetch native interchain tokens on destination chains');
}
}

async function isNativeInterchainToken(token) {
try {
return await token.interchainTokenId();
} catch {
throw new Error(`The token at address ${await token.address} is not a Interchain Token`);
}
return invalidDestinationChains;
}

async function isGatewayToken(apiUrl, address) {
async function loadCsvFile(filePath, startingIndex = 0) {
const results = [];

try {
const { data: sourceData } = await axios.get(apiUrl);
const stream = createReadStream(filePath).pipe(csv());

if (!(sourceData.confirmed && !sourceData.is_external)) {
throw new Error();
for await (const row of stream) {
results.push(Object.values(row));
}
} catch {
throw new Error(`The token at address ${address} is not deployed through Axelar Gateway.`);

return results.slice(startingIndex);
} catch (error) {
throw new Error(`Error loading CSV file: ${error}`);
}
}

async function fetchGatewayTokens(address, source, destination, provider, api) {
const gatewayTokens = [];
const apiUrl = api || 'https://lcd-axelar.imperator.co/axelar/evm/v1beta1/token_info/';
async function createCsvFile(filePath, data) {
if (!data.length) {
printWarn('Input data is empty. No CSV file created.');
return;
}

await isGatewayToken(`${apiUrl}${source}?address=${address}`, address);
const columnNames = ['Token Address', 'Chains to claim token ownership on', 'Telegram Contact details'];
const selectedColumns = [0, 2, 3]; // Indexes of required columns

try {
const token = new Contract(address, erc20ABI, provider);
const symbol = await token.symbol();

for (const chain of destination) {
const { data: chainData } = await axios.get(`${apiUrl}${chain}?symbol=${symbol}`);
const filteredData = data.map((row) => {
return selectedColumns.map((index) => row[index]);
});

if (!(chainData.confirmed && !chainData.is_external && chainData.address)) {
printWarn(`No Gateway token found for token symbol ${symbol} on chain ${chain}`);
} else {
gatewayTokens.push({ [chain]: chainData.address });
}
}
const csvContent = [columnNames, ...filteredData].map((row) => row.join(',')).join('\n');

if (destination.length !== gatewayTokens.length) {
printError('Gateway tokens not found on all destination chains');
writeFile(filePath, csvContent, { encoding: 'utf8' }, (error) => {
if (error) {
printError('Error writing CSV file:', error);
} else {
printInfo('Created CSV file at', filePath);
}

return gatewayTokens;
} catch (error) {
throw new Error('Unable to fetch gateway tokens on destination chains');
}
});
}

async function main(options) {
Expand All @@ -188,36 +186,21 @@ async function main(options) {
if (require.main === module) {
const program = new Command();

program.name('check-ownership-request').description('Script to check token ownership claim request');

program.addOption(
new Option('--deployer <deployer>', 'deployed through which axelar product')
.choices(['gateway', 'its'])
.makeOptionMandatory(true)
.env('DEPLOYER'),
);
program.addOption(
new Option('-s, --source <sourceChain>', 'source chain on which provided contract address is deployed')
.makeOptionMandatory(true)
.env('SOURCE'),
);
program.addOption(
new Option('-d, --destination <destinationChains>', 'destination chains on which other tokens are deployed')
.makeOptionMandatory(true)
.env('DESTINATION'),
);
program.addOption(
new Option('-a, --address <token address>', 'deployed token address on source chain').makeOptionMandatory(true).env('ADDRESS'),
);
program.addOption(
new Option('-r, --rpc <rpc>', 'The rpc url for creating a provider on source chain to fetch token information').env('RPC'),
);
program.addOption(new Option('-i, --its <its>', 'Interchain token service override address'));
program.addOption(new Option('--api <apiUrl>', 'api url to check token deployed through gateway and the token details'));

program.action((options) => {
main(options);
});
program
.name('check-ownership-requests')
.description('Script to check token ownership claim requests')
.addOption(
new Option('-f, --file <file>', 'The csv file path containing details about pending token ownership requests')
.makeOptionMandatory(true)
.env('FILE'),
)
.addOption(
new Option(
'-s, --startingIndex <startingIndex>',
'The starting index from which data will be read. if not provided then whole file will be read',
),
)
.action(main);

program.parse();
}
Loading

0 comments on commit 79c9c5c

Please sign in to comment.