Skip to content

Commit

Permalink
feat(fast-usdc): detect transfer completion in cli
Browse files Browse the repository at this point in the history
  • Loading branch information
samsiegart committed Dec 18, 2024
1 parent 9e5f628 commit 2828444
Show file tree
Hide file tree
Showing 8 changed files with 136 additions and 15 deletions.
9 changes: 8 additions & 1 deletion packages/fast-usdc/demo/testnet/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
]
}
5 changes: 5 additions & 0 deletions packages/fast-usdc/src/cli/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -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`,
);
},
);

Expand Down
9 changes: 9 additions & 0 deletions packages/fast-usdc/src/cli/config.js
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -11,6 +19,7 @@ import { stdin as input, stdout as output } from 'node:process';
ethRpc: string,
tokenMessengerAddress: string,
tokenAddress: string
destinationChains?: DestinationChain[]
}} ConfigOpts
*/

Expand Down
42 changes: 42 additions & 0 deletions packages/fast-usdc/src/cli/transfer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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' */
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -81,6 +95,34 @@ const transfer = async (
amount,
out,
);

const refreshDelayMS = 1200;
const completeP = /** @type {Promise<void>} */ (
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;
Expand Down
12 changes: 12 additions & 0 deletions packages/fast-usdc/src/util/bank.js
Original file line number Diff line number Diff line change
@@ -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);
};
2 changes: 1 addition & 1 deletion packages/fast-usdc/src/util/cctp.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
};
68 changes: 57 additions & 11 deletions packages/fast-usdc/test/cli/transfer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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));
Expand All @@ -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();
Expand All @@ -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());
Expand All @@ -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,
Expand All @@ -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));
Expand All @@ -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();
Expand All @@ -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);
});
4 changes: 2 additions & 2 deletions packages/fast-usdc/testing/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand Down

0 comments on commit 2828444

Please sign in to comment.