From 28284443141f700d2214c42d8d7b983b40f569fc Mon Sep 17 00:00:00 2001 From: samsiegart Date: Tue, 17 Dec 2024 10:57:43 -0800 Subject: [PATCH] feat(fast-usdc): detect transfer completion in cli --- packages/fast-usdc/demo/testnet/config.json | 9 ++- packages/fast-usdc/src/cli/cli.js | 5 ++ packages/fast-usdc/src/cli/config.js | 9 +++ packages/fast-usdc/src/cli/transfer.js | 42 ++++++++++++ packages/fast-usdc/src/util/bank.js | 12 ++++ packages/fast-usdc/src/util/cctp.js | 2 +- packages/fast-usdc/test/cli/transfer.test.ts | 68 ++++++++++++++++---- packages/fast-usdc/testing/mocks.ts | 4 +- 8 files changed, 136 insertions(+), 15 deletions(-) create mode 100644 packages/fast-usdc/src/util/bank.js diff --git a/packages/fast-usdc/demo/testnet/config.json b/packages/fast-usdc/demo/testnet/config.json index 13b4420b23f..67a26f2a6d7 100644 --- a/packages/fast-usdc/demo/testnet/config.json +++ b/packages/fast-usdc/demo/testnet/config.json @@ -7,5 +7,12 @@ "nobleApi": "https://noble-api.polkachu.com", "ethRpc": "https://sepolia.drpc.org", "tokenMessengerAddress": "0x9f3B8679c73C2Fef8b59B4f3444d4e156fb70AA5", - "tokenAddress": "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238" + "tokenAddress": "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238", + "destinationChains": [ + { + "bech32prefix": "osmo", + "api": "https://lcd.osmosis.zone", + "USDCDenom": "ibc/498A0751C798A0D9A389AA3691123DADA57DAA4FE165D5C75894505B876BA6E4" + } + ] } diff --git a/packages/fast-usdc/src/cli/cli.js b/packages/fast-usdc/src/cli/cli.js index 8676ef955e2..18acb636fe9 100644 --- a/packages/fast-usdc/src/cli/cli.js +++ b/packages/fast-usdc/src/cli/cli.js @@ -91,7 +91,12 @@ export const initProgram = ( /** @type {string} */ amount, /** @type {string} */ destination, ) => { + const start = now(); await transferHelpers.transfer(makeConfigFile(), amount, destination); + const duration = now() - start; + stdout.write( + `Transfer finished in ${(duration / 1000).toFixed(1)} seconds`, + ); }, ); diff --git a/packages/fast-usdc/src/cli/config.js b/packages/fast-usdc/src/cli/config.js index 2b67a4868a3..c59e6c8fc13 100644 --- a/packages/fast-usdc/src/cli/config.js +++ b/packages/fast-usdc/src/cli/config.js @@ -1,6 +1,14 @@ import * as readline from 'node:readline/promises'; import { stdin as input, stdout as output } from 'node:process'; +/** + @typedef {{ + bech32Prefix: string, + api: string, + USDCDenom: string + }} DestinationChain + */ + /** @typedef {{ nobleSeed: string, @@ -11,6 +19,7 @@ import { stdin as input, stdout as output } from 'node:process'; ethRpc: string, tokenMessengerAddress: string, tokenAddress: string + destinationChains?: DestinationChain[] }} ConfigOpts */ diff --git a/packages/fast-usdc/src/cli/transfer.js b/packages/fast-usdc/src/cli/transfer.js index 41719ac1c37..31b8e212f65 100644 --- a/packages/fast-usdc/src/cli/transfer.js +++ b/packages/fast-usdc/src/cli/transfer.js @@ -14,6 +14,7 @@ import { queryForwardingAccount, registerFwdAccount, } from '../util/noble.js'; +import { queryUSDCBalance } from '../util/bank.js'; /** @import { File } from '../util/file' */ /** @import { VStorage } from '@agoric/client-utils' */ @@ -30,6 +31,7 @@ const transfer = async ( /** @type {{signer: SigningStargateClient, address: string} | undefined} */ nobleSigner, /** @type {ethProvider | undefined} */ ethProvider, env = process.env, + setTimeout = globalThis.setTimeout, ) => { const execute = async ( /** @type {import('./config').ConfigOpts} */ config, @@ -71,6 +73,18 @@ const transfer = async ( } } + const destChain = config.destinationChains?.find(chain => + EUD.startsWith(chain.bech32Prefix), + ); + if (!destChain) { + out.error( + `No destination chain found in config with matching bech32 prefix for ${EUD}, cannot query destination address`, + ); + throw new Error(); + } + const { api, USDCDenom } = destChain; + const startingBalance = await queryUSDCBalance(EUD, api, USDCDenom, fetch); + ethProvider ||= makeProvider(config.ethRpc); await depositForBurn( ethProvider, @@ -81,6 +95,34 @@ const transfer = async ( amount, out, ); + + const refreshDelayMS = 1200; + const completeP = /** @type {Promise} */ ( + new Promise((res, rej) => { + const refreshUSDCBalance = async () => { + out.log('polling usdc balance'); + const currentBalance = await queryUSDCBalance( + EUD, + api, + USDCDenom, + fetch, + ); + if (currentBalance !== startingBalance) { + res(); + } else { + setTimeout(() => refreshUSDCBalance().catch(rej), refreshDelayMS); + } + }; + refreshUSDCBalance().catch(rej); + }) + ).catch(e => { + out.error( + 'Error checking destination address balance, could not detect completion of transfer.', + ); + out.error(e.message); + }); + + await completeP; }; let config; diff --git a/packages/fast-usdc/src/util/bank.js b/packages/fast-usdc/src/util/bank.js new file mode 100644 index 00000000000..24f1ced5265 --- /dev/null +++ b/packages/fast-usdc/src/util/bank.js @@ -0,0 +1,12 @@ +export const queryUSDCBalance = async ( + /** @type {string} */ address, + /** @type {string} */ api, + /** @type {string} */ denom, + /** @type {typeof globalThis.fetch} */ fetch, +) => { + const query = `${api}/cosmos/bank/v1beta1/balances/${address}`; + const json = await fetch(query).then(res => res.json()); + const amount = json.balances?.find(b => b.denom === denom)?.amount ?? '0'; + + return BigInt(amount); +}; diff --git a/packages/fast-usdc/src/util/cctp.js b/packages/fast-usdc/src/util/cctp.js index 08d8d475564..761e5221f69 100644 --- a/packages/fast-usdc/src/util/cctp.js +++ b/packages/fast-usdc/src/util/cctp.js @@ -67,5 +67,5 @@ export const depositForBurn = async ( out.log('Transaction confirmed in block', receipt.blockNumber); out.log('Transaction hash:', receipt.hash); - out.log('USDC transfer initiated successfully, our work here is done.'); + out.log('USDC transfer initiated successfully'); }; diff --git a/packages/fast-usdc/test/cli/transfer.test.ts b/packages/fast-usdc/test/cli/transfer.test.ts index 28cd3d41569..1f75734d5ae 100644 --- a/packages/fast-usdc/test/cli/transfer.test.ts +++ b/packages/fast-usdc/test/cli/transfer.test.ts @@ -52,6 +52,8 @@ test('Transfer registers the noble forwarding account if it does not exist', asy const path = 'config/dir/.fast-usdc/config.json'; const nobleApi = 'http://api.noble.test'; const nobleToAgoricChannel = 'channel-test-7'; + const destinationChainApi = 'http://api.dydx.fake-test'; + const destinationUSDCDenom = 'ibc/USDCDENOM'; const config = { agoricRpc: 'http://rpc.agoric.test', nobleApi, @@ -61,6 +63,13 @@ test('Transfer registers the noble forwarding account if it does not exist', asy ethSeed: 'a4b7f431465df5dc1458cd8a9be10c42da8e3729e3ce53f18814f48ae2a98a08', tokenMessengerAddress: '0x9f3B8679c73C2Fef8b59B4f3444d4e156fb70AA5', tokenAddress: '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238', + destinationChains: [ + { + bech32Prefix: 'dydx', + api: destinationChainApi, + USDCDenom: destinationUSDCDenom, + }, + ], }; const out = mockOut(); const file = mockFile(path, JSON.stringify(config)); @@ -76,11 +85,25 @@ test('Transfer registers the noble forwarding account if it does not exist', asy agoricSettlementAccount, { EUD }, )}/`; - const fetchMock = makeFetchMock({ - [nobleFwdAccountQuery]: { - address: 'noble14lwerrcfzkzrv626w49pkzgna4dtga8c5x479h', - exists: false, - }, + const destinationBankQuery = `${destinationChainApi}/cosmos/bank/v1beta1/balances/${EUD}`; + let balanceQueryCount = 0; + const fetchMock = makeFetchMock((query: string) => { + if (query === nobleFwdAccountQuery) { + return { + address: 'noble14lwerrcfzkzrv626w49pkzgna4dtga8c5x479h', + exists: false, + }; + } + if (query === destinationBankQuery) { + if (balanceQueryCount > 1) { + return { + balances: [{ denom: destinationUSDCDenom, amount }], + }; + } else { + balanceQueryCount += 1; + return {}; + } + } }); const nobleSignerAddress = 'noble09876'; const signerMock = makeMockSigner(); @@ -97,7 +120,6 @@ test('Transfer registers the noble forwarding account if it does not exist', asy { signer: signerMock.signer, address: nobleSignerAddress }, mockEthProvider.provider, ); - t.is(vstorageMock.getQueryCounts()[settlementAccountVstoragePath], 1); t.is(fetchMock.getQueryCounts()[nobleFwdAccountQuery], 1); t.snapshot(signerMock.getSigned()); @@ -107,6 +129,8 @@ test('Transfer signs and broadcasts the depositForBurn message on Ethereum', asy const path = 'config/dir/.fast-usdc/config.json'; const nobleApi = 'http://api.noble.test'; const nobleToAgoricChannel = 'channel-test-7'; + const destinationChainApi = 'http://api.dydx.fake-test'; + const destinationUSDCDenom = 'ibc/USDCDENOM'; const config = { agoricRpc: 'http://rpc.agoric.test', nobleApi, @@ -116,6 +140,13 @@ test('Transfer signs and broadcasts the depositForBurn message on Ethereum', asy ethSeed: 'a4b7f431465df5dc1458cd8a9be10c42da8e3729e3ce53f18814f48ae2a98a08', tokenMessengerAddress: '0x9f3B8679c73C2Fef8b59B4f3444d4e156fb70AA5', tokenAddress: '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238', + destinationChains: [ + { + bech32Prefix: 'dydx', + api: destinationChainApi, + USDCDenom: destinationUSDCDenom, + }, + ], }; const out = mockOut(); const file = mockFile(path, JSON.stringify(config)); @@ -131,11 +162,25 @@ test('Transfer signs and broadcasts the depositForBurn message on Ethereum', asy agoricSettlementAccount, { EUD }, )}/`; - const fetchMock = makeFetchMock({ - [nobleFwdAccountQuery]: { - address: 'noble14lwerrcfzkzrv626w49pkzgna4dtga8c5x479h', - exists: true, - }, + const destinationBankQuery = `${destinationChainApi}/cosmos/bank/v1beta1/balances/${EUD}`; + let balanceQueryCount = 0; + const fetchMock = makeFetchMock((query: string) => { + if (query === nobleFwdAccountQuery) { + return { + address: 'noble14lwerrcfzkzrv626w49pkzgna4dtga8c5x479h', + exists: true, + }; + } + if (query === destinationBankQuery) { + if (balanceQueryCount > 1) { + return { + balances: [{ denom: destinationUSDCDenom, amount }], + }; + } else { + balanceQueryCount += 1; + return {}; + } + } }); const nobleSignerAddress = 'noble09876'; const signerMock = makeMockSigner(); @@ -162,4 +207,5 @@ test('Transfer signs and broadcasts the depositForBurn message on Ethereum', asy t.deepEqual(mockEthProvider.getTxnArgs()[1], [ '0xf8e4800180949f3b8679c73c2fef8b59b4f3444d4e156fb70aa580b8846fd3504e0000000000000000000000000000000000000000000000000000000008f0d1800000000000000000000000000000000000000000000000000000000000000004000000000000000000000000afdd918f09158436695a754a1b0913ed5ab474f80000000000000000000000001c7d4b196cb0c7b01d743fbc6116a902379c723882011aa09fc97790b2ba23fbb974554dbcee00df1a1f50e9fec4fdf370454773604aa477a038a1d86afc2a7afdc78088878a912f1a7c678b10c3120d308f8260a277b135a3', ]); + t.is(fetchMock.getQueryCounts()[destinationBankQuery], 3); }); diff --git a/packages/fast-usdc/testing/mocks.ts b/packages/fast-usdc/testing/mocks.ts index 92f6c885d45..55b0a495ee6 100644 --- a/packages/fast-usdc/testing/mocks.ts +++ b/packages/fast-usdc/testing/mocks.ts @@ -43,11 +43,11 @@ export const makeVstorageMock = (records: { [key: string]: any }) => { return { vstorage, getQueryCounts: () => queryCounts }; }; -export const makeFetchMock = (records: { [key: string]: any }) => { +export const makeFetchMock = get => { const queryCounts = {}; const fetch = async (path: string) => { queryCounts[path] = (queryCounts[path] ?? 0) + 1; - return { json: async () => records[path] }; + return { json: async () => get(path) }; }; return { fetch, getQueryCounts: () => queryCounts };