From 996329eb0a9ba7c99d772fd53b1551dcebeb0fa6 Mon Sep 17 00:00:00 2001 From: Milap Sheth Date: Tue, 10 Mar 2026 04:06:59 +1100 Subject: [PATCH 01/10] chore(evm): simplify ITS flow limit cmd --- evm/its.js | 111 +++++++++++++++++++++++++---------------------------- 1 file changed, 53 insertions(+), 58 deletions(-) diff --git a/evm/its.js b/evm/its.js index df047f118..9ecf9a3cf 100644 --- a/evm/its.js +++ b/evm/its.js @@ -3,7 +3,7 @@ const { ethers } = require('hardhat'); const { getDefaultProvider, - utils: { hexZeroPad, toUtf8Bytes, keccak256, parseUnits }, + utils: { hexZeroPad, toUtf8Bytes, keccak256, parseUnits, formatUnits }, Contract, } = ethers; const { Command, Option, Argument } = require('commander'); @@ -95,6 +95,17 @@ async function handleTx(tx, chain, contract, action, firstEvent, secondEvent) { } } +async function tokenManagerAndMetadata(interchainTokenService, tokenId, wallet) { + const tokenManagerAddress = await interchainTokenService.deployedTokenManager(tokenId); + const tokenManager = new Contract(tokenManagerAddress, ITokenManager.abi, wallet); + + const tokenAddress = await interchainTokenService.registeredTokenAddress(tokenId); + const token = new Contract(tokenAddress, getContractJSON('ERC20').abi, wallet); + const [symbol, decimals] = await Promise.all([token.symbol(), token.decimals()]); + + return { tokenManager, tokenManagerAddress, tokenAddress, symbol, decimals }; +} + async function getTrustedChains(chains, interchainTokenService, version) { const chainIds = Object.values(chains) .filter((chain) => chain.contracts.InterchainTokenService !== undefined) @@ -260,37 +271,36 @@ async function processCommand(_axelar, chain, chains, action, options) { validateTokenIds(interchainTokenService, [tokenId]); - const tokenManagerAddress = await interchainTokenService.deployedTokenManager(tokenId); - const tokenManager = new Contract(tokenManagerAddress, ITokenManager.abi, wallet); - + const { tokenManager, symbol, decimals } = await tokenManagerAndMetadata(interchainTokenService, tokenId, wallet); const flowLimit = await tokenManager.flowLimit(); - printInfo(`Flow limit for tokenId ${tokenId}`, flowLimit); + + printInfo(`Flow limit for tokenId ${tokenId}`, `${formatUnits(flowLimit, decimals)} ${symbol}`); break; } case 'flow-out-amount': { const [tokenId] = args; - validateTokenIds(interchainTokenService, [tokenId]); - const tokenManagerAddress = await interchainTokenService.deployedTokenManager(tokenId); - const tokenManager = new Contract(tokenManagerAddress, ITokenManager.abi, wallet); + validateTokenIds(interchainTokenService, [tokenId]); + const { tokenManager, symbol, decimals } = await tokenManagerAndMetadata(interchainTokenService, tokenId, wallet); const flowOutAmount = await tokenManager.flowOutAmount(); - printInfo(`Flow out amount for tokenId ${tokenId}`, flowOutAmount); + + printInfo(`Flow out amount for tokenId ${tokenId}`, `${formatUnits(flowOutAmount, decimals)} ${symbol}`); break; } case 'flow-in-amount': { const [tokenId] = args; - validateTokenIds(interchainTokenService, [tokenId]); - const tokenManagerAddress = await interchainTokenService.deployedTokenManager(tokenId); - const tokenManager = new Contract(tokenManagerAddress, ITokenManager.abi, wallet); + validateTokenIds(interchainTokenService, [tokenId]); + const { tokenManager, symbol, decimals } = await tokenManagerAndMetadata(interchainTokenService, tokenId, wallet); const flowInAmount = await tokenManager.flowInAmount(); - printInfo(`Flow in amount for tokenId ${tokenId}`, flowInAmount); + + printInfo(`Flow in amount for tokenId ${tokenId}`, `${formatUnits(flowInAmount, decimals)} ${symbol}`); break; } @@ -344,29 +354,22 @@ async function processCommand(_axelar, chain, chains, action, options) { }); const tokenIdBytes32 = hexZeroPad(tokenId.startsWith('0x') ? tokenId : '0x' + tokenId, 32); - - const tokenManager = new Contract( - await interchainTokenService.deployedTokenManager(tokenIdBytes32), - getContractJSON('ITokenManager').abi, - wallet, - ); - const token = new Contract( - await interchainTokenService.registeredTokenAddress(tokenIdBytes32), - getContractJSON('InterchainToken').abi, - wallet, - ); - - await printTokenInfo(await interchainTokenService.registeredTokenAddress(tokenIdBytes32), provider); + const { tokenManager, tokenAddress, symbol, decimals } = await tokenManagerAndMetadata(interchainTokenService, tokenIdBytes32, wallet); + const token = new Contract(tokenAddress, getContractJSON('InterchainToken').abi, wallet); const implementationType = (await tokenManager.implementationType()).toNumber(); - const decimals = await token.decimals(); const amountInUnits = parseUnits(amount, decimals); const balance = await token.balanceOf(wallet.address); if (balance.lt(amountInUnits)) { - throw new Error(`Insufficient balance for transfer. Balance: ${balance}, amount: ${amountInUnits}`); + throw new Error( + `Insufficient balance. Balance: ${formatUnits(balance, decimals)} ${symbol}, required: ${amount} ${symbol}`, + ); } + printInfo(`Token address`, tokenAddress); + printInfo(`Transfer amount`, `${amount} ${symbol}`); + if (implementationType !== tokenManagerTypes.MINT_BURN && implementationType !== tokenManagerTypes.NATIVE_INTERCHAIN_TOKEN) { printInfo('Approving ITS for a transfer for token with token manager type', implementationType); await token.approve(interchainTokenService.address, amountInUnits, gasOptions).then((tx) => tx.wait()); @@ -414,9 +417,14 @@ async function processCommand(_axelar, chain, chains, action, options) { const [tokenId, flowLimit] = args; validateTokenIds(interchainTokenService, [tokenId]); - validateParameters({ isValidNumber: { flowLimit } }); + validateParameters({ isValidDecimal: { flowLimit } }); + + const { tokenAddress, symbol, decimals } = await tokenManagerAndMetadata(interchainTokenService, tokenId, wallet); + await printTokenInfo(tokenAddress, provider); + const flowLimitInUnits = parseUnits(flowLimit, decimals); + printInfo(`Setting flow limit for tokenId ${tokenId}`, `${flowLimit} ${symbol}`); - const tx = await interchainTokenService.setFlowLimits([tokenId], [flowLimit], gasOptions); + const tx = await interchainTokenService.setFlowLimits([tokenId], [flowLimitInUnits], gasOptions); await handleTx(tx, chain, interchainTokenService, action); break; } @@ -483,9 +491,9 @@ async function processCommand(_axelar, chain, chains, action, options) { validateParameters({ isNonEmptyString: { itsChain } }); if (await isTrustedChain(itsChain, interchainTokenService, itsVersion)) { - printInfo(`${itsChain} is a trusted chain`); + printInfo(`${itsChain}`, 'Trusted'); } else { - printInfo(`${itsChain} is not a trusted chain`); + printWarn(`${itsChain}`, 'Not trusted'); } break; @@ -764,20 +772,13 @@ async function processCommand(_axelar, chain, chains, action, options) { validateParameters({ isValidTokenId: { tokenId }, isValidAddress: { to }, isValidNumber: { amount } }); const tokenIdBytes32 = hexZeroPad(tokenId.startsWith('0x') ? tokenId : '0x' + tokenId, 32); - - // Get token manager address - const tokenManagerAddress = await interchainTokenService.deployedTokenManager(tokenIdBytes32); + const { tokenManager, tokenManagerAddress, tokenAddress, symbol, decimals } = await tokenManagerAndMetadata(interchainTokenService, tokenIdBytes32, wallet); printInfo(`TokenManager address for tokenId: ${tokenId}`, tokenManagerAddress); - - // Get token address - const tokenAddress = await interchainTokenService.registeredTokenAddress(tokenIdBytes32); printInfo(`Token address for tokenId: ${tokenId}`, tokenAddress); - const tokenManager = new Contract(tokenManagerAddress, ITokenManager.abi, wallet); - const amountInUnits = ethers.BigNumber.from(amount.toString()); - if (prompt(`Proceed with minting ${amount} to ${to}?`, yes)) { + if (prompt(`Proceed with minting ${formatUnits(amountInUnits, decimals)} ${symbol} to ${to}?`, yes)) { return; } @@ -793,22 +794,19 @@ async function processCommand(_axelar, chain, chains, action, options) { validateParameters({ isValidTokenId: { tokenId }, isValidAddress: { spender }, isValidNumber: { amount } }); const tokenIdBytes32 = hexZeroPad(tokenId.startsWith('0x') ? tokenId : '0x' + tokenId, 32); - - // Get token address - const tokenAddress = await interchainTokenService.registeredTokenAddress(tokenIdBytes32); + const { tokenAddress, symbol, decimals } = await tokenManagerAndMetadata(interchainTokenService, tokenIdBytes32, wallet); printInfo(`Token address for tokenId: ${tokenId}`, tokenAddress); - // Create token contract instance const token = new Contract(tokenAddress, getContractJSON('InterchainToken').abi, wallet); - const amountInUnits = ethers.BigNumber.from(amount.toString()); - printInfo(`Approving ${spender} to spend ${amount} of token ${tokenId}`); + const formattedAmount = `${formatUnits(amountInUnits, decimals)} ${symbol}`; - if (prompt(`Proceed with approving ${spender} to spend ${amount}?`, yes)) { + printInfo(`Approving ${spender} to spend`, formattedAmount); + + if (prompt(`Proceed with approving ${spender} to spend ${formattedAmount}?`, yes)) { return; } - // Execute approval const tx = await token.approve(spender, amountInUnits, gasOptions); await handleTx(tx, chain, token, action, 'Approval'); @@ -1038,7 +1036,7 @@ if (require.main === module) { .command('set-flow-limit') .description('Set flow limit for a token') .argument('', 'Token ID') - .argument('', 'Flow limit') + .argument('', 'Flow limit in token units (e.g. 100.5)') .action((tokenId, flowLimit, options, cmd) => { return main(cmd.name(), [tokenId, flowLimit], options); }); @@ -1083,32 +1081,29 @@ if (require.main === module) { return main(cmd.name(), [itsChain], options); }); - const setTrustedChainsCommand = program + program .command('set-trusted-chains') .description('Set trusted chains') .argument('', 'Chains to trust') .action((chains, options, cmd) => { return main(cmd.name(), chains, options); }); - addGovernanceOptions(setTrustedChainsCommand); - const removeTrustedChainsCommand = program + program .command('remove-trusted-chains') .description('Remove trusted chains') .argument('', 'Chains to not trust') .action((chains, options, cmd) => { return main(cmd.name(), chains, options); }); - addGovernanceOptions(removeTrustedChainsCommand); - const setPauseStatusCommand = program + program .command('set-pause-status') .description('Set pause status') .argument(new Argument('', 'Pause status (true/false)').choices(['true', 'false'])) .action((pauseStatus, options, cmd) => { return main(cmd.name(), [pauseStatus], options); }); - addGovernanceOptions(setPauseStatusCommand); program .command('execute') @@ -1128,14 +1123,13 @@ if (require.main === module) { return main(cmd.name(), [], options); }); - const migrateInterchainTokenCommand = program + program .command('migrate-interchain-token') .description('Migrate interchain token') .argument('', 'Token ID') .action((tokenId, options, cmd) => { return main(cmd.name(), [tokenId], options); }); - addGovernanceOptions(migrateInterchainTokenCommand); program .command('mint-token') @@ -1181,6 +1175,7 @@ if (require.main === module) { }); addOptionsToCommands(program, addEvmOptions, { address: true, salt: true }); + addOptionsToCommands(program, addGovernanceOptions); program.parseAsync().then(() => process.exit(0)); } From f95739dd00854b77274980e0cdf2eae2cf87edb0 Mon Sep 17 00:00:00 2001 From: Milap Sheth Date: Tue, 10 Mar 2026 04:17:53 +1100 Subject: [PATCH 02/10] prettier --- evm/its.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/evm/its.js b/evm/its.js index 9ecf9a3cf..760c28335 100644 --- a/evm/its.js +++ b/evm/its.js @@ -354,7 +354,11 @@ async function processCommand(_axelar, chain, chains, action, options) { }); const tokenIdBytes32 = hexZeroPad(tokenId.startsWith('0x') ? tokenId : '0x' + tokenId, 32); - const { tokenManager, tokenAddress, symbol, decimals } = await tokenManagerAndMetadata(interchainTokenService, tokenIdBytes32, wallet); + const { tokenManager, tokenAddress, symbol, decimals } = await tokenManagerAndMetadata( + interchainTokenService, + tokenIdBytes32, + wallet, + ); const token = new Contract(tokenAddress, getContractJSON('InterchainToken').abi, wallet); const implementationType = (await tokenManager.implementationType()).toNumber(); @@ -772,7 +776,11 @@ async function processCommand(_axelar, chain, chains, action, options) { validateParameters({ isValidTokenId: { tokenId }, isValidAddress: { to }, isValidNumber: { amount } }); const tokenIdBytes32 = hexZeroPad(tokenId.startsWith('0x') ? tokenId : '0x' + tokenId, 32); - const { tokenManager, tokenManagerAddress, tokenAddress, symbol, decimals } = await tokenManagerAndMetadata(interchainTokenService, tokenIdBytes32, wallet); + const { tokenManager, tokenManagerAddress, tokenAddress, symbol, decimals } = await tokenManagerAndMetadata( + interchainTokenService, + tokenIdBytes32, + wallet, + ); printInfo(`TokenManager address for tokenId: ${tokenId}`, tokenManagerAddress); printInfo(`Token address for tokenId: ${tokenId}`, tokenAddress); From 05b1918f49807c64fa93b4380905a8bc9991bf78 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 9 Mar 2026 17:25:32 +0000 Subject: [PATCH 03/10] Fix: Restrict governance options to commands that handle them Previously, governance options were added to all commands via addOptionsToCommands(program, addGovernanceOptions), which allowed users to pass --governance to commands that don't handle it (like set-flow-limit, freeze-tokens, mint-token, approve, interchain-transfer). This caused the main function to enter the governance code path, but processCommand would still execute transactions directly on-chain, leading to silent direct execution instead of creating a governance proposal. Now governance options are only added to the 4 commands that properly handle them: - set-trusted-chains - remove-trusted-chains - set-pause-status - migrate-interchain-token Applied via @cursor push command --- evm/its.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/evm/its.js b/evm/its.js index 760c28335..2cb8e4b7d 100644 --- a/evm/its.js +++ b/evm/its.js @@ -1089,7 +1089,7 @@ if (require.main === module) { return main(cmd.name(), [itsChain], options); }); - program + const setTrustedChainsCmd = program .command('set-trusted-chains') .description('Set trusted chains') .argument('', 'Chains to trust') @@ -1097,7 +1097,7 @@ if (require.main === module) { return main(cmd.name(), chains, options); }); - program + const removeTrustedChainsCmd = program .command('remove-trusted-chains') .description('Remove trusted chains') .argument('', 'Chains to not trust') @@ -1105,7 +1105,7 @@ if (require.main === module) { return main(cmd.name(), chains, options); }); - program + const setPauseStatusCmd = program .command('set-pause-status') .description('Set pause status') .argument(new Argument('', 'Pause status (true/false)').choices(['true', 'false'])) @@ -1131,7 +1131,7 @@ if (require.main === module) { return main(cmd.name(), [], options); }); - program + const migrateInterchainTokenCmd = program .command('migrate-interchain-token') .description('Migrate interchain token') .argument('', 'Token ID') @@ -1183,7 +1183,12 @@ if (require.main === module) { }); addOptionsToCommands(program, addEvmOptions, { address: true, salt: true }); - addOptionsToCommands(program, addGovernanceOptions); + + // Add governance options only to commands that handle them + addGovernanceOptions(setTrustedChainsCmd); + addGovernanceOptions(removeTrustedChainsCmd); + addGovernanceOptions(setPauseStatusCmd); + addGovernanceOptions(migrateInterchainTokenCmd); program.parseAsync().then(() => process.exit(0)); } From 4a82a009100aff63ed4d9c827b8f5b8c689be351 Mon Sep 17 00:00:00 2001 From: Milap Sheth Date: Mon, 16 Mar 2026 18:11:01 +1100 Subject: [PATCH 04/10] refactor stellar its --- stellar/its.js | 124 +++++++++++++++++++++++++++++++++++++++++++---- stellar/utils.ts | 5 +- 2 files changed, 118 insertions(+), 11 deletions(-) diff --git a/stellar/its.js b/stellar/its.js index 865d7e47d..f4b8c5fd8 100644 --- a/stellar/its.js +++ b/stellar/its.js @@ -39,6 +39,49 @@ const { estimateITSFee, } = require('../common/utils'); +async function tokenMetadataFromId(wallet, chain, itsContract, tokenId, options) { + const tokenAddressResult = await broadcast( + itsContract.call('registered_token_address', hexToScVal(tokenId)), + wallet, + chain, + 'Get registered token address', + options, + ); + const tokenAddress = serializeValue(tokenAddressResult.value()); + const tokenContract = new Contract(tokenAddress); + + const symbolResult = await broadcast(tokenContract.call('symbol'), wallet, chain, 'Get token symbol', options); + const decimalsResult = await broadcast(tokenContract.call('decimals'), wallet, chain, 'Get token decimals', options); + + const symbol = serializeValue(symbolResult.value()); + const decimals = decimalsResult.value(); + + return { tokenAddress, symbol, decimals }; +} + +function formatTokenAmount(amount, decimals) { + const str = amount.toString(); + + if (decimals === 0) {return str;} + + const padded = str.padStart(decimals + 1, '0'); + const intPart = padded.slice(0, padded.length - decimals); + const fracPart = padded.slice(padded.length - decimals).replace(/0+$/, ''); + + return fracPart ? `${intPart}.${fracPart}` : intPart; +} + +function parseTokenAmount(amount, decimals) { + const [intPart = '0', fracPart = ''] = amount.split('.'); + + if (fracPart.length > decimals) { + throw new Error(`Too many decimal places for amount ${amount} (max ${decimals})`); + } + + const raw = intPart + fracPart.padEnd(decimals, '0'); + return raw.replace(/^0+/, '') || '0'; +} + async function manageTrustedChains(action, wallet, config, chain, contract, args, options) { const trustedChains = parseTrustedChains(config.chains, args); @@ -215,6 +258,9 @@ async function interchainTransfer(wallet, config, chain, contract, args, options isValidStellarAddress: { gasTokenAddress }, }); + const { symbol, decimals } = await tokenMetadataFromId(wallet, chain, contract, tokenId, options); + const amountInUnits = parseTokenAmount(amount, decimals); + const { gasFeeValue } = await estimateITSFee( chain, destinationChain, @@ -226,6 +272,7 @@ async function interchainTransfer(wallet, config, chain, contract, args, options const itsDestinationAddress = encodeITSDestination(config.chains, destinationChain, destinationAddress); printInfo('Human-readable destination address', destinationAddress); + printInfo('Transfer amount', `${amount} ${symbol}`); printInfo('Gas fee value', gasFeeValue); const operation = contract.call( @@ -234,7 +281,7 @@ async function interchainTransfer(wallet, config, chain, contract, args, options hexToScVal(tokenId), nativeToScVal(destinationChain, { type: 'string' }), hexToScVal(itsDestinationAddress), - nativeToScVal(amount, { type: 'i128' }), + nativeToScVal(amountInUnits, { type: 'i128' }), data, tokenToScVal(gasTokenAddress, gasFeeValue), ); @@ -265,25 +312,70 @@ async function flowLimit(wallet, _config, chain, contract, args, options) { const operation = contract.call('flow_limit', hexToScVal(tokenId)); const response = await broadcast(operation, wallet, chain, 'Get Flow Limit', options); - const flowLimit = response.value(); + const flowLimitValue = response.value(); + + if (flowLimitValue === undefined || flowLimitValue === null) { + printInfo('Flow Limit', 'No limit set'); + return; + } - printInfo('Flow Limit', flowLimit || 'No limit set'); + const { symbol, decimals } = await tokenMetadataFromId(wallet, chain, contract, tokenId, options); + printInfo('Flow Limit', `${formatTokenAmount(flowLimitValue, decimals)} ${symbol}`); } async function setFlowLimit(wallet, _config, chain, contract, args, options) { const [tokenId, flowLimit] = args; validateParameters({ - isNonEmptyString: { tokenId }, - isValidNumber: { flowLimit }, + isNonEmptyString: { tokenId, flowLimit }, }); - const flowLimitScVal = nativeToScVal(flowLimit, { type: 'i128' }); + const { symbol, decimals } = await tokenMetadataFromId(wallet, chain, contract, tokenId, options); + const flowLimitInUnits = parseTokenAmount(flowLimit, decimals); + + printInfo(`Setting flow limit for tokenId ${tokenId}`, `${flowLimit} ${symbol}`); + const flowLimitScVal = nativeToScVal(flowLimitInUnits, { type: 'i128' }); const operation = contract.call('set_flow_limit', hexToScVal(tokenId), flowLimitScVal); await broadcast(operation, wallet, chain, 'Set Flow Limit', options); - printInfo('Successfully set flow limit', flowLimit); + printInfo('Successfully set flow limit', `${flowLimit} ${symbol}`); +} + +async function freezeToken(wallet, _config, chain, contract, args, options) { + const [tokenId] = args; + + validateParameters({ + isNonEmptyString: { tokenId }, + }); + + const { symbol } = await tokenMetadataFromId(wallet, chain, contract, tokenId, options); + + printInfo(`Freezing token ${symbol} (tokenId: ${tokenId})`); + + const flowLimitScVal = nativeToScVal('0', { type: 'i128' }); + const operation = contract.call('set_flow_limit', hexToScVal(tokenId), flowLimitScVal); + + await broadcast(operation, wallet, chain, 'Freeze Token', options); + printInfo('Successfully froze token', symbol); +} + +async function unfreezeToken(wallet, _config, chain, contract, args, options) { + const [tokenId] = args; + + validateParameters({ + isNonEmptyString: { tokenId }, + }); + + const { symbol } = await tokenMetadataFromId(wallet, chain, contract, tokenId, options); + + printInfo(`Unfreezing token ${symbol} (tokenId: ${tokenId})`); + + const flowLimitScVal = nativeToScVal(null, { type: 'void' }); + const operation = contract.call('set_flow_limit', hexToScVal(tokenId), flowLimitScVal); + + await broadcast(operation, wallet, chain, 'Unfreeze Token', options); + printInfo('Successfully unfroze token', symbol); } async function removeFlowLimit(wallet, _config, chain, contract, args, options) { @@ -606,7 +698,7 @@ if (require.main === module) { program .command('interchain-transfer ') - .description('interchain transfer') + .description('interchain transfer (amount in token units, e.g. 100.5)') .addOption(new Option('--data ', 'data').default('')) .addOption(new Option('--gas-token-address ', 'gas token address (default: XLM)')) .addOption(new Option('--gas-amount ', 'gas amount').default('auto')) @@ -630,7 +722,7 @@ if (require.main === module) { program .command('set-flow-limit ') - .description('Set the flow limit for a token') + .description('Set the flow limit for a token (flowLimit in token units, e.g. 100.5)') .action((tokenId, flowLimit, options) => { return mainProcessor(setFlowLimit, [tokenId, flowLimit], options); }); @@ -642,6 +734,20 @@ if (require.main === module) { return mainProcessor(removeFlowLimit, [tokenId], options); }); + program + .command('freeze-token ') + .description('Freeze a token by setting flow limit to 0') + .action((tokenId, options) => { + return mainProcessor(freezeToken, [tokenId], options); + }); + + program + .command('unfreeze-token ') + .description('Unfreeze a token by removing the flow limit') + .action((tokenId, options) => { + return mainProcessor(unfreezeToken, [tokenId], options); + }); + program .command('interchain-token-address ') .description('Get the interchain token address with the given token id') diff --git a/stellar/utils.ts b/stellar/utils.ts index 302d38fbd..37124d2fa 100644 --- a/stellar/utils.ts +++ b/stellar/utils.ts @@ -10,6 +10,7 @@ import { authorizeInvocation, nativeToScVal, rpc, + scValToNative, xdr, } from '@stellar/stellar-sdk'; import { Command, Option } from 'commander'; @@ -98,7 +99,7 @@ async function getAuthValidUntilLedger(chain) { const addBaseOptions = (command: Command, options: Options = {}) => { addEnvOption(command); command.addOption(new Option('-y, --yes', 'skip deployment prompt confirmation').env('YES')); - command.addOption(new Option('--chain-name ', 'chain name for stellar in amplifier').default('stellar').env('CHAIN')); + command.addOption(new Option('-n, --chain-name ', 'chain name for stellar in amplifier').default('stellar').env('CHAIN')); command.addOption(new Option('-v, --verbose', 'verbose output').default(false)); command.addOption(new Option('--estimate-cost', 'estimate on-chain resources').default(false)); @@ -223,7 +224,7 @@ async function sendTransaction(tx, server, action, options: Options = {}) { } function parseSimulatedResponse(response) { - return response.result.retval._value; + return scValToNative(response.result.retval); } function isReadOnly(response: rpc.Api.SimulateTransactionResponse, action?: string): boolean { From 73b95573648c3cae678703d0a9000276bb8432bd Mon Sep 17 00:00:00 2001 From: Milap Sheth Date: Mon, 16 Mar 2026 18:12:54 +1100 Subject: [PATCH 05/10] update workflow --- .github/workflows/test-stellar.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/test-stellar.yaml b/.github/workflows/test-stellar.yaml index 3092cfeac..6ea34a940 100644 --- a/.github/workflows/test-stellar.yaml +++ b/.github/workflows/test-stellar.yaml @@ -257,6 +257,12 @@ jobs: - name: ITS Flow Limit Remove run: ts-node stellar/its.js remove-flow-limit ${{ steps.env_vars.outputs.tokenId }} + - name: ITS Freeze Token + run: ts-node stellar/its.js freeze-token ${{ steps.env_vars.outputs.tokenId }} + + - name: ITS Unfreeze Token + run: ts-node stellar/its.js unfreeze-token ${{ steps.env_vars.outputs.tokenId }} + ###### Command: Operators ###### - name: Operator From 6c7565312af7867733a41bd4101ed62c020ea406 Mon Sep 17 00:00:00 2001 From: Milap Sheth Date: Mon, 16 Mar 2026 18:20:29 +1100 Subject: [PATCH 06/10] lint --- stellar/its.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/stellar/its.js b/stellar/its.js index f4b8c5fd8..455b33789 100644 --- a/stellar/its.js +++ b/stellar/its.js @@ -62,7 +62,9 @@ async function tokenMetadataFromId(wallet, chain, itsContract, tokenId, options) function formatTokenAmount(amount, decimals) { const str = amount.toString(); - if (decimals === 0) {return str;} + if (decimals === 0) { + return str; + } const padded = str.padStart(decimals + 1, '0'); const intPart = padded.slice(0, padded.length - decimals); From b246e88fb7d02008f7d7f7091c3a6ef281a18023 Mon Sep 17 00:00:00 2001 From: Milap Sheth Date: Mon, 16 Mar 2026 20:11:52 +1100 Subject: [PATCH 07/10] stellar token deployment --- stellar/its.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/stellar/its.js b/stellar/its.js index 455b33789..e1b948d22 100644 --- a/stellar/its.js +++ b/stellar/its.js @@ -152,21 +152,25 @@ async function deployInterchainToken(wallet, _config, chain, contract, args, opt const minter = caller; const [symbol, name, decimal, salt, initialSupply] = args; const saltBytes32 = saltToBytes32(salt); + const initialSupplyInUnits = parseTokenAmount(initialSupply, Number(decimal)); validateParameters({ isNonEmptyString: { symbol, name }, - isValidNumber: { decimal, initialSupply }, + isValidNumber: { decimal }, + isNonEmptyString: { initialSupply }, }); printInfo('Salt', salt); printInfo('Deployment salt (bytes32)', saltBytes32); + printInfo('Initial supply', `${initialSupply} ${symbol}`); + printInfo('Initial supply (units)', initialSupplyInUnits); const operation = contract.call( 'deploy_interchain_token', caller, hexToScVal(saltBytes32), tokenMetadataToScVal(decimal, name, symbol), - nativeToScVal(initialSupply, { type: 'i128' }), + nativeToScVal(initialSupplyInUnits, { type: 'i128' }), minter, ); From 78e4b5199f39ef6de1d444bfa0d501967ce7b304 Mon Sep 17 00:00:00 2001 From: Milap Sheth Date: Tue, 31 Mar 2026 23:49:04 +1100 Subject: [PATCH 08/10] fix(stellar): validate deploy token name and symbol --- stellar/its.js | 3 +-- sui/its.js | 46 ++++++++++++++++++++++++---------------- sui/utils/token-utils.js | 27 +++++++++++++++++++++++ 3 files changed, 56 insertions(+), 20 deletions(-) diff --git a/stellar/its.js b/stellar/its.js index e1b948d22..78d718ec1 100644 --- a/stellar/its.js +++ b/stellar/its.js @@ -155,9 +155,8 @@ async function deployInterchainToken(wallet, _config, chain, contract, args, opt const initialSupplyInUnits = parseTokenAmount(initialSupply, Number(decimal)); validateParameters({ - isNonEmptyString: { symbol, name }, + isNonEmptyString: { symbol, name, initialSupply }, isValidNumber: { decimal }, - isNonEmptyString: { initialSupply }, }); printInfo('Salt', salt); diff --git a/sui/its.js b/sui/its.js index 6490c1761..4ebc39cd5 100644 --- a/sui/its.js +++ b/sui/its.js @@ -38,7 +38,7 @@ const chalk = require('chalk'); const { utils: { arrayify, parseUnits }, } = require('hardhat').ethers; -const { checkIfCoinExists, senderHasSufficientBalance } = require('./utils/token-utils'); +const { checkIfCoinExists, tokenMetadata, senderHasSufficientBalance } = require('./utils/token-utils'); async function setFlowLimits(keypair, client, config, contracts, args, options) { let [tokenIds, flowLimits] = args; @@ -56,6 +56,8 @@ async function setFlowLimits(keypair, client, config, contracts, args, options) throw new Error(' and have to have the same length.'); } + const prettyFlowLimits = []; + for (let i = 0; i < tokenIds.length; i++) { const coinTypeTxBuilder = new TxBuilder(client); let tokenId = await coinTypeTxBuilder.moveCall({ @@ -70,6 +72,7 @@ async function setFlowLimits(keypair, client, config, contracts, args, options) const resp = await coinTypeTxBuilder.devInspect(keypair.toSuiAddress()); const coinType = bcs.String.parse(new Uint8Array(resp.results[1].returnValues[0][0])); + const { symbol, decimals } = await tokenMetadata(client, tokenIds[i], coinType); tokenId = await txBuilder.moveCall({ target: `${itsConfig.address}::token_id::from_address`, @@ -92,6 +95,10 @@ async function setFlowLimits(keypair, client, config, contracts, args, options) }); } + const prettyFlowLimit = flowLimits[i] === 'none' ? 'none' : `${getFormattedAmount(flowLimits[i], decimals)} ${symbol}`; + prettyFlowLimits.push(`${tokenIds[i]}=${prettyFlowLimit}`); + printInfo(`Setting flow limit for tokenId ${tokenIds[i]}`, prettyFlowLimit); + await txBuilder.moveCall({ target: `${itsConfig.address}::interchain_token_service::set_flow_limit`, arguments: [InterchainTokenService, OperatorCap, tokenId, flowLimit], @@ -103,7 +110,7 @@ async function setFlowLimits(keypair, client, config, contracts, args, options) const tx = txBuilder.tx; const sender = options.sender || keypair.toSuiAddress(); tx.setSender(sender); - await saveGeneratedTx(tx, `Set flow limits for ${tokenIds} to ${flowLimits}`, client, options); + await saveGeneratedTx(tx, `Set flow limits for ${prettyFlowLimits.join(', ')}`, client, options); } else { await broadcastFromTxBuilder(txBuilder, keypair, 'Set flow limits', options); } @@ -176,6 +183,7 @@ async function registerCoinFromInfo(keypair, client, config, contracts, args, op // Deploy token on Sui const [metadata, packageId, tokenType, treasuryCap] = await deployTokenFromInfo(deployConfig, symbol, name, decimals); + const resolvedTokenDecimals = decimals; // New CoinManagement const [txBuilder, coinManagement] = await createLockedCoinManagement(deployConfig, itsConfig, tokenType); @@ -183,7 +191,7 @@ async function registerCoinFromInfo(keypair, client, config, contracts, args, op // Register deployed token (from info) await txBuilder.moveCall({ target: `${itsConfig.address}::interchain_token_service::register_coin_from_info`, - arguments: [InterchainTokenService, name, symbol, decimals, coinManagement], + arguments: [InterchainTokenService, name, symbol, resolvedTokenDecimals, coinManagement], typeArguments: [tokenType], }); @@ -200,7 +208,7 @@ async function registerCoinFromInfo(keypair, client, config, contracts, args, op const tokenId = result.events[0].parsedJson.token_id.id; // Save the deployed token - saveTokenDeployment(packageId, tokenType, contracts, symbol, decimals, tokenId, treasuryCap, metadata); + saveTokenDeployment(packageId, tokenType, contracts, symbol, resolvedTokenDecimals, tokenId, treasuryCap, metadata); } async function registerCoinFromMetadata(keypair, client, config, contracts, args, options) { @@ -213,6 +221,7 @@ async function registerCoinFromMetadata(keypair, client, config, contracts, args // Deploy token on Sui const [metadata, packageId, tokenType, treasuryCap] = await deployTokenFromInfo(deployConfig, symbol, name, decimals); + const resolvedTokenDecimals = decimals; // New CoinManagement const [txBuilder, coinManagement] = await createLockedCoinManagement(deployConfig, itsConfig, tokenType); @@ -236,7 +245,7 @@ async function registerCoinFromMetadata(keypair, client, config, contracts, args const tokenId = result.events[0].parsedJson.token_id.id; // Save the deployed token - saveTokenDeployment(packageId, tokenType, contracts, symbol, decimals, tokenId, treasuryCap, metadata); + saveTokenDeployment(packageId, tokenType, contracts, symbol, resolvedTokenDecimals, tokenId, treasuryCap, metadata); } async function registerCustomCoin(keypair, client, config, contracts, args, options) { @@ -260,13 +269,12 @@ async function registerCustomCoin(keypair, client, config, contracts, args, opti const [metadata, packageId, tokenType, treasuryCap] = options.published ? [coin.objects.Metadata, coin.address, coin.typeArgument, coin.objects.TreasuryCap] : await deployTokenFromInfo(deployConfig, symbol, name, decimals); + const resolvedTokenDecimals = options.published ? (await coinMetadataByType(client, tokenType)).decimals : decimals; // Mint pre-registration coins const amount = Number.isFinite(Number(options.mintAmount)) ? parseInt(options.mintAmount) : 0; if (amount) { - const unitAmount = options.published - ? getUnitAmount(options.mintAmount, coin.decimals) - : getUnitAmount(options.mintAmount, decimals); + const unitAmount = getUnitAmount(options.mintAmount, resolvedTokenDecimals); const mintTxBuilder = new TxBuilder(client); @@ -297,7 +305,7 @@ async function registerCustomCoin(keypair, client, config, contracts, args, opti } // Save the deployed token - saveTokenDeployment(packageId, tokenType, contracts, symbol, decimals, tokenId, treasuryCap, metadata, [], saltAddress); + saveTokenDeployment(packageId, tokenType, contracts, symbol, resolvedTokenDecimals, tokenId, treasuryCap, metadata, [], saltAddress); // Save TreasuryCapReclaimer to coin config (if exists) if (options.treasuryCap && contracts[symbol.toUpperCase()]) { @@ -472,11 +480,11 @@ async function giveUnlinkedCoin(keypair, client, _, contracts, args, options) { throw new Error(`Cannot find coin with symbol ${symbol} in config`); } - const decimals = coin.decimals; const metadata = coin.objects.Metadata; const packageId = coin.address; const tokenType = coin.typeArgument; const treasuryCap = coin.objects.TreasuryCap; + const { decimals } = await tokenMetadata(client, tokenId, tokenType); // TokenId const tokenIdObject = await txBuilder.moveCall({ @@ -602,7 +610,7 @@ async function registerCoinMetadata(keypair, client, config, contracts, args, op ); } - let metadata, packageId, tokenType, treasuryCap; + let metadata, packageId, tokenType, treasuryCap, decimals; if (!savedCoin) { // Deploy source token on Sui [metadata, packageId, tokenType, treasuryCap] = await deployTokenFromInfo( @@ -611,12 +619,14 @@ async function registerCoinMetadata(keypair, client, config, contracts, args, op options.coinName, options.coinDecimals, ); + decimals = options.coinDecimals; } else { // Load saved coin params metadata = savedCoin.objects.Metadata; packageId = savedCoin.address; tokenType = savedCoin.typeArgument; treasuryCap = savedCoin.objects.TreasuryCap; + ({ decimals } = await tokenMetadata(client, savedCoin.objects.TokenId, tokenType)); } // User calls registerTokenMetadata on ITS Chain A to submit a RegisterTokenMetadata msg type to @@ -661,7 +671,7 @@ async function registerCoinMetadata(keypair, client, config, contracts, args, op tokenType, contracts, symbol, - options.coinDecimals, + decimals, null, // TokenId does not yet exist (pre-registration) treasuryCap, metadata, @@ -693,11 +703,11 @@ async function linkCoin(keypair, client, config, contracts, args, options) { // Coin params const coin = contracts[symbol.toUpperCase()]; - const decimals = coin.decimals; const metadata = coin.objects.Metadata; const packageId = coin.address; const tokenType = coin.typeArgument; const treasuryCap = coin.objects.TreasuryCap; + const { decimals } = await tokenMetadata(client, coin.objects.TokenId, tokenType); // Token Manager settings const tokenManager = options.tokenManagerMode; @@ -964,10 +974,9 @@ async function interchainTransfer(keypair, client, config, contracts, args, opti // Fetch CoinType from on-chain TokenID const coinType = await tokenIdToCoinType(client, walletAddress, itsConfig, tokenId); - let coinPackageId, coinDecimals; + let coinPackageId, symbol, coinDecimals; try { - const coinMetadata = await client.getCoinMetadata({ coinType }); - coinDecimals = coinMetadata.decimals; + ({ symbol, decimals: coinDecimals } = await tokenMetadata(client, tokenId, coinType)); coinPackageId = coinType.split('::')[0]; } catch { throw new Error(`Error parsing coin metadata for coin ${coinType}`); @@ -993,6 +1002,7 @@ async function interchainTransfer(keypair, client, config, contracts, args, opti // Convert human readable coin amount to send value const unitAmount = getUnitAmount(amount, coinDecimals); + printInfo(`Interchain transfer for tokenId ${tokenId}`, `${amount} ${symbol}`); // Check balance and load valid coin id const { coinObjectId, balance } = await senderHasSufficientBalance(client, keypair, options, coinType, unitAmount); @@ -1052,9 +1062,9 @@ async function interchainTransfer(keypair, client, config, contracts, args, opti const tx = txBuilder.tx; const sender = options.sender || keypair.toSuiAddress(); tx.setSender(sender); - await saveGeneratedTx(tx, `Interchain transfer for ${tokenId}`, client, options); + await saveGeneratedTx(tx, `Interchain transfer for ${tokenId} (${amount} ${symbol})`, client, options); } else { - await broadcastFromTxBuilder(txBuilder, keypair, 'Interchain Transfer', options); + await broadcastFromTxBuilder(txBuilder, keypair, `Interchain Transfer (${symbol})`, options); } } diff --git a/sui/utils/token-utils.js b/sui/utils/token-utils.js index 6bcd3fca3..feaaabb40 100644 --- a/sui/utils/token-utils.js +++ b/sui/utils/token-utils.js @@ -85,6 +85,31 @@ async function checkIfCoinExists(client, coinPackageId, coinType) { } } +async function coinMetadataByType(client, coinType) { + const coinMetadata = await client.getCoinMetadata({ coinType }); + + if (!coinMetadata) { + throw new Error(`Coin metadata not found for coin type ${coinType}`); + } + + return coinMetadata; +} + +async function tokenMetadata(client, tokenId, tokenType) { + if (!tokenId || !tokenType) { + throw new Error(`Token id and token type are required to resolve token metadata for ${tokenId || 'token'}`); + } + + const chainMetadata = await coinMetadataByType(client, tokenType); + + return { + symbol: chainMetadata.symbol, + decimals: chainMetadata.decimals, + tokenType, + tokenId, + }; +} + /** * Get a coin object id for a coin held by the user and meeting a given threshold. * Returns `undefined` if balance threshold criteria are not met. @@ -146,5 +171,7 @@ module.exports = { createLockedCoinManagement, saveTokenDeployment, checkIfCoinExists, + coinMetadataByType, + tokenMetadata, senderHasSufficientBalance, }; From 4c0111bdf9aa9af304b2c7a6dac5521bc6b71297 Mon Sep 17 00:00:00 2001 From: Milap Sheth Date: Tue, 31 Mar 2026 11:52:25 -0700 Subject: [PATCH 09/10] fix(stellar): validate deploy token inputs earlier --- .github/workflows/test-stellar.yaml | 2 +- stellar/its.js | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test-stellar.yaml b/.github/workflows/test-stellar.yaml index 6ea34a940..0a9a10b74 100644 --- a/.github/workflows/test-stellar.yaml +++ b/.github/workflows/test-stellar.yaml @@ -207,7 +207,7 @@ jobs: - name: Deploy Remote Interchain Tokens run: | - ts-node stellar/its.js deploy-interchain-token ${{ steps.env_vars.outputs.name }} ${{ steps.env_vars.outputs.symbol }} ${{ steps.env_vars.outputs.decimal }} ${{ steps.env_vars.outputs.salt }} ${{ steps.env_vars.outputs.amount }} + ts-node stellar/its.js deploy-interchain-token ${{ steps.env_vars.outputs.symbol }} ${{ steps.env_vars.outputs.name }} ${{ steps.env_vars.outputs.decimal }} ${{ steps.env_vars.outputs.salt }} ${{ steps.env_vars.outputs.amount }} ts-node stellar/its.js deploy-remote-interchain-token ${{ steps.env_vars.outputs.salt }} ${{ steps.env_vars.outputs.destinationChain }} ###### Command: ITS - Create a Custom Token ###### diff --git a/stellar/its.js b/stellar/its.js index 78d718ec1..a3ad48460 100644 --- a/stellar/its.js +++ b/stellar/its.js @@ -151,14 +151,15 @@ async function deployInterchainToken(wallet, _config, chain, contract, args, opt const caller = addressToScVal(wallet.publicKey()); const minter = caller; const [symbol, name, decimal, salt, initialSupply] = args; - const saltBytes32 = saltToBytes32(salt); - const initialSupplyInUnits = parseTokenAmount(initialSupply, Number(decimal)); validateParameters({ isNonEmptyString: { symbol, name, initialSupply }, isValidNumber: { decimal }, }); + const saltBytes32 = saltToBytes32(salt); + const initialSupplyInUnits = parseTokenAmount(initialSupply, Number(decimal)); + printInfo('Salt', salt); printInfo('Deployment salt (bytes32)', saltBytes32); printInfo('Initial supply', `${initialSupply} ${symbol}`); From e2d8f5f474e49f887ac3d7e390a4f881df29a22a Mon Sep 17 00:00:00 2001 From: Milap Sheth Date: Wed, 29 Apr 2026 11:15:28 +0900 Subject: [PATCH 10/10] changes --- axelar-chains-config/info/mainnet.json | 2 +- common/utils.js | 2 +- evm/its.js | 6 +- sui/its.js | 140 ++++++++++++++++++++++++- 4 files changed, 146 insertions(+), 4 deletions(-) diff --git a/axelar-chains-config/info/mainnet.json b/axelar-chains-config/info/mainnet.json index b03130a2a..3499cf185 100644 --- a/axelar-chains-config/info/mainnet.json +++ b/axelar-chains-config/info/mainnet.json @@ -2556,7 +2556,7 @@ "name": "Sui", "axelarId": "sui", "networkType": "mainnet", - "rpc": "https://sui-mainnet.gateway.tatum.io/", + "rpc": "https://fullnode.mainnet.sui.io", "tokenSymbol": "SUI", "decimals": 9, "chainType": "sui", diff --git a/common/utils.js b/common/utils.js index 3a5cce707..4d9ccabbc 100644 --- a/common/utils.js +++ b/common/utils.js @@ -180,7 +180,7 @@ const isValidDecimal = (arg) => { const num = parseFloat(arg); - return !isNaN(num) && isFinite(num) && num === parseFloat(String(arg).trim()); + return !isNaN(num) && isFinite(num) && num === parseFloat(String(arg).trim()) && num >= 0; }; const isNumberArray = (arr) => { diff --git a/evm/its.js b/evm/its.js index 2cb8e4b7d..557833201 100644 --- a/evm/its.js +++ b/evm/its.js @@ -274,7 +274,11 @@ async function processCommand(_axelar, chain, chains, action, options) { const { tokenManager, symbol, decimals } = await tokenManagerAndMetadata(interchainTokenService, tokenId, wallet); const flowLimit = await tokenManager.flowLimit(); - printInfo(`Flow limit for tokenId ${tokenId}`, `${formatUnits(flowLimit, decimals)} ${symbol}`); + if (flowLimit.isZero()) { + printInfo(`Flow limit for tokenId ${tokenId}`, 'No limit set'); + } else { + printInfo(`Flow limit for tokenId ${tokenId}`, `${formatUnits(flowLimit, decimals)} ${symbol}`); + } break; } diff --git a/sui/its.js b/sui/its.js index 4ebc39cd5..768a20715 100644 --- a/sui/its.js +++ b/sui/its.js @@ -40,6 +40,67 @@ const { } = require('hardhat').ethers; const { checkIfCoinExists, tokenMetadata, senderHasSufficientBalance } = require('./utils/token-utils'); +async function registeredCoinData(client, itsConfig, tokenId) { + const itsObject = await client.getObject({ + id: itsConfig.objects.InterchainTokenServicev0, + options: { showContent: true }, + }); + + const registeredCoinsId = itsObject?.data?.content?.fields?.value?.fields?.registered_coins?.fields?.id?.id; + if (!registeredCoinsId) { + throw new Error(`Unable to query registered coins bag for ITS object ${itsConfig.objects.InterchainTokenServicev0}`); + } + + let cursor = null; + do { + const page = await client.getDynamicFields({ parentId: registeredCoinsId, cursor }); + const entry = (page.data || []).find((coin) => coin.name?.value === tokenId || coin.name?.fields?.id === tokenId); + if (entry) { + const coinObject = await client.getObject({ + id: entry.objectId, + options: { showContent: true }, + }); + + return coinObject?.data?.content?.fields?.value?.fields || null; + } + + cursor = page.hasNextPage ? page.nextCursor : null; + } while (cursor); + + throw new Error(`Registered coin data not found for tokenId ${tokenId}`); +} + +function optionValue(option) { + if (!option) { + return null; + } + + if (Array.isArray(option.vec)) { + return option.vec.length ? option.vec[0] : null; + } + + if (option.fields && Array.isArray(option.fields.vec)) { + return option.fields.vec.length ? option.fields.vec[0] : null; + } + + return null; +} + +async function flowAmount(client, itsConfig, tokenId) { + const coinData = await registeredCoinData(client, itsConfig, tokenId); + const flowLimitData = coinData?.coin_management?.fields?.flow_limit?.fields; + + if (!flowLimitData) { + throw new Error(`Flow limit data not found for tokenId ${tokenId}`); + } + + return { + flowLimit: optionValue(flowLimitData.flow_limit), + flowInAmount: flowLimitData.flow_in, + flowOutAmount: flowLimitData.flow_out, + }; +} + async function setFlowLimits(keypair, client, config, contracts, args, options) { let [tokenIds, flowLimits] = args; @@ -116,6 +177,56 @@ async function setFlowLimits(keypair, client, config, contracts, args, options) } } +async function flowLimit(keypair, client, _config, contracts, args, _options) { + const { InterchainTokenService: itsConfig } = contracts; + const [tokenId] = args; + + validateParameters({ + isHexString: { tokenId }, + }); + + const coinType = await tokenIdToCoinType(client, keypair.toSuiAddress(), itsConfig, tokenId); + const { symbol, decimals } = await tokenMetadata(client, tokenId, coinType); + const { flowLimit } = await flowAmount(client, itsConfig, tokenId); + + if (flowLimit === null) { + printInfo(`Flow limit for tokenId ${tokenId}`, 'No limit set'); + return; + } + + printInfo(`Flow limit for tokenId ${tokenId}`, `${getFormattedAmount(flowLimit, decimals)} ${symbol}`); +} + +async function flowOutAmount(keypair, client, _config, contracts, args, _options) { + const { InterchainTokenService: itsConfig } = contracts; + const [tokenId] = args; + + validateParameters({ + isHexString: { tokenId }, + }); + + const coinType = await tokenIdToCoinType(client, keypair.toSuiAddress(), itsConfig, tokenId); + const { symbol, decimals } = await tokenMetadata(client, tokenId, coinType); + const { flowOutAmount } = await flowAmount(client, itsConfig, tokenId); + + printInfo(`Flow out amount for tokenId ${tokenId}`, `${getFormattedAmount(flowOutAmount, decimals)} ${symbol}`); +} + +async function flowInAmount(keypair, client, _config, contracts, args, _options) { + const { InterchainTokenService: itsConfig } = contracts; + const [tokenId] = args; + + validateParameters({ + isHexString: { tokenId }, + }); + + const coinType = await tokenIdToCoinType(client, keypair.toSuiAddress(), itsConfig, tokenId); + const { symbol, decimals } = await tokenMetadata(client, tokenId, coinType); + const { flowInAmount } = await flowAmount(client, itsConfig, tokenId); + + printInfo(`Flow in amount for tokenId ${tokenId}`, `${getFormattedAmount(flowInAmount, decimals)} ${symbol}`); +} + async function addTrustedChains(keypair, client, config, contracts, args, options) { const { InterchainTokenService: itsConfig } = contracts; @@ -1157,6 +1268,30 @@ if (require.main === module) { return mainProcessor(setFlowLimits, options, [tokenIds, flowLimits], processCommand); }); + const flowLimitProgram = new Command() + .name('flow-limit') + .command('flow-limit ') + .description('Get flow limit for token') + .action((tokenId, options) => { + return mainProcessor(flowLimit, options, [tokenId], processCommand); + }); + + const flowOutAmountProgram = new Command() + .name('flow-out-amount') + .command('flow-out-amount ') + .description('Get flow out amount for token') + .action((tokenId, options) => { + return mainProcessor(flowOutAmount, options, [tokenId], processCommand); + }); + + const flowInAmountProgram = new Command() + .name('flow-in-amount') + .command('flow-in-amount ') + .description('Get flow in amount for token') + .action((tokenId, options) => { + return mainProcessor(flowInAmount, options, [tokenId], processCommand); + }); + const registerCoinFromInfoProgram = new Command() .name('register-coin-from-info') .command('register-coin-from-info ') @@ -1322,6 +1457,9 @@ if (require.main === module) { program.addCommand(addTrustedChainsProgram); program.addCommand(checkVersionControlProgram); program.addCommand(deployRemoteCoinProgram); + program.addCommand(flowInAmountProgram); + program.addCommand(flowLimitProgram); + program.addCommand(flowOutAmountProgram); program.addCommand(giveUnlinkedCoinProgram); program.addCommand(interchainTransferProgram); program.addCommand(linkCoinProgram); @@ -1343,4 +1481,4 @@ if (require.main === module) { program.parseAsync().then(() => process.exit(0)); } -module.exports = { addTrustedChains, removeTrustedChains, setFlowLimits }; +module.exports = { addTrustedChains, flowInAmount, flowLimit, flowOutAmount, removeTrustedChains, setFlowLimits };