From f1f07bb81e0b8aa2528d74424f77f765f97e780c Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Tue, 9 Jul 2024 16:51:07 -0400 Subject: [PATCH] test: kitchen sink fixture for async-flow continuing offers --- .../test/bootstrapTests/orchestration.test.ts | 45 ++++++++ .../orchestration/init-kitchen-sink.js | 25 +++++ .../src/examples/kitchen-sink.contract.js | 103 ++++++++++++++++++ .../src/exos/local-orchestration-account.js | 1 - .../src/proposals/start-kitchen-sink.js | 97 +++++++++++++++++ .../examples/kitchen-sink.contract.test.ts | 94 ++++++++++++++++ 6 files changed, 364 insertions(+), 1 deletion(-) create mode 100644 packages/builders/scripts/orchestration/init-kitchen-sink.js create mode 100644 packages/orchestration/src/examples/kitchen-sink.contract.js create mode 100644 packages/orchestration/src/proposals/start-kitchen-sink.js create mode 100644 packages/orchestration/test/examples/kitchen-sink.contract.test.ts diff --git a/packages/boot/test/bootstrapTests/orchestration.test.ts b/packages/boot/test/bootstrapTests/orchestration.test.ts index 841fb290fc5d..4a9c85b9cdb6 100644 --- a/packages/boot/test/bootstrapTests/orchestration.test.ts +++ b/packages/boot/test/bootstrapTests/orchestration.test.ts @@ -227,3 +227,48 @@ test.serial('revise chain info', async t => { client_id: '07-tendermint-3', }); }); + +test('kitchen-sink', async t => { + const { buildProposal, evalProposal, agoricNamesRemotes, readLatest } = + t.context; + + await evalProposal( + buildProposal( + '@agoric/builders/scripts/orchestration/init-kitchen-sink.js', + ), + ); + + const wd = + await t.context.walletFactoryDriver.provideSmartWallet('agoric1test'); + + await wd.executeOffer({ + id: 'request-coa', + invitationSpec: { + source: 'agoricContract', + instancePath: ['kitchenSink'], + callPipe: [['makeCosmosOrchAcctInvitation']], + }, + offerArgs: { + chainName: 'cosmoshub', + }, + proposal: {}, + }); + t.like(wd.getCurrentWalletRecord(), { + offerToPublicSubscriberPaths: [ + [ + 'request-coa', + { + // FIXME in this PR + account: { payload: { vowV0: { vowV0: undefined } } }, + // account: 'published.kitchenSink.cosmos1test', + }, + ], + ], + }); + t.like(wd.getLatestUpdateRecord(), { + status: { id: 'request-coa', numWantsSatisfied: 1 }, + }); + t.is(readLatest('published.kitchenSink'), ''); + // FIXME in this PR + t.not(readLatest('published.kitchenSink.cosmos1test'), ''); +}); diff --git a/packages/builders/scripts/orchestration/init-kitchen-sink.js b/packages/builders/scripts/orchestration/init-kitchen-sink.js new file mode 100644 index 000000000000..fd104e21c6a4 --- /dev/null +++ b/packages/builders/scripts/orchestration/init-kitchen-sink.js @@ -0,0 +1,25 @@ +import { makeHelpers } from '@agoric/deploy-script-support'; + +/** @type {import('@agoric/deploy-script-support/src/externalTypes.js').CoreEvalBuilder} */ +export const defaultProposalBuilder = async ({ publishRef, install }) => { + return harden({ + sourceSpec: '@agoric/orchestration/src/proposals/start-kitchen-sink.js', + getManifestCall: [ + 'getManifestForContract', + { + installKeys: { + kitchenSink: publishRef( + install( + '@agoric/orchestration/src/examples/kitchen-sink.contract.js', + ), + ), + }, + }, + ], + }); +}; + +export default async (homeP, endowments) => { + const { writeCoreEval } = await makeHelpers(homeP, endowments); + await writeCoreEval('start-kitchen-sink', defaultProposalBuilder); +}; diff --git a/packages/orchestration/src/examples/kitchen-sink.contract.js b/packages/orchestration/src/examples/kitchen-sink.contract.js new file mode 100644 index 000000000000..a38162cb95fc --- /dev/null +++ b/packages/orchestration/src/examples/kitchen-sink.contract.js @@ -0,0 +1,103 @@ +/** + * @file Primarily a testing fixture, but also serves as an example of how to + * leverage basic functionality of the Orchestration API with async-flow. + */ +import { InvitationShape } from '@agoric/zoe/src/typeGuards.js'; +import { M, mustMatch } from '@endo/patterns'; +import { provideOrchestration } from '../utils/start-helper.js'; + +/** + * @import {Baggage} from '@agoric/vat-data'; + * @import {Orchestrator} from '@agoric/orchestration'; + * @import {OrchestrationPowers} from '../utils/start-helper.js'; + */ + +/** + * Create an account on the local chain and return a continuing offer with + * invitations for Delegate, WithdrawRewards, Transfer, etc. + * + * @param {Orchestrator} orch + * @param {undefined} _ctx + * @param {ZCFSeat} seat + */ +const makeLocalOrchAcctHandler = async (orch, _ctx, seat) => { + seat.exit(); // no funds exchanged + const agoric = await orch.getChain('agoric'); + const localAccount = await agoric.makeAccount(); + // @ts-expect-error asContinuingOffer does not exist on OrchestrationAccountI + return localAccount.asContinuingOffer(); +}; + +/** + * Create an account on a Cosmos chain and return a continuing offer with + * invitations makers for Delegate, WithdrawRewards, Transfer, etc. + * + * @param {Orchestrator} orch + * @param {undefined} _ctx + * @param {ZCFSeat} seat + * @param {{ chainName: string }} offerArgs + */ +const makeCosmosOrchAcctHandler = async (orch, _ctx, seat, { chainName }) => { + seat.exit(); // no funds exchanged + mustMatch(chainName, M.string()); + const remoteChain = await orch.getChain(chainName); + const cosmosAccount = await remoteChain.makeAccount(); + // @ts-expect-error asContinuingOffer does not exist on OrchestrationAccountI + return cosmosAccount.asContinuingOffer(); +}; + +/** + * @param {ZCF} zcf + * @param {OrchestrationPowers & { + * marshaller: Marshaller; + * }} privateArgs + * @param {Baggage} baggage + */ +export const start = async (zcf, privateArgs, baggage) => { + const { orchestrate, zone } = provideOrchestration( + zcf, + baggage, + privateArgs, + privateArgs.marshaller, + ); + + /** @type {OfferHandler} */ + const makeLocalOrchAccount = orchestrate( + 'makeLocalAccount', + undefined, + makeLocalOrchAcctHandler, + ); + + /** @type {OfferHandler} */ + const makeCosmosOrchAccount = orchestrate( + 'makeCosmosICAAccount', + undefined, + makeCosmosOrchAcctHandler, + ); + + const publicFacet = zone.exo( + 'Kitchen Sink Public Facet', + M.interface('Kitchen Sink PF', { + makeLocalOrchAcctInvitation: M.callWhen().returns(InvitationShape), + makeCosmosOrchAcctInvitation: M.callWhen().returns(InvitationShape), + }), + { + makeLocalOrchAcctInvitation() { + return zcf.makeInvitation( + makeLocalOrchAccount, + 'Make Local Orchestration Account', + ); + }, + makeCosmosOrchAcctInvitation() { + return zcf.makeInvitation( + makeCosmosOrchAccount, + 'Make Cosmos Orchestration Account', + ); + }, + }, + ); + + return { publicFacet }; +}; + +/** @typedef {typeof start} KitchenSinkSF */ diff --git a/packages/orchestration/src/exos/local-orchestration-account.js b/packages/orchestration/src/exos/local-orchestration-account.js index 52c95c9580d6..94e8d3a1368f 100644 --- a/packages/orchestration/src/exos/local-orchestration-account.js +++ b/packages/orchestration/src/exos/local-orchestration-account.js @@ -318,7 +318,6 @@ export const prepareLocalOrchestrationAccountKit = ( // TODO https://github.com/Agoric/agoric-sdk/issues/9610 return asVow(() => Fail`not yet implemented`); }, - getPublicTopics() { const { topicKit } = this.state; return harden({ diff --git a/packages/orchestration/src/proposals/start-kitchen-sink.js b/packages/orchestration/src/proposals/start-kitchen-sink.js new file mode 100644 index 000000000000..fede33d43517 --- /dev/null +++ b/packages/orchestration/src/proposals/start-kitchen-sink.js @@ -0,0 +1,97 @@ +/** + * @file A proposal to start the kitchen sink contract. + */ +import { makeTracer } from '@agoric/internal'; +import { makeStorageNodeChild } from '@agoric/internal/src/lib-chainStorage.js'; +import { E } from '@endo/far'; + +/** + * @import {KitchenSinkSF} from '../examples/kitchen-sink.contract.js'; + */ + +const trace = makeTracer('StartKitchenSink', true); +const contractName = 'kitchenSink'; + +/** + * See `@agoric/builders/builders/scripts/orchestration/init-kitchen-sink.js` + * for the accompanying proposal builder. Run `agoric run + * packages/builders/scripts/orchestration/init-kitchen-sink.js` to build the + * contract and proposal files. + * + * @param {BootstrapPowers} powers + */ +export const startKitchenSink = async ({ + consume: { + agoricNames, + board, + chainStorage, + chainTimerService, + cosmosInterchainService, + localchain, + startUpgradable, + }, + installation: { + // @ts-expect-error not a WellKnownName + consume: { [contractName]: installation }, + }, + instance: { + // @ts-expect-error not a WellKnownName + produce: { [contractName]: produceInstance }, + }, +}) => { + trace(`start ${contractName}`); + await null; + + const storageNode = await makeStorageNodeChild(chainStorage, contractName); + const marshaller = await E(board).getPublishingMarshaller(); + + /** @type {StartUpgradableOpts} */ + const startOpts = { + label: 'kitchenSink', + installation, + terms: undefined, + privateArgs: { + agoricNames: await agoricNames, + orchestrationService: await cosmosInterchainService, + localchain: await localchain, + storageNode, + marshaller, + timerService: await chainTimerService, + }, + }; + + const { instance } = await E(startUpgradable)(startOpts); + produceInstance.resolve(instance); +}; +harden(startKitchenSink); + +export const getManifestForContract = ( + { restoreRef }, + { installKeys, ...options }, +) => { + return { + manifest: { + [startKitchenSink.name]: { + consume: { + agoricNames: true, + board: true, + chainStorage: true, + chainTimerService: true, + cosmosInterchainService: true, + localchain: true, + startUpgradable: true, + }, + installation: { + consume: { [contractName]: true }, + }, + instance: { + produce: { [contractName]: true }, + }, + }, + }, + installations: { + [contractName]: restoreRef(installKeys[contractName]), + }, + options, + }; +}; diff --git a/packages/orchestration/test/examples/kitchen-sink.contract.test.ts b/packages/orchestration/test/examples/kitchen-sink.contract.test.ts new file mode 100644 index 000000000000..d38d13850b38 --- /dev/null +++ b/packages/orchestration/test/examples/kitchen-sink.contract.test.ts @@ -0,0 +1,94 @@ +import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js'; +import { setUpZoeForTest } from '@agoric/zoe/tools/setup-zoe.js'; +import { E, getInterfaceOf } from '@endo/far'; +import path from 'path'; +import { commonSetup } from '../supports.js'; + +const dirname = path.dirname(new URL(import.meta.url).pathname); + +const contractName = 'kitchen-sink'; +const contractFile = `${dirname}/../../src/examples/${contractName}.contract.js`; +type StartFn = + typeof import('../../src/examples/kitchen-sink.contract.js').start; + +test('orchestrate - LocalOrchAccount returns a ContinuingOfferResult', async t => { + t.log('bootstrap, orchestration core-eval'); + const { + bootstrap: { storage }, + commonPrivateArgs, + } = await commonSetup(t); + + const { zoe, bundleAndInstall } = await setUpZoeForTest(); + + t.log('contract coreEval', contractName); + + const installation: Installation = + await bundleAndInstall(contractFile); + + const storageNode = await E(storage.rootNode).makeChildNode(contractName); + const kitchenSinkKit = await E(zoe).startInstance( + installation, + undefined, + {}, + { ...commonPrivateArgs, storageNode }, + ); + + const publicFacet = await E(zoe).getPublicFacet(kitchenSinkKit.instance); + const inv = E(publicFacet).makeLocalOrchAcctInvitation(); + const userSeat = E(zoe).offer(inv, {}); + // @ts-expect-error TODO: type expected offer result + const { holder, invitationMakers, publicSubscribers } = + await E(userSeat).getOfferResult(); + + t.regex(getInterfaceOf(holder)!, /Local Orchestration (.*) holder/); + t.regex( + getInterfaceOf(invitationMakers)!, + /Local Orchestration (.*) invitationMakers/, + ); + const { description, storagePath, subscriber } = publicSubscribers.account; + t.regex(description, /Account holder/); + // FIXME in this PR, currently a vow + t.is(getInterfaceOf(storagePath), undefined); + t.regex(getInterfaceOf(subscriber)!, /Durable Publish Kit subscriber/); +}); + +test('orchestrate - CosmosOrchAccount returns a ContinuingOfferResult', async t => { + t.log('bootstrap, orchestration core-eval'); + const { + bootstrap: { storage }, + commonPrivateArgs, + } = await commonSetup(t); + + const { zoe, bundleAndInstall } = await setUpZoeForTest(); + + t.log('contract coreEval', contractName); + + const installation: Installation = + await bundleAndInstall(contractFile); + + const storageNode = await E(storage.rootNode).makeChildNode(contractName); + const kitchenSinkKit = await E(zoe).startInstance( + installation, + undefined, + {}, + { ...commonPrivateArgs, storageNode }, + ); + + const publicFacet = await E(zoe).getPublicFacet(kitchenSinkKit.instance); + const inv = E(publicFacet).makeCosmosOrchAcctInvitation(); + const userSeat = E(zoe).offer(inv, {}, undefined, { chainName: 'cosmoshub' }); + // @ts-expect-error TODO: type expected offer result + const { holder, invitationMakers, publicSubscribers } = + await E(userSeat).getOfferResult(); + + t.regex(getInterfaceOf(holder)!, /Staking Account (.*) holder/); + t.regex( + getInterfaceOf(invitationMakers)!, + /Staking Account (.*) invitationMakers/, + ); + const { description, storagePath, subscriber } = publicSubscribers.account; + t.regex(description, /Account holder/); + // FIXME in this PR, currently a vow + t.is(getInterfaceOf(storagePath), undefined); + t.regex(getInterfaceOf(subscriber)!, /Durable Publish Kit subscriber/); +});