From 14fa39b8e29da56315ea1c0c225e1592b674d0b6 Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Tue, 3 Dec 2024 23:06:25 -0500 Subject: [PATCH 1/9] chore: `setupTestKeys` optionally takes mnemonics --- multichain-testing/test/support.ts | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/multichain-testing/test/support.ts b/multichain-testing/test/support.ts index 18e8006f155..656c282df46 100644 --- a/multichain-testing/test/support.ts +++ b/multichain-testing/test/support.ts @@ -42,13 +42,19 @@ const makeKeyring = async ( e2eTools: Pick, ) => { let _keys = ['user1']; - const setupTestKeys = async (keys = ['user1']) => { + const setupTestKeys = async ( + keys = ['user1'], + mnemonics?: (string | undefined)[], + ) => { _keys = keys; const wallets: Record = {}; - for (const name of keys) { - const res = await e2eTools.addKey(name, generateMnemonic()); + for (const i in keys) { + const res = await e2eTools.addKey( + keys[i], + mnemonics?.[i] || generateMnemonic(), + ); const { address } = JSON.parse(res); - wallets[name] = address; + wallets[keys[i]] = address; } return wallets; }; @@ -117,10 +123,7 @@ export const commonSetup = async (t: ExecutionContext) => { ? objectMap( // eslint-disable-next-line @typescript-eslint/no-explicit-any builderOpts as Record, - value => { - if (typeof value === 'string') return value; - return JSON.stringify(value); - }, + value => (typeof value === 'string' ? value : JSON.stringify(value)), ) : undefined; await deployBuilder(contractBuilder, formattedOpts); From 6cb025af9158f72b491f3d5fb492b108aa8b5f34 Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Tue, 3 Dec 2024 23:06:57 -0500 Subject: [PATCH 2/9] chore: export denom tools --- multichain-testing/tools/asset-info.ts | 46 +++++++++++++++----------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/multichain-testing/tools/asset-info.ts b/multichain-testing/tools/asset-info.ts index ef4faf1d5ac..568cf6019c3 100644 --- a/multichain-testing/tools/asset-info.ts +++ b/multichain-testing/tools/asset-info.ts @@ -6,6 +6,31 @@ import { } from '@agoric/orchestration'; import type { IBCChannelID } from '@agoric/vats'; +export const makeDenomTools = (chainInfo: Record) => { + const getTransferChannelId = ( + destChainId: string, + fromChainName: string, + ): IBCChannelID | undefined => + chainInfo[fromChainName]?.connections?.[destChainId]?.transferChannel + .channelId; + + const toDenomHash = ( + denom: Denom, + destChainId: string, + fromChainName: string, + ): Denom => { + const channelId = getTransferChannelId(destChainId, fromChainName); + if (!channelId) { + throw new Error( + `No channel found for ${destChainId} -> ${fromChainName}`, + ); + } + return `ibc/${denomHash({ denom, channelId })}`; + }; + + return harden({ getTransferChannelId, toDenomHash }); +}; + /** * Make asset info for the current environment. * @@ -21,26 +46,7 @@ export const makeAssetInfo = ( osmosis: ['uosmo', 'uion'], }, ): [Denom, DenomDetail][] => { - const getChannelId = ( - issuingChainId: string, - holdingChainName: string, - ): IBCChannelID | undefined => - chainInfo[holdingChainName]?.connections?.[issuingChainId]?.transferChannel - .channelId; - - const toDenomHash = ( - denom: Denom, - issuingChainId: string, - holdingChainName: string, - ): Denom => { - const channelId = getChannelId(issuingChainId, holdingChainName); - if (!channelId) { - throw new Error( - `No channel found for ${issuingChainId} -> ${holdingChainName}`, - ); - } - return `ibc/${denomHash({ denom, channelId })}`; - }; + const { toDenomHash } = makeDenomTools(chainInfo); // only include chains present in `chainInfo` const tokens = Object.entries(tokenMap) From 1f4b4c6219061972bb7d7ee62114e0ed93522457 Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Tue, 3 Dec 2024 23:09:02 -0500 Subject: [PATCH 3/9] chore: `WalletDriver` type --- multichain-testing/tools/e2e-tools.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/multichain-testing/tools/e2e-tools.js b/multichain-testing/tools/e2e-tools.js index f820e7c8077..b2a77f1a4fb 100644 --- a/multichain-testing/tools/e2e-tools.js +++ b/multichain-testing/tools/e2e-tools.js @@ -297,6 +297,8 @@ export const provisionSmartWallet = async ( return { offers, deposit, peek, query: q }; }; +/** @typedef {Awaited>} WalletDriver */ + /** * @param {{ * agd: import('./agd-lib.js').Agd; From 46f94d5ed69a296fb0692433263ce4e56df9f2a0 Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Wed, 4 Dec 2024 23:45:30 -0500 Subject: [PATCH 4/9] refactor: `commonBuilderOpts` - include `chainInfo` and `assetInfo` as common builder options for orchestration contracts --- multichain-testing/test/auto-stake-it.test.ts | 8 ++------ multichain-testing/test/basic-flows.test.ts | 7 ++----- .../test/deposit-withdraw-lca.test.ts | 7 ++----- .../test/deposit-withdraw-portfolio.test.ts | 7 ++----- .../test/ica-channel-close.test.ts | 7 ++----- multichain-testing/test/send-anywhere.test.ts | 8 ++------ multichain-testing/test/support.ts | 18 +++++++----------- 7 files changed, 19 insertions(+), 43 deletions(-) diff --git a/multichain-testing/test/auto-stake-it.test.ts b/multichain-testing/test/auto-stake-it.test.ts index d4bfded2086..01afae775f2 100644 --- a/multichain-testing/test/auto-stake-it.test.ts +++ b/multichain-testing/test/auto-stake-it.test.ts @@ -18,15 +18,11 @@ const contractBuilder = test.before(async t => { const { setupTestKeys, ...common } = await commonSetup(t); - const { assetInfo, chainInfo, deleteTestKeys, startContract } = common; + const { commonBuilderOpts, deleteTestKeys, startContract } = common; deleteTestKeys(accounts).catch(); const wallets = await setupTestKeys(accounts); t.context = { ...common, wallets }; - - await startContract(contractName, contractBuilder, { - chainInfo, - assetInfo, - }); + await startContract(contractName, contractBuilder, commonBuilderOpts); }); test.after(async t => { diff --git a/multichain-testing/test/basic-flows.test.ts b/multichain-testing/test/basic-flows.test.ts index 689db323524..9e9ff738a4d 100644 --- a/multichain-testing/test/basic-flows.test.ts +++ b/multichain-testing/test/basic-flows.test.ts @@ -17,14 +17,11 @@ const contractBuilder = test.before(async t => { const { setupTestKeys, ...common } = await commonSetup(t); - const { assetInfo, chainInfo, deleteTestKeys, startContract } = common; + const { commonBuilderOpts, deleteTestKeys, startContract } = common; deleteTestKeys(accounts).catch(); const wallets = await setupTestKeys(accounts); t.context = { ...common, wallets }; - await startContract(contractName, contractBuilder, { - chainInfo, - assetInfo, - }); + await startContract(contractName, contractBuilder, commonBuilderOpts); }); test.after(async t => { diff --git a/multichain-testing/test/deposit-withdraw-lca.test.ts b/multichain-testing/test/deposit-withdraw-lca.test.ts index ad7fd4824d9..e9127f3919a 100644 --- a/multichain-testing/test/deposit-withdraw-lca.test.ts +++ b/multichain-testing/test/deposit-withdraw-lca.test.ts @@ -15,14 +15,11 @@ const contractBuilder = test.before(async t => { const { setupTestKeys, ...common } = await commonSetup(t); - const { assetInfo, chainInfo, deleteTestKeys, startContract } = common; + const { commonBuilderOpts, deleteTestKeys, startContract } = common; deleteTestKeys(accounts).catch(); const wallets = await setupTestKeys(accounts); t.context = { ...common, wallets }; - await startContract(contractName, contractBuilder, { - chainInfo, - assetInfo, - }); + await startContract(contractName, contractBuilder, commonBuilderOpts); }); test.after(async t => { diff --git a/multichain-testing/test/deposit-withdraw-portfolio.test.ts b/multichain-testing/test/deposit-withdraw-portfolio.test.ts index ea97f1e7f17..0bccf0bad60 100644 --- a/multichain-testing/test/deposit-withdraw-portfolio.test.ts +++ b/multichain-testing/test/deposit-withdraw-portfolio.test.ts @@ -15,14 +15,11 @@ const contractBuilder = test.before(async t => { const { setupTestKeys, ...common } = await commonSetup(t); - const { assetInfo, chainInfo, deleteTestKeys, startContract } = common; + const { commonBuilderOpts, deleteTestKeys, startContract } = common; deleteTestKeys(accounts).catch(); const wallets = await setupTestKeys(accounts); t.context = { ...common, wallets }; - await startContract(contractName, contractBuilder, { - chainInfo, - assetInfo, - }); + await startContract(contractName, contractBuilder, commonBuilderOpts); }); test.after(async t => { diff --git a/multichain-testing/test/ica-channel-close.test.ts b/multichain-testing/test/ica-channel-close.test.ts index 03b0ef9ac95..f7799dac1e0 100644 --- a/multichain-testing/test/ica-channel-close.test.ts +++ b/multichain-testing/test/ica-channel-close.test.ts @@ -23,14 +23,11 @@ const contractBuilder = test.before(async t => { const { setupTestKeys, ...common } = await commonSetup(t); - const { assetInfo, chainInfo, deleteTestKeys, startContract } = common; + const { commonBuilderOpts, deleteTestKeys, startContract } = common; deleteTestKeys(accounts).catch(); const wallets = await setupTestKeys(accounts); t.context = { ...common, wallets }; - await startContract(contractName, contractBuilder, { - chainInfo, - assetInfo, - }); + await startContract(contractName, contractBuilder, commonBuilderOpts); }); test.after(async t => { diff --git a/multichain-testing/test/send-anywhere.test.ts b/multichain-testing/test/send-anywhere.test.ts index 11508f22cc6..c33084f4eff 100644 --- a/multichain-testing/test/send-anywhere.test.ts +++ b/multichain-testing/test/send-anywhere.test.ts @@ -21,16 +21,12 @@ const contractBuilder = test.before(async t => { const { setupTestKeys, ...common } = await commonSetup(t); - const { assetInfo, chainInfo, deleteTestKeys, faucetTools, startContract } = + const { commonBuilderOpts, deleteTestKeys, faucetTools, startContract } = common; deleteTestKeys(accounts).catch(); const wallets = await setupTestKeys(accounts); t.context = { ...common, wallets }; - - await startContract(contractName, contractBuilder, { - chainInfo, - assetInfo, - }); + await startContract(contractName, contractBuilder, commonBuilderOpts); await faucetTools.fundFaucet([ ['cosmoshub', 'uatom'], diff --git a/multichain-testing/test/support.ts b/multichain-testing/test/support.ts index 656c282df46..f71e2c112cc 100644 --- a/multichain-testing/test/support.ts +++ b/multichain-testing/test/support.ts @@ -4,7 +4,6 @@ import { execa } from 'execa'; import fse from 'fs-extra'; import childProcess from 'node:child_process'; import { withChainCapabilities } from '@agoric/orchestration'; -import { objectMap } from '@endo/patterns'; import { makeAgdTools } from '../tools/agd-tools.js'; import { type E2ETools } from '../tools/e2e-tools.js'; import { @@ -97,6 +96,10 @@ export const commonSetup = async (t: ExecutionContext) => { retryUntilCondition, useChain, ); + const commonBuilderOpts = harden({ + assetInfo: JSON.stringify(assetInfo), + chainInfo: JSON.stringify(chainInfo), + }); /** * Starts a contract if instance not found. Takes care of installing @@ -108,7 +111,7 @@ export const commonSetup = async (t: ExecutionContext) => { const startContract = async ( contractName: string, contractBuilder: string, - builderOpts?: Record, + builderOpts?: Record, ) => { const { vstorageClient } = tools; const instances = Object.fromEntries( @@ -118,15 +121,7 @@ export const commonSetup = async (t: ExecutionContext) => { return t.log('Contract found. Skipping installation...'); } t.log('bundle and install contract', contractName); - - const formattedOpts = builderOpts - ? objectMap( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - builderOpts as Record, - value => (typeof value === 'string' ? value : JSON.stringify(value)), - ) - : undefined; - await deployBuilder(contractBuilder, formattedOpts); + await deployBuilder(contractBuilder, builderOpts); await retryUntilCondition( () => vstorageClient.queryData(`published.agoricNames.instance`), res => contractName in Object.fromEntries(res), @@ -145,6 +140,7 @@ export const commonSetup = async (t: ExecutionContext) => { startContract, assetInfo, chainInfo, + commonBuilderOpts, faucetTools, }; }; From e124d855ffa6c659c50330aa4953500411d676bf Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Thu, 5 Dec 2024 00:03:16 -0500 Subject: [PATCH 5/9] chore: `flags` supports mutli option --- multichain-testing/tools/agd-lib.js | 9 +++++++-- multichain-testing/tools/deploy.ts | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/multichain-testing/tools/agd-lib.js b/multichain-testing/tools/agd-lib.js index a28d408493b..71ed0f69cbd 100644 --- a/multichain-testing/tools/agd-lib.js +++ b/multichain-testing/tools/agd-lib.js @@ -16,7 +16,7 @@ const binaryArgs = [ ]; /** - * @param {Record} record - e.g. { color: 'blue' } + * @param {Record} record - e.g. { color: 'blue' } * @returns {string[]} - e.g. ['--color', 'blue'] */ export const flags = record => { @@ -25,7 +25,12 @@ export const flags = record => { /** @type {[string, string][]} */ // @ts-expect-error undefined is filtered out const skipUndef = Object.entries(record).filter(([_k, v]) => v !== undefined); - return skipUndef.map(([k, v]) => [`--${k}`, v]).flat(); + return skipUndef.flatMap(([key, value]) => { + if (Array.isArray(value)) { + return value.flatMap(v => [`--${key}`, v]); + } + return [`--${key}`, value]; + }); }; /** diff --git a/multichain-testing/tools/deploy.ts b/multichain-testing/tools/deploy.ts index 52a460f6094..289c4d971e9 100755 --- a/multichain-testing/tools/deploy.ts +++ b/multichain-testing/tools/deploy.ts @@ -13,7 +13,7 @@ export const makeDeployBuilder = ( ) => async function deployBuilder( builder: string, - builderOpts?: Record, + builderOpts?: Record, ) { console.log(`building plan: ${builder}`); const args = ['run', builder]; From e60589f52358441642bb879747d208172d88da15 Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Tue, 10 Dec 2024 16:18:11 -0500 Subject: [PATCH 6/9] chore: ensure latest view on vbank entries - in the testing environment, we might see multiple USDC entries in `vbankAsset`. This change ensures the `byName` record contains the values needed for the test, reliant on the consistent ordering currently present in `vbankAsset` entries --- multichain-testing/tools/e2e-tools.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/multichain-testing/tools/e2e-tools.js b/multichain-testing/tools/e2e-tools.js index b2a77f1a4fb..6ad5575acb7 100644 --- a/multichain-testing/tools/e2e-tools.js +++ b/multichain-testing/tools/e2e-tools.js @@ -144,9 +144,9 @@ export const provisionSmartWallet = async ( // TODO: skip this query if balances is {} const vbankEntries = await q.queryData('published.agoricNames.vbankAsset'); const byName = Object.fromEntries( - vbankEntries.map(([denom, info]) => { - /// XXX better way to filter out old ATOM denom? - if (denom === 'ibc/toyatom') return [undefined, undefined]; + // reverse entries, so we get the latest view on the denom since there are + // multiple entries in the testing environment + [...vbankEntries].reverse().map(([_, info]) => { return [info.issuerName, info]; }), ); From 46f574333a6f25ed92340f22827fceefba72cdca Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Wed, 4 Dec 2024 00:33:26 -0500 Subject: [PATCH 7/9] test: fast-usdc advance happy path --- multichain-testing/test/fast-usdc/config.ts | 28 ++ .../test/fast-usdc/fast-usdc.test.ts | 386 ++++++++++++++++++ multichain-testing/tools/noble-tools.ts | 23 +- multichain-testing/tools/purse.ts | 10 + multichain-testing/tools/random.ts | 11 + multichain-testing/yarn.lock | 47 ++- 6 files changed, 494 insertions(+), 11 deletions(-) create mode 100644 multichain-testing/test/fast-usdc/config.ts create mode 100644 multichain-testing/test/fast-usdc/fast-usdc.test.ts create mode 100644 multichain-testing/tools/purse.ts create mode 100644 multichain-testing/tools/random.ts diff --git a/multichain-testing/test/fast-usdc/config.ts b/multichain-testing/test/fast-usdc/config.ts new file mode 100644 index 00000000000..c7b1833ec6d --- /dev/null +++ b/multichain-testing/test/fast-usdc/config.ts @@ -0,0 +1,28 @@ +import type { IBCChannelID } from '@agoric/vats'; + +export const oracleMnemonics = { + oracle1: + 'cause eight cattle slot course mail more aware vapor slab hobby match', + oracle2: + 'flower salute inspire label latin cattle believe sausage match total bless refuse', + oracle3: + 'surge magnet typical drive cement artist stay latin chief obey word always', +}; +harden(oracleMnemonics); + +export const makeFeedPolicy = (nobleAgoricChannelId: IBCChannelID) => { + return { + nobleAgoricChannelId, + nobleDomainId: 4, + chainPolicies: { + Arbitrum: { + attenuatedCttpBridgeAddress: + '0xe298b93ffB5eA1FB628e0C0D55A43aeaC268e347', + cctpTokenMessengerAddress: '0x19330d10D9Cc8751218eaf51E8885D058642E08A', + chainId: 42161, + confirmations: 2, + }, + }, + }; +}; +harden(makeFeedPolicy); diff --git a/multichain-testing/test/fast-usdc/fast-usdc.test.ts b/multichain-testing/test/fast-usdc/fast-usdc.test.ts new file mode 100644 index 00000000000..1c169197cb0 --- /dev/null +++ b/multichain-testing/test/fast-usdc/fast-usdc.test.ts @@ -0,0 +1,386 @@ +import anyTest from '@endo/ses-ava/prepare-endo.js'; +import type { TestFn } from 'ava'; +import { encodeAddressHook } from '@agoric/cosmic-proto/address-hooks.js'; +import { AmountMath } from '@agoric/ertp'; +import type { Denom } from '@agoric/orchestration'; +import { divideBy, multiplyBy } from '@agoric/zoe/src/contractSupport/ratio.js'; +import type { IBCChannelID } from '@agoric/vats'; +import { makeDoOffer, type WalletDriver } from '../../tools/e2e-tools.js'; +import { makeDenomTools } from '../../tools/asset-info.js'; +import { createWallet } from '../../tools/wallet.js'; +import { makeQueryClient } from '../../tools/query.js'; +import { commonSetup, type SetupContextWithWallets } from '../support.js'; +import { makeFeedPolicy, oracleMnemonics } from './config.js'; +import { makeRandomDigits } from '../../tools/random.js'; +import { balancesFromPurses } from '../../tools/purse.js'; + +const { keys, values, fromEntries } = Object; +const { isGTE, isEmpty, make } = AmountMath; + +const makeRandomNumber = () => Math.random(); + +const test = anyTest as TestFn< + SetupContextWithWallets & { + lpUser: WalletDriver; + oracleWds: WalletDriver[]; + nobleAgoricChannelId: IBCChannelID; + usdcOnOsmosis: Denom; + /** usdc on agoric */ + usdcDenom: Denom; + } +>; + +const accounts = [...keys(oracleMnemonics), 'lp']; +const contractName = 'fastUsdc'; +const contractBuilder = + '../packages/builders/scripts/fast-usdc/init-fast-usdc.js'; +const LP_DEPOSIT_AMOUNT = 10_000_000n; + +test.before(async t => { + const { setupTestKeys, ...common } = await commonSetup(t); + const { + chainInfo, + commonBuilderOpts, + deleteTestKeys, + faucetTools, + provisionSmartWallet, + startContract, + } = common; + deleteTestKeys(accounts).catch(); + const wallets = await setupTestKeys(accounts, values(oracleMnemonics)); + + // provision oracle wallets first so invitation deposits don't fail + const oracleWdPs = keys(oracleMnemonics).map(n => + provisionSmartWallet(wallets[n], { + BLD: 100n, + }), + ); + // execute sequentially, to avoid "published.wallet.${addr}.current: fetch failed" + const oracleWds: WalletDriver[] = []; + for (const p of oracleWdPs) { + const wd = await p; + oracleWds.push(wd); + } + + // calculate denomHash and channelId for privateArgs / builder opts + const { getTransferChannelId, toDenomHash } = makeDenomTools(chainInfo); + const usdcDenom = toDenomHash('uusdc', 'noblelocal', 'agoric'); + const usdcOnOsmosis = toDenomHash('uusdc', 'noblelocal', 'osmosis'); + const nobleAgoricChannelId = getTransferChannelId('agoriclocal', 'noble'); + if (!nobleAgoricChannelId) throw new Error('nobleAgoricChannelId not found'); + t.log('nobleAgoricChannelId', nobleAgoricChannelId); + t.log('usdcDenom', usdcDenom); + + await startContract(contractName, contractBuilder, { + oracle: keys(oracleMnemonics).map(n => `${n}:${wallets[n]}`), + usdcDenom, + feedPolicy: JSON.stringify(makeFeedPolicy(nobleAgoricChannelId)), + ...commonBuilderOpts, + }); + + // provide faucet funds for LPs + await faucetTools.fundFaucet([['noble', 'uusdc']]); + + // save an LP in test context + const lpUser = await provisionSmartWallet(wallets['lp'], { + USDC: 100n, + BLD: 100n, + }); + + t.context = { + ...common, + lpUser, + oracleWds, + nobleAgoricChannelId, + usdcOnOsmosis, + usdcDenom, + wallets, + }; +}); + +test.after(async t => { + const { deleteTestKeys } = t.context; + deleteTestKeys(accounts); +}); + +const toOracleOfferId = (idx: number) => `oracle${idx + 1}-accept`; + +test.serial('oracles accept', async t => { + const { oracleWds, retryUntilCondition, vstorageClient, wallets } = t.context; + + const instances = await vstorageClient.queryData( + 'published.agoricNames.instance', + ); + const instance = fromEntries(instances)[contractName]; + + // accept oracle operator invitations + await Promise.all( + oracleWds.map(makeDoOffer).map((doOffer, i) => + doOffer({ + id: toOracleOfferId(i), + invitationSpec: { + source: 'purse', + instance, + description: 'oracle operator invitation', // TODO export/import INVITATION_MAKERS_DESC + }, + proposal: {}, + }), + ), + ); + + for (const name of keys(oracleMnemonics)) { + const addr = wallets[name]; + await t.notThrowsAsync(() => + retryUntilCondition( + () => vstorageClient.queryData(`published.wallet.${addr}.current`), + ({ offerToUsedInvitation }) => { + return offerToUsedInvitation[0][0] === `${name}-accept`; + }, + `${name} invitation used`, + ), + ); + } +}); + +test.serial('lp deposits', async t => { + const { lpUser, retryUntilCondition, vstorageClient, wallets } = t.context; + + const lpDoOffer = makeDoOffer(lpUser); + const brands = await vstorageClient.queryData('published.agoricNames.brand'); + const { USDC, FastLP } = Object.fromEntries(brands); + + const usdcToGive = make(USDC, LP_DEPOSIT_AMOUNT); + + const { shareWorth: currShareWorth } = await vstorageClient.queryData( + `published.${contractName}.poolMetrics`, + ); + const poolSharesWanted = divideBy(usdcToGive, currShareWorth); + + await lpDoOffer({ + id: `lp-deposit-${Date.now()}`, + invitationSpec: { + source: 'agoricContract', + instancePath: [contractName], + callPipe: [['makeDepositInvitation']], + }, + proposal: { + give: { USDC: usdcToGive }, + want: { PoolShare: poolSharesWanted }, + }, + }); + + await t.notThrowsAsync(() => + retryUntilCondition( + () => vstorageClient.queryData(`published.${contractName}.poolMetrics`), + ({ shareWorth }) => + !isGTE(currShareWorth.numerator, shareWorth.numerator), + 'share worth numerator increases from deposit', + ), + ); + + await t.notThrowsAsync(() => + retryUntilCondition( + () => + vstorageClient.queryData(`published.wallet.${wallets['lp']}.current`), + ({ purses }) => { + const currentPoolShares = balancesFromPurses(purses)[FastLP]; + return currentPoolShares && isGTE(currentPoolShares, poolSharesWanted); + }, + 'lp has pool shares', + ), + ); +}); + +test.serial('advance and settlement', async t => { + const { + nobleTools, + nobleAgoricChannelId, + oracleWds, + retryUntilCondition, + useChain, + usdcOnOsmosis, + vstorageClient, + } = t.context; + + // EUD wallet on osmosis + const eudWallet = await createWallet(useChain('osmosis').chain.bech32_prefix); + const EUD = (await eudWallet.getAccounts())[0].address; + + // parameterize agoric address + const { settlementAccount } = await vstorageClient.queryData( + `published.${contractName}`, + ); + t.log('settlementAccount address', settlementAccount); + + const recipientAddress = encodeAddressHook(settlementAccount, { EUD }); + t.log('recipientAddress', recipientAddress); + + // register forwarding address on noble + const txRes = nobleTools.registerForwardingAcct( + nobleAgoricChannelId, + recipientAddress, + ); + t.is(txRes?.code, 0, 'registered forwarding account'); + + const { address: userForwardingAddr } = nobleTools.queryForwardingAddress( + nobleAgoricChannelId, + recipientAddress, + ); + t.log('got forwardingAddress', userForwardingAddr); + + const mintAmount = 800_000n; + + // TODO export CctpTxEvidence type + const evidence = harden({ + blockHash: + '0x90d7343e04f8160892e94f02d6a9b9f255663ed0ac34caca98544c8143fee665', + blockNumber: 21037663n, + txHash: `0xc81bc6105b60a234c7c50ac17816ebcd5561d366df8bf3be59ff3875527617${makeRandomDigits(makeRandomNumber(), 2n)}`, + tx: { + amount: mintAmount, + forwardingAddress: userForwardingAddr, + }, + aux: { + forwardingChannel: nobleAgoricChannelId, + recipientAddress, + }, + chainId: 42161, + }); + + console.log('User initiates evm mint', evidence.txHash); + + // submit evidences + await Promise.all( + oracleWds.map(makeDoOffer).map((doOffer, i) => + doOffer({ + id: `${Date.now()}-evm-evidence`, + invitationSpec: { + source: 'continuing', + previousOffer: toOracleOfferId(i), + invitationMakerName: 'SubmitEvidence', + invitationArgs: [evidence], + }, + proposal: {}, + }), + ), + ); + + const queryClient = makeQueryClient( + await useChain('osmosis').getRestEndpoint(), + ); + + await t.notThrowsAsync(() => + retryUntilCondition( + () => queryClient.queryBalance(EUD, usdcOnOsmosis), + ({ balance }) => !!balance?.amount && BigInt(balance.amount) < mintAmount, + `${EUD} advance available from fast-usdc`, + { + // this resolves quickly, so _decrease_ the interval so the timing is more apparent + retryIntervalMs: 500, + }, + ), + ); + + const queryTxStatus = async () => + vstorageClient.queryData( + `published.${contractName}.status.${evidence.txHash}`, + ); + + const assertTxStatus = async (status: string) => + t.notThrowsAsync(() => + retryUntilCondition( + () => queryTxStatus(), + txStatus => { + console.log('tx status', txStatus); + return txStatus === status; + }, + `${evidence.txHash} is ${status}`, + ), + ); + + await assertTxStatus('ADVANCED'); + console.log('Advance completed, waiting for mint...'); + + nobleTools.mockCctpMint(mintAmount, userForwardingAddr); + await t.notThrowsAsync(() => + retryUntilCondition( + () => vstorageClient.queryData(`published.${contractName}.poolMetrics`), + ({ encumberedBalance }) => + encumberedBalance && isEmpty(encumberedBalance), + 'encumberedBalance returns to 0', + ), + ); + + await assertTxStatus('DISBURSED'); +}); + +test.serial('lp withdraws', async t => { + const { + lpUser, + retryUntilCondition, + useChain, + usdcDenom, + vstorageClient, + wallets, + } = t.context; + const queryClient = makeQueryClient( + await useChain('agoric').getRestEndpoint(), + ); + const lpDoOffer = makeDoOffer(lpUser); + const brands = await vstorageClient.queryData('published.agoricNames.brand'); + const { FastLP } = Object.fromEntries(brands); + t.log('FastLP brand', FastLP); + + const { shareWorth: currShareWorth } = await vstorageClient.queryData( + `published.${contractName}.poolMetrics`, + ); + const { purses } = await vstorageClient.queryData( + `published.wallet.${wallets['lp']}.current`, + ); + const currentPoolShares = balancesFromPurses(purses)[FastLP]; + t.log('currentPoolShares', currentPoolShares); + const usdcWanted = multiplyBy(currentPoolShares, currShareWorth); + t.log('usdcWanted', usdcWanted); + + const { balance: currentUSDCBalance } = await queryClient.queryBalance( + wallets['lp'], + usdcDenom, + ); + t.log(`current ${usdcDenom} balance`, currentUSDCBalance); + + await lpDoOffer({ + id: `lp-withdraw-${Date.now()}`, + invitationSpec: { + source: 'agoricContract', + instancePath: [contractName], + callPipe: [['makeWithdrawInvitation']], + }, + proposal: { + give: { PoolShare: currentPoolShares }, + want: { USDC: usdcWanted }, + }, + }); + + await t.notThrowsAsync(() => + retryUntilCondition( + () => + vstorageClient.queryData(`published.wallet.${wallets['lp']}.current`), + ({ purses }) => { + const currentPoolShares = balancesFromPurses(purses)[FastLP]; + return !currentPoolShares || isEmpty(currentPoolShares); + }, + 'lp no longer has pool shares', + ), + ); + + await t.notThrowsAsync(() => + retryUntilCondition( + () => queryClient.queryBalance(wallets['lp'], usdcDenom), + ({ balance }) => + !!balance?.amount && + BigInt(balance.amount) - BigInt(currentUSDCBalance!.amount!) > + LP_DEPOSIT_AMOUNT, + "lp's USDC balance increases", + ), + ); +}); diff --git a/multichain-testing/tools/noble-tools.ts b/multichain-testing/tools/noble-tools.ts index 0ece8dfd8b8..8a08e6a85bb 100644 --- a/multichain-testing/tools/noble-tools.ts +++ b/multichain-testing/tools/noble-tools.ts @@ -19,11 +19,14 @@ const makeKubeArgs = () => { ]; }; -export const makeNobleTools = ({ - execFileSync, -}: { - execFileSync: ExecSync; -}) => { +export const makeNobleTools = ( + { + execFileSync, + }: { + execFileSync: ExecSync; + }, + log: (...args: unknown[]) => void = console.log, +) => { const exec = ( args: string[], opts = { encoding: 'utf-8' as const, stdio: ['ignore', 'pipe', 'ignore'] }, @@ -38,8 +41,9 @@ export const makeNobleTools = ({ const registerForwardingAcct = ( channelId: IBCChannelID, address: ChainAddress['value'], - ) => { + ): { txhash: string; code: number; data: string; height: string } => { checkEnv(); + log('creating forwarding address', address, channelId); return JSON.parse( exec([ 'tx', @@ -57,6 +61,8 @@ export const makeNobleTools = ({ const mockCctpMint = (amount: bigint, destination: ChainAddress['value']) => { checkEnv(); + const denomAmount = `${Number(amount)}uusdc`; + log('mock cctp mint', destination, denomAmount); return JSON.parse( exec([ 'tx', @@ -64,7 +70,7 @@ export const makeNobleTools = ({ 'send', 'faucet', destination, - `${Number(amount)}uusdc`, + denomAmount, '--from=faucet', '-y', '-b', @@ -76,8 +82,9 @@ export const makeNobleTools = ({ const queryForwardingAddress = ( channelId: IBCChannelID, address: ChainAddress['value'], - ) => { + ): { address: string; exists: boolean } => { checkEnv(); + log('querying forwarding address', address, channelId); return JSON.parse( exec([ 'query', diff --git a/multichain-testing/tools/purse.ts b/multichain-testing/tools/purse.ts new file mode 100644 index 00000000000..82a76d2a3f4 --- /dev/null +++ b/multichain-testing/tools/purse.ts @@ -0,0 +1,10 @@ +import type { Amount, Brand } from '@agoric/ertp'; +const { fromEntries } = Object; + +// @ts-expect-error Type 'Brand' does not satisfy the constraint 'string | number | symbol' +type BrandToBalance = Record; + +export const balancesFromPurses = ( + purses: { balance: Amount; brand: Brand }[], +): BrandToBalance => + fromEntries(purses.map(({ balance, brand }) => [brand, balance])); diff --git a/multichain-testing/tools/random.ts b/multichain-testing/tools/random.ts new file mode 100644 index 00000000000..1928bb1ebd3 --- /dev/null +++ b/multichain-testing/tools/random.ts @@ -0,0 +1,11 @@ +/** + * @param randomN pseudorandom number between 0 and 1, e.g. Math.random() + * @param digits number of digits to generate + * @returns a string of digits + */ +export function makeRandomDigits(randomN: number, digits = 2n) { + if (digits < 1n) throw new Error('digits must be positive'); + const maxValue = Math.pow(10, Number(digits)) - 1; + const num = Math.floor(randomN * (maxValue + 1)); + return num.toString().padStart(Number(digits), '0'); +} diff --git a/multichain-testing/yarn.lock b/multichain-testing/yarn.lock index 66b1b0557dc..fd3e9510370 100644 --- a/multichain-testing/yarn.lock +++ b/multichain-testing/yarn.lock @@ -6,12 +6,14 @@ __metadata: cacheKey: 10c0 "@agoric/cosmic-proto@npm:dev": - version: 0.4.1-dev-e596a01.0 - resolution: "@agoric/cosmic-proto@npm:0.4.1-dev-e596a01.0" + version: 0.4.1-dev-bdf5c17.0 + resolution: "@agoric/cosmic-proto@npm:0.4.1-dev-bdf5c17.0" dependencies: "@endo/base64": "npm:^1.0.9" "@endo/init": "npm:^1.1.7" - checksum: 10c0/2048e794ec9a346fb3a618b1b64d54985241967930b8b34c9220316b206fca4d3ecdf738e23e56021d45c3818f4513842e6d4c4d917a537dad59c13651d0ae35 + bech32: "npm:^2.0.0" + query-string: "npm:^9.1.1" + checksum: 10c0/20d4f8763a091b0b741c754fcceb82d666c4eb55bab2eaaef8821f8f7da644e2ee70c1134ef0e1cf90cc940150d61437d935913549d0da8ea17a8f0c80f2d36c languageName: node linkType: hard @@ -1028,6 +1030,13 @@ __metadata: languageName: node linkType: hard +"bech32@npm:^2.0.0": + version: 2.0.0 + resolution: "bech32@npm:2.0.0" + checksum: 10c0/45e7cc62758c9b26c05161b4483f40ea534437cf68ef785abadc5b62a2611319b878fef4f86ddc14854f183b645917a19addebc9573ab890e19194bc8f521942 + languageName: node + linkType: hard + "bfs-path@npm:^1.0.2": version: 1.0.2 resolution: "bfs-path@npm:1.0.2" @@ -1369,6 +1378,13 @@ __metadata: languageName: node linkType: hard +"decode-uri-component@npm:^0.4.1": + version: 0.4.1 + resolution: "decode-uri-component@npm:0.4.1" + checksum: 10c0/a180bbdb5398ec8270d236a3ac07cb988bbf6097428481780b85840f088951dc0318a8d8f9d56796e1a322b55b29859cea29982f22f9b03af0bc60974c54e591 + languageName: node + linkType: hard + "deep-is@npm:^0.1.3": version: 0.1.4 resolution: "deep-is@npm:0.1.4" @@ -1764,6 +1780,13 @@ __metadata: languageName: node linkType: hard +"filter-obj@npm:^5.1.0": + version: 5.1.0 + resolution: "filter-obj@npm:5.1.0" + checksum: 10c0/716e8ad2bc352e206556b3e5695b3cdff8aab80c53ea4b00c96315bbf467b987df3640575100aef8b84e812cf5ea4251db4cd672bbe33b1e78afea88400c67dd + languageName: node + linkType: hard + "find-up-simple@npm:^1.0.0": version: 1.0.0 resolution: "find-up-simple@npm:1.0.0" @@ -2924,6 +2947,17 @@ __metadata: languageName: node linkType: hard +"query-string@npm:^9.1.1": + version: 9.1.1 + resolution: "query-string@npm:9.1.1" + dependencies: + decode-uri-component: "npm:^0.4.1" + filter-obj: "npm:^5.1.0" + split-on-first: "npm:^3.0.0" + checksum: 10c0/16481f17754f660aec3cae7abb838a70e383dfcf152414d184e0d0f81fae426acf112b4d51bf754f9c256eaf83ba4241241ba907c8d58b6ed9704425e1712e8c + languageName: node + linkType: hard + "queue-microtask@npm:^1.2.2": version: 1.2.3 resolution: "queue-microtask@npm:1.2.3" @@ -3183,6 +3217,13 @@ __metadata: languageName: node linkType: hard +"split-on-first@npm:^3.0.0": + version: 3.0.0 + resolution: "split-on-first@npm:3.0.0" + checksum: 10c0/a1262eae12b68de235e1a08e011bf5b42c42621985ddf807e6221fb1e2b3304824913ae7019f18436b96b8fab8aef5f1ad80dedd2385317fdc51b521c3882cd0 + languageName: node + linkType: hard + "sprintf-js@npm:~1.0.2": version: 1.0.3 resolution: "sprintf-js@npm:1.0.3" From 2c4d7cd77a3113f1a43df536c001e1e2ab6b9180 Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Wed, 11 Dec 2024 13:34:01 -0500 Subject: [PATCH 8/9] chore: `await` `deleteTestKeys` - get rid of dangling promise and await something we actually rely on --- multichain-testing/test/account-balance-queries.test.ts | 2 +- multichain-testing/test/auto-stake-it.test.ts | 2 +- multichain-testing/test/basic-flows.test.ts | 2 +- multichain-testing/test/chain-queries.test.ts | 2 +- multichain-testing/test/deposit-withdraw-lca.test.ts | 2 +- multichain-testing/test/deposit-withdraw-portfolio.test.ts | 2 +- multichain-testing/test/fast-usdc/fast-usdc.test.ts | 2 +- multichain-testing/test/ica-channel-close.test.ts | 2 +- multichain-testing/test/send-anywhere.test.ts | 2 +- multichain-testing/test/stake-ica.test.ts | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/multichain-testing/test/account-balance-queries.test.ts b/multichain-testing/test/account-balance-queries.test.ts index 6707d9b102e..c067ced9446 100644 --- a/multichain-testing/test/account-balance-queries.test.ts +++ b/multichain-testing/test/account-balance-queries.test.ts @@ -20,7 +20,7 @@ const contractBuilder = test.before(async t => { const { deleteTestKeys, setupTestKeys, ...rest } = await commonSetup(t); - deleteTestKeys(accounts).catch(); + await deleteTestKeys(accounts).catch(); const wallets = await setupTestKeys(accounts); t.context = { ...rest, wallets, deleteTestKeys }; const { startContract } = rest; diff --git a/multichain-testing/test/auto-stake-it.test.ts b/multichain-testing/test/auto-stake-it.test.ts index 01afae775f2..10e3bac8db8 100644 --- a/multichain-testing/test/auto-stake-it.test.ts +++ b/multichain-testing/test/auto-stake-it.test.ts @@ -19,7 +19,7 @@ const contractBuilder = test.before(async t => { const { setupTestKeys, ...common } = await commonSetup(t); const { commonBuilderOpts, deleteTestKeys, startContract } = common; - deleteTestKeys(accounts).catch(); + await deleteTestKeys(accounts).catch(); const wallets = await setupTestKeys(accounts); t.context = { ...common, wallets }; await startContract(contractName, contractBuilder, commonBuilderOpts); diff --git a/multichain-testing/test/basic-flows.test.ts b/multichain-testing/test/basic-flows.test.ts index 9e9ff738a4d..1d53b4aae8d 100644 --- a/multichain-testing/test/basic-flows.test.ts +++ b/multichain-testing/test/basic-flows.test.ts @@ -18,7 +18,7 @@ const contractBuilder = test.before(async t => { const { setupTestKeys, ...common } = await commonSetup(t); const { commonBuilderOpts, deleteTestKeys, startContract } = common; - deleteTestKeys(accounts).catch(); + await deleteTestKeys(accounts).catch(); const wallets = await setupTestKeys(accounts); t.context = { ...common, wallets }; await startContract(contractName, contractBuilder, commonBuilderOpts); diff --git a/multichain-testing/test/chain-queries.test.ts b/multichain-testing/test/chain-queries.test.ts index a9b91c97fe3..56673742582 100644 --- a/multichain-testing/test/chain-queries.test.ts +++ b/multichain-testing/test/chain-queries.test.ts @@ -28,7 +28,7 @@ const contractBuilder = test.before(async t => { const { deleteTestKeys, setupTestKeys, ...rest } = await commonSetup(t); - deleteTestKeys(accounts).catch(); + await deleteTestKeys(accounts).catch(); const wallets = await setupTestKeys(accounts); t.context = { ...rest, wallets, deleteTestKeys }; const { startContract } = rest; diff --git a/multichain-testing/test/deposit-withdraw-lca.test.ts b/multichain-testing/test/deposit-withdraw-lca.test.ts index e9127f3919a..7d1cbafa97c 100644 --- a/multichain-testing/test/deposit-withdraw-lca.test.ts +++ b/multichain-testing/test/deposit-withdraw-lca.test.ts @@ -16,7 +16,7 @@ const contractBuilder = test.before(async t => { const { setupTestKeys, ...common } = await commonSetup(t); const { commonBuilderOpts, deleteTestKeys, startContract } = common; - deleteTestKeys(accounts).catch(); + await deleteTestKeys(accounts).catch(); const wallets = await setupTestKeys(accounts); t.context = { ...common, wallets }; await startContract(contractName, contractBuilder, commonBuilderOpts); diff --git a/multichain-testing/test/deposit-withdraw-portfolio.test.ts b/multichain-testing/test/deposit-withdraw-portfolio.test.ts index 0bccf0bad60..49b7f3d8ad7 100644 --- a/multichain-testing/test/deposit-withdraw-portfolio.test.ts +++ b/multichain-testing/test/deposit-withdraw-portfolio.test.ts @@ -16,7 +16,7 @@ const contractBuilder = test.before(async t => { const { setupTestKeys, ...common } = await commonSetup(t); const { commonBuilderOpts, deleteTestKeys, startContract } = common; - deleteTestKeys(accounts).catch(); + await deleteTestKeys(accounts).catch(); const wallets = await setupTestKeys(accounts); t.context = { ...common, wallets }; await startContract(contractName, contractBuilder, commonBuilderOpts); diff --git a/multichain-testing/test/fast-usdc/fast-usdc.test.ts b/multichain-testing/test/fast-usdc/fast-usdc.test.ts index 1c169197cb0..3b3d7f79dce 100644 --- a/multichain-testing/test/fast-usdc/fast-usdc.test.ts +++ b/multichain-testing/test/fast-usdc/fast-usdc.test.ts @@ -46,7 +46,7 @@ test.before(async t => { provisionSmartWallet, startContract, } = common; - deleteTestKeys(accounts).catch(); + await deleteTestKeys(accounts).catch(); const wallets = await setupTestKeys(accounts, values(oracleMnemonics)); // provision oracle wallets first so invitation deposits don't fail diff --git a/multichain-testing/test/ica-channel-close.test.ts b/multichain-testing/test/ica-channel-close.test.ts index f7799dac1e0..6ee59dc825f 100644 --- a/multichain-testing/test/ica-channel-close.test.ts +++ b/multichain-testing/test/ica-channel-close.test.ts @@ -24,7 +24,7 @@ const contractBuilder = test.before(async t => { const { setupTestKeys, ...common } = await commonSetup(t); const { commonBuilderOpts, deleteTestKeys, startContract } = common; - deleteTestKeys(accounts).catch(); + await deleteTestKeys(accounts).catch(); const wallets = await setupTestKeys(accounts); t.context = { ...common, wallets }; await startContract(contractName, contractBuilder, commonBuilderOpts); diff --git a/multichain-testing/test/send-anywhere.test.ts b/multichain-testing/test/send-anywhere.test.ts index c33084f4eff..df4bbdca8d6 100644 --- a/multichain-testing/test/send-anywhere.test.ts +++ b/multichain-testing/test/send-anywhere.test.ts @@ -23,7 +23,7 @@ test.before(async t => { const { setupTestKeys, ...common } = await commonSetup(t); const { commonBuilderOpts, deleteTestKeys, faucetTools, startContract } = common; - deleteTestKeys(accounts).catch(); + await deleteTestKeys(accounts).catch(); const wallets = await setupTestKeys(accounts); t.context = { ...common, wallets }; await startContract(contractName, contractBuilder, commonBuilderOpts); diff --git a/multichain-testing/test/stake-ica.test.ts b/multichain-testing/test/stake-ica.test.ts index af73531bf61..7ef65ad33eb 100644 --- a/multichain-testing/test/stake-ica.test.ts +++ b/multichain-testing/test/stake-ica.test.ts @@ -18,7 +18,7 @@ test.before(async t => { const { deleteTestKeys, setupTestKeys, ...rest } = await commonSetup(t); // XXX not necessary for CI, but helpful for unexpected failures in // active development (test.after cleanup doesn't run). - deleteTestKeys(accounts).catch(); + await deleteTestKeys(accounts).catch(); const wallets = await setupTestKeys(accounts); t.context = { ...rest, wallets, deleteTestKeys }; }); From a56f407129e73cc57e024f9abd758825e44b5569 Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Wed, 11 Dec 2024 14:36:36 -0500 Subject: [PATCH 9/9] chore: ava `failFast: true` --- multichain-testing/ava.main.config.js | 1 + 1 file changed, 1 insertion(+) diff --git a/multichain-testing/ava.main.config.js b/multichain-testing/ava.main.config.js index 37c7ca0b467..da0d373e10c 100644 --- a/multichain-testing/ava.main.config.js +++ b/multichain-testing/ava.main.config.js @@ -9,4 +9,5 @@ export default { concurrency: 1, serial: true, timeout: '125s', + failFast: true, };