From 16585a30e0b4b7db62905cd1b91b648be2ccb85b Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Fri, 17 Jan 2025 17:00:23 -0600 Subject: [PATCH 1/5] feat(client-utils): makeIntervalIterable, makeBlocksIterable --- packages/client-utils/src/clock-timer.js | 50 ++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 packages/client-utils/src/clock-timer.js diff --git a/packages/client-utils/src/clock-timer.js b/packages/client-utils/src/clock-timer.js new file mode 100644 index 00000000000..a0e32f37fac --- /dev/null +++ b/packages/client-utils/src/clock-timer.js @@ -0,0 +1,50 @@ +const { freeze } = Object; + +/** + * @typedef {object} IntervalIO + * @property {typeof setTimeout} setTimeout + * @property {typeof clearTimeout} clearTimeout + * @property {typeof Date.now} now + */ + +/** + * Creates an async iterable that emits values at specified intervals. + * @param {number} intervalMs - The interval duration in milliseconds. + * @param {IntervalIO} io + * @returns {{ + * [Symbol.asyncIterator]: () => AsyncGenerator + * }} + */ +export const makeIntervalIterable = ( + intervalMs, + { setTimeout, clearTimeout, now }, +) => { + const self = freeze({ + /** + * The async generator implementation. + * @returns {AsyncGenerator} + */ + async *[Symbol.asyncIterator]() { + let timeoutId; + /** @type {undefined | ((x: number) => void) } */ + let resolveNext; + + await null; + try { + for (;;) { + timeoutId = setTimeout(() => { + // Promise.withResovers() would obviate this check + if (resolveNext) { + resolveNext(now()); + } + }, intervalMs); + yield await new Promise(resolve => (resolveNext = resolve)); + } + } finally { + // Ensure cleanup on completion + clearTimeout(timeoutId); + } + }, + }); + return self; +}; From 25e0ce1c73e6d912eb0f3e6b7bf761d66568497e Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Fri, 17 Jan 2025 17:00:36 -0600 Subject: [PATCH 2/5] feat(multichain/tools): executeOfferTx does not wait for completion - wdr.deposit.getAddress() - agd types --- multichain-testing/tools/agd-lib.js | 2 + multichain-testing/tools/block-iter.js | 59 ++++++++++++++++++++++++++ multichain-testing/tools/e2e-tools.js | 9 +++- multichain-testing/tools/query.ts | 11 +++++ 4 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 multichain-testing/tools/block-iter.js diff --git a/multichain-testing/tools/agd-lib.js b/multichain-testing/tools/agd-lib.js index 71ed0f69cbd..53afbea5e44 100644 --- a/multichain-testing/tools/agd-lib.js +++ b/multichain-testing/tools/agd-lib.js @@ -139,6 +139,8 @@ export const makeAgd = ({ execFileSync }) => { console.log('$$$ agd', ...args); const out = exec(args, { stdio: ['ignore', 'pipe', 'ignore'] }); try { + // XXX approximate type + /** @type {{ height: string, txhash: string, code: number, codespace: string, raw_log: string }} */ const detail = JSON.parse(out); if (detail.code !== 0) { throw Error(detail.raw_log); diff --git a/multichain-testing/tools/block-iter.js b/multichain-testing/tools/block-iter.js new file mode 100644 index 00000000000..191baac7bdc --- /dev/null +++ b/multichain-testing/tools/block-iter.js @@ -0,0 +1,59 @@ +import { makeIntervalIterable } from '@agoric/client-utils/src/clock-timer.js'; + +const { freeze } = Object; + +/** + * @import {IntervalIO} from '@agoric/client-utils/src/clock-timer.js'; + * @import {makeQueryClient} from './query.js'; + */ + +/** + * @param {ReturnType} api + * @param {number} [delta] + */ +const recentBlockRate = async (api, delta = 2) => { + /** @param {{ height: number, time: string }} header */ + const relevant = ({ height, time }) => ({ height, time }); + const { block: latest } = await api.queryBlock(); + const heightRecent = Number(latest.header.height) - delta; + const { block: recent } = await api.queryBlock(heightRecent); + const t0 = Date.parse(recent.header.time); + const t1 = Date.parse(latest.header.time); + return { + delta, + latest: { ...relevant(latest.header) }, + recent: { ...relevant(recent.header) }, + elapsed: t1 - t0, + period: (t1 - t0) / delta, + }; +}; + +/** + * Make an iterator for observing each block. + * + * We measure the block rate by observing the latest block + * and one earlier (by `delta`) block. + * + * Then we poll at 2x that rate (Nyquist frequency) and + * fire the iterator when the height changes. + * + * @param {IntervalIO & { api: ReturnType, delta?: number }} io + */ +export const makeBlocksIterable = ({ api, delta = 2, ...io }) => { + return freeze({ + async *[Symbol.asyncIterator]() { + const { period } = await recentBlockRate(api, delta); + const nyquist = period / 2; + let prev; + const ticks = makeIntervalIterable(nyquist, io); + for await (const tick of ticks) { + const { block } = await api.queryBlock(); + const current = Number(block.header.height); + if (current === prev) continue; + prev = current; + const { time } = block.header; + yield freeze({ tick, height: current, time }); + } + }, + }); +}; diff --git a/multichain-testing/tools/e2e-tools.js b/multichain-testing/tools/e2e-tools.js index f99d43e0aa4..bc46cc6fba6 100644 --- a/multichain-testing/tools/e2e-tools.js +++ b/multichain-testing/tools/e2e-tools.js @@ -14,6 +14,8 @@ import { makeTracer } from '@agoric/internal'; /** * @import {OfferSpec} from '@agoric/smart-wallet/src/offers.js'; + * @import {UpdateRecord} from '@agoric/smart-wallet/src/smartWallet.js'; + * * @import { EnglishMnemonic } from '@cosmjs/crypto'; * @import { RetryUntilCondition } from './sleep.js'; */ @@ -109,6 +111,7 @@ const installBundle = async (fullPath, opts) => { ['swingset', 'install-bundle', `@${fullPath}`, '--gas', 'auto'], { from, chainId, yes: true }, ); + assert(tx); progress({ id, installTx: tx.txhash, height: tx.height }); @@ -222,8 +225,9 @@ export const provisionSmartWallet = async ( return txInfo; }; - /** @param {import('@agoric/smart-wallet/src/offers.js').OfferSpec} offer */ + /** @param {OfferSpec} offer */ async function* executeOffer(offer) { + /** @type {AsyncGenerator} */ const updates = q.follow(`published.wallet.${address}`, { delay }); const txInfo = await sendAction({ method: 'executeOffer', offer }); console.debug('spendAction', txInfo); @@ -239,12 +243,15 @@ export const provisionSmartWallet = async ( // XXX /** @type {import('../test/wallet-tools.js').MockWallet['offers']} */ const offers = Far('Offers', { executeOffer, + /** @param {OfferSpec} offer */ + executeOfferTx: offer => sendAction({ method: 'executeOffer', offer }), /** @param {string | number} offerId */ tryExit: offerId => sendAction({ method: 'tryExitOffer', offerId }), }); // XXX /** @type {import('../test/wallet-tools.js').MockWallet['deposit']} */ const deposit = Far('DepositFacet', { + getAddress: () => address, receive: async payment => { const brand = await E(payment).getAllegedBrand(); const asset = vbankEntries.find(([_denom, a]) => a.brand === brand); diff --git a/multichain-testing/tools/query.ts b/multichain-testing/tools/query.ts index 1e4c10b13f4..b8633825ba4 100644 --- a/multichain-testing/tools/query.ts +++ b/multichain-testing/tools/query.ts @@ -10,8 +10,15 @@ import type { QueryDenomHashResponseSDKType } from '@agoric/cosmic-proto/ibc/app import type { QueryChannelResponseSDKType } from '@agoric/cosmic-proto/ibc/core/channel/v1/query.js'; import type { QueryChannelsResponseSDKType } from '@agoric/cosmic-proto/ibc/core/channel/v1/query.js'; +// TODO: export tendermint Block from @agoric/cosmic-proto +type BlockHeaderPartial = { + height: number; + time: string; +}; + // TODO use telescope generated query client from @agoric/cosmic-proto // https://github.com/Agoric/agoric-sdk/issues/9200 +// TODO: inject fetch export function makeQueryClient(apiUrl: string) { const query = async (path: string): Promise => { try { @@ -26,6 +33,10 @@ export function makeQueryClient(apiUrl: string) { return { query, + queryBlock: (height?: number) => + query<{ block: { header: BlockHeaderPartial } }>( + `/cosmos/base/tendermint/v1beta1/blocks/${height || 'latest'}`, + ), queryBalances: (address: string) => query( `/cosmos/bank/v1beta1/balances/${address}`, From b82575655bed69070bffa7fc5f2bf8be4adf3aba Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Fri, 17 Jan 2025 17:09:39 -0600 Subject: [PATCH 3/5] chore: fixup distribute: test.serial, ordering --- .../test/fast-usdc/fast-usdc.test.ts | 54 +++++++++---------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/multichain-testing/test/fast-usdc/fast-usdc.test.ts b/multichain-testing/test/fast-usdc/fast-usdc.test.ts index 2086c5324d3..f179c080756 100644 --- a/multichain-testing/test/fast-usdc/fast-usdc.test.ts +++ b/multichain-testing/test/fast-usdc/fast-usdc.test.ts @@ -423,33 +423,6 @@ const advanceAndSettleScenario = test.macro({ }, }); -test('distribute FastUSDC contract fees', async t => { - const io = t.context; - const queryClient = makeQueryClient( - await io.useChain('agoric').getRestEndpoint(), - ); - const builder = - '../packages/builders/scripts/fast-usdc/fast-usdc-fees.build.js'; - - const opts = { - destinationAddress: io.wallets['feeDest'], - feePortion: 0.25, - }; - t.log('build, run proposal to distribute fees', opts); - await io.deployBuilder(builder, { - ...opts, - feePortion: `${opts.feePortion}`, - }); - - const { balance } = await io.retryUntilCondition( - () => queryClient.queryBalance(opts.destinationAddress, io.usdcDenom), - ({ balance }) => !!balance && BigInt(balance.amount) > 0n, - `fees received at ${opts.destinationAddress}`, - ); - t.log('fees received', balance); - t.truthy(balance?.amount); -}); - test.serial(advanceAndSettleScenario, LP_DEPOSIT_AMOUNT / 4n, 'osmosis'); test.serial(advanceAndSettleScenario, LP_DEPOSIT_AMOUNT / 8n, 'noble'); test.serial(advanceAndSettleScenario, LP_DEPOSIT_AMOUNT / 5n, 'agoric'); @@ -523,6 +496,33 @@ test.serial('lp withdraws', async t => { ); }); +test.serial('distribute FastUSDC contract fees', async t => { + const io = t.context; + const queryClient = makeQueryClient( + await io.useChain('agoric').getRestEndpoint(), + ); + const builder = + '../packages/builders/scripts/fast-usdc/fast-usdc-fees.build.js'; + + const opts = { + destinationAddress: io.wallets['feeDest'], + feePortion: 0.25, + }; + t.log('build, run proposal to distribute fees', opts); + await io.deployBuilder(builder, { + ...opts, + feePortion: `${opts.feePortion}`, + }); + + const { balance } = await io.retryUntilCondition( + () => queryClient.queryBalance(opts.destinationAddress, io.usdcDenom), + ({ balance }) => !!balance && BigInt(balance.amount) > 0n, + `fees received at ${opts.destinationAddress}`, + ); + t.log('fees received', balance); + t.truthy(balance?.amount); +}); + test.todo('insufficient LP funds; forward path'); test.todo('mint while Advancing; still Disbursed'); test.todo('transfer failed (e.g. to cosmos, not in env)'); From 7917e05e9d777ec2e80cca23750cf694a2a8c4e9 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Fri, 17 Jan 2025 17:02:23 -0600 Subject: [PATCH 4/5] test(multichain): fast-usdc performance - advanceAndSettleScenario: check estimated mainnet wall-clock time - when oracles submit evidence, wait only for seated, not paid out - extract makeTxOracle - move agoricNamesQ etc. - use makeTestContext to avoid duplicating TestFn type --- .../test/fast-usdc/fast-usdc.test.ts | 214 ++++++++---------- .../test/fast-usdc/fu-actors.ts | 197 ++++++++++++++++ 2 files changed, 289 insertions(+), 122 deletions(-) create mode 100644 multichain-testing/test/fast-usdc/fu-actors.ts diff --git a/multichain-testing/test/fast-usdc/fast-usdc.test.ts b/multichain-testing/test/fast-usdc/fast-usdc.test.ts index f179c080756..4aecf11f3dc 100644 --- a/multichain-testing/test/fast-usdc/fast-usdc.test.ts +++ b/multichain-testing/test/fast-usdc/fast-usdc.test.ts @@ -7,42 +7,30 @@ import type { USDCProposalShapes } from '@agoric/fast-usdc/src/pool-share-math.j import type { CctpTxEvidence, EvmAddress, - PoolMetrics, } from '@agoric/fast-usdc/src/types.js'; import { makeTracer } from '@agoric/internal'; -import type { Denom } from '@agoric/orchestration'; -import type { CurrentWalletRecord } from '@agoric/smart-wallet/src/smartWallet.js'; -import type { IBCChannelID } from '@agoric/vats'; import { divideBy, multiplyBy } from '@agoric/zoe/src/contractSupport/ratio.js'; import type { TestFn } from 'ava'; import { makeDenomTools } from '../../tools/asset-info.js'; -import { makeDoOffer, type WalletDriver } from '../../tools/e2e-tools.js'; +import { makeBlocksIterable } from '../../tools/block-iter.js'; +import { makeDoOffer } from '../../tools/e2e-tools.js'; import { makeQueryClient } from '../../tools/query.js'; import { makeRandomDigits } from '../../tools/random.js'; import { createWallet } from '../../tools/wallet.js'; -import { commonSetup, type SetupContextWithWallets } from '../support.js'; +import { commonSetup } from '../support.js'; import { makeFeedPolicyPartial, oracleMnemonics } from './config.js'; +import { agoricNamesQ, fastLPQ, makeTxOracle } from './fu-actors.js'; const { RELAYER_TYPE } = process.env; const log = makeTracer('MCFU'); -const { keys, values, fromEntries } = Object; +const { keys, values } = Object; const { isGTE, isEmpty, make, subtract } = AmountMath; const makeRandomNumber = () => Math.random(); -const test = anyTest as TestFn< - SetupContextWithWallets & { - lpUser: WalletDriver; - feeUser: WalletDriver; - oracleWds: WalletDriver[]; - nobleAgoricChannelId: IBCChannelID; - usdcOnOsmosis: Denom; - /** usdc on agoric */ - usdcDenom: Denom; - } ->; +const test = anyTest as TestFn>>; const accounts = [...keys(oracleMnemonics), 'lp', 'feeDest']; const contractName = 'fastUsdc'; @@ -50,7 +38,7 @@ const contractBuilder = '../packages/builders/scripts/fast-usdc/start-fast-usdc.build.js'; const LP_DEPOSIT_AMOUNT = 8_000n * 10n ** 6n; -test.before(async t => { +const makeTestContext = async t => { const { setupTestKeys, ...common } = await commonSetup(t, { config: `../config.fusdc${RELAYER_TYPE ? '.' + RELAYER_TYPE : ''}.yaml`, }); @@ -61,9 +49,11 @@ test.before(async t => { faucetTools, provisionSmartWallet, startContract, + useChain, } = common; await deleteTestKeys(accounts).catch(); const wallets = await setupTestKeys(accounts, values(oracleMnemonics)); + t.log('setupTestKeys:', wallets); // provision oracle wallets first so invitation deposits don't fail const oracleWds = await Promise.all( @@ -100,82 +90,49 @@ test.before(async t => { }); const feeUser = await provisionSmartWallet(wallets['feeDest'], { BLD: 100n }); - t.context = { + const { vstorageClient } = common; + const api = makeQueryClient(await useChain('agoric').getRestEndpoint()); + const now = () => Date.now(); + const blockIter = makeBlocksIterable({ + api, + setTimeout, + clearTimeout, + now, + }); + const oKeys = keys(oracleMnemonics); + const txOracles = oracleWds.map((wd, ix) => + makeTxOracle(oKeys[ix], { wd, vstorageClient, blockIter, now }), + ); + + return { ...common, + api, lpUser, feeUser, oracleWds, + txOracles, nobleAgoricChannelId, usdcOnOsmosis, usdcDenom, wallets, }; -}); +}; +test.before(async t => (t.context = await makeTestContext(t))); test.after(async t => { const { deleteTestKeys } = t.context; deleteTestKeys(accounts); }); -type VStorageClient = Awaited>['vstorageClient']; -const agoricNamesQ = (vsc: VStorageClient) => - harden({ - brands: (_assetKind: K) => - vsc - .queryData('published.agoricNames.brand') - .then(pairs => fromEntries(pairs) as Record>), - }); -const walletQ = (vsc: VStorageClient) => { - const self = harden({ - current: (addr: string) => - vsc.queryData( - `published.wallet.${addr}.current`, - ) as Promise, - findInvitationDetail: async (addr: string, description: string) => { - const { Invitation } = await agoricNamesQ(vsc).brands('set'); - const current = await self.current(addr); - const { purses } = current; - const { value: details } = purses.find(p => p.brand === Invitation)! - .balance as Amount<'set', InvitationDetails>; - const detail = details.find(x => x.description === description); - return { current, detail }; - }, - }); - return self; -}; - -const fastLPQ = (vsc: VStorageClient) => - harden({ - metrics: () => - vsc.queryData(`published.fastUsdc.poolMetrics`) as Promise, - info: () => - vsc.queryData(`published.${contractName}`) as Promise<{ - poolAccount: string; - settlementAccount: string; - }>, - }); - -const toOracleOfferId = (idx: number) => `oracle${idx + 1}-accept`; - test.serial('oracles accept', async t => { - const { oracleWds, retryUntilCondition, vstorageClient, wallets } = t.context; - - const description = 'oracle operator invitation'; + const { txOracles, retryUntilCondition } = t.context; // ensure we have an unused (or used) oracle invitation in each purse let hasAccepted = false; - for (const name of keys(oracleMnemonics)) { - const { - current: { offerToUsedInvitation }, - detail, - } = await walletQ(vstorageClient).findInvitationDetail( - wallets[name], - description, - ); - const hasInvitation = !!detail; - const usedInvitation = offerToUsedInvitation?.[0]?.[0] === `${name}-accept`; - t.log({ name, hasInvitation, usedInvitation }); - t.true(hasInvitation || usedInvitation, 'has or accepted invitation'); + for (const op of txOracles) { + const { detail, usedInvitation } = await op.checkInvitation(); + t.log({ name: op.getName(), hasInvitation: !!detail, usedInvitation }); + t.true(!!detail || usedInvitation, 'has or accepted invitation'); if (usedInvitation) hasAccepted = true; } // if the oracles have already accepted, skip the rest of the test this is @@ -184,32 +141,14 @@ test.serial('oracles accept', async t => { if (hasAccepted) return t.pass(); // accept oracle operator invitations - const instance = fromEntries( - await vstorageClient.queryData('published.agoricNames.instance'), - )[contractName]; - await Promise.all( - oracleWds.map(makeDoOffer).map((doOffer, i) => - doOffer({ - id: toOracleOfferId(i), - invitationSpec: { - source: 'purse', - instance, - description, - }, - proposal: {}, - }), - ), - ); + await Promise.all(txOracles.map(op => op.acceptInvitation())); - for (const name of keys(oracleMnemonics)) { - const addr = wallets[name]; + for (const op of txOracles) { await t.notThrowsAsync(() => retryUntilCondition( - () => vstorageClient.queryData(`published.wallet.${addr}.current`), - ({ offerToUsedInvitation }) => { - return offerToUsedInvitation[0][0] === `${name}-accept`; - }, - `${name} invitation used`, + () => op.checkInvitation(), + ({ usedInvitation }) => !!usedInvitation, + `${op.getName()} invitation used`, { log }, ), ); @@ -274,9 +213,10 @@ const advanceAndSettleScenario = test.macro({ `advance ${mintAmt} uusdc to ${eudChain} and settle`, exec: async (t, mintAmt: bigint, eudChain: string) => { const { + api, nobleTools, nobleAgoricChannelId, - oracleWds, + txOracles, retryUntilCondition, smartWalletKit, useChain, @@ -331,23 +271,24 @@ const advanceAndSettleScenario = test.macro({ chainId: 42161, }); - log('User initiates evm mint:', evidence.txHash); + log('User initiates EVM burn:', evidence.txHash); + const { block: initialBlock } = await api.queryBlock(); + console.time(`UX->${eudChain}`); + console.timeLog( + `UX->${eudChain}`, + 'initial block', + initialBlock.header.height, + initialBlock.header.time, + ); // 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: {}, - }), - ), + txOracles.map(async o => { + const { block } = await o.submit(evidence); + console.timeLog(`UX->${eudChain}`, o.getName(), block); + }), ); + console.timeLog(`UX->${eudChain}`, 'submitted x', txOracles.length); const queryClient = makeQueryClient( await useChain(eudChain).getRestEndpoint(), @@ -366,18 +307,47 @@ const advanceAndSettleScenario = test.macro({ } }; - await t.notThrowsAsync(() => - retryUntilCondition( + let finalBlock; + await t.notThrowsAsync(async () => { + const q = await retryUntilCondition( () => queryClient.queryBalance(EUD, getUsdcDenom(eudChain)), - ({ balance }) => - !!balance?.amount && - BigInt(balance?.amount) > 0n && - BigInt(balance.amount) < mintAmt, + ({ balance }) => { + if (!balance) return false; // retry + const value = BigInt(balance.amount); + if (value >= mintAmt) { + throw Error(`no fees were deducted: ${value} >= ${mintAmt}`); + } + if (value === 0n) return false; // retry + t.log('advance done', value, 'uusdc'); + return true; + }, `${EUD} advance available from fast-usdc`, // this resolves quickly, so _decrease_ the interval so the timing is more apparent - { retryIntervalMs: 500 }, - ), - ); + { retryIntervalMs: 500, maxRetries: 20 }, + ); + console.timeLog(`UX->${eudChain}`, 'rxd', q.balance?.amount); + ({ block: finalBlock } = await api.queryBlock()); + console.timeLog( + `UX->${eudChain}`, + 'final block', + finalBlock.header.height, + finalBlock.header.time, + ); + }); + console.timeEnd(`UX->${eudChain}`); + const blockDur = + Number(finalBlock.header.height) - Number(initialBlock.header.height); + const MAIN_BLOCK_SECS = 7; + const MAIN_MAX_DUR = 120; // product requirement: 2 min + const MARGIN_OF_ERROR = 0.2; + const mainWallClockEstimate = blockDur * MAIN_BLOCK_SECS; + t.log({ + initialHeight: initialBlock.header.height, + finalHeight: finalBlock.header.height, + blockDur, + mainWallClockEstimate, + }); + t.true(mainWallClockEstimate * (1 + MARGIN_OF_ERROR) <= MAIN_MAX_DUR); const queryTxStatus = async () => { const record = await smartWalletKit.readPublished( diff --git a/multichain-testing/test/fast-usdc/fu-actors.ts b/multichain-testing/test/fast-usdc/fu-actors.ts new file mode 100644 index 00000000000..2594a288ec3 --- /dev/null +++ b/multichain-testing/test/fast-usdc/fu-actors.ts @@ -0,0 +1,197 @@ +import { encodeAddressHook } from '@agoric/cosmic-proto/address-hooks.js'; +import type { + CctpTxEvidence, + PoolMetrics, +} from '@agoric/fast-usdc/src/types.js'; +import type { + CurrentWalletRecord, + UpdateRecord, +} from '@agoric/smart-wallet/src/smartWallet.js'; +import type { IBCChannelID } from '@agoric/vats'; +import type { ExecutionContext } from 'ava'; +import { makeDoOffer, type WalletDriver } from '../../tools/e2e-tools.js'; +import type { createWallet } from '../../tools/wallet.js'; +import type { commonSetup, SetupContextWithWallets } from '../support.js'; + +const { fromEntries } = Object; + +const contractName = 'fastUsdc'; + +type VStorageClient = Awaited>['vstorageClient']; +export const agoricNamesQ = (vsc: VStorageClient) => + harden({ + brands: (_assetKind: K) => + vsc + .queryData('published.agoricNames.brand') + .then(pairs => fromEntries(pairs) as Record>), + instance: (name: string) => + vsc + .queryData('published.agoricNames.instance') + .then(pairs => fromEntries(pairs)[name] as Instance), + }); +const walletQ = (vsc: VStorageClient) => { + const self = harden({ + current: (addr: string) => + vsc.queryData( + `published.wallet.${addr}.current`, + ) as Promise, + findInvitationDetail: async (addr: string, description: string) => { + const { Invitation } = await agoricNamesQ(vsc).brands('set'); + const current = await self.current(addr); + const { purses } = current; + const { value: details } = purses.find(p => p.brand === Invitation)! + .balance as Amount<'set', InvitationDetails>; + const detail = details.find(x => x.description === description); + return { current, detail }; + }, + update: (addr: string) => + vsc.queryData(`published.wallet.${addr}`) as Promise, + }); + return self; +}; + +export const fastLPQ = (vsc: VStorageClient) => + harden({ + metrics: () => + vsc.queryData(`published.fastUsdc.poolMetrics`) as Promise, + info: () => + vsc.queryData(`published.fastUsdc`) as Promise<{ + poolAccount: string; + settlementAccount: string; + }>, + }); + +export const makeTxOracle = ( + name: string, + io: { + wd: WalletDriver; + vstorageClient: VStorageClient; + blockIter: AsyncIterable<{ height: number; time: unknown }, void, void>; + now: () => number; + }, +) => { + const { wd, vstorageClient, blockIter, now } = io; + + const description = 'oracle operator invitation'; + const address = wd.deposit.getAddress(); + const acceptOfferId = `${name}-accept`; + + const instanceP = agoricNamesQ(vstorageClient).instance(contractName); + + const doOffer = makeDoOffer(wd); + const self = harden({ + getName: () => name, + getAddress: () => address, + acceptInvitation: async () => { + const instance = await instanceP; + await doOffer({ + id: acceptOfferId, + invitationSpec: { source: 'purse', instance, description }, + proposal: {}, + }); + + for await (const block of blockIter) { + const check = await self.checkInvitation(); + if (check.usedInvitation) break; + console.log(block.height, `${name} invitation used`); + } + }, + checkInvitation: async () => { + const { + current: { offerToUsedInvitation }, + detail, + } = await walletQ(vstorageClient).findInvitationDetail( + address, + description, + ); + const usedInvitation = offerToUsedInvitation.some( + ([k, _v]) => k === `${name}-accept`, + ); + return { detail, usedInvitation }; + }, + submit: async (evidence: CctpTxEvidence) => { + const id = `${now()}-evm-evidence`; + const tx = await wd.offers.executeOfferTx({ + id, + invitationSpec: { + source: 'continuing', + previousOffer: acceptOfferId, + invitationMakerName: 'SubmitEvidence', + invitationArgs: [evidence], + }, + proposal: {}, + }); + for await (const block of blockIter) { + const update = await walletQ(vstorageClient).update(address); + if (update.updated === 'offerStatus' && update.status.id === id) { + if (update.status.error) { + throw Error(update.status.error); + } + console.log(block.height, name, 'seated', id); + return { txhash: tx?.txhash, block }; + } + console.log(block.height, name, 'not seated', id); + } + throw Error('no more blocks'); + }, + }); + return self; +}; +export type TxOracle = ReturnType; + +export const makeUserAgent = ( + my: { wallet: Awaited> }, + vstorageClient: VStorageClient, + nobleTools: SetupContextWithWallets['nobleTools'], + nobleAgoricChannelId: IBCChannelID, +) => { + const fastInfoP = fastLPQ(vstorageClient).info(); + return harden({ + makeSendTx: async ( + t: ExecutionContext, + mintAmt: bigint, + EUD: string, + chainId = 42161, + ) => { + t.log(`sending to EUD`, EUD); + + // parameterize agoric address + const { settlementAccount } = await fastInfoP; + 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 senderDigits = 'FAKE_SENDER_ADDR' as string & { length: 40 }; + const tx: Omit = harden({ + txHash: `0xFAKE_TX_HASH`, + tx: { + sender: `0x${senderDigits}`, + amount: mintAmt, + forwardingAddress: userForwardingAddr, + }, + aux: { + forwardingChannel: nobleAgoricChannelId, + recipientAddress, + }, + chainId, + }); + + console.log('User initiates evm mint:', tx.txHash); + + return tx; + }, + }); +}; From 2c79e74a4014f39c1ef11c9df7577be59a2c14d8 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Tue, 21 Jan 2025 17:40:59 -0600 Subject: [PATCH 5/5] refactor: reuse agoricNamesQ, fastLQP --- multichain-testing/scripts/fast-usdc-tool.ts | 23 +------------------- 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/multichain-testing/scripts/fast-usdc-tool.ts b/multichain-testing/scripts/fast-usdc-tool.ts index 2a927777f4a..db51c913a05 100755 --- a/multichain-testing/scripts/fast-usdc-tool.ts +++ b/multichain-testing/scripts/fast-usdc-tool.ts @@ -8,7 +8,6 @@ import type { ExecutionContext } from 'ava'; import { encodeAddressHook } from '@agoric/cosmic-proto/address-hooks.js'; import { AmountMath, type Brand } from '@agoric/ertp'; import type { USDCProposalShapes } from '@agoric/fast-usdc/src/pool-share-math.js'; -import type { PoolMetrics } from '@agoric/fast-usdc/src/types.js'; import { divideBy } from '@agoric/zoe/src/contractSupport/ratio.js'; import { makeDenomTools } from '../tools/asset-info.js'; import { makeDoOffer } from '../tools/e2e-tools.js'; @@ -17,6 +16,7 @@ import { makeFeedPolicyPartial, oracleMnemonics, } from '../test/fast-usdc/config.js'; +import { agoricNamesQ, fastLPQ } from '../test/fast-usdc/fu-actors.js'; const USAGE = ` Usage: @@ -55,27 +55,6 @@ const runT = { }, } as ExecutionContext; -// from ../test/fast-usdc/fast-usdc.test.ts -type VStorageClient = Awaited>['vstorageClient']; -const agoricNamesQ = (vsc: VStorageClient) => - harden({ - brands: (_assetKind: K) => - vsc - .queryData('published.agoricNames.brand') - .then(pairs => Object.fromEntries(pairs) as Record>), - }); - -const fastLPQ = (vsc: VStorageClient) => - harden({ - metrics: () => - vsc.queryData(`published.fastUsdc.poolMetrics`) as Promise, - info: () => - vsc.queryData(`published.${contractName}`) as Promise<{ - poolAccount: string; - settlementAccount: string; - }>, - }); - const parseCommandLine = () => { const { values, positionals } = parseArgs({ options: {