From dd751ff5dc1bc19154ad77d337fadf1a1b409f84 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Fri, 18 Oct 2024 22:22:50 -0500 Subject: [PATCH 01/48] docs: missing typeof --- packages/SwingSet/tools/run-utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/SwingSet/tools/run-utils.js b/packages/SwingSet/tools/run-utils.js index 5c1ed2e4b39..0bb0c5e3866 100644 --- a/packages/SwingSet/tools/run-utils.js +++ b/packages/SwingSet/tools/run-utils.js @@ -17,7 +17,7 @@ export const makeRunUtils = controller => { * Wait for exclusive access to the controller, then before relinquishing that access, * enqueue and process a delivery and return the result. * - * @param {() => ERef>} deliveryThunk + * @param {() => ERef>} deliveryThunk * function for enqueueing a delivery and returning the result kpid (if any) * @param {boolean} [voidResult] whether to ignore the result * @returns {Promise} From ef9f55ebddceff3b3b7d792a8f03ab7b2127d32e Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Mon, 28 Oct 2024 12:29:36 -0500 Subject: [PATCH 02/48] chore: let makeRunUtils caller provide run policy --- packages/SwingSet/tools/run-utils.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/SwingSet/tools/run-utils.js b/packages/SwingSet/tools/run-utils.js index 0bb0c5e3866..d2676f1055b 100644 --- a/packages/SwingSet/tools/run-utils.js +++ b/packages/SwingSet/tools/run-utils.js @@ -2,12 +2,18 @@ import { Fail, q } from '@endo/errors'; import { kunser } from '@agoric/kmarshal'; import { makeQueue } from '@endo/stream'; -/** @import { ERef } from '@endo/far' */ +/** + * @import { ERef } from '@endo/far' + * @import { RunPolicy } from '../src/types-external.js' + */ + +/** @typedef {{ provideRunPolicy: () => RunPolicy | undefined }} RunPolicyMaker */ /** * @param {import('../src/controller/controller.js').SwingsetController} controller + * @param {RunPolicyMaker} [perfTool] */ -export const makeRunUtils = controller => { +export const makeRunUtils = (controller, perfTool) => { const mutex = makeQueue(); const logRunFailure = reason => console.log('controller.run() failure', reason); @@ -25,7 +31,8 @@ export const makeRunUtils = controller => { const queueAndRun = async (deliveryThunk, voidResult = false) => { await mutex.get(); const kpid = await deliveryThunk(); - const runResultP = controller.run(); + const runPolicy = perfTool && perfTool.provideRunPolicy(); + const runResultP = controller.run(runPolicy); mutex.put(runResultP.catch(logRunFailure)); await runResultP; From d6993613242164013d1232fa6812c4fb22a99167 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Mon, 28 Oct 2024 12:49:17 -0500 Subject: [PATCH 03/48] refactor: factor out computronCounter --- .../cosmic-swingset/src/computron-counter.js | 68 +++++++++++++++++++ packages/cosmic-swingset/src/launch-chain.js | 67 +----------------- 2 files changed, 69 insertions(+), 66 deletions(-) create mode 100644 packages/cosmic-swingset/src/computron-counter.js diff --git a/packages/cosmic-swingset/src/computron-counter.js b/packages/cosmic-swingset/src/computron-counter.js new file mode 100644 index 00000000000..4f331c444e9 --- /dev/null +++ b/packages/cosmic-swingset/src/computron-counter.js @@ -0,0 +1,68 @@ +// @ts-check + +import { assert } from '@endo/errors'; +import { + BeansPerBlockComputeLimit, + BeansPerVatCreation, + BeansPerXsnapComputron, +} from './sim-params.js'; + +/** + * @typedef {object} BeansPerUnit + * @property {bigint} blockComputeLimit + * @property {bigint} vatCreation + * @property {bigint} xsnapComputron + */ +/** + * @param {BeansPerUnit} beansPerUnit + * @param {boolean} [ignoreBlockLimit] + * @returns {import('./launch-chain.js').ChainRunPolicy} + */ +export function computronCounter( + { + [BeansPerBlockComputeLimit]: blockComputeLimit, + [BeansPerVatCreation]: vatCreation, + [BeansPerXsnapComputron]: xsnapComputron, + }, + ignoreBlockLimit = false, +) { + assert.typeof(blockComputeLimit, 'bigint'); + assert.typeof(vatCreation, 'bigint'); + assert.typeof(xsnapComputron, 'bigint'); + let totalBeans = 0n; + const shouldRun = () => ignoreBlockLimit || totalBeans < blockComputeLimit; + const remainingBeans = () => + ignoreBlockLimit ? undefined : blockComputeLimit - totalBeans; + + const policy = harden({ + vatCreated() { + totalBeans += vatCreation; + return shouldRun(); + }, + crankComplete(details = {}) { + assert.typeof(details, 'object'); + if (details.computrons) { + assert.typeof(details.computrons, 'bigint'); + + // TODO: xsnapComputron should not be assumed here. + // Instead, SwingSet should describe the computron model it uses. + totalBeans += details.computrons * xsnapComputron; + } + return shouldRun(); + }, + crankFailed() { + const failedComputrons = 1000000n; // who knows, 1M is as good as anything + totalBeans += failedComputrons * xsnapComputron; + return shouldRun(); + }, + emptyCrank() { + return shouldRun(); + }, + shouldRun, + remainingBeans, + totalBeans() { + return totalBeans; + }, + }); + return policy; +} diff --git a/packages/cosmic-swingset/src/launch-chain.js b/packages/cosmic-swingset/src/launch-chain.js index c8b113d5528..e0289f08a23 100644 --- a/packages/cosmic-swingset/src/launch-chain.js +++ b/packages/cosmic-swingset/src/launch-chain.js @@ -42,15 +42,11 @@ import { makeSlogCallbacks, } from './kernel-stats.js'; -import { - BeansPerBlockComputeLimit, - BeansPerVatCreation, - BeansPerXsnapComputron, -} from './sim-params.js'; import { parseParams } from './params.js'; import { makeQueue, makeQueueStorageMock } from './helpers/make-queue.js'; import { exportStorage } from './export-storage.js'; import { parseLocatedJson } from './helpers/json.js'; +import { computronCounter } from './computron-counter.js'; /** @import { Mailbox, RunPolicy, SwingSetConfig } from '@agoric/swingset-vat' */ /** @import { KVStore } from './helpers/bufferedStorage.js' */ @@ -251,67 +247,6 @@ export async function buildSwingset( * }} ChainRunPolicy */ -/** - * @typedef {object} BeansPerUnit - * @property {bigint} blockComputeLimit - * @property {bigint} vatCreation - * @property {bigint} xsnapComputron - */ - -/** - * @param {BeansPerUnit} beansPerUnit - * @param {boolean} [ignoreBlockLimit] - * @returns {ChainRunPolicy} - */ -function computronCounter( - { - [BeansPerBlockComputeLimit]: blockComputeLimit, - [BeansPerVatCreation]: vatCreation, - [BeansPerXsnapComputron]: xsnapComputron, - }, - ignoreBlockLimit = false, -) { - assert.typeof(blockComputeLimit, 'bigint'); - assert.typeof(vatCreation, 'bigint'); - assert.typeof(xsnapComputron, 'bigint'); - let totalBeans = 0n; - const shouldRun = () => ignoreBlockLimit || totalBeans < blockComputeLimit; - const remainingBeans = () => - ignoreBlockLimit ? undefined : blockComputeLimit - totalBeans; - - const policy = harden({ - vatCreated() { - totalBeans += vatCreation; - return shouldRun(); - }, - crankComplete(details = {}) { - assert.typeof(details, 'object'); - if (details.computrons) { - assert.typeof(details.computrons, 'bigint'); - - // TODO: xsnapComputron should not be assumed here. - // Instead, SwingSet should describe the computron model it uses. - totalBeans += details.computrons * xsnapComputron; - } - return shouldRun(); - }, - crankFailed() { - const failedComputrons = 1000000n; // who knows, 1M is as good as anything - totalBeans += failedComputrons * xsnapComputron; - return shouldRun(); - }, - emptyCrank() { - return shouldRun(); - }, - shouldRun, - remainingBeans, - totalBeans() { - return totalBeans; - }, - }); - return policy; -} - export async function launch({ actionQueueStorage, highPriorityQueueStorage, From 8d2793b5cc6d1fa731a2c597b035f06e91cabd65 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Tue, 29 Oct 2024 13:39:08 -0500 Subject: [PATCH 04/48] test(boot): thread computron counter thru test context --- .../boot/test/bootstrapTests/walletFactory.ts | 2 + packages/boot/tools/liquidation.ts | 13 ++++- packages/boot/tools/supports.ts | 53 ++++++++++++++++++- 3 files changed, 66 insertions(+), 2 deletions(-) diff --git a/packages/boot/test/bootstrapTests/walletFactory.ts b/packages/boot/test/bootstrapTests/walletFactory.ts index 0b8fea676bc..ce964047233 100644 --- a/packages/boot/test/bootstrapTests/walletFactory.ts +++ b/packages/boot/test/bootstrapTests/walletFactory.ts @@ -9,9 +9,11 @@ import { makeWalletFactoryDriver } from '../../tools/drivers.js'; export const makeWalletFactoryContext = async ( t, configSpecifier = '@agoric/vm-config/decentral-main-vaults-config.json', + opts = {}, ) => { const swingsetTestKit = await makeSwingsetTestKit(t.log, undefined, { configSpecifier, + ...opts, }); const { runUtils, storage } = swingsetTestKit; diff --git a/packages/boot/tools/liquidation.ts b/packages/boot/tools/liquidation.ts index 175186a9b49..aa652b48013 100644 --- a/packages/boot/tools/liquidation.ts +++ b/packages/boot/tools/liquidation.ts @@ -9,6 +9,7 @@ import { } from '@agoric/vats/tools/board-utils.js'; import { Offers } from '@agoric/inter-protocol/src/clientSupport.js'; import type { ExecutionContext } from 'ava'; +import { insistManagerType, makePolicyProvider } from './supports.js'; import { type SwingsetTestKit, makeSwingsetTestKit } from './supports.js'; import { type GovernanceDriver, @@ -313,8 +314,17 @@ export const makeLiquidationTestContext = async ( io: { env?: Record } = {}, ) => { const { env = {} } = io; + const { + SLOGFILE: slogFile, + SWINGSET_WORKER_TYPE: defaultManagerType = 'local', + } = env; + insistManagerType(defaultManagerType); + const perfTool = + defaultManagerType === 'xsnap' ? makePolicyProvider() : undefined; const swingsetTestKit = await makeSwingsetTestKit(t.log, undefined, { - slogFile: env.SLOGFILE, + slogFile, + defaultManagerType, + perfTool, }); console.time('DefaultTestContext'); @@ -372,6 +382,7 @@ export const makeLiquidationTestContext = async ( refreshAgoricNamesRemotes, walletFactoryDriver, governanceDriver, + perfTool, }; }; diff --git a/packages/boot/tools/supports.ts b/packages/boot/tools/supports.ts index 8b4a810f799..9dd4bf41214 100644 --- a/packages/boot/tools/supports.ts +++ b/packages/boot/tools/supports.ts @@ -31,6 +31,7 @@ import { Fail } from '@endo/errors'; import { makeRunUtils, type RunUtils, + type RunPolicyMaker, } from '@agoric/swingset-vat/tools/run-utils.js'; import { boardSlottingMarshaller, @@ -45,6 +46,11 @@ import type { SwingsetController } from '@agoric/swingset-vat/src/controller/con import type { BridgeHandler, IBCMethod } from '@agoric/vats'; import type { BootstrapRootObject } from '@agoric/vats/src/core/lib-boot.js'; import type { EProxy } from '@endo/eventual-send'; +import { + defaultBeansPerVatCreation, + defaultBeansPerXsnapComputron, +} from '@agoric/cosmic-swingset/src/sim-params.js'; +import { computronCounter } from '@agoric/cosmic-swingset/src/computron-counter.js'; import { icaMocks, protoMsgMockMap, protoMsgMocks } from './ibc/mocks.js'; const trace = makeTracer('BSTSupport', false); @@ -73,6 +79,7 @@ type BootstrapEV = EProxy & { const makeBootstrapRunUtils = makeRunUtils as ( controller: SwingsetController, + perfTool?: RunPolicyMaker, ) => Omit & { EV: BootstrapEV }; const keysToObject = ( @@ -304,6 +311,7 @@ export const matchIter = (t: AvaT, iter, valueRef) => { * @param [options.profileVats] * @param [options.debugVats] * @param [options.defaultManagerType] + * @param [options.perfTool] */ export const makeSwingsetTestKit = async ( log: (..._: any[]) => void, @@ -317,6 +325,7 @@ export const makeSwingsetTestKit = async ( profileVats = [] as string[], debugVats = [] as string[], defaultManagerType = 'local' as ManagerType, + perfTool = undefined as RunPolicyMaker | undefined, } = {}, ) => { console.time('makeBaseSwingsetTestKit'); @@ -532,7 +541,7 @@ export const makeSwingsetTestKit = async ( console.timeLog('makeBaseSwingsetTestKit', 'buildSwingset'); - const runUtils = makeBootstrapRunUtils(controller); + const runUtils = makeBootstrapRunUtils(controller, perfTool); const buildProposal = makeProposalExtractor({ childProcess: childProcessAmbient, @@ -653,3 +662,45 @@ export const makeSwingsetTestKit = async ( }; }; export type SwingsetTestKit = Awaited>; + +export const makePolicyProvider = () => { + const c2b = defaultBeansPerXsnapComputron; + const mainParams = { + // see https://cosgov.org/agoric?msgType=parameterChangeProposal&network=main + blockComputeLimit: 65_000_000n * c2b, + vatCreation: defaultBeansPerVatCreation, + xsnapComputron: c2b, + }; + + /** @type {ReturnType | undefined} */ + let policy; + let counting = false; + + const meter = harden({ + provideRunPolicy: () => { + if (counting && !policy) { + policy = computronCounter(mainParams); + } + return policy; + }, + /** @param {boolean} x */ + usePolicy: x => { + counting = x; + if (!counting) { + policy = undefined; + } + }, + totalCount: () => (policy?.totalBeans() || 0n) / c2b, + resetPolicy: () => (policy = undefined), + }); + return meter; +}; + +/** + * + * @param {string} mt + * @returns {asserts mt is ManagerType} + */ +export function insistManagerType(mt) { + assert(['local', 'node-subprocess', 'xsnap', 'xs-worker'].includes(mt)); +} From f715b73d5684dd76d27d8524fad23125404b39e6 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Tue, 29 Oct 2024 13:39:24 -0500 Subject: [PATCH 05/48] test: demo SWINGSET_WORKER_TYPE, computron count MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit in a couple tests: $ cd agoric-sdk/packages/boot $ SWINGSET_WORKER_TYPE=xsnap yarn test test/bootstrapTests/price-feed-replace.test.ts ... ✔ setupVaults; run updatePriceFeeds proposals (1m 27.6s) ℹ setPrice computrons 65536531n $ SWINGSET_WORKER_TYPE=xsnap yarn test test/bootstrapTests/orchestration.test.ts ... ✔ stakeAtom - smart wallet (13.8s) ℹ makeAccount computrons 15231491n --- .../test/bootstrapTests/orchestration.test.ts | 25 +++++++++++++++++-- .../bootstrapTests/price-feed-replace.test.ts | 4 +++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/packages/boot/test/bootstrapTests/orchestration.test.ts b/packages/boot/test/bootstrapTests/orchestration.test.ts index ba56accaabe..5925fcbb7cd 100644 --- a/packages/boot/test/bootstrapTests/orchestration.test.ts +++ b/packages/boot/test/bootstrapTests/orchestration.test.ts @@ -11,8 +11,13 @@ import { makeWalletFactoryContext, type WalletFactoryTestContext, } from './walletFactory.js'; +import { insistManagerType, makePolicyProvider } from '../../tools/supports.js'; -const test: TestFn = anyTest; +const test: TestFn< + WalletFactoryTestContext & { + perfTool?: ReturnType; + } +> = anyTest; const validatorAddress: CosmosValidatorAddress = { value: 'cosmosvaloper1test', @@ -22,11 +27,21 @@ const validatorAddress: CosmosValidatorAddress = { const ATOM_DENOM = 'uatom'; +const { + SLOGFILE: slogFile, + SWINGSET_WORKER_TYPE: defaultManagerType = 'local', +} = process.env; + test.before(async t => { - t.context = await makeWalletFactoryContext( + insistManagerType(defaultManagerType); + const perfTool = + defaultManagerType === 'xsnap' ? makePolicyProvider() : undefined; + const ctx = await makeWalletFactoryContext( t, '@agoric/vm-config/decentral-itest-orchestration-config.json', + { slogFile, defaultManagerType, perfTool }, ); + t.context = { ...ctx, perfTool }; }); test.after.always(t => t.context.shutdown?.()); @@ -103,6 +118,7 @@ test.skip('stakeOsmo - queries', async t => { buildProposal, evalProposal, runUtils: { EV }, + perfTool, } = t.context; await evalProposal( buildProposal('@agoric/builders/scripts/orchestration/init-stakeOsmo.js'), @@ -141,6 +157,7 @@ test.serial('stakeAtom - smart wallet', async t => { agoricNamesRemotes, bridgeUtils: { flushInboundQueue }, readLatest, + perfTool, } = t.context; await evalProposal( @@ -151,6 +168,7 @@ test.serial('stakeAtom - smart wallet', async t => { 'agoric1testStakAtom', ); + perfTool?.usePolicy(true); await wd.sendOffer({ id: 'request-account', invitationSpec: { @@ -160,6 +178,9 @@ test.serial('stakeAtom - smart wallet', async t => { }, proposal: {}, }); + perfTool && t.log('makeAccount computrons', perfTool.totalCount()); + perfTool?.usePolicy(false); + await flushInboundQueue(); t.like(wd.getCurrentWalletRecord(), { offerToPublicSubscriberPaths: [ diff --git a/packages/boot/test/bootstrapTests/price-feed-replace.test.ts b/packages/boot/test/bootstrapTests/price-feed-replace.test.ts index 84a077f336e..076fab573fe 100644 --- a/packages/boot/test/bootstrapTests/price-feed-replace.test.ts +++ b/packages/boot/test/bootstrapTests/price-feed-replace.test.ts @@ -64,6 +64,7 @@ test.serial('setupVaults; run updatePriceFeeds proposals', async t => { setupVaults, governanceDriver: gd, readLatest, + perfTool, } = t.context; await setupVaults(collateralBrandKey, managerIndex, setup); @@ -74,7 +75,10 @@ test.serial('setupVaults; run updatePriceFeeds proposals', async t => { roundId: 1n, }); + perfTool && perfTool.usePolicy(true); await priceFeedDrivers[collateralBrandKey].setPrice(15.99); + perfTool && t.log('setPrice computrons', perfTool.totalCount()); + perfTool && perfTool.usePolicy(false); t.like(readLatest('published.priceFeed.ATOM-USD_price_feed.latestRound'), { roundId: 2n, From f3883e490672c2e4ac4459729eef198dfaac8811 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Thu, 19 Sep 2024 14:43:55 -0500 Subject: [PATCH 06/48] test: bootstrap test for trivial quickSend contract --- .../test/bootstrapTests/quickSend.test.ts | 61 +++++++++ .../scripts/orchestration/init-quickSend.js | 26 ++++ .../src/examples/quickSend.contract.js | 29 +++++ .../src/proposals/start-quickSend.js | 116 ++++++++++++++++++ 4 files changed, 232 insertions(+) create mode 100644 packages/boot/test/bootstrapTests/quickSend.test.ts create mode 100644 packages/builders/scripts/orchestration/init-quickSend.js create mode 100644 packages/orchestration/src/examples/quickSend.contract.js create mode 100644 packages/orchestration/src/proposals/start-quickSend.js diff --git a/packages/boot/test/bootstrapTests/quickSend.test.ts b/packages/boot/test/bootstrapTests/quickSend.test.ts new file mode 100644 index 00000000000..bcaf8ebdba2 --- /dev/null +++ b/packages/boot/test/bootstrapTests/quickSend.test.ts @@ -0,0 +1,61 @@ +import { test as anyTest } from '@agoric/zoe/tools/prepare-test-env-ava.js'; + +import type { TestFn } from 'ava'; +import { + makeWalletFactoryContext, + type WalletFactoryTestContext, +} from './walletFactory.js'; +import { WalletFactoryDriver } from '../../tools/drivers.js'; + +const test: TestFn = anyTest; + +test.before(async t => { + t.context = await makeWalletFactoryContext( + t, + '@agoric/vm-config/decentral-itest-orchestration-config.json', + ); +}); +test.after.always(t => t.context.shutdown?.()); + +test.serial('deploy contract', async t => { + const { + agoricNamesRemotes, + evalProposal, + buildProposal, + refreshAgoricNamesRemotes, + } = t.context; + await evalProposal( + buildProposal('@agoric/builders/scripts/orchestration/init-quickSend.js'), + ); + // update now that quickSend is instantiated + refreshAgoricNamesRemotes(); + t.truthy(agoricNamesRemotes.instance.quickSend); +}); + +type SmartWallet = Awaited< + ReturnType +>; + +test.serial('expedited send', async t => { + const alice = async (sw: SmartWallet) => { + await sw.executeOffer({ + id: 'request-dest-addr', + invitationSpec: { + source: 'agoricContract', + instancePath: ['quickSend'], + callPipe: [['makeInvitation']], + }, + proposal: {}, + }); + const update = sw.getLatestUpdateRecord(); + t.like(update, { + updated: 'offerStatus', + status: { id: 'request-dest-addr', numWantsSatisfied: 1, result: 'TODO' }, + }); + }; + + const wd = + await t.context.walletFactoryDriver.provideSmartWallet('agoric1alice'); + + await alice(wd); +}); diff --git a/packages/builders/scripts/orchestration/init-quickSend.js b/packages/builders/scripts/orchestration/init-quickSend.js new file mode 100644 index 00000000000..98e27f3f1ad --- /dev/null +++ b/packages/builders/scripts/orchestration/init-quickSend.js @@ -0,0 +1,26 @@ +// @ts-check +import { makeHelpers } from '@agoric/deploy-script-support'; +import { getManifestForQuickSend } from '@agoric/orchestration/src/proposals/start-quickSend.js'; + +/** @import {CoreEvalBuilder} from '@agoric/deploy-script-support/src/externalTypes.js'; */ + +/** @type {CoreEvalBuilder} */ +export const defaultProposalBuilder = async ({ publishRef, install }) => + harden({ + sourceSpec: '@agoric/orchestration/src/proposals/start-quickSend.js', + getManifestCall: [ + getManifestForQuickSend.name, + { + installKeys: { + quickSend: publishRef( + install('@agoric/orchestration/src/examples/quickSend.contract.js'), + ), + }, + }, + ], + }); + +export default async (homeP, endowments) => { + const { writeCoreEval } = await makeHelpers(homeP, endowments); + await writeCoreEval('start-quickSend', defaultProposalBuilder); +}; diff --git a/packages/orchestration/src/examples/quickSend.contract.js b/packages/orchestration/src/examples/quickSend.contract.js new file mode 100644 index 00000000000..b3eb74c7a01 --- /dev/null +++ b/packages/orchestration/src/examples/quickSend.contract.js @@ -0,0 +1,29 @@ +import { Far } from '@endo/far'; + +/** + * @import {Baggage} from '@agoric/vat-data'; + */ + +/** + * @param {ZCF} zcf + * @param {{}} privateArgs + * @param {Baggage} baggage + * @returns + */ +export const start = (zcf, privateArgs, baggage) => { + /** @type {OfferHandler} */ + const handler = Far('OfferHandler', { + handle: seat => { + seat.exit(); + return 'TODO'; + }, + }); + + harden(handler); + return harden({ + publicFacet: Far('QuickSend API', { + makeInvitation: () => zcf.makeInvitation(handler, 'request destination'), + }), + }); +}; +harden(start); diff --git a/packages/orchestration/src/proposals/start-quickSend.js b/packages/orchestration/src/proposals/start-quickSend.js new file mode 100644 index 00000000000..bf3a08efc75 --- /dev/null +++ b/packages/orchestration/src/proposals/start-quickSend.js @@ -0,0 +1,116 @@ +import { deeplyFulfilledObject, makeTracer } from '@agoric/internal'; +import { Stake } from '@agoric/internal/src/tokens.js'; +import { E } from '@endo/far'; + +const trace = makeTracer('StartQuickSend', true); +const { Fail } = assert; + +/** + * @import {Instance} from '@agoric/zoe/src/zoeService/utils'; + * @import {Board} from '@agoric/vats'; + */ + +/** + * @param {string} path + * @param {{ + * chainStorage: ERef; + * board: ERef; + * }} io + */ +const makePublishingStorageKit = async (path, { chainStorage, board }) => { + const root = await chainStorage; + root || Fail`chainStorage null case is vestigial`; + const storageNode = await E(chainStorage)?.makeChildNode(path); + + const marshaller = await E(board).getPublishingMarshaller(); + return { storageNode, marshaller }; +}; + +/** + * @param {BootstrapPowers & { + * installation: PromiseSpaceOf<{ + * quickSend: Installation< + * import('../examples/quickSend.contract.js').start + * >; + * }>; + * instance: PromiseSpaceOf<{ + * quickSend: Instance; + * }>; + * }} powers + */ +export const startQuickSend = async ({ + consume: { + agoricNames, + board, + chainStorage, + chainTimerService: timerService, + localchain, + startUpgradable, + }, + installation: { + consume: { quickSend }, + }, + instance: { + produce: { quickSend: produceInstance }, + }, + issuer: { + consume: { [Stake.symbol]: stakeIssuer }, + }, +}) => { + trace('startQuickSend'); + const { storageNode, marshaller } = await makePublishingStorageKit( + 'quickSend', + { board, chainStorage }, + ); + + const privateArgs = await deeplyFulfilledObject( + harden({ agoricNames, localchain, timerService, storageNode, marshaller }), + ); + + /** + * @type {StartUpgradableOpts< + * import('../examples/quickSend.contract.js').start + * >} + */ + const startOpts = { + label: 'quickSend', + installation: quickSend, + issuerKeywordRecord: harden({ In: await stakeIssuer }), + terms: {}, + privateArgs, + }; + + const { instance } = await E(startUpgradable)(startOpts); + produceInstance.resolve(instance); + trace('done'); +}; +harden(startQuickSend); + +export const getManifestForQuickSend = ({ restoreRef }, { installKeys }) => { + return { + manifest: { + [startQuickSend.name]: { + consume: { + agoricNames: true, + board: true, + chainStorage: true, + chainTimerService: true, + localchain: true, + startUpgradable: true, + }, + installation: { + consume: { quickSend: true }, + }, + instance: { + produce: { quickSend: true }, + }, + issuer: { + consume: { [Stake.symbol]: true }, + }, + }, + }, + installations: { + quickSend: restoreRef(installKeys.quickSend), + }, + }; +}; From 1620ffc187a0c35ef09a3afb212c8816433359c3 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Thu, 10 Oct 2024 14:29:12 -0500 Subject: [PATCH 07/48] chore(quickSend): skip bootstrap test (WIP) --- packages/boot/test/bootstrapTests/quickSend.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/boot/test/bootstrapTests/quickSend.test.ts b/packages/boot/test/bootstrapTests/quickSend.test.ts index bcaf8ebdba2..931abaefff5 100644 --- a/packages/boot/test/bootstrapTests/quickSend.test.ts +++ b/packages/boot/test/bootstrapTests/quickSend.test.ts @@ -9,7 +9,7 @@ import { WalletFactoryDriver } from '../../tools/drivers.js'; const test: TestFn = anyTest; -test.before(async t => { +test.skip('bootstrap', async t => { t.context = await makeWalletFactoryContext( t, '@agoric/vm-config/decentral-itest-orchestration-config.json', @@ -17,7 +17,7 @@ test.before(async t => { }); test.after.always(t => t.context.shutdown?.()); -test.serial('deploy contract', async t => { +test.skip('deploy contract', async t => { const { agoricNamesRemotes, evalProposal, @@ -36,7 +36,7 @@ type SmartWallet = Awaited< ReturnType >; -test.serial('expedited send', async t => { +test.skip('expedited send', async t => { const alice = async (sw: SmartWallet) => { await sw.executeOffer({ id: 'request-dest-addr', From 368cbeb7de10f4e70caa615d76c2d508bc997362 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Wed, 9 Oct 2024 19:11:30 -0500 Subject: [PATCH 08/48] test: reify participants from Fast USDC sequence diagram - part 1: send advance - part 2: settlement - sync arrow labels with sequence diagram --- .../test/examples/quickSend-tx.test.ts | 653 ++++++++++++++++++ 1 file changed, 653 insertions(+) create mode 100644 packages/orchestration/test/examples/quickSend-tx.test.ts diff --git a/packages/orchestration/test/examples/quickSend-tx.test.ts b/packages/orchestration/test/examples/quickSend-tx.test.ts new file mode 100644 index 00000000000..77edc1d6e6a --- /dev/null +++ b/packages/orchestration/test/examples/quickSend-tx.test.ts @@ -0,0 +1,653 @@ +import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js'; + +import { createRequire } from 'node:module'; + +const nodeRequire = createRequire(import.meta.url); +const contractName = 'sendAnywhere'; +const contractFile = nodeRequire.resolve( + `../../src/examples/send-anywhere.contract.js`, +); + +type StartFn = typeof import('../../src/examples/quickSend.contract.js').start; + +const todo = () => assert.fail('TODO'); + +const NobleCalc = harden({ + fwdAddressFor: (dest: string) => `noble1${dest.length}${dest.slice(-4)}`, +}); + +const AgoricCalc = harden({ + virtualAddressFor: (base: string, dest: string) => `${base}+${dest}`, + isVirtualAddress: addr => addr.includes('+'), + virtualAddressParts: addr => addr.split('+'), +}); + +const logged = (it, label) => { + console.debug(label, it); + return it; +}; + +test.before(async t => { + let label; + const startLabels = () => (label = 0); + const nextLabel = () => + typeof label === 'number' ? `--> #${(label += 1)}:` : ''; + harden(nextLabel); + t.context = { startLabels, nextLabel }; +}); + +const makeEventCounter = ({ setTimeout }) => { + let current = 0; + let going = true; + async function* eachEvent() { + await null; + for (; ; current += 1) { + if (!going) break; + yield current; + await new Promise(resolve => setTimeout(resolve, 100)); + } + } + const iter = eachEvent(); + + return harden({ + getCurrent: () => current, + cancel: () => (going = false), + [Symbol.asyncIterator]: () => iter, + }); +}; + +const makeCosmosChain = (prefix, t) => { + let nonce = 10; + const balances = new Map(); + const whaleAddress = `${prefix}${(nonce += 1)}`; + balances.set(whaleAddress, 1_000_000); + const { nextLabel: next } = t.context; + + const burn = async ({ from, amount }) => { + const fromPre = balances.get(from) || 0; + const fromPost = fromPre - amount; + fromPost >= 0 || assert.fail(`${from} overdrawn: ${fromPre} - ${amount}`); + balances.set(from, fromPost); + }; + + const mint = async ({ dest, amount }) => { + const destPre = balances.get(dest) || 0; + const destPost = destPre + amount; + balances.set(dest, destPost); + }; + + return harden({ + prefix, + whaleAddress, + makeAccount: async () => { + const address = `${prefix}${(nonce += 1)}`; + balances.set(address, 0); + t.log('chain', prefix, 'makeAccount', address); + return address; + }, + getBalance: async dest => balances.get(dest), + send: async ({ amount, from, dest, quiet }) => { + t.true(dest.startsWith(prefix), dest); + await burn({ from, amount }); + await mint({ dest, amount }); + const label = quiet ? '' : next(); + t.log(label, dest, 'balance +=', amount, '=', balances.get(dest)); + }, + }); +}; + +const pickChain = (chains, dest) => { + const pfxLen = dest.indexOf('1'); + const pfx = dest.slice(0, pfxLen); + const chain = chains[pfx]; + assert(chain, dest); + return chain; +}; + +const ibcTransfer = async (chains, { amount, from, dest, t }) => { + const { nextLabel: next } = t.context; + t.log(next(), 'ibc transfer', amount, 'to', dest); + const chainSrc = pickChain(chains, from); + const chainDest = pickChain(chains, dest); + const burn = `${chainSrc.prefix}IBCburn`; + const quiet = true; + await chainSrc.send({ amount, from, dest: burn, quiet }); + await chainDest.send({ amount, from: chainDest.whaleAddress, dest, quiet }); +}; + +const withForwarding = (chain, chains, t) => { + const destOf = new Map(); + const { nextLabel: next } = t.context; + return harden({ + ...chain, + provideForwardingAccount: async dest => { + const address = NobleCalc.fwdAddressFor(dest); + destOf.set(address, dest); + t.log('x/forwarding fwd', address, '->', dest); + return address; + }, + send: async ({ amount, from, dest }) => { + await chain.send({ amount, from, dest, quiet: true }); + if (!destOf.has(dest)) return; + const fwd = destOf.get(dest); + t.log(next(), 'fwd', { amount, dest, fwd }); + await ibcTransfer(chains, { amount, from: dest, dest: fwd, t }); + }, + }); +}; + +const withVTransfer = (chain, t) => { + const addrToTap = new Map(); + return harden({ + ...chain, + register: async ({ addr, handler }) => { + t.log('vtransfer register', { addr, handler }); + !addrToTap.has(addr) || assert.fail('already registered'); + addrToTap.set(addr, handler); + }, + send: async ({ amount, from, dest }) => { + const [agAddr, extra] = AgoricCalc.isVirtualAddress(dest) + ? AgoricCalc.virtualAddressParts(dest) + : [dest, undefined]; + const quiet = true; + const result = await chain.send({ amount, from, dest: agAddr, quiet }); + + if (extra === undefined) return result; + t.log('vtransfer to virtual address', { agAddr, extra }); + if (!addrToTap.has(agAddr)) return result; + + const handler = addrToTap.get(agAddr); + void handler.onReceive({ amount, extra }).catch(err => { + console.error('onRecieve rejected', err); + }); + + return result; + }, + }); +}; + +const makeUser = ({ nobleApp, ethereum, myAddr, cctpAddr }) => + harden({ + doTransfer: async (amount, dest) => { + const nobleFwd = await nobleApp.getNobleFwd(dest); + const { setup, done } = await nobleApp.initiateTransaction({ + dest, + amount, + nobleFwd, + }); + await setup; + await ethereum.call({ sender: myAddr }, cctpAddr, 'bridge', [ + { dest: nobleFwd, amount }, + ]); + return done; + }, + }); + +const makeNobleApp = async ({ + t, + nobleService, + chains, + agoricRpc, + setTimeout, +}) => { + const watchAddr = async ({ dest, amount }) => { + const chain = pickChain(chains, dest); + const balancePre = await chain.getBalance(dest); + const events = makeEventCounter({ setTimeout }); + for await (const tick of events) { + const balancePost = await chain.getBalance(dest); + t.log('app watch dest', { balancePre, balancePost, dest, tick }); + if (balancePost > balancePre) { + return harden({ dest, amount, balance: balancePost }); + } + } + assert.fail('unreachable'); + }; + + const { nextLabel } = t.context; + const settlementBase = await agoricRpc.getData( + 'published.quickSend.settlementBase', + ); + t.log(nextLabel(), 'app got settlementBase', settlementBase); + return harden({ + getNobleFwd: async (dest: string) => { + const nobleFwd = NobleCalc.fwdAddressFor( + AgoricCalc.virtualAddressFor(settlementBase, dest), + ); + return nobleFwd; + }, + initiateTransaction: async ({ dest, amount, nobleFwd }) => { + t.log(nextLabel(), 'app initiate', { amount, dest }); + const setup = nobleService.initiateTransfer({ amount, dest, nobleFwd }); + const done = watchAddr({ dest, amount }); + return { setup, done }; + }, + }); +}; + +const makeNobleExpress = ({ agoricWatcher, chain, t, agoricRpc }) => { + const baseP = agoricRpc.getData('published.quickSend.settlementBase'); + const { nextLabel: next } = t.context; + return harden({ + initiateTransfer: async ({ dest, amount, nobleFwd }) => { + const base = await baseP; + const agAddr = AgoricCalc.virtualAddressFor(base, dest); + const fwd = await chain.provideForwardingAccount(agAddr); + t.log(next(), 'express initiate', { dest, base, agAddr, fwd, nobleFwd }); + fwd === nobleFwd || assert.fail('mismatch'); + await agoricWatcher.startWatchingFor({ dest, amount, nobleFwd }); + }, + }); +}; + +const makeERC20 = (t, msg0, supply) => { + const balances = new Map(); + balances.set(msg0.sender, supply); + const balanceOf = account => balances.get(account) || 0; + return harden({ + balanceOf, + transfer: (msg, dest, numTokens) => { + const srcBal = balanceOf(msg.sender); + t.log('ERC20 transfer', { sender: msg.sender, srcBal, numTokens, dest }); + t.true(srcBal > numTokens); + const destBal = balanceOf(dest); + balances.set(msg.sender, srcBal - numTokens); + balances.set(dest, destBal + numTokens); + }, + }); +}; + +const makeCCTP = ({ t, usdc, noble, events }) => { + const { nextLabel: next } = t.context; + return harden({ + bridge: (msg, { dest, amount }) => { + t.regex(dest, /^noble/); + t.log(next(), 'cctp.bridge:', { msg, dest }); + usdc.transfer(msg, '0x0000', amount); // burn + const t0 = events.getCurrent(); + void (async () => { + t.log('waiting 3 blocks (TODO: 20min) before minting on noble'); + for await (const te of events) { + if (te - t0 > 3) break; + } + noble.send({ amount, from: 'noble1mint', dest }); + })(); + }, + }); +}; + +type EthAddr = `0x${string}`; +type EthData = Record; +type EthMsgInfo = { sender: EthAddr; value?: number }; +type EthCallTx = { + txId: number; + msg: EthMsgInfo; + contractAddress: EthAddr; + method: string; + args: EthData[]; +}; + +const makeEthChain = (heightInitial: number, { t, setTimeout }) => { + let nonce = 10; + let height = heightInitial; + const contracts = new Map(); + const mempool: EthCallTx[] = []; + const blocks: EthCallTx[][] = [[]]; + const emptyBlock = harden([]); + + let going = true; + const advanceBlock = () => { + blocks.push(harden([...mempool])); + mempool.splice(0, mempool.length); + height += 1; + t.log('eth advance to block', height); + }; + void (async () => { + const ticks = makeEventCounter({ setTimeout }); + for await (const tick of ticks) { + if (!going) break; + advanceBlock(); + } + })(); + + const { nextLabel: next } = t.context; + return harden({ + deployContract: async c => { + const address = `0x${(nonce += 1)}`; + contracts.set(address, c); + return address; + }, + call: async ( + msg: EthMsgInfo, + addr: EthAddr, + method: string, + args: EthData[], + ) => { + const txId = nonce; + nonce += 1; + mempool.push({ txId, msg, contractAddress: addr, method, args }); + t.log(next(), 'eth call', addr, '.', method, '(', ...args, ')'); + const contract = contracts.get(addr); + const result = contract[method](msg, ...args); + t.is(result, undefined); + }, + currentHeight: () => height - 1, + getBlock: (h: number) => blocks[h - heightInitial] || emptyBlock, + stop: () => (going = false), + }); +}; +type EthChain = ReturnType; + +const makeAgoricWatcher = ({ + t, + ethereum: eth, + cctpAddr, + contract, + setTimeout, +}) => { + const ethereum: EthChain = eth; // TODO: concise typing + const pending: { dest: string; amount: number; nobleFwd: string }[] = []; + let done = false; + + const { nextLabel: next } = t.context; + const check = async height => { + await null; + const txs = await ethereum.getBlock(height); + t.log('watcher found', txs.length, 'txs at height', height); + for (const tx of txs) { + if (done) break; + if (tx.contractAddress !== cctpAddr) break; + const [{ dest, amount }] = tx.args; + const ix = pending.findIndex( + item => item.nobleFwd === dest && item.amount === amount, + ); + if (ix < 0) continue; + const item = pending[ix]; + pending.splice(ix, 1); + t.log(next(), 'watcher checked', tx.txId); + t.log(next(), 'watcher confirmed', item); + await contract.releaseAdvance(item); + } + }; + + const events = makeEventCounter({ setTimeout }); + + return harden({ + startWatchingFor: async ({ dest, amount, nobleFwd }) => { + t.log(next(), 'watcher.startWatchingFor', { dest, amount, nobleFwd }); + pending.push(harden({ dest, amount, nobleFwd })); + await check(ethereum.currentHeight()); + }, + watch: async () => { + await null; + for await (const tick of events) { + const ethHeight = ethereum.currentHeight(); + if (done) break; + await check(ethHeight); + } + }, + stop: () => { + done = true; + t.log('watcher: stop at', ethereum.currentHeight()); + }, + }); +}; + +// funding pool is a local account +const makeOrchestration = (t, chains) => { + const { nextLabel: next } = t.context; + return harden({ + makeLocalAccount: async () => { + const addr = await chains.agoric.makeAccount(); + return harden({ + getAddress: () => addr, + transfer: async ({ amount, dest }) => { + t.log(next(), 'orch acct', addr, 'txfr', amount, 'to', dest); + await ibcTransfer(chains, { amount, dest, from: addr, t }); + }, + send: async ({ amount, dest }) => { + t.log(next(), 'orch acct', addr, 'send', amount, 'to', dest); + await chains.agoric.send({ amount, dest, from: addr }); + }, + tap: async handler => { + await chains.agoric.register({ addr, handler }); + }, + }); + }, + }); +}; + +const makeVStorage = () => { + const data = new Map(); + const storageNode = harden({ + makeChildNode: path => + harden({ + setValue: value => data.set(path, value), + }), + }); + const rpc = harden({ + getData: async path => data.get(path), + }); + + return { storageNode, rpc }; +}; + +const makeQuickSend = async ({ + t, + privateArgs: { orch, storageNode }, + terms: { makerFee, contractFee }, +}) => { + const fundingPool = await orch.makeLocalAccount(); + const settlement = await orch.makeLocalAccount(); + const feeAccount = await orch.makeLocalAccount(); + + const { nextLabel: next } = t.context; + storageNode.setValue(settlement.getAddress()); + + await settlement.tap( + harden({ + onReceive: async ({ amount, extra }) => { + t.log(next(), 'tap onReceive', { amount }); + // XXX partial failure? + await Promise.all([ + settlement.send({ + dest: fundingPool.getAddress(), + amount: amount - contractFee, + }), + settlement.send({ + dest: feeAccount.getAddress(), + amount: contractFee, + }), + ]); + }, + }), + ); + + return harden({ + releaseAdvance: async ({ amount, dest, nobleFwd }) => { + t.log(next(), 'contract.releaseAdvance', { amount, dest }); + t.is( + NobleCalc.fwdAddressFor( + AgoricCalc.virtualAddressFor(fundingPool.getAddress(), dest), + ), + nobleFwd, + ); + const advance = amount - makerFee - contractFee; + await fundingPool.transfer({ dest, amount: advance }); + }, + getPoolAddress: async () => fundingPool.getAddress(), + getSettlementAddress: async () => settlement.getAddress(), + getFeeAddress: async () => feeAccount.getAddress(), + }); +}; + +const terms = { makerFee: 3, contractFee: 2 }; +const startFunds = { + usdcMint: 10_000, + nobleMint: 100_000, + pool: 2000, + user: 150, +}; + +const setup = async (t, io) => { + // const t = { + // ...t0, + // log: (...args) => { + // console.debug(...args); + // t0.log(...args); + // }, + // }; + t.log('-- SETUP...'); + const ethereum = makeEthChain(9876, { t, setTimeout }); + let chains = harden({ + agoric: withVTransfer(makeCosmosChain('agoric1', t), t), + noble: makeCosmosChain('noble1', t), + dydx: makeCosmosChain('dydx1', t), + }); + chains = harden({ + ...chains, + noble: withForwarding(chains.noble, chains, t), + }); + + const orch = makeOrchestration(t, chains); + const { storageNode: chainStorage, rpc: agoricRpc } = makeVStorage(); + const storageNode = chainStorage.makeChildNode( + 'published.quickSend.settlementBase', + ); + const contract = await makeQuickSend({ + t, + privateArgs: { orch, storageNode }, + terms, + }); + const poolAddr = await contract.getPoolAddress(); + await chains.agoric.send({ + amount: startFunds.pool, + from: chains.agoric.whaleAddress, // TODO: market maker contributes + dest: poolAddr, + }); + + const usdc = makeERC20(t, { sender: '0xcircle' }, startFunds.usdcMint); + const usdcAddr = await ethereum.deployContract(usdc); + await chains.noble.send({ + dest: 'noble1mint', + amount: startFunds.nobleMint, + from: chains.noble.whaleAddress, + }); + + const cctp = makeCCTP({ + t, + usdc, + noble: chains.noble, + events: makeEventCounter(io), + }); + const cctpAddr = await ethereum.deployContract(cctp); + + const agoricWatcher = makeAgoricWatcher({ + t, + ethereum, + cctpAddr, + contract, + setTimeout: globalThis.setTimeout, + }); + const nobleService = makeNobleExpress({ + agoricWatcher, + chain: chains.noble, + agoricRpc, + t, + }); + + void agoricWatcher.watch().catch(err => { + console.error('failure while watching', err); + t.fail(err.message); + }); + usdc.transfer({ sender: '0xcircle' }, '0xUrsula', startFunds.user); + + const quiesce = async () => { + const ticks = makeEventCounter({ setTimeout }); + for await (const tick of ticks) { + if (tick >= 5) break; + } + t.log('quiesced for 5 ticks'); + agoricWatcher.stop(); + ethereum.stop(); + }; + + t.log('-- SETUP done.'); + t.context.startLabels(); + const nobleApp = await makeNobleApp({ + t, + nobleService, + chains, + agoricRpc, + setTimeout: globalThis.setTimeout, + }); + const ursula = makeUser({ + nobleApp, + ethereum, + myAddr: '0xUrsula', + cctpAddr, + }); + + return { chains, ursula, quiesce, contract, usdc }; +}; + +test('tx lifecycle', async t => { + const io = { setTimeout }; + const { chains, ursula, quiesce, contract, usdc } = await setup(t, io); + + const destAddr = await chains.dydx.makeAccount(); // is this a prereq? + await ursula.doTransfer(100, destAddr); + + await quiesce(); + + const poolAddr = await contract.getPoolAddress(); + const feeAddr = await contract.getFeeAddress(); + const settlementAddr = await contract.getSettlementAddress(); + const actual = { + user: { + addr: '0xUrsula', + start: startFunds.user, + balance: usdc.balanceOf('0xUrsula'), + }, + dest: { + addr: destAddr, + balance: await chains.dydx.getBalance(destAddr), + }, + pool: { + addr: poolAddr, + start: startFunds.pool, + balance: await chains.agoric.getBalance(poolAddr), + }, + fee: { + addr: feeAddr, + balance: await chains.agoric.getBalance(feeAddr), + }, + settlement: { + addr: settlementAddr, + balance: await chains.agoric.getBalance(settlementAddr), + }, + }; + const expected = { + user: { + addr: '0xUrsula', + start: startFunds.user, + balance: 50, + }, + dest: { + addr: 'dydx112', + balance: 100 - terms.makerFee - terms.contractFee, + }, + pool: { + addr: 'agoric112', + start: startFunds.pool, + balance: startFunds.pool + terms.makerFee, + }, + fee: { addr: 'agoric114', balance: terms.contractFee }, + settlement: { + addr: 'agoric113', + balance: 0, + }, + }; + t.log('actual result', actual); + t.deepEqual(actual, expected); +}); From c7a8b9695260e70ddb558295e0e4ee95ee5d3bf7 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Thu, 10 Oct 2024 14:03:45 -0500 Subject: [PATCH 09/48] refactor: move contract logic to .contract.js file --- .../src/examples/quickSend.contract.js | 71 +++- .../test/examples/quickSend-tx.test.ts | 381 ++---------------- packages/orchestration/tools/agoric-mock.js | 75 ++++ .../orchestration/tools/cosmoverse-mock.js | 58 +++ packages/orchestration/tools/eth-mock.ts | 98 +++++ packages/orchestration/tools/noble-mock.js | 44 ++ 6 files changed, 365 insertions(+), 362 deletions(-) create mode 100644 packages/orchestration/tools/agoric-mock.js create mode 100644 packages/orchestration/tools/cosmoverse-mock.js create mode 100644 packages/orchestration/tools/eth-mock.ts create mode 100644 packages/orchestration/tools/noble-mock.js diff --git a/packages/orchestration/src/examples/quickSend.contract.js b/packages/orchestration/src/examples/quickSend.contract.js index b3eb74c7a01..31f1f1c3f63 100644 --- a/packages/orchestration/src/examples/quickSend.contract.js +++ b/packages/orchestration/src/examples/quickSend.contract.js @@ -1,29 +1,72 @@ -import { Far } from '@endo/far'; +import { NobleCalc } from '../../tools/noble-mock.js'; +import { AgoricCalc } from '../../tools/agoric-mock.js'; /** * @import {Baggage} from '@agoric/vat-data'; */ /** - * @param {ZCF} zcf - * @param {{}} privateArgs + * @param {ZCF<{ makerFee: number; contractFee: number }>} zcf + * @param {{ orch: any; storageNode: any; t: any }} privateArgs * @param {Baggage} baggage - * @returns */ -export const start = (zcf, privateArgs, baggage) => { - /** @type {OfferHandler} */ - const handler = Far('OfferHandler', { - handle: seat => { - seat.exit(); - return 'TODO'; +export const start = async (zcf, privateArgs, baggage) => { + const { orch, storageNode, t } = privateArgs; + const { contractFee, makerFee } = zcf.getTerms(); + const fundingPool = await orch.makeLocalAccount(); + const settlement = await orch.makeLocalAccount(); + const feeAccount = await orch.makeLocalAccount(); + + const { nextLabel: next } = t.context; + storageNode.setValue(settlement.getAddress()); + + const publicFacet = harden({ + getPoolAddress: async () => fundingPool.getAddress(), + getSettlementAddress: async () => settlement.getAddress(), + getFeeAddress: async () => feeAccount.getAddress(), + }); + + await settlement.tap( + harden({ + onReceive: async ({ amount, extra }) => { + t.log(next(), 'tap onReceive', { amount }); + // XXX partial failure? + await Promise.all([ + settlement.send({ + dest: fundingPool.getAddress(), + amount: amount - contractFee, + }), + settlement.send({ + dest: feeAccount.getAddress(), + amount: contractFee, + }), + ]); + }, + }), + ); + + const watcherFacet = harden({ + releaseAdvance: async ({ amount, dest, nobleFwd }) => { + t.log(next(), 'contract.releaseAdvance', { amount, dest }); + t.is( + NobleCalc.fwdAddressFor( + AgoricCalc.virtualAddressFor(fundingPool.getAddress(), dest), + ), + nobleFwd, + ); + const advance = amount - makerFee - contractFee; + await fundingPool.transfer({ dest, amount: advance }); }, }); - harden(handler); + const creatorFacet = harden({ + // TODO: continuing invitation pattern + getWatcherFacet: () => watcherFacet, + }); + return harden({ - publicFacet: Far('QuickSend API', { - makeInvitation: () => zcf.makeInvitation(handler, 'request destination'), - }), + publicFacet, + creatorFacet, }); }; harden(start); diff --git a/packages/orchestration/test/examples/quickSend-tx.test.ts b/packages/orchestration/test/examples/quickSend-tx.test.ts index 77edc1d6e6a..4f8205424b1 100644 --- a/packages/orchestration/test/examples/quickSend-tx.test.ts +++ b/packages/orchestration/test/examples/quickSend-tx.test.ts @@ -1,27 +1,32 @@ import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js'; import { createRequire } from 'node:module'; +import { + AgoricCalc, + makeOrchestration, + makeVStorage, + withVTransfer, +} from '../../tools/agoric-mock.js'; +import { makeCCTP, NobleCalc, withForwarding } from '../../tools/noble-mock.js'; +import { makeCosmosChain, pickChain } from '../../tools/cosmoverse-mock.js'; +import { + makeERC20, + makeEthChain, + makeEventCounter, +} from '../../tools/eth-mock.js'; +import type { EthChain } from '../../tools/eth-mock.js'; +import { start as startContract } from '../../src/examples/quickSend.contract.js'; const nodeRequire = createRequire(import.meta.url); -const contractName = 'sendAnywhere'; +const contractName = 'quickSend'; const contractFile = nodeRequire.resolve( - `../../src/examples/send-anywhere.contract.js`, + `../../src/examples/quickSend.contract.js`, ); type StartFn = typeof import('../../src/examples/quickSend.contract.js').start; const todo = () => assert.fail('TODO'); -const NobleCalc = harden({ - fwdAddressFor: (dest: string) => `noble1${dest.length}${dest.slice(-4)}`, -}); - -const AgoricCalc = harden({ - virtualAddressFor: (base: string, dest: string) => `${base}+${dest}`, - isVirtualAddress: addr => addr.includes('+'), - virtualAddressParts: addr => addr.split('+'), -}); - const logged = (it, label) => { console.debug(label, it); return it; @@ -36,136 +41,6 @@ test.before(async t => { t.context = { startLabels, nextLabel }; }); -const makeEventCounter = ({ setTimeout }) => { - let current = 0; - let going = true; - async function* eachEvent() { - await null; - for (; ; current += 1) { - if (!going) break; - yield current; - await new Promise(resolve => setTimeout(resolve, 100)); - } - } - const iter = eachEvent(); - - return harden({ - getCurrent: () => current, - cancel: () => (going = false), - [Symbol.asyncIterator]: () => iter, - }); -}; - -const makeCosmosChain = (prefix, t) => { - let nonce = 10; - const balances = new Map(); - const whaleAddress = `${prefix}${(nonce += 1)}`; - balances.set(whaleAddress, 1_000_000); - const { nextLabel: next } = t.context; - - const burn = async ({ from, amount }) => { - const fromPre = balances.get(from) || 0; - const fromPost = fromPre - amount; - fromPost >= 0 || assert.fail(`${from} overdrawn: ${fromPre} - ${amount}`); - balances.set(from, fromPost); - }; - - const mint = async ({ dest, amount }) => { - const destPre = balances.get(dest) || 0; - const destPost = destPre + amount; - balances.set(dest, destPost); - }; - - return harden({ - prefix, - whaleAddress, - makeAccount: async () => { - const address = `${prefix}${(nonce += 1)}`; - balances.set(address, 0); - t.log('chain', prefix, 'makeAccount', address); - return address; - }, - getBalance: async dest => balances.get(dest), - send: async ({ amount, from, dest, quiet }) => { - t.true(dest.startsWith(prefix), dest); - await burn({ from, amount }); - await mint({ dest, amount }); - const label = quiet ? '' : next(); - t.log(label, dest, 'balance +=', amount, '=', balances.get(dest)); - }, - }); -}; - -const pickChain = (chains, dest) => { - const pfxLen = dest.indexOf('1'); - const pfx = dest.slice(0, pfxLen); - const chain = chains[pfx]; - assert(chain, dest); - return chain; -}; - -const ibcTransfer = async (chains, { amount, from, dest, t }) => { - const { nextLabel: next } = t.context; - t.log(next(), 'ibc transfer', amount, 'to', dest); - const chainSrc = pickChain(chains, from); - const chainDest = pickChain(chains, dest); - const burn = `${chainSrc.prefix}IBCburn`; - const quiet = true; - await chainSrc.send({ amount, from, dest: burn, quiet }); - await chainDest.send({ amount, from: chainDest.whaleAddress, dest, quiet }); -}; - -const withForwarding = (chain, chains, t) => { - const destOf = new Map(); - const { nextLabel: next } = t.context; - return harden({ - ...chain, - provideForwardingAccount: async dest => { - const address = NobleCalc.fwdAddressFor(dest); - destOf.set(address, dest); - t.log('x/forwarding fwd', address, '->', dest); - return address; - }, - send: async ({ amount, from, dest }) => { - await chain.send({ amount, from, dest, quiet: true }); - if (!destOf.has(dest)) return; - const fwd = destOf.get(dest); - t.log(next(), 'fwd', { amount, dest, fwd }); - await ibcTransfer(chains, { amount, from: dest, dest: fwd, t }); - }, - }); -}; - -const withVTransfer = (chain, t) => { - const addrToTap = new Map(); - return harden({ - ...chain, - register: async ({ addr, handler }) => { - t.log('vtransfer register', { addr, handler }); - !addrToTap.has(addr) || assert.fail('already registered'); - addrToTap.set(addr, handler); - }, - send: async ({ amount, from, dest }) => { - const [agAddr, extra] = AgoricCalc.isVirtualAddress(dest) - ? AgoricCalc.virtualAddressParts(dest) - : [dest, undefined]; - const quiet = true; - const result = await chain.send({ amount, from, dest: agAddr, quiet }); - - if (extra === undefined) return result; - t.log('vtransfer to virtual address', { agAddr, extra }); - if (!addrToTap.has(agAddr)) return result; - - const handler = addrToTap.get(agAddr); - void handler.onReceive({ amount, extra }).catch(err => { - console.error('onRecieve rejected', err); - }); - - return result; - }, - }); -}; - const makeUser = ({ nobleApp, ethereum, myAddr, cctpAddr }) => harden({ doTransfer: async (amount, dest) => { @@ -240,109 +115,11 @@ const makeNobleExpress = ({ agoricWatcher, chain, t, agoricRpc }) => { }); }; -const makeERC20 = (t, msg0, supply) => { - const balances = new Map(); - balances.set(msg0.sender, supply); - const balanceOf = account => balances.get(account) || 0; - return harden({ - balanceOf, - transfer: (msg, dest, numTokens) => { - const srcBal = balanceOf(msg.sender); - t.log('ERC20 transfer', { sender: msg.sender, srcBal, numTokens, dest }); - t.true(srcBal > numTokens); - const destBal = balanceOf(dest); - balances.set(msg.sender, srcBal - numTokens); - balances.set(dest, destBal + numTokens); - }, - }); -}; - -const makeCCTP = ({ t, usdc, noble, events }) => { - const { nextLabel: next } = t.context; - return harden({ - bridge: (msg, { dest, amount }) => { - t.regex(dest, /^noble/); - t.log(next(), 'cctp.bridge:', { msg, dest }); - usdc.transfer(msg, '0x0000', amount); // burn - const t0 = events.getCurrent(); - void (async () => { - t.log('waiting 3 blocks (TODO: 20min) before minting on noble'); - for await (const te of events) { - if (te - t0 > 3) break; - } - noble.send({ amount, from: 'noble1mint', dest }); - })(); - }, - }); -}; - -type EthAddr = `0x${string}`; -type EthData = Record; -type EthMsgInfo = { sender: EthAddr; value?: number }; -type EthCallTx = { - txId: number; - msg: EthMsgInfo; - contractAddress: EthAddr; - method: string; - args: EthData[]; -}; - -const makeEthChain = (heightInitial: number, { t, setTimeout }) => { - let nonce = 10; - let height = heightInitial; - const contracts = new Map(); - const mempool: EthCallTx[] = []; - const blocks: EthCallTx[][] = [[]]; - const emptyBlock = harden([]); - - let going = true; - const advanceBlock = () => { - blocks.push(harden([...mempool])); - mempool.splice(0, mempool.length); - height += 1; - t.log('eth advance to block', height); - }; - void (async () => { - const ticks = makeEventCounter({ setTimeout }); - for await (const tick of ticks) { - if (!going) break; - advanceBlock(); - } - })(); - - const { nextLabel: next } = t.context; - return harden({ - deployContract: async c => { - const address = `0x${(nonce += 1)}`; - contracts.set(address, c); - return address; - }, - call: async ( - msg: EthMsgInfo, - addr: EthAddr, - method: string, - args: EthData[], - ) => { - const txId = nonce; - nonce += 1; - mempool.push({ txId, msg, contractAddress: addr, method, args }); - t.log(next(), 'eth call', addr, '.', method, '(', ...args, ')'); - const contract = contracts.get(addr); - const result = contract[method](msg, ...args); - t.is(result, undefined); - }, - currentHeight: () => height - 1, - getBlock: (h: number) => blocks[h - heightInitial] || emptyBlock, - stop: () => (going = false), - }); -}; -type EthChain = ReturnType; - const makeAgoricWatcher = ({ t, ethereum: eth, cctpAddr, - contract, + watcherFacet, setTimeout, }) => { const ethereum: EthChain = eth; // TODO: concise typing @@ -366,7 +143,7 @@ const makeAgoricWatcher = ({ pending.splice(ix, 1); t.log(next(), 'watcher checked', tx.txId); t.log(next(), 'watcher confirmed', item); - await contract.releaseAdvance(item); + await watcherFacet.releaseAdvance(item); // TODO: continuing offerSpec } }; @@ -393,94 +170,6 @@ const makeAgoricWatcher = ({ }); }; -// funding pool is a local account -const makeOrchestration = (t, chains) => { - const { nextLabel: next } = t.context; - return harden({ - makeLocalAccount: async () => { - const addr = await chains.agoric.makeAccount(); - return harden({ - getAddress: () => addr, - transfer: async ({ amount, dest }) => { - t.log(next(), 'orch acct', addr, 'txfr', amount, 'to', dest); - await ibcTransfer(chains, { amount, dest, from: addr, t }); - }, - send: async ({ amount, dest }) => { - t.log(next(), 'orch acct', addr, 'send', amount, 'to', dest); - await chains.agoric.send({ amount, dest, from: addr }); - }, - tap: async handler => { - await chains.agoric.register({ addr, handler }); - }, - }); - }, - }); -}; - -const makeVStorage = () => { - const data = new Map(); - const storageNode = harden({ - makeChildNode: path => - harden({ - setValue: value => data.set(path, value), - }), - }); - const rpc = harden({ - getData: async path => data.get(path), - }); - - return { storageNode, rpc }; -}; - -const makeQuickSend = async ({ - t, - privateArgs: { orch, storageNode }, - terms: { makerFee, contractFee }, -}) => { - const fundingPool = await orch.makeLocalAccount(); - const settlement = await orch.makeLocalAccount(); - const feeAccount = await orch.makeLocalAccount(); - - const { nextLabel: next } = t.context; - storageNode.setValue(settlement.getAddress()); - - await settlement.tap( - harden({ - onReceive: async ({ amount, extra }) => { - t.log(next(), 'tap onReceive', { amount }); - // XXX partial failure? - await Promise.all([ - settlement.send({ - dest: fundingPool.getAddress(), - amount: amount - contractFee, - }), - settlement.send({ - dest: feeAccount.getAddress(), - amount: contractFee, - }), - ]); - }, - }), - ); - - return harden({ - releaseAdvance: async ({ amount, dest, nobleFwd }) => { - t.log(next(), 'contract.releaseAdvance', { amount, dest }); - t.is( - NobleCalc.fwdAddressFor( - AgoricCalc.virtualAddressFor(fundingPool.getAddress(), dest), - ), - nobleFwd, - ); - const advance = amount - makerFee - contractFee; - await fundingPool.transfer({ dest, amount: advance }); - }, - getPoolAddress: async () => fundingPool.getAddress(), - getSettlementAddress: async () => settlement.getAddress(), - getFeeAddress: async () => feeAccount.getAddress(), - }); -}; - const terms = { makerFee: 3, contractFee: 2 }; const startFunds = { usdcMint: 10_000, @@ -498,7 +187,7 @@ const setup = async (t, io) => { // }, // }; t.log('-- SETUP...'); - const ethereum = makeEthChain(9876, { t, setTimeout }); + // aka cosmoverse let chains = harden({ agoric: withVTransfer(makeCosmosChain('agoric1', t), t), noble: makeCosmosChain('noble1', t), @@ -514,18 +203,16 @@ const setup = async (t, io) => { const storageNode = chainStorage.makeChildNode( 'published.quickSend.settlementBase', ); - const contract = await makeQuickSend({ - t, - privateArgs: { orch, storageNode }, - terms, - }); - const poolAddr = await contract.getPoolAddress(); + const zcf: ZCF = harden({ getTerms: () => terms }) as any; + const contract = await startContract(zcf, { t, orch, storageNode }, {}); + const poolAddr = await contract.publicFacet.getPoolAddress(); await chains.agoric.send({ amount: startFunds.pool, from: chains.agoric.whaleAddress, // TODO: market maker contributes dest: poolAddr, }); + const ethereum = makeEthChain(9876, { t, setTimeout }); const usdc = makeERC20(t, { sender: '0xcircle' }, startFunds.usdcMint); const usdcAddr = await ethereum.deployContract(usdc); await chains.noble.send({ @@ -542,11 +229,12 @@ const setup = async (t, io) => { }); const cctpAddr = await ethereum.deployContract(cctp); + const watcherFacet = await contract.creatorFacet.getWatcherFacet(); // TODO: cont. const agoricWatcher = makeAgoricWatcher({ t, ethereum, cctpAddr, - contract, + watcherFacet, setTimeout: globalThis.setTimeout, }); const nobleService = makeNobleExpress({ @@ -574,19 +262,16 @@ const setup = async (t, io) => { t.log('-- SETUP done.'); t.context.startLabels(); + const nobleApp = await makeNobleApp({ t, nobleService, - chains, + chains, // to watch balance at EUD agoricRpc, setTimeout: globalThis.setTimeout, }); - const ursula = makeUser({ - nobleApp, - ethereum, - myAddr: '0xUrsula', - cctpAddr, - }); + + const ursula = makeUser({ nobleApp, ethereum, myAddr: '0xUrsula', cctpAddr }); return { chains, ursula, quiesce, contract, usdc }; }; @@ -600,9 +285,9 @@ test('tx lifecycle', async t => { await quiesce(); - const poolAddr = await contract.getPoolAddress(); - const feeAddr = await contract.getFeeAddress(); - const settlementAddr = await contract.getSettlementAddress(); + const poolAddr = await contract.publicFacet.getPoolAddress(); + const feeAddr = await contract.publicFacet.getFeeAddress(); + const settlementAddr = await contract.publicFacet.getSettlementAddress(); const actual = { user: { addr: '0xUrsula', diff --git a/packages/orchestration/tools/agoric-mock.js b/packages/orchestration/tools/agoric-mock.js new file mode 100644 index 00000000000..e9b0e6763b7 --- /dev/null +++ b/packages/orchestration/tools/agoric-mock.js @@ -0,0 +1,75 @@ +import { ibcTransfer } from './cosmoverse-mock.js'; + +export const AgoricCalc = harden({ + virtualAddressFor: (base, dest) => `${base}+${dest}`, + isVirtualAddress: addr => addr.includes('+'), + virtualAddressParts: addr => addr.split('+'), +}); + +export const makeOrchestration = (t, chains) => { + const { nextLabel: next } = t.context; + return harden({ + makeLocalAccount: async () => { + const addr = await chains.agoric.makeAccount(); + return harden({ + getAddress: () => addr, + transfer: async ({ amount, dest }) => { + t.log(next(), 'orch acct', addr, 'txfr', amount, 'to', dest); + await ibcTransfer(chains, { amount, dest, from: addr, t }); + }, + send: async ({ amount, dest }) => { + t.log(next(), 'orch acct', addr, 'send', amount, 'to', dest); + await chains.agoric.send({ amount, dest, from: addr }); + }, + tap: async handler => { + await chains.agoric.register({ addr, handler }); + }, + }); + }, + }); +}; + +export const makeVStorage = () => { + const data = new Map(); + const storageNode = harden({ + makeChildNode: path => + harden({ + setValue: value => data.set(path, value), + }), + }); + const rpc = harden({ + getData: async path => data.get(path), + }); + + return { storageNode, rpc }; +}; + +export const withVTransfer = (chain, t) => { + const addrToTap = new Map(); + return harden({ + ...chain, + register: async ({ addr, handler }) => { + t.log('vtransfer register', { addr, handler }); + !addrToTap.has(addr) || assert.fail('already registered'); + addrToTap.set(addr, handler); + }, + send: async ({ amount, from, dest }) => { + const [agAddr, extra] = AgoricCalc.isVirtualAddress(dest) + ? AgoricCalc.virtualAddressParts(dest) + : [dest, undefined]; + const quiet = true; + const result = await chain.send({ amount, from, dest: agAddr, quiet }); + + if (extra === undefined) return result; + t.log('vtransfer to virtual address', { agAddr, extra }); + if (!addrToTap.has(agAddr)) return result; + + const handler = addrToTap.get(agAddr); + void handler.onReceive({ amount, extra }).catch(err => { + console.error('onRecieve rejected', err); + }); + + return result; + }, + }); +}; diff --git a/packages/orchestration/tools/cosmoverse-mock.js b/packages/orchestration/tools/cosmoverse-mock.js new file mode 100644 index 00000000000..2ab6280098e --- /dev/null +++ b/packages/orchestration/tools/cosmoverse-mock.js @@ -0,0 +1,58 @@ +export const makeCosmosChain = (prefix, t) => { + let nonce = 10; + const balances = new Map(); + const whaleAddress = `${prefix}${(nonce += 1)}`; + balances.set(whaleAddress, 1_000_000); + const { nextLabel: next } = t.context; + + const burn = async ({ from, amount }) => { + const fromPre = balances.get(from) || 0; + const fromPost = fromPre - amount; + fromPost >= 0 || assert.fail(`${from} overdrawn: ${fromPre} - ${amount}`); + balances.set(from, fromPost); + }; + + const mint = async ({ dest, amount }) => { + const destPre = balances.get(dest) || 0; + const destPost = destPre + amount; + balances.set(dest, destPost); + }; + + return harden({ + prefix, + whaleAddress, + makeAccount: async () => { + const address = `${prefix}${(nonce += 1)}`; + balances.set(address, 0); + t.log('chain', prefix, 'makeAccount', address); + return address; + }, + getBalance: async dest => balances.get(dest), + send: async ({ amount, from, dest, quiet = false }) => { + t.true(dest.startsWith(prefix), dest); + await burn({ from, amount }); + await mint({ dest, amount }); + const label = quiet ? '' : next(); + t.log(label, dest, 'balance +=', amount, '=', balances.get(dest)); + }, + }); +}; + +export const pickChain = (chains, dest) => { + const pfxLen = dest.indexOf('1'); + const pfx = dest.slice(0, pfxLen); + const chain = chains[pfx]; + assert(chain, dest); + return chain; +}; + +export const ibcTransfer = async (chains, { amount, from, dest, t }) => { + const { nextLabel: next } = t.context; + t.log(next(), 'ibc transfer', amount, 'to', dest); + const chainSrc = pickChain(chains, from); + const chainDest = pickChain(chains, dest); + const burn = `${chainSrc.prefix}IBCburn`; + const quiet = true; + await chainSrc.send({ amount, from, dest: burn, quiet }); + await chainDest.send({ amount, from: chainDest.whaleAddress, dest, quiet }); +}; diff --git a/packages/orchestration/tools/eth-mock.ts b/packages/orchestration/tools/eth-mock.ts new file mode 100644 index 00000000000..c0c8bf0e3d5 --- /dev/null +++ b/packages/orchestration/tools/eth-mock.ts @@ -0,0 +1,98 @@ +type EthAddr = `0x${string}`; +type EthData = Record; +type EthMsgInfo = { sender: EthAddr; value?: number }; +type EthCallTx = { + txId: number; + msg: EthMsgInfo; + contractAddress: EthAddr; + method: string; + args: EthData[]; +}; + +export const makeEventCounter = ({ setTimeout }) => { + let current = 0; + let going = true; + async function* eachEvent() { + await null; + for (; ; current += 1) { + if (!going) break; + yield current; + await new Promise(resolve => setTimeout(resolve, 100)); + } + } + const iter = eachEvent(); + + return harden({ + getCurrent: () => current, + cancel: () => (going = false), + [Symbol.asyncIterator]: () => iter, + }); +}; + +export const makeEthChain = (heightInitial: number, { t, setTimeout }) => { + let nonce = 10; + let height = heightInitial; + const contracts = new Map(); + const mempool: EthCallTx[] = []; + const blocks: EthCallTx[][] = [[]]; + const emptyBlock = harden([]); + + let going = true; + const advanceBlock = () => { + blocks.push(harden([...mempool])); + mempool.splice(0, mempool.length); + height += 1; + t.log('eth advance to block', height); + }; + void (async () => { + const ticks = makeEventCounter({ setTimeout }); + for await (const tick of ticks) { + if (!going) break; + advanceBlock(); + } + })(); + + const { nextLabel: next } = t.context; + return harden({ + deployContract: async c => { + const address: EthAddr = `0x${(nonce += 1)}`; + contracts.set(address, c); + return address; + }, + call: async ( + msg: EthMsgInfo, + addr: EthAddr, + method: string, + args: EthData[], + ) => { + const txId = nonce; + nonce += 1; + mempool.push({ txId, msg, contractAddress: addr, method, args }); + t.log(next(), 'eth call', addr, '.', method, '(', ...args, ')'); + const contract = contracts.get(addr); + const result = contract[method](msg, ...args); + t.is(result, undefined); + }, + currentHeight: () => height - 1, + getBlock: (h: number) => blocks[h - heightInitial] || emptyBlock, + stop: () => (going = false), + }); +}; +export type EthChain = ReturnType; + +export const makeERC20 = (t, msg0, supply) => { + const balances = new Map(); + balances.set(msg0.sender, supply); + const balanceOf = account => balances.get(account) || 0; + return harden({ + balanceOf, + transfer: (msg, dest, numTokens) => { + const srcBal = balanceOf(msg.sender); + t.log('ERC20 transfer', { sender: msg.sender, srcBal, numTokens, dest }); + t.true(srcBal > numTokens); + const destBal = balanceOf(dest); + balances.set(msg.sender, srcBal - numTokens); + balances.set(dest, destBal + numTokens); + }, + }); +}; diff --git a/packages/orchestration/tools/noble-mock.js b/packages/orchestration/tools/noble-mock.js new file mode 100644 index 00000000000..f8fc257375e --- /dev/null +++ b/packages/orchestration/tools/noble-mock.js @@ -0,0 +1,44 @@ +import { ibcTransfer } from './cosmoverse-mock.js'; + +export const NobleCalc = harden({ + fwdAddressFor: dest => `noble1${dest.length}${dest.slice(-4)}`, +}); +export const withForwarding = (chain, chains, t) => { + const destOf = new Map(); + const { nextLabel: next } = t.context; + return harden({ + ...chain, + provideForwardingAccount: async dest => { + const address = NobleCalc.fwdAddressFor(dest); + destOf.set(address, dest); + t.log('x/forwarding fwd', address, '->', dest); + return address; + }, + send: async ({ amount, from, dest, quiet = false }) => { + await chain.send({ amount, from, dest, quiet: true }); + if (!destOf.has(dest)) return; + const fwd = destOf.get(dest); + t.log(next(), 'fwd', { amount, dest, fwd }); + await ibcTransfer(chains, { amount, from: dest, dest: fwd, t }); + }, + }); +}; + +export const makeCCTP = ({ t, usdc, noble, events }) => { + const { nextLabel: next } = t.context; + return harden({ + bridge: (msg, { dest, amount }) => { + t.regex(dest, /^noble/); + t.log(next(), 'cctp.bridge:', { msg, dest }); + usdc.transfer(msg, '0x0000', amount); // burn + const t0 = events.getCurrent(); + void (async () => { + t.log('waiting 3 blocks (TODO: 20min) before minting on noble'); + for await (const te of events) { + if (te - t0 > 3) break; + } + noble.send({ amount, from: 'noble1mint', dest }); + })(); + }, + }); +}; From eb59f53e9efba8e9c6144b1d39bb407a5d02c983 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Fri, 11 Oct 2024 14:26:50 -0500 Subject: [PATCH 10/48] test: refine quickSend prototype - use more of real Orchestration API: tap -> monitorTransfers - contract: use withOrchestration; factor out flows - use public topics - use (more of) continuing invitation pattern - use BigInt, Amount<'nat'> --- .../src/examples/quickSend.contract.js | 93 ++++++------- .../src/examples/quickSend.flows.js | 105 +++++++++++++++ packages/orchestration/src/utils/address.js | 15 +++ .../test/examples/quickSend-tx.test.ts | 123 ++++++++++++------ packages/orchestration/tools/agoric-mock.js | 76 +++++++---- .../orchestration/tools/cosmoverse-mock.js | 9 +- packages/orchestration/tools/eth-mock.ts | 2 +- packages/orchestration/tools/noble-mock.js | 4 +- 8 files changed, 302 insertions(+), 125 deletions(-) create mode 100644 packages/orchestration/src/examples/quickSend.flows.js diff --git a/packages/orchestration/src/examples/quickSend.contract.js b/packages/orchestration/src/examples/quickSend.contract.js index 31f1f1c3f63..e1fc278dfb4 100644 --- a/packages/orchestration/src/examples/quickSend.contract.js +++ b/packages/orchestration/src/examples/quickSend.contract.js @@ -1,72 +1,55 @@ -import { NobleCalc } from '../../tools/noble-mock.js'; -import { AgoricCalc } from '../../tools/agoric-mock.js'; +import { BrandShape } from '@agoric/ertp/src/typeGuards.js'; +import { M } from '@endo/patterns'; +import { withOrchestration } from '../utils/start-helper.js'; +import * as flows from './quickSend.flows.js'; /** - * @import {Baggage} from '@agoric/vat-data'; + * @import {OrchestrationPowers, OrchestrationTools} from '../utils/start-helper.js'; + * @import {Zone} from '@agoric/zone'; */ +const NatAmountShape = { brand: BrandShape, value: M.nat() }; +export const meta = { + customTermsShape: { + contractFee: NatAmountShape, + makerFee: NatAmountShape, + }, +}; +harden(meta); + /** - * @param {ZCF<{ makerFee: number; contractFee: number }>} zcf - * @param {{ orch: any; storageNode: any; t: any }} privateArgs - * @param {Baggage} baggage + * @typedef {{ makerFee: Amount<'nat'>; contractFee: Amount<'nat'> }} QuickSendTerms + * @param {ZCF} zcf + * @param {OrchestrationPowers & { + * marshaller: Marshaller; + * }} privateArgs + * @param {Zone} zone + * @param {OrchestrationTools & { t: any }} tools */ -export const start = async (zcf, privateArgs, baggage) => { - const { orch, storageNode, t } = privateArgs; - const { contractFee, makerFee } = zcf.getTerms(); - const fundingPool = await orch.makeLocalAccount(); - const settlement = await orch.makeLocalAccount(); - const feeAccount = await orch.makeLocalAccount(); - - const { nextLabel: next } = t.context; - storageNode.setValue(settlement.getAddress()); - - const publicFacet = harden({ - getPoolAddress: async () => fundingPool.getAddress(), - getSettlementAddress: async () => settlement.getAddress(), - getFeeAddress: async () => feeAccount.getAddress(), - }); - - await settlement.tap( - harden({ - onReceive: async ({ amount, extra }) => { - t.log(next(), 'tap onReceive', { amount }); - // XXX partial failure? - await Promise.all([ - settlement.send({ - dest: fundingPool.getAddress(), - amount: amount - contractFee, - }), - settlement.send({ - dest: feeAccount.getAddress(), - amount: contractFee, - }), - ]); - }, - }), - ); - - const watcherFacet = harden({ - releaseAdvance: async ({ amount, dest, nobleFwd }) => { - t.log(next(), 'contract.releaseAdvance', { amount, dest }); - t.is( - NobleCalc.fwdAddressFor( - AgoricCalc.virtualAddressFor(fundingPool.getAddress(), dest), - ), - nobleFwd, - ); - const advance = amount - makerFee - contractFee; - await fundingPool.transfer({ dest, amount: advance }); - }, +export const contract = async (zcf, privateArgs, zone, tools) => { + const { storageNode } = privateArgs; + const { t } = tools; + const terms = zcf.getTerms(); + + const { initAccounts } = tools.orchestrateAll(flows, { + storageNode, // TODO: storage node per init? + terms, + makeInvitation: tools.zcfTools.makeInvitation, + t, }); const creatorFacet = harden({ // TODO: continuing invitation pattern - getWatcherFacet: () => watcherFacet, + getWatcherInvitation: () => + zcf.makeInvitation(initAccounts, 'initAccounts'), }); return harden({ - publicFacet, + publicFacet: {}, creatorFacet, }); }; +harden(contract); + +export const start = withOrchestration(contract); harden(start); diff --git a/packages/orchestration/src/examples/quickSend.flows.js b/packages/orchestration/src/examples/quickSend.flows.js new file mode 100644 index 00000000000..53b6c014d01 --- /dev/null +++ b/packages/orchestration/src/examples/quickSend.flows.js @@ -0,0 +1,105 @@ +/** + * @import {ExecutionContext} from 'ava'; + * + * @import {GuestInterface, GuestOf} from '@agoric/async-flow'; + * @import {OrchestrationFlow, Orchestrator, ZcfTools} from '@agoric/orchestration'; + * @import {VTransferIBCEvent} from '@agoric/vats'; + * @import {ResolvedContinuingOfferResult} from '../../src/utils/zoe-tools.js'; + * @import {QuickSendTerms} from './quickSend.contract.js'; + */ + +import { AmountMath } from '@agoric/ertp/src/amountMath.js'; +import { BrandShape } from '@agoric/ertp/src/typeGuards.js'; +import { mustMatch, M } from '@endo/patterns'; +import { AgoricCalc, NobleCalc } from '../utils/address.js'; + +const NatAmountShape = { brand: BrandShape, value: M.nat() }; +const AddressShape = M.string(); // XXX + +/** + * @satisfies {OrchestrationFlow} + * @param {Orchestrator} orch + * @param {{ + * terms: QuickSendTerms & StandardTerms; + * t: ExecutionContext<{ nextLabel: Function }>; // XXX + * makeInvitation: Function; // XXX ZcfTools['makeInvitation'] + * }} ctx + * @param {ZCFSeat} _seat + * @param {{}} _offerArgs + */ +export const initAccounts = async (orch, ctx, _seat, _offerArgs) => { + const { nextLabel: next } = ctx.t.context; + const { makerFee, contractFee } = ctx.terms; + const { USDC } = ctx.terms.brands; + + const agoric = await orch.getChain('agoric'); + + const fundingPool = await agoric.makeAccount(); + const settlement = await agoric.makeAccount(); + const feeAccount = await agoric.makeAccount(); + + const CallDetailsShape = harden({ + amount: M.nat(), + dest: AddressShape, + nobleFwd: AddressShape, + }); + + const { add, make, subtract } = AmountMath; + + const registration = await settlement.monitorTransfers( + harden({ + /** + * @param {VTransferIBCEvent} event + * @returns {Promise} + */ + async receiveUpcall(event) { + // TODO: real encoding / decoding of packet + const { amount: inUSDC } = JSON.parse(event.packet.data); + const amount = make(USDC, BigInt(inUSDC)); + ctx.t.log(next(), 'tap onReceive', { amount }); + // XXX partial failure? + await Promise.all([ + settlement.send( + fundingPool.getAddress(), + subtract(amount, contractFee), + ), + settlement.send(feeAccount.getAddress(), contractFee), + ]); + }, + }), + ); + ctx.t.log('@@@what to do with registration?', registration); + + const handleCCTPCall = async (_s, offerArgs) => { + mustMatch(offerArgs, CallDetailsShape); + const { amount, dest, nobleFwd } = offerArgs; + ctx.t.log(next(), 'contract.reportCCTPCall', { amount, dest }); + assert.equal( + NobleCalc.fwdAddressFor( + AgoricCalc.virtualAddressFor(fundingPool.getAddress(), dest), + ), + nobleFwd, + ); + const withBrand = make(USDC, amount); + const advance = subtract(withBrand, add(makerFee, contractFee)); + await fundingPool.transfer(dest, advance); + }; + + /** @type {ResolvedContinuingOfferResult} */ + const watcherFacet = harden({ + publicSubscribers: { + fundingPool: (await fundingPool.getPublicTopics()).account, + settlement: (await settlement.getPublicTopics()).account, + feeAccount: (await feeAccount.getPublicTopics()).account, + }, + invitationMakers: { + ReportCCTPCall: () => + ctx.makeInvitation(handleCCTPCall, 'reportCCTPCall'), + }, + // TODO: skip continuing invitation gymnastics + actions: { handleCCTPCall }, + }); + + return watcherFacet; +}; +harden(initAccounts); diff --git a/packages/orchestration/src/utils/address.js b/packages/orchestration/src/utils/address.js index 1735040c401..7a9c0221e22 100644 --- a/packages/orchestration/src/utils/address.js +++ b/packages/orchestration/src/utils/address.js @@ -84,3 +84,18 @@ export const findAddressField = remoteAddressString => { } }; harden(findAddressField); + +export const AgoricCalc = harden({ + virtualAddressFor: (base, supplemental) => { + assert.typeof(base, 'string'); + assert.typeof(supplemental, 'string'); + return `${base}+${supplemental}`; + }, + isVirtualAddress: addr => addr.includes('+'), + virtualAddressParts: addr => addr.split('+'), // XXX 1st split only +}); + +export const NobleCalc = harden({ + // XXX mock only + fwdAddressFor: dest => `noble1${dest.length}${dest.slice(-4)}`, +}); diff --git a/packages/orchestration/test/examples/quickSend-tx.test.ts b/packages/orchestration/test/examples/quickSend-tx.test.ts index 4f8205424b1..e5ce9a593b8 100644 --- a/packages/orchestration/test/examples/quickSend-tx.test.ts +++ b/packages/orchestration/test/examples/quickSend-tx.test.ts @@ -1,13 +1,16 @@ import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js'; import { createRequire } from 'node:module'; +import { E, passStyleOf } from '@endo/far'; +import { objectMap } from '@endo/patterns'; +import { deeplyFulfilledObject } from '@agoric/internal'; +import { AmountMath, makeIssuerKit } from '@agoric/ertp'; import { - AgoricCalc, makeOrchestration, makeVStorage, withVTransfer, } from '../../tools/agoric-mock.js'; -import { makeCCTP, NobleCalc, withForwarding } from '../../tools/noble-mock.js'; +import { makeCCTP, withForwarding } from '../../tools/noble-mock.js'; import { makeCosmosChain, pickChain } from '../../tools/cosmoverse-mock.js'; import { makeERC20, @@ -15,7 +18,10 @@ import { makeEventCounter, } from '../../tools/eth-mock.js'; import type { EthChain } from '../../tools/eth-mock.js'; -import { start as startContract } from '../../src/examples/quickSend.contract.js'; +import type { QuickSendTerms } from '../../src/examples/quickSend.contract.js'; +import { contract as contractFn } from '../../src/examples/quickSend.contract.js'; +import { AgoricCalc, NobleCalc } from '../../src/utils/address.js'; +import { ResolvedContinuingOfferResult } from '../../src/utils/zoe-tools.js'; const nodeRequire = createRequire(import.meta.url); const contractName = 'quickSend'; @@ -143,7 +149,7 @@ const makeAgoricWatcher = ({ pending.splice(ix, 1); t.log(next(), 'watcher checked', tx.txId); t.log(next(), 'watcher confirmed', item); - await watcherFacet.releaseAdvance(item); // TODO: continuing offerSpec + await watcherFacet.actions.handleCCTPCall(null, item); // TODO: continuing offerSpec } }; @@ -170,22 +176,22 @@ const makeAgoricWatcher = ({ }); }; -const terms = { makerFee: 3, contractFee: 2 }; +const termValues = { makerFee: 3n, contractFee: 2n }; const startFunds = { - usdcMint: 10_000, - nobleMint: 100_000, - pool: 2000, - user: 150, + usdcMint: 10_000n, + nobleMint: 100_000n, + pool: 2000n, + user: 150n, }; const setup = async (t, io) => { - // const t = { - // ...t0, - // log: (...args) => { - // console.debug(...args); - // t0.log(...args); - // }, - // }; + // const t = { + // ...t0, + // log: (...args) => { + // console.debug(...args); + // t0.log(...args); + // }, + // }; t.log('-- SETUP...'); // aka cosmoverse let chains = harden({ @@ -200,17 +206,38 @@ const setup = async (t, io) => { const orch = makeOrchestration(t, chains); const { storageNode: chainStorage, rpc: agoricRpc } = makeVStorage(); - const storageNode = chainStorage.makeChildNode( + const storageNode = await E(chainStorage).makeChildNode( 'published.quickSend.settlementBase', ); - const zcf: ZCF = harden({ getTerms: () => terms }) as any; - const contract = await startContract(zcf, { t, orch, storageNode }, {}); - const poolAddr = await contract.publicFacet.getPoolAddress(); - await chains.agoric.send({ - amount: startFunds.pool, - from: chains.agoric.whaleAddress, // TODO: market maker contributes - dest: poolAddr, - }); + + const USDCe = makeIssuerKit('USDC'); + const terms = { + issuers: { USDC: USDCe.issuer }, + brands: { USDC: USDCe.brand }, + makerFee: AmountMath.make(USDCe.brand, termValues.makerFee), + contractFee: AmountMath.make(USDCe.brand, termValues.contractFee), + }; + const handlers = new Map(); + const zcf: ZCF = harden({ + getTerms: () => terms, + makeInvitation: (handler, desc) => { + handlers.set(desc, handler); + }, + }) as any; + const zone = {} as any; + const privateArgs: Parameters[1] = { + storageNode, + } as any; + const tools: Parameters[3] = { + t, + zcfTools: { makeInvitation: zcf.makeInvitation }, + orchestrateAll: (flows, ctx) => + objectMap( + flows, + (h, k) => (seat, offerArgs) => h(orch, ctx, seat, offerArgs), + ), + } as any; + const contract = await contractFn(zcf, privateArgs, zone, tools); const ethereum = makeEthChain(9876, { t, setTimeout }); const usdc = makeERC20(t, { sender: '0xcircle' }, startFunds.usdcMint); @@ -229,7 +256,27 @@ const setup = async (t, io) => { }); const cctpAddr = await ethereum.deployContract(cctp); - const watcherFacet = await contract.creatorFacet.getWatcherFacet(); // TODO: cont. + const toWatch = await E(contract.creatorFacet).getWatcherInvitation(); + const watcherFacet: ResolvedContinuingOfferResult = + await handlers.get('initAccounts')(); + console.debug(watcherFacet); + + const addrs = await deeplyFulfilledObject( + objectMap(watcherFacet.publicSubscribers, topic => + E(topic.subscriber) + .getUpdateSince() + .then(x => x.value), + ), + ); + console.debug(addrs); + await storageNode.setValue(addrs.settlement); + + await chains.agoric.send({ + amount: startFunds.pool, + from: chains.agoric.whaleAddress, // TODO: market maker contributes + dest: addrs.fundingPool, + }); + const agoricWatcher = makeAgoricWatcher({ t, ethereum, @@ -273,21 +320,23 @@ const setup = async (t, io) => { const ursula = makeUser({ nobleApp, ethereum, myAddr: '0xUrsula', cctpAddr }); - return { chains, ursula, quiesce, contract, usdc }; + return { chains, ursula, quiesce, contract, addrs, usdc }; }; test('tx lifecycle', async t => { const io = { setTimeout }; - const { chains, ursula, quiesce, contract, usdc } = await setup(t, io); + const { chains, ursula, quiesce, contract, addrs, usdc } = await setup(t, io); const destAddr = await chains.dydx.makeAccount(); // is this a prereq? - await ursula.doTransfer(100, destAddr); + await ursula.doTransfer(100n, destAddr); await quiesce(); - const poolAddr = await contract.publicFacet.getPoolAddress(); - const feeAddr = await contract.publicFacet.getFeeAddress(); - const settlementAddr = await contract.publicFacet.getSettlementAddress(); + const { + fundingPool: poolAddr, + feeAccount: feeAddr, + settlement: settlementAddr, + } = addrs; const actual = { user: { addr: '0xUrsula', @@ -316,21 +365,21 @@ test('tx lifecycle', async t => { user: { addr: '0xUrsula', start: startFunds.user, - balance: 50, + balance: 50n, }, dest: { addr: 'dydx112', - balance: 100 - terms.makerFee - terms.contractFee, + balance: 100n - termValues.makerFee - termValues.contractFee, }, pool: { addr: 'agoric112', start: startFunds.pool, - balance: startFunds.pool + terms.makerFee, + balance: startFunds.pool + termValues.makerFee, }, - fee: { addr: 'agoric114', balance: terms.contractFee }, + fee: { addr: 'agoric114', balance: termValues.contractFee }, settlement: { addr: 'agoric113', - balance: 0, + balance: 0n, }, }; t.log('actual result', actual); diff --git a/packages/orchestration/tools/agoric-mock.js b/packages/orchestration/tools/agoric-mock.js index e9b0e6763b7..9fe3103fc83 100644 --- a/packages/orchestration/tools/agoric-mock.js +++ b/packages/orchestration/tools/agoric-mock.js @@ -1,39 +1,61 @@ +import { AgoricCalc } from '../src/utils/address.js'; import { ibcTransfer } from './cosmoverse-mock.js'; -export const AgoricCalc = harden({ - virtualAddressFor: (base, dest) => `${base}+${dest}`, - isVirtualAddress: addr => addr.includes('+'), - virtualAddressParts: addr => addr.split('+'), -}); +/** + * @import {Remote} from '@agoric/vow'; + * @import {OrchestrationAccount} from '../src/orchestration-api.js'; + */ export const makeOrchestration = (t, chains) => { const { nextLabel: next } = t.context; - return harden({ - makeLocalAccount: async () => { - const addr = await chains.agoric.makeAccount(); - return harden({ - getAddress: () => addr, - transfer: async ({ amount, dest }) => { - t.log(next(), 'orch acct', addr, 'txfr', amount, 'to', dest); - await ibcTransfer(chains, { amount, dest, from: addr, t }); - }, - send: async ({ amount, dest }) => { - t.log(next(), 'orch acct', addr, 'send', amount, 'to', dest); - await chains.agoric.send({ amount, dest, from: addr }); + /** @returns {Promise>} */ + const makeAccount = async () => { + const addr = await chains.agoric.makeAccount(); + return harden({ + getAddress: () => addr, + getPublicTopics: async () => ({ + account: { + subscriber: { + subscribeAfter: async _ => ({ + value: addr, + publishCount: 1n, + head: /** @type {any} */ (null), + tail: /** @type {any} */ (null), + }), + getUpdateSince: async _ => ({ value: addr, updateCount: 1n }), + }, + storagePath: 'XXX', }, - tap: async handler => { - await chains.agoric.register({ addr, handler }); - }, - }); - }, + }), + transfer: async (dest, { value: amount }) => { + t.log(next(), 'orch acct', addr, 'txfr', amount, 'to', dest); + await ibcTransfer(chains, { amount, dest, from: addr, t }); + }, + send: async (dest, { value: amount }) => { + t.log(next(), 'orch acct', addr, 'send', amount, 'to', dest); + await chains.agoric.send({ amount, dest, from: addr }); + }, + monitorTransfers: async tap => { + await chains.agoric.register({ addr, handler: tap }); + }, + }); + }; + const chainHub = harden({ + agoric: { makeAccount }, + }); + return harden({ + getChain: name => chainHub[name], }); }; export const makeVStorage = () => { const data = new Map(); + /** @type {Remote} */ const storageNode = harden({ makeChildNode: path => + // @ts-expect-error mock harden({ + /** @param {string} value */ setValue: value => data.set(path, value), }), }); @@ -65,9 +87,13 @@ export const withVTransfer = (chain, t) => { if (!addrToTap.has(agAddr)) return result; const handler = addrToTap.get(agAddr); - void handler.onReceive({ amount, extra }).catch(err => { - console.error('onRecieve rejected', err); - }); + void handler + .receiveUpcall({ + packet: { data: JSON.stringify({ amount: Number(amount), extra }) }, + }) + .catch(err => { + console.error('receiveUpcall rejected', err); + }); return result; }, diff --git a/packages/orchestration/tools/cosmoverse-mock.js b/packages/orchestration/tools/cosmoverse-mock.js index 2ab6280098e..171c26a6241 100644 --- a/packages/orchestration/tools/cosmoverse-mock.js +++ b/packages/orchestration/tools/cosmoverse-mock.js @@ -2,18 +2,19 @@ export const makeCosmosChain = (prefix, t) => { let nonce = 10; const balances = new Map(); const whaleAddress = `${prefix}${(nonce += 1)}`; - balances.set(whaleAddress, 1_000_000); + balances.set(whaleAddress, 1_000_000n); const { nextLabel: next } = t.context; + const balanceOf = a => balances.get(a) || 0n; const burn = async ({ from, amount }) => { - const fromPre = balances.get(from) || 0; + const fromPre = balanceOf(from); const fromPost = fromPre - amount; - fromPost >= 0 || assert.fail(`${from} overdrawn: ${fromPre} - ${amount}`); + fromPost >= 0n || assert.fail(`${from} overdrawn: ${fromPre} - ${amount}`); balances.set(from, fromPost); }; const mint = async ({ dest, amount }) => { - const destPre = balances.get(dest) || 0; + const destPre = balanceOf(dest); const destPost = destPre + amount; balances.set(dest, destPost); }; diff --git a/packages/orchestration/tools/eth-mock.ts b/packages/orchestration/tools/eth-mock.ts index c0c8bf0e3d5..b0504541b51 100644 --- a/packages/orchestration/tools/eth-mock.ts +++ b/packages/orchestration/tools/eth-mock.ts @@ -83,7 +83,7 @@ export type EthChain = ReturnType; export const makeERC20 = (t, msg0, supply) => { const balances = new Map(); balances.set(msg0.sender, supply); - const balanceOf = account => balances.get(account) || 0; + const balanceOf = account => balances.get(account) || 0n; return harden({ balanceOf, transfer: (msg, dest, numTokens) => { diff --git a/packages/orchestration/tools/noble-mock.js b/packages/orchestration/tools/noble-mock.js index f8fc257375e..a3450f4cda1 100644 --- a/packages/orchestration/tools/noble-mock.js +++ b/packages/orchestration/tools/noble-mock.js @@ -1,8 +1,6 @@ +import { NobleCalc } from '../src/utils/address.js'; import { ibcTransfer } from './cosmoverse-mock.js'; -export const NobleCalc = harden({ - fwdAddressFor: dest => `noble1${dest.length}${dest.slice(-4)}`, -}); export const withForwarding = (chain, chains, t) => { const destOf = new Map(); const { nextLabel: next } = t.context; From 0131fea7a4ff439f6e2ab6f94e9d108c9d089a59 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Sun, 13 Oct 2024 22:35:14 -0500 Subject: [PATCH 11/48] test: accept watcher invitation bootstrap test (WIP) - core eval: - make invitation; send to depositFacet - take watcherAddress in config - pass cosmosInterchainService in privateArgs - provide USDC issuer / brand - contract: - use exoClass for tap; call settle flow - tools.t is optional - creatorFacet is remotable (exo) - flows: - really decode IBC event - make invitationMakers remotable with Far - WIP Error#2: invitationMakers: cannot yet send guest remotables "[Alleged: Maker]" --- .../test/bootstrapTests/quickSend.test.ts | 27 ++-- .../src/examples/quickSend.contract.js | 49 ++++-- .../src/examples/quickSend.flows.js | 153 +++++++++++------- .../src/proposals/start-quickSend.js | 109 +++++++++---- .../test/examples/quickSend-tx.test.ts | 9 +- 5 files changed, 229 insertions(+), 118 deletions(-) diff --git a/packages/boot/test/bootstrapTests/quickSend.test.ts b/packages/boot/test/bootstrapTests/quickSend.test.ts index 931abaefff5..d8e39416c4c 100644 --- a/packages/boot/test/bootstrapTests/quickSend.test.ts +++ b/packages/boot/test/bootstrapTests/quickSend.test.ts @@ -9,15 +9,18 @@ import { WalletFactoryDriver } from '../../tools/drivers.js'; const test: TestFn = anyTest; -test.skip('bootstrap', async t => { +test.before('bootstrap', async t => { t.context = await makeWalletFactoryContext( t, '@agoric/vm-config/decentral-itest-orchestration-config.json', ); + + // TODO: handle creating watcher smart wallet _after_ deploying contract + await t.context.walletFactoryDriver.provideSmartWallet('agoric1watcher'); }); test.after.always(t => t.context.shutdown?.()); -test.skip('deploy contract', async t => { +test.serial('deploy contract', async t => { const { agoricNamesRemotes, evalProposal, @@ -36,26 +39,28 @@ type SmartWallet = Awaited< ReturnType >; -test.skip('expedited send', async t => { - const alice = async (sw: SmartWallet) => { +test.serial('accept watcher invitation', async t => { + const { agoricNamesRemotes } = t.context; + + const william = async (sw: SmartWallet) => { await sw.executeOffer({ - id: 'request-dest-addr', + id: 'accept', invitationSpec: { - source: 'agoricContract', - instancePath: ['quickSend'], - callPipe: [['makeInvitation']], + source: 'purse', + instance: agoricNamesRemotes.instance.quickSend, + description: 'initAccounts', }, proposal: {}, }); const update = sw.getLatestUpdateRecord(); t.like(update, { updated: 'offerStatus', - status: { id: 'request-dest-addr', numWantsSatisfied: 1, result: 'TODO' }, + status: { id: 'accept', numWantsSatisfied: 1, result: 'TODO' }, }); }; const wd = - await t.context.walletFactoryDriver.provideSmartWallet('agoric1alice'); + await t.context.walletFactoryDriver.provideSmartWallet('agoric1watcher'); - await alice(wd); + await william(wd); }); diff --git a/packages/orchestration/src/examples/quickSend.contract.js b/packages/orchestration/src/examples/quickSend.contract.js index e1fc278dfb4..139006d8fb6 100644 --- a/packages/orchestration/src/examples/quickSend.contract.js +++ b/packages/orchestration/src/examples/quickSend.contract.js @@ -4,8 +4,13 @@ import { withOrchestration } from '../utils/start-helper.js'; import * as flows from './quickSend.flows.js'; /** + * @import {ExecutionContext} from 'ava'; // XXX + * + * @import {CopyRecord} from '@endo/pass-style'; * @import {OrchestrationPowers, OrchestrationTools} from '../utils/start-helper.js'; * @import {Zone} from '@agoric/zone'; + * @import {VTransferIBCEvent} from '@agoric/vats'; + * @import {QuickSendAccounts} from './quickSend.flows.js'; */ const NatAmountShape = { brand: BrandShape, value: M.nat() }; @@ -24,32 +29,52 @@ harden(meta); * marshaller: Marshaller; * }} privateArgs * @param {Zone} zone - * @param {OrchestrationTools & { t: any }} tools + * @param {OrchestrationTools & { + * t?: ExecutionContext<{ nextLabel: Function }>; + * }} tools */ export const contract = async (zcf, privateArgs, zone, tools) => { const { storageNode } = privateArgs; const { t } = tools; const terms = zcf.getTerms(); - const { initAccounts } = tools.orchestrateAll(flows, { - storageNode, // TODO: storage node per init? - terms, - makeInvitation: tools.zcfTools.makeInvitation, - t, - }); + const makeSettleTap = zone.exoClass( + 'SettleTap', + M.interface('SettleTap', { + receiveUpcall: M.call(M.record()).returns(M.undefined()), + }), + /** @param {QuickSendAccounts & CopyRecord} accts */ + accts => accts, + { + /** @param {VTransferIBCEvent & CopyRecord} event */ + receiveUpcall(event) { + const accts = this.state; + // eslint-disable-next-line no-use-before-define -- see orchestrate below + settle(accts, harden(event)); + }, + }, + ); + + const { makeInvitation } = tools.zcfTools; + const initAccounts = tools.orchestrate( + 'initAccounts', + { terms, makeSettleTap, makeInvitation, t }, + flows.initAccounts, + ); + + const settle = tools.orchestrate('settle', { terms, t }, flows.settle); - const creatorFacet = harden({ + const creatorFacet = zone.exo('QuickSend Creator', undefined, { // TODO: continuing invitation pattern getWatcherInvitation: () => zcf.makeInvitation(initAccounts, 'initAccounts'), }); - return harden({ - publicFacet: {}, - creatorFacet, - }); + return harden({ creatorFacet }); }; harden(contract); export const start = withOrchestration(contract); harden(start); + +/** @typedef {typeof start} QuickSendContractFn */ diff --git a/packages/orchestration/src/examples/quickSend.flows.js b/packages/orchestration/src/examples/quickSend.flows.js index 53b6c014d01..c2351d411e0 100644 --- a/packages/orchestration/src/examples/quickSend.flows.js +++ b/packages/orchestration/src/examples/quickSend.flows.js @@ -1,34 +1,56 @@ +import { AmountMath } from '@agoric/ertp/src/amountMath.js'; +import { atob } from '@endo/base64'; +import { Far } from '@endo/far'; +import { M, mustMatch } from '@endo/patterns'; +import { AgoricCalc, NobleCalc } from '../utils/address.js'; + /** * @import {ExecutionContext} from 'ava'; * - * @import {GuestInterface, GuestOf} from '@agoric/async-flow'; - * @import {OrchestrationFlow, Orchestrator, ZcfTools} from '@agoric/orchestration'; + * @import {Passable} from '@endo/pass-style'; + * @import {Guarded} from '@endo/exo'; + * @import {OrchestrationAccountI, OrchestrationFlow, Orchestrator, ZcfTools} from '@agoric/orchestration'; * @import {VTransferIBCEvent} from '@agoric/vats'; * @import {ResolvedContinuingOfferResult} from '../../src/utils/zoe-tools.js'; * @import {QuickSendTerms} from './quickSend.contract.js'; + * @import {FungibleTokenPacketData} from '@agoric/cosmic-proto/ibc/applications/transfer/v2/packet.js'; */ -import { AmountMath } from '@agoric/ertp/src/amountMath.js'; -import { BrandShape } from '@agoric/ertp/src/typeGuards.js'; -import { mustMatch, M } from '@endo/patterns'; -import { AgoricCalc, NobleCalc } from '../utils/address.js'; - -const NatAmountShape = { brand: BrandShape, value: M.nat() }; const AddressShape = M.string(); // XXX +const CallDetailsShape = harden({ + amount: M.nat(), + dest: AddressShape, + nobleFwd: AddressShape, +}); + +const { add, make, subtract } = AmountMath; + +/** + * @typedef {{ + * settlement: OrchestrationAccountI; + * fundingPool: OrchestrationAccountI; + * feeAccount: OrchestrationAccountI; + * }} QuickSendAccounts + */ + /** * @satisfies {OrchestrationFlow} * @param {Orchestrator} orch * @param {{ * terms: QuickSendTerms & StandardTerms; - * t: ExecutionContext<{ nextLabel: Function }>; // XXX - * makeInvitation: Function; // XXX ZcfTools['makeInvitation'] + * t?: ExecutionContext<{ nextLabel: Function }>; // XXX + * makeInvitation: Function; // XXX ZcfTools['makeInvitation']; + * makeSettleTap: ( + * accts: QuickSendAccounts, + * ) => Guarded<{ receiveUpcall: (event: VTransferIBCEvent) => void }>; * }} ctx * @param {ZCFSeat} _seat * @param {{}} _offerArgs */ export const initAccounts = async (orch, ctx, _seat, _offerArgs) => { - const { nextLabel: next } = ctx.t.context; + const { nextLabel: next = () => '#?' } = ctx.t?.context || {}; + const { log = console.log } = ctx.t || {}; const { makerFee, contractFee } = ctx.terms; const { USDC } = ctx.terms.brands; @@ -37,53 +59,28 @@ export const initAccounts = async (orch, ctx, _seat, _offerArgs) => { const fundingPool = await agoric.makeAccount(); const settlement = await agoric.makeAccount(); const feeAccount = await agoric.makeAccount(); + const accts = harden({ fundingPool, settlement, feeAccount }); + const registration = await ctx.makeSettleTap(accts); - const CallDetailsShape = harden({ - amount: M.nat(), - dest: AddressShape, - nobleFwd: AddressShape, - }); - - const { add, make, subtract } = AmountMath; - - const registration = await settlement.monitorTransfers( - harden({ - /** - * @param {VTransferIBCEvent} event - * @returns {Promise} - */ - async receiveUpcall(event) { - // TODO: real encoding / decoding of packet - const { amount: inUSDC } = JSON.parse(event.packet.data); - const amount = make(USDC, BigInt(inUSDC)); - ctx.t.log(next(), 'tap onReceive', { amount }); - // XXX partial failure? - await Promise.all([ - settlement.send( - fundingPool.getAddress(), - subtract(amount, contractFee), - ), - settlement.send(feeAccount.getAddress(), contractFee), - ]); - }, - }), - ); - ctx.t.log('@@@what to do with registration?', registration); + log('@@@what to do with registration?', registration); - const handleCCTPCall = async (_s, offerArgs) => { - mustMatch(offerArgs, CallDetailsShape); - const { amount, dest, nobleFwd } = offerArgs; - ctx.t.log(next(), 'contract.reportCCTPCall', { amount, dest }); - assert.equal( - NobleCalc.fwdAddressFor( - AgoricCalc.virtualAddressFor(fundingPool.getAddress(), dest), - ), - nobleFwd, - ); - const withBrand = make(USDC, amount); - const advance = subtract(withBrand, add(makerFee, contractFee)); - await fundingPool.transfer(dest, advance); - }; + /** @type {OfferHandler} */ + const handleCCTPCall = Far('Handler', { + handle: async (_s, offerArgs) => { + mustMatch(offerArgs, CallDetailsShape); + const { amount, dest, nobleFwd } = offerArgs; + log(next(), 'contract.reportCCTPCall', { amount, dest }); + assert.equal( + NobleCalc.fwdAddressFor( + AgoricCalc.virtualAddressFor(fundingPool.getAddress(), dest), + ), + nobleFwd, + ); + const withBrand = make(USDC, amount); + const advance = subtract(withBrand, add(makerFee, contractFee)); + await fundingPool.transfer(dest, advance); + }, + }); /** @type {ResolvedContinuingOfferResult} */ const watcherFacet = harden({ @@ -92,10 +89,10 @@ export const initAccounts = async (orch, ctx, _seat, _offerArgs) => { settlement: (await settlement.getPublicTopics()).account, feeAccount: (await feeAccount.getPublicTopics()).account, }, - invitationMakers: { + invitationMakers: Far('WatcherInvitationMakers', { ReportCCTPCall: () => ctx.makeInvitation(handleCCTPCall, 'reportCCTPCall'), - }, + }), // TODO: skip continuing invitation gymnastics actions: { handleCCTPCall }, }); @@ -103,3 +100,43 @@ export const initAccounts = async (orch, ctx, _seat, _offerArgs) => { return watcherFacet; }; harden(initAccounts); + +/** + * @satisfies {OrchestrationFlow} + * @param {Orchestrator} orch + * @param {{ + * terms: QuickSendTerms & StandardTerms; + * t?: ExecutionContext<{ nextLabel: Function }>; // XXX + * }} ctx + * @param {QuickSendAccounts & Passable} acct + * @param {VTransferIBCEvent & Passable} event + * @returns {Promise} + */ +export const settle = async (orch, ctx, acct, event) => { + const config = {}; // TODO + const { log = console.log } = ctx?.t || {}; + // ignore packets from unknown channels + if (event.packet.source_channel !== config.sourceChannel) { + return; + } + + const tx = /** @type {FungibleTokenPacketData} */ ( + JSON.parse(atob(event.packet.data)) + ); + // only interested in transfers of `remoteDenom` + if (tx.denom !== config.remoteDenom) { + return; + } + const { contractFee } = ctx.terms; + const { USDC } = ctx.terms.brands; + const { settlement, fundingPool, feeAccount } = acct; + const { nextLabel: next = () => '#?' } = ctx.t?.context || {}; + const amount = make(USDC, BigInt(tx.amount)); + log(next(), 'tap onReceive', { amount }); + // XXX partial failure? + await Promise.all([ + settlement.send(fundingPool.getAddress(), subtract(amount, contractFee)), + settlement.send(feeAccount.getAddress(), contractFee), + ]); +}; +harden(settle); diff --git a/packages/orchestration/src/proposals/start-quickSend.js b/packages/orchestration/src/proposals/start-quickSend.js index bf3a08efc75..f1cb26a79c5 100644 --- a/packages/orchestration/src/proposals/start-quickSend.js +++ b/packages/orchestration/src/proposals/start-quickSend.js @@ -1,5 +1,5 @@ +import { AmountMath } from '@agoric/ertp/src/amountMath.js'; import { deeplyFulfilledObject, makeTracer } from '@agoric/internal'; -import { Stake } from '@agoric/internal/src/tokens.js'; import { E } from '@endo/far'; const trace = makeTracer('StartQuickSend', true); @@ -8,6 +8,7 @@ const { Fail } = assert; /** * @import {Instance} from '@agoric/zoe/src/zoeService/utils'; * @import {Board} from '@agoric/vats'; + * @import {QuickSendContractFn} from '../examples/quickSend.contract.js'; */ /** @@ -29,58 +30,85 @@ const makePublishingStorageKit = async (path, { chainStorage, board }) => { /** * @param {BootstrapPowers & { * installation: PromiseSpaceOf<{ - * quickSend: Installation< - * import('../examples/quickSend.contract.js').start - * >; + * quickSend: Installation; * }>; * instance: PromiseSpaceOf<{ - * quickSend: Instance; + * quickSend: Instance; * }>; + * brand: PromiseSpaceOf<{ USDC: Brand<'nat'> }>; * }} powers + * @param {{ options?: { quickSend?: { watcherAddress: string } } }} config */ -export const startQuickSend = async ({ - consume: { - agoricNames, - board, - chainStorage, - chainTimerService: timerService, - localchain, - startUpgradable, - }, - installation: { - consume: { quickSend }, - }, - instance: { - produce: { quickSend: produceInstance }, - }, - issuer: { - consume: { [Stake.symbol]: stakeIssuer }, +export const startQuickSend = async ( + { + consume: { + agoricNames, + namesByAddress, + board, + chainStorage, + chainTimerService: timerService, + localchain, + cosmosInterchainService, + startUpgradable, + }, + installation: { + consume: { quickSend }, + }, + instance: { + produce: { quickSend: produceInstance }, + }, + brand, + issuer, }, -}) => { + config = {}, +) => { trace('startQuickSend'); + const { watcherAddress = 'agoric1watcher' } = config.options?.quickSend || {}; + + await null; + const USDC = { + brand: await brand.consume.IST, // TODO: USDC interchain asset + issuer: await issuer.consume.IST, + }; + const terms = { + makerFee: AmountMath.make(USDC.brand, 100n), // TODO: parameterize + contractFee: AmountMath.make(USDC.brand, 30n), + }; const { storageNode, marshaller } = await makePublishingStorageKit( 'quickSend', { board, chainStorage }, ); + assert(storageNode); const privateArgs = await deeplyFulfilledObject( - harden({ agoricNames, localchain, timerService, storageNode, marshaller }), + harden({ + agoricNames, + localchain, + orchestrationService: cosmosInterchainService, + storageNode, + timerService, + marshaller, + }), ); /** - * @type {StartUpgradableOpts< - * import('../examples/quickSend.contract.js').start - * >} + * @type {StartUpgradableOpts} */ const startOpts = { label: 'quickSend', installation: quickSend, - issuerKeywordRecord: harden({ In: await stakeIssuer }), - terms: {}, + issuerKeywordRecord: harden({ Fee: USDC.issuer }), + terms, privateArgs, }; - const { instance } = await E(startUpgradable)(startOpts); + const { instance, creatorFacet } = await E(startUpgradable)(startOpts); + trace('CF', creatorFacet); + const toWatch = await E(creatorFacet).getWatcherInvitation(); + /** @type {ERef} */ + const wdf = E(namesByAddress).lookup(watcherAddress, 'depositFacet'); + await E(wdf).receive(toWatch); + produceInstance.resolve(instance); trace('done'); }; @@ -91,12 +119,18 @@ export const getManifestForQuickSend = ({ restoreRef }, { installKeys }) => { manifest: { [startQuickSend.name]: { consume: { - agoricNames: true, - board: true, chainStorage: true, chainTimerService: true, localchain: true, + cosmosInterchainService: true, + + // limited distribution durin MN2: contract installation startUpgradable: true, + + // widely shared: name services + agoricNames: true, + namesByAddress: true, + board: true, }, installation: { consume: { quickSend: true }, @@ -104,8 +138,17 @@ export const getManifestForQuickSend = ({ restoreRef }, { installKeys }) => { instance: { produce: { quickSend: true }, }, + brand: { + consume: { + // TODO USDC + IST: true, + }, + }, issuer: { - consume: { [Stake.symbol]: true }, + consume: { + // TODO USDC + IST: true, + }, }, }, }, diff --git a/packages/orchestration/test/examples/quickSend-tx.test.ts b/packages/orchestration/test/examples/quickSend-tx.test.ts index e5ce9a593b8..1c8b94eea0b 100644 --- a/packages/orchestration/test/examples/quickSend-tx.test.ts +++ b/packages/orchestration/test/examples/quickSend-tx.test.ts @@ -18,10 +18,13 @@ import { makeEventCounter, } from '../../tools/eth-mock.js'; import type { EthChain } from '../../tools/eth-mock.js'; -import type { QuickSendTerms } from '../../src/examples/quickSend.contract.js'; +import type { + QuickSendTerms, + QuickSendContractFn, +} from '../../src/examples/quickSend.contract.js'; import { contract as contractFn } from '../../src/examples/quickSend.contract.js'; import { AgoricCalc, NobleCalc } from '../../src/utils/address.js'; -import { ResolvedContinuingOfferResult } from '../../src/utils/zoe-tools.js'; +import type { ResolvedContinuingOfferResult } from '../../src/utils/zoe-tools.js'; const nodeRequire = createRequire(import.meta.url); const contractName = 'quickSend'; @@ -29,8 +32,6 @@ const contractFile = nodeRequire.resolve( `../../src/examples/quickSend.contract.js`, ); -type StartFn = typeof import('../../src/examples/quickSend.contract.js').start; - const todo = () => assert.fail('TODO'); const logged = (it, label) => { From 1657d208ea89ff812d779b5132ba6594baa97546 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Mon, 14 Oct 2024 13:43:06 -0500 Subject: [PATCH 12/48] test(quickSend): accept watcher invitation (done) - hoist handleCCTPCall to top-level flow - call it from invitationMakers exoClassKit facet --- .../test/bootstrapTests/quickSend.test.ts | 2 +- .../src/examples/quickSend.contract.js | 36 +++++++- .../src/examples/quickSend.flows.js | 85 +++++++++++-------- 3 files changed, 85 insertions(+), 38 deletions(-) diff --git a/packages/boot/test/bootstrapTests/quickSend.test.ts b/packages/boot/test/bootstrapTests/quickSend.test.ts index d8e39416c4c..98887c7cc9e 100644 --- a/packages/boot/test/bootstrapTests/quickSend.test.ts +++ b/packages/boot/test/bootstrapTests/quickSend.test.ts @@ -55,7 +55,7 @@ test.serial('accept watcher invitation', async t => { const update = sw.getLatestUpdateRecord(); t.like(update, { updated: 'offerStatus', - status: { id: 'accept', numWantsSatisfied: 1, result: 'TODO' }, + status: { id: 'accept', numWantsSatisfied: 1, result: 'UNPUBLISHED' }, }); }; diff --git a/packages/orchestration/src/examples/quickSend.contract.js b/packages/orchestration/src/examples/quickSend.contract.js index 139006d8fb6..63858d683d7 100644 --- a/packages/orchestration/src/examples/quickSend.contract.js +++ b/packages/orchestration/src/examples/quickSend.contract.js @@ -10,6 +10,7 @@ import * as flows from './quickSend.flows.js'; * @import {OrchestrationPowers, OrchestrationTools} from '../utils/start-helper.js'; * @import {Zone} from '@agoric/zone'; * @import {VTransferIBCEvent} from '@agoric/vats'; + * @import {InvitationMakers} from '@agoric/smart-wallet/src/types.js'; * @import {QuickSendAccounts} from './quickSend.flows.js'; */ @@ -48,17 +49,46 @@ export const contract = async (zcf, privateArgs, zone, tools) => { { /** @param {VTransferIBCEvent & CopyRecord} event */ receiveUpcall(event) { - const accts = this.state; // eslint-disable-next-line no-use-before-define -- see orchestrate below - settle(accts, harden(event)); + settle({ ...this.state }, harden(event)); }, }, ); + const handleCCTPCall = tools.orchestrate( + 'handleCCTPCall', + { terms, t }, + flows.handleCCTPCall, + ); + const { makeInvitation } = tools.zcfTools; + const ifaceTODO = undefined; + const makeWatcherContKit = zone.exoClassKit( + 'WatcherCont', + ifaceTODO, + /** @param {QuickSendAccounts & CopyRecord} accts */ + accts => accts, + { + offerHandler: { + handle(seat, offerArgs) { + seat.exit(); + return handleCCTPCall({ ...this.state }, offerArgs); + }, + }, + /** @type {import('@agoric/async-flow').HostInterface} */ + invitationMakers: { + ReportCCTPCall() { + const { offerHandler } = this.facets; + return makeInvitation(offerHandler, 'reportCCTPCall'); + }, + }, + }, + ); + const makeWatcherCont = accts => makeWatcherContKit(accts).invitationMakers; + const initAccounts = tools.orchestrate( 'initAccounts', - { terms, makeSettleTap, makeInvitation, t }, + { terms, makeSettleTap, makeInvitation, makeWatcherCont, t }, flows.initAccounts, ); diff --git a/packages/orchestration/src/examples/quickSend.flows.js b/packages/orchestration/src/examples/quickSend.flows.js index c2351d411e0..5fb495b4602 100644 --- a/packages/orchestration/src/examples/quickSend.flows.js +++ b/packages/orchestration/src/examples/quickSend.flows.js @@ -1,7 +1,8 @@ import { AmountMath } from '@agoric/ertp/src/amountMath.js'; +import { mustMatch } from '@agoric/internal'; import { atob } from '@endo/base64'; -import { Far } from '@endo/far'; -import { M, mustMatch } from '@endo/patterns'; +import { M } from '@endo/patterns'; +import { ChainAddressShape } from '../typeGuards.js'; import { AgoricCalc, NobleCalc } from '../utils/address.js'; /** @@ -9,18 +10,29 @@ import { AgoricCalc, NobleCalc } from '../utils/address.js'; * * @import {Passable} from '@endo/pass-style'; * @import {Guarded} from '@endo/exo'; - * @import {OrchestrationAccountI, OrchestrationFlow, Orchestrator, ZcfTools} from '@agoric/orchestration'; + * @import {ChainAddress, OrchestrationAccountI, OrchestrationFlow, Orchestrator, ZcfTools} from '@agoric/orchestration'; * @import {VTransferIBCEvent} from '@agoric/vats'; * @import {ResolvedContinuingOfferResult} from '../../src/utils/zoe-tools.js'; + * @import {InvitationMakers} from '@agoric/smart-wallet/src/types.js'; * @import {QuickSendTerms} from './quickSend.contract.js'; * @import {FungibleTokenPacketData} from '@agoric/cosmic-proto/ibc/applications/transfer/v2/packet.js'; + * @import {TypedPattern} from '@agoric/internal'; */ const AddressShape = M.string(); // XXX +/** + * @typedef {{ + * amount: NatValue; + * dest: ChainAddress; + * nobleFwd: string; + * }} CallDetails + */ + +/** @type {TypedPattern} */ const CallDetailsShape = harden({ amount: M.nat(), - dest: AddressShape, + dest: ChainAddressShape, nobleFwd: AddressShape, }); @@ -38,21 +50,17 @@ const { add, make, subtract } = AmountMath; * @satisfies {OrchestrationFlow} * @param {Orchestrator} orch * @param {{ - * terms: QuickSendTerms & StandardTerms; * t?: ExecutionContext<{ nextLabel: Function }>; // XXX - * makeInvitation: Function; // XXX ZcfTools['makeInvitation']; * makeSettleTap: ( * accts: QuickSendAccounts, * ) => Guarded<{ receiveUpcall: (event: VTransferIBCEvent) => void }>; + * makeWatcherCont: (accts: QuickSendAccounts) => InvitationMakers; * }} ctx - * @param {ZCFSeat} _seat + * @param {ZCFSeat} seat * @param {{}} _offerArgs */ -export const initAccounts = async (orch, ctx, _seat, _offerArgs) => { - const { nextLabel: next = () => '#?' } = ctx.t?.context || {}; +export const initAccounts = async (orch, ctx, seat, _offerArgs) => { const { log = console.log } = ctx.t || {}; - const { makerFee, contractFee } = ctx.terms; - const { USDC } = ctx.terms.brands; const agoric = await orch.getChain('agoric'); @@ -64,24 +72,6 @@ export const initAccounts = async (orch, ctx, _seat, _offerArgs) => { log('@@@what to do with registration?', registration); - /** @type {OfferHandler} */ - const handleCCTPCall = Far('Handler', { - handle: async (_s, offerArgs) => { - mustMatch(offerArgs, CallDetailsShape); - const { amount, dest, nobleFwd } = offerArgs; - log(next(), 'contract.reportCCTPCall', { amount, dest }); - assert.equal( - NobleCalc.fwdAddressFor( - AgoricCalc.virtualAddressFor(fundingPool.getAddress(), dest), - ), - nobleFwd, - ); - const withBrand = make(USDC, amount); - const advance = subtract(withBrand, add(makerFee, contractFee)); - await fundingPool.transfer(dest, advance); - }, - }); - /** @type {ResolvedContinuingOfferResult} */ const watcherFacet = harden({ publicSubscribers: { @@ -89,18 +79,45 @@ export const initAccounts = async (orch, ctx, _seat, _offerArgs) => { settlement: (await settlement.getPublicTopics()).account, feeAccount: (await feeAccount.getPublicTopics()).account, }, - invitationMakers: Far('WatcherInvitationMakers', { - ReportCCTPCall: () => - ctx.makeInvitation(handleCCTPCall, 'reportCCTPCall'), - }), + invitationMakers: ctx.makeWatcherCont(accts), // TODO: skip continuing invitation gymnastics - actions: { handleCCTPCall }, + // actions: { handleCCTPCall }, }); + seat.exit(); return watcherFacet; }; harden(initAccounts); +/** + * @param {Orchestrator} _orch + * @param {{ + * terms: QuickSendTerms & StandardTerms; + * t?: ExecutionContext<{ nextLabel: Function }>; // XXX + * }} ctx + * @param {QuickSendAccounts & Passable} accts + * @param {unknown} offerArgs + */ +export const handleCCTPCall = async (_orch, ctx, accts, offerArgs) => { + const { nextLabel: next = () => '#?' } = ctx.t?.context || {}; + const { log = console.log } = ctx.t || {}; + mustMatch(offerArgs, CallDetailsShape); + const { amount, dest, nobleFwd } = offerArgs; + log(next(), 'contract.reportCCTPCall', { amount, dest }); + const { makerFee, contractFee } = ctx.terms; + const { USDC } = ctx.terms.brands; + const { fundingPool } = accts; + + const fAddr = fundingPool.getAddress().value; + const vAddr = AgoricCalc.virtualAddressFor(fAddr, dest.value); + const nfAddr = NobleCalc.fwdAddressFor(vAddr); + assert.equal(nfAddr, nobleFwd, `for ${vAddr}`); + const withBrand = make(USDC, amount); + const advance = subtract(withBrand, add(makerFee, contractFee)); + await fundingPool.transfer(dest, advance); +}; +harden(handleCCTPCall); + /** * @satisfies {OrchestrationFlow} * @param {Orchestrator} orch From 223b58a6e5b28d90a19829c4b6a9747a5777d1fb Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Mon, 14 Oct 2024 15:21:04 -0500 Subject: [PATCH 13/48] test(quickSend): restore ava test - oops: dropped monitorTransfer in the exo transition - upcall is sync or async??? - restore .actions kludge - fix string vs. ChainAddress in various places - CallDetailsShape for CallDetails - pass BigInt thru JSON as string - more thorough mock: seat.exit(), orchestrate, ... - base64 encode IBC packet data --- .../src/examples/quickSend.contract.js | 12 +- .../src/examples/quickSend.flows.js | 38 +++--- packages/orchestration/src/utils/address.js | 4 + .../test/examples/quickSend-tx.test.ts | 114 +++++++++++------- packages/orchestration/tools/agoric-mock.js | 30 ++--- .../orchestration/tools/cosmoverse-mock.js | 5 + packages/orchestration/tools/eth-mock.ts | 2 +- packages/orchestration/tools/noble-mock.js | 3 +- 8 files changed, 129 insertions(+), 79 deletions(-) diff --git a/packages/orchestration/src/examples/quickSend.contract.js b/packages/orchestration/src/examples/quickSend.contract.js index 63858d683d7..14940270c25 100644 --- a/packages/orchestration/src/examples/quickSend.contract.js +++ b/packages/orchestration/src/examples/quickSend.contract.js @@ -63,12 +63,18 @@ export const contract = async (zcf, privateArgs, zone, tools) => { const { makeInvitation } = tools.zcfTools; const ifaceTODO = undefined; - const makeWatcherContKit = zone.exoClassKit( + const makeWatcherCont = zone.exoClassKit( 'WatcherCont', ifaceTODO, /** @param {QuickSendAccounts & CopyRecord} accts */ - accts => accts, + accts => ({ ...accts }), { + actions: { + // TODO: skip continuing invitation gymnastics + handleCCTPCall(offerArgs) { + return handleCCTPCall({ ...this.state }, offerArgs); + }, + }, offerHandler: { handle(seat, offerArgs) { seat.exit(); @@ -84,7 +90,7 @@ export const contract = async (zcf, privateArgs, zone, tools) => { }, }, ); - const makeWatcherCont = accts => makeWatcherContKit(accts).invitationMakers; + // const makeWatcherCont = accts => makeWatcherContKit(accts).invitationMakers; const initAccounts = tools.orchestrate( 'initAccounts', diff --git a/packages/orchestration/src/examples/quickSend.flows.js b/packages/orchestration/src/examples/quickSend.flows.js index 5fb495b4602..b669a65a39e 100644 --- a/packages/orchestration/src/examples/quickSend.flows.js +++ b/packages/orchestration/src/examples/quickSend.flows.js @@ -51,10 +51,13 @@ const { add, make, subtract } = AmountMath; * @param {Orchestrator} orch * @param {{ * t?: ExecutionContext<{ nextLabel: Function }>; // XXX - * makeSettleTap: ( - * accts: QuickSendAccounts, - * ) => Guarded<{ receiveUpcall: (event: VTransferIBCEvent) => void }>; - * makeWatcherCont: (accts: QuickSendAccounts) => InvitationMakers; + * makeSettleTap: (accts: QuickSendAccounts) => Guarded<{ + * receiveUpcall: (event: VTransferIBCEvent) => void; + * }>; + * makeWatcherCont: (accts: QuickSendAccounts) => { + * invitationMakers: InvitationMakers; + * actions: Record; + * }; * }} ctx * @param {ZCFSeat} seat * @param {{}} _offerArgs @@ -68,10 +71,12 @@ export const initAccounts = async (orch, ctx, seat, _offerArgs) => { const settlement = await agoric.makeAccount(); const feeAccount = await agoric.makeAccount(); const accts = harden({ fundingPool, settlement, feeAccount }); - const registration = await ctx.makeSettleTap(accts); + const tap = ctx.makeSettleTap(accts); + const registration = await settlement.monitorTransfers(tap); log('@@@what to do with registration?', registration); + const cont = ctx.makeWatcherCont(accts); /** @type {ResolvedContinuingOfferResult} */ const watcherFacet = harden({ publicSubscribers: { @@ -79,9 +84,7 @@ export const initAccounts = async (orch, ctx, seat, _offerArgs) => { settlement: (await settlement.getPublicTopics()).account, feeAccount: (await feeAccount.getPublicTopics()).account, }, - invitationMakers: ctx.makeWatcherCont(accts), - // TODO: skip continuing invitation gymnastics - // actions: { handleCCTPCall }, + ...cont, }); seat.exit(); @@ -103,7 +106,7 @@ export const handleCCTPCall = async (_orch, ctx, accts, offerArgs) => { const { log = console.log } = ctx.t || {}; mustMatch(offerArgs, CallDetailsShape); const { amount, dest, nobleFwd } = offerArgs; - log(next(), 'contract.reportCCTPCall', { amount, dest }); + log(next(), 'flows.reportCCTPCall', { amount, dest }); const { makerFee, contractFee } = ctx.terms; const { USDC } = ctx.terms.brands; const { fundingPool } = accts; @@ -130,20 +133,19 @@ harden(handleCCTPCall); * @returns {Promise} */ export const settle = async (orch, ctx, acct, event) => { - const config = {}; // TODO const { log = console.log } = ctx?.t || {}; - // ignore packets from unknown channels - if (event.packet.source_channel !== config.sourceChannel) { - return; - } + // TODO: ignore packets from unknown channels + // if (event.packet.source_channel !== config.sourceChannel) { + // return; + // } const tx = /** @type {FungibleTokenPacketData} */ ( JSON.parse(atob(event.packet.data)) ); - // only interested in transfers of `remoteDenom` - if (tx.denom !== config.remoteDenom) { - return; - } + // TODO: only interested in transfers of `remoteDenom` + // if (tx.denom !== config.remoteDenom) { + // return; + // } const { contractFee } = ctx.terms; const { USDC } = ctx.terms.brands; const { settlement, fundingPool, feeAccount } = acct; diff --git a/packages/orchestration/src/utils/address.js b/packages/orchestration/src/utils/address.js index 7a9c0221e22..591634e44df 100644 --- a/packages/orchestration/src/utils/address.js +++ b/packages/orchestration/src/utils/address.js @@ -86,6 +86,10 @@ export const findAddressField = remoteAddressString => { harden(findAddressField); export const AgoricCalc = harden({ + /** + * @param {string} base + * @param {string} supplemental + */ virtualAddressFor: (base, supplemental) => { assert.typeof(base, 'string'); assert.typeof(supplemental, 'string'); diff --git a/packages/orchestration/test/examples/quickSend-tx.test.ts b/packages/orchestration/test/examples/quickSend-tx.test.ts index 1c8b94eea0b..6af9306e737 100644 --- a/packages/orchestration/test/examples/quickSend-tx.test.ts +++ b/packages/orchestration/test/examples/quickSend-tx.test.ts @@ -1,30 +1,34 @@ import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js'; -import { createRequire } from 'node:module'; -import { E, passStyleOf } from '@endo/far'; -import { objectMap } from '@endo/patterns'; -import { deeplyFulfilledObject } from '@agoric/internal'; import { AmountMath, makeIssuerKit } from '@agoric/ertp'; +import { deeplyFulfilledObject } from '@agoric/internal'; +import { makeHeapZone } from '@agoric/zone'; +import { E } from '@endo/far'; +import { Nat } from '@endo/nat'; +import { objectMap } from '@endo/patterns'; +import { createRequire } from 'node:module'; +import { NatAmount } from '@agoric/ertp/src/types.js'; +import type { QuickSendTerms } from '../../src/examples/quickSend.contract.js'; +import { contract as contractFn } from '../../src/examples/quickSend.contract.js'; +import { CallDetails } from '../../src/examples/quickSend.flows.js'; +import { AgoricCalc, NobleCalc } from '../../src/utils/address.js'; +import type { ResolvedContinuingOfferResult } from '../../src/utils/zoe-tools.js'; import { makeOrchestration, makeVStorage, withVTransfer, } from '../../tools/agoric-mock.js'; -import { makeCCTP, withForwarding } from '../../tools/noble-mock.js'; import { makeCosmosChain, pickChain } from '../../tools/cosmoverse-mock.js'; +import type { EthAddr, EthChain } from '../../tools/eth-mock.js'; import { makeERC20, makeEthChain, makeEventCounter, } from '../../tools/eth-mock.js'; -import type { EthChain } from '../../tools/eth-mock.js'; -import type { - QuickSendTerms, - QuickSendContractFn, -} from '../../src/examples/quickSend.contract.js'; -import { contract as contractFn } from '../../src/examples/quickSend.contract.js'; -import { AgoricCalc, NobleCalc } from '../../src/utils/address.js'; -import type { ResolvedContinuingOfferResult } from '../../src/utils/zoe-tools.js'; +import { makeCCTP, withForwarding } from '../../tools/noble-mock.js'; +import type { ChainAddress } from '../../src/types.js'; + +type NatValue = bigint; const nodeRequire = createRequire(import.meta.url); const contractName = 'quickSend'; @@ -48,18 +52,26 @@ test.before(async t => { t.context = { startLabels, nextLabel }; }); -const makeUser = ({ nobleApp, ethereum, myAddr, cctpAddr }) => +const makeUser = ({ + nobleApp, + ethereum, + myAddr, + cctpAddr, +}: { + // eslint-disable-next-line no-use-before-define + nobleApp: NobleApp; + ethereum: EthChain; + myAddr: EthAddr; + cctpAddr: EthAddr; +}) => harden({ - doTransfer: async (amount, dest) => { - const nobleFwd = await nobleApp.getNobleFwd(dest); - const { setup, done } = await nobleApp.initiateTransaction({ - dest, - amount, - nobleFwd, - }); + doTransfer: async (amount: NatValue, dest: ChainAddress) => { + const nobleFwd = await nobleApp.getNobleFwd(dest.value); + const detail = harden({ dest, amount, nobleFwd }); + const { setup, done } = await nobleApp.initiateTransaction(detail); await setup; await ethereum.call({ sender: myAddr }, cctpAddr, 'bridge', [ - { dest: nobleFwd, amount }, + { dest: nobleFwd, amount: `${amount}` }, ]); return done; }, @@ -72,7 +84,13 @@ const makeNobleApp = async ({ agoricRpc, setTimeout, }) => { - const watchAddr = async ({ dest, amount }) => { + const watchAddr = async ({ + dest, + amount, + }: { + dest: string; + amount: bigint; + }) => { const chain = pickChain(chains, dest); const balancePre = await chain.getBalance(dest); const events = makeEventCounter({ setTimeout }); @@ -98,26 +116,29 @@ const makeNobleApp = async ({ ); return nobleFwd; }, - initiateTransaction: async ({ dest, amount, nobleFwd }) => { + initiateTransaction: async (detail: CallDetails) => { + const { amount, dest, nobleFwd } = detail; t.log(nextLabel(), 'app initiate', { amount, dest }); - const setup = nobleService.initiateTransfer({ amount, dest, nobleFwd }); - const done = watchAddr({ dest, amount }); + const setup = nobleService.initiateTransfer(detail); + const done = watchAddr({ dest: dest.value, amount }); return { setup, done }; }, }); }; +type NobleApp = Awaited>; const makeNobleExpress = ({ agoricWatcher, chain, t, agoricRpc }) => { const baseP = agoricRpc.getData('published.quickSend.settlementBase'); const { nextLabel: next } = t.context; return harden({ - initiateTransfer: async ({ dest, amount, nobleFwd }) => { + initiateTransfer: async (detail: CallDetails) => { const base = await baseP; - const agAddr = AgoricCalc.virtualAddressFor(base, dest); + const { dest, amount, nobleFwd } = detail; + const agAddr = AgoricCalc.virtualAddressFor(base, dest.value); const fwd = await chain.provideForwardingAccount(agAddr); t.log(next(), 'express initiate', { dest, base, agAddr, fwd, nobleFwd }); fwd === nobleFwd || assert.fail('mismatch'); - await agoricWatcher.startWatchingFor({ dest, amount, nobleFwd }); + await agoricWatcher.startWatchingFor(detail); }, }); }; @@ -130,7 +151,7 @@ const makeAgoricWatcher = ({ setTimeout, }) => { const ethereum: EthChain = eth; // TODO: concise typing - const pending: { dest: string; amount: number; nobleFwd: string }[] = []; + const pending: CallDetails[] = []; let done = false; const { nextLabel: next } = t.context; @@ -143,23 +164,24 @@ const makeAgoricWatcher = ({ if (tx.contractAddress !== cctpAddr) break; const [{ dest, amount }] = tx.args; const ix = pending.findIndex( - item => item.nobleFwd === dest && item.amount === amount, + item => item.nobleFwd === dest && item.amount === BigInt(amount), ); if (ix < 0) continue; const item = pending[ix]; pending.splice(ix, 1); t.log(next(), 'watcher checked', tx.txId); t.log(next(), 'watcher confirmed', item); - await watcherFacet.actions.handleCCTPCall(null, item); // TODO: continuing offerSpec + await watcherFacet.actions.handleCCTPCall(item); // TODO: continuing offerSpec } }; const events = makeEventCounter({ setTimeout }); return harden({ - startWatchingFor: async ({ dest, amount, nobleFwd }) => { - t.log(next(), 'watcher.startWatchingFor', { dest, amount, nobleFwd }); - pending.push(harden({ dest, amount, nobleFwd })); + startWatchingFor: async (detail: CallDetails) => { + const { dest, amount, nobleFwd } = detail; + t.log(next(), 'watcher.startWatchingFor', detail); + pending.push(detail); await check(ethereum.currentHeight()); }, watch: async () => { @@ -225,18 +247,21 @@ const setup = async (t, io) => { handlers.set(desc, handler); }, }) as any; - const zone = {} as any; + const zone = makeHeapZone(); const privateArgs: Parameters[1] = { storageNode, } as any; + const orchestrate = + (n, ctx, h) => + (...args) => + h(orch, ctx, ...args); const tools: Parameters[3] = { t, zcfTools: { makeInvitation: zcf.makeInvitation }, + vowTools: { watch: x => x }, + orchestrate, orchestrateAll: (flows, ctx) => - objectMap( - flows, - (h, k) => (seat, offerArgs) => h(orch, ctx, seat, offerArgs), - ), + objectMap(flows, (h, n) => orchestrate(n, ctx, h)), } as any; const contract = await contractFn(zcf, privateArgs, zone, tools); @@ -257,9 +282,10 @@ const setup = async (t, io) => { }); const cctpAddr = await ethereum.deployContract(cctp); + const aSeat = { exit: () => {} }; const toWatch = await E(contract.creatorFacet).getWatcherInvitation(); const watcherFacet: ResolvedContinuingOfferResult = - await handlers.get('initAccounts')(); + await handlers.get('initAccounts')(aSeat); console.debug(watcherFacet); const addrs = await deeplyFulfilledObject( @@ -329,7 +355,11 @@ test('tx lifecycle', async t => { const { chains, ursula, quiesce, contract, addrs, usdc } = await setup(t, io); const destAddr = await chains.dydx.makeAccount(); // is this a prereq? - await ursula.doTransfer(100n, destAddr); + await ursula.doTransfer(100n, { + chainId: 'dydx2', + encoding: 'bech32', + value: destAddr, + }); await quiesce(); diff --git a/packages/orchestration/tools/agoric-mock.js b/packages/orchestration/tools/agoric-mock.js index 9fe3103fc83..35ee44bd5f1 100644 --- a/packages/orchestration/tools/agoric-mock.js +++ b/packages/orchestration/tools/agoric-mock.js @@ -1,3 +1,4 @@ +import { btoa } from '@endo/base64'; import { AgoricCalc } from '../src/utils/address.js'; import { ibcTransfer } from './cosmoverse-mock.js'; @@ -6,13 +7,18 @@ import { ibcTransfer } from './cosmoverse-mock.js'; * @import {OrchestrationAccount} from '../src/orchestration-api.js'; */ +/** + * @param {any} t + * @param {Record { const { nextLabel: next } = t.context; + const encoding = 'bech32'; /** @returns {Promise>} */ const makeAccount = async () => { const addr = await chains.agoric.makeAccount(); return harden({ - getAddress: () => addr, + getAddress: () => ({ value: addr, chainId: 'agoric3', encoding }), getPublicTopics: async () => ({ account: { subscriber: { @@ -28,15 +34,15 @@ export const makeOrchestration = (t, chains) => { }, }), transfer: async (dest, { value: amount }) => { - t.log(next(), 'orch acct', addr, 'txfr', amount, 'to', dest); - await ibcTransfer(chains, { amount, dest, from: addr, t }); + t.log(next(), 'orch acct', addr, 'txfr', amount, 'to', dest.value); + await ibcTransfer(chains, { amount, dest: dest.value, from: addr, t }); }, send: async (dest, { value: amount }) => { - t.log(next(), 'orch acct', addr, 'send', amount, 'to', dest); - await chains.agoric.send({ amount, dest, from: addr }); + t.log(next(), 'orch acct', addr, 'send', amount, 'to', dest.value); + await chains.agoric.send({ amount, dest: dest.value, from: addr }); }, - monitorTransfers: async tap => { - await chains.agoric.register({ addr, handler: tap }); + monitorTransfers: async handler => { + await chains.agoric.register({ addr, handler }); }, }); }; @@ -66,6 +72,8 @@ export const makeVStorage = () => { return { storageNode, rpc }; }; +const mkEvent = data => ({ packet: { data: btoa(JSON.stringify(data)) } }); + export const withVTransfer = (chain, t) => { const addrToTap = new Map(); return harden({ @@ -87,13 +95,7 @@ export const withVTransfer = (chain, t) => { if (!addrToTap.has(agAddr)) return result; const handler = addrToTap.get(agAddr); - void handler - .receiveUpcall({ - packet: { data: JSON.stringify({ amount: Number(amount), extra }) }, - }) - .catch(err => { - console.error('receiveUpcall rejected', err); - }); + handler.receiveUpcall(mkEvent({ amount: Number(amount), extra })); return result; }, diff --git a/packages/orchestration/tools/cosmoverse-mock.js b/packages/orchestration/tools/cosmoverse-mock.js index 171c26a6241..aa55c4fc12a 100644 --- a/packages/orchestration/tools/cosmoverse-mock.js +++ b/packages/orchestration/tools/cosmoverse-mock.js @@ -38,7 +38,12 @@ export const makeCosmosChain = (prefix, t) => { }, }); }; +/** @typedef {ReturnType} CosmosChain */ +/** + * @param {Record} chains + * @param {string} dest + */ export const pickChain = (chains, dest) => { const pfxLen = dest.indexOf('1'); const pfx = dest.slice(0, pfxLen); diff --git a/packages/orchestration/tools/eth-mock.ts b/packages/orchestration/tools/eth-mock.ts index b0504541b51..4cbb9f9bee8 100644 --- a/packages/orchestration/tools/eth-mock.ts +++ b/packages/orchestration/tools/eth-mock.ts @@ -1,4 +1,4 @@ -type EthAddr = `0x${string}`; +export type EthAddr = `0x${string}`; type EthData = Record; type EthMsgInfo = { sender: EthAddr; value?: number }; type EthCallTx = { diff --git a/packages/orchestration/tools/noble-mock.js b/packages/orchestration/tools/noble-mock.js index a3450f4cda1..639db68814b 100644 --- a/packages/orchestration/tools/noble-mock.js +++ b/packages/orchestration/tools/noble-mock.js @@ -25,9 +25,10 @@ export const withForwarding = (chain, chains, t) => { export const makeCCTP = ({ t, usdc, noble, events }) => { const { nextLabel: next } = t.context; return harden({ - bridge: (msg, { dest, amount }) => { + bridge: (msg, { dest, amount: aNumeral }) => { t.regex(dest, /^noble/); t.log(next(), 'cctp.bridge:', { msg, dest }); + const amount = BigInt(aNumeral); usdc.transfer(msg, '0x0000', amount); // burn const t0 = events.getCurrent(); void (async () => { From 31b95d5d221ddfa7a37f6ea0a438d9d4ab77687f Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Wed, 16 Oct 2024 21:59:09 -0500 Subject: [PATCH 14/48] feat(quickSend): thread watcherAddress config out to builder CLI --- packages/builders/package.json | 1 + .../scripts/orchestration/init-quickSend.js | 38 +++++++++++++++++-- .../src/proposals/start-quickSend.js | 24 +++++++++++- 3 files changed, 57 insertions(+), 6 deletions(-) diff --git a/packages/builders/package.json b/packages/builders/package.json index 727c0c7bf8a..8e3393b05f3 100644 --- a/packages/builders/package.json +++ b/packages/builders/package.json @@ -36,6 +36,7 @@ "@endo/far": "^1.1.8", "@endo/init": "^1.1.6", "@endo/marshal": "^1.6.1", + "@endo/patterns": "^1.4.6", "@endo/promise-kit": "^1.1.7", "@endo/stream": "^1.2.7", "import-meta-resolve": "^2.2.1" diff --git a/packages/builders/scripts/orchestration/init-quickSend.js b/packages/builders/scripts/orchestration/init-quickSend.js index 98e27f3f1ad..da2a11da1af 100644 --- a/packages/builders/scripts/orchestration/init-quickSend.js +++ b/packages/builders/scripts/orchestration/init-quickSend.js @@ -1,16 +1,36 @@ // @ts-check import { makeHelpers } from '@agoric/deploy-script-support'; +import { mustMatch } from '@agoric/internal'; import { getManifestForQuickSend } from '@agoric/orchestration/src/proposals/start-quickSend.js'; +import { M } from '@endo/patterns'; +import { parseArgs } from 'node:util'; -/** @import {CoreEvalBuilder} from '@agoric/deploy-script-support/src/externalTypes.js'; */ +/** + * @import {CoreEvalBuilder} from '@agoric/deploy-script-support/src/externalTypes.js'; + * @import {QuickSendConfig} from '@agoric/orchestration/src/proposals/start-quickSend.js'; + * @import {TypedPattern} from '@agoric/internal'; + * @import {ParseArgsConfig} from 'node:util'; + */ + +/** @type {ParseArgsConfig['options']} */ +const options = { + watcher: { type: 'string' }, +}; +/** @typedef {{ watcher?: string }} QuickSendOpts */ + +/** @type {TypedPattern} */ +const QuickSendConfigShape = M.splitRecord({ watcherAddress: M.string() }); /** @type {CoreEvalBuilder} */ -export const defaultProposalBuilder = async ({ publishRef, install }) => - harden({ +export const defaultProposalBuilder = async ({ publishRef, install }, opts) => { + opts && mustMatch(opts, QuickSendConfigShape); + return harden({ sourceSpec: '@agoric/orchestration/src/proposals/start-quickSend.js', + /** @type {[string, Parameters[1]]} */ getManifestCall: [ getManifestForQuickSend.name, { + options: { quickSend: opts }, installKeys: { quickSend: publishRef( install('@agoric/orchestration/src/examples/quickSend.contract.js'), @@ -19,8 +39,18 @@ export const defaultProposalBuilder = async ({ publishRef, install }) => }, ], }); +}; export default async (homeP, endowments) => { const { writeCoreEval } = await makeHelpers(homeP, endowments); - await writeCoreEval('start-quickSend', defaultProposalBuilder); + const { scriptArgs } = endowments; + + /** @type {{ values: QuickSendOpts }} */ + const { values: flags } = parseArgs({ args: scriptArgs, options }); + const config = flags.watcher + ? harden({ watcherAddress: flags.watcher }) + : undefined; + await writeCoreEval('start-quickSend', utils => + defaultProposalBuilder(utils, config), + ); }; diff --git a/packages/orchestration/src/proposals/start-quickSend.js b/packages/orchestration/src/proposals/start-quickSend.js index f1cb26a79c5..17b268de667 100644 --- a/packages/orchestration/src/proposals/start-quickSend.js +++ b/packages/orchestration/src/proposals/start-quickSend.js @@ -9,6 +9,8 @@ const { Fail } = assert; * @import {Instance} from '@agoric/zoe/src/zoeService/utils'; * @import {Board} from '@agoric/vats'; * @import {QuickSendContractFn} from '../examples/quickSend.contract.js'; + * @import {ManifestBundleRef} from '@agoric/deploy-script-support/src/externalTypes.js'; + * @import {BootstrapManifest} from '@agoric/vats/src/core/lib-boot.js'; */ /** @@ -27,6 +29,10 @@ const makePublishingStorageKit = async (path, { chainStorage, board }) => { return { storageNode, marshaller }; }; +/** + * @typedef {{ watcherAddress: string }} QuickSendConfig + */ + /** * @param {BootstrapPowers & { * installation: PromiseSpaceOf<{ @@ -37,7 +43,7 @@ const makePublishingStorageKit = async (path, { chainStorage, board }) => { * }>; * brand: PromiseSpaceOf<{ USDC: Brand<'nat'> }>; * }} powers - * @param {{ options?: { quickSend?: { watcherAddress: string } } }} config + * @param {{ options?: { quickSend?: QuickSendConfig } }} config */ export const startQuickSend = async ( { @@ -114,8 +120,21 @@ export const startQuickSend = async ( }; harden(startQuickSend); -export const getManifestForQuickSend = ({ restoreRef }, { installKeys }) => { +/** + * @param {{ + * restoreRef: (b: ERef) => Promise; + * }} utils + * @param {{ + * installKeys: { quickSend: ERef }; + * options?: { quickSend?: QuickSendConfig }; + * }} param1 + */ +export const getManifestForQuickSend = ( + { restoreRef }, + { installKeys, options }, +) => { return { + /** @type {BootstrapManifest} */ manifest: { [startQuickSend.name]: { consume: { @@ -155,5 +174,6 @@ export const getManifestForQuickSend = ({ restoreRef }, { installKeys }) => { installations: { quickSend: restoreRef(installKeys.quickSend), }, + options, }; }; From 342313d6c0f781b665d0a046ed5f1af404e935e4 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Thu, 17 Oct 2024 00:16:51 -0500 Subject: [PATCH 15/48] chore: quick-n-dirty vstorage client w/explicit io --- .../d:quick-send/test-lib/cosmos-api.js | 42 +++++++++++++++++++ .../d:quick-send/test-lib/vstorage-client.js | 34 +++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 a3p-integration/proposals/d:quick-send/test-lib/cosmos-api.js create mode 100644 a3p-integration/proposals/d:quick-send/test-lib/vstorage-client.js diff --git a/a3p-integration/proposals/d:quick-send/test-lib/cosmos-api.js b/a3p-integration/proposals/d:quick-send/test-lib/cosmos-api.js new file mode 100644 index 00000000000..b4d6e1540c8 --- /dev/null +++ b/a3p-integration/proposals/d:quick-send/test-lib/cosmos-api.js @@ -0,0 +1,42 @@ +// @ts-check +const { freeze } = Object; + +/** + * @see {@link https://docs.cosmos.network/v0.46/core/grpc_rest.html#rest-server} + */ +export const defaultAPIPort = 1317; +export const localAPI = `http://localhost:${defaultAPIPort}`; + +/** + * @param {string} apiURL + * @param {object} io + * @param {typeof fetch} io.fetch + */ +export const makeLCD = (apiURL, { fetch }) => { + if (typeof apiURL !== 'string') throw TypeError(typeof apiURL); + + /** + * @param {string} href + * @param {object} [options] + * @param {Record} [options.headers] + */ + const getJSON = async (href, options = {}) => { + const opts = { + keepalive: true, + headers: { + 'Content-Type': 'application/json', + ...options.headers, + }, + }; + const url = `${apiURL}${href}`; + const r = await fetch(url, opts); + if (!r.ok) throw Error(r.statusText); + return r.json(); + }; + + return freeze({ + getJSON, + latestBlock: () => getJSON(`/cosmos/base/tendermint/v1beta1/blocks/latest`), + }); +}; +/** @typedef {ReturnType} LCD */ diff --git a/a3p-integration/proposals/d:quick-send/test-lib/vstorage-client.js b/a3p-integration/proposals/d:quick-send/test-lib/vstorage-client.js new file mode 100644 index 00000000000..497e63fd2b5 --- /dev/null +++ b/a3p-integration/proposals/d:quick-send/test-lib/vstorage-client.js @@ -0,0 +1,34 @@ +// @ts-check + +/** + * @param {import("./cosmos-api").LCD} lcd + */ +export const makeVStorage = lcd => { + const getJSON = (href, options) => lcd.getJSON(href, options); + + // height=0 is the same as omitting height and implies the highest block + const href = (path = 'published', { kind = 'data' } = {}) => + `/agoric/vstorage/${kind}/${path}`; + const headers = height => + height ? { 'x-cosmos-block-height': `${height}` } : undefined; + + const readStorage = ( + path = 'published', + { kind = 'data', height = 0 } = {}, + ) => + getJSON(href(path, { kind }), { headers: headers(height) }).catch(err => { + throw Error( + `cannot read ${kind} of ${path}: ${err.message} ${err?.cause.message}`, + ); + }); + const readCell = (path, opts) => + readStorage(path, opts) + .then(data => data.value) + .then(s => (s === '' ? {} : JSON.parse(s))); + + return { + lcd, + readStorage, + readCell, + }; +}; From 473f14723bdc032cdf785757b22f556754ca98b3 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Wed, 16 Oct 2024 22:35:42 -0500 Subject: [PATCH 16/48] test(quickSend): a3p deployment to agoricNames.instance --- .../proposals/d:quick-send/.yarnrc.yml | 1 + .../proposals/d:quick-send/package.json | 27 + .../proposals/d:quick-send/quick-load.test.js | 32 + .../proposals/d:quick-send/test.sh | 2 + .../proposals/d:quick-send/yarn.lock | 2345 +++++++++++++++++ .../src/proposals/start-quickSend.js | 18 +- 6 files changed, 2420 insertions(+), 5 deletions(-) create mode 100644 a3p-integration/proposals/d:quick-send/.yarnrc.yml create mode 100644 a3p-integration/proposals/d:quick-send/package.json create mode 100644 a3p-integration/proposals/d:quick-send/quick-load.test.js create mode 100755 a3p-integration/proposals/d:quick-send/test.sh create mode 100644 a3p-integration/proposals/d:quick-send/yarn.lock diff --git a/a3p-integration/proposals/d:quick-send/.yarnrc.yml b/a3p-integration/proposals/d:quick-send/.yarnrc.yml new file mode 100644 index 00000000000..3186f3f0795 --- /dev/null +++ b/a3p-integration/proposals/d:quick-send/.yarnrc.yml @@ -0,0 +1 @@ +nodeLinker: node-modules diff --git a/a3p-integration/proposals/d:quick-send/package.json b/a3p-integration/proposals/d:quick-send/package.json new file mode 100644 index 00000000000..472220b5163 --- /dev/null +++ b/a3p-integration/proposals/d:quick-send/package.json @@ -0,0 +1,27 @@ +{ + "agoricProposal": { + "source": "subdir", + "sdk-generate": [ + "vats/init-orchestration.js", + "fast-usdc/init-quickSend.js" + ], + "type": "/agoric.swingset.CoreEvalProposal" + }, + "type": "module", + "license": "Apache-2.0", + "dependencies": { + "@agoric/synthetic-chain": "^0.3.0", + "ava": "^5.3.1" + }, + "ava": { + "concurrency": 1, + "timeout": "2m", + "files": [ + "!submission" + ] + }, + "scripts": { + "agops": "yarn --cwd /usr/src/agoric-sdk/ --silent agops" + }, + "packageManager": "yarn@4.2.2" +} diff --git a/a3p-integration/proposals/d:quick-send/quick-load.test.js b/a3p-integration/proposals/d:quick-send/quick-load.test.js new file mode 100644 index 00000000000..955f9226c16 --- /dev/null +++ b/a3p-integration/proposals/d:quick-send/quick-load.test.js @@ -0,0 +1,32 @@ +// @ts-check +/* global globalThis */ +import anyTest from 'ava'; +import { extractStreamCellValue } from '@agoric/synthetic-chain'; +import { localAPI, makeLCD } from './test-lib/cosmos-api.js'; +import { makeVStorage } from './test-lib/vstorage-client.js'; + +/** + * @import {TestFn} from 'ava'; + */ +/** @type {TestFn>>} */ +// @ts-expect-error XXX something weird about test.after +const test = anyTest; + +const makeTestContext = async t => { + const api = makeLCD(localAPI, { fetch: globalThis.fetch }); + return { api }; +}; + +test.before('IO setup', async t => (t.context = await makeTestContext(t))); + +test('quickSend is in agoricNames.instance', async t => { + const { api } = t.context; + const vs = makeVStorage(api); + const data = await vs.readStorage('published.agoricNames.instance'); + const value = extractStreamCellValue(data); + const capData = JSON.parse(value); + const encoding = JSON.parse(capData?.body.replace(/^#/, '')); + const byName = Object.fromEntries(encoding); + t.log('agoricNames.instance keys', Object.keys(byName)); + t.truthy(byName.quickSend); +}); diff --git a/a3p-integration/proposals/d:quick-send/test.sh b/a3p-integration/proposals/d:quick-send/test.sh new file mode 100755 index 00000000000..ff669d07327 --- /dev/null +++ b/a3p-integration/proposals/d:quick-send/test.sh @@ -0,0 +1,2 @@ +#!/bin/bash +yarn ava diff --git a/a3p-integration/proposals/d:quick-send/yarn.lock b/a3p-integration/proposals/d:quick-send/yarn.lock new file mode 100644 index 00000000000..33593f4224b --- /dev/null +++ b/a3p-integration/proposals/d:quick-send/yarn.lock @@ -0,0 +1,2345 @@ +# This file is generated by running "yarn install" inside your project. +# Manual changes might be lost - proceed with caution! + +__metadata: + version: 8 + cacheKey: 10c0 + +"@agoric/synthetic-chain@npm:^0.3.0": + version: 0.3.0 + resolution: "@agoric/synthetic-chain@npm:0.3.0" + dependencies: + "@endo/zip": "npm:^1.0.7" + better-sqlite3: "npm:^9.6.0" + chalk: "npm:^5.3.0" + cosmjs-types: "npm:^0.9.0" + execa: "npm:^9.3.1" + bin: + synthetic-chain: dist/cli/cli.js + checksum: 10c0/17c6241bdc48b8a2a7608c9d4d7c0a0c76fb10d4ee44a31a1150104a792bcd1133f4b1a7e8ab26673a07450b3ceabccd9911999117568221b49221b6ee4306a1 + languageName: node + linkType: hard + +"@endo/zip@npm:^1.0.7": + version: 1.0.8 + resolution: "@endo/zip@npm:1.0.8" + checksum: 10c0/bb74862121932cd27eef59326325c4b61671ce0962033a2ad18e6d6978a9e94bfe604dbfa8c0b039a38fa7eefc495e6a69c583c0e75929cd267979b2c65b775b + languageName: node + linkType: hard + +"@isaacs/cliui@npm:^8.0.2": + version: 8.0.2 + resolution: "@isaacs/cliui@npm:8.0.2" + dependencies: + string-width: "npm:^5.1.2" + string-width-cjs: "npm:string-width@^4.2.0" + strip-ansi: "npm:^7.0.1" + strip-ansi-cjs: "npm:strip-ansi@^6.0.1" + wrap-ansi: "npm:^8.1.0" + wrap-ansi-cjs: "npm:wrap-ansi@^7.0.0" + checksum: 10c0/b1bf42535d49f11dc137f18d5e4e63a28c5569de438a221c369483731e9dac9fb797af554e8bf02b6192d1e5eba6e6402cf93900c3d0ac86391d00d04876789e + languageName: node + linkType: hard + +"@nodelib/fs.scandir@npm:2.1.5": + version: 2.1.5 + resolution: "@nodelib/fs.scandir@npm:2.1.5" + dependencies: + "@nodelib/fs.stat": "npm:2.0.5" + run-parallel: "npm:^1.1.9" + checksum: 10c0/732c3b6d1b1e967440e65f284bd06e5821fedf10a1bea9ed2bb75956ea1f30e08c44d3def9d6a230666574edbaf136f8cfd319c14fd1f87c66e6a44449afb2eb + languageName: node + linkType: hard + +"@nodelib/fs.stat@npm:2.0.5, @nodelib/fs.stat@npm:^2.0.2": + version: 2.0.5 + resolution: "@nodelib/fs.stat@npm:2.0.5" + checksum: 10c0/88dafe5e3e29a388b07264680dc996c17f4bda48d163a9d4f5c1112979f0ce8ec72aa7116122c350b4e7976bc5566dc3ddb579be1ceaacc727872eb4ed93926d + languageName: node + linkType: hard + +"@nodelib/fs.walk@npm:^1.2.3": + version: 1.2.8 + resolution: "@nodelib/fs.walk@npm:1.2.8" + dependencies: + "@nodelib/fs.scandir": "npm:2.1.5" + fastq: "npm:^1.6.0" + checksum: 10c0/db9de047c3bb9b51f9335a7bb46f4fcfb6829fb628318c12115fbaf7d369bfce71c15b103d1fc3b464812d936220ee9bc1c8f762d032c9f6be9acc99249095b1 + languageName: node + linkType: hard + +"@npmcli/agent@npm:^2.0.0": + version: 2.2.2 + resolution: "@npmcli/agent@npm:2.2.2" + dependencies: + agent-base: "npm:^7.1.0" + http-proxy-agent: "npm:^7.0.0" + https-proxy-agent: "npm:^7.0.1" + lru-cache: "npm:^10.0.1" + socks-proxy-agent: "npm:^8.0.3" + checksum: 10c0/325e0db7b287d4154ecd164c0815c08007abfb07653cc57bceded17bb7fd240998a3cbdbe87d700e30bef494885eccc725ab73b668020811d56623d145b524ae + languageName: node + linkType: hard + +"@npmcli/fs@npm:^3.1.0": + version: 3.1.1 + resolution: "@npmcli/fs@npm:3.1.1" + dependencies: + semver: "npm:^7.3.5" + checksum: 10c0/c37a5b4842bfdece3d14dfdb054f73fe15ed2d3da61b34ff76629fb5b1731647c49166fd2a8bf8b56fcfa51200382385ea8909a3cbecdad612310c114d3f6c99 + languageName: node + linkType: hard + +"@pkgjs/parseargs@npm:^0.11.0": + version: 0.11.0 + resolution: "@pkgjs/parseargs@npm:0.11.0" + checksum: 10c0/5bd7576bb1b38a47a7fc7b51ac9f38748e772beebc56200450c4a817d712232b8f1d3ef70532c80840243c657d491cf6a6be1e3a214cff907645819fdc34aadd + languageName: node + linkType: hard + +"@sec-ant/readable-stream@npm:^0.4.1": + version: 0.4.1 + resolution: "@sec-ant/readable-stream@npm:0.4.1" + checksum: 10c0/64e9e9cf161e848067a5bf60cdc04d18495dc28bb63a8d9f8993e4dd99b91ad34e4b563c85de17d91ffb177ec17a0664991d2e115f6543e73236a906068987af + languageName: node + linkType: hard + +"@sindresorhus/merge-streams@npm:^4.0.0": + version: 4.0.0 + resolution: "@sindresorhus/merge-streams@npm:4.0.0" + checksum: 10c0/482ee543629aa1933b332f811a1ae805a213681ecdd98c042b1c1b89387df63e7812248bb4df3910b02b3cc5589d3d73e4393f30e197c9dde18046ccd471fc6b + languageName: node + linkType: hard + +"abbrev@npm:^2.0.0": + version: 2.0.0 + resolution: "abbrev@npm:2.0.0" + checksum: 10c0/f742a5a107473946f426c691c08daba61a1d15942616f300b5d32fd735be88fef5cba24201757b6c407fd564555fb48c751cfa33519b2605c8a7aadd22baf372 + languageName: node + linkType: hard + +"acorn-walk@npm:^8.2.0": + version: 8.3.4 + resolution: "acorn-walk@npm:8.3.4" + dependencies: + acorn: "npm:^8.11.0" + checksum: 10c0/76537ac5fb2c37a64560feaf3342023dadc086c46da57da363e64c6148dc21b57d49ace26f949e225063acb6fb441eabffd89f7a3066de5ad37ab3e328927c62 + languageName: node + linkType: hard + +"acorn@npm:^8.11.0, acorn@npm:^8.8.2": + version: 8.13.0 + resolution: "acorn@npm:8.13.0" + bin: + acorn: bin/acorn + checksum: 10c0/f35dd53d68177c90699f4c37d0bb205b8abe036d955d0eb011ddb7f14a81e6fd0f18893731c457c1b5bd96754683f4c3d80d9a5585ddecaa53cdf84e0b3d68f7 + languageName: node + linkType: hard + +"agent-base@npm:^7.0.2, agent-base@npm:^7.1.0, agent-base@npm:^7.1.1": + version: 7.1.1 + resolution: "agent-base@npm:7.1.1" + dependencies: + debug: "npm:^4.3.4" + checksum: 10c0/e59ce7bed9c63bf071a30cc471f2933862044c97fd9958967bfe22521d7a0f601ce4ed5a8c011799d0c726ca70312142ae193bbebb60f576b52be19d4a363b50 + languageName: node + linkType: hard + +"aggregate-error@npm:^3.0.0": + version: 3.1.0 + resolution: "aggregate-error@npm:3.1.0" + dependencies: + clean-stack: "npm:^2.0.0" + indent-string: "npm:^4.0.0" + checksum: 10c0/a42f67faa79e3e6687a4923050e7c9807db3848a037076f791d10e092677d65c1d2d863b7848560699f40fc0502c19f40963fb1cd1fb3d338a7423df8e45e039 + languageName: node + linkType: hard + +"aggregate-error@npm:^4.0.0": + version: 4.0.1 + resolution: "aggregate-error@npm:4.0.1" + dependencies: + clean-stack: "npm:^4.0.0" + indent-string: "npm:^5.0.0" + checksum: 10c0/75fd739f5c4c60a667cce35ccaf0edf135e147ef0be9a029cab75de14ac9421779b15339d562e58d25b233ea0ef2bbd4c916f149fdbcb73c2b9a62209e611343 + languageName: node + linkType: hard + +"ansi-regex@npm:^5.0.1": + version: 5.0.1 + resolution: "ansi-regex@npm:5.0.1" + checksum: 10c0/9a64bb8627b434ba9327b60c027742e5d17ac69277960d041898596271d992d4d52ba7267a63ca10232e29f6107fc8a835f6ce8d719b88c5f8493f8254813737 + languageName: node + linkType: hard + +"ansi-regex@npm:^6.0.1": + version: 6.1.0 + resolution: "ansi-regex@npm:6.1.0" + checksum: 10c0/a91daeddd54746338478eef88af3439a7edf30f8e23196e2d6ed182da9add559c601266dbef01c2efa46a958ad6f1f8b176799657616c702b5b02e799e7fd8dc + languageName: node + linkType: hard + +"ansi-styles@npm:^4.0.0": + version: 4.3.0 + resolution: "ansi-styles@npm:4.3.0" + dependencies: + color-convert: "npm:^2.0.1" + checksum: 10c0/895a23929da416f2bd3de7e9cb4eabd340949328ab85ddd6e484a637d8f6820d485f53933446f5291c3b760cbc488beb8e88573dd0f9c7daf83dccc8fe81b041 + languageName: node + linkType: hard + +"ansi-styles@npm:^6.0.0, ansi-styles@npm:^6.1.0, ansi-styles@npm:^6.2.1": + version: 6.2.1 + resolution: "ansi-styles@npm:6.2.1" + checksum: 10c0/5d1ec38c123984bcedd996eac680d548f31828bd679a66db2bdf11844634dde55fec3efa9c6bb1d89056a5e79c1ac540c4c784d592ea1d25028a92227d2f2d5c + languageName: node + linkType: hard + +"anymatch@npm:~3.1.2": + version: 3.1.3 + resolution: "anymatch@npm:3.1.3" + dependencies: + normalize-path: "npm:^3.0.0" + picomatch: "npm:^2.0.4" + checksum: 10c0/57b06ae984bc32a0d22592c87384cd88fe4511b1dd7581497831c56d41939c8a001b28e7b853e1450f2bf61992dfcaa8ae2d0d161a0a90c4fb631ef07098fbac + languageName: node + linkType: hard + +"argparse@npm:^1.0.7": + version: 1.0.10 + resolution: "argparse@npm:1.0.10" + dependencies: + sprintf-js: "npm:~1.0.2" + checksum: 10c0/b2972c5c23c63df66bca144dbc65d180efa74f25f8fd9b7d9a0a6c88ae839db32df3d54770dcb6460cf840d232b60695d1a6b1053f599d84e73f7437087712de + languageName: node + linkType: hard + +"array-find-index@npm:^1.0.1": + version: 1.0.2 + resolution: "array-find-index@npm:1.0.2" + checksum: 10c0/86b9485c74ddd324feab807e10a6de3f9c1683856267236fac4bb4d4667ada6463e106db3f6c540ae6b720e0442b590ec701d13676df4c6af30ebf4da09b4f57 + languageName: node + linkType: hard + +"arrgv@npm:^1.0.2": + version: 1.0.2 + resolution: "arrgv@npm:1.0.2" + checksum: 10c0/7e6e782e6b749923ac7cbc4048ef6fe0844c4a59bfc8932fcd4c44566ba25eed46501f94dd7cf3c7297da88f3f599ca056bfb77d0c2484aebc92f04239f69124 + languageName: node + linkType: hard + +"arrify@npm:^3.0.0": + version: 3.0.0 + resolution: "arrify@npm:3.0.0" + checksum: 10c0/2e26601b8486f29780f1f70f7ac05a226755814c2a3ab42e196748f650af1dc310cd575a11dd4b9841c70fd7460b2dd2b8fe6fb7a3375878e2660706efafa58e + languageName: node + linkType: hard + +"ava@npm:^5.3.1": + version: 5.3.1 + resolution: "ava@npm:5.3.1" + dependencies: + acorn: "npm:^8.8.2" + acorn-walk: "npm:^8.2.0" + ansi-styles: "npm:^6.2.1" + arrgv: "npm:^1.0.2" + arrify: "npm:^3.0.0" + callsites: "npm:^4.0.0" + cbor: "npm:^8.1.0" + chalk: "npm:^5.2.0" + chokidar: "npm:^3.5.3" + chunkd: "npm:^2.0.1" + ci-info: "npm:^3.8.0" + ci-parallel-vars: "npm:^1.0.1" + clean-yaml-object: "npm:^0.1.0" + cli-truncate: "npm:^3.1.0" + code-excerpt: "npm:^4.0.0" + common-path-prefix: "npm:^3.0.0" + concordance: "npm:^5.0.4" + currently-unhandled: "npm:^0.4.1" + debug: "npm:^4.3.4" + emittery: "npm:^1.0.1" + figures: "npm:^5.0.0" + globby: "npm:^13.1.4" + ignore-by-default: "npm:^2.1.0" + indent-string: "npm:^5.0.0" + is-error: "npm:^2.2.2" + is-plain-object: "npm:^5.0.0" + is-promise: "npm:^4.0.0" + matcher: "npm:^5.0.0" + mem: "npm:^9.0.2" + ms: "npm:^2.1.3" + p-event: "npm:^5.0.1" + p-map: "npm:^5.5.0" + picomatch: "npm:^2.3.1" + pkg-conf: "npm:^4.0.0" + plur: "npm:^5.1.0" + pretty-ms: "npm:^8.0.0" + resolve-cwd: "npm:^3.0.0" + stack-utils: "npm:^2.0.6" + strip-ansi: "npm:^7.0.1" + supertap: "npm:^3.0.1" + temp-dir: "npm:^3.0.0" + write-file-atomic: "npm:^5.0.1" + yargs: "npm:^17.7.2" + peerDependencies: + "@ava/typescript": "*" + peerDependenciesMeta: + "@ava/typescript": + optional: true + bin: + ava: entrypoints/cli.mjs + checksum: 10c0/262cbdb9e8c3ce7177be91b92ba521e9d5aef577dcc8095cc591f86baaa291b91c88925928f5d26832c4d1b381a6ae99f2e8804077c592d0d32322c1212605cc + languageName: node + linkType: hard + +"balanced-match@npm:^1.0.0": + version: 1.0.2 + resolution: "balanced-match@npm:1.0.2" + checksum: 10c0/9308baf0a7e4838a82bbfd11e01b1cb0f0cf2893bc1676c27c2a8c0e70cbae1c59120c3268517a8ae7fb6376b4639ef81ca22582611dbee4ed28df945134aaee + languageName: node + linkType: hard + +"base64-js@npm:^1.3.1": + version: 1.5.1 + resolution: "base64-js@npm:1.5.1" + checksum: 10c0/f23823513b63173a001030fae4f2dabe283b99a9d324ade3ad3d148e218134676f1ee8568c877cd79ec1c53158dcf2d2ba527a97c606618928ba99dd930102bf + languageName: node + linkType: hard + +"better-sqlite3@npm:^9.6.0": + version: 9.6.0 + resolution: "better-sqlite3@npm:9.6.0" + dependencies: + bindings: "npm:^1.5.0" + node-gyp: "npm:latest" + prebuild-install: "npm:^7.1.1" + checksum: 10c0/8db9b38f414e26a56d4c40fc16e94a253118491dae0e2c054338a9e470f1a883c7eb4cb330f2f5737db30f704d4f2e697c59071ca04e03364ee9fe04375aa9c8 + languageName: node + linkType: hard + +"binary-extensions@npm:^2.0.0": + version: 2.3.0 + resolution: "binary-extensions@npm:2.3.0" + checksum: 10c0/75a59cafc10fb12a11d510e77110c6c7ae3f4ca22463d52487709ca7f18f69d886aa387557cc9864fbdb10153d0bdb4caacabf11541f55e89ed6e18d12ece2b5 + languageName: node + linkType: hard + +"bindings@npm:^1.5.0": + version: 1.5.0 + resolution: "bindings@npm:1.5.0" + dependencies: + file-uri-to-path: "npm:1.0.0" + checksum: 10c0/3dab2491b4bb24124252a91e656803eac24292473e56554e35bbfe3cc1875332cfa77600c3bac7564049dc95075bf6fcc63a4609920ff2d64d0fe405fcf0d4ba + languageName: node + linkType: hard + +"bl@npm:^4.0.3": + version: 4.1.0 + resolution: "bl@npm:4.1.0" + dependencies: + buffer: "npm:^5.5.0" + inherits: "npm:^2.0.4" + readable-stream: "npm:^3.4.0" + checksum: 10c0/02847e1d2cb089c9dc6958add42e3cdeaf07d13f575973963335ac0fdece563a50ac770ac4c8fa06492d2dd276f6cc3b7f08c7cd9c7a7ad0f8d388b2a28def5f + languageName: node + linkType: hard + +"blueimp-md5@npm:^2.10.0": + version: 2.19.0 + resolution: "blueimp-md5@npm:2.19.0" + checksum: 10c0/85d04343537dd99a288c62450341dcce7380d3454c81f8e5a971ddd80307d6f9ef51b5b92ad7d48aaaa92fd6d3a1f6b2f4fada068faae646887f7bfabc17a346 + languageName: node + linkType: hard + +"brace-expansion@npm:^2.0.1": + version: 2.0.1 + resolution: "brace-expansion@npm:2.0.1" + dependencies: + balanced-match: "npm:^1.0.0" + checksum: 10c0/b358f2fe060e2d7a87aa015979ecea07f3c37d4018f8d6deb5bd4c229ad3a0384fe6029bb76cd8be63c81e516ee52d1a0673edbe2023d53a5191732ae3c3e49f + languageName: node + linkType: hard + +"braces@npm:^3.0.3, braces@npm:~3.0.2": + version: 3.0.3 + resolution: "braces@npm:3.0.3" + dependencies: + fill-range: "npm:^7.1.1" + checksum: 10c0/7c6dfd30c338d2997ba77500539227b9d1f85e388a5f43220865201e407e076783d0881f2d297b9f80951b4c957fcf0b51c1d2d24227631643c3f7c284b0aa04 + languageName: node + linkType: hard + +"buffer@npm:^5.5.0": + version: 5.7.1 + resolution: "buffer@npm:5.7.1" + dependencies: + base64-js: "npm:^1.3.1" + ieee754: "npm:^1.1.13" + checksum: 10c0/27cac81cff434ed2876058d72e7c4789d11ff1120ef32c9de48f59eab58179b66710c488987d295ae89a228f835fc66d088652dffeb8e3ba8659f80eb091d55e + languageName: node + linkType: hard + +"cacache@npm:^18.0.0": + version: 18.0.4 + resolution: "cacache@npm:18.0.4" + dependencies: + "@npmcli/fs": "npm:^3.1.0" + fs-minipass: "npm:^3.0.0" + glob: "npm:^10.2.2" + lru-cache: "npm:^10.0.1" + minipass: "npm:^7.0.3" + minipass-collect: "npm:^2.0.1" + minipass-flush: "npm:^1.0.5" + minipass-pipeline: "npm:^1.2.4" + p-map: "npm:^4.0.0" + ssri: "npm:^10.0.0" + tar: "npm:^6.1.11" + unique-filename: "npm:^3.0.0" + checksum: 10c0/6c055bafed9de4f3dcc64ac3dc7dd24e863210902b7c470eb9ce55a806309b3efff78033e3d8b4f7dcc5d467f2db43c6a2857aaaf26f0094b8a351d44c42179f + languageName: node + linkType: hard + +"callsites@npm:^4.0.0": + version: 4.2.0 + resolution: "callsites@npm:4.2.0" + checksum: 10c0/8f7e269ec09fc0946bb22d838a8bc7932e1909ab4a833b964749f4d0e8bdeaa1f253287c4f911f61781f09620b6925ccd19a5ea4897489c4e59442c660c312a3 + languageName: node + linkType: hard + +"cbor@npm:^8.1.0": + version: 8.1.0 + resolution: "cbor@npm:8.1.0" + dependencies: + nofilter: "npm:^3.1.0" + checksum: 10c0/a836e2e7ea0efb1b9c4e5a4be906c57113d730cc42293a34072e0164ed110bb8ac035dc7dca2e3ebb641bd4b37e00fdbbf09c951aa864b3d4888a6ed8c6243f7 + languageName: node + linkType: hard + +"chalk@npm:^5.2.0, chalk@npm:^5.3.0": + version: 5.3.0 + resolution: "chalk@npm:5.3.0" + checksum: 10c0/8297d436b2c0f95801103ff2ef67268d362021b8210daf8ddbe349695333eb3610a71122172ff3b0272f1ef2cf7cc2c41fdaa4715f52e49ffe04c56340feed09 + languageName: node + linkType: hard + +"chokidar@npm:^3.5.3": + version: 3.6.0 + resolution: "chokidar@npm:3.6.0" + dependencies: + anymatch: "npm:~3.1.2" + braces: "npm:~3.0.2" + fsevents: "npm:~2.3.2" + glob-parent: "npm:~5.1.2" + is-binary-path: "npm:~2.1.0" + is-glob: "npm:~4.0.1" + normalize-path: "npm:~3.0.0" + readdirp: "npm:~3.6.0" + dependenciesMeta: + fsevents: + optional: true + checksum: 10c0/8361dcd013f2ddbe260eacb1f3cb2f2c6f2b0ad118708a343a5ed8158941a39cb8fb1d272e0f389712e74ee90ce8ba864eece9e0e62b9705cb468a2f6d917462 + languageName: node + linkType: hard + +"chownr@npm:^1.1.1": + version: 1.1.4 + resolution: "chownr@npm:1.1.4" + checksum: 10c0/ed57952a84cc0c802af900cf7136de643d3aba2eecb59d29344bc2f3f9bf703a301b9d84cdc71f82c3ffc9ccde831b0d92f5b45f91727d6c9da62f23aef9d9db + languageName: node + linkType: hard + +"chownr@npm:^2.0.0": + version: 2.0.0 + resolution: "chownr@npm:2.0.0" + checksum: 10c0/594754e1303672171cc04e50f6c398ae16128eb134a88f801bf5354fd96f205320f23536a045d9abd8b51024a149696e51231565891d4efdab8846021ecf88e6 + languageName: node + linkType: hard + +"chunkd@npm:^2.0.1": + version: 2.0.1 + resolution: "chunkd@npm:2.0.1" + checksum: 10c0/4e0c5aac6048ecedfa4cd0a5f6c4f010c70a7b7645aeca7bfeb47cb0733c3463054f0ced3f2667b2e0e67edd75d68a8e05481b01115ba3f8a952a93026254504 + languageName: node + linkType: hard + +"ci-info@npm:^3.8.0": + version: 3.9.0 + resolution: "ci-info@npm:3.9.0" + checksum: 10c0/6f0109e36e111684291d46123d491bc4e7b7a1934c3a20dea28cba89f1d4a03acd892f5f6a81ed3855c38647e285a150e3c9ba062e38943bef57fee6c1554c3a + languageName: node + linkType: hard + +"ci-parallel-vars@npm:^1.0.1": + version: 1.0.1 + resolution: "ci-parallel-vars@npm:1.0.1" + checksum: 10c0/80952f699cbbc146092b077b4f3e28d085620eb4e6be37f069b4dbb3db0ee70e8eec3beef4ebe70ff60631e9fc743b9d0869678489f167442cac08b260e5ac08 + languageName: node + linkType: hard + +"clean-stack@npm:^2.0.0": + version: 2.2.0 + resolution: "clean-stack@npm:2.2.0" + checksum: 10c0/1f90262d5f6230a17e27d0c190b09d47ebe7efdd76a03b5a1127863f7b3c9aec4c3e6c8bb3a7bbf81d553d56a1fd35728f5a8ef4c63f867ac8d690109742a8c1 + languageName: node + linkType: hard + +"clean-stack@npm:^4.0.0": + version: 4.2.0 + resolution: "clean-stack@npm:4.2.0" + dependencies: + escape-string-regexp: "npm:5.0.0" + checksum: 10c0/2bdf981a0fef0a23c14255df693b30eb9ae27eedf212470d8c400a0c0b6fb82fbf1ff8c5216ccd5721e3670b700389c886b1dce5070776dc9fbcc040957758c0 + languageName: node + linkType: hard + +"clean-yaml-object@npm:^0.1.0": + version: 0.1.0 + resolution: "clean-yaml-object@npm:0.1.0" + checksum: 10c0/a6505310590038afb9f0adc7f17a4c66787719c94d23f8491267ea4d9c405cdd378bd576ae1926169b6d997d4c59a8b86516bf4d16ba228280cf615598c58e05 + languageName: node + linkType: hard + +"cli-truncate@npm:^3.1.0": + version: 3.1.0 + resolution: "cli-truncate@npm:3.1.0" + dependencies: + slice-ansi: "npm:^5.0.0" + string-width: "npm:^5.0.0" + checksum: 10c0/a19088878409ec0e5dc2659a5166929629d93cfba6d68afc9cde2282fd4c751af5b555bf197047e31c87c574396348d011b7aa806fec29c4139ea4f7f00b324c + languageName: node + linkType: hard + +"cliui@npm:^8.0.1": + version: 8.0.1 + resolution: "cliui@npm:8.0.1" + dependencies: + string-width: "npm:^4.2.0" + strip-ansi: "npm:^6.0.1" + wrap-ansi: "npm:^7.0.0" + checksum: 10c0/4bda0f09c340cbb6dfdc1ed508b3ca080f12992c18d68c6be4d9cf51756033d5266e61ec57529e610dacbf4da1c634423b0c1b11037709cc6b09045cbd815df5 + languageName: node + linkType: hard + +"code-excerpt@npm:^4.0.0": + version: 4.0.0 + resolution: "code-excerpt@npm:4.0.0" + dependencies: + convert-to-spaces: "npm:^2.0.1" + checksum: 10c0/b6c5a06e039cecd2ab6a0e10ee0831de8362107d1f298ca3558b5f9004cb8e0260b02dd6c07f57b9a0e346c76864d2873311ee1989809fdeb05bd5fbbadde773 + languageName: node + linkType: hard + +"color-convert@npm:^2.0.1": + version: 2.0.1 + resolution: "color-convert@npm:2.0.1" + dependencies: + color-name: "npm:~1.1.4" + checksum: 10c0/37e1150172f2e311fe1b2df62c6293a342ee7380da7b9cfdba67ea539909afbd74da27033208d01d6d5cfc65ee7868a22e18d7e7648e004425441c0f8a15a7d7 + languageName: node + linkType: hard + +"color-name@npm:~1.1.4": + version: 1.1.4 + resolution: "color-name@npm:1.1.4" + checksum: 10c0/a1a3f914156960902f46f7f56bc62effc6c94e84b2cae157a526b1c1f74b677a47ec602bf68a61abfa2b42d15b7c5651c6dbe72a43af720bc588dff885b10f95 + languageName: node + linkType: hard + +"common-path-prefix@npm:^3.0.0": + version: 3.0.0 + resolution: "common-path-prefix@npm:3.0.0" + checksum: 10c0/c4a74294e1b1570f4a8ab435285d185a03976c323caa16359053e749db4fde44e3e6586c29cd051100335e11895767cbbd27ea389108e327d62f38daf4548fdb + languageName: node + linkType: hard + +"concordance@npm:^5.0.4": + version: 5.0.4 + resolution: "concordance@npm:5.0.4" + dependencies: + date-time: "npm:^3.1.0" + esutils: "npm:^2.0.3" + fast-diff: "npm:^1.2.0" + js-string-escape: "npm:^1.0.1" + lodash: "npm:^4.17.15" + md5-hex: "npm:^3.0.1" + semver: "npm:^7.3.2" + well-known-symbols: "npm:^2.0.0" + checksum: 10c0/59b440f330df3a7c9aa148ba588b3e99aed86acab225b4f01ffcea34ace4cf11f817e31153254e8f38ed48508998dad40b9106951a743c334d751f7ab21afb8a + languageName: node + linkType: hard + +"convert-to-spaces@npm:^2.0.1": + version: 2.0.1 + resolution: "convert-to-spaces@npm:2.0.1" + checksum: 10c0/d90aa0e3b6a27f9d5265a8d32def3c5c855b3e823a9db1f26d772f8146d6b91020a2fdfd905ce8048a73fad3aaf836fef8188c67602c374405e2ae8396c4ac46 + languageName: node + linkType: hard + +"cosmjs-types@npm:^0.9.0": + version: 0.9.0 + resolution: "cosmjs-types@npm:0.9.0" + checksum: 10c0/bc20f4293fb34629d7c5f96bafe533987f753df957ff68eb078d0128ae5a418320cb945024441769a07bb9bc5dde9d22b972fd40d485933e5706ea191c43727b + languageName: node + linkType: hard + +"cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.3": + version: 7.0.3 + resolution: "cross-spawn@npm:7.0.3" + dependencies: + path-key: "npm:^3.1.0" + shebang-command: "npm:^2.0.0" + which: "npm:^2.0.1" + checksum: 10c0/5738c312387081c98d69c98e105b6327b069197f864a60593245d64c8089c8a0a744e16349281210d56835bb9274130d825a78b2ad6853ca13cfbeffc0c31750 + languageName: node + linkType: hard + +"currently-unhandled@npm:^0.4.1": + version: 0.4.1 + resolution: "currently-unhandled@npm:0.4.1" + dependencies: + array-find-index: "npm:^1.0.1" + checksum: 10c0/32d197689ec32f035910202c1abb0dc6424dce01d7b51779c685119b380d98535c110ffff67a262fc7e367612a7dfd30d3d3055f9a6634b5a9dd1302de7ef11c + languageName: node + linkType: hard + +"date-time@npm:^3.1.0": + version: 3.1.0 + resolution: "date-time@npm:3.1.0" + dependencies: + time-zone: "npm:^1.0.0" + checksum: 10c0/aa3e2e930d74b0b9e90f69de7a16d3376e30f21f1f4ce9a2311d8fec32d760e776efea752dafad0ce188187265235229013036202be053fc2d7979813bfb6ded + languageName: node + linkType: hard + +"debug@npm:4, debug@npm:^4.3.4": + version: 4.3.7 + resolution: "debug@npm:4.3.7" + dependencies: + ms: "npm:^2.1.3" + peerDependenciesMeta: + supports-color: + optional: true + checksum: 10c0/1471db19c3b06d485a622d62f65947a19a23fbd0dd73f7fd3eafb697eec5360cde447fb075919987899b1a2096e85d35d4eb5a4de09a57600ac9cf7e6c8e768b + languageName: node + linkType: hard + +"decompress-response@npm:^6.0.0": + version: 6.0.0 + resolution: "decompress-response@npm:6.0.0" + dependencies: + mimic-response: "npm:^3.1.0" + checksum: 10c0/bd89d23141b96d80577e70c54fb226b2f40e74a6817652b80a116d7befb8758261ad073a8895648a29cc0a5947021ab66705cb542fa9c143c82022b27c5b175e + languageName: node + linkType: hard + +"deep-extend@npm:^0.6.0": + version: 0.6.0 + resolution: "deep-extend@npm:0.6.0" + checksum: 10c0/1c6b0abcdb901e13a44c7d699116d3d4279fdb261983122a3783e7273844d5f2537dc2e1c454a23fcf645917f93fbf8d07101c1d03c015a87faa662755212566 + languageName: node + linkType: hard + +"detect-libc@npm:^2.0.0": + version: 2.0.3 + resolution: "detect-libc@npm:2.0.3" + checksum: 10c0/88095bda8f90220c95f162bf92cad70bd0e424913e655c20578600e35b91edc261af27531cf160a331e185c0ced93944bc7e09939143225f56312d7fd800fdb7 + languageName: node + linkType: hard + +"dir-glob@npm:^3.0.1": + version: 3.0.1 + resolution: "dir-glob@npm:3.0.1" + dependencies: + path-type: "npm:^4.0.0" + checksum: 10c0/dcac00920a4d503e38bb64001acb19df4efc14536ada475725e12f52c16777afdee4db827f55f13a908ee7efc0cb282e2e3dbaeeb98c0993dd93d1802d3bf00c + languageName: node + linkType: hard + +"eastasianwidth@npm:^0.2.0": + version: 0.2.0 + resolution: "eastasianwidth@npm:0.2.0" + checksum: 10c0/26f364ebcdb6395f95124fda411f63137a4bfb5d3a06453f7f23dfe52502905bd84e0488172e0f9ec295fdc45f05c23d5d91baf16bd26f0fe9acd777a188dc39 + languageName: node + linkType: hard + +"emittery@npm:^1.0.1": + version: 1.0.3 + resolution: "emittery@npm:1.0.3" + checksum: 10c0/91605d044f3891dd1f8ab731aeb94b520488b21e707f7064dcbcf5303bac3b4e7133dfa23c343ede1fc970340bd78a9b1aed522b805bc15104606bba630dd71e + languageName: node + linkType: hard + +"emoji-regex@npm:^8.0.0": + version: 8.0.0 + resolution: "emoji-regex@npm:8.0.0" + checksum: 10c0/b6053ad39951c4cf338f9092d7bfba448cdfd46fe6a2a034700b149ac9ffbc137e361cbd3c442297f86bed2e5f7576c1b54cc0a6bf8ef5106cc62f496af35010 + languageName: node + linkType: hard + +"emoji-regex@npm:^9.2.2": + version: 9.2.2 + resolution: "emoji-regex@npm:9.2.2" + checksum: 10c0/af014e759a72064cf66e6e694a7fc6b0ed3d8db680427b021a89727689671cefe9d04151b2cad51dbaf85d5ba790d061cd167f1cf32eb7b281f6368b3c181639 + languageName: node + linkType: hard + +"encoding@npm:^0.1.13": + version: 0.1.13 + resolution: "encoding@npm:0.1.13" + dependencies: + iconv-lite: "npm:^0.6.2" + checksum: 10c0/36d938712ff00fe1f4bac88b43bcffb5930c1efa57bbcdca9d67e1d9d6c57cfb1200fb01efe0f3109b2ce99b231f90779532814a81370a1bd3274a0f58585039 + languageName: node + linkType: hard + +"end-of-stream@npm:^1.1.0, end-of-stream@npm:^1.4.1": + version: 1.4.4 + resolution: "end-of-stream@npm:1.4.4" + dependencies: + once: "npm:^1.4.0" + checksum: 10c0/870b423afb2d54bb8d243c63e07c170409d41e20b47eeef0727547aea5740bd6717aca45597a9f2745525667a6b804c1e7bede41f856818faee5806dd9ff3975 + languageName: node + linkType: hard + +"env-paths@npm:^2.2.0": + version: 2.2.1 + resolution: "env-paths@npm:2.2.1" + checksum: 10c0/285325677bf00e30845e330eec32894f5105529db97496ee3f598478e50f008c5352a41a30e5e72ec9de8a542b5a570b85699cd63bd2bc646dbcb9f311d83bc4 + languageName: node + linkType: hard + +"err-code@npm:^2.0.2": + version: 2.0.3 + resolution: "err-code@npm:2.0.3" + checksum: 10c0/b642f7b4dd4a376e954947550a3065a9ece6733ab8e51ad80db727aaae0817c2e99b02a97a3d6cecc648a97848305e728289cf312d09af395403a90c9d4d8a66 + languageName: node + linkType: hard + +"escalade@npm:^3.1.1": + version: 3.2.0 + resolution: "escalade@npm:3.2.0" + checksum: 10c0/ced4dd3a78e15897ed3be74e635110bbf3b08877b0a41be50dcb325ee0e0b5f65fc2d50e9845194d7c4633f327e2e1c6cce00a71b617c5673df0374201d67f65 + languageName: node + linkType: hard + +"escape-string-regexp@npm:5.0.0, escape-string-regexp@npm:^5.0.0": + version: 5.0.0 + resolution: "escape-string-regexp@npm:5.0.0" + checksum: 10c0/6366f474c6f37a802800a435232395e04e9885919873e382b157ab7e8f0feb8fed71497f84a6f6a81a49aab41815522f5839112bd38026d203aea0c91622df95 + languageName: node + linkType: hard + +"escape-string-regexp@npm:^2.0.0": + version: 2.0.0 + resolution: "escape-string-regexp@npm:2.0.0" + checksum: 10c0/2530479fe8db57eace5e8646c9c2a9c80fa279614986d16dcc6bcaceb63ae77f05a851ba6c43756d816c61d7f4534baf56e3c705e3e0d884818a46808811c507 + languageName: node + linkType: hard + +"esprima@npm:^4.0.0": + version: 4.0.1 + resolution: "esprima@npm:4.0.1" + bin: + esparse: ./bin/esparse.js + esvalidate: ./bin/esvalidate.js + checksum: 10c0/ad4bab9ead0808cf56501750fd9d3fb276f6b105f987707d059005d57e182d18a7c9ec7f3a01794ebddcca676773e42ca48a32d67a250c9d35e009ca613caba3 + languageName: node + linkType: hard + +"esutils@npm:^2.0.3": + version: 2.0.3 + resolution: "esutils@npm:2.0.3" + checksum: 10c0/9a2fe69a41bfdade834ba7c42de4723c97ec776e40656919c62cbd13607c45e127a003f05f724a1ea55e5029a4cf2de444b13009f2af71271e42d93a637137c7 + languageName: node + linkType: hard + +"execa@npm:^9.3.1": + version: 9.4.1 + resolution: "execa@npm:9.4.1" + dependencies: + "@sindresorhus/merge-streams": "npm:^4.0.0" + cross-spawn: "npm:^7.0.3" + figures: "npm:^6.1.0" + get-stream: "npm:^9.0.0" + human-signals: "npm:^8.0.0" + is-plain-obj: "npm:^4.1.0" + is-stream: "npm:^4.0.1" + npm-run-path: "npm:^6.0.0" + pretty-ms: "npm:^9.0.0" + signal-exit: "npm:^4.1.0" + strip-final-newline: "npm:^4.0.0" + yoctocolors: "npm:^2.0.0" + checksum: 10c0/2166de02c8c940312481e480ef47f54636725b24a402d25bdbaeca97f6eeb82b21def1279e00378872fbe6a0c675d6b5cd144be4d52c1485a7a3895e611ac03e + languageName: node + linkType: hard + +"expand-template@npm:^2.0.3": + version: 2.0.3 + resolution: "expand-template@npm:2.0.3" + checksum: 10c0/1c9e7afe9acadf9d373301d27f6a47b34e89b3391b1ef38b7471d381812537ef2457e620ae7f819d2642ce9c43b189b3583813ec395e2938319abe356a9b2f51 + languageName: node + linkType: hard + +"exponential-backoff@npm:^3.1.1": + version: 3.1.1 + resolution: "exponential-backoff@npm:3.1.1" + checksum: 10c0/160456d2d647e6019640bd07111634d8c353038d9fa40176afb7cd49b0548bdae83b56d05e907c2cce2300b81cae35d800ef92fefb9d0208e190fa3b7d6bb579 + languageName: node + linkType: hard + +"fast-diff@npm:^1.2.0": + version: 1.3.0 + resolution: "fast-diff@npm:1.3.0" + checksum: 10c0/5c19af237edb5d5effda008c891a18a585f74bf12953be57923f17a3a4d0979565fc64dbc73b9e20926b9d895f5b690c618cbb969af0cf022e3222471220ad29 + languageName: node + linkType: hard + +"fast-glob@npm:^3.3.0": + version: 3.3.2 + resolution: "fast-glob@npm:3.3.2" + dependencies: + "@nodelib/fs.stat": "npm:^2.0.2" + "@nodelib/fs.walk": "npm:^1.2.3" + glob-parent: "npm:^5.1.2" + merge2: "npm:^1.3.0" + micromatch: "npm:^4.0.4" + checksum: 10c0/42baad7b9cd40b63e42039132bde27ca2cb3a4950d0a0f9abe4639ea1aa9d3e3b40f98b1fe31cbc0cc17b664c9ea7447d911a152fa34ec5b72977b125a6fc845 + languageName: node + linkType: hard + +"fastq@npm:^1.6.0": + version: 1.17.1 + resolution: "fastq@npm:1.17.1" + dependencies: + reusify: "npm:^1.0.4" + checksum: 10c0/1095f16cea45fb3beff558bb3afa74ca7a9250f5a670b65db7ed585f92b4b48381445cd328b3d87323da81e43232b5d5978a8201bde84e0cd514310f1ea6da34 + languageName: node + linkType: hard + +"figures@npm:^5.0.0": + version: 5.0.0 + resolution: "figures@npm:5.0.0" + dependencies: + escape-string-regexp: "npm:^5.0.0" + is-unicode-supported: "npm:^1.2.0" + checksum: 10c0/ce0f17d4ea8b0fc429c5207c343534a2f5284ecfb22aa08607da7dc84ed9e1cf754f5b97760e8dcb98d3c9d1a1e4d3d578fe3b5b99c426f05d0f06c7ba618e16 + languageName: node + linkType: hard + +"figures@npm:^6.1.0": + version: 6.1.0 + resolution: "figures@npm:6.1.0" + dependencies: + is-unicode-supported: "npm:^2.0.0" + checksum: 10c0/9159df4264d62ef447a3931537de92f5012210cf5135c35c010df50a2169377581378149abfe1eb238bd6acbba1c0d547b1f18e0af6eee49e30363cedaffcfe4 + languageName: node + linkType: hard + +"file-uri-to-path@npm:1.0.0": + version: 1.0.0 + resolution: "file-uri-to-path@npm:1.0.0" + checksum: 10c0/3b545e3a341d322d368e880e1c204ef55f1d45cdea65f7efc6c6ce9e0c4d22d802d5629320eb779d006fe59624ac17b0e848d83cc5af7cd101f206cb704f5519 + languageName: node + linkType: hard + +"fill-range@npm:^7.1.1": + version: 7.1.1 + resolution: "fill-range@npm:7.1.1" + dependencies: + to-regex-range: "npm:^5.0.1" + checksum: 10c0/b75b691bbe065472f38824f694c2f7449d7f5004aa950426a2c28f0306c60db9b880c0b0e4ed819997ffb882d1da02cfcfc819bddc94d71627f5269682edf018 + languageName: node + linkType: hard + +"find-up@npm:^6.0.0": + version: 6.3.0 + resolution: "find-up@npm:6.3.0" + dependencies: + locate-path: "npm:^7.1.0" + path-exists: "npm:^5.0.0" + checksum: 10c0/07e0314362d316b2b13f7f11ea4692d5191e718ca3f7264110127520f3347996349bf9e16805abae3e196805814bc66ef4bff2b8904dc4a6476085fc9b0eba07 + languageName: node + linkType: hard + +"foreground-child@npm:^3.1.0": + version: 3.3.0 + resolution: "foreground-child@npm:3.3.0" + dependencies: + cross-spawn: "npm:^7.0.0" + signal-exit: "npm:^4.0.1" + checksum: 10c0/028f1d41000553fcfa6c4bb5c372963bf3d9bf0b1f25a87d1a6253014343fb69dfb1b42d9625d7cf44c8ba429940f3d0ff718b62105d4d4a4f6ef8ca0a53faa2 + languageName: node + linkType: hard + +"fs-constants@npm:^1.0.0": + version: 1.0.0 + resolution: "fs-constants@npm:1.0.0" + checksum: 10c0/a0cde99085f0872f4d244e83e03a46aa387b74f5a5af750896c6b05e9077fac00e9932fdf5aef84f2f16634cd473c63037d7a512576da7d5c2b9163d1909f3a8 + languageName: node + linkType: hard + +"fs-minipass@npm:^2.0.0": + version: 2.1.0 + resolution: "fs-minipass@npm:2.1.0" + dependencies: + minipass: "npm:^3.0.0" + checksum: 10c0/703d16522b8282d7299337539c3ed6edddd1afe82435e4f5b76e34a79cd74e488a8a0e26a636afc2440e1a23b03878e2122e3a2cfe375a5cf63c37d92b86a004 + languageName: node + linkType: hard + +"fs-minipass@npm:^3.0.0": + version: 3.0.3 + resolution: "fs-minipass@npm:3.0.3" + dependencies: + minipass: "npm:^7.0.3" + checksum: 10c0/63e80da2ff9b621e2cb1596abcb9207f1cf82b968b116ccd7b959e3323144cce7fb141462200971c38bbf2ecca51695069db45265705bed09a7cd93ae5b89f94 + languageName: node + linkType: hard + +"fsevents@npm:~2.3.2": + version: 2.3.3 + resolution: "fsevents@npm:2.3.3" + dependencies: + node-gyp: "npm:latest" + checksum: 10c0/a1f0c44595123ed717febbc478aa952e47adfc28e2092be66b8ab1635147254ca6cfe1df792a8997f22716d4cbafc73309899ff7bfac2ac3ad8cf2e4ecc3ec60 + conditions: os=darwin + languageName: node + linkType: hard + +"fsevents@patch:fsevents@npm%3A~2.3.2#optional!builtin": + version: 2.3.3 + resolution: "fsevents@patch:fsevents@npm%3A2.3.3#optional!builtin::version=2.3.3&hash=df0bf1" + dependencies: + node-gyp: "npm:latest" + conditions: os=darwin + languageName: node + linkType: hard + +"get-caller-file@npm:^2.0.5": + version: 2.0.5 + resolution: "get-caller-file@npm:2.0.5" + checksum: 10c0/c6c7b60271931fa752aeb92f2b47e355eac1af3a2673f47c9589e8f8a41adc74d45551c1bc57b5e66a80609f10ffb72b6f575e4370d61cc3f7f3aaff01757cde + languageName: node + linkType: hard + +"get-stream@npm:^9.0.0": + version: 9.0.1 + resolution: "get-stream@npm:9.0.1" + dependencies: + "@sec-ant/readable-stream": "npm:^0.4.1" + is-stream: "npm:^4.0.1" + checksum: 10c0/d70e73857f2eea1826ac570c3a912757dcfbe8a718a033fa0c23e12ac8e7d633195b01710e0559af574cbb5af101009b42df7b6f6b29ceec8dbdf7291931b948 + languageName: node + linkType: hard + +"github-from-package@npm:0.0.0": + version: 0.0.0 + resolution: "github-from-package@npm:0.0.0" + checksum: 10c0/737ee3f52d0a27e26332cde85b533c21fcdc0b09fb716c3f8e522cfaa9c600d4a631dec9fcde179ec9d47cca89017b7848ed4d6ae6b6b78f936c06825b1fcc12 + languageName: node + linkType: hard + +"glob-parent@npm:^5.1.2, glob-parent@npm:~5.1.2": + version: 5.1.2 + resolution: "glob-parent@npm:5.1.2" + dependencies: + is-glob: "npm:^4.0.1" + checksum: 10c0/cab87638e2112bee3f839ef5f6e0765057163d39c66be8ec1602f3823da4692297ad4e972de876ea17c44d652978638d2fd583c6713d0eb6591706825020c9ee + languageName: node + linkType: hard + +"glob@npm:^10.2.2, glob@npm:^10.3.10": + version: 10.4.5 + resolution: "glob@npm:10.4.5" + dependencies: + foreground-child: "npm:^3.1.0" + jackspeak: "npm:^3.1.2" + minimatch: "npm:^9.0.4" + minipass: "npm:^7.1.2" + package-json-from-dist: "npm:^1.0.0" + path-scurry: "npm:^1.11.1" + bin: + glob: dist/esm/bin.mjs + checksum: 10c0/19a9759ea77b8e3ca0a43c2f07ecddc2ad46216b786bb8f993c445aee80d345925a21e5280c7b7c6c59e860a0154b84e4b2b60321fea92cd3c56b4a7489f160e + languageName: node + linkType: hard + +"globby@npm:^13.1.4": + version: 13.2.2 + resolution: "globby@npm:13.2.2" + dependencies: + dir-glob: "npm:^3.0.1" + fast-glob: "npm:^3.3.0" + ignore: "npm:^5.2.4" + merge2: "npm:^1.4.1" + slash: "npm:^4.0.0" + checksum: 10c0/a8d7cc7cbe5e1b2d0f81d467bbc5bc2eac35f74eaded3a6c85fc26d7acc8e6de22d396159db8a2fc340b8a342e74cac58de8f4aee74146d3d146921a76062664 + languageName: node + linkType: hard + +"graceful-fs@npm:^4.2.6": + version: 4.2.11 + resolution: "graceful-fs@npm:4.2.11" + checksum: 10c0/386d011a553e02bc594ac2ca0bd6d9e4c22d7fa8cfbfc448a6d148c59ea881b092db9dbe3547ae4b88e55f1b01f7c4a2ecc53b310c042793e63aa44cf6c257f2 + languageName: node + linkType: hard + +"http-cache-semantics@npm:^4.1.1": + version: 4.1.1 + resolution: "http-cache-semantics@npm:4.1.1" + checksum: 10c0/ce1319b8a382eb3cbb4a37c19f6bfe14e5bb5be3d09079e885e8c513ab2d3cd9214902f8a31c9dc4e37022633ceabfc2d697405deeaf1b8f3552bb4ed996fdfc + languageName: node + linkType: hard + +"http-proxy-agent@npm:^7.0.0": + version: 7.0.2 + resolution: "http-proxy-agent@npm:7.0.2" + dependencies: + agent-base: "npm:^7.1.0" + debug: "npm:^4.3.4" + checksum: 10c0/4207b06a4580fb85dd6dff521f0abf6db517489e70863dca1a0291daa7f2d3d2d6015a57bd702af068ea5cf9f1f6ff72314f5f5b4228d299c0904135d2aef921 + languageName: node + linkType: hard + +"https-proxy-agent@npm:^7.0.1": + version: 7.0.5 + resolution: "https-proxy-agent@npm:7.0.5" + dependencies: + agent-base: "npm:^7.0.2" + debug: "npm:4" + checksum: 10c0/2490e3acec397abeb88807db52cac59102d5ed758feee6df6112ab3ccd8325e8a1ce8bce6f4b66e5470eca102d31e425ace904242e4fa28dbe0c59c4bafa7b2c + languageName: node + linkType: hard + +"human-signals@npm:^8.0.0": + version: 8.0.0 + resolution: "human-signals@npm:8.0.0" + checksum: 10c0/e4dac4f7d3eb791ed04129fc6a85bd454a9102d3e3b76c911d0db7057ebd60b2956b435b5b5712aec18960488ede3c21ef7c56e42cdd70760c0d84d3c05cd92e + languageName: node + linkType: hard + +"iconv-lite@npm:^0.6.2": + version: 0.6.3 + resolution: "iconv-lite@npm:0.6.3" + dependencies: + safer-buffer: "npm:>= 2.1.2 < 3.0.0" + checksum: 10c0/98102bc66b33fcf5ac044099d1257ba0b7ad5e3ccd3221f34dd508ab4070edff183276221684e1e0555b145fce0850c9f7d2b60a9fcac50fbb4ea0d6e845a3b1 + languageName: node + linkType: hard + +"ieee754@npm:^1.1.13": + version: 1.2.1 + resolution: "ieee754@npm:1.2.1" + checksum: 10c0/b0782ef5e0935b9f12883a2e2aa37baa75da6e66ce6515c168697b42160807d9330de9a32ec1ed73149aea02e0d822e572bca6f1e22bdcbd2149e13b050b17bb + languageName: node + linkType: hard + +"ignore-by-default@npm:^2.1.0": + version: 2.1.0 + resolution: "ignore-by-default@npm:2.1.0" + checksum: 10c0/3a6040dac25ed9da39dee73bf1634fdd1e15b0eb7cf52a6bdec81c310565782d8811c104ce40acb3d690d61c5fc38a91c78e6baee830a8a2232424dbc6b66981 + languageName: node + linkType: hard + +"ignore@npm:^5.2.4": + version: 5.3.2 + resolution: "ignore@npm:5.3.2" + checksum: 10c0/f9f652c957983634ded1e7f02da3b559a0d4cc210fca3792cb67f1b153623c9c42efdc1c4121af171e295444459fc4a9201101fb041b1104a3c000bccb188337 + languageName: node + linkType: hard + +"imurmurhash@npm:^0.1.4": + version: 0.1.4 + resolution: "imurmurhash@npm:0.1.4" + checksum: 10c0/8b51313850dd33605c6c9d3fd9638b714f4c4c40250cff658209f30d40da60f78992fb2df5dabee4acf589a6a82bbc79ad5486550754bd9ec4e3fc0d4a57d6a6 + languageName: node + linkType: hard + +"indent-string@npm:^4.0.0": + version: 4.0.0 + resolution: "indent-string@npm:4.0.0" + checksum: 10c0/1e1904ddb0cb3d6cce7cd09e27a90184908b7a5d5c21b92e232c93579d314f0b83c246ffb035493d0504b1e9147ba2c9b21df0030f48673fba0496ecd698161f + languageName: node + linkType: hard + +"indent-string@npm:^5.0.0": + version: 5.0.0 + resolution: "indent-string@npm:5.0.0" + checksum: 10c0/8ee77b57d92e71745e133f6f444d6fa3ed503ad0e1bcd7e80c8da08b42375c07117128d670589725ed07b1978065803fa86318c309ba45415b7fe13e7f170220 + languageName: node + linkType: hard + +"inherits@npm:^2.0.3, inherits@npm:^2.0.4": + version: 2.0.4 + resolution: "inherits@npm:2.0.4" + checksum: 10c0/4e531f648b29039fb7426fb94075e6545faa1eb9fe83c29f0b6d9e7263aceb4289d2d4557db0d428188eeb449cc7c5e77b0a0b2c4e248ff2a65933a0dee49ef2 + languageName: node + linkType: hard + +"ini@npm:~1.3.0": + version: 1.3.8 + resolution: "ini@npm:1.3.8" + checksum: 10c0/ec93838d2328b619532e4f1ff05df7909760b6f66d9c9e2ded11e5c1897d6f2f9980c54dd638f88654b00919ce31e827040631eab0a3969e4d1abefa0719516a + languageName: node + linkType: hard + +"ip-address@npm:^9.0.5": + version: 9.0.5 + resolution: "ip-address@npm:9.0.5" + dependencies: + jsbn: "npm:1.1.0" + sprintf-js: "npm:^1.1.3" + checksum: 10c0/331cd07fafcb3b24100613e4b53e1a2b4feab11e671e655d46dc09ee233da5011284d09ca40c4ecbdfe1d0004f462958675c224a804259f2f78d2465a87824bc + languageName: node + linkType: hard + +"irregular-plurals@npm:^3.3.0": + version: 3.5.0 + resolution: "irregular-plurals@npm:3.5.0" + checksum: 10c0/7c033bbe7325e5a6e0a26949cc6863b6ce273403d4cd5b93bd99b33fecb6605b0884097c4259c23ed0c52c2133bf7d1cdcdd7a0630e8c325161fe269b3447918 + languageName: node + linkType: hard + +"is-binary-path@npm:~2.1.0": + version: 2.1.0 + resolution: "is-binary-path@npm:2.1.0" + dependencies: + binary-extensions: "npm:^2.0.0" + checksum: 10c0/a16eaee59ae2b315ba36fad5c5dcaf8e49c3e27318f8ab8fa3cdb8772bf559c8d1ba750a589c2ccb096113bb64497084361a25960899cb6172a6925ab6123d38 + languageName: node + linkType: hard + +"is-error@npm:^2.2.2": + version: 2.2.2 + resolution: "is-error@npm:2.2.2" + checksum: 10c0/475d3463968bf16e94485555d7cb7a879ed68685e08d365a3370972e626054f1846ebbb3934403091e06682445568601fe919e41646096e5007952d0c1f4fd9b + languageName: node + linkType: hard + +"is-extglob@npm:^2.1.1": + version: 2.1.1 + resolution: "is-extglob@npm:2.1.1" + checksum: 10c0/5487da35691fbc339700bbb2730430b07777a3c21b9ebaecb3072512dfd7b4ba78ac2381a87e8d78d20ea08affb3f1971b4af629173a6bf435ff8a4c47747912 + languageName: node + linkType: hard + +"is-fullwidth-code-point@npm:^3.0.0": + version: 3.0.0 + resolution: "is-fullwidth-code-point@npm:3.0.0" + checksum: 10c0/bb11d825e049f38e04c06373a8d72782eee0205bda9d908cc550ccb3c59b99d750ff9537982e01733c1c94a58e35400661f57042158ff5e8f3e90cf936daf0fc + languageName: node + linkType: hard + +"is-fullwidth-code-point@npm:^4.0.0": + version: 4.0.0 + resolution: "is-fullwidth-code-point@npm:4.0.0" + checksum: 10c0/df2a717e813567db0f659c306d61f2f804d480752526886954a2a3e2246c7745fd07a52b5fecf2b68caf0a6c79dcdace6166fdf29cc76ed9975cc334f0a018b8 + languageName: node + linkType: hard + +"is-glob@npm:^4.0.1, is-glob@npm:~4.0.1": + version: 4.0.3 + resolution: "is-glob@npm:4.0.3" + dependencies: + is-extglob: "npm:^2.1.1" + checksum: 10c0/17fb4014e22be3bbecea9b2e3a76e9e34ff645466be702f1693e8f1ee1adac84710d0be0bd9f967d6354036fd51ab7c2741d954d6e91dae6bb69714de92c197a + languageName: node + linkType: hard + +"is-lambda@npm:^1.0.1": + version: 1.0.1 + resolution: "is-lambda@npm:1.0.1" + checksum: 10c0/85fee098ae62ba6f1e24cf22678805473c7afd0fb3978a3aa260e354cb7bcb3a5806cf0a98403188465efedec41ab4348e8e4e79305d409601323855b3839d4d + languageName: node + linkType: hard + +"is-number@npm:^7.0.0": + version: 7.0.0 + resolution: "is-number@npm:7.0.0" + checksum: 10c0/b4686d0d3053146095ccd45346461bc8e53b80aeb7671cc52a4de02dbbf7dc0d1d2a986e2fe4ae206984b4d34ef37e8b795ebc4f4295c978373e6575e295d811 + languageName: node + linkType: hard + +"is-plain-obj@npm:^4.1.0": + version: 4.1.0 + resolution: "is-plain-obj@npm:4.1.0" + checksum: 10c0/32130d651d71d9564dc88ba7e6fda0e91a1010a3694648e9f4f47bb6080438140696d3e3e15c741411d712e47ac9edc1a8a9de1fe76f3487b0d90be06ac9975e + languageName: node + linkType: hard + +"is-plain-object@npm:^5.0.0": + version: 5.0.0 + resolution: "is-plain-object@npm:5.0.0" + checksum: 10c0/893e42bad832aae3511c71fd61c0bf61aa3a6d853061c62a307261842727d0d25f761ce9379f7ba7226d6179db2a3157efa918e7fe26360f3bf0842d9f28942c + languageName: node + linkType: hard + +"is-promise@npm:^4.0.0": + version: 4.0.0 + resolution: "is-promise@npm:4.0.0" + checksum: 10c0/ebd5c672d73db781ab33ccb155fb9969d6028e37414d609b115cc534654c91ccd061821d5b987eefaa97cf4c62f0b909bb2f04db88306de26e91bfe8ddc01503 + languageName: node + linkType: hard + +"is-stream@npm:^4.0.1": + version: 4.0.1 + resolution: "is-stream@npm:4.0.1" + checksum: 10c0/2706c7f19b851327ba374687bc4a3940805e14ca496dc672b9629e744d143b1ad9c6f1b162dece81c7bfbc0f83b32b61ccc19ad2e05aad2dd7af347408f60c7f + languageName: node + linkType: hard + +"is-unicode-supported@npm:^1.2.0": + version: 1.3.0 + resolution: "is-unicode-supported@npm:1.3.0" + checksum: 10c0/b8674ea95d869f6faabddc6a484767207058b91aea0250803cbf1221345cb0c56f466d4ecea375dc77f6633d248d33c47bd296fb8f4cdba0b4edba8917e83d8a + languageName: node + linkType: hard + +"is-unicode-supported@npm:^2.0.0": + version: 2.1.0 + resolution: "is-unicode-supported@npm:2.1.0" + checksum: 10c0/a0f53e9a7c1fdbcf2d2ef6e40d4736fdffff1c9f8944c75e15425118ff3610172c87bf7bc6c34d3903b04be59790bb2212ddbe21ee65b5a97030fc50370545a5 + languageName: node + linkType: hard + +"isexe@npm:^2.0.0": + version: 2.0.0 + resolution: "isexe@npm:2.0.0" + checksum: 10c0/228cfa503fadc2c31596ab06ed6aa82c9976eec2bfd83397e7eaf06d0ccf42cd1dfd6743bf9aeb01aebd4156d009994c5f76ea898d2832c1fe342da923ca457d + languageName: node + linkType: hard + +"isexe@npm:^3.1.1": + version: 3.1.1 + resolution: "isexe@npm:3.1.1" + checksum: 10c0/9ec257654093443eb0a528a9c8cbba9c0ca7616ccb40abd6dde7202734d96bb86e4ac0d764f0f8cd965856aacbff2f4ce23e730dc19dfb41e3b0d865ca6fdcc7 + languageName: node + linkType: hard + +"jackspeak@npm:^3.1.2": + version: 3.4.3 + resolution: "jackspeak@npm:3.4.3" + dependencies: + "@isaacs/cliui": "npm:^8.0.2" + "@pkgjs/parseargs": "npm:^0.11.0" + dependenciesMeta: + "@pkgjs/parseargs": + optional: true + checksum: 10c0/6acc10d139eaefdbe04d2f679e6191b3abf073f111edf10b1de5302c97ec93fffeb2fdd8681ed17f16268aa9dd4f8c588ed9d1d3bffbbfa6e8bf897cbb3149b9 + languageName: node + linkType: hard + +"js-string-escape@npm:^1.0.1": + version: 1.0.1 + resolution: "js-string-escape@npm:1.0.1" + checksum: 10c0/2c33b9ff1ba6b84681c51ca0997e7d5a1639813c95d5b61cb7ad47e55cc28fa4a0b1935c3d218710d8e6bcee5d0cd8c44755231e3a4e45fc604534d9595a3628 + languageName: node + linkType: hard + +"js-yaml@npm:^3.14.1": + version: 3.14.1 + resolution: "js-yaml@npm:3.14.1" + dependencies: + argparse: "npm:^1.0.7" + esprima: "npm:^4.0.0" + bin: + js-yaml: bin/js-yaml.js + checksum: 10c0/6746baaaeac312c4db8e75fa22331d9a04cccb7792d126ed8ce6a0bbcfef0cedaddd0c5098fade53db067c09fe00aa1c957674b4765610a8b06a5a189e46433b + languageName: node + linkType: hard + +"jsbn@npm:1.1.0": + version: 1.1.0 + resolution: "jsbn@npm:1.1.0" + checksum: 10c0/4f907fb78d7b712e11dea8c165fe0921f81a657d3443dde75359ed52eb2b5d33ce6773d97985a089f09a65edd80b11cb75c767b57ba47391fee4c969f7215c96 + languageName: node + linkType: hard + +"load-json-file@npm:^7.0.0": + version: 7.0.1 + resolution: "load-json-file@npm:7.0.1" + checksum: 10c0/7117459608a0b6329c7f78e6e1f541b3162dd901c29dd5af721fec8b270177d2e3d7999c971f344fff04daac368d052732e2c7146014bc84d15e0b636975e19a + languageName: node + linkType: hard + +"locate-path@npm:^7.1.0": + version: 7.2.0 + resolution: "locate-path@npm:7.2.0" + dependencies: + p-locate: "npm:^6.0.0" + checksum: 10c0/139e8a7fe11cfbd7f20db03923cacfa5db9e14fa14887ea121345597472b4a63c1a42a8a5187defeeff6acf98fd568da7382aa39682d38f0af27433953a97751 + languageName: node + linkType: hard + +"lodash@npm:^4.17.15": + version: 4.17.21 + resolution: "lodash@npm:4.17.21" + checksum: 10c0/d8cbea072bb08655bb4c989da418994b073a608dffa608b09ac04b43a791b12aeae7cd7ad919aa4c925f33b48490b5cfe6c1f71d827956071dae2e7bb3a6b74c + languageName: node + linkType: hard + +"lru-cache@npm:^10.0.1, lru-cache@npm:^10.2.0": + version: 10.4.3 + resolution: "lru-cache@npm:10.4.3" + checksum: 10c0/ebd04fbca961e6c1d6c0af3799adcc966a1babe798f685bb84e6599266599cd95d94630b10262f5424539bc4640107e8a33aa28585374abf561d30d16f4b39fb + languageName: node + linkType: hard + +"make-fetch-happen@npm:^13.0.0": + version: 13.0.1 + resolution: "make-fetch-happen@npm:13.0.1" + dependencies: + "@npmcli/agent": "npm:^2.0.0" + cacache: "npm:^18.0.0" + http-cache-semantics: "npm:^4.1.1" + is-lambda: "npm:^1.0.1" + minipass: "npm:^7.0.2" + minipass-fetch: "npm:^3.0.0" + minipass-flush: "npm:^1.0.5" + minipass-pipeline: "npm:^1.2.4" + negotiator: "npm:^0.6.3" + proc-log: "npm:^4.2.0" + promise-retry: "npm:^2.0.1" + ssri: "npm:^10.0.0" + checksum: 10c0/df5f4dbb6d98153b751bccf4dc4cc500de85a96a9331db9805596c46aa9f99d9555983954e6c1266d9f981ae37a9e4647f42b9a4bb5466f867f4012e582c9e7e + languageName: node + linkType: hard + +"map-age-cleaner@npm:^0.1.3": + version: 0.1.3 + resolution: "map-age-cleaner@npm:0.1.3" + dependencies: + p-defer: "npm:^1.0.0" + checksum: 10c0/7495236c7b0950956c144fd8b4bc6399d4e78072a8840a4232fe1c4faccbb5eb5d842e5c0a56a60afc36d723f315c1c672325ca03c1b328650f7fcc478f385fd + languageName: node + linkType: hard + +"matcher@npm:^5.0.0": + version: 5.0.0 + resolution: "matcher@npm:5.0.0" + dependencies: + escape-string-regexp: "npm:^5.0.0" + checksum: 10c0/eda5471fc9d5b7264d63c81727824adc3585ddb5cfdc5fce5a9b7c86f946ff181610735d330b1c37a84811df872d1290bf4e9401d2be2a414204343701144b18 + languageName: node + linkType: hard + +"md5-hex@npm:^3.0.1": + version: 3.0.1 + resolution: "md5-hex@npm:3.0.1" + dependencies: + blueimp-md5: "npm:^2.10.0" + checksum: 10c0/ee2b4d8da16b527b3a3fe4d7a96720f43afd07b46a82d49421208b5a126235fb75cfb30b80d4029514772c8844273f940bddfbf4155c787f968f3be4060d01e4 + languageName: node + linkType: hard + +"mem@npm:^9.0.2": + version: 9.0.2 + resolution: "mem@npm:9.0.2" + dependencies: + map-age-cleaner: "npm:^0.1.3" + mimic-fn: "npm:^4.0.0" + checksum: 10c0/c2c56141399e520d8f0e50186bb7e4b49300b33984dc919682f3f13e53dec0e6608fbd327d5ae99494f45061a3a05a8ee04ccba6dcf795c3c215b5aa906eb41f + languageName: node + linkType: hard + +"merge2@npm:^1.3.0, merge2@npm:^1.4.1": + version: 1.4.1 + resolution: "merge2@npm:1.4.1" + checksum: 10c0/254a8a4605b58f450308fc474c82ac9a094848081bf4c06778200207820e5193726dc563a0d2c16468810516a5c97d9d3ea0ca6585d23c58ccfff2403e8dbbeb + languageName: node + linkType: hard + +"micromatch@npm:^4.0.4": + version: 4.0.8 + resolution: "micromatch@npm:4.0.8" + dependencies: + braces: "npm:^3.0.3" + picomatch: "npm:^2.3.1" + checksum: 10c0/166fa6eb926b9553f32ef81f5f531d27b4ce7da60e5baf8c021d043b27a388fb95e46a8038d5045877881e673f8134122b59624d5cecbd16eb50a42e7a6b5ca8 + languageName: node + linkType: hard + +"mimic-fn@npm:^4.0.0": + version: 4.0.0 + resolution: "mimic-fn@npm:4.0.0" + checksum: 10c0/de9cc32be9996fd941e512248338e43407f63f6d497abe8441fa33447d922e927de54d4cc3c1a3c6d652857acd770389d5a3823f311a744132760ce2be15ccbf + languageName: node + linkType: hard + +"mimic-response@npm:^3.1.0": + version: 3.1.0 + resolution: "mimic-response@npm:3.1.0" + checksum: 10c0/0d6f07ce6e03e9e4445bee655202153bdb8a98d67ee8dc965ac140900d7a2688343e6b4c9a72cfc9ef2f7944dfd76eef4ab2482eb7b293a68b84916bac735362 + languageName: node + linkType: hard + +"minimatch@npm:^9.0.4": + version: 9.0.5 + resolution: "minimatch@npm:9.0.5" + dependencies: + brace-expansion: "npm:^2.0.1" + checksum: 10c0/de96cf5e35bdf0eab3e2c853522f98ffbe9a36c37797778d2665231ec1f20a9447a7e567cb640901f89e4daaa95ae5d70c65a9e8aa2bb0019b6facbc3c0575ed + languageName: node + linkType: hard + +"minimist@npm:^1.2.0, minimist@npm:^1.2.3": + version: 1.2.8 + resolution: "minimist@npm:1.2.8" + checksum: 10c0/19d3fcdca050087b84c2029841a093691a91259a47def2f18222f41e7645a0b7c44ef4b40e88a1e58a40c84d2ef0ee6047c55594d298146d0eb3f6b737c20ce6 + languageName: node + linkType: hard + +"minipass-collect@npm:^2.0.1": + version: 2.0.1 + resolution: "minipass-collect@npm:2.0.1" + dependencies: + minipass: "npm:^7.0.3" + checksum: 10c0/5167e73f62bb74cc5019594709c77e6a742051a647fe9499abf03c71dca75515b7959d67a764bdc4f8b361cf897fbf25e2d9869ee039203ed45240f48b9aa06e + languageName: node + linkType: hard + +"minipass-fetch@npm:^3.0.0": + version: 3.0.5 + resolution: "minipass-fetch@npm:3.0.5" + dependencies: + encoding: "npm:^0.1.13" + minipass: "npm:^7.0.3" + minipass-sized: "npm:^1.0.3" + minizlib: "npm:^2.1.2" + dependenciesMeta: + encoding: + optional: true + checksum: 10c0/9d702d57f556274286fdd97e406fc38a2f5c8d15e158b498d7393b1105974b21249289ec571fa2b51e038a4872bfc82710111cf75fae98c662f3d6f95e72152b + languageName: node + linkType: hard + +"minipass-flush@npm:^1.0.5": + version: 1.0.5 + resolution: "minipass-flush@npm:1.0.5" + dependencies: + minipass: "npm:^3.0.0" + checksum: 10c0/2a51b63feb799d2bb34669205eee7c0eaf9dce01883261a5b77410c9408aa447e478efd191b4de6fc1101e796ff5892f8443ef20d9544385819093dbb32d36bd + languageName: node + linkType: hard + +"minipass-pipeline@npm:^1.2.4": + version: 1.2.4 + resolution: "minipass-pipeline@npm:1.2.4" + dependencies: + minipass: "npm:^3.0.0" + checksum: 10c0/cbda57cea20b140b797505dc2cac71581a70b3247b84480c1fed5ca5ba46c25ecc25f68bfc9e6dcb1a6e9017dab5c7ada5eab73ad4f0a49d84e35093e0c643f2 + languageName: node + linkType: hard + +"minipass-sized@npm:^1.0.3": + version: 1.0.3 + resolution: "minipass-sized@npm:1.0.3" + dependencies: + minipass: "npm:^3.0.0" + checksum: 10c0/298f124753efdc745cfe0f2bdfdd81ba25b9f4e753ca4a2066eb17c821f25d48acea607dfc997633ee5bf7b6dfffb4eee4f2051eb168663f0b99fad2fa4829cb + languageName: node + linkType: hard + +"minipass@npm:^3.0.0": + version: 3.3.6 + resolution: "minipass@npm:3.3.6" + dependencies: + yallist: "npm:^4.0.0" + checksum: 10c0/a114746943afa1dbbca8249e706d1d38b85ed1298b530f5808ce51f8e9e941962e2a5ad2e00eae7dd21d8a4aae6586a66d4216d1a259385e9d0358f0c1eba16c + languageName: node + linkType: hard + +"minipass@npm:^5.0.0": + version: 5.0.0 + resolution: "minipass@npm:5.0.0" + checksum: 10c0/a91d8043f691796a8ac88df039da19933ef0f633e3d7f0d35dcd5373af49131cf2399bfc355f41515dc495e3990369c3858cd319e5c2722b4753c90bf3152462 + languageName: node + linkType: hard + +"minipass@npm:^5.0.0 || ^6.0.2 || ^7.0.0, minipass@npm:^7.0.2, minipass@npm:^7.0.3, minipass@npm:^7.1.2": + version: 7.1.2 + resolution: "minipass@npm:7.1.2" + checksum: 10c0/b0fd20bb9fb56e5fa9a8bfac539e8915ae07430a619e4b86ff71f5fc757ef3924b23b2c4230393af1eda647ed3d75739e4e0acb250a6b1eb277cf7f8fe449557 + languageName: node + linkType: hard + +"minizlib@npm:^2.1.1, minizlib@npm:^2.1.2": + version: 2.1.2 + resolution: "minizlib@npm:2.1.2" + dependencies: + minipass: "npm:^3.0.0" + yallist: "npm:^4.0.0" + checksum: 10c0/64fae024e1a7d0346a1102bb670085b17b7f95bf6cfdf5b128772ec8faf9ea211464ea4add406a3a6384a7d87a0cd1a96263692134323477b4fb43659a6cab78 + languageName: node + linkType: hard + +"mkdirp-classic@npm:^0.5.2, mkdirp-classic@npm:^0.5.3": + version: 0.5.3 + resolution: "mkdirp-classic@npm:0.5.3" + checksum: 10c0/95371d831d196960ddc3833cc6907e6b8f67ac5501a6582f47dfae5eb0f092e9f8ce88e0d83afcae95d6e2b61a01741ba03714eeafb6f7a6e9dcc158ac85b168 + languageName: node + linkType: hard + +"mkdirp@npm:^1.0.3": + version: 1.0.4 + resolution: "mkdirp@npm:1.0.4" + bin: + mkdirp: bin/cmd.js + checksum: 10c0/46ea0f3ffa8bc6a5bc0c7081ffc3907777f0ed6516888d40a518c5111f8366d97d2678911ad1a6882bf592fa9de6c784fea32e1687bb94e1f4944170af48a5cf + languageName: node + linkType: hard + +"ms@npm:^2.1.3": + version: 2.1.3 + resolution: "ms@npm:2.1.3" + checksum: 10c0/d924b57e7312b3b63ad21fc5b3dc0af5e78d61a1fc7cfb5457edaf26326bf62be5307cc87ffb6862ef1c2b33b0233cdb5d4f01c4c958cc0d660948b65a287a48 + languageName: node + linkType: hard + +"napi-build-utils@npm:^1.0.1": + version: 1.0.2 + resolution: "napi-build-utils@npm:1.0.2" + checksum: 10c0/37fd2cd0ff2ad20073ce78d83fd718a740d568b225924e753ae51cb69d68f330c80544d487e5e5bd18e28702ed2ca469c2424ad948becd1862c1b0209542b2e9 + languageName: node + linkType: hard + +"negotiator@npm:^0.6.3": + version: 0.6.3 + resolution: "negotiator@npm:0.6.3" + checksum: 10c0/3ec9fd413e7bf071c937ae60d572bc67155262068ed522cf4b3be5edbe6ddf67d095ec03a3a14ebf8fc8e95f8e1d61be4869db0dbb0de696f6b837358bd43fc2 + languageName: node + linkType: hard + +"node-abi@npm:^3.3.0": + version: 3.71.0 + resolution: "node-abi@npm:3.71.0" + dependencies: + semver: "npm:^7.3.5" + checksum: 10c0/dbd0792ea729329cd9d099f28a5681ff9e8a6db48cf64e1437bf6a7fd669009d1e758a784619a1c4cc8bfd1ed17162f042c787654edf19a1f64b5018457c9c1f + languageName: node + linkType: hard + +"node-gyp@npm:latest": + version: 10.2.0 + resolution: "node-gyp@npm:10.2.0" + dependencies: + env-paths: "npm:^2.2.0" + exponential-backoff: "npm:^3.1.1" + glob: "npm:^10.3.10" + graceful-fs: "npm:^4.2.6" + make-fetch-happen: "npm:^13.0.0" + nopt: "npm:^7.0.0" + proc-log: "npm:^4.1.0" + semver: "npm:^7.3.5" + tar: "npm:^6.2.1" + which: "npm:^4.0.0" + bin: + node-gyp: bin/node-gyp.js + checksum: 10c0/00630d67dbd09a45aee0a5d55c05e3916ca9e6d427ee4f7bc392d2d3dc5fad7449b21fc098dd38260a53d9dcc9c879b36704a1994235d4707e7271af7e9a835b + languageName: node + linkType: hard + +"nofilter@npm:^3.1.0": + version: 3.1.0 + resolution: "nofilter@npm:3.1.0" + checksum: 10c0/92459f3864a067b347032263f0b536223cbfc98153913b5dce350cb39c8470bc1813366e41993f22c33cc6400c0f392aa324a4b51e24c22040635c1cdb046499 + languageName: node + linkType: hard + +"nopt@npm:^7.0.0": + version: 7.2.1 + resolution: "nopt@npm:7.2.1" + dependencies: + abbrev: "npm:^2.0.0" + bin: + nopt: bin/nopt.js + checksum: 10c0/a069c7c736767121242037a22a788863accfa932ab285a1eb569eb8cd534b09d17206f68c37f096ae785647435e0c5a5a0a67b42ec743e481a455e5ae6a6df81 + languageName: node + linkType: hard + +"normalize-path@npm:^3.0.0, normalize-path@npm:~3.0.0": + version: 3.0.0 + resolution: "normalize-path@npm:3.0.0" + checksum: 10c0/e008c8142bcc335b5e38cf0d63cfd39d6cf2d97480af9abdbe9a439221fd4d749763bab492a8ee708ce7a194bb00c9da6d0a115018672310850489137b3da046 + languageName: node + linkType: hard + +"npm-run-path@npm:^6.0.0": + version: 6.0.0 + resolution: "npm-run-path@npm:6.0.0" + dependencies: + path-key: "npm:^4.0.0" + unicorn-magic: "npm:^0.3.0" + checksum: 10c0/b223c8a0dcd608abf95363ea5c3c0ccc3cd877daf0102eaf1b0f2390d6858d8337fbb7c443af2403b067a7d2c116d10691ecd22ab3c5273c44da1ff8d07753bd + languageName: node + linkType: hard + +"once@npm:^1.3.1, once@npm:^1.4.0": + version: 1.4.0 + resolution: "once@npm:1.4.0" + dependencies: + wrappy: "npm:1" + checksum: 10c0/5d48aca287dfefabd756621c5dfce5c91a549a93e9fdb7b8246bc4c4790aa2ec17b34a260530474635147aeb631a2dcc8b32c613df0675f96041cbb8244517d0 + languageName: node + linkType: hard + +"p-defer@npm:^1.0.0": + version: 1.0.0 + resolution: "p-defer@npm:1.0.0" + checksum: 10c0/ed603c3790e74b061ac2cb07eb6e65802cf58dce0fbee646c113a7b71edb711101329ad38f99e462bd2e343a74f6e9366b496a35f1d766c187084d3109900487 + languageName: node + linkType: hard + +"p-event@npm:^5.0.1": + version: 5.0.1 + resolution: "p-event@npm:5.0.1" + dependencies: + p-timeout: "npm:^5.0.2" + checksum: 10c0/2317171489537f316661fa863f3bb711b2ceb89182937238422cec10223cbb958c432d6c26a238446a622d788187bdd295b1d8ecedbe2e467e045930d60202b0 + languageName: node + linkType: hard + +"p-limit@npm:^4.0.0": + version: 4.0.0 + resolution: "p-limit@npm:4.0.0" + dependencies: + yocto-queue: "npm:^1.0.0" + checksum: 10c0/a56af34a77f8df2ff61ddfb29431044557fcbcb7642d5a3233143ebba805fc7306ac1d448de724352861cb99de934bc9ab74f0d16fe6a5460bdbdf938de875ad + languageName: node + linkType: hard + +"p-locate@npm:^6.0.0": + version: 6.0.0 + resolution: "p-locate@npm:6.0.0" + dependencies: + p-limit: "npm:^4.0.0" + checksum: 10c0/d72fa2f41adce59c198270aa4d3c832536c87a1806e0f69dffb7c1a7ca998fb053915ca833d90f166a8c082d3859eabfed95f01698a3214c20df6bb8de046312 + languageName: node + linkType: hard + +"p-map@npm:^4.0.0": + version: 4.0.0 + resolution: "p-map@npm:4.0.0" + dependencies: + aggregate-error: "npm:^3.0.0" + checksum: 10c0/592c05bd6262c466ce269ff172bb8de7c6975afca9b50c975135b974e9bdaafbfe80e61aaaf5be6d1200ba08b30ead04b88cfa7e25ff1e3b93ab28c9f62a2c75 + languageName: node + linkType: hard + +"p-map@npm:^5.5.0": + version: 5.5.0 + resolution: "p-map@npm:5.5.0" + dependencies: + aggregate-error: "npm:^4.0.0" + checksum: 10c0/410bce846b1e3db6bb2ccab6248372ecf4e635fc2b31331c8f56478e73fec9e146e8b4547585e635703160a3d252a6a65b8f855834aebc2c3408eb5789630cc4 + languageName: node + linkType: hard + +"p-timeout@npm:^5.0.2": + version: 5.1.0 + resolution: "p-timeout@npm:5.1.0" + checksum: 10c0/1b026cf9d5878c64bec4341ca9cda8ec6b8b3aea8a57885ca0fe2b35753a20d767fb6f9d3aa41e1252f42bc95432c05ea33b6b18f271fb10bfb0789591850a41 + languageName: node + linkType: hard + +"package-json-from-dist@npm:^1.0.0": + version: 1.0.1 + resolution: "package-json-from-dist@npm:1.0.1" + checksum: 10c0/62ba2785eb655fec084a257af34dbe24292ab74516d6aecef97ef72d4897310bc6898f6c85b5cd22770eaa1ce60d55a0230e150fb6a966e3ecd6c511e23d164b + languageName: node + linkType: hard + +"parse-ms@npm:^3.0.0": + version: 3.0.0 + resolution: "parse-ms@npm:3.0.0" + checksum: 10c0/056b4a32a9d3749f3f4cfffefb45c45540491deaa8e1d8ad43c2ddde7ba04edd076bd1b298f521238bb5fb084a9b2c4a2ebb78aefa651afbc4c2b0af4232fc54 + languageName: node + linkType: hard + +"parse-ms@npm:^4.0.0": + version: 4.0.0 + resolution: "parse-ms@npm:4.0.0" + checksum: 10c0/a7900f4f1ebac24cbf5e9708c16fb2fd482517fad353aecd7aefb8c2ba2f85ce017913ccb8925d231770404780df46244ea6fec598b3bde6490882358b4d2d16 + languageName: node + linkType: hard + +"path-exists@npm:^5.0.0": + version: 5.0.0 + resolution: "path-exists@npm:5.0.0" + checksum: 10c0/b170f3060b31604cde93eefdb7392b89d832dfbc1bed717c9718cbe0f230c1669b7e75f87e19901da2250b84d092989a0f9e44d2ef41deb09aa3ad28e691a40a + languageName: node + linkType: hard + +"path-key@npm:^3.1.0": + version: 3.1.1 + resolution: "path-key@npm:3.1.1" + checksum: 10c0/748c43efd5a569c039d7a00a03b58eecd1d75f3999f5a28303d75f521288df4823bc057d8784eb72358b2895a05f29a070bc9f1f17d28226cc4e62494cc58c4c + languageName: node + linkType: hard + +"path-key@npm:^4.0.0": + version: 4.0.0 + resolution: "path-key@npm:4.0.0" + checksum: 10c0/794efeef32863a65ac312f3c0b0a99f921f3e827ff63afa5cb09a377e202c262b671f7b3832a4e64731003fa94af0263713962d317b9887bd1e0c48a342efba3 + languageName: node + linkType: hard + +"path-scurry@npm:^1.11.1": + version: 1.11.1 + resolution: "path-scurry@npm:1.11.1" + dependencies: + lru-cache: "npm:^10.2.0" + minipass: "npm:^5.0.0 || ^6.0.2 || ^7.0.0" + checksum: 10c0/32a13711a2a505616ae1cc1b5076801e453e7aae6ac40ab55b388bb91b9d0547a52f5aaceff710ea400205f18691120d4431e520afbe4266b836fadede15872d + languageName: node + linkType: hard + +"path-type@npm:^4.0.0": + version: 4.0.0 + resolution: "path-type@npm:4.0.0" + checksum: 10c0/666f6973f332f27581371efaf303fd6c272cc43c2057b37aa99e3643158c7e4b2626549555d88626e99ea9e046f82f32e41bbde5f1508547e9a11b149b52387c + languageName: node + linkType: hard + +"picomatch@npm:^2.0.4, picomatch@npm:^2.2.1, picomatch@npm:^2.3.1": + version: 2.3.1 + resolution: "picomatch@npm:2.3.1" + checksum: 10c0/26c02b8d06f03206fc2ab8d16f19960f2ff9e81a658f831ecb656d8f17d9edc799e8364b1f4a7873e89d9702dff96204be0fa26fe4181f6843f040f819dac4be + languageName: node + linkType: hard + +"pkg-conf@npm:^4.0.0": + version: 4.0.0 + resolution: "pkg-conf@npm:4.0.0" + dependencies: + find-up: "npm:^6.0.0" + load-json-file: "npm:^7.0.0" + checksum: 10c0/27d027609f27228edcde121f6f707b4ba1f5488e95e98f2e58652ae4e99792081bd1de67d591f4a0f05b02c0b66d745591d49f82041cbc8d41e2238ef5d73eb4 + languageName: node + linkType: hard + +"plur@npm:^5.1.0": + version: 5.1.0 + resolution: "plur@npm:5.1.0" + dependencies: + irregular-plurals: "npm:^3.3.0" + checksum: 10c0/26bb622b8545fcfd47bbf56fbcca66c08693708a232e403fa3589e00003c56c14231ac57c7588ca5db83ef4be1f61383402c4ea954000768f779f8aef6eb6da8 + languageName: node + linkType: hard + +"prebuild-install@npm:^7.1.1": + version: 7.1.2 + resolution: "prebuild-install@npm:7.1.2" + dependencies: + detect-libc: "npm:^2.0.0" + expand-template: "npm:^2.0.3" + github-from-package: "npm:0.0.0" + minimist: "npm:^1.2.3" + mkdirp-classic: "npm:^0.5.3" + napi-build-utils: "npm:^1.0.1" + node-abi: "npm:^3.3.0" + pump: "npm:^3.0.0" + rc: "npm:^1.2.7" + simple-get: "npm:^4.0.0" + tar-fs: "npm:^2.0.0" + tunnel-agent: "npm:^0.6.0" + bin: + prebuild-install: bin.js + checksum: 10c0/e64868ba9ef2068fd7264f5b03e5298a901e02a450acdb1f56258d88c09dea601eefdb3d1dfdff8513fdd230a92961712be0676192626a3b4d01ba154d48bdd3 + languageName: node + linkType: hard + +"pretty-ms@npm:^8.0.0": + version: 8.0.0 + resolution: "pretty-ms@npm:8.0.0" + dependencies: + parse-ms: "npm:^3.0.0" + checksum: 10c0/e960d633ecca45445cf5c6dffc0f5e4bef6744c92449ab0e8c6c704800675ab71e181c5e02ece5265e02137a33e313d3f3e355fbf8ea30b4b5b23de423329f8d + languageName: node + linkType: hard + +"pretty-ms@npm:^9.0.0": + version: 9.1.0 + resolution: "pretty-ms@npm:9.1.0" + dependencies: + parse-ms: "npm:^4.0.0" + checksum: 10c0/fd111aad8800a04dfd654e6016da69bdaa6fc6a4c280f8e727cffd8b5960558e94942f1a94d4aa6e4d179561a0fbb0366a9ebe0ccefbbb0f8ff853b129cdefb9 + languageName: node + linkType: hard + +"proc-log@npm:^4.1.0, proc-log@npm:^4.2.0": + version: 4.2.0 + resolution: "proc-log@npm:4.2.0" + checksum: 10c0/17db4757c2a5c44c1e545170e6c70a26f7de58feb985091fb1763f5081cab3d01b181fb2dd240c9f4a4255a1d9227d163d5771b7e69c9e49a561692db865efb9 + languageName: node + linkType: hard + +"promise-retry@npm:^2.0.1": + version: 2.0.1 + resolution: "promise-retry@npm:2.0.1" + dependencies: + err-code: "npm:^2.0.2" + retry: "npm:^0.12.0" + checksum: 10c0/9c7045a1a2928094b5b9b15336dcd2a7b1c052f674550df63cc3f36cd44028e5080448175b6f6ca32b642de81150f5e7b1a98b728f15cb069f2dd60ac2616b96 + languageName: node + linkType: hard + +"pump@npm:^3.0.0": + version: 3.0.2 + resolution: "pump@npm:3.0.2" + dependencies: + end-of-stream: "npm:^1.1.0" + once: "npm:^1.3.1" + checksum: 10c0/5ad655cb2a7738b4bcf6406b24ad0970d680649d996b55ad20d1be8e0c02394034e4c45ff7cd105d87f1e9b96a0e3d06fd28e11fae8875da26e7f7a8e2c9726f + languageName: node + linkType: hard + +"queue-microtask@npm:^1.2.2": + version: 1.2.3 + resolution: "queue-microtask@npm:1.2.3" + checksum: 10c0/900a93d3cdae3acd7d16f642c29a642aea32c2026446151f0778c62ac089d4b8e6c986811076e1ae180a694cedf077d453a11b58ff0a865629a4f82ab558e102 + languageName: node + linkType: hard + +"rc@npm:^1.2.7": + version: 1.2.8 + resolution: "rc@npm:1.2.8" + dependencies: + deep-extend: "npm:^0.6.0" + ini: "npm:~1.3.0" + minimist: "npm:^1.2.0" + strip-json-comments: "npm:~2.0.1" + bin: + rc: ./cli.js + checksum: 10c0/24a07653150f0d9ac7168e52943cc3cb4b7a22c0e43c7dff3219977c2fdca5a2760a304a029c20811a0e79d351f57d46c9bde216193a0f73978496afc2b85b15 + languageName: node + linkType: hard + +"readable-stream@npm:^3.1.1, readable-stream@npm:^3.4.0": + version: 3.6.2 + resolution: "readable-stream@npm:3.6.2" + dependencies: + inherits: "npm:^2.0.3" + string_decoder: "npm:^1.1.1" + util-deprecate: "npm:^1.0.1" + checksum: 10c0/e37be5c79c376fdd088a45fa31ea2e423e5d48854be7a22a58869b4e84d25047b193f6acb54f1012331e1bcd667ffb569c01b99d36b0bd59658fb33f513511b7 + languageName: node + linkType: hard + +"readdirp@npm:~3.6.0": + version: 3.6.0 + resolution: "readdirp@npm:3.6.0" + dependencies: + picomatch: "npm:^2.2.1" + checksum: 10c0/6fa848cf63d1b82ab4e985f4cf72bd55b7dcfd8e0a376905804e48c3634b7e749170940ba77b32804d5fe93b3cc521aa95a8d7e7d725f830da6d93f3669ce66b + languageName: node + linkType: hard + +"require-directory@npm:^2.1.1": + version: 2.1.1 + resolution: "require-directory@npm:2.1.1" + checksum: 10c0/83aa76a7bc1531f68d92c75a2ca2f54f1b01463cb566cf3fbc787d0de8be30c9dbc211d1d46be3497dac5785fe296f2dd11d531945ac29730643357978966e99 + languageName: node + linkType: hard + +"resolve-cwd@npm:^3.0.0": + version: 3.0.0 + resolution: "resolve-cwd@npm:3.0.0" + dependencies: + resolve-from: "npm:^5.0.0" + checksum: 10c0/e608a3ebd15356264653c32d7ecbc8fd702f94c6703ea4ac2fb81d9c359180cba0ae2e6b71faa446631ed6145454d5a56b227efc33a2d40638ac13f8beb20ee4 + languageName: node + linkType: hard + +"resolve-from@npm:^5.0.0": + version: 5.0.0 + resolution: "resolve-from@npm:5.0.0" + checksum: 10c0/b21cb7f1fb746de8107b9febab60095187781137fd803e6a59a76d421444b1531b641bba5857f5dc011974d8a5c635d61cec49e6bd3b7fc20e01f0fafc4efbf2 + languageName: node + linkType: hard + +"retry@npm:^0.12.0": + version: 0.12.0 + resolution: "retry@npm:0.12.0" + checksum: 10c0/59933e8501727ba13ad73ef4a04d5280b3717fd650408460c987392efe9d7be2040778ed8ebe933c5cbd63da3dcc37919c141ef8af0a54a6e4fca5a2af177bfe + languageName: node + linkType: hard + +"reusify@npm:^1.0.4": + version: 1.0.4 + resolution: "reusify@npm:1.0.4" + checksum: 10c0/c19ef26e4e188f408922c46f7ff480d38e8dfc55d448310dfb518736b23ed2c4f547fb64a6ed5bdba92cd7e7ddc889d36ff78f794816d5e71498d645ef476107 + languageName: node + linkType: hard + +"root-workspace-0b6124@workspace:.": + version: 0.0.0-use.local + resolution: "root-workspace-0b6124@workspace:." + dependencies: + "@agoric/synthetic-chain": "npm:^0.3.0" + ava: "npm:^5.3.1" + languageName: unknown + linkType: soft + +"run-parallel@npm:^1.1.9": + version: 1.2.0 + resolution: "run-parallel@npm:1.2.0" + dependencies: + queue-microtask: "npm:^1.2.2" + checksum: 10c0/200b5ab25b5b8b7113f9901bfe3afc347e19bb7475b267d55ad0eb86a62a46d77510cb0f232507c9e5d497ebda569a08a9867d0d14f57a82ad5564d991588b39 + languageName: node + linkType: hard + +"safe-buffer@npm:^5.0.1, safe-buffer@npm:~5.2.0": + version: 5.2.1 + resolution: "safe-buffer@npm:5.2.1" + checksum: 10c0/6501914237c0a86e9675d4e51d89ca3c21ffd6a31642efeba25ad65720bce6921c9e7e974e5be91a786b25aa058b5303285d3c15dbabf983a919f5f630d349f3 + languageName: node + linkType: hard + +"safer-buffer@npm:>= 2.1.2 < 3.0.0": + version: 2.1.2 + resolution: "safer-buffer@npm:2.1.2" + checksum: 10c0/7e3c8b2e88a1841c9671094bbaeebd94448111dd90a81a1f606f3f67708a6ec57763b3b47f06da09fc6054193e0e6709e77325415dc8422b04497a8070fa02d4 + languageName: node + linkType: hard + +"semver@npm:^7.3.2, semver@npm:^7.3.5": + version: 7.6.3 + resolution: "semver@npm:7.6.3" + bin: + semver: bin/semver.js + checksum: 10c0/88f33e148b210c153873cb08cfe1e281d518aaa9a666d4d148add6560db5cd3c582f3a08ccb91f38d5f379ead256da9931234ed122057f40bb5766e65e58adaf + languageName: node + linkType: hard + +"serialize-error@npm:^7.0.1": + version: 7.0.1 + resolution: "serialize-error@npm:7.0.1" + dependencies: + type-fest: "npm:^0.13.1" + checksum: 10c0/7982937d578cd901276c8ab3e2c6ed8a4c174137730f1fb0402d005af209a0e84d04acc874e317c936724c7b5b26c7a96ff7e4b8d11a469f4924a4b0ea814c05 + languageName: node + linkType: hard + +"shebang-command@npm:^2.0.0": + version: 2.0.0 + resolution: "shebang-command@npm:2.0.0" + dependencies: + shebang-regex: "npm:^3.0.0" + checksum: 10c0/a41692e7d89a553ef21d324a5cceb5f686d1f3c040759c50aab69688634688c5c327f26f3ecf7001ebfd78c01f3c7c0a11a7c8bfd0a8bc9f6240d4f40b224e4e + languageName: node + linkType: hard + +"shebang-regex@npm:^3.0.0": + version: 3.0.0 + resolution: "shebang-regex@npm:3.0.0" + checksum: 10c0/1dbed0726dd0e1152a92696c76c7f06084eb32a90f0528d11acd764043aacf76994b2fb30aa1291a21bd019d6699164d048286309a278855ee7bec06cf6fb690 + languageName: node + linkType: hard + +"signal-exit@npm:^4.0.1, signal-exit@npm:^4.1.0": + version: 4.1.0 + resolution: "signal-exit@npm:4.1.0" + checksum: 10c0/41602dce540e46d599edba9d9860193398d135f7ff72cab629db5171516cfae628d21e7bfccde1bbfdf11c48726bc2a6d1a8fb8701125852fbfda7cf19c6aa83 + languageName: node + linkType: hard + +"simple-concat@npm:^1.0.0": + version: 1.0.1 + resolution: "simple-concat@npm:1.0.1" + checksum: 10c0/62f7508e674414008910b5397c1811941d457dfa0db4fd5aa7fa0409eb02c3609608dfcd7508cace75b3a0bf67a2a77990711e32cd213d2c76f4fd12ee86d776 + languageName: node + linkType: hard + +"simple-get@npm:^4.0.0": + version: 4.0.1 + resolution: "simple-get@npm:4.0.1" + dependencies: + decompress-response: "npm:^6.0.0" + once: "npm:^1.3.1" + simple-concat: "npm:^1.0.0" + checksum: 10c0/b0649a581dbca741babb960423248899203165769747142033479a7dc5e77d7b0fced0253c731cd57cf21e31e4d77c9157c3069f4448d558ebc96cf9e1eebcf0 + languageName: node + linkType: hard + +"slash@npm:^4.0.0": + version: 4.0.0 + resolution: "slash@npm:4.0.0" + checksum: 10c0/b522ca75d80d107fd30d29df0549a7b2537c83c4c4ecd12cd7d4ea6c8aaca2ab17ada002e7a1d78a9d736a0261509f26ea5b489082ee443a3a810586ef8eff18 + languageName: node + linkType: hard + +"slice-ansi@npm:^5.0.0": + version: 5.0.0 + resolution: "slice-ansi@npm:5.0.0" + dependencies: + ansi-styles: "npm:^6.0.0" + is-fullwidth-code-point: "npm:^4.0.0" + checksum: 10c0/2d4d40b2a9d5cf4e8caae3f698fe24ae31a4d778701724f578e984dcb485ec8c49f0c04dab59c401821e80fcdfe89cace9c66693b0244e40ec485d72e543914f + languageName: node + linkType: hard + +"smart-buffer@npm:^4.2.0": + version: 4.2.0 + resolution: "smart-buffer@npm:4.2.0" + checksum: 10c0/a16775323e1404dd43fabafe7460be13a471e021637bc7889468eb45ce6a6b207261f454e4e530a19500cc962c4cc5348583520843b363f4193cee5c00e1e539 + languageName: node + linkType: hard + +"socks-proxy-agent@npm:^8.0.3": + version: 8.0.4 + resolution: "socks-proxy-agent@npm:8.0.4" + dependencies: + agent-base: "npm:^7.1.1" + debug: "npm:^4.3.4" + socks: "npm:^2.8.3" + checksum: 10c0/345593bb21b95b0508e63e703c84da11549f0a2657d6b4e3ee3612c312cb3a907eac10e53b23ede3557c6601d63252103494caa306b66560f43af7b98f53957a + languageName: node + linkType: hard + +"socks@npm:^2.8.3": + version: 2.8.3 + resolution: "socks@npm:2.8.3" + dependencies: + ip-address: "npm:^9.0.5" + smart-buffer: "npm:^4.2.0" + checksum: 10c0/d54a52bf9325165770b674a67241143a3d8b4e4c8884560c4e0e078aace2a728dffc7f70150660f51b85797c4e1a3b82f9b7aa25e0a0ceae1a243365da5c51a7 + languageName: node + linkType: hard + +"sprintf-js@npm:^1.1.3": + version: 1.1.3 + resolution: "sprintf-js@npm:1.1.3" + checksum: 10c0/09270dc4f30d479e666aee820eacd9e464215cdff53848b443964202bf4051490538e5dd1b42e1a65cf7296916ca17640aebf63dae9812749c7542ee5f288dec + languageName: node + linkType: hard + +"sprintf-js@npm:~1.0.2": + version: 1.0.3 + resolution: "sprintf-js@npm:1.0.3" + checksum: 10c0/ecadcfe4c771890140da5023d43e190b7566d9cf8b2d238600f31bec0fc653f328da4450eb04bd59a431771a8e9cc0e118f0aa3974b683a4981b4e07abc2a5bb + languageName: node + linkType: hard + +"ssri@npm:^10.0.0": + version: 10.0.6 + resolution: "ssri@npm:10.0.6" + dependencies: + minipass: "npm:^7.0.3" + checksum: 10c0/e5a1e23a4057a86a97971465418f22ea89bd439ac36ade88812dd920e4e61873e8abd6a9b72a03a67ef50faa00a2daf1ab745c5a15b46d03e0544a0296354227 + languageName: node + linkType: hard + +"stack-utils@npm:^2.0.6": + version: 2.0.6 + resolution: "stack-utils@npm:2.0.6" + dependencies: + escape-string-regexp: "npm:^2.0.0" + checksum: 10c0/651c9f87667e077584bbe848acaecc6049bc71979f1e9a46c7b920cad4431c388df0f51b8ad7cfd6eed3db97a2878d0fc8b3122979439ea8bac29c61c95eec8a + languageName: node + linkType: hard + +"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.3": + version: 4.2.3 + resolution: "string-width@npm:4.2.3" + dependencies: + emoji-regex: "npm:^8.0.0" + is-fullwidth-code-point: "npm:^3.0.0" + strip-ansi: "npm:^6.0.1" + checksum: 10c0/1e525e92e5eae0afd7454086eed9c818ee84374bb80328fc41217ae72ff5f065ef1c9d7f72da41de40c75fa8bb3dee63d92373fd492c84260a552c636392a47b + languageName: node + linkType: hard + +"string-width@npm:^5.0.0, string-width@npm:^5.0.1, string-width@npm:^5.1.2": + version: 5.1.2 + resolution: "string-width@npm:5.1.2" + dependencies: + eastasianwidth: "npm:^0.2.0" + emoji-regex: "npm:^9.2.2" + strip-ansi: "npm:^7.0.1" + checksum: 10c0/ab9c4264443d35b8b923cbdd513a089a60de339216d3b0ed3be3ba57d6880e1a192b70ae17225f764d7adbf5994e9bb8df253a944736c15a0240eff553c678ca + languageName: node + linkType: hard + +"string_decoder@npm:^1.1.1": + version: 1.3.0 + resolution: "string_decoder@npm:1.3.0" + dependencies: + safe-buffer: "npm:~5.2.0" + checksum: 10c0/810614ddb030e271cd591935dcd5956b2410dd079d64ff92a1844d6b7588bf992b3e1b69b0f4d34a3e06e0bd73046ac646b5264c1987b20d0601f81ef35d731d + languageName: node + linkType: hard + +"strip-ansi-cjs@npm:strip-ansi@^6.0.1, strip-ansi@npm:^6.0.0, strip-ansi@npm:^6.0.1": + version: 6.0.1 + resolution: "strip-ansi@npm:6.0.1" + dependencies: + ansi-regex: "npm:^5.0.1" + checksum: 10c0/1ae5f212a126fe5b167707f716942490e3933085a5ff6c008ab97ab2f272c8025d3aa218b7bd6ab25729ca20cc81cddb252102f8751e13482a5199e873680952 + languageName: node + linkType: hard + +"strip-ansi@npm:^7.0.1": + version: 7.1.0 + resolution: "strip-ansi@npm:7.1.0" + dependencies: + ansi-regex: "npm:^6.0.1" + checksum: 10c0/a198c3762e8832505328cbf9e8c8381de14a4fa50a4f9b2160138158ea88c0f5549fb50cb13c651c3088f47e63a108b34622ec18c0499b6c8c3a5ddf6b305ac4 + languageName: node + linkType: hard + +"strip-final-newline@npm:^4.0.0": + version: 4.0.0 + resolution: "strip-final-newline@npm:4.0.0" + checksum: 10c0/b0cf2b62d597a1b0e3ebc42b88767f0a0d45601f89fd379a928a1812c8779440c81abba708082c946445af1d6b62d5f16e2a7cf4f30d9d6587b89425fae801ff + languageName: node + linkType: hard + +"strip-json-comments@npm:~2.0.1": + version: 2.0.1 + resolution: "strip-json-comments@npm:2.0.1" + checksum: 10c0/b509231cbdee45064ff4f9fd73609e2bcc4e84a4d508e9dd0f31f70356473fde18abfb5838c17d56fb236f5a06b102ef115438de0600b749e818a35fbbc48c43 + languageName: node + linkType: hard + +"supertap@npm:^3.0.1": + version: 3.0.1 + resolution: "supertap@npm:3.0.1" + dependencies: + indent-string: "npm:^5.0.0" + js-yaml: "npm:^3.14.1" + serialize-error: "npm:^7.0.1" + strip-ansi: "npm:^7.0.1" + checksum: 10c0/8164674f2e280cab875f0fef5bb36c15553c13e29697ff92f4e0d6bc62149f0303a89eee47535413ed145ea72e14a24d065bab233059d48a499ec5ebb4566b0f + languageName: node + linkType: hard + +"tar-fs@npm:^2.0.0": + version: 2.1.1 + resolution: "tar-fs@npm:2.1.1" + dependencies: + chownr: "npm:^1.1.1" + mkdirp-classic: "npm:^0.5.2" + pump: "npm:^3.0.0" + tar-stream: "npm:^2.1.4" + checksum: 10c0/871d26a934bfb7beeae4c4d8a09689f530b565f79bd0cf489823ff0efa3705da01278160da10bb006d1a793fa0425cf316cec029b32a9159eacbeaff4965fb6d + languageName: node + linkType: hard + +"tar-stream@npm:^2.1.4": + version: 2.2.0 + resolution: "tar-stream@npm:2.2.0" + dependencies: + bl: "npm:^4.0.3" + end-of-stream: "npm:^1.4.1" + fs-constants: "npm:^1.0.0" + inherits: "npm:^2.0.3" + readable-stream: "npm:^3.1.1" + checksum: 10c0/2f4c910b3ee7196502e1ff015a7ba321ec6ea837667220d7bcb8d0852d51cb04b87f7ae471008a6fb8f5b1a1b5078f62f3a82d30c706f20ada1238ac797e7692 + languageName: node + linkType: hard + +"tar@npm:^6.1.11, tar@npm:^6.2.1": + version: 6.2.1 + resolution: "tar@npm:6.2.1" + dependencies: + chownr: "npm:^2.0.0" + fs-minipass: "npm:^2.0.0" + minipass: "npm:^5.0.0" + minizlib: "npm:^2.1.1" + mkdirp: "npm:^1.0.3" + yallist: "npm:^4.0.0" + checksum: 10c0/a5eca3eb50bc11552d453488344e6507156b9193efd7635e98e867fab275d527af53d8866e2370cd09dfe74378a18111622ace35af6a608e5223a7d27fe99537 + languageName: node + linkType: hard + +"temp-dir@npm:^3.0.0": + version: 3.0.0 + resolution: "temp-dir@npm:3.0.0" + checksum: 10c0/a86978a400984cd5f315b77ebf3fe53bb58c61f192278cafcb1f3fb32d584a21dc8e08b93171d7874b7cc972234d3455c467306cc1bfc4524b622e5ad3bfd671 + languageName: node + linkType: hard + +"time-zone@npm:^1.0.0": + version: 1.0.0 + resolution: "time-zone@npm:1.0.0" + checksum: 10c0/d00ebd885039109011b6e2423ebbf225160927333c2ade6d833e9cc4676db20759f1f3855fafde00d1bd668c243a6aa68938ce71fe58aab0d514e820d59c1d81 + languageName: node + linkType: hard + +"to-regex-range@npm:^5.0.1": + version: 5.0.1 + resolution: "to-regex-range@npm:5.0.1" + dependencies: + is-number: "npm:^7.0.0" + checksum: 10c0/487988b0a19c654ff3e1961b87f471702e708fa8a8dd02a298ef16da7206692e8552a0250e8b3e8759270f62e9d8314616f6da274734d3b558b1fc7b7724e892 + languageName: node + linkType: hard + +"tunnel-agent@npm:^0.6.0": + version: 0.6.0 + resolution: "tunnel-agent@npm:0.6.0" + dependencies: + safe-buffer: "npm:^5.0.1" + checksum: 10c0/4c7a1b813e7beae66fdbf567a65ec6d46313643753d0beefb3c7973d66fcec3a1e7f39759f0a0b4465883499c6dc8b0750ab8b287399af2e583823e40410a17a + languageName: node + linkType: hard + +"type-fest@npm:^0.13.1": + version: 0.13.1 + resolution: "type-fest@npm:0.13.1" + checksum: 10c0/0c0fa07ae53d4e776cf4dac30d25ad799443e9eef9226f9fddbb69242db86b08584084a99885cfa5a9dfe4c063ebdc9aa7b69da348e735baede8d43f1aeae93b + languageName: node + linkType: hard + +"unicorn-magic@npm:^0.3.0": + version: 0.3.0 + resolution: "unicorn-magic@npm:0.3.0" + checksum: 10c0/0a32a997d6c15f1c2a077a15b1c4ca6f268d574cf5b8975e778bb98e6f8db4ef4e86dfcae4e158cd4c7e38fb4dd383b93b13eefddc7f178dea13d3ac8a603271 + languageName: node + linkType: hard + +"unique-filename@npm:^3.0.0": + version: 3.0.0 + resolution: "unique-filename@npm:3.0.0" + dependencies: + unique-slug: "npm:^4.0.0" + checksum: 10c0/6363e40b2fa758eb5ec5e21b3c7fb83e5da8dcfbd866cc0c199d5534c42f03b9ea9ab069769cc388e1d7ab93b4eeef28ef506ab5f18d910ef29617715101884f + languageName: node + linkType: hard + +"unique-slug@npm:^4.0.0": + version: 4.0.0 + resolution: "unique-slug@npm:4.0.0" + dependencies: + imurmurhash: "npm:^0.1.4" + checksum: 10c0/cb811d9d54eb5821b81b18205750be84cb015c20a4a44280794e915f5a0a70223ce39066781a354e872df3572e8155c228f43ff0cce94c7cbf4da2cc7cbdd635 + languageName: node + linkType: hard + +"util-deprecate@npm:^1.0.1": + version: 1.0.2 + resolution: "util-deprecate@npm:1.0.2" + checksum: 10c0/41a5bdd214df2f6c3ecf8622745e4a366c4adced864bc3c833739791aeeeb1838119af7daed4ba36428114b5c67dcda034a79c882e97e43c03e66a4dd7389942 + languageName: node + linkType: hard + +"well-known-symbols@npm:^2.0.0": + version: 2.0.0 + resolution: "well-known-symbols@npm:2.0.0" + checksum: 10c0/cb6c12e98877e8952ec28d13ae6f4fdb54ae1cb49b16a728720276dadd76c930e6cb0e174af3a4620054dd2752546f842540122920c6e31410208abd4958ee6b + languageName: node + linkType: hard + +"which@npm:^2.0.1": + version: 2.0.2 + resolution: "which@npm:2.0.2" + dependencies: + isexe: "npm:^2.0.0" + bin: + node-which: ./bin/node-which + checksum: 10c0/66522872a768b60c2a65a57e8ad184e5372f5b6a9ca6d5f033d4b0dc98aff63995655a7503b9c0a2598936f532120e81dd8cc155e2e92ed662a2b9377cc4374f + languageName: node + linkType: hard + +"which@npm:^4.0.0": + version: 4.0.0 + resolution: "which@npm:4.0.0" + dependencies: + isexe: "npm:^3.1.1" + bin: + node-which: bin/which.js + checksum: 10c0/449fa5c44ed120ccecfe18c433296a4978a7583bf2391c50abce13f76878d2476defde04d0f79db8165bdf432853c1f8389d0485ca6e8ebce3bbcded513d5e6a + languageName: node + linkType: hard + +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0, wrap-ansi@npm:^7.0.0": + version: 7.0.0 + resolution: "wrap-ansi@npm:7.0.0" + dependencies: + ansi-styles: "npm:^4.0.0" + string-width: "npm:^4.1.0" + strip-ansi: "npm:^6.0.0" + checksum: 10c0/d15fc12c11e4cbc4044a552129ebc75ee3f57aa9c1958373a4db0292d72282f54373b536103987a4a7594db1ef6a4f10acf92978f79b98c49306a4b58c77d4da + languageName: node + linkType: hard + +"wrap-ansi@npm:^8.1.0": + version: 8.1.0 + resolution: "wrap-ansi@npm:8.1.0" + dependencies: + ansi-styles: "npm:^6.1.0" + string-width: "npm:^5.0.1" + strip-ansi: "npm:^7.0.1" + checksum: 10c0/138ff58a41d2f877eae87e3282c0630fc2789012fc1af4d6bd626eeb9a2f9a65ca92005e6e69a75c7b85a68479fe7443c7dbe1eb8fbaa681a4491364b7c55c60 + languageName: node + linkType: hard + +"wrappy@npm:1": + version: 1.0.2 + resolution: "wrappy@npm:1.0.2" + checksum: 10c0/56fece1a4018c6a6c8e28fbc88c87e0fbf4ea8fd64fc6c63b18f4acc4bd13e0ad2515189786dd2c30d3eec9663d70f4ecf699330002f8ccb547e4a18231fc9f0 + languageName: node + linkType: hard + +"write-file-atomic@npm:^5.0.1": + version: 5.0.1 + resolution: "write-file-atomic@npm:5.0.1" + dependencies: + imurmurhash: "npm:^0.1.4" + signal-exit: "npm:^4.0.1" + checksum: 10c0/e8c850a8e3e74eeadadb8ad23c9d9d63e4e792bd10f4836ed74189ef6e996763959f1249c5650e232f3c77c11169d239cbfc8342fc70f3fe401407d23810505d + languageName: node + linkType: hard + +"y18n@npm:^5.0.5": + version: 5.0.8 + resolution: "y18n@npm:5.0.8" + checksum: 10c0/4df2842c36e468590c3691c894bc9cdbac41f520566e76e24f59401ba7d8b4811eb1e34524d57e54bc6d864bcb66baab7ffd9ca42bf1eda596618f9162b91249 + languageName: node + linkType: hard + +"yallist@npm:^4.0.0": + version: 4.0.0 + resolution: "yallist@npm:4.0.0" + checksum: 10c0/2286b5e8dbfe22204ab66e2ef5cc9bbb1e55dfc873bbe0d568aa943eb255d131890dfd5bf243637273d31119b870f49c18fcde2c6ffbb7a7a092b870dc90625a + languageName: node + linkType: hard + +"yargs-parser@npm:^21.1.1": + version: 21.1.1 + resolution: "yargs-parser@npm:21.1.1" + checksum: 10c0/f84b5e48169479d2f402239c59f084cfd1c3acc197a05c59b98bab067452e6b3ea46d4dd8ba2985ba7b3d32a343d77df0debd6b343e5dae3da2aab2cdf5886b2 + languageName: node + linkType: hard + +"yargs@npm:^17.7.2": + version: 17.7.2 + resolution: "yargs@npm:17.7.2" + dependencies: + cliui: "npm:^8.0.1" + escalade: "npm:^3.1.1" + get-caller-file: "npm:^2.0.5" + require-directory: "npm:^2.1.1" + string-width: "npm:^4.2.3" + y18n: "npm:^5.0.5" + yargs-parser: "npm:^21.1.1" + checksum: 10c0/ccd7e723e61ad5965fffbb791366db689572b80cca80e0f96aad968dfff4156cd7cd1ad18607afe1046d8241e6fb2d6c08bf7fa7bfb5eaec818735d8feac8f05 + languageName: node + linkType: hard + +"yocto-queue@npm:^1.0.0": + version: 1.1.1 + resolution: "yocto-queue@npm:1.1.1" + checksum: 10c0/cb287fe5e6acfa82690acb43c283de34e945c571a78a939774f6eaba7c285bacdf6c90fbc16ce530060863984c906d2b4c6ceb069c94d1e0a06d5f2b458e2a92 + languageName: node + linkType: hard + +"yoctocolors@npm:^2.0.0": + version: 2.1.1 + resolution: "yoctocolors@npm:2.1.1" + checksum: 10c0/85903f7fa96f1c70badee94789fade709f9d83dab2ec92753d612d84fcea6d34c772337a9f8914c6bed2f5fc03a428ac5d893e76fab636da5f1236ab725486d0 + languageName: node + linkType: hard diff --git a/packages/orchestration/src/proposals/start-quickSend.js b/packages/orchestration/src/proposals/start-quickSend.js index 17b268de667..b7cf24287d2 100644 --- a/packages/orchestration/src/proposals/start-quickSend.js +++ b/packages/orchestration/src/proposals/start-quickSend.js @@ -109,14 +109,22 @@ export const startQuickSend = async ( }; const { instance, creatorFacet } = await E(startUpgradable)(startOpts); - trace('CF', creatorFacet); + const toWatch = await E(creatorFacet).getWatcherInvitation(); - /** @type {ERef} */ - const wdf = E(namesByAddress).lookup(watcherAddress, 'depositFacet'); - await E(wdf).receive(toWatch); + trace('got invitation', { toWatch, watcherAddress }); + + /** @type {import('@agoric/ertp/src/types.js').DepositFacet} */ + const depositFacet = await E(namesByAddress).lookup( + watcherAddress, + 'depositFacet', + ); + trace(depositFacet, '.receive(', toWatch, ')'); + const amt = await E(depositFacet).receive(toWatch); + trace('sent', amt, 'to', watcherAddress, 'using', depositFacet); + produceInstance.reset(); produceInstance.resolve(instance); - trace('done'); + trace('startQuickSend done', instance); }; harden(startQuickSend); From 02ad1bd31b4b861d63db6b39f4a21de7d7326eb6 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Mon, 21 Oct 2024 15:46:15 -0500 Subject: [PATCH 17/48] chore: extract computron-counter for boot testing --- packages/boot/tools/computron-counter.js | 63 ++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 packages/boot/tools/computron-counter.js diff --git a/packages/boot/tools/computron-counter.js b/packages/boot/tools/computron-counter.js new file mode 100644 index 00000000000..9db1d9e0cf7 --- /dev/null +++ b/packages/boot/tools/computron-counter.js @@ -0,0 +1,63 @@ +// @ts-check + +/** + * @typedef {Awaited>} Controller + */ + +// excerpted from launch-chain.js around line 254 + +/** + * @typedef {object} BeansPerUnit + * @property {bigint} blockComputeLimit + * @property {bigint} vatCreation + * @property {bigint} xsnapComputron + */ + +/** + * @param {BeansPerUnit} beansPerUnit + * @param {boolean} [ignoreBlockLimit] + */ +export function computronCounter( + { blockComputeLimit, vatCreation, xsnapComputron }, + ignoreBlockLimit = false, +) { + assert.typeof(blockComputeLimit, 'bigint'); + assert.typeof(vatCreation, 'bigint'); + assert.typeof(xsnapComputron, 'bigint'); + let totalBeans = 0n; + const shouldRun = () => ignoreBlockLimit || totalBeans < blockComputeLimit; + const remainingBeans = () => + ignoreBlockLimit ? undefined : blockComputeLimit - totalBeans; + + const policy = harden({ + vatCreated() { + totalBeans += vatCreation; + return shouldRun(); + }, + crankComplete(details = {}) { + assert.typeof(details, 'object'); + if (details.computrons) { + assert.typeof(details.computrons, 'bigint'); + + // TODO: xsnapComputron should not be assumed here. + // Instead, SwingSet should describe the computron model it uses. + totalBeans += details.computrons * xsnapComputron; + } + return shouldRun(); + }, + crankFailed() { + const failedComputrons = 1000000n; // who knows, 1M is as good as anything + totalBeans += failedComputrons * xsnapComputron; + return shouldRun(); + }, + emptyCrank() { + return shouldRun(); + }, + shouldRun, + remainingBeans, + totalBeans() { + return totalBeans; + }, + }); + return policy; +} From 1c07c4b07c0a670d695ada2435b0815bfb833615 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Fri, 18 Oct 2024 22:25:12 -0500 Subject: [PATCH 18/48] test(quickSend): initial computron performance measurement We're not measuing a particularly relevant workload: this is the watcher accepting the invitation (and the contract setting up accounts). The more performance-relevant workloads are user advance and settlement. --- .../test/bootstrapTests/quickSend.test.ts | 40 +++++++++++++++++-- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/packages/boot/test/bootstrapTests/quickSend.test.ts b/packages/boot/test/bootstrapTests/quickSend.test.ts index 98887c7cc9e..f737bdcd679 100644 --- a/packages/boot/test/bootstrapTests/quickSend.test.ts +++ b/packages/boot/test/bootstrapTests/quickSend.test.ts @@ -6,14 +6,43 @@ import { type WalletFactoryTestContext, } from './walletFactory.js'; import { WalletFactoryDriver } from '../../tools/drivers.js'; +import { computronCounter } from '../../tools/computron-counter.js'; -const test: TestFn = anyTest; +const makeMeter = () => { + const beansPer = 100n; + // see https://cosgov.org/agoric?msgType=parameterChangeProposal&network=main + const mainParams = { + blockComputeLimit: 65_000_000n * beansPer, + vatCreation: 300_000n * beansPer, + xsnapComputron: beansPer, + }; + + let metering = false; + let policy; + + const meter = harden({ + makeRunPolicy: () => { + policy = metering ? computronCounter(mainParams) : undefined; + return policy; + }, + setMetering: x => (metering = x), + getValue: () => policy.remainingBeans(), + }); + return meter; +}; + +const test: TestFn< + WalletFactoryTestContext & { meter: ReturnType } +> = anyTest; test.before('bootstrap', async t => { - t.context = await makeWalletFactoryContext( + const meter = makeMeter(); + const ctx = await makeWalletFactoryContext( t, '@agoric/vm-config/decentral-itest-orchestration-config.json', + { defaultManagerType: 'xsnap', meter }, ); + t.context = { ...ctx, meter }; // TODO: handle creating watcher smart wallet _after_ deploying contract await t.context.walletFactoryDriver.provideSmartWallet('agoric1watcher'); @@ -21,6 +50,8 @@ test.before('bootstrap', async t => { test.after.always(t => t.context.shutdown?.()); test.serial('deploy contract', async t => { + await t.context.walletFactoryDriver.provideSmartWallet('agoric1watcher'); + const { agoricNamesRemotes, evalProposal, @@ -40,7 +71,7 @@ type SmartWallet = Awaited< >; test.serial('accept watcher invitation', async t => { - const { agoricNamesRemotes } = t.context; + const { agoricNamesRemotes, meter } = t.context; const william = async (sw: SmartWallet) => { await sw.executeOffer({ @@ -62,5 +93,8 @@ test.serial('accept watcher invitation', async t => { const wd = await t.context.walletFactoryDriver.provideSmartWallet('agoric1watcher'); + t.log('start metering'); + meter.setMetering(true); await william(wd); + t.log('metered cost (beans)', meter.getValue()); }); From e91261a0e390a3d4b27bbfb13596b4d0067c679d Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Sun, 20 Oct 2024 00:06:03 -0500 Subject: [PATCH 19/48] fix(quickSend): contract requires USDC brand --- packages/orchestration/src/examples/quickSend.contract.js | 1 + packages/orchestration/src/proposals/start-quickSend.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/orchestration/src/examples/quickSend.contract.js b/packages/orchestration/src/examples/quickSend.contract.js index 14940270c25..df199a07803 100644 --- a/packages/orchestration/src/examples/quickSend.contract.js +++ b/packages/orchestration/src/examples/quickSend.contract.js @@ -38,6 +38,7 @@ export const contract = async (zcf, privateArgs, zone, tools) => { const { storageNode } = privateArgs; const { t } = tools; const terms = zcf.getTerms(); + assert('USDC' in terms.brands, 'no USDC brand'); const makeSettleTap = zone.exoClass( 'SettleTap', diff --git a/packages/orchestration/src/proposals/start-quickSend.js b/packages/orchestration/src/proposals/start-quickSend.js index b7cf24287d2..00c5d61432d 100644 --- a/packages/orchestration/src/proposals/start-quickSend.js +++ b/packages/orchestration/src/proposals/start-quickSend.js @@ -103,7 +103,7 @@ export const startQuickSend = async ( const startOpts = { label: 'quickSend', installation: quickSend, - issuerKeywordRecord: harden({ Fee: USDC.issuer }), + issuerKeywordRecord: harden({ USDC: USDC.issuer }), terms, privateArgs, }; From 41ded8c2617470cfb5201f8180af4783067e7b4c Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Sun, 20 Oct 2024 00:10:11 -0500 Subject: [PATCH 20/48] chore(quickSend): register USDC asset for transfer - use zcf.makeInvitation; we don't want a Vow --- .../src/examples/quickSend.contract.js | 10 ++++------ .../src/examples/quickSend.flows.js | 18 ++++++++++++++++++ 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/packages/orchestration/src/examples/quickSend.contract.js b/packages/orchestration/src/examples/quickSend.contract.js index df199a07803..c84ae445721 100644 --- a/packages/orchestration/src/examples/quickSend.contract.js +++ b/packages/orchestration/src/examples/quickSend.contract.js @@ -10,7 +10,6 @@ import * as flows from './quickSend.flows.js'; * @import {OrchestrationPowers, OrchestrationTools} from '../utils/start-helper.js'; * @import {Zone} from '@agoric/zone'; * @import {VTransferIBCEvent} from '@agoric/vats'; - * @import {InvitationMakers} from '@agoric/smart-wallet/src/types.js'; * @import {QuickSendAccounts} from './quickSend.flows.js'; */ @@ -36,7 +35,8 @@ harden(meta); */ export const contract = async (zcf, privateArgs, zone, tools) => { const { storageNode } = privateArgs; - const { t } = tools; + const { t, chainHub } = tools; + const terms = zcf.getTerms(); assert('USDC' in terms.brands, 'no USDC brand'); @@ -62,7 +62,6 @@ export const contract = async (zcf, privateArgs, zone, tools) => { flows.handleCCTPCall, ); - const { makeInvitation } = tools.zcfTools; const ifaceTODO = undefined; const makeWatcherCont = zone.exoClassKit( 'WatcherCont', @@ -82,11 +81,10 @@ export const contract = async (zcf, privateArgs, zone, tools) => { return handleCCTPCall({ ...this.state }, offerArgs); }, }, - /** @type {import('@agoric/async-flow').HostInterface} */ invitationMakers: { ReportCCTPCall() { const { offerHandler } = this.facets; - return makeInvitation(offerHandler, 'reportCCTPCall'); + return zcf.makeInvitation(offerHandler, 'reportCCTPCall'); }, }, }, @@ -95,7 +93,7 @@ export const contract = async (zcf, privateArgs, zone, tools) => { const initAccounts = tools.orchestrate( 'initAccounts', - { terms, makeSettleTap, makeInvitation, makeWatcherCont, t }, + { terms, chainHub, makeSettleTap, makeWatcherCont, t }, flows.initAccounts, ); diff --git a/packages/orchestration/src/examples/quickSend.flows.js b/packages/orchestration/src/examples/quickSend.flows.js index b669a65a39e..38a3e82f08d 100644 --- a/packages/orchestration/src/examples/quickSend.flows.js +++ b/packages/orchestration/src/examples/quickSend.flows.js @@ -10,6 +10,7 @@ import { AgoricCalc, NobleCalc } from '../utils/address.js'; * * @import {Passable} from '@endo/pass-style'; * @import {Guarded} from '@endo/exo'; + * @import {GuestInterface} from '@agoric/async-flow'; * @import {ChainAddress, OrchestrationAccountI, OrchestrationFlow, Orchestrator, ZcfTools} from '@agoric/orchestration'; * @import {VTransferIBCEvent} from '@agoric/vats'; * @import {ResolvedContinuingOfferResult} from '../../src/utils/zoe-tools.js'; @@ -17,6 +18,7 @@ import { AgoricCalc, NobleCalc } from '../utils/address.js'; * @import {QuickSendTerms} from './quickSend.contract.js'; * @import {FungibleTokenPacketData} from '@agoric/cosmic-proto/ibc/applications/transfer/v2/packet.js'; * @import {TypedPattern} from '@agoric/internal'; + * @import {ChainHub} from '../types.js'; */ const AddressShape = M.string(); // XXX @@ -50,6 +52,8 @@ const { add, make, subtract } = AmountMath; * @satisfies {OrchestrationFlow} * @param {Orchestrator} orch * @param {{ + * terms: QuickSendTerms & StandardTerms; + * chainHub: GuestInterface; * t?: ExecutionContext<{ nextLabel: Function }>; // XXX * makeSettleTap: (accts: QuickSendAccounts) => Guarded<{ * receiveUpcall: (event: VTransferIBCEvent) => void; @@ -66,6 +70,20 @@ export const initAccounts = async (orch, ctx, seat, _offerArgs) => { const { log = console.log } = ctx.t || {}; const agoric = await orch.getChain('agoric'); + const aInfo = await agoric.getVBankAssetInfo(); + for (const a of aInfo) { + if (a.brand === ctx.terms.brands.USDC) { + const [baseName, baseDenom] = ['noble', 'uusdc']; // ??? + await orch.getChain(baseName); + ctx.chainHub.registerAsset(a.denom, { + baseDenom, + baseName, + chainName: 'agoric', + brand: ctx.terms.brands.USDC, + }); + break; + } + } const fundingPool = await agoric.makeAccount(); const settlement = await agoric.makeAccount(); From 80947396a31c48a0eed4fd004c268da24ded84ca Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Sun, 20 Oct 2024 00:14:17 -0500 Subject: [PATCH 21/48] test(quickSend): measure advance computrons - informative offer result from report/advance --- .../test/bootstrapTests/quickSend.test.ts | 130 ++++++++++++++---- .../src/examples/quickSend.flows.js | 2 + 2 files changed, 104 insertions(+), 28 deletions(-) diff --git a/packages/boot/test/bootstrapTests/quickSend.test.ts b/packages/boot/test/bootstrapTests/quickSend.test.ts index f737bdcd679..d552bbdae59 100644 --- a/packages/boot/test/bootstrapTests/quickSend.test.ts +++ b/packages/boot/test/bootstrapTests/quickSend.test.ts @@ -1,12 +1,21 @@ import { test as anyTest } from '@agoric/zoe/tools/prepare-test-env-ava.js'; +import type { CallDetails } from '@agoric/orchestration/src/examples/quickSend.flows.js'; +import { makePromiseKit } from '@endo/promise-kit'; import type { TestFn } from 'ava'; +import type { OfferId } from '@agoric/smart-wallet/src/offers.js'; +import { + AgoricCalc, + NobleCalc, +} from '@agoric/orchestration/src/utils/address.js'; +import { buildVTransferEvent } from '@agoric/orchestration/tools/ibc-mocks.js'; +import { BridgeId } from '@agoric/internal'; +import { computronCounter } from '../../tools/computron-counter.js'; +import { type WalletFactoryDriver } from '../../tools/drivers.js'; import { makeWalletFactoryContext, type WalletFactoryTestContext, } from './walletFactory.js'; -import { WalletFactoryDriver } from '../../tools/drivers.js'; -import { computronCounter } from '../../tools/computron-counter.js'; const makeMeter = () => { const beansPer = 100n; @@ -26,7 +35,8 @@ const makeMeter = () => { return policy; }, setMetering: x => (metering = x), - getValue: () => policy.remainingBeans(), + getValue: () => policy?.remainingBeans(), + resetPolicy: () => (policy = undefined), }); return meter; }; @@ -40,7 +50,8 @@ test.before('bootstrap', async t => { const ctx = await makeWalletFactoryContext( t, '@agoric/vm-config/decentral-itest-orchestration-config.json', - { defaultManagerType: 'xsnap', meter }, + { defaultManagerType: 'xsnap', meter }, // for perf testing + // { defaultManagerType: 'local' }, // 3x faster, node debugger ); t.context = { ...ctx, meter }; @@ -70,31 +81,94 @@ type SmartWallet = Awaited< ReturnType >; -test.serial('accept watcher invitation', async t => { - const { agoricNamesRemotes, meter } = t.context; +const makeWatcher = (sw: SmartWallet, instance, runInbound) => { + let seq = 0; + const accepted: PromiseKit = makePromiseKit(); - const william = async (sw: SmartWallet) => { - await sw.executeOffer({ - id: 'accept', - invitationSpec: { - source: 'purse', - instance: agoricNamesRemotes.instance.quickSend, - description: 'initAccounts', - }, - proposal: {}, - }); - const update = sw.getLatestUpdateRecord(); - t.like(update, { - updated: 'offerStatus', - status: { id: 'accept', numWantsSatisfied: 1, result: 'UNPUBLISHED' }, - }); - }; + return harden({ + accept: async () => { + const id = `accept-${(seq += 1)}`; + accepted.resolve(id); + await sw.executeOffer({ + id, + invitationSpec: { + source: 'purse', + instance, + description: 'initAccounts', + }, + proposal: {}, + }); + return sw.getLatestUpdateRecord(); + }, + + report: async (callDetails: CallDetails) => { + await sw.sendOffer({ + id: `report-${(seq += 1)}`, + invitationSpec: { + source: 'continuing', + invitationMakerName: 'ReportCCTPCall', + previousOffer: await accepted.promise, + }, + proposal: {}, + offerArgs: callDetails, + }); - const wd = - await t.context.walletFactoryDriver.provideSmartWallet('agoric1watcher'); + // simulate ibc/MsgTransfer ack from remote chain, + // enabling `.transfer()` promise to resolve + await runInbound( + BridgeId.VTRANSFER, + buildVTransferEvent({ + sourceChannel: 'channel-1', // agoric->osmosis + sequence: '1', // NOTE: only works once + }), + ); - t.log('start metering'); - meter.setMetering(true); - await william(wd); - t.log('metered cost (beans)', meter.getValue()); + return sw.getLatestUpdateRecord(); + }, + }); +}; + +test.serial('watcher: accept, report', async t => { + const { agoricNamesRemotes, meter, walletFactoryDriver } = t.context; + const { runInbound } = t.context.bridgeUtils; + + const wd = await walletFactoryDriver.provideSmartWallet('agoric1watcher'); + const william = makeWatcher( + wd, + agoricNamesRemotes.instance.quickSend, + runInbound, + ); + + { + t.log('start metering'); + meter.setMetering(true); + const update = await william.accept(); + t.log('accept cost (beans)', meter.getValue()); + t.like(update, { status: { id: 'accept-1', numWantsSatisfied: 1 } }); + } + + { + meter.resetPolicy(); + const settlementBase = 'agoric1fakeLCAAddress'; // TODO: read from vstorage + + const encoding = 'bech32' as const; + const dest = { chainId: 'osmosis-1', encoding, value: 'osmo1333' }; + const vAddr = AgoricCalc.virtualAddressFor(settlementBase, dest.value); + const nobleFwd = NobleCalc.fwdAddressFor(vAddr); + const status = await william.report({ amount: 1234n, dest, nobleFwd }); + t.log('advance cost (beans)', meter.getValue()); + + t.like(status, { + status: { + id: 'report-2', + invitationSpec: { previousOffer: 'accept-1' }, + offerArgs: { + amount: 1234n, + dest: { value: 'osmo1333' }, + nobleFwd: 'noble1301333', + }, + result: 'advance 1104 uusdc sent to osmo1333', + }, + }); + } }); diff --git a/packages/orchestration/src/examples/quickSend.flows.js b/packages/orchestration/src/examples/quickSend.flows.js index 38a3e82f08d..a3986117bf4 100644 --- a/packages/orchestration/src/examples/quickSend.flows.js +++ b/packages/orchestration/src/examples/quickSend.flows.js @@ -135,7 +135,9 @@ export const handleCCTPCall = async (_orch, ctx, accts, offerArgs) => { assert.equal(nfAddr, nobleFwd, `for ${vAddr}`); const withBrand = make(USDC, amount); const advance = subtract(withBrand, add(makerFee, contractFee)); + log('transfer advance', { dest, advance }); await fundingPool.transfer(dest, advance); + return `advance ${advance.value} uusdc sent to ${dest.value}`; }; harden(handleCCTPCall); From c55f27d7106de497e4ac95c4d78de9d004c8999f Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Mon, 21 Oct 2024 22:10:47 -0500 Subject: [PATCH 22/48] test: fix computron handling; report Mc --- packages/boot/test/bootstrapTests/quickSend.test.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/boot/test/bootstrapTests/quickSend.test.ts b/packages/boot/test/bootstrapTests/quickSend.test.ts index d552bbdae59..f20db5e86cc 100644 --- a/packages/boot/test/bootstrapTests/quickSend.test.ts +++ b/packages/boot/test/bootstrapTests/quickSend.test.ts @@ -30,12 +30,14 @@ const makeMeter = () => { let policy; const meter = harden({ - makeRunPolicy: () => { - policy = metering ? computronCounter(mainParams) : undefined; + provideRunPolicy: () => { + if (metering && !policy) { + policy = computronCounter(mainParams); + } return policy; }, setMetering: x => (metering = x), - getValue: () => policy?.remainingBeans(), + getValue: () => (policy?.totalBeans() || 0) / mainParams.xsnapComputron, resetPolicy: () => (policy = undefined), }); return meter; @@ -131,6 +133,7 @@ const makeWatcher = (sw: SmartWallet, instance, runInbound) => { test.serial('watcher: accept, report', async t => { const { agoricNamesRemotes, meter, walletFactoryDriver } = t.context; const { runInbound } = t.context.bridgeUtils; + const mc = qty => Number(qty) / 1_000_000; const wd = await walletFactoryDriver.provideSmartWallet('agoric1watcher'); const william = makeWatcher( @@ -143,7 +146,7 @@ test.serial('watcher: accept, report', async t => { t.log('start metering'); meter.setMetering(true); const update = await william.accept(); - t.log('accept cost (beans)', meter.getValue()); + t.log('accept cost (Mc)', mc(meter.getValue())); t.like(update, { status: { id: 'accept-1', numWantsSatisfied: 1 } }); } @@ -156,7 +159,7 @@ test.serial('watcher: accept, report', async t => { const vAddr = AgoricCalc.virtualAddressFor(settlementBase, dest.value); const nobleFwd = NobleCalc.fwdAddressFor(vAddr); const status = await william.report({ amount: 1234n, dest, nobleFwd }); - t.log('advance cost (beans)', meter.getValue()); + t.log('advance cost (Mc)', mc(meter.getValue())); t.like(status, { status: { From 60832db5b5681978492a1e925ce8700de69d09a9 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Tue, 22 Oct 2024 09:44:01 -0500 Subject: [PATCH 23/48] chore: support $SLOGFILE in quickSend perf test --- packages/boot/test/bootstrapTests/quickSend.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/boot/test/bootstrapTests/quickSend.test.ts b/packages/boot/test/bootstrapTests/quickSend.test.ts index f20db5e86cc..893319322eb 100644 --- a/packages/boot/test/bootstrapTests/quickSend.test.ts +++ b/packages/boot/test/bootstrapTests/quickSend.test.ts @@ -49,10 +49,11 @@ const test: TestFn< test.before('bootstrap', async t => { const meter = makeMeter(); + const { SLOGFILE: slogFile } = process.env; const ctx = await makeWalletFactoryContext( t, '@agoric/vm-config/decentral-itest-orchestration-config.json', - { defaultManagerType: 'xsnap', meter }, // for perf testing + { defaultManagerType: 'xsnap', meter, slogFile }, // for perf testing // { defaultManagerType: 'local' }, // 3x faster, node debugger ); t.context = { ...ctx, meter }; From 23b2c414e50e366e385fc49367171c9ab3b8cc92 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Tue, 22 Oct 2024 09:44:43 -0500 Subject: [PATCH 24/48] chore: mock registerAsset in quickSend-tx.test --- packages/orchestration/test/examples/quickSend-tx.test.ts | 8 ++++++-- packages/orchestration/tools/agoric-mock.js | 7 ++++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/orchestration/test/examples/quickSend-tx.test.ts b/packages/orchestration/test/examples/quickSend-tx.test.ts index 6af9306e737..e572e4de05d 100644 --- a/packages/orchestration/test/examples/quickSend-tx.test.ts +++ b/packages/orchestration/test/examples/quickSend-tx.test.ts @@ -227,13 +227,14 @@ const setup = async (t, io) => { noble: withForwarding(chains.noble, chains, t), }); - const orch = makeOrchestration(t, chains); + const USDCe = makeIssuerKit('USDC'); + const assetInfo = [{ brand: USDCe.brand, denom: 'ibc/toyusd' }]; + const orch = makeOrchestration(t, chains, assetInfo); const { storageNode: chainStorage, rpc: agoricRpc } = makeVStorage(); const storageNode = await E(chainStorage).makeChildNode( 'published.quickSend.settlementBase', ); - const USDCe = makeIssuerKit('USDC'); const terms = { issuers: { USDC: USDCe.issuer }, brands: { USDC: USDCe.brand }, @@ -262,6 +263,9 @@ const setup = async (t, io) => { orchestrate, orchestrateAll: (flows, ctx) => objectMap(flows, (h, n) => orchestrate(n, ctx, h)), + chainHub: { + registerAsset: (...args) => t.log('@@@register', args), + }, } as any; const contract = await contractFn(zcf, privateArgs, zone, tools); diff --git a/packages/orchestration/tools/agoric-mock.js b/packages/orchestration/tools/agoric-mock.js index 35ee44bd5f1..4498bf53b56 100644 --- a/packages/orchestration/tools/agoric-mock.js +++ b/packages/orchestration/tools/agoric-mock.js @@ -9,9 +9,10 @@ import { ibcTransfer } from './cosmoverse-mock.js'; /** * @param {any} t - * @param {Record} chains + * @param {{ brand: Brand; denom: string }[]} assetInfo */ -export const makeOrchestration = (t, chains) => { +export const makeOrchestration = (t, chains, assetInfo) => { const { nextLabel: next } = t.context; const encoding = 'bech32'; /** @returns {Promise>} */ @@ -47,7 +48,7 @@ export const makeOrchestration = (t, chains) => { }); }; const chainHub = harden({ - agoric: { makeAccount }, + agoric: { makeAccount, getVBankAssetInfo: async () => assetInfo }, }); return harden({ getChain: name => chainHub[name], From da6949b0f3404d02902cb918367abbc51d20581d Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Mon, 21 Oct 2024 14:26:36 -0700 Subject: [PATCH 25/48] refactor: fast-usdc files to own package --- .../contract}/quickSend.contract.js | 5 ++- .../contract}/quickSend.flows.js | 11 ++++--- .../contract}/start-quickSend.js | 2 +- .../test}/quickSend-tx.test.ts | 33 +++++++++---------- .../tools/agoric-mock.js | 4 +-- .../tools/cosmoverse-mock.js | 0 .../tools/eth-mock.ts | 0 .../tools/noble-mock.js | 4 +-- packages/orchestration/src/utils/address.js | 2 +- 9 files changed, 31 insertions(+), 30 deletions(-) rename packages/{orchestration/src/examples => fast-usdc/contract}/quickSend.contract.js (94%) rename packages/{orchestration/src/examples => fast-usdc/contract}/quickSend.flows.js (95%) rename packages/{orchestration/src/proposals => fast-usdc/contract}/start-quickSend.js (98%) rename packages/{orchestration/test/examples => fast-usdc/test}/quickSend-tx.test.ts (92%) rename packages/{orchestration => fast-usdc}/tools/agoric-mock.js (95%) rename packages/{orchestration => fast-usdc}/tools/cosmoverse-mock.js (100%) rename packages/{orchestration => fast-usdc}/tools/eth-mock.ts (100%) rename packages/{orchestration => fast-usdc}/tools/noble-mock.js (91%) diff --git a/packages/orchestration/src/examples/quickSend.contract.js b/packages/fast-usdc/contract/quickSend.contract.js similarity index 94% rename from packages/orchestration/src/examples/quickSend.contract.js rename to packages/fast-usdc/contract/quickSend.contract.js index c84ae445721..15e52d3ad06 100644 --- a/packages/orchestration/src/examples/quickSend.contract.js +++ b/packages/fast-usdc/contract/quickSend.contract.js @@ -1,13 +1,13 @@ import { BrandShape } from '@agoric/ertp/src/typeGuards.js'; import { M } from '@endo/patterns'; -import { withOrchestration } from '../utils/start-helper.js'; +import { withOrchestration } from '@agoric/orchestration'; import * as flows from './quickSend.flows.js'; /** * @import {ExecutionContext} from 'ava'; // XXX * * @import {CopyRecord} from '@endo/pass-style'; - * @import {OrchestrationPowers, OrchestrationTools} from '../utils/start-helper.js'; + * @import {OrchestrationPowers, OrchestrationTools} from '@agoric/orchestration/src/utils/start-helper.js'; * @import {Zone} from '@agoric/zone'; * @import {VTransferIBCEvent} from '@agoric/vats'; * @import {QuickSendAccounts} from './quickSend.flows.js'; @@ -34,7 +34,6 @@ harden(meta); * }} tools */ export const contract = async (zcf, privateArgs, zone, tools) => { - const { storageNode } = privateArgs; const { t, chainHub } = tools; const terms = zcf.getTerms(); diff --git a/packages/orchestration/src/examples/quickSend.flows.js b/packages/fast-usdc/contract/quickSend.flows.js similarity index 95% rename from packages/orchestration/src/examples/quickSend.flows.js rename to packages/fast-usdc/contract/quickSend.flows.js index a3986117bf4..43ded25eb17 100644 --- a/packages/orchestration/src/examples/quickSend.flows.js +++ b/packages/fast-usdc/contract/quickSend.flows.js @@ -2,8 +2,11 @@ import { AmountMath } from '@agoric/ertp/src/amountMath.js'; import { mustMatch } from '@agoric/internal'; import { atob } from '@endo/base64'; import { M } from '@endo/patterns'; -import { ChainAddressShape } from '../typeGuards.js'; -import { AgoricCalc, NobleCalc } from '../utils/address.js'; +import { ChainAddressShape } from '@agoric/orchestration'; +import { + AgoricCalc, + NobleCalc, +} from '@agoric/orchestration/src/utils/address.js'; /** * @import {ExecutionContext} from 'ava'; @@ -13,12 +16,12 @@ import { AgoricCalc, NobleCalc } from '../utils/address.js'; * @import {GuestInterface} from '@agoric/async-flow'; * @import {ChainAddress, OrchestrationAccountI, OrchestrationFlow, Orchestrator, ZcfTools} from '@agoric/orchestration'; * @import {VTransferIBCEvent} from '@agoric/vats'; - * @import {ResolvedContinuingOfferResult} from '../../src/utils/zoe-tools.js'; + * @import {ResolvedContinuingOfferResult} from '@agoric/orchestration/src/utils/zoe-tools.js'; * @import {InvitationMakers} from '@agoric/smart-wallet/src/types.js'; * @import {QuickSendTerms} from './quickSend.contract.js'; * @import {FungibleTokenPacketData} from '@agoric/cosmic-proto/ibc/applications/transfer/v2/packet.js'; * @import {TypedPattern} from '@agoric/internal'; - * @import {ChainHub} from '../types.js'; + * @import {ChainHub} from '@agoric/orchestration'; */ const AddressShape = M.string(); // XXX diff --git a/packages/orchestration/src/proposals/start-quickSend.js b/packages/fast-usdc/contract/start-quickSend.js similarity index 98% rename from packages/orchestration/src/proposals/start-quickSend.js rename to packages/fast-usdc/contract/start-quickSend.js index 00c5d61432d..558728008fe 100644 --- a/packages/orchestration/src/proposals/start-quickSend.js +++ b/packages/fast-usdc/contract/start-quickSend.js @@ -8,7 +8,7 @@ const { Fail } = assert; /** * @import {Instance} from '@agoric/zoe/src/zoeService/utils'; * @import {Board} from '@agoric/vats'; - * @import {QuickSendContractFn} from '../examples/quickSend.contract.js'; + * @import {QuickSendContractFn} from './quickSend.contract.js'; * @import {ManifestBundleRef} from '@agoric/deploy-script-support/src/externalTypes.js'; * @import {BootstrapManifest} from '@agoric/vats/src/core/lib-boot.js'; */ diff --git a/packages/orchestration/test/examples/quickSend-tx.test.ts b/packages/fast-usdc/test/quickSend-tx.test.ts similarity index 92% rename from packages/orchestration/test/examples/quickSend-tx.test.ts rename to packages/fast-usdc/test/quickSend-tx.test.ts index e572e4de05d..e663c380502 100644 --- a/packages/orchestration/test/examples/quickSend-tx.test.ts +++ b/packages/fast-usdc/test/quickSend-tx.test.ts @@ -2,39 +2,38 @@ import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js'; import { AmountMath, makeIssuerKit } from '@agoric/ertp'; import { deeplyFulfilledObject } from '@agoric/internal'; +import type { ChainAddress } from '@agoric/orchestration'; +import { + AgoricCalc, + NobleCalc, +} from '@agoric/orchestration/src/utils/address.js'; +import type { ResolvedContinuingOfferResult } from '@agoric/orchestration/src/utils/zoe-tools.js'; import { makeHeapZone } from '@agoric/zone'; import { E } from '@endo/far'; -import { Nat } from '@endo/nat'; import { objectMap } from '@endo/patterns'; import { createRequire } from 'node:module'; -import { NatAmount } from '@agoric/ertp/src/types.js'; -import type { QuickSendTerms } from '../../src/examples/quickSend.contract.js'; -import { contract as contractFn } from '../../src/examples/quickSend.contract.js'; -import { CallDetails } from '../../src/examples/quickSend.flows.js'; -import { AgoricCalc, NobleCalc } from '../../src/utils/address.js'; -import type { ResolvedContinuingOfferResult } from '../../src/utils/zoe-tools.js'; +import type { QuickSendTerms } from '../contract/quickSend.contract.js'; +import { contract as contractFn } from '../contract/quickSend.contract.js'; +import type { CallDetails } from '../contract/quickSend.flows.js'; import { makeOrchestration, makeVStorage, withVTransfer, -} from '../../tools/agoric-mock.js'; -import { makeCosmosChain, pickChain } from '../../tools/cosmoverse-mock.js'; -import type { EthAddr, EthChain } from '../../tools/eth-mock.js'; +} from '../tools/agoric-mock.js'; +import { makeCosmosChain, pickChain } from '../tools/cosmoverse-mock.js'; +import type { EthAddr, EthChain } from '../tools/eth-mock.js'; import { makeERC20, makeEthChain, makeEventCounter, -} from '../../tools/eth-mock.js'; -import { makeCCTP, withForwarding } from '../../tools/noble-mock.js'; -import type { ChainAddress } from '../../src/types.js'; +} from '../tools/eth-mock.js'; +import { makeCCTP, withForwarding } from '../tools/noble-mock.js'; type NatValue = bigint; const nodeRequire = createRequire(import.meta.url); const contractName = 'quickSend'; -const contractFile = nodeRequire.resolve( - `../../src/examples/quickSend.contract.js`, -); +const contractFile = nodeRequire.resolve(`../contract/quickSend.contract.js`); const todo = () => assert.fail('TODO'); @@ -354,7 +353,7 @@ const setup = async (t, io) => { return { chains, ursula, quiesce, contract, addrs, usdc }; }; -test('tx lifecycle', async t => { +test.failing('tx lifecycle', async t => { const io = { setTimeout }; const { chains, ursula, quiesce, contract, addrs, usdc } = await setup(t, io); diff --git a/packages/orchestration/tools/agoric-mock.js b/packages/fast-usdc/tools/agoric-mock.js similarity index 95% rename from packages/orchestration/tools/agoric-mock.js rename to packages/fast-usdc/tools/agoric-mock.js index 4498bf53b56..a1dc7d32f52 100644 --- a/packages/orchestration/tools/agoric-mock.js +++ b/packages/fast-usdc/tools/agoric-mock.js @@ -1,10 +1,10 @@ import { btoa } from '@endo/base64'; -import { AgoricCalc } from '../src/utils/address.js'; +import { AgoricCalc } from '@agoric/orchestration/src/utils/address.js'; import { ibcTransfer } from './cosmoverse-mock.js'; /** * @import {Remote} from '@agoric/vow'; - * @import {OrchestrationAccount} from '../src/orchestration-api.js'; + * @import {OrchestrationAccount} from '@agoric/orchestration'; */ /** diff --git a/packages/orchestration/tools/cosmoverse-mock.js b/packages/fast-usdc/tools/cosmoverse-mock.js similarity index 100% rename from packages/orchestration/tools/cosmoverse-mock.js rename to packages/fast-usdc/tools/cosmoverse-mock.js diff --git a/packages/orchestration/tools/eth-mock.ts b/packages/fast-usdc/tools/eth-mock.ts similarity index 100% rename from packages/orchestration/tools/eth-mock.ts rename to packages/fast-usdc/tools/eth-mock.ts diff --git a/packages/orchestration/tools/noble-mock.js b/packages/fast-usdc/tools/noble-mock.js similarity index 91% rename from packages/orchestration/tools/noble-mock.js rename to packages/fast-usdc/tools/noble-mock.js index 639db68814b..24858c37692 100644 --- a/packages/orchestration/tools/noble-mock.js +++ b/packages/fast-usdc/tools/noble-mock.js @@ -1,4 +1,4 @@ -import { NobleCalc } from '../src/utils/address.js'; +import { NobleCalc } from '@agoric/orchestration/src/utils/address.js'; import { ibcTransfer } from './cosmoverse-mock.js'; export const withForwarding = (chain, chains, t) => { @@ -13,7 +13,7 @@ export const withForwarding = (chain, chains, t) => { return address; }, send: async ({ amount, from, dest, quiet = false }) => { - await chain.send({ amount, from, dest, quiet: true }); + await chain.send({ amount, from, dest, quiet }); if (!destOf.has(dest)) return; const fwd = destOf.get(dest); t.log(next(), 'fwd', { amount, dest, fwd }); diff --git a/packages/orchestration/src/utils/address.js b/packages/orchestration/src/utils/address.js index 591634e44df..e5539df5a35 100644 --- a/packages/orchestration/src/utils/address.js +++ b/packages/orchestration/src/utils/address.js @@ -2,7 +2,7 @@ import { Fail } from '@endo/errors'; /** * @import {IBCConnectionID} from '@agoric/vats'; - * @import {ChainAddress} from '../types.js'; + * @import {ChainAddress} from '@agoric/orchestration'; * @import {RemoteIbcAddress} from '@agoric/vats/tools/ibc-utils.js'; */ From 38ac63fc24b5e02ad2d913e275e3c390ed533846 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Wed, 23 Oct 2024 17:08:20 -0500 Subject: [PATCH 26/48] test(fast-usdc): tx lifecycle no longer failing --- packages/fast-usdc/test/quickSend-tx.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/fast-usdc/test/quickSend-tx.test.ts b/packages/fast-usdc/test/quickSend-tx.test.ts index e663c380502..56426cd06c1 100644 --- a/packages/fast-usdc/test/quickSend-tx.test.ts +++ b/packages/fast-usdc/test/quickSend-tx.test.ts @@ -353,7 +353,7 @@ const setup = async (t, io) => { return { chains, ursula, quiesce, contract, addrs, usdc }; }; -test.failing('tx lifecycle', async t => { +test('tx lifecycle', async t => { const io = { setTimeout }; const { chains, ursula, quiesce, contract, addrs, usdc } = await setup(t, io); From 1375ac141a8af644e58a8a23a7f17c5d6b5019dc Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Wed, 23 Oct 2024 17:50:52 -0500 Subject: [PATCH 27/48] test(fast-usdc): restore quickSend boot test - support $SWINGSET_WORKER_TYPE --- .../boot/test/bootstrapTests/quickSend.test.ts | 14 ++++++++------ packages/builders/package.json | 1 + .../{orchestration => fast-usdc}/init-quickSend.js | 8 ++++---- 3 files changed, 13 insertions(+), 10 deletions(-) rename packages/builders/scripts/{orchestration => fast-usdc}/init-quickSend.js (82%) diff --git a/packages/boot/test/bootstrapTests/quickSend.test.ts b/packages/boot/test/bootstrapTests/quickSend.test.ts index 893319322eb..3134ae7415e 100644 --- a/packages/boot/test/bootstrapTests/quickSend.test.ts +++ b/packages/boot/test/bootstrapTests/quickSend.test.ts @@ -1,6 +1,6 @@ import { test as anyTest } from '@agoric/zoe/tools/prepare-test-env-ava.js'; -import type { CallDetails } from '@agoric/orchestration/src/examples/quickSend.flows.js'; +import type { CallDetails } from 'fast-usdc/contract/quickSend.flows.js'; import { makePromiseKit } from '@endo/promise-kit'; import type { TestFn } from 'ava'; import type { OfferId } from '@agoric/smart-wallet/src/offers.js'; @@ -37,7 +37,7 @@ const makeMeter = () => { return policy; }, setMetering: x => (metering = x), - getValue: () => (policy?.totalBeans() || 0) / mainParams.xsnapComputron, + getValue: () => (policy?.totalBeans() || 0n) / mainParams.xsnapComputron, resetPolicy: () => (policy = undefined), }); return meter; @@ -49,12 +49,14 @@ const test: TestFn< test.before('bootstrap', async t => { const meter = makeMeter(); - const { SLOGFILE: slogFile } = process.env; + const { + SLOGFILE: slogFile, + SWINGSET_WORKER_TYPE: defaultManagerType = 'xsnap', + } = process.env; const ctx = await makeWalletFactoryContext( t, '@agoric/vm-config/decentral-itest-orchestration-config.json', - { defaultManagerType: 'xsnap', meter, slogFile }, // for perf testing - // { defaultManagerType: 'local' }, // 3x faster, node debugger + { defaultManagerType, meter, slogFile }, // for perf testing ); t.context = { ...ctx, meter }; @@ -73,7 +75,7 @@ test.serial('deploy contract', async t => { refreshAgoricNamesRemotes, } = t.context; await evalProposal( - buildProposal('@agoric/builders/scripts/orchestration/init-quickSend.js'), + buildProposal('@agoric/builders/scripts/fast-usdc/init-quickSend.js'), ); // update now that quickSend is instantiated refreshAgoricNamesRemotes(); diff --git a/packages/builders/package.json b/packages/builders/package.json index 8e3393b05f3..24c50959d94 100644 --- a/packages/builders/package.json +++ b/packages/builders/package.json @@ -39,6 +39,7 @@ "@endo/patterns": "^1.4.6", "@endo/promise-kit": "^1.1.7", "@endo/stream": "^1.2.7", + "fast-usdc": "^0.1.0", "import-meta-resolve": "^2.2.1" }, "devDependencies": { diff --git a/packages/builders/scripts/orchestration/init-quickSend.js b/packages/builders/scripts/fast-usdc/init-quickSend.js similarity index 82% rename from packages/builders/scripts/orchestration/init-quickSend.js rename to packages/builders/scripts/fast-usdc/init-quickSend.js index da2a11da1af..6cfabfe4f0c 100644 --- a/packages/builders/scripts/orchestration/init-quickSend.js +++ b/packages/builders/scripts/fast-usdc/init-quickSend.js @@ -1,13 +1,13 @@ // @ts-check import { makeHelpers } from '@agoric/deploy-script-support'; import { mustMatch } from '@agoric/internal'; -import { getManifestForQuickSend } from '@agoric/orchestration/src/proposals/start-quickSend.js'; +import { getManifestForQuickSend } from 'fast-usdc/contract/start-quickSend.js'; import { M } from '@endo/patterns'; import { parseArgs } from 'node:util'; /** * @import {CoreEvalBuilder} from '@agoric/deploy-script-support/src/externalTypes.js'; - * @import {QuickSendConfig} from '@agoric/orchestration/src/proposals/start-quickSend.js'; + * @import {QuickSendConfig} from 'fast-usdc/contract/start-quickSend.js'; * @import {TypedPattern} from '@agoric/internal'; * @import {ParseArgsConfig} from 'node:util'; */ @@ -25,7 +25,7 @@ const QuickSendConfigShape = M.splitRecord({ watcherAddress: M.string() }); export const defaultProposalBuilder = async ({ publishRef, install }, opts) => { opts && mustMatch(opts, QuickSendConfigShape); return harden({ - sourceSpec: '@agoric/orchestration/src/proposals/start-quickSend.js', + sourceSpec: 'fast-usdc/contract/start-quickSend.js', /** @type {[string, Parameters[1]]} */ getManifestCall: [ getManifestForQuickSend.name, @@ -33,7 +33,7 @@ export const defaultProposalBuilder = async ({ publishRef, install }, opts) => { options: { quickSend: opts }, installKeys: { quickSend: publishRef( - install('@agoric/orchestration/src/examples/quickSend.contract.js'), + install('fast-usdc/contract/quickSend.contract.js'), ), }, }, From 85bae1dfda0b2e6d7af86a84ff7206ae984e705b Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Thu, 24 Oct 2024 13:12:09 -0500 Subject: [PATCH 28/48] chore(deps): avoid cycle by pruning agoric --- packages/fast-usdc/package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/fast-usdc/package.json b/packages/fast-usdc/package.json index b54b1a6bbe7..bbfaf39f7a1 100644 --- a/packages/fast-usdc/package.json +++ b/packages/fast-usdc/package.json @@ -27,7 +27,6 @@ "ts-blank-space": "^0.4.1" }, "dependencies": { - "agoric": "^0.21.1", "commander": "^12.1.0" }, "ava": { From 652c459d6175f50bb3d4e138eeac174e98754e6e Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Thu, 24 Oct 2024 16:46:28 -0500 Subject: [PATCH 29/48] chore(fast-usdc): turn off compilerOptions.strict ... and include contract, tools --- packages/fast-usdc/tsconfig.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/fast-usdc/tsconfig.json b/packages/fast-usdc/tsconfig.json index 2bb806097ef..5252d0cf78d 100644 --- a/packages/fast-usdc/tsconfig.json +++ b/packages/fast-usdc/tsconfig.json @@ -1,10 +1,10 @@ { "extends": "../../tsconfig.json", - "compilerOptions": { - "strict": true, - }, + "compilerOptions": {}, "include": [ + "contract", "src", "test", + "tools" ], } From bfe06a006414529fca6ea3ec8a1e4899d0064289 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Thu, 24 Oct 2024 16:47:12 -0500 Subject: [PATCH 30/48] chore(deps): fast-usdc devDependencies --- packages/fast-usdc/package.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/fast-usdc/package.json b/packages/fast-usdc/package.json index bbfaf39f7a1..4e6d25d8c92 100644 --- a/packages/fast-usdc/package.json +++ b/packages/fast-usdc/package.json @@ -22,6 +22,8 @@ "lint:eslint": "eslint ." }, "devDependencies": { + "@agoric/zoe": "^0.26.2", + "@agoric/zone": "^0.2.2", "ava": "^5.3.0", "c8": "^9.1.0", "ts-blank-space": "^0.4.1" From 7c7d238b7ce9cdf57bb07855d84c1da337474a55 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Thu, 24 Oct 2024 16:47:26 -0500 Subject: [PATCH 31/48] chore(deps): fast-usdc contract dependencies --- packages/fast-usdc/package.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/fast-usdc/package.json b/packages/fast-usdc/package.json index 4e6d25d8c92..b97ecfbcaa4 100644 --- a/packages/fast-usdc/package.json +++ b/packages/fast-usdc/package.json @@ -29,6 +29,12 @@ "ts-blank-space": "^0.4.1" }, "dependencies": { + "@agoric/ertp": "^0.16.2", + "@endo/patterns": "^1.4.6", + "@agoric/orchestration": "^0.1.0", + "@agoric/internal": "^0.3.2", + "@endo/base64": "^1.0.8", + "@endo/far": "^1.1.8", "commander": "^12.1.0" }, "ava": { From f10b43b4fb05f6d434ab53aa0b5d599e2a909729 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Thu, 24 Oct 2024 16:48:27 -0500 Subject: [PATCH 32/48] docs(fast-usdc): WIP: initAccounts @ts-expect-error --- packages/fast-usdc/contract/quickSend.contract.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/fast-usdc/contract/quickSend.contract.js b/packages/fast-usdc/contract/quickSend.contract.js index 15e52d3ad06..cadd2d1903c 100644 --- a/packages/fast-usdc/contract/quickSend.contract.js +++ b/packages/fast-usdc/contract/quickSend.contract.js @@ -93,6 +93,7 @@ export const contract = async (zcf, privateArgs, zone, tools) => { const initAccounts = tools.orchestrate( 'initAccounts', { terms, chainHub, makeSettleTap, makeWatcherCont, t }, + // @ts-expect-error HALP! monster error around ctx flows.initAccounts, ); From f8aebcfde3e34bbf82acbb8a212bb08dd0464708 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Thu, 24 Oct 2024 16:49:07 -0500 Subject: [PATCH 33/48] docs(fast-usdc): types for .flows --- packages/fast-usdc/contract/quickSend.flows.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/fast-usdc/contract/quickSend.flows.js b/packages/fast-usdc/contract/quickSend.flows.js index 43ded25eb17..1b683106246 100644 --- a/packages/fast-usdc/contract/quickSend.flows.js +++ b/packages/fast-usdc/contract/quickSend.flows.js @@ -93,6 +93,7 @@ export const initAccounts = async (orch, ctx, seat, _offerArgs) => { const feeAccount = await agoric.makeAccount(); const accts = harden({ fundingPool, settlement, feeAccount }); const tap = ctx.makeSettleTap(accts); + // @ts-expect-error tap.receiveUpcall: 'Vow | undefined' not assignable to 'Promise' const registration = await settlement.monitorTransfers(tap); log('@@@what to do with registration?', registration); From eedcfdf56ce7aa8b3f51e31f6b23b4146ffa46c6 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Thu, 24 Oct 2024 16:49:24 -0500 Subject: [PATCH 34/48] test(fast-usdc): types for agoric-mock --- packages/fast-usdc/tools/agoric-mock.js | 39 ++++++++++++++++++++----- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/packages/fast-usdc/tools/agoric-mock.js b/packages/fast-usdc/tools/agoric-mock.js index a1dc7d32f52..42eeb7e0d36 100644 --- a/packages/fast-usdc/tools/agoric-mock.js +++ b/packages/fast-usdc/tools/agoric-mock.js @@ -5,11 +5,18 @@ import { ibcTransfer } from './cosmoverse-mock.js'; /** * @import {Remote} from '@agoric/vow'; * @import {OrchestrationAccount} from '@agoric/orchestration'; + * @import {LocalAccountMethods} from '@agoric/orchestration'; + * @import {TargetRegistration} from '@agoric/vats/src/bridge-target.js'; + * @import {CosmosChain} from './cosmoverse-mock.js'; + * @import {ExecutionContext as AvaT} from 'ava'; */ /** - * @param {any} t - * @param {Record} chains + * @param {AvaT} t + * @param {{ + * agoric: ReturnType + * } & Record + * } chains * @param {{ brand: Brand; denom: string }[]} assetInfo */ export const makeOrchestration = (t, chains, assetInfo) => { @@ -18,8 +25,11 @@ export const makeOrchestration = (t, chains, assetInfo) => { /** @returns {Promise>} */ const makeAccount = async () => { const addr = await chains.agoric.makeAccount(); + // @ts-expect-error only some methods are mocked return harden({ + /** @type {OrchestrationAccount['getAddress']} */ getAddress: () => ({ value: addr, chainId: 'agoric3', encoding }), + /** @type {OrchestrationAccount['getPublicTopics']} */ getPublicTopics: async () => ({ account: { subscriber: { @@ -34,16 +44,19 @@ export const makeOrchestration = (t, chains, assetInfo) => { storagePath: 'XXX', }, }), + /** @type {OrchestrationAccount['transfer']} */ transfer: async (dest, { value: amount }) => { t.log(next(), 'orch acct', addr, 'txfr', amount, 'to', dest.value); await ibcTransfer(chains, { amount, dest: dest.value, from: addr, t }); }, + /** @type {OrchestrationAccount['send']} */ send: async (dest, { value: amount }) => { t.log(next(), 'orch acct', addr, 'send', amount, 'to', dest.value); await chains.agoric.send({ amount, dest: dest.value, from: addr }); }, - monitorTransfers: async handler => { - await chains.agoric.register({ addr, handler }); + /** @type {LocalAccountMethods['monitorTransfers']} */ + monitorTransfers: async tap => { + return chains.agoric.register({ addr, tap }); }, }); }; @@ -75,14 +88,26 @@ export const makeVStorage = () => { const mkEvent = data => ({ packet: { data: btoa(JSON.stringify(data)) } }); +/** + * @param {CosmosChain} chain + * @param {AvaT} t + */ export const withVTransfer = (chain, t) => { const addrToTap = new Map(); return harden({ ...chain, - register: async ({ addr, handler }) => { - t.log('vtransfer register', { addr, handler }); + /** + * @param {{addr: string, tap: {receiveUpcall: (obj: any) => Promise}}} param0 + * @returns {Promise} + */ + register: async ({ addr, tap }) => { + t.log('vtransfer register', { addr, tap }); !addrToTap.has(addr) || assert.fail('already registered'); - addrToTap.set(addr, handler); + addrToTap.set(addr, tap); + return harden({ + revoke: async () => {}, + updateTargetApp: async () => {}, + }); }, send: async ({ amount, from, dest }) => { const [agAddr, extra] = AgoricCalc.isVirtualAddress(dest) From 8beb9db22669b6e5d8c003e110c9de59270ad644 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Thu, 24 Oct 2024 16:56:59 -0500 Subject: [PATCH 35/48] refactor: simplify io in quick-load.test --- .../proposals/d:quick-send/quick-load.test.js | 20 ++++--------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/a3p-integration/proposals/d:quick-send/quick-load.test.js b/a3p-integration/proposals/d:quick-send/quick-load.test.js index 955f9226c16..7f03e411b17 100644 --- a/a3p-integration/proposals/d:quick-send/quick-load.test.js +++ b/a3p-integration/proposals/d:quick-send/quick-load.test.js @@ -1,29 +1,17 @@ // @ts-check /* global globalThis */ -import anyTest from 'ava'; +import test from 'ava'; import { extractStreamCellValue } from '@agoric/synthetic-chain'; import { localAPI, makeLCD } from './test-lib/cosmos-api.js'; import { makeVStorage } from './test-lib/vstorage-client.js'; -/** - * @import {TestFn} from 'ava'; - */ -/** @type {TestFn>>} */ -// @ts-expect-error XXX something weird about test.after -const test = anyTest; - -const makeTestContext = async t => { - const api = makeLCD(localAPI, { fetch: globalThis.fetch }); - return { api }; -}; - -test.before('IO setup', async t => (t.context = await makeTestContext(t))); +const io = { api: makeLCD(localAPI, { fetch: globalThis.fetch }) }; test('quickSend is in agoricNames.instance', async t => { - const { api } = t.context; - const vs = makeVStorage(api); + const vs = makeVStorage(io.api); const data = await vs.readStorage('published.agoricNames.instance'); const value = extractStreamCellValue(data); + assert.typeof(value, 'string'); const capData = JSON.parse(value); const encoding = JSON.parse(capData?.body.replace(/^#/, '')); const byName = Object.fromEntries(encoding); From 93264960ee520072caa90abdf89a2ae48c57e920 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Fri, 25 Oct 2024 00:04:37 -0500 Subject: [PATCH 36/48] v0.0.0 --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 981b20e9eb3..902ff22e9ae 100644 --- a/package.json +++ b/package.json @@ -79,5 +79,6 @@ }, "dependencies": { "patch-package": "^8.0.0" - } + }, + "version": "0.0.0" } From f000b5b84f585eed76afb6047a32a9f9dbd0b72f Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Fri, 25 Oct 2024 18:00:41 -0500 Subject: [PATCH 37/48] chore(fast-usdc): use CCTPTxEvidence in flows --- packages/fast-usdc/contract/client-support.js | 24 ++++++++ packages/fast-usdc/contract/client-types.ts | 20 +++++++ .../fast-usdc/contract/quickSend.flows.js | 55 ++++++++++--------- 3 files changed, 73 insertions(+), 26 deletions(-) create mode 100644 packages/fast-usdc/contract/client-support.js create mode 100644 packages/fast-usdc/contract/client-types.ts diff --git a/packages/fast-usdc/contract/client-support.js b/packages/fast-usdc/contract/client-support.js new file mode 100644 index 00000000000..97401e35c92 --- /dev/null +++ b/packages/fast-usdc/contract/client-support.js @@ -0,0 +1,24 @@ +import { M } from '@endo/patterns'; +import { IBCChannelIDShape } from '@agoric/orchestration'; + +/** + * @import {TypedPattern} from '@agoric/internal'; + * @import {CCTPTxEvidence} from './client-types.js'; + */ + +/** @type {TypedPattern} */ +export const CCTPTxEvidenceShape = harden({ + txHash: M.string(), + tx: { + amount: M.nat(), + forwardingAddress: M.string(), + }, + blockHash: M.string(), + blockNumber: M.nat(), + blockTimestamp: M.nat(), + /** from Noble RPC */ + aux: { + forwardingChannel: IBCChannelIDShape, + recipientAddress: M.string(), + }, +}); diff --git a/packages/fast-usdc/contract/client-types.ts b/packages/fast-usdc/contract/client-types.ts new file mode 100644 index 00000000000..a624ba52a74 --- /dev/null +++ b/packages/fast-usdc/contract/client-types.ts @@ -0,0 +1,20 @@ +export type Hex = `0x${string}`; // as in viem +export type NobleAddress = `noble1${string}`; +export type IBCChannelID = `channel-${number}`; + +export type CCTPTxEvidence = { + txHash: Hex; + /** data covered by signature (aka txHash) */ + tx: { + amount: bigint; + forwardingAddress: NobleAddress; + }; + blockHash: Hex; + blockNumber: bigint; + blockTimestamp: bigint; // seconds since the epoch + /** from Noble RPC */ + aux: { + forwardingChannel: IBCChannelID; + recipientAddress: string; + }; +}; diff --git a/packages/fast-usdc/contract/quickSend.flows.js b/packages/fast-usdc/contract/quickSend.flows.js index 1b683106246..ddeb20be539 100644 --- a/packages/fast-usdc/contract/quickSend.flows.js +++ b/packages/fast-usdc/contract/quickSend.flows.js @@ -1,12 +1,11 @@ import { AmountMath } from '@agoric/ertp/src/amountMath.js'; import { mustMatch } from '@agoric/internal'; import { atob } from '@endo/base64'; -import { M } from '@endo/patterns'; -import { ChainAddressShape } from '@agoric/orchestration'; import { AgoricCalc, NobleCalc, } from '@agoric/orchestration/src/utils/address.js'; +import { CCTPTxEvidenceShape } from './client-support.js'; /** * @import {ExecutionContext} from 'ava'; @@ -14,33 +13,14 @@ import { * @import {Passable} from '@endo/pass-style'; * @import {Guarded} from '@endo/exo'; * @import {GuestInterface} from '@agoric/async-flow'; - * @import {ChainAddress, OrchestrationAccountI, OrchestrationFlow, Orchestrator, ZcfTools} from '@agoric/orchestration'; + * @import {ChainAddress, ChainHub, OrchestrationAccountI, OrchestrationFlow, Orchestrator} from '@agoric/orchestration'; * @import {VTransferIBCEvent} from '@agoric/vats'; * @import {ResolvedContinuingOfferResult} from '@agoric/orchestration/src/utils/zoe-tools.js'; * @import {InvitationMakers} from '@agoric/smart-wallet/src/types.js'; * @import {QuickSendTerms} from './quickSend.contract.js'; * @import {FungibleTokenPacketData} from '@agoric/cosmic-proto/ibc/applications/transfer/v2/packet.js'; - * @import {TypedPattern} from '@agoric/internal'; - * @import {ChainHub} from '@agoric/orchestration'; */ -const AddressShape = M.string(); // XXX - -/** - * @typedef {{ - * amount: NatValue; - * dest: ChainAddress; - * nobleFwd: string; - * }} CallDetails - */ - -/** @type {TypedPattern} */ -const CallDetailsShape = harden({ - amount: M.nat(), - dest: ChainAddressShape, - nobleFwd: AddressShape, -}); - const { add, make, subtract } = AmountMath; /** @@ -114,6 +94,25 @@ export const initAccounts = async (orch, ctx, seat, _offerArgs) => { }; harden(initAccounts); +/** + * TODO: move to a method on ChainHub + * + * @param {string} value + * @returns {ChainAddress} + */ +const asChainAddress = value => { + // TODO: from ChainInfo, from chain-registry + const toId = { + dydx: 'dydx-mainnet-1', + osmo: 'osmosis-1', + }; + for (const [pfx, chainId] of Object.entries(toId)) { + if (value.startsWith(pfx)) + return harden({ encoding: 'bech32', value, chainId }); + } + assert.fail(`unsupported prefix: ${value}`); +}; + /** * @param {Orchestrator} _orch * @param {{ @@ -121,14 +120,18 @@ harden(initAccounts); * t?: ExecutionContext<{ nextLabel: Function }>; // XXX * }} ctx * @param {QuickSendAccounts & Passable} accts - * @param {unknown} offerArgs + * @param {import('./client-types.js').CCTPTxEvidence} offerArgs */ export const handleCCTPCall = async (_orch, ctx, accts, offerArgs) => { const { nextLabel: next = () => '#?' } = ctx.t?.context || {}; const { log = console.log } = ctx.t || {}; - mustMatch(offerArgs, CallDetailsShape); - const { amount, dest, nobleFwd } = offerArgs; - log(next(), 'flows.reportCCTPCall', { amount, dest }); + log(next(), 'flows.reportCCTPCall', offerArgs); + mustMatch(offerArgs, CCTPTxEvidenceShape); + const { + tx: { amount, forwardingAddress: nobleFwd }, + aux: { recipientAddress }, + } = offerArgs; + const dest = asChainAddress(recipientAddress); const { makerFee, contractFee } = ctx.terms; const { USDC } = ctx.terms.brands; const { fundingPool } = accts; From 1e15177153402602e9c7f6279e0896f91935eddc Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Fri, 25 Oct 2024 18:01:24 -0500 Subject: [PATCH 38/48] test(fast-usdc): update mocks to use CCTPTxEvidence in logs --- packages/fast-usdc/test/quickSend-tx.test.ts | 42 +++++++++++++++----- packages/fast-usdc/tools/eth-mock.ts | 41 +++++++++++++------ packages/fast-usdc/tools/noble-mock.js | 19 ++++++++- 3 files changed, 79 insertions(+), 23 deletions(-) diff --git a/packages/fast-usdc/test/quickSend-tx.test.ts b/packages/fast-usdc/test/quickSend-tx.test.ts index 56426cd06c1..2380250ea6c 100644 --- a/packages/fast-usdc/test/quickSend-tx.test.ts +++ b/packages/fast-usdc/test/quickSend-tx.test.ts @@ -14,7 +14,7 @@ import { objectMap } from '@endo/patterns'; import { createRequire } from 'node:module'; import type { QuickSendTerms } from '../contract/quickSend.contract.js'; import { contract as contractFn } from '../contract/quickSend.contract.js'; -import type { CallDetails } from '../contract/quickSend.flows.js'; +import type { CCTPTxEvidence } from '../contract/client-types.js'; import { makeOrchestration, makeVStorage, @@ -42,6 +42,12 @@ const logged = (it, label) => { return it; }; +type CallDetails = { + amount: NatValue; + dest: ChainAddress; + nobleFwd: `noble1${string}`; +}; + test.before(async t => { let label; const startLabels = () => (label = 0); @@ -154,23 +160,39 @@ const makeAgoricWatcher = ({ let done = false; const { nextLabel: next } = t.context; - const check = async height => { + const check = async (height: number) => { await null; - const txs = await ethereum.getBlock(height); - t.log('watcher found', txs.length, 'txs at height', height); - for (const tx of txs) { + const block = await ethereum.getBlock(height); + const { events } = block; + t.log('watcher found', events.length, 'events at height', height); + for (const ev of events) { if (done) break; - if (tx.contractAddress !== cctpAddr) break; - const [{ dest, amount }] = tx.args; + if (ev.address !== cctpAddr) break; + const { mintRecipient, amount } = ev.args; const ix = pending.findIndex( - item => item.nobleFwd === dest && item.amount === BigInt(amount), + item => item.nobleFwd === mintRecipient && item.amount === amount, ); if (ix < 0) continue; const item = pending[ix]; pending.splice(ix, 1); - t.log(next(), 'watcher checked', tx.txId); + t.log(next(), 'watcher checked', ev.transactionHash); t.log(next(), 'watcher confirmed', item); - await watcherFacet.actions.handleCCTPCall(item); // TODO: continuing offerSpec + + const evidence: CCTPTxEvidence = { + tx: { + amount: item.amount, + forwardingAddress: item.nobleFwd, + }, + txHash: ev.transactionHash, + aux: { + forwardingChannel: 'channel-999', // not used??? + recipientAddress: item.dest.value, + }, + blockHash: block.blockHash, + blockNumber: block.blockNumber, + blockTimestamp: block.timeStamp, + }; + await watcherFacet.actions.handleCCTPCall(evidence); // TODO: continuing offerSpec } }; diff --git a/packages/fast-usdc/tools/eth-mock.ts b/packages/fast-usdc/tools/eth-mock.ts index 4cbb9f9bee8..b4aafc6ffde 100644 --- a/packages/fast-usdc/tools/eth-mock.ts +++ b/packages/fast-usdc/tools/eth-mock.ts @@ -1,13 +1,19 @@ +export type Hex = `0x${string}`; // as in viem export type EthAddr = `0x${string}`; type EthData = Record; type EthMsgInfo = { sender: EthAddr; value?: number }; type EthCallTx = { - txId: number; msg: EthMsgInfo; contractAddress: EthAddr; method: string; args: EthData[]; }; +export type EthEvent = { + eventName: string; + args: Record; + address: string; + transactionHash; +}; export const makeEventCounter = ({ setTimeout }) => { let current = 0; @@ -30,17 +36,20 @@ export const makeEventCounter = ({ setTimeout }) => { }; export const makeEthChain = (heightInitial: number, { t, setTimeout }) => { + const modern = new Date('2024-10-29T20:00:00'); let nonce = 10; let height = heightInitial; const contracts = new Map(); const mempool: EthCallTx[] = []; - const blocks: EthCallTx[][] = [[]]; - const emptyBlock = harden([]); + const eventLog: EthEvent[] = []; + const emptyBlock = harden({ txs: [], events: [] }); + const blocks: { txs: EthCallTx[]; events: EthEvent[] }[] = [emptyBlock]; let going = true; const advanceBlock = () => { - blocks.push(harden([...mempool])); + blocks.push(harden({ txs: [...mempool], events: [...eventLog] })); mempool.splice(0, mempool.length); + eventLog.splice(0, eventLog.length); height += 1; t.log('eth advance to block', height); }; @@ -61,20 +70,28 @@ export const makeEthChain = (heightInitial: number, { t, setTimeout }) => { }, call: async ( msg: EthMsgInfo, - addr: EthAddr, + address: EthAddr, method: string, args: EthData[], ) => { - const txId = nonce; - nonce += 1; - mempool.push({ txId, msg, contractAddress: addr, method, args }); - t.log(next(), 'eth call', addr, '.', method, '(', ...args, ')'); - const contract = contracts.get(addr); + mempool.push({ msg, contractAddress: address, method, args }); + t.log(next(), 'eth call', address, '.', method, '(', ...args, ')'); + const contract = contracts.get(address); const result = contract[method](msg, ...args); - t.is(result, undefined); + const transactionHash = `0x${(nonce += 1)}`; + eventLog.push(...result.map(e => ({ ...e, address, transactionHash }))); }, currentHeight: () => height - 1, - getBlock: (h: number) => blocks[h - heightInitial] || emptyBlock, + getBlock: (h: number) => { + const secs = modern.getTime() / 1000 + 15 * (h - heightInitial); + const blockHash: Hex = '0xBLOCK_HASH_TODO'; + return harden({ + timeStamp: BigInt(secs), + blockNumber: BigInt(height), + blockHash, + ...(blocks[h - heightInitial] || emptyBlock), + }); + }, stop: () => (going = false), }); }; diff --git a/packages/fast-usdc/tools/noble-mock.js b/packages/fast-usdc/tools/noble-mock.js index 24858c37692..7b51193bd17 100644 --- a/packages/fast-usdc/tools/noble-mock.js +++ b/packages/fast-usdc/tools/noble-mock.js @@ -1,6 +1,10 @@ import { NobleCalc } from '@agoric/orchestration/src/utils/address.js'; import { ibcTransfer } from './cosmoverse-mock.js'; +/** + * @import {EthEvent} from './eth-mock.js'; + */ + export const withForwarding = (chain, chains, t) => { const destOf = new Map(); const { nextLabel: next } = t.context; @@ -27,8 +31,19 @@ export const makeCCTP = ({ t, usdc, noble, events }) => { return harden({ bridge: (msg, { dest, amount: aNumeral }) => { t.regex(dest, /^noble/); - t.log(next(), 'cctp.bridge:', { msg, dest }); + const amount = BigInt(aNumeral); + /** @type {Omit} */ + const event = { + eventName: 'DepositForBurn', + args: { + amount, + mintRecipient: dest, // TODO: hex + }, + // caller adds txHash + }; + + t.log(next(), 'cctp.bridge:', { msg, dest }); usdc.transfer(msg, '0x0000', amount); // burn const t0 = events.getCurrent(); void (async () => { @@ -38,6 +53,8 @@ export const makeCCTP = ({ t, usdc, noble, events }) => { } noble.send({ amount, from: 'noble1mint', dest }); })(); + + return [event]; }, }); }; From cc8d0a04206e13f3cb063ad4a17928c09847a7a6 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Tue, 29 Oct 2024 14:38:29 -0500 Subject: [PATCH 39/48] docs: refine type of NobleCalc.fwdAddressFor --- packages/orchestration/src/utils/address.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/orchestration/src/utils/address.js b/packages/orchestration/src/utils/address.js index e5539df5a35..64b1d0f68ac 100644 --- a/packages/orchestration/src/utils/address.js +++ b/packages/orchestration/src/utils/address.js @@ -100,6 +100,11 @@ export const AgoricCalc = harden({ }); export const NobleCalc = harden({ - // XXX mock only + /** + * XXX mock only + * + * @param {string} dest + * @returns {`noble1${string}`} + */ fwdAddressFor: dest => `noble1${dest.length}${dest.slice(-4)}`, }); From 578771672692f6b641d23037b0a83c9b6695fe2d Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Tue, 29 Oct 2024 14:41:09 -0500 Subject: [PATCH 40/48] test: re-integrate computron counter --- .../test/bootstrapTests/quickSend.test.ts | 46 ++++--------------- 1 file changed, 10 insertions(+), 36 deletions(-) diff --git a/packages/boot/test/bootstrapTests/quickSend.test.ts b/packages/boot/test/bootstrapTests/quickSend.test.ts index 3134ae7415e..f62883a66e3 100644 --- a/packages/boot/test/bootstrapTests/quickSend.test.ts +++ b/packages/boot/test/bootstrapTests/quickSend.test.ts @@ -10,45 +10,19 @@ import { } from '@agoric/orchestration/src/utils/address.js'; import { buildVTransferEvent } from '@agoric/orchestration/tools/ibc-mocks.js'; import { BridgeId } from '@agoric/internal'; -import { computronCounter } from '../../tools/computron-counter.js'; import { type WalletFactoryDriver } from '../../tools/drivers.js'; import { makeWalletFactoryContext, type WalletFactoryTestContext, } from './walletFactory.js'; - -const makeMeter = () => { - const beansPer = 100n; - // see https://cosgov.org/agoric?msgType=parameterChangeProposal&network=main - const mainParams = { - blockComputeLimit: 65_000_000n * beansPer, - vatCreation: 300_000n * beansPer, - xsnapComputron: beansPer, - }; - - let metering = false; - let policy; - - const meter = harden({ - provideRunPolicy: () => { - if (metering && !policy) { - policy = computronCounter(mainParams); - } - return policy; - }, - setMetering: x => (metering = x), - getValue: () => (policy?.totalBeans() || 0n) / mainParams.xsnapComputron, - resetPolicy: () => (policy = undefined), - }); - return meter; -}; +import { makePolicyProvider } from '../../tools/supports.js'; const test: TestFn< - WalletFactoryTestContext & { meter: ReturnType } + WalletFactoryTestContext & { perfTool: ReturnType } > = anyTest; test.before('bootstrap', async t => { - const meter = makeMeter(); + const perfTool = makePolicyProvider(); const { SLOGFILE: slogFile, SWINGSET_WORKER_TYPE: defaultManagerType = 'xsnap', @@ -56,9 +30,9 @@ test.before('bootstrap', async t => { const ctx = await makeWalletFactoryContext( t, '@agoric/vm-config/decentral-itest-orchestration-config.json', - { defaultManagerType, meter, slogFile }, // for perf testing + { defaultManagerType, perfTool, slogFile }, // for perf testing ); - t.context = { ...ctx, meter }; + t.context = { ...ctx, perfTool }; // TODO: handle creating watcher smart wallet _after_ deploying contract await t.context.walletFactoryDriver.provideSmartWallet('agoric1watcher'); @@ -134,7 +108,7 @@ const makeWatcher = (sw: SmartWallet, instance, runInbound) => { }; test.serial('watcher: accept, report', async t => { - const { agoricNamesRemotes, meter, walletFactoryDriver } = t.context; + const { agoricNamesRemotes, perfTool, walletFactoryDriver } = t.context; const { runInbound } = t.context.bridgeUtils; const mc = qty => Number(qty) / 1_000_000; @@ -147,14 +121,14 @@ test.serial('watcher: accept, report', async t => { { t.log('start metering'); - meter.setMetering(true); + perfTool.usePolicy(true); const update = await william.accept(); - t.log('accept cost (Mc)', mc(meter.getValue())); + t.log('accept cost (Mc)', mc(perfTool.totalCount())); t.like(update, { status: { id: 'accept-1', numWantsSatisfied: 1 } }); } { - meter.resetPolicy(); + perfTool.resetPolicy(); const settlementBase = 'agoric1fakeLCAAddress'; // TODO: read from vstorage const encoding = 'bech32' as const; @@ -162,7 +136,7 @@ test.serial('watcher: accept, report', async t => { const vAddr = AgoricCalc.virtualAddressFor(settlementBase, dest.value); const nobleFwd = NobleCalc.fwdAddressFor(vAddr); const status = await william.report({ amount: 1234n, dest, nobleFwd }); - t.log('advance cost (Mc)', mc(meter.getValue())); + t.log('advance cost (Mc)', mc(perfTool.totalCount())); t.like(status, { status: { From e5352f10975d3ab1f194868daa8eada1d2725f00 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Tue, 29 Oct 2024 14:41:34 -0500 Subject: [PATCH 41/48] test: update bootstrap test for CCTPTxEvidence --- .../test/bootstrapTests/quickSend.test.ts | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/packages/boot/test/bootstrapTests/quickSend.test.ts b/packages/boot/test/bootstrapTests/quickSend.test.ts index f62883a66e3..31f271e68d5 100644 --- a/packages/boot/test/bootstrapTests/quickSend.test.ts +++ b/packages/boot/test/bootstrapTests/quickSend.test.ts @@ -1,6 +1,6 @@ import { test as anyTest } from '@agoric/zoe/tools/prepare-test-env-ava.js'; -import type { CallDetails } from 'fast-usdc/contract/quickSend.flows.js'; +import type { CCTPTxEvidence } from 'fast-usdc/contract/client-types.js'; import { makePromiseKit } from '@endo/promise-kit'; import type { TestFn } from 'ava'; import type { OfferId } from '@agoric/smart-wallet/src/offers.js'; @@ -80,7 +80,7 @@ const makeWatcher = (sw: SmartWallet, instance, runInbound) => { return sw.getLatestUpdateRecord(); }, - report: async (callDetails: CallDetails) => { + report: async (evidence: CCTPTxEvidence) => { await sw.sendOffer({ id: `report-${(seq += 1)}`, invitationSpec: { @@ -89,7 +89,7 @@ const makeWatcher = (sw: SmartWallet, instance, runInbound) => { previousOffer: await accepted.promise, }, proposal: {}, - offerArgs: callDetails, + offerArgs: evidence, }); // simulate ibc/MsgTransfer ack from remote chain, @@ -127,6 +127,7 @@ test.serial('watcher: accept, report', async t => { t.like(update, { status: { id: 'accept-1', numWantsSatisfied: 1 } }); } + const modern = new Date('2024-10-29T20:00:00'); { perfTool.resetPolicy(); const settlementBase = 'agoric1fakeLCAAddress'; // TODO: read from vstorage @@ -135,7 +136,14 @@ test.serial('watcher: accept, report', async t => { const dest = { chainId: 'osmosis-1', encoding, value: 'osmo1333' }; const vAddr = AgoricCalc.virtualAddressFor(settlementBase, dest.value); const nobleFwd = NobleCalc.fwdAddressFor(vAddr); - const status = await william.report({ amount: 1234n, dest, nobleFwd }); + const status = await william.report({ + tx: { amount: 1234n, forwardingAddress: nobleFwd }, + txHash: '0xABCD1234', + blockHash: '0xDEADBEEF', + blockNumber: 1234n, + blockTimestamp: BigInt(modern.getTime() / 1000), + aux: { forwardingChannel: 'channel-123', recipientAddress: dest.value }, + }); t.log('advance cost (Mc)', mc(perfTool.totalCount())); t.like(status, { @@ -143,9 +151,8 @@ test.serial('watcher: accept, report', async t => { id: 'report-2', invitationSpec: { previousOffer: 'accept-1' }, offerArgs: { - amount: 1234n, - dest: { value: 'osmo1333' }, - nobleFwd: 'noble1301333', + tx: { amount: 1234n, forwardingAddress: 'noble1301333' }, + aux: { recipientAddress: 'osmo1333' }, }, result: 'advance 1104 uusdc sent to osmo1333', }, From 1fbd6f8f1ef23cba7f9c15073a0e38f556939e29 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Tue, 29 Oct 2024 15:53:43 -0500 Subject: [PATCH 42/48] chore(fast-usdc): move IST/USDC kludge from core-eval to test --- .../test/bootstrapTests/quickSend.test.ts | 14 ++++++++++++++ .../fast-usdc/contract/start-quickSend.js | 19 ++----------------- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/packages/boot/test/bootstrapTests/quickSend.test.ts b/packages/boot/test/bootstrapTests/quickSend.test.ts index 31f271e68d5..9777d25c153 100644 --- a/packages/boot/test/bootstrapTests/quickSend.test.ts +++ b/packages/boot/test/bootstrapTests/quickSend.test.ts @@ -39,6 +39,20 @@ test.before('bootstrap', async t => { }); test.after.always(t => t.context.shutdown?.()); +test.serial('add USDC work-alike', async t => { + const { EV } = t.context.runUtils; + const agoricNames = await EV.vat('bootstrap').consumeItem('agoricNames'); + const agoricNamesAdmin = + await EV.vat('bootstrap').consumeItem('agoricNamesAdmin'); + for (const kind of ['brand', 'issuer']) { + const it = await EV(agoricNames).lookup(kind, 'IST'); + t.log(kind, it); + t.truthy(it); + const admin = await EV(agoricNamesAdmin).lookupAdmin(kind); + await EV(admin).update('USDC', it); + } +}); + test.serial('deploy contract', async t => { await t.context.walletFactoryDriver.provideSmartWallet('agoric1watcher'); diff --git a/packages/fast-usdc/contract/start-quickSend.js b/packages/fast-usdc/contract/start-quickSend.js index 558728008fe..3e470f21831 100644 --- a/packages/fast-usdc/contract/start-quickSend.js +++ b/packages/fast-usdc/contract/start-quickSend.js @@ -41,7 +41,6 @@ const makePublishingStorageKit = async (path, { chainStorage, board }) => { * instance: PromiseSpaceOf<{ * quickSend: Instance; * }>; - * brand: PromiseSpaceOf<{ USDC: Brand<'nat'> }>; * }} powers * @param {{ options?: { quickSend?: QuickSendConfig } }} config */ @@ -63,8 +62,6 @@ export const startQuickSend = async ( instance: { produce: { quickSend: produceInstance }, }, - brand, - issuer, }, config = {}, ) => { @@ -73,8 +70,8 @@ export const startQuickSend = async ( await null; const USDC = { - brand: await brand.consume.IST, // TODO: USDC interchain asset - issuer: await issuer.consume.IST, + brand: await E(agoricNames).lookup('brand', 'USDC'), + issuer: await E(agoricNames).lookup('issuer', 'USDC'), }; const terms = { makerFee: AmountMath.make(USDC.brand, 100n), // TODO: parameterize @@ -165,18 +162,6 @@ export const getManifestForQuickSend = ( instance: { produce: { quickSend: true }, }, - brand: { - consume: { - // TODO USDC - IST: true, - }, - }, - issuer: { - consume: { - // TODO USDC - IST: true, - }, - }, }, }, installations: { From 631684a0c1e504f2b731164936940b53c7af3ec1 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Tue, 29 Oct 2024 16:27:34 -0500 Subject: [PATCH 43/48] chore(fast-usdc): fee account pre-exists contract --- .../fast-usdc/contract/quickSend.contract.js | 7 +++++- .../fast-usdc/contract/quickSend.flows.js | 14 +++++------ .../fast-usdc/contract/start-quickSend.js | 8 +++++-- packages/fast-usdc/test/quickSend-tx.test.ts | 23 +++++++++---------- 4 files changed, 30 insertions(+), 22 deletions(-) diff --git a/packages/fast-usdc/contract/quickSend.contract.js b/packages/fast-usdc/contract/quickSend.contract.js index cadd2d1903c..27d917000ae 100644 --- a/packages/fast-usdc/contract/quickSend.contract.js +++ b/packages/fast-usdc/contract/quickSend.contract.js @@ -18,12 +18,17 @@ export const meta = { customTermsShape: { contractFee: NatAmountShape, makerFee: NatAmountShape, + feeAccountAddress: M.string(), }, }; harden(meta); /** - * @typedef {{ makerFee: Amount<'nat'>; contractFee: Amount<'nat'> }} QuickSendTerms + * @typedef {{ + * makerFee: Amount<'nat'>; + * contractFee: Amount<'nat'>; + * feeAccountAddress: string; + * }} QuickSendTerms * @param {ZCF} zcf * @param {OrchestrationPowers & { * marshaller: Marshaller; diff --git a/packages/fast-usdc/contract/quickSend.flows.js b/packages/fast-usdc/contract/quickSend.flows.js index ddeb20be539..83d9eaeb028 100644 --- a/packages/fast-usdc/contract/quickSend.flows.js +++ b/packages/fast-usdc/contract/quickSend.flows.js @@ -27,7 +27,6 @@ const { add, make, subtract } = AmountMath; * @typedef {{ * settlement: OrchestrationAccountI; * fundingPool: OrchestrationAccountI; - * feeAccount: OrchestrationAccountI; * }} QuickSendAccounts */ @@ -70,8 +69,7 @@ export const initAccounts = async (orch, ctx, seat, _offerArgs) => { const fundingPool = await agoric.makeAccount(); const settlement = await agoric.makeAccount(); - const feeAccount = await agoric.makeAccount(); - const accts = harden({ fundingPool, settlement, feeAccount }); + const accts = harden({ fundingPool, settlement }); const tap = ctx.makeSettleTap(accts); // @ts-expect-error tap.receiveUpcall: 'Vow | undefined' not assignable to 'Promise' const registration = await settlement.monitorTransfers(tap); @@ -84,7 +82,6 @@ export const initAccounts = async (orch, ctx, seat, _offerArgs) => { publicSubscribers: { fundingPool: (await fundingPool.getPublicTopics()).account, settlement: (await settlement.getPublicTopics()).account, - feeAccount: (await feeAccount.getPublicTopics()).account, }, ...cont, }); @@ -175,14 +172,17 @@ export const settle = async (orch, ctx, acct, event) => { // } const { contractFee } = ctx.terms; const { USDC } = ctx.terms.brands; - const { settlement, fundingPool, feeAccount } = acct; + const { settlement, fundingPool } = acct; const { nextLabel: next = () => '#?' } = ctx.t?.context || {}; const amount = make(USDC, BigInt(tx.amount)); log(next(), 'tap onReceive', { amount }); + + const poolAddr = fundingPool.getAddress(); + const feeAddr = harden({ ...poolAddr, value: ctx.terms.feeAccountAddress }); // XXX partial failure? await Promise.all([ - settlement.send(fundingPool.getAddress(), subtract(amount, contractFee)), - settlement.send(feeAccount.getAddress(), contractFee), + settlement.send(poolAddr, subtract(amount, contractFee)), + settlement.send(feeAddr, contractFee), ]); }; harden(settle); diff --git a/packages/fast-usdc/contract/start-quickSend.js b/packages/fast-usdc/contract/start-quickSend.js index 3e470f21831..ee4638df39c 100644 --- a/packages/fast-usdc/contract/start-quickSend.js +++ b/packages/fast-usdc/contract/start-quickSend.js @@ -30,7 +30,7 @@ const makePublishingStorageKit = async (path, { chainStorage, board }) => { }; /** - * @typedef {{ watcherAddress: string }} QuickSendConfig + * @typedef {{ watcherAddress: string, feeAccountAddress: string }} QuickSendConfig */ /** @@ -66,7 +66,10 @@ export const startQuickSend = async ( config = {}, ) => { trace('startQuickSend'); - const { watcherAddress = 'agoric1watcher' } = config.options?.quickSend || {}; + const { + watcherAddress = 'agoric1watcher', + feeAccountAddress = 'agoric1fee', + } = config.options?.quickSend || {}; await null; const USDC = { @@ -76,6 +79,7 @@ export const startQuickSend = async ( const terms = { makerFee: AmountMath.make(USDC.brand, 100n), // TODO: parameterize contractFee: AmountMath.make(USDC.brand, 30n), + feeAccountAddress, }; const { storageNode, marshaller } = await makePublishingStorageKit( 'quickSend', diff --git a/packages/fast-usdc/test/quickSend-tx.test.ts b/packages/fast-usdc/test/quickSend-tx.test.ts index 2380250ea6c..c8d8eea288d 100644 --- a/packages/fast-usdc/test/quickSend-tx.test.ts +++ b/packages/fast-usdc/test/quickSend-tx.test.ts @@ -256,11 +256,13 @@ const setup = async (t, io) => { 'published.quickSend.settlementBase', ); + const feeAccountAddress = await chains.agoric.makeAccount(); const terms = { issuers: { USDC: USDCe.issuer }, brands: { USDC: USDCe.brand }, makerFee: AmountMath.make(USDCe.brand, termValues.makerFee), contractFee: AmountMath.make(USDCe.brand, termValues.contractFee), + feeAccountAddress, }; const handlers = new Map(); const zcf: ZCF = harden({ @@ -372,12 +374,13 @@ const setup = async (t, io) => { const ursula = makeUser({ nobleApp, ethereum, myAddr: '0xUrsula', cctpAddr }); - return { chains, ursula, quiesce, contract, addrs, usdc }; + return { chains, ursula, quiesce, contract, addrs, terms, usdc }; }; test('tx lifecycle', async t => { const io = { setTimeout }; - const { chains, ursula, quiesce, contract, addrs, usdc } = await setup(t, io); + const info = await setup(t, io); + const { chains, ursula, quiesce, addrs, terms, usdc } = info; const destAddr = await chains.dydx.makeAccount(); // is this a prereq? await ursula.doTransfer(100n, { @@ -388,11 +391,7 @@ test('tx lifecycle', async t => { await quiesce(); - const { - fundingPool: poolAddr, - feeAccount: feeAddr, - settlement: settlementAddr, - } = addrs; + const { fundingPool: poolAddr, settlement: settlementAddr } = addrs; const actual = { user: { addr: '0xUrsula', @@ -409,8 +408,8 @@ test('tx lifecycle', async t => { balance: await chains.agoric.getBalance(poolAddr), }, fee: { - addr: feeAddr, - balance: await chains.agoric.getBalance(feeAddr), + addr: terms.feeAccountAddress, + balance: await chains.agoric.getBalance(terms.feeAccountAddress), }, settlement: { addr: settlementAddr, @@ -428,13 +427,13 @@ test('tx lifecycle', async t => { balance: 100n - termValues.makerFee - termValues.contractFee, }, pool: { - addr: 'agoric112', + addr: 'agoric113', start: startFunds.pool, balance: startFunds.pool + termValues.makerFee, }, - fee: { addr: 'agoric114', balance: termValues.contractFee }, + fee: { addr: 'agoric112', balance: termValues.contractFee }, settlement: { - addr: 'agoric113', + addr: 'agoric114', balance: 0n, }, }; From 0e9f79464796faf79372b3c50ae26a921051aeb3 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Mon, 4 Nov 2024 11:56:49 -0600 Subject: [PATCH 44/48] chore(fast-usdc): docker-compose file for ad-hoc deployment testing --- packages/fast-usdc/docker-compose.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 packages/fast-usdc/docker-compose.yml diff --git a/packages/fast-usdc/docker-compose.yml b/packages/fast-usdc/docker-compose.yml new file mode 100644 index 00000000000..afdb2813e40 --- /dev/null +++ b/packages/fast-usdc/docker-compose.yml @@ -0,0 +1,15 @@ +services: + agd: + # cf. https://github.com/Agoric/agoric-3-proposals + image: ghcr.io/agoric/agoric-3-proposals:latest + platform: linux/amd64 + ports: + - 26656:26656 + - 26657:26657 + - 1317:1317 + environment: + DEST: 1 + DEBUG: 'SwingSet:ls,SwingSet:vat' + volumes: + - .:/workspace + entrypoint: /workspace/contract/scripts/run-chain.sh From bedff43a889ca3b2286c20450b415f3bfb340f02 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Mon, 4 Nov 2024 11:52:15 -0600 Subject: [PATCH 45/48] chore: copy deploy-cli and deps from dapp-orch --- .../fast-usdc/contract/scripts/run-chain.sh | 14 + packages/fast-usdc/contract/tools/agd-lib.js | 233 ++++++ .../fast-usdc/contract/tools/batchQuery.js | 179 ++++ packages/fast-usdc/contract/tools/deploy.js | 75 ++ .../fast-usdc/contract/tools/e2e-tools.js | 769 ++++++++++++++++++ .../contract/tools/makeHttpClient.js | 100 +++ .../fast-usdc/contract/tools/marshalTables.js | 89 ++ packages/fast-usdc/contract/tools/queryKit.js | 152 ++++ .../e2e-testing/scripts/deploy-cli.ts | 44 + 9 files changed, 1655 insertions(+) create mode 100755 packages/fast-usdc/contract/scripts/run-chain.sh create mode 100644 packages/fast-usdc/contract/tools/agd-lib.js create mode 100644 packages/fast-usdc/contract/tools/batchQuery.js create mode 100644 packages/fast-usdc/contract/tools/deploy.js create mode 100644 packages/fast-usdc/contract/tools/e2e-tools.js create mode 100644 packages/fast-usdc/contract/tools/makeHttpClient.js create mode 100644 packages/fast-usdc/contract/tools/marshalTables.js create mode 100644 packages/fast-usdc/contract/tools/queryKit.js create mode 100644 packages/fast-usdc/e2e-testing/scripts/deploy-cli.ts diff --git a/packages/fast-usdc/contract/scripts/run-chain.sh b/packages/fast-usdc/contract/scripts/run-chain.sh new file mode 100755 index 00000000000..019cfa9f75a --- /dev/null +++ b/packages/fast-usdc/contract/scripts/run-chain.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +. /usr/src/upgrade-test-scripts/env_setup.sh + +# Start the chain in the background +/usr/src/upgrade-test-scripts/start_agd.sh & + +# wait for blocks to start being produced +waitForBlock 1 + +make -C /workspace/contract mint100 + +# bring back chain process to foreground +wait diff --git a/packages/fast-usdc/contract/tools/agd-lib.js b/packages/fast-usdc/contract/tools/agd-lib.js new file mode 100644 index 00000000000..f895c283d03 --- /dev/null +++ b/packages/fast-usdc/contract/tools/agd-lib.js @@ -0,0 +1,233 @@ +// @ts-check +import assert from 'node:assert'; + +const { freeze } = Object; + +const agdBinary = 'agd'; + +/** + * @param {Record} record - e.g. { color: 'blue' } + * @returns {string[]} - e.g. ['--color', 'blue'] + */ +export const flags = record => { + // TODO? support --yes with boolean? + + /** @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(); +}; + +/** + * @callback ExecSync + * @param {string} file + * @param {string[]} args + * @param {{ encoding: 'utf-8' } & { [k: string]: unknown }} opts + * @returns {string} + */ + +/** tell execFileSync to return a string, not a Buffer */ +const returnString = /** @type {const} */ ({ encoding: 'utf-8' }); + +/** + * @param {{ execFileSync: ExecSync, log?: typeof console.log }} io + */ +export const makeAgd = ({ execFileSync, log = console.log }) => { + /** + * @param { { + * home?: string; + * keyringBackend?: string; + * rpcAddrs?: string[]; + * }} opts + */ + const make = ({ home, keyringBackend, rpcAddrs } = {}) => { + const keyringArgs = flags({ home, 'keyring-backend': keyringBackend }); + if (rpcAddrs) { + assert.equal( + rpcAddrs.length, + 1, + 'XXX rpcAddrs must contain only one entry', + ); + } + const nodeArgs = flags({ node: rpcAddrs && rpcAddrs[0] }); + + /** + * @param {string[]} args + * @param {*} [opts] + */ + const exec = (args, opts = { encoding: 'utf-8' }) => + execFileSync(agdBinary, args, opts); + + const outJson = flags({ output: 'json' }); + + const ro = freeze({ + status: async () => JSON.parse(exec([...nodeArgs, 'status'])), + /** + * @param {| [kind: 'gov', domain: string, ...rest: any] + * | [kind: 'tx', txhash: string] + * | [mod: 'vstorage', kind: 'data' | 'children', path: string] + * | [mod: 'ibc', ...rest: string[]] + * } qArgs + */ + query: async qArgs => { + const out = exec(['query', ...qArgs, ...nodeArgs, ...outJson], { + encoding: 'utf-8', + stdio: ['ignore', 'pipe', 'ignore'], + }); + + try { + return JSON.parse(out); + } catch (e) { + console.error(e); + console.info('output:', out); + } + }, + }); + const nameHub = freeze({ + /** + * NOTE: synchronous I/O + * + * @param {string[]} path + */ + lookup: (...path) => { + if (!Array.isArray(path)) { + // TODO: use COND || Fail`` + throw TypeError(); + } + if (path.length !== 1) { + throw Error(`path length limited to 1: ${path.length}`); + } + const [name] = path; + const txt = exec(['keys', 'show', `--address`, name, ...keyringArgs]); + return txt.trim(); + }, + }); + const rw = freeze({ + /** + * TODO: gas + * @param {string[]} txArgs + * @param {{ chainId: string; from: string; yes?: boolean }} opts + */ + tx: async (txArgs, { chainId, from, yes }) => { + const args = [ + 'tx', + ...txArgs, + ...nodeArgs, + ...keyringArgs, + ...flags({ 'chain-id': chainId, from }), + ...flags({ + 'broadcast-mode': 'block', + gas: 'auto', + 'gas-adjustment': '1.4', + }), + ...(yes ? ['--yes'] : []), + ...outJson, + ]; + log('$$$', agdBinary, ...args); + const out = exec(args); + try { + const detail = JSON.parse(out); + if (detail.code !== 0) { + throw Error(detail.raw_log); + } + return detail; + } catch (e) { + console.error(e); + console.info('output:', out); + } + }, + ...ro, + ...nameHub, + readOnly: () => ro, + nameHub: () => nameHub, + keys: { + /** + * @param {string} name + * @param {string} mnemonic + */ + add: (name, mnemonic) => { + return execFileSync( + agdBinary, + [...keyringArgs, 'keys', 'add', name, '--recover'], + { encoding: 'utf-8', input: mnemonic }, + ).toString(); + }, + showAddress: nameHub.lookup, + /** @param {string} name */ + delete: name => { + return exec([...keyringArgs, 'keys', 'delete', name, '-y']); + }, + }, + /** + * @param {Record} opts + */ + withOpts: opts => make({ home, keyringBackend, rpcAddrs, ...opts }), + }); + return rw; + }; + return make(); +}; + +/** @typedef {ReturnType} Agd */ + +/** + * @param {{ + * pod?: string; + * container?: string; + * cmd?: string[]; + * destDir?: string; + * execFileSync: ExecSync; + * log?: Console['log']; + * join?: (...paths: string[]) => string + * basename?: (path: string) => string + * }} io + */ +export const makeContainer = ({ + execFileSync, + pod = 'agoriclocal-genesis-0', + container = 'validator', + destDir = '/root', + cmd = ['kubectl', 'exec', '-i'], + log = console.log, + join = (...paths) => paths.join('/'), + basename = path => path.split('/').at(-1) || assert.fail('bad path'), +}) => { + /** @param {{ [k: string]: unknown }} hFlags */ + const make = (hFlags = {}) => { + const runtime = { + /** + * @type {ExecSync} + */ + execFileSync: (file, args, opts = returnString) => { + const execArgs = [...cmd.slice(1), container]; + log(`${pod}/${container}$`, ...[file, ...args].map(x => `${x}`)); + const exFlags = flags({ container, ...hFlags }); + const [hFile, ...hArgs] = [...cmd, pod, ...exFlags]; + return execFileSync(hFile, [...hArgs, '--', file, ...args], opts); + }, + /** @param {string[]} paths } */ + copyFiles: paths => { + // Create the destination directory if it doesn't exist + runtime.execFileSync('mkdir', ['-p', destDir], returnString); + for (const path of paths) { + execFileSync( + 'kubectl', + [`cp`, path, `${pod}:${destDir}/`, ...flags({ container })], + returnString, + ); + log(`Copied ${path} to ${destDir} in pod ${pod}`); + } + const lsOutput = runtime.execFileSync('ls', [destDir], returnString); + log(`ls ${destDir}:\n`, lsOutput); + const destPaths = paths.map(p => join(destDir, basename(p))); + return destPaths; + }, + /** @param {{ [k: string]: unknown }} newFlags */ + withFlags: newFlags => make({ ...hFlags, ...newFlags }), + }; + return runtime; + }; + return make(); +}; + +/** @typedef {ReturnType} Container */ diff --git a/packages/fast-usdc/contract/tools/batchQuery.js b/packages/fast-usdc/contract/tools/batchQuery.js new file mode 100644 index 00000000000..18f0ea71c14 --- /dev/null +++ b/packages/fast-usdc/contract/tools/batchQuery.js @@ -0,0 +1,179 @@ +import { assert } from '@endo/errors'; +import { E } from '@endo/far'; + +/** @typedef {'children' | 'data'} AgoricChainStoragePathKind */ +/** @template T @typedef {import('@endo/marshal').FromCapData} FromCapData */ +/** @template T @typedef {import('@endo/eventual-send').ERef} ERef */ + +/** + * @param {[kind: AgoricChainStoragePathKind, item: string]} path + */ +export const pathToKey = path => path.join('.'); + +/** @param {string} key */ +export const keyToPath = key => { + const [kind, ...rest] = key.split('.'); + assert(kind === 'children' || kind === 'data'); + /** @type {[kind: 'children' | 'data', item: string]} */ + const out = [kind, rest.join('.')]; + return out; +}; + +/** + * @template T + * @param {(value: string) => T} f + * @param {AsyncGenerator} chunks + */ +async function* mapHistory(f, chunks) { + for await (const chunk of chunks) { + if (chunk === undefined) continue; + for (const value of chunk.reverse()) { + yield f(value); + } + } +} + +/** + * @param {ERef} lcd + */ +export const makeVStorage = lcd => { + const getJSON = (href, options) => E(lcd).getJSON(href, options); + + // height=0 is the same as omitting height and implies the highest block + const href = (path = 'published', { kind = 'data' } = {}) => + `/agoric/vstorage/${kind}/${path}`; + const headers = height => + height ? { 'x-cosmos-block-height': `${height}` } : undefined; + + const readStorage = ( + path = 'published', + { kind = 'data', height = 0 } = {}, + ) => + getJSON(href(path, { kind }), { headers: headers(height) }).catch(err => { + throw Error(`cannot read ${kind} of ${path}: ${err.message}`); + }); + const readCell = (path, opts) => + readStorage(path, opts) + .then(data => data.value) + .then(s => (s === '' ? {} : JSON.parse(s))); + + /** + * Read values going back as far as available + * + * @param {string} path + * @param {number | string} [minHeight] + */ + async function* readHistory(path, minHeight = undefined) { + // undefined the first iteration, to query at the highest + let blockHeight; + await null; + do { + // console.debug('READING', { blockHeight }); + /** @type {string[]} */ + let values = []; + try { + ({ blockHeight, values } = await readCell(path, { + kind: 'data', + height: blockHeight && Number(blockHeight) - 1, + })); + // console.debug('readAt returned', { blockHeight }); + } catch (err) { + if (err.message.match(/unknown request/)) { + // XXX FIXME + // console.error(err); + break; + } + throw err; + } + yield values; + // console.debug('PUSHED', values); + // console.debug('NEW', { blockHeight, minHeight }); + if (minHeight && Number(blockHeight) <= Number(minHeight)) break; + } while (blockHeight > 0); + } + + /** + * @template T + * @param {(value: string) => T} f + * @param {string} path + * @param {number | string} [minHeight] + */ + const readHistoryBy = (f, path, minHeight) => + mapHistory(f, readHistory(path, minHeight)); + + return { + lcd, + readStorage, + readCell, + readHistory, + readHistoryBy, + }; +}; + +/** @typedef {ReturnType} VStorage */ + +/** @param {string | unknown} d */ +const parseIfJSON = d => { + if (typeof d !== 'string') return d; + try { + return JSON.parse(d); + } catch { + return d; + } +}; + +/** + * @param {ReturnType} vstorage + * @param {FromCapData} unmarshal + * @param {[AgoricChainStoragePathKind, string][]} paths + */ +export const batchVstorageQuery = async (vstorage, unmarshal, paths) => { + const requests = paths.map(([kind, path]) => + vstorage.readStorage(path, { kind }), + ); + + return Promise.all(requests).then(responses => + responses.map((res, index) => { + // console.log('responses', res); + // console.log('responses', index); + + if (paths[index][0] === 'children') { + return [ + pathToKey(paths[index]), + { value: res.children, blockHeight: undefined }, + ]; + } + + if (!res.value) { + return [ + pathToKey(paths[index]), + { + error: `Cannot parse value of response for path [${ + paths[index] + }]: ${JSON.stringify(res)}`, + }, + ]; + } + + const data = parseIfJSON(res.value); + + const latestValue = + typeof data.values !== 'undefined' + ? parseIfJSON(data.values[data.values.length - 1]) + : parseIfJSON(data.value); + + const unserialized = + typeof latestValue.slots !== 'undefined' + ? unmarshal(latestValue) + : latestValue; + + return [ + pathToKey(paths[index]), + { + blockHeight: data.blockHeight, + value: unserialized, + }, + ]; + }), + ); +}; diff --git a/packages/fast-usdc/contract/tools/deploy.js b/packages/fast-usdc/contract/tools/deploy.js new file mode 100644 index 00000000000..c7350eec705 --- /dev/null +++ b/packages/fast-usdc/contract/tools/deploy.js @@ -0,0 +1,75 @@ +/** @file run a builder and deploy it onto the Agoric chain in local Starship cluster */ + +/** @import {E2ETools} from './e2e-tools'; */ + +/** + * @param {E2ETools} tools + * @param {(path: string) => Promise} readJSON + * @param {(file: string, args: string[]) => Promise<{stdout: string}>} npx + */ +export const makeDeployBuilder = (tools, readJSON, npx) => + /** @param {string} builder */ + async function deployBuilder(builder) { + console.log(`building plan: ${builder}`); + // build the plan + const { stdout } = await npx('agoric', ['run', builder]); + const match = stdout.match(/ (?[-\w]+)-permit.json/); + if (!(match && match.groups)) { + throw new Error('no permit found'); + } + const plan = await readJSON(`./${match.groups.name}-plan.json`); + console.log(plan); + + console.log('copying files to container'); + + const [code, permit] = tools.copyFiles([ + `./${plan.script}`, + `./${plan.permit}`, + ]); + + const bFiles = tools.copyFiles(plan.bundles.map(b => b.fileName)); + + console.log('installing bundles'); + await tools.installBundles(bFiles, console.log); + + console.log('executing proposal'); + await tools.runCoreEval({ + name: plan.name, + description: `${plan.name} proposal`, + code, + permit, + }); + }; + +/** + * @param {E2ETools} tools + * @param {(path: string) => Promise} readJSON + * @param {(file: string, args: string[]) => Promise<{stdout: string}>} npx + */ +export const makeDeployBuilderE2E = (tools, readJSON, npx) => + async function deployBuilder(builder) { + console.log(`building plan: ${builder}`); + // build the plan + const { stdout } = await npx('agoric', ['run', builder]); + const match = stdout.match(/ (?[-\w]+)-permit.json/); + if (!(match && match.groups)) { + throw new Error('no permit found'); + } + const plan = await readJSON(`./${match.groups.name}-plan.json`); + console.log(plan); + + console.log('copying files to container'); + const [cScript, cPermit] = tools.copyFiles([plan.script, plan.permit]); + const [cBundles] = tools.copyFiles(plan.bundles.map(b => b.fileName)); + + console.log('installing bundles'); + await tools.installBundles(cBundles, console.log); + + console.log('executing proposal'); + await tools.runCoreEval({ + name: plan.name, + description: `${plan.name} proposal`, + code: cScript, // awkward: plan file says script; .proto says code + permit: cPermit, + }); + }; diff --git a/packages/fast-usdc/contract/tools/e2e-tools.js b/packages/fast-usdc/contract/tools/e2e-tools.js new file mode 100644 index 00000000000..96177211423 --- /dev/null +++ b/packages/fast-usdc/contract/tools/e2e-tools.js @@ -0,0 +1,769 @@ +/** global harden */ +import { assert } from '@endo/errors'; +import { E, Far } from '@endo/far'; +import { Nat } from '@endo/nat'; +import { makePromiseKit } from '@endo/promise-kit'; +import { flags, makeAgd, makeContainer } from './agd-lib.js'; +import { makeHttpClient, makeAPI } from './makeHttpClient.js'; +import { dedup, makeQueryKit, poll } from './queryKit.js'; +import { makeVStorage } from './batchQuery.js'; +import { getBundleId } from './bundle-tools.js'; + +///////// + +import { exec } from 'child_process'; +import path from 'path'; +import fs from 'fs'; +import { makeDeployBuilder } from '../tools/deploy.js'; +import fse from 'fs-extra'; +import { execa } from 'execa'; +import os from 'os'; +import { createRequire } from 'module'; +const nodeRequire = createRequire(import.meta.url); + +/** @import { Container, ExecSync } from './agd-lib.js'; */ + +const BLD = '000000ubld'; + +export const txAbbr = tx => { + // eslint-disable-next-line camelcase + const { txhash, code, height, gas_used } = tx; + // eslint-disable-next-line camelcase + return { txhash, code, height, gas_used }; +}; + +/** + * @param {object} io + * @param {import('@cosmjs/tendermint-rpc').RpcClient} io.rpc + * @param {(ms: number, info?: unknown) => Promise} io.delay + */ +const makeBlockTool = ({ rpc, delay }) => { + let id = 1; + const waitForBootstrap = async (period = 2000, info = {}) => { + await null; + for (;;) { + id += 1; + const data = await rpc + .execute({ jsonrpc: '2.0', id, method: 'status', params: [] }) + .catch(err => { + console.debug('fetch error', err); + }); + + if (!data) throw Error('no data from status'); + + if (data.jsonrpc !== '2.0') { + await delay(period, { ...info, method: 'status' }); + continue; + } + + const lastHeight = data.result.sync_info.latest_block_height; + + if (lastHeight !== '1') { + return Number(lastHeight); + } + + await delay(period, { ...info, lastHeight }); + } + }; + + let last; + const waitForBlock = async (times = 1, info = {}) => { + await null; + for (let time = 0; time < times; time += 1) { + for (;;) { + const cur = await waitForBootstrap(2000, { ...info, last }); + + if (cur !== last) { + last = cur; + break; + } + + await delay(1000, info); + } + time += 1; + } + }; + + return { waitForBootstrap, waitForBlock }; +}; +/** @typedef {ReturnType} BlockTool */ + +/** + * @param {string} fullPath + * @param {object} opts + * @param {string} opts.id + * @param {import('./agd-lib.js').Agd} opts.agd + * @param {import('./queryKit.js').QueryTool['follow']} opts.follow + * @param {(ms: number) => Promise} opts.delay + * @param {typeof console.log} [opts.progress] + * @param {string} [opts.chainId] + * @param {string} [opts.installer] + * @param {string} [opts.bundleId] + */ +const installBundle = async (fullPath, opts) => { + const { id, agd, progress = console.log } = opts; + const { chainId = 'agoriclocal', installer = 'faucet' } = opts; + const from = await agd.lookup(installer); + // const explainDelay = (ms, info) => { + // progress('follow', { ...info, delay: ms / 1000 }, '...'); + // return delay(ms); + // }; + // const updates = follow('bundles', { delay: explainDelay }); + // await updates.next(); + const tx = await agd.tx( + ['swingset', 'install-bundle', `@${fullPath}`, '--gas', 'auto'], + { from, chainId, yes: true }, + ); + + progress({ id, installTx: tx.txhash, height: tx.height }); + + // const { value: confirm } = await updates.next(); + // assert(!confirm.error, confirm.error); + // assert.equal(confirm.installed, true); + // if (opts.bundleId) { + // assert.equal(`b1-${confirm.endoZipBase64Sha512}`, opts.bundleId); + // } + // TODO: return block height at which confirm went into vstorage + return { tx, confirm: true }; +}; + +/** + * @param {string} address + * @param {Record} balances + * @param {{ + * agd: import('./agd-lib.js').Agd; + * blockTool: BlockTool; + * lcd: import('./makeHttpClient.js').LCD; + * delay: (ms: number) => Promise; + * chainId?: string; + * whale?: string; + * progress?: typeof console.log; + * }} opts + */ +export const provisionSmartWallet = async ( + address, + balances, + { + agd, + blockTool, + lcd, + delay, + chainId = 'agoriclocal', + whale = 'faucet', + progress = console.log, + // q = makeQueryKit(makeVStorage(lcd)).query, + }, +) => { + const q = makeQueryKit(makeVStorage(lcd)).query; + + // TODO: skip this query if balances is {} + const vbankEntries = await q.queryData('published.agoricNames.vbankAsset'); + const byName = Object.fromEntries( + vbankEntries.map(([_denom, info]) => [info.issuerName, info]), + ); + progress({ send: balances, to: address }); + + /** + * @param {string} denom + * @param {bigint} value + */ + const sendFromWhale = async (denom, value) => { + const amount = `${value}${denom}`; + progress({ amount, to: address }); + // TODO: refactor agd.tx to support a per-sender object + // that enforces one-tx-per-block so this + // ad-hoc waitForBlock stuff is not necessary. + await agd.tx(['bank', 'send', whale, address, amount], { + chainId, + from: whale, + yes: true, + }); + await blockTool.waitForBlock(1, { step: 'bank send' }); + }; + + for await (const [name, qty] of Object.entries(balances)) { + const info = byName[name]; + if (!info) { + throw Error(name); + } + const { denom, displayInfo } = info; + const { decimalPlaces } = displayInfo; + const value = Nat(Number(qty) * 10 ** decimalPlaces); + await sendFromWhale(denom, value); + } + + progress({ provisioning: address }); + await agd.tx( + ['swingset', 'provision-one', 'my-wallet', address, 'SMART_WALLET'], + // ['swingset', 'provision-one', 'alice', address, 'SMART_WALLET'], + { chainId, from: address, yes: true }, + ); + + const info = await q.queryData(`published.wallet.${address}.current`); + progress({ + provisioned: address, + purses: info.purses.length, + used: info.offerToUsedInvitation.length, + }); + + /** @param {import('@agoric/smart-wallet/src/smartWallet.js').BridgeAction} bridgeAction */ + const sendAction = async bridgeAction => { + // eslint-disable-next-line no-undef + const capData = q.toCapData(harden(bridgeAction)); + const offerBody = JSON.stringify(capData); + const txInfo = await agd.tx( + ['swingset', 'wallet-action', offerBody, '--allow-spend'], + { from: address, chainId, yes: true }, + ); + return txInfo; + }; + + /** @param {import('@agoric/smart-wallet/src/offers.js').OfferSpec} offer */ + async function* executeOffer(offer) { + const updates = q.follow(`published.wallet.${address}`, { delay }); + const txInfo = await sendAction({ method: 'executeOffer', offer }); + console.debug('spendAction', txInfo); + for await (const update of updates) { + // console.log('update', address, update); + if (update.updated !== 'offerStatus' || update.status.id !== offer.id) { + continue; + } + yield update; + } + } + + // XXX /** @type {import('../test/wallet-tools.js').MockWallet['offers']} */ + const offers = Far('Offers', { + executeOffer, + /** @param {string | number} offerId */ + tryExit: offerId => sendAction({ method: 'tryExitOffer', offerId }), + }); + + // XXX /** @type {import('../test/wallet-tools.js').MockWallet['deposit']} */ + const deposit = Far('DepositFacet', { + receive: async payment => { + const brand = await E(payment).getAllegedBrand(); + const asset = vbankEntries.find(([_denom, a]) => a.brand === brand); + if (!asset) throw Error(`unknown brand`); + /** @type {Issuer<'nat'>} */ + const issuer = asset.issuer; + const amt = await E(issuer).getAmountOf(payment); + await sendFromWhale(asset.denom, amt.value); + return amt; + }, + }); + + const { stringify: lit } = JSON; + /** + * @returns {Promise<{ + * balances: Coins; + * pagination: unknown; + * }>} + * + * @typedef {{ denom: string; amount: string }[]} Coins + */ + const getCosmosBalances = () => + lcd.getJSON(`/cosmos/bank/v1beta1/balances/${address}`); + const cosmosBalanceUpdates = () => + dedup(poll(getCosmosBalances, { delay }), (a, b) => lit(a) === lit(b)); + + async function* vbankAssetBalanceUpdates(denom, brand) { + for await (const { balances: haystack } of cosmosBalanceUpdates()) { + for (const candidate of haystack) { + if (candidate.denom === denom) { + // eslint-disable-next-line no-undef + const amt = harden({ brand, value: BigInt(candidate.amount) }); + yield amt; + } + } + } + } + + async function* purseUpdates(brand) { + const brandAssetInfo = Object.values(byName).find(a => a.brand === brand); + await null; + if (brandAssetInfo) { + yield* vbankAssetBalanceUpdates(brandAssetInfo.denom, brand); + return; + } + const updates = q.follow(`published.wallet.${address}`, { delay }); + for await (const update of updates) { + if (update.updated !== 'balance') { + // console.log('skip: not balance', update.updated, address); + continue; + } + /** @type {Amount} */ + const amt = update.currentAmount; + if (amt.brand !== brand) { + // console.log('brand expected', brand, 'got', amt.brand, address); + continue; + } + yield amt; + } + } + + /** @type {import('../test/wallet-tools.js').MockWallet['peek']} */ + const peek = Far('Peek', { purseUpdates }); + + return { offers, deposit, peek, query: q }; +}; + +/** + * @param {{ + * agd: import('./agd-lib.js').Agd; + * blockTool: BlockTool; + * validator?: string; + * chainId?: string; + * }} opts + * @returns {Promise<{ + * proposal_id: string; + * voting_end_time: unknown; + * status: string; + * }>} + */ +const voteLatestProposalAndWait = async ({ + agd, + blockTool, + chainId = 'agoriclocal', + validator = 'genesis', +}) => { + await blockTool.waitForBlock(1, { before: 'get latest proposal' }); + const proposalsData = await agd.query(['gov', 'proposals']); + const lastProposal = proposalsData.proposals.at(-1); + const lastProposalId = lastProposal.id || lastProposal.proposal_id; + + await blockTool.waitForBlock(1, { + before: 'deposit', + on: lastProposalId, + }); + + const deposit = '50000000ubld'; + const sigOpts = { from: validator, chainId, yes: true }; + await agd.tx(['gov', 'deposit', lastProposalId, deposit], sigOpts); + + await blockTool.waitForBlock(1, { before: 'vote', on: lastProposalId }); + + await agd.tx(['gov', 'vote', lastProposalId, 'yes'], sigOpts); + + let info = {}; + for ( + ; + info.status !== 'PROPOSAL_STATUS_REJECTED' && + info.status !== 'PROPOSAL_STATUS_PASSED'; + await blockTool.waitForBlock(1, { step: `voting`, on: lastProposalId }) + ) { + info = await agd.query(['gov', 'proposal', lastProposalId]); + console.log( + `Waiting for proposal ${lastProposalId} to pass (status=${info.status})`, + ); + } + + // @ts-expect-error cast + return info; +}; + +/** + * @param {typeof console.log} log + * @param {{ + * evals: { permit: string; code: string }[]; + * title: string; + * description: string; + * }} info + * @param {{ + * agd: import('./agd-lib.js').Agd; + * blockTool: BlockTool; + * proposer?: string; + * deposit?: string; + * chainId?: string; + * }} opts + */ +const runCoreEval = async ( + log, + { evals, title, description }, + { + agd, + blockTool, + chainId = 'agoriclocal', + proposer = 'genesis', + deposit = `1${BLD}`, + }, +) => { + const from = await agd.lookup(proposer); + const info = { title, description }; + log('submit proposal', title); + + // TODO? double-check that bundles are loaded + + const evalPaths = evals.map(e => [e.permit, e.code]).flat(); + log('swingset-core-eval', evalPaths); + const result = await agd.tx( + [ + 'gov', + 'submit-proposal', + 'swingset-core-eval', + ...evalPaths, + ...flags({ ...info, deposit }), + ], + { from, chainId, yes: true }, + ); + log(txAbbr(result)); + // FIXME TypeError#1: unrecognized details 0 + // assert(result.code, 0); + + log('await voteLatestProposalAndWait', evalPaths); + const detail = await voteLatestProposalAndWait({ agd, blockTool }); + log('proposal result detail', detail); + log(detail.proposal_id, detail.voting_end_time, detail.status); + // log(detail.id, detail.voting_end_time, detail.status); + + // TODO: how long is long enough? poll? + await blockTool.waitForBlock(5, { step: 'run', propsal: detail.proposal_id }); + // await blockTool.waitForBlock(5, { step: 'run', propsal: detail.id }); + + assert(detail.status, 'PROPOSAL_STATUS_PASSED'); + return detail; +}; + +/** + * @param {typeof console.log} log + * @param {import('@agoric/swingset-vat/tools/bundleTool.js').BundleCache} bundleCache + * @param {object} io + * @param {ExecSync} io.execFileSync + * @param {Container['copyFiles']} io.copyFiles + * @param {typeof window.fetch} io.fetch + * @param {typeof window.setTimeout} io.setTimeout + * @param {string} [io.bundleDir] + * @param {string} [io.rpcAddress] + * @param {string} [io.apiAddress] + * @param {(...parts: string[]) => string} [io.join] + */ +export const makeE2ETools = ( + log, + bundleCache, + { + execFileSync, + copyFiles, + fetch, + setTimeout, + rpcAddress = 'http://localhost:26657', + apiAddress = 'http://localhost:1317', + }, +) => { + const agd = makeAgd({ execFileSync }).withOpts({ keyringBackend: 'test' }); + const rpc = makeHttpClient(rpcAddress, fetch); + const lcd = makeAPI(apiAddress, { fetch }); + const delay = ms => new Promise(resolve => setTimeout(resolve, ms)); + + const explainDelay = (ms, info) => { + if (typeof info === 'object' && Object.keys(info).length > 0) { + // XXX normally we have the caller pass in the log function + // later, but the way blockTool is factored, we have to supply it early. + console.log({ ...info, delay: ms / 1000 }, '...'); + } + return delay(ms); + }; + const blockTool = makeBlockTool({ rpc, delay: explainDelay }); + + const vstorage = makeVStorage(lcd); + const qt = makeQueryKit(vstorage); + + const installBundles = async (fullPaths, progress) => { + await null; + /** @type {Record} */ + const bundles = {}; + // for (const [name, rootModPath] of Object.entries(bundleRoots)) { + console.log('fullPaths', fullPaths); + + for (const fullPath of fullPaths) { + const { tx, confirm } = await installBundle(fullPath, { + id: fullPath, + agd, + follow: qt.query.follow, + progress, + delay, + // bundleId: getBundleId(bundle), + bundleId: undefined, + }); + console.log('confirm', confirm); + progress({ + // name, + id: fullPath, + installHeight: tx.height, + installed: confirm, + }); + } + // eslint-disable-next-line no-undef + return harden(bundles); + }; + + /** + * @param {Iterable} fullPaths + * @param {typeof console.log} progress + */ + const installBundlesE2E = async (fullPaths, progress) => { + await null; + /** @type {Record} */ + const bundles = {}; + // for (const [name, rootModPath] of Object.entries(bundleRoots)) { + console.log('fullPaths E2E', fullPaths); + + console.log('getBundleId(bundle)'); + + for (const _fullPath of fullPaths) { + console.log('+fullPath'); + console.log(_fullPath); + + const pathSlices = _fullPath.split(','); + // if (pathSlices.length != 2) throw 'invalid path slices length'; + const contractPath = pathSlices[0]; + const proposalPath = pathSlices[1]; + + console.log('contractPath'); + console.log(contractPath); + console.log('proposalPath'); + console.log(proposalPath); + + const fullPath = contractPath; + const containerPath = fullPath.includes('contract/src/') + ? '/root/src/' + fullPath.split('contract/src/').pop() + : fullPath; + + console.log('containerPath'); + console.log(containerPath); + + // load bundle + const bundle = await bundleCache.load(fullPath, 'orca'); + const bundle_proposal = await bundleCache.load(proposalPath, 'orca'); + + console.log('bundle'); + console.log(bundle); + console.log(bundle_proposal); + + //copy to + const homeDir = os.homedir(); + const bundleId = getBundleId(bundle); + const bundleFileName = path.join( + homeDir, + '.agoric/cache', + `${bundleId}.json`, + ); + + console.log('bundleFileName'); + console.log(bundleFileName); + + if (fs.existsSync(bundleFileName)) { + console.log(`copying ${bundleFileName} to container...`); + const copyCommand = `kubectl cp ${bundleFileName} agoriclocal-genesis-0:/root/bundles/${path.basename(bundleFileName)}`; + exec(copyCommand, (error, stdout, stderr) => { + if (error) { + console.error(`Error copying file: ${stderr}`); + return; + } + }); + console.log( + `bundle copied to container at /root/${path.basename(bundleFileName)}`, + ); + } else { + console.error(`bundle file ${bundleFileName} does not exist!`); + // exit(1) + // return; + } + + // generate plan, etc + // const keyring = await makeKeyring(tools); + // const deployBuilder = makeDeployBuilder(tools, fse.readJSON, execa); + const contractBuilder = './test/builder/init-orca.js'; + // await deployBuilder(contractBuilder); + const { stdout } = await execa`agoric run ${contractBuilder}`; + const match = stdout.match(/ (?[-\w]+)-permit.json/); + if (!(match && match.groups)) { + throw new Error('no permit found'); + } + const plan = await fse.readJSON(`./${match.groups.name}-plan.json`); + console.log(plan); + + console.log('copying files to containr'); + + // copy artifacts to container + copyFiles([ + nodeRequire.resolve(`../${plan.script}`), + nodeRequire.resolve(`../${plan.permit}`), + ...plan.bundles.map(b => b.fileName), + ]); + + console.log( + 'getBundleId(bundle)', + getBundleId(bundle), + plan.bundles[0].bundleID, + getBundleId(bundle) == plan.bundles[0].bundleID, + ); + + //install proposal + const proposalResult = await installBundle( + `/root/${plan.bundles[1].bundleID}.json`, + { + id: fullPath, + agd, + follow: qt.query.follow, + progress, + delay, + // bundleId: getBundleId(bundle), + bundleId: plan.bundles[1].bundleID, + // bundleId: undefined, + }, + ); + + console.log('confirm_contract', proposalResult.confirm); + + progress({ + // name, + id: fullPath, + installHeight: proposalResult.tx.height, + installed: proposalResult.confirm, + }); + + //install contract + + // const { tx, confirm } = await installBundle(fullPath, { + // const { tx, confirm } = await installBundle(containerPath, { + let { tx, confirm } = await installBundle( + `/root/${plan.bundles[0].bundleID}.json`, + { + id: fullPath, + agd, + follow: qt.query.follow, + progress, + delay, + // bundleId: getBundleId(bundle), + bundleId: plan.bundles[0].bundleID, + // bundleId: undefined, + }, + ); + + console.log('confirm_contract', confirm); + + progress({ + // name, + id: fullPath, + installHeight: tx.height, + installed: confirm, + }); + } + // eslint-disable-next-line no-undef + return harden(bundles); + }; + + /** + * NOTE: name only comes through as orca, not the actual file names + * + * @param {{ + * name: string; + * title?: string; + * description?: string; + * code?: string; + * permit?: string; + * } & { + * behavior?: Function; + * }} info + */ + const buildAndRunCoreEval = async info => { + if ('builderPath' in info) { + throw Error('@@TODO: agoric run style'); + } + + console.log('info'); + console.log(info); + const { + name, + title = name, + description = title, + code = `${name}.js`, + permit = `${name}-permit.json`, + } = info; + const eval0 = { code, permit }; + const detail = { evals: [eval0], title, description }; + // await runPackageScript('build:deployer', entryFile); + console.log('log:', log); + const proposal = await runCoreEval(console.log, detail, { agd, blockTool }); + return proposal; + }; + + const vstorageClient = makeQueryKit(vstorage).query; + + const tools = harden({ + vstorageClient, + agd, + installBundles, + installBundlesE2E, + runCoreEval: buildAndRunCoreEval, + /** + * @param {string} address + * @param {Record} amount + */ + provisionSmartWallet: (address, amount) => + provisionSmartWallet(address, amount, { + agd, + blockTool, + lcd, + delay, + // q: vstorageClient, + }), + + copyFiles, + }); + return tools; +}; + +/** @typedef {ReturnType} E2ETools */ + +/** + * Seat-like API from wallet updates + * + * @param {AsyncGenerator} updates + */ +export const seatLike = updates => { + const sync = { + result: makePromiseKit(), + // /** @type {PromiseKit} */ + /** @type {ReturnType} */ + payouts: makePromiseKit(), + }; + (async () => { + await null; + try { + // XXX an error here is somehow and unhandled rejection + for await (const update of updates) { + if (update.updated !== 'offerStatus') continue; + const { result, payouts } = update.status; + if ('result' in update.status) sync.result.resolve(result); + if ('payouts' in update.status && payouts) { + sync.payouts.resolve(payouts); + console.debug('paid out', update.status.id); + return; + } + } + } catch (reason) { + sync.result.reject(reason); + sync.payouts.reject(reason); + throw reason; + } + })(); + // eslint-disable-next-line no-undef + return harden({ + getOfferResult: () => sync.result.promise, + getPayoutAmounts: () => sync.payouts.promise, + }); +}; + +/** @param {Awaited>} wallet */ +export const makeDoOffer = wallet => { + const doOffer = async offer => { + const updates = wallet.offers.executeOffer(offer); + // const seat = seatLike(updates); + // const result = await seat.getOfferResult(); + await seatLike(updates).getPayoutAmounts(); + // return result; + }; + + return doOffer; +}; diff --git a/packages/fast-usdc/contract/tools/makeHttpClient.js b/packages/fast-usdc/contract/tools/makeHttpClient.js new file mode 100644 index 00000000000..e422f3f3c38 --- /dev/null +++ b/packages/fast-usdc/contract/tools/makeHttpClient.js @@ -0,0 +1,100 @@ +import { assert } from '@endo/errors'; +import { Far } from '@endo/far'; + +const { freeze } = Object; + +const jsonType = { 'Content-Type': 'application/json' }; + +const filterBadStatus = res => { + if (res.status >= 400) { + throw new Error(`Bad status on response: ${res.status}`); + } + return res; +}; + +/** + * Make an RpcClient using explicit access to the network. + * + * The RpcClient implementations included in cosmjs + * such as {@link https://cosmos.github.io/cosmjs/latest/tendermint-rpc/classes/HttpClient.html HttpClient} + * use ambient authority (fetch or axios) for network access. + * + * To facilitate cooperation without vulnerability, + * as well as unit testing, etc. this RpcClient maker takes + * network access as a parameter, following + * {@link https://github.com/Agoric/agoric-sdk/wiki/OCap-Discipline|OCap Discipline}. + * + * @param {string} url + * @param {typeof globalThis.fetch} fetch + * @returns {import('@cosmjs/tendermint-rpc').RpcClient} + */ +export const makeHttpClient = (url, fetch) => { + const headers = {}; // XXX needed? + + // based on cosmjs 0.30.1: + // https://github.com/cosmos/cosmjs/blob/33271bc51cdc865cadb647a1b7ab55d873637f39/packages/tendermint-rpc/src/rpcclients/http.ts#L37 + // https://github.com/cosmos/cosmjs/blob/33271bc51cdc865cadb647a1b7ab55d873637f39/packages/tendermint-rpc/src/rpcclients/httpclient.ts#L25 + return freeze({ + disconnect: () => { + // nothing to be done + }, + + /** + * @param {import('@cosmjs/json-rpc').JsonRpcRequest} request + */ + execute: async request => { + const settings = { + method: 'POST', + body: request ? JSON.stringify(request) : undefined, + headers: { ...jsonType, ...headers }, + }; + return fetch(url, settings) + .then(filterBadStatus) + .then(res => res.json()); + }, + }); +}; + +/** + * gRPC-gateway REST API access + * + * @see {@link https://docs.cosmos.network/v0.45/core/grpc_rest.html#rest-server Cosmos SDK REST Server} + * + * Note: avoid Legacy REST routes, per + * {@link https://docs.cosmos.network/v0.45/migrations/rest.html Cosmos SDK REST Endpoints Migration}. + * + * @param {string} apiAddress nodes default to port 1317 + * @param {object} io + * @param {typeof fetch} io.fetch + */ +export const makeAPI = (apiAddress, { fetch }) => { + assert.typeof(apiAddress, 'string'); + + /** + * @param {string} href + * @param {object} [options] + * @param {Record} [options.headers] + */ + const getJSON = (href, options = {}) => { + const opts = { + keepalive: true, + headers: { + 'Content-Type': 'application/json', + ...options.headers, + }, + }; + const url = `${apiAddress}${href}`; + return fetch(url, opts).then(r => { + if (!r.ok) throw Error(r.statusText); + return r.json().then(data => { + return data; + }); + }); + }; + + return Far('LCD', { + getJSON, + latestBlock: () => getJSON(`/cosmos/base/tendermint/v1beta1/blocks/latest`), + }); +}; +/** @typedef {ReturnType} LCD */ diff --git a/packages/fast-usdc/contract/tools/marshalTables.js b/packages/fast-usdc/contract/tools/marshalTables.js new file mode 100644 index 00000000000..ce31fd5ff08 --- /dev/null +++ b/packages/fast-usdc/contract/tools/marshalTables.js @@ -0,0 +1,89 @@ +/** + * @file marshal tools for vstorage clients + * + * TODO: integrate back into @agoric/rpc + * - fixes: calls to makeClientMarshaller share static mutable state + * https://github.com/Agoric/ui-kit/issues/73 + * - fits in this plain .js project + */ +/** global harden */ +import { Far, makeMarshal } from '@endo/marshal'; + +/** + * The null slot indicates that identity is not intended to be preserved. + * + * @typedef { string | null } WildSlot + */ + +/** + * Implement conventional parts of convertValToSlot, convertSlotToVal functions + * for use with makeMarshal based on a slot <-> value translation table, + * indexed in both directions. Caller supplies functions for making + * slots, values when not present in the table. + * + * @template Val + * @param {(val: Val, size: number) => string} makeSlot + * @param {(slot: WildSlot, iface: string | undefined) => Val} makeVal + */ +const makeTranslationTable = (makeSlot, makeVal) => { + /** @type {Map} */ + const valToSlot = new Map(); + /** @type {Map} */ + const slotToVal = new Map(); + + /** @type {(val: Val) => string} */ + const convertValToSlot = val => { + if (valToSlot.has(val)) { + // @ts-expect-error https://github.com/microsoft/TypeScript/issues/13086 + return valToSlot.get(val); + } + const slot = makeSlot(val, valToSlot.size); + valToSlot.set(val, slot); + slotToVal.set(slot, val); + return slot; + }; + + /** @type {(slot: WildSlot, iface: string | undefined) => Val} */ + const convertSlotToVal = (slot, iface) => { + if (slot === null) return makeVal(slot, iface); + if (slotToVal.has(slot)) { + // @ts-expect-error https://github.com/microsoft/TypeScript/issues/13086 + return slotToVal.get(slot); + } + const val = makeVal(slot, iface); + valToSlot.set(val, slot); + slotToVal.set(slot, val); + return val; + }; + // eslint-disable-next-line no-undef + return harden({ convertValToSlot, convertSlotToVal }); +}; + +// /** @type {(slot: string, iface: string | undefined) => any} */ +/** @type {(slot: string | null, iface: string | undefined) => any} */ +const synthesizeRemotable = (slot, iface) => { + const ifaceStr = iface ?? ''; + const suffix = ifaceStr.endsWith(`#${slot}`) ? '' : `#${slot}`; + return Far(`${ifaceStr.replace(/^Alleged: /, '')}${suffix}`, {}); +}; + +/** + * Make a marshaller that synthesizes a remotable the first + * time it sees a slot identifier, allowing clients to recognize + * object identity for brands, instances, etc. + * + * @param {(v: unknown) => string} [valToSlot] + */ +export const makeClientMarshaller = valToSlot => { + const noNewSlots = val => { + throw new Error(`unknown value: ${val}`); + }; + const { convertValToSlot, convertSlotToVal } = makeTranslationTable( + valToSlot || noNewSlots, + synthesizeRemotable, + ); + + return makeMarshal(convertValToSlot, convertSlotToVal, { + serializeBodyFormat: 'smallcaps', + }); +}; diff --git a/packages/fast-usdc/contract/tools/queryKit.js b/packages/fast-usdc/contract/tools/queryKit.js new file mode 100644 index 00000000000..d12a74246c3 --- /dev/null +++ b/packages/fast-usdc/contract/tools/queryKit.js @@ -0,0 +1,152 @@ +import { E, Far } from '@endo/far'; +import { batchVstorageQuery } from './batchQuery.js'; +import { makeClientMarshaller } from './marshalTables.js'; + +/** + * Iter tools... + * + * @template {Promise} PT + * @param {() => PT} fn + * @param {{ delay: (ms: number) => Promise, period?: number }} opts + */ +export async function* poll(fn, { delay, period = 1000 }) { + await null; + for (;;) { + const x = await fn(); + yield x; + await delay(period); + } +} + +/** + * @template {Promise} PT + * @param {AsyncGenerator>} src + * @param {(a: unknown, b: unknown) => boolean} [equal] + */ +export async function* dedup(src, equal = (x, y) => x === y) { + let last; + for await (const x of src) { + if (!equal(x, last)) { + yield x; + last = x; + } + } +} + +/** + * @template {Promise} PT + * @template {Promise} PU + * @param {AsyncGenerator>} src + * @param {(x: Awaited) => PU} fn + */ +export async function* mapIter(src, fn) { + for await (const item of src) { + yield fn(item); + } +} + +/** + * @param {string} key + * @param {object} io + * @param {import('./batchQuery.js').VStorage} io.vstorage + * @param {(ms: number, opts?: unknown) => Promise} io.delay + */ +export async function* eachVstorageUpdate(key, { vstorage, delay }) { + const { stringify: q } = JSON; + const updates = dedup( + poll(() => vstorage.readCell(key, { kind: 'data' }), { + delay, + period: 2000, + }), + (a, b) => q(a) === q(b), + ); + + for await (const cell of updates) { + // use blockHeight? + const { values } = cell; + for (const value of values) { + yield value; + } + } +} + +/** + * @param {string} addr + * @param {object} powers + * @param {QueryTool} powers.query + * @param {import('./batchQuery.js').VStorage} powers.vstorage + */ +export const makeWalletView = (addr, { query, vstorage }) => { + return Far('WalletQuery', { + current: () => query.queryData(`published.wallet.${addr}.current`), + /** + * TODO: visit in chunks by block + * @param {import('@endo/eventual-send').ERef<{visit: (r: import('@agoric/smart-wallet/src/smartWallet.js').UpdateRecord) => void}>} visitor + * @param {number} [minHeight] + */ + history: async (visitor, minHeight) => { + const history = vstorage.readHistoryBy( + s => query.fromCapData(JSON.parse(s)), + `published.wallet.${addr}`, + minHeight, + ); + for await (const record of history) { + await E(visitor).visit(record); + } + }, + }); +}; +/** @typedef {ReturnType} WalletView } */ + +/** + * @param {import('./batchQuery.js').VStorage} vstorage + * @param {import('@endo/marshal').Marshal} [m] + */ +// export const makeQueryKit = (vstorage, m = makeClientMarshaller()) => { +export const makeQueryKit = ( + vstorage, + m = /** @type {import('@endo/marshal').Marshal} */ ( + makeClientMarshaller() + ), +) => { + /** @param {['children' | 'data', string][]} paths */ + const batchQuery = async paths => + batchVstorageQuery(vstorage, m.fromCapData, paths); + + /** @param {string} path */ + const queryData = async path => { + const [[_p, answer]] = await batchQuery([['data', path]]); + if (typeof answer === 'string') return answer; + if (answer.error) throw Error(answer.error); + return answer.value; + }; + + /** @param {string} path */ + const queryChildren = async path => { + const [[_p, answer]] = await batchQuery([['children', path]]); + if (typeof answer === 'string') return answer; + if (answer.error) throw Error(answer.error); + return answer.value; + }; + + async function* follow(path, { delay }) { + for await (const txt of eachVstorageUpdate(path, { vstorage, delay })) { + const value = m.fromCapData(JSON.parse(txt)); + yield value; + } + } + + const query = Far('QueryTool', { + batchQuery, + queryData, + follow, + queryChildren, + fromCapData: m.fromCapData, + toCapData: m.toCapData, + // XXX wrong layer? add makeWalletView(query) helper function instead? + walletView: addr => makeWalletView(addr, { query, vstorage }), + }); + + return { vstorage, query }; +}; +/** @typedef {Awaited>['query']} QueryTool */ diff --git a/packages/fast-usdc/e2e-testing/scripts/deploy-cli.ts b/packages/fast-usdc/e2e-testing/scripts/deploy-cli.ts new file mode 100644 index 00000000000..2d32aaf4195 --- /dev/null +++ b/packages/fast-usdc/e2e-testing/scripts/deploy-cli.ts @@ -0,0 +1,44 @@ +#!/usr/bin/env tsx +import '@endo/init/debug.js'; + +import { makeNodeBundleCache } from '@endo/bundle-source/cache.js'; +import childProcess from 'node:child_process'; +import fsp from 'node:fs/promises'; +import { promisify } from 'node:util'; +import { makeContainer } from '../../contract/tools/agd-lib.js'; +import { makeDeployBuilder } from '../../contract/tools/deploy.js'; +import { makeE2ETools } from '../../contract/tools/e2e-tools.js'; + +async function main() { + const builder = process.argv[2]; + + if (!builder) { + console.error('USAGE: deploy-cli.ts '); + process.exit(1); + } + + const container = makeContainer({ execFileSync: childProcess.execFileSync }); + + const bundleCache = await makeNodeBundleCache('bundles', {}, s => import(s)); + const tools = makeE2ETools(console.log, bundleCache, { + execFileSync: container.execFileSync, + copyFiles: container.copyFiles, + fetch, + setTimeout, + }); + + const readJSON = (path: string) => + fsp.readFile(path, 'utf-8').then(x => JSON.parse(x)); + const execFileP = promisify(childProcess.execFile); + const npx = (file: string, args: string[]) => + execFileP('npx', ['--no-install', file, ...args]); + const deployBuilder = makeDeployBuilder(tools, readJSON, npx); + try { + await deployBuilder(builder); + } catch (err) { + console.error(err); + process.exit(1); + } +} + +main(); From 66e17e6775e08e02a9a50026a5d50d9ccd5c88f1 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Mon, 4 Nov 2024 11:57:27 -0600 Subject: [PATCH 46/48] chore: port deploy-cli for ad-hoc deployment - genesis vs. validator etc. - kubectl vs docker compose - skip mint100; gov1 has plenty - avoid tsx dep --- .../fast-usdc/contract/scripts/run-chain.sh | 2 +- packages/fast-usdc/contract/tools/agd-lib.js | 8 ++-- .../fast-usdc/contract/tools/e2e-tools.js | 45 +++++++++---------- .../scripts/{deploy-cli.ts => deploy-cli.js} | 32 ++++++------- 4 files changed, 43 insertions(+), 44 deletions(-) rename packages/fast-usdc/e2e-testing/scripts/{deploy-cli.ts => deploy-cli.js} (63%) diff --git a/packages/fast-usdc/contract/scripts/run-chain.sh b/packages/fast-usdc/contract/scripts/run-chain.sh index 019cfa9f75a..ac6e09f4746 100755 --- a/packages/fast-usdc/contract/scripts/run-chain.sh +++ b/packages/fast-usdc/contract/scripts/run-chain.sh @@ -8,7 +8,7 @@ # wait for blocks to start being produced waitForBlock 1 -make -C /workspace/contract mint100 +# make -C /workspace/contract mint100 # bring back chain process to foreground wait diff --git a/packages/fast-usdc/contract/tools/agd-lib.js b/packages/fast-usdc/contract/tools/agd-lib.js index f895c283d03..4ccaca883d5 100644 --- a/packages/fast-usdc/contract/tools/agd-lib.js +++ b/packages/fast-usdc/contract/tools/agd-lib.js @@ -201,9 +201,9 @@ export const makeContainer = ({ execFileSync: (file, args, opts = returnString) => { const execArgs = [...cmd.slice(1), container]; log(`${pod}/${container}$`, ...[file, ...args].map(x => `${x}`)); - const exFlags = flags({ container, ...hFlags }); + const exFlags = flags({ ...hFlags }); const [hFile, ...hArgs] = [...cmd, pod, ...exFlags]; - return execFileSync(hFile, [...hArgs, '--', file, ...args], opts); + return execFileSync(hFile, [...hArgs, file, ...args], opts); }, /** @param {string[]} paths } */ copyFiles: paths => { @@ -211,8 +211,8 @@ export const makeContainer = ({ runtime.execFileSync('mkdir', ['-p', destDir], returnString); for (const path of paths) { execFileSync( - 'kubectl', - [`cp`, path, `${pod}:${destDir}/`, ...flags({ container })], + 'docker', + ['compose', `cp`, path, `${pod}:${destDir}/`], returnString, ); log(`Copied ${path} to ${destDir} in pod ${pod}`); diff --git a/packages/fast-usdc/contract/tools/e2e-tools.js b/packages/fast-usdc/contract/tools/e2e-tools.js index 96177211423..6a4bb6244fe 100644 --- a/packages/fast-usdc/contract/tools/e2e-tools.js +++ b/packages/fast-usdc/contract/tools/e2e-tools.js @@ -3,22 +3,22 @@ import { assert } from '@endo/errors'; import { E, Far } from '@endo/far'; import { Nat } from '@endo/nat'; import { makePromiseKit } from '@endo/promise-kit'; +import { exec } from 'child_process'; +import path from 'path'; +import fs from 'fs'; +import fse from 'fs-extra'; +import os from 'os'; +import { createRequire } from 'module'; import { flags, makeAgd, makeContainer } from './agd-lib.js'; import { makeHttpClient, makeAPI } from './makeHttpClient.js'; import { dedup, makeQueryKit, poll } from './queryKit.js'; import { makeVStorage } from './batchQuery.js'; import { getBundleId } from './bundle-tools.js'; -///////// +/// ////// + +import { makeDeployBuilder } from './deploy.js'; -import { exec } from 'child_process'; -import path from 'path'; -import fs from 'fs'; -import { makeDeployBuilder } from '../tools/deploy.js'; -import fse from 'fs-extra'; -import { execa } from 'execa'; -import os from 'os'; -import { createRequire } from 'module'; const nodeRequire = createRequire(import.meta.url); /** @import { Container, ExecSync } from './agd-lib.js'; */ @@ -102,7 +102,7 @@ const makeBlockTool = ({ rpc, delay }) => { */ const installBundle = async (fullPath, opts) => { const { id, agd, progress = console.log } = opts; - const { chainId = 'agoriclocal', installer = 'faucet' } = opts; + const { chainId = 'agoriclocal', installer = 'gov1' } = opts; const from = await agd.lookup(installer); // const explainDelay = (ms, info) => { // progress('follow', { ...info, delay: ms / 1000 }, '...'); @@ -149,7 +149,7 @@ export const provisionSmartWallet = async ( lcd, delay, chainId = 'agoriclocal', - whale = 'faucet', + whale = 'validator', progress = console.log, // q = makeQueryKit(makeVStorage(lcd)).query, }, @@ -208,7 +208,6 @@ export const provisionSmartWallet = async ( /** @param {import('@agoric/smart-wallet/src/smartWallet.js').BridgeAction} bridgeAction */ const sendAction = async bridgeAction => { - // eslint-disable-next-line no-undef const capData = q.toCapData(harden(bridgeAction)); const offerBody = JSON.stringify(capData); const txInfo = await agd.tx( @@ -271,7 +270,6 @@ export const provisionSmartWallet = async ( for await (const { balances: haystack } of cosmosBalanceUpdates()) { for (const candidate of haystack) { if (candidate.denom === denom) { - // eslint-disable-next-line no-undef const amt = harden({ brand, value: BigInt(candidate.amount) }); yield amt; } @@ -325,7 +323,7 @@ const voteLatestProposalAndWait = async ({ agd, blockTool, chainId = 'agoriclocal', - validator = 'genesis', + validator = 'validator', }) => { await blockTool.waitForBlock(1, { before: 'get latest proposal' }); const proposalsData = await agd.query(['gov', 'proposals']); @@ -384,7 +382,7 @@ const runCoreEval = async ( agd, blockTool, chainId = 'agoriclocal', - proposer = 'genesis', + proposer = 'validator', deposit = `1${BLD}`, }, ) => { @@ -492,7 +490,7 @@ export const makeE2ETools = ( installed: confirm, }); } - // eslint-disable-next-line no-undef + return harden(bundles); }; @@ -525,7 +523,7 @@ export const makeE2ETools = ( const fullPath = contractPath; const containerPath = fullPath.includes('contract/src/') - ? '/root/src/' + fullPath.split('contract/src/').pop() + ? `/root/src/${fullPath.split('contract/src/').pop()}` : fullPath; console.log('containerPath'); @@ -539,7 +537,7 @@ export const makeE2ETools = ( console.log(bundle); console.log(bundle_proposal); - //copy to + // copy to const homeDir = os.homedir(); const bundleId = getBundleId(bundle); const bundleFileName = path.join( @@ -557,7 +555,6 @@ export const makeE2ETools = ( exec(copyCommand, (error, stdout, stderr) => { if (error) { console.error(`Error copying file: ${stderr}`); - return; } }); console.log( @@ -598,7 +595,7 @@ export const makeE2ETools = ( getBundleId(bundle) == plan.bundles[0].bundleID, ); - //install proposal + // install proposal const proposalResult = await installBundle( `/root/${plan.bundles[1].bundleID}.json`, { @@ -622,11 +619,11 @@ export const makeE2ETools = ( installed: proposalResult.confirm, }); - //install contract + // install contract // const { tx, confirm } = await installBundle(fullPath, { // const { tx, confirm } = await installBundle(containerPath, { - let { tx, confirm } = await installBundle( + const { tx, confirm } = await installBundle( `/root/${plan.bundles[0].bundleID}.json`, { id: fullPath, @@ -649,7 +646,7 @@ export const makeE2ETools = ( installed: confirm, }); } - // eslint-disable-next-line no-undef + return harden(bundles); }; @@ -748,7 +745,7 @@ export const seatLike = updates => { throw reason; } })(); - // eslint-disable-next-line no-undef + return harden({ getOfferResult: () => sync.result.promise, getPayoutAmounts: () => sync.payouts.promise, diff --git a/packages/fast-usdc/e2e-testing/scripts/deploy-cli.ts b/packages/fast-usdc/e2e-testing/scripts/deploy-cli.js similarity index 63% rename from packages/fast-usdc/e2e-testing/scripts/deploy-cli.ts rename to packages/fast-usdc/e2e-testing/scripts/deploy-cli.js index 2d32aaf4195..bc146fb5701 100644 --- a/packages/fast-usdc/e2e-testing/scripts/deploy-cli.ts +++ b/packages/fast-usdc/e2e-testing/scripts/deploy-cli.js @@ -1,4 +1,5 @@ -#!/usr/bin/env tsx +#!/usr/bin/env node +/* global globalThis, process */ import '@endo/init/debug.js'; import { makeNodeBundleCache } from '@endo/bundle-source/cache.js'; @@ -17,28 +18,29 @@ async function main() { process.exit(1); } - const container = makeContainer({ execFileSync: childProcess.execFileSync }); + const container = makeContainer({ + execFileSync: childProcess.execFileSync, + cmd: ['docker', 'compose', 'exec'], + pod: 'agd', + }); const bundleCache = await makeNodeBundleCache('bundles', {}, s => import(s)); const tools = makeE2ETools(console.log, bundleCache, { execFileSync: container.execFileSync, copyFiles: container.copyFiles, - fetch, - setTimeout, + fetch: globalThis.fetch, + setTimeout: globalThis.setTimeout, }); - const readJSON = (path: string) => - fsp.readFile(path, 'utf-8').then(x => JSON.parse(x)); + const readJSON = path => fsp.readFile(path, 'utf-8').then(x => JSON.parse(x)); const execFileP = promisify(childProcess.execFile); - const npx = (file: string, args: string[]) => - execFileP('npx', ['--no-install', file, ...args]); + const npx = (file, args) => execFileP('npx', ['--no-install', file, ...args]); const deployBuilder = makeDeployBuilder(tools, readJSON, npx); - try { - await deployBuilder(builder); - } catch (err) { - console.error(err); - process.exit(1); - } + + await deployBuilder(builder); } -main(); +main().catch(err => { + console.error(err); + process.exit(1); +}); From d736cb0ace991831a149fa2e009c79607b436424 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Mon, 4 Nov 2024 12:46:52 -0600 Subject: [PATCH 47/48] feat: support scriptArgs in deploy-cli --- packages/fast-usdc/contract/tools/deploy.js | 9 ++++++--- packages/fast-usdc/e2e-testing/scripts/deploy-cli.js | 6 +++--- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/fast-usdc/contract/tools/deploy.js b/packages/fast-usdc/contract/tools/deploy.js index c7350eec705..193feb742d1 100644 --- a/packages/fast-usdc/contract/tools/deploy.js +++ b/packages/fast-usdc/contract/tools/deploy.js @@ -8,11 +8,14 @@ * @param {(file: string, args: string[]) => Promise<{stdout: string}>} npx */ export const makeDeployBuilder = (tools, readJSON, npx) => - /** @param {string} builder */ - async function deployBuilder(builder) { + /** + * @param {string} builder + * @param {string[]} [scriptArgs] + */ + async function deployBuilder(builder, scriptArgs = []) { console.log(`building plan: ${builder}`); // build the plan - const { stdout } = await npx('agoric', ['run', builder]); + const { stdout } = await npx('agoric', ['run', builder, ...scriptArgs]); const match = stdout.match(/ (?[-\w]+)-permit.json/); if (!(match && match.groups)) { throw new Error('no permit found'); diff --git a/packages/fast-usdc/e2e-testing/scripts/deploy-cli.js b/packages/fast-usdc/e2e-testing/scripts/deploy-cli.js index bc146fb5701..ae2eda93da8 100644 --- a/packages/fast-usdc/e2e-testing/scripts/deploy-cli.js +++ b/packages/fast-usdc/e2e-testing/scripts/deploy-cli.js @@ -11,10 +11,10 @@ import { makeDeployBuilder } from '../../contract/tools/deploy.js'; import { makeE2ETools } from '../../contract/tools/e2e-tools.js'; async function main() { - const builder = process.argv[2]; + const [builder, ...scriptArgs] = process.argv.slice(2); if (!builder) { - console.error('USAGE: deploy-cli.ts '); + console.error('USAGE: deploy-cli.ts ...'); process.exit(1); } @@ -37,7 +37,7 @@ async function main() { const npx = (file, args) => execFileP('npx', ['--no-install', file, ...args]); const deployBuilder = makeDeployBuilder(tools, readJSON, npx); - await deployBuilder(builder); + await deployBuilder(builder, scriptArgs); } main().catch(err => { From 74d293b9fae15081b92e3876beba7ed6278e4b39 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Mon, 4 Nov 2024 12:47:04 -0600 Subject: [PATCH 48/48] KLUDGE: USDC_axl --- packages/fast-usdc/contract/start-quickSend.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/fast-usdc/contract/start-quickSend.js b/packages/fast-usdc/contract/start-quickSend.js index ee4638df39c..b6844c6b95f 100644 --- a/packages/fast-usdc/contract/start-quickSend.js +++ b/packages/fast-usdc/contract/start-quickSend.js @@ -73,8 +73,9 @@ export const startQuickSend = async ( await null; const USDC = { - brand: await E(agoricNames).lookup('brand', 'USDC'), - issuer: await E(agoricNames).lookup('issuer', 'USDC'), + // UNTIL mainnet 59 is available in agoric-3-proposal + brand: await E(agoricNames).lookup('brand', 'USDC_axl'), + issuer: await E(agoricNames).lookup('issuer', 'USDC_axl'), }; const terms = { makerFee: AmountMath.make(USDC.brand, 100n), // TODO: parameterize