From e1cc43a49ff239fd40e36a2405f66e1035c2252a Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Tue, 16 Jul 2024 17:57:35 -0400 Subject: [PATCH] test: bootstrap test for portfolio holder - adds portofolio holder to basic-flows.contract.js and tests wallet offers in bootstrap environment --- .../test/bootstrapTests/orchestration.test.ts | 136 +++++++++++++++++- .../src/examples/basic-flows.contract.js | 86 +++++++++-- 2 files changed, 205 insertions(+), 17 deletions(-) diff --git a/packages/boot/test/bootstrapTests/orchestration.test.ts b/packages/boot/test/bootstrapTests/orchestration.test.ts index 2817b8a8c776..c73b3073b87d 100644 --- a/packages/boot/test/bootstrapTests/orchestration.test.ts +++ b/packages/boot/test/bootstrapTests/orchestration.test.ts @@ -13,6 +13,12 @@ import { const test: TestFn = anyTest; +const validatorAddress: CosmosValidatorAddress = { + value: 'cosmosvaloper1test', + chainId: 'gaiatest', + encoding: 'bech32', +}; + test.before(async t => { t.context = await makeWalletFactoryContext( t, @@ -163,11 +169,6 @@ test.serial('stakeAtom - smart wallet', async t => { const { ATOM } = agoricNamesRemotes.brand; ATOM || Fail`ATOM missing from agoricNames`; - const validatorAddress: CosmosValidatorAddress = { - value: 'cosmosvaloper1test', - chainId: 'gaiatest', - encoding: 'bech32', - }; await t.notThrowsAsync( wd.executeOffer({ @@ -320,3 +321,128 @@ test.serial('auto-stake-it - proposal', async t => { ), ); }); + +test.serial('basic-flows - portfolio holder', async t => { + const { buildProposal, evalProposal, readLatest, agoricNamesRemotes } = + t.context; + + await evalProposal( + buildProposal('@agoric/builders/scripts/orchestration/init-basic-flows.js'), + ); + + const wd = + await t.context.walletFactoryDriver.provideSmartWallet('agoric1test2'); + + // create a cosmos orchestration account + await wd.executeOffer({ + id: 'request-portfolio-acct', + invitationSpec: { + source: 'agoricContract', + instancePath: ['basicFlows'], + callPipe: [['makePortfolioAccountInvitation']], + }, + offerArgs: { + chainNames: ['agoric', 'cosmoshub', 'osmosis'], + }, + proposal: {}, + }); + t.like(wd.getCurrentWalletRecord(), { + offerToPublicSubscriberPaths: [ + [ + 'request-portfolio-acct', + { + agoric: 'published.basicFlows.agoric1mockVlocalchainAddress', + cosmoshub: 'published.basicFlows.cosmos1test', + // XXX support multiple chain addresses in ibc mocks + osmosis: 'published.basicFlows.cosmos1test', + }, + ], + ], + }); + t.like(wd.getLatestUpdateRecord(), { + status: { id: 'request-portfolio-acct', numWantsSatisfied: 1 }, + }); + // XXX this overrides a previous account, since mocks only provide one address + t.is(readLatest('published.basicFlows.cosmos1test'), ''); + // XXX this overrides a previous account, since mocks only provide one address + t.is(readLatest('published.basicFlows.agoric1mockVlocalchainAddress'), ''); + + const { ATOM, BLD } = agoricNamesRemotes.brand; + ATOM || Fail`ATOM missing from agoricNames`; + BLD || Fail`BLD missing from agoricNames`; + + await t.notThrowsAsync( + wd.executeOffer({ + id: 'delegate-cosmoshub', + invitationSpec: { + source: 'continuing', + previousOffer: 'request-portfolio-acct', + invitationMakerName: 'MakeInvitation', + invitationArgs: [ + 'cosmoshub', + 'Delegate', + [validatorAddress, { brand: ATOM, value: 10n }], + ], + }, + proposal: {}, + }), + ); + t.like(wd.getLatestUpdateRecord(), { + status: { id: 'delegate-cosmoshub', numWantsSatisfied: 1 }, + }); + + await t.notThrowsAsync( + wd.executeOffer({ + id: 'delegate-agoric', + invitationSpec: { + source: 'continuing', + previousOffer: 'request-portfolio-acct', + invitationMakerName: 'MakeInvitation', + invitationArgs: [ + 'agoric', + 'Delegate', + // XXX use ChainAddress for LocalOrchAccount + ['agoric1validator1', { brand: BLD, value: 10n }], + ], + }, + proposal: {}, + }), + ); + t.like(wd.getLatestUpdateRecord(), { + status: { id: 'delegate-agoric', numWantsSatisfied: 1 }, + }); + + await t.throwsAsync( + wd.executeOffer({ + id: 'delegate-2-cosmoshub', + invitationSpec: { + source: 'continuing', + previousOffer: 'request-portfolio-acct', + invitationMakerName: 'MakeInvitation', + invitationArgs: [ + 'cosmoshub', + 'Delegate', + [validatorAddress, { brand: ATOM, value: 504n }], + ], + }, + proposal: {}, + }), + ); + + await t.throwsAsync( + wd.executeOffer({ + id: 'delegate-2-agoric', + invitationSpec: { + source: 'continuing', + previousOffer: 'request-portfolio-acct', + invitationMakerName: 'MakeInvitation', + invitationArgs: [ + 'agoric', + 'Delegate', + ['agoric1validator1', { brand: BLD, value: 504n }], + ], + }, + proposal: {}, + }), + ); +}); diff --git a/packages/orchestration/src/examples/basic-flows.contract.js b/packages/orchestration/src/examples/basic-flows.contract.js index d3b8a60d71ee..78928843cbad 100644 --- a/packages/orchestration/src/examples/basic-flows.contract.js +++ b/packages/orchestration/src/examples/basic-flows.contract.js @@ -4,13 +4,16 @@ */ import { InvitationShape } from '@agoric/zoe/src/typeGuards.js'; import { M, mustMatch } from '@endo/patterns'; -import { provideOrchestration } from '../utils/start-helper.js'; +import { withOrchestration } from '../utils/start-helper.js'; +import { preparePortfolioHolder } from '../exos/portfolio-holder-kit.js'; /** - * @import {Baggage} from '@agoric/vat-data'; - * @import {Orchestrator} from '@agoric/orchestration'; - * @import {Vow, VowTools} from '@agoric/vow'; + * @import {Zone} from '@agoric/zone'; + * @import {OrchestrationAccount, Orchestrator} from '@agoric/orchestration'; + * @import {ResolvedPublicTopic} from '@agoric/zoe/src/contractSupport/topics.js'; * @import {OrchestrationPowers} from '../utils/start-helper.js'; + * @import {MakePortfolioHolder} from '../exos/portfolio-holder-kit.js'; + * @import {OrchestrationTools} from '../utils/start-helper.js'; */ /** @@ -30,19 +33,63 @@ const makeOrchAccountHandler = async (orch, _ctx, seat, { chainName }) => { return cosmosAccount.asContinuingOffer(); }; +/** + * Create accounts on multiple chains and return them in a single continuing + * offer with invitations makers for Delegate, WithdrawRewards, Transfer, etc. + * Calls to the underlying invitationMakers are proxied through the + * `MakeInvitation` invitation maker. + * + * @param {Orchestrator} orch + * @param {MakePortfolioHolder} makePortfolioHolder + * @param {ZCFSeat} seat + * @param {{ chainNames: string[] }} offerArgs + */ +const makePortfolioAcctHandler = async ( + orch, + makePortfolioHolder, + seat, + { chainNames }, +) => { + seat.exit(); // no funds exchanged + mustMatch(chainNames, M.arrayOf(M.string())); + const allChains = await Promise.all(chainNames.map(n => orch.getChain(n))); + const allAccounts = await Promise.all(allChains.map(c => c.makeAccount())); + + const accountEntries = harden( + /** @type {[string, OrchestrationAccount][]} */ ( + chainNames.map((chainName, index) => [chainName, allAccounts[index]]) + ), + ); + const publicTopicEntries = harden( + /** @type {[string, ResolvedPublicTopic][]} */ ( + await Promise.all( + accountEntries.map(async ([name, account]) => { + const { account: topicRecord } = await account.getPublicTopics(); + return [name, topicRecord]; + }), + ) + ), + ); + const portfolioHolder = makePortfolioHolder( + accountEntries, + publicTopicEntries, + ); + + return portfolioHolder.asContinuingOffer(); +}; + /** * @param {ZCF} zcf * @param {OrchestrationPowers & { * marshaller: Marshaller; - * }} privateArgs - * @param {Baggage} baggage + * }} _privateArgs + * @param {Zone} zone + * @param {OrchestrationTools} tools */ -export const start = async (zcf, privateArgs, baggage) => { - const { orchestrate, zone } = provideOrchestration( - zcf, - baggage, - privateArgs, - privateArgs.marshaller, +const contract = async (zcf, _privateArgs, zone, { orchestrate, vowTools }) => { + const makePortfolioHolder = preparePortfolioHolder( + zone.subZone('portfolio'), + vowTools, ); const makeOrchAccount = orchestrate( @@ -51,10 +98,17 @@ export const start = async (zcf, privateArgs, baggage) => { makeOrchAccountHandler, ); + const makePortfolioAccount = orchestrate( + 'makePortfolioAccount', + makePortfolioHolder, + makePortfolioAcctHandler, + ); + const publicFacet = zone.exo( 'Basic Flows Public Facet', M.interface('Basic Flows PF', { makeOrchAccountInvitation: M.callWhen().returns(InvitationShape), + makePortfolioAccountInvitation: M.callWhen().returns(InvitationShape), }), { makeOrchAccountInvitation() { @@ -63,10 +117,18 @@ export const start = async (zcf, privateArgs, baggage) => { 'Make an Orchestration Account', ); }, + makePortfolioAccountInvitation() { + return zcf.makeInvitation( + makePortfolioAccount, + 'Make an Orchestration Account', + ); + }, }, ); return { publicFacet }; }; +export const start = withOrchestration(contract); + /** @typedef {typeof start} BasicFlowsSF */