diff --git a/packages/boot/test/bootstrapTests/addAssets.test.ts b/packages/boot/test/bootstrapTests/addAssets.test.ts index 9cdaea18c6d..00173ea587e 100644 --- a/packages/boot/test/bootstrapTests/addAssets.test.ts +++ b/packages/boot/test/bootstrapTests/addAssets.test.ts @@ -61,7 +61,7 @@ test('addAsset to quiescent auction', async t => { const { liveAuctionSchedule, nextAuctionSchedule } = schedules; const nextEndTime = liveAuctionSchedule ? liveAuctionSchedule.endTime - : nextAuctionSchedule.endTime; + : nextAuctionSchedule!.endTime; const fiveMinutes = harden({ relValue: 5n * 60n, timerBrand: nextEndTime.timerBrand, @@ -83,7 +83,7 @@ test('addAsset to active auction', async t => { const auctioneerKit = await EV.vat('bootstrap').consumeItem('auctioneerKit'); const schedules = await EV(auctioneerKit.creatorFacet).getSchedule(); const { nextAuctionSchedule } = schedules; - t.truthy(nextAuctionSchedule); + assert(nextAuctionSchedule); const nextStartTime = nextAuctionSchedule.startTime; const fiveMinutes = harden({ relValue: 5n * 60n, @@ -105,9 +105,10 @@ test('addAsset to active auction', async t => { const coreEvalBridgeHandler = await EV.vat('bootstrap').consumeItem( 'coreEvalBridgeHandler', ); - EV(coreEvalBridgeHandler).fromBridge(bridgeMessage); + // XXX races with the following lines + void EV(coreEvalBridgeHandler).fromBridge(bridgeMessage); - const nextEndTime = nextAuctionSchedule.endTime; + const nextEndTime = nextAuctionSchedule!.endTime; const afterEndTime = TimeMath.addAbsRel(nextEndTime, fiveMinutes); await advanceTimeTo(afterEndTime); t.log('proposal executed'); @@ -115,8 +116,8 @@ test('addAsset to active auction', async t => { const schedulesAfter = await EV(auctioneerKit.creatorFacet).getSchedule(); // TimeMath.compareAbs() can't handle brands processed by kmarshall t.truthy( - schedules.nextAuctionSchedule.endTime.absValue < - schedulesAfter.nextAuctionSchedule.endTime.absValue, + schedules.nextAuctionSchedule!.endTime.absValue < + schedulesAfter.nextAuctionSchedule!.endTime.absValue, ); t.like(readLatest(`${auctioneerPath}.book1`), { currentPriceLevel: null }); @@ -129,7 +130,7 @@ test('addAsset to auction starting soon', async t => { const auctioneerKit = await EV.vat('bootstrap').consumeItem('auctioneerKit'); const schedules = await EV(auctioneerKit.creatorFacet).getSchedule(); const { nextAuctionSchedule } = schedules; - t.truthy(nextAuctionSchedule); + assert(nextAuctionSchedule); const nextStartTime = nextAuctionSchedule.startTime; const fiveMinutes = harden({ relValue: 5n * 60n, @@ -150,7 +151,8 @@ test('addAsset to auction starting soon', async t => { const coreEvalBridgeHandler = await EV.vat('bootstrap').consumeItem( 'coreEvalBridgeHandler', ); - EV(coreEvalBridgeHandler).fromBridge(bridgeMessage); + // XXX races with the following lines + void EV(coreEvalBridgeHandler).fromBridge(bridgeMessage); const nextEndTime = nextAuctionSchedule.endTime; const afterEndTime = TimeMath.addAbsRel(nextEndTime, fiveMinutes); @@ -160,8 +162,8 @@ test('addAsset to auction starting soon', async t => { const schedulesAfter = await EV(auctioneerKit.creatorFacet).getSchedule(); t.truthy( - schedules.nextAuctionSchedule.endTime.absValue < - schedulesAfter.nextAuctionSchedule.endTime.absValue, + schedules.nextAuctionSchedule!.endTime.absValue < + schedulesAfter.nextAuctionSchedule!.endTime.absValue, ); t.like(readLatest(`${auctioneerPath}.book1`), { currentPriceLevel: null }); }); diff --git a/packages/boot/test/bootstrapTests/orchestration.test.ts b/packages/boot/test/bootstrapTests/orchestration.test.ts index c73b3073b87..ecaeceeb0c7 100644 --- a/packages/boot/test/bootstrapTests/orchestration.test.ts +++ b/packages/boot/test/bootstrapTests/orchestration.test.ts @@ -132,8 +132,13 @@ test.skip('stakeOsmo - queries', async t => { }); test.serial('stakeAtom - smart wallet', async t => { - const { buildProposal, evalProposal, agoricNamesRemotes, readLatest } = - t.context; + const { + buildProposal, + evalProposal, + agoricNamesRemotes, + flushInboundQueue, + readLatest, + } = t.context; await evalProposal( buildProposal('@agoric/builders/scripts/orchestration/init-stakeAtom.js'), @@ -143,7 +148,7 @@ test.serial('stakeAtom - smart wallet', async t => { 'agoric1testStakAtom', ); - await wd.executeOffer({ + await wd.sendOffer({ id: 'request-account', invitationSpec: { source: 'agoricContract', @@ -152,6 +157,7 @@ test.serial('stakeAtom - smart wallet', async t => { }, proposal: {}, }); + await flushInboundQueue(); t.like(wd.getCurrentWalletRecord(), { offerToPublicSubscriberPaths: [ [ @@ -170,18 +176,18 @@ test.serial('stakeAtom - smart wallet', async t => { const { ATOM } = agoricNamesRemotes.brand; ATOM || Fail`ATOM missing from agoricNames`; - await t.notThrowsAsync( - wd.executeOffer({ - id: 'request-delegate-success', - invitationSpec: { - source: 'continuing', - previousOffer: 'request-account', - invitationMakerName: 'Delegate', - invitationArgs: [validatorAddress, { brand: ATOM, value: 10n }], - }, - proposal: {}, - }), - ); + // Cannot await executeOffer because the offer won't resolve until after the bridge's inbound queue is flushed. + // But this test doesn't require that. + await wd.sendOffer({ + id: 'request-delegate-success', + invitationSpec: { + source: 'continuing', + previousOffer: 'request-account', + invitationMakerName: 'Delegate', + invitationArgs: [validatorAddress, { brand: ATOM, value: 10n }], + }, + proposal: {}, + }); t.like(wd.getLatestUpdateRecord(), { status: { id: 'request-delegate-success', numWantsSatisfied: 1 }, }); @@ -192,6 +198,7 @@ test.serial('stakeAtom - smart wallet', async t => { encoding: 'bech32', }; + // This will trigger the immediate ack of the mock bridge await t.throwsAsync( wd.executeOffer({ id: 'request-delegate-fail', @@ -248,8 +255,13 @@ test.serial('revise chain info', async t => { }); test('basic-flows', async t => { - const { buildProposal, evalProposal, agoricNamesRemotes, readLatest } = - t.context; + const { + buildProposal, + evalProposal, + agoricNamesRemotes, + readLatest, + flushInboundQueue, + } = t.context; await evalProposal( buildProposal('@agoric/builders/scripts/orchestration/init-basic-flows.js'), @@ -259,7 +271,7 @@ test('basic-flows', async t => { await t.context.walletFactoryDriver.provideSmartWallet('agoric1test'); // create a cosmos orchestration account - await wd.executeOffer({ + await wd.sendOffer({ id: 'request-coa', invitationSpec: { source: 'agoricContract', @@ -271,6 +283,7 @@ test('basic-flows', async t => { }, proposal: {}, }); + await flushInboundQueue(); t.like(wd.getCurrentWalletRecord(), { offerToPublicSubscriberPaths: [ [ @@ -323,8 +336,13 @@ test.serial('auto-stake-it - proposal', async t => { }); test.serial('basic-flows - portfolio holder', async t => { - const { buildProposal, evalProposal, readLatest, agoricNamesRemotes } = - t.context; + const { + buildProposal, + evalProposal, + readLatest, + agoricNamesRemotes, + flushInboundQueue, + } = t.context; await evalProposal( buildProposal('@agoric/builders/scripts/orchestration/init-basic-flows.js'), @@ -334,7 +352,7 @@ test.serial('basic-flows - portfolio holder', async t => { await t.context.walletFactoryDriver.provideSmartWallet('agoric1test2'); // create a cosmos orchestration account - await wd.executeOffer({ + await wd.sendOffer({ id: 'request-portfolio-acct', invitationSpec: { source: 'agoricContract', @@ -346,6 +364,14 @@ test.serial('basic-flows - portfolio holder', async t => { }, proposal: {}, }); + t.like( + wd.getLatestUpdateRecord(), + { + status: { id: 'request-portfolio-acct', numWantsSatisfied: 1 }, + }, + 'trivially satisfied', + ); + await flushInboundQueue(); t.like(wd.getCurrentWalletRecord(), { offerToPublicSubscriberPaths: [ [ @@ -371,43 +397,39 @@ test.serial('basic-flows - portfolio holder', async t => { 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: {}, - }), - ); + await wd.sendOffer({ + 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: {}, - }), - ); + await wd.sendOffer({ + 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 }, }); diff --git a/packages/boot/test/bootstrapTests/vats-restart.test.ts b/packages/boot/test/bootstrapTests/vats-restart.test.ts index 0655b96ec2c..e8b5eac33dd 100644 --- a/packages/boot/test/bootstrapTests/vats-restart.test.ts +++ b/packages/boot/test/bootstrapTests/vats-restart.test.ts @@ -2,19 +2,12 @@ import { test as anyTest } from '@agoric/zoe/tools/prepare-test-env-ava.js'; import { TestFn } from 'ava'; -import processAmbient from 'child_process'; -import { promises as fsAmbientPromises } from 'fs'; - import { Fail } from '@endo/errors'; import { eventLoopIteration } from '@agoric/internal/src/testing-utils.js'; import { Offers } from '@agoric/inter-protocol/src/clientSupport.js'; import { makeAgoricNamesRemotesFromFakeStorage } from '@agoric/vats/tools/board-utils.js'; import { BridgeHandler, ScopedBridgeManager } from '@agoric/vats'; -import type { EconomyBootstrapSpace } from '@agoric/inter-protocol/src/proposals/econ-behaviors.js'; -import { - makeProposalExtractor, - makeSwingsetTestKit, -} from '../../tools/supports.ts'; +import { makeSwingsetTestKit } from '../../tools/supports.ts'; import { makeWalletFactoryDriver } from '../../tools/drivers.ts'; // main/production config doesn't have initialPrice, upon which 'open vaults' depends @@ -132,9 +125,8 @@ test.serial('use IBC callbacks after upgrade', async t => { test.serial('read metrics', async t => { const { EV } = t.context.runUtils; - const vaultFactoryKit: Awaited< - EconomyBootstrapSpace['consume']['vaultFactoryKit'] - > = await EV.vat('bootstrap').consumeItem('vaultFactoryKit'); + const vaultFactoryKit = + await EV.vat('bootstrap').consumeItem('vaultFactoryKit'); const vfTopics = await EV(vaultFactoryKit.publicFacet).getPublicTopics(); diff --git a/packages/boot/test/bootstrapTests/vaults-upgrade.test.ts b/packages/boot/test/bootstrapTests/vaults-upgrade.test.ts index d1f8c29429d..324a7e2fdb9 100644 --- a/packages/boot/test/bootstrapTests/vaults-upgrade.test.ts +++ b/packages/boot/test/bootstrapTests/vaults-upgrade.test.ts @@ -15,7 +15,6 @@ import { SECONDS_PER_YEAR } from '@agoric/inter-protocol/src/interest.js'; import { makeAgoricNamesRemotesFromFakeStorage } from '@agoric/vats/tools/board-utils.js'; import { ExecutionContext, TestFn } from 'ava'; import { FakeStorageKit } from '@agoric/internal/src/storage-test-utils.js'; -import { EconomyBootstrapSpace } from '@agoric/inter-protocol/src/proposals/econ-behaviors.js'; import { makeSwingsetTestKit } from '../../tools/supports.ts'; import { makeWalletFactoryDriver } from '../../tools/drivers.ts'; @@ -284,17 +283,14 @@ test.serial('open vault', async t => { test.serial('restart vaultFactory', async t => { const { runUtils, readCollateralMetrics } = t.context.shared; const { EV } = runUtils; - const vaultFactoryKit = await (EV.vat('bootstrap').consumeItem( - 'vaultFactoryKit', - ) as EconomyBootstrapSpace['consume']['vaultFactoryKit']); + const vaultFactoryKit = + await EV.vat('bootstrap').consumeItem('vaultFactoryKit'); - const reserveKit = await (EV.vat('bootstrap').consumeItem( - 'reserveKit', - ) as EconomyBootstrapSpace['consume']['reserveKit']); + const reserveKit = await EV.vat('bootstrap').consumeItem('reserveKit'); const bootstrapVat = EV.vat('bootstrap'); - const electorateCreatorFacet = await (bootstrapVat.consumeItem( + const electorateCreatorFacet = await bootstrapVat.consumeItem( 'economicCommitteeCreatorFacet', - ) as EconomyBootstrapSpace['consume']['economicCommitteeCreatorFacet']); + ); const poserInvitation = await EV(electorateCreatorFacet).getPoserInvitation(); const creatorFacet1 = await EV.get(reserveKit).creatorFacet; @@ -327,9 +323,8 @@ test.serial('restart vaultFactory', async t => { test.serial('restart contractGovernor', async t => { const { EV } = t.context.shared.runUtils; - const vaultFactoryKit = await (EV.vat('bootstrap').consumeItem( - 'vaultFactoryKit', - ) as EconomyBootstrapSpace['consume']['vaultFactoryKit']); + const vaultFactoryKit = + await EV.vat('bootstrap').consumeItem('vaultFactoryKit'); const { governorAdminFacet } = vaultFactoryKit; // has no privateArgs of its own. the privateArgs.governed is only for the @@ -498,9 +493,10 @@ test.serial( await EV.vat('bootstrap').consumeItem('powerStore'); const getStoreSnapshot = async (name: string) => - EV.vat('bootstrap').snapshotStore( - await EV(powerStore).get(name), - ) as Promise<[any, any][]>; + EV.vat('bootstrap').snapshotStore(await EV(powerStore).get(name)) as [ + any, + any, + ][]; const contractKits = await getStoreSnapshot('contractKits'); // TODO refactor the entries to go into governedContractKits too (so the latter is sufficient to test) diff --git a/packages/boot/test/orchestration/restart-contracts.test.ts b/packages/boot/test/orchestration/restart-contracts.test.ts new file mode 100644 index 00000000000..96013a65b59 --- /dev/null +++ b/packages/boot/test/orchestration/restart-contracts.test.ts @@ -0,0 +1,169 @@ +/** @file Bootstrap test of restarting contracts using orchestration */ +import { test as anyTest } from '@agoric/zoe/tools/prepare-test-env-ava.js'; +import { TestFn } from 'ava'; + +import type { CosmosValidatorAddress } from '@agoric/orchestration'; +import type { UpdateRecord } from '@agoric/smart-wallet/src/smartWallet.js'; +import { + makeWalletFactoryContext, + type WalletFactoryTestContext, +} from '../bootstrapTests/walletFactory.ts'; + +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?.()); + +// Not interesting because it doesn't wait on other chains. Leaving here because maybe it will before it's done. +test.serial('sendAnywhere', async t => { + const { + walletFactoryDriver, + buildProposal, + evalProposal, + flushInboundQueue, + } = t.context; + + const { IST } = t.context.agoricNamesRemotes.brand; + + t.log('start sendAnywhere'); + await evalProposal( + buildProposal('@agoric/builders/scripts/testing/start-sendAnywhere.js'), + ); + + t.log('making offer'); + const wallet = await walletFactoryDriver.provideSmartWallet('agoric1test'); + // no money in wallet to actually send + const zero = { brand: IST, value: 0n }; + // send because it won't resolve + await wallet.sendOffer({ + id: 'send-somewhere', + invitationSpec: { + source: 'agoricContract', + instancePath: ['sendAnywhere'], + callPipe: [['makeSendInvitation']], + }, + proposal: { + // @ts-expect-error XXX BoardRemote + give: { Send: zero }, + }, + offerArgs: { + // meaningless address + destAddr: 'cosmos1qy352eufjjmc9c', + chainName: 'cosmoshub', + }, + }); + // no errors and no resolution + const beforeFlush = wallet.getLatestUpdateRecord(); + t.like(wallet.getLatestUpdateRecord(), { + updated: 'offerStatus', + status: { + id: 'send-somewhere', + error: undefined, + }, + numWantsSatisfied: undefined, + payouts: undefined, + result: undefined, + }); + + t.is(await flushInboundQueue(), 0); + t.deepEqual(wallet.getLatestUpdateRecord(), beforeFlush); + + t.log('restart sendAnywhere'); + await evalProposal( + buildProposal('@agoric/builders/scripts/testing/restart-sendAnywhere.js'), + ); + + const conclusion = wallet.getLatestUpdateRecord(); + console.log('conclusion', conclusion); + t.like(conclusion, { + updated: 'offerStatus', + status: { + id: 'send-somewhere', + error: undefined, + }, + numWantsSatisfied: undefined, + payouts: undefined, + result: undefined, + }); + + await flushInboundQueue(); + + // Nothing interesting to confirm here. +}); + +const validatorAddress: CosmosValidatorAddress = { + value: 'cosmosvaloper1test', + chainId: 'gaiatest', + encoding: 'bech32', +}; + +// check for key because the value will be 'undefined' when the result is provided +// TODO should it be something truthy? +const hasResult = (r: UpdateRecord) => { + assert(r.updated === 'offerStatus'); + return 'result' in r.status; +}; + +// Tests restart but not of an orchestration() flow +test('stakeAtom', async t => { + const { + buildProposal, + evalProposal, + agoricNamesRemotes, + flushInboundQueue, + readLatest, + } = t.context; + + await evalProposal( + buildProposal('@agoric/builders/scripts/orchestration/init-stakeAtom.js'), + ); + + const wd = await t.context.walletFactoryDriver.provideSmartWallet( + 'agoric1testStakAtom', + ); + + await wd.sendOffer({ + id: 'request-account', + invitationSpec: { + source: 'agoricContract', + instancePath: ['stakeAtom'], + callPipe: [['makeAccountInvitationMaker']], + }, + proposal: {}, + }); + // cosmos1test is from ibc/mocks.js + const accountPath = 'published.stakeAtom.accounts.cosmos1test'; + t.throws(() => readLatest(accountPath)); + t.is(await flushInboundQueue(), 1); + t.is(readLatest(accountPath), ''); + // request-account is complete + + const { ATOM } = agoricNamesRemotes.brand; + assert(ATOM); + + await wd.sendOffer({ + id: 'request-delegate', + invitationSpec: { + source: 'continuing', + previousOffer: 'request-account', + invitationMakerName: 'Delegate', + invitationArgs: [validatorAddress, { brand: ATOM, value: 10n }], + }, + proposal: {}, + }); + // no result yet because the IBC incoming messages haven't arrived + // and won't until we flush. + t.false(hasResult(wd.getLatestUpdateRecord())); + + t.log('restart stakeAtom'); + await evalProposal( + buildProposal('@agoric/builders/scripts/testing/restart-stakeAtom.js'), + ); + + t.is(await flushInboundQueue(), 1); + t.true(hasResult(wd.getLatestUpdateRecord())); +}); diff --git a/packages/boot/tools/drivers.ts b/packages/boot/tools/drivers.ts index b9f0c8bb207..20c048ad6a3 100644 --- a/packages/boot/tools/drivers.ts +++ b/packages/boot/tools/drivers.ts @@ -324,7 +324,7 @@ export const makeZoeDriver = async (testKit: SwingsetTestKit) => { const { EV } = testKit.runUtils; const zoe = await EV.vat('bootstrap').consumeItem('zoe'); const chainStorage = await EV.vat('bootstrap').consumeItem('chainStorage'); - const storageNode = await EV(chainStorage).makeChildNode('prober-asid9a'); + const storageNode = await EV(chainStorage!).makeChildNode('prober-asid9a'); let creatorFacet; let adminFacet; let brand; diff --git a/packages/boot/tools/ibc/mocks.js b/packages/boot/tools/ibc/mocks.js index 72cbcee4b65..351f5cab139 100644 --- a/packages/boot/tools/ibc/mocks.js +++ b/packages/boot/tools/ibc/mocks.js @@ -130,7 +130,7 @@ export const icaMocks = { * @param {string} acknowledgement acknowledgement response as base64 encoded bytes * @returns {IBCEvent<'acknowledgementPacket'>} */ - ackPacket: (obj, sequence = 1, acknowledgement) => { + ackPacketEvent: (obj, sequence = 1, acknowledgement) => { return { acknowledgement, blockHeight: 289, diff --git a/packages/boot/tools/supports.ts b/packages/boot/tools/supports.ts index ac9db683abe..3f878149a29 100644 --- a/packages/boot/tools/supports.ts +++ b/packages/boot/tools/supports.ts @@ -7,13 +7,14 @@ import { resolve as importMetaResolve } from 'import-meta-resolve'; import { basename, join } from 'path'; import { inspect } from 'util'; -import { Fail } from '@endo/errors'; import { buildSwingset } from '@agoric/cosmic-swingset/src/launch-chain.js'; import { BridgeId, NonNullish, VBankAccount, makeTracer, + type BridgeIdValue, + type Remote, } from '@agoric/internal'; import { unmarshalFromVstorage } from '@agoric/internal/src/marshal.js'; import { makeFakeStorageKit } from '@agoric/internal/src/storage-test-utils.js'; @@ -22,21 +23,57 @@ import { initSwingStore } from '@agoric/swing-store'; import { loadSwingsetConfigFile } from '@agoric/swingset-vat'; import { makeSlogSender } from '@agoric/telemetry'; import { TimeMath, Timestamp } from '@agoric/time'; +import { Fail } from '@endo/errors'; +import { + makeRunUtils, + type RunUtils, +} from '@agoric/swingset-vat/tools/run-utils.js'; import { boardSlottingMarshaller, slotToBoardRemote, } from '@agoric/vats/tools/board-utils.js'; -import { makeRunUtils } from '@agoric/swingset-vat/tools/run-utils.js'; import type { ExecutionContext as AvaT } from 'ava'; +import type { JsonSafe } from '@agoric/cosmic-proto'; +import type { MsgDelegateResponse } from '@agoric/cosmic-proto/cosmos/staking/v1beta1/tx.js'; import type { CoreEvalSDKType } from '@agoric/cosmic-proto/swingset/swingset.js'; -import type { BridgeHandler, IBCMethod } from '@agoric/vats'; +import type { EconomyBootstrapPowers } from '@agoric/inter-protocol/src/proposals/econ-behaviors.js'; +import type { SwingsetController } from '@agoric/swingset-vat/src/controller/controller.js'; +import type { BridgeHandler, IBCMethod, IBCPacket } from '@agoric/vats'; +import type { BootstrapRootObject } from '@agoric/vats/src/core/lib-boot.js'; +import type { EProxy } from '@endo/eventual-send'; import { icaMocks, protoMsgMocks } from './ibc/mocks.js'; const trace = makeTracer('BSTSupport', false); +type ConsumeBootrapItem = ( + name: N, +) => N extends keyof EconomyBootstrapPowers['consume'] + ? EconomyBootstrapPowers['consume'][N] + : unknown; + +// XXX should satisfy EVProxy from run-utils.js but that's failing to import +/** + * Elaboration of EVProxy with knowledge of bootstrap space in these tests. + */ +type BootstrapEV = EProxy & { + sendOnly: (presence: unknown) => Record void>; + vat: ( + name: N, + ) => N extends 'bootstrap' + ? Omit & { + // XXX not really local + consumeItem: ConsumeBootrapItem; + } & Remote<{ consumeItem: ConsumeBootrapItem }> + : Record Promise>; +}; + +const makeBootstrapRunUtils = makeRunUtils as ( + controller: SwingsetController, +) => Omit & { EV: BootstrapEV }; + const keysToObject = ( keys: K[], valueMaker: (key: K, i: number) => V, @@ -288,15 +325,50 @@ export const makeSwingsetTestKit = async ( const outboundMessages = new Map(); - let inbound; + const inbound: Awaited>['bridgeInbound'] = ( + ...args + ) => { + console.log('inbound', ...args); + // eslint-disable-next-line no-use-before-define + bridgeInbound!(...args); + }; let ibcSequenceNonce = 0; - const makeAckEvent = (obj: IBCMethod<'sendPacket'>, ack: string) => { + const addSequenceNonce = ({ packet }: IBCMethod<'sendPacket'>): IBCPacket => { ibcSequenceNonce += 1; - const msg = icaMocks.ackPacket(obj, ibcSequenceNonce, ack); - inbound(BridgeId.DIBC, msg); + return { ...packet, sequence: ibcSequenceNonce }; + }; + + /** + * Adds the sequence so the bridge knows what response to connect it to. + * Then queue it send it over the bridge over this returns. + * Finally return the packet that will be sent. + */ + const ackImmediately = (obj: IBCMethod<'sendPacket'>, ack: string) => { + ibcSequenceNonce += 1; + const msg = icaMocks.ackPacketEvent(obj, ibcSequenceNonce, ack); + setTimeout(() => { + /** + * Mock when Agoric receives the ack from another chain over DIBC. Always + * happens after the packet is returned. + */ + inbound(BridgeId.DIBC, msg); + }); return msg.packet; }; + + const inboundQueue: [bridgeId: BridgeIdValue, arg1: unknown][] = []; + /** + * Like ackImmediately but defers in the inbound receiverAck + * until `bridgeQueue()` is awaited. + */ + const ackLater = (obj: IBCMethod<'sendPacket'>, ack: string) => { + ibcSequenceNonce += 1; + const msg = icaMocks.ackPacketEvent(obj, ibcSequenceNonce, ack); + inboundQueue.push([BridgeId.DIBC, msg]); + return msg.packet; + }; + /** * Mock the bridge outbound handler. The real one is implemented in Golang so * changes there will sometimes require changes here. @@ -365,42 +437,37 @@ export const makeSwingsetTestKit = async ( case 'IBC_METHOD': switch (obj.method) { case 'startChannelOpenInit': - inbound(BridgeId.DIBC, icaMocks.channelOpenAck(obj)); + inboundQueue.push([ + BridgeId.DIBC, + icaMocks.channelOpenAck(obj), + ]); return undefined; case 'sendPacket': switch (obj.packet.data) { case protoMsgMocks.delegate.msg: { - return makeAckEvent(obj, protoMsgMocks.delegate.ack); + return ackLater(obj, protoMsgMocks.delegate.ack); } case protoMsgMocks.delegateWithOpts.msg: { - return makeAckEvent( - obj, - protoMsgMocks.delegateWithOpts.ack, - ); + return ackLater(obj, protoMsgMocks.delegateWithOpts.ack); } case protoMsgMocks.queryBalance.msg: { - return makeAckEvent(obj, protoMsgMocks.queryBalance.ack); + return ackLater(obj, protoMsgMocks.queryBalance.ack); } case protoMsgMocks.queryUnknownPath.msg: { - return makeAckEvent( - obj, - protoMsgMocks.queryUnknownPath.ack, - ); + return ackLater(obj, protoMsgMocks.queryUnknownPath.ack); } case protoMsgMocks.queryBalanceMulti.msg: { - return makeAckEvent( - obj, - protoMsgMocks.queryBalanceMulti.ack, - ); + return ackLater(obj, protoMsgMocks.queryBalanceMulti.ack); } case protoMsgMocks.queryBalanceUnknownDenom.msg: { - return makeAckEvent( + return ackLater( obj, protoMsgMocks.queryBalanceUnknownDenom.ack, ); } default: { - return makeAckEvent(obj, protoMsgMocks.error.ack); + // An error that would be triggered before reception on another chain + return ackImmediately(obj, protoMsgMocks.error.ack); } } default: @@ -430,7 +497,7 @@ export const makeSwingsetTestKit = async ( // this results in `syscall.callNow failed: device.invoke failed, see logs for details` throw Error('simulated packet timeout'); } - return /** @type {JsonSafe} */ {}; + return {} as JsonSafe; } // returns one empty object per message unless specified default: @@ -473,11 +540,10 @@ export const makeSwingsetTestKit = async ( debugVats, }, ); - inbound = bridgeInbound; console.timeLog('makeBaseSwingsetTestKit', 'buildSwingset'); - const runUtils = makeRunUtils(controller); + const runUtils = makeBootstrapRunUtils(controller); const buildProposal = makeProposalExtractor({ childProcess: childProcessAmbient, @@ -556,12 +622,30 @@ export const makeSwingsetTestKit = async ( const getOutboundMessages = (bridgeId: string) => harden([...outboundMessages.get(bridgeId)]); + /** + * @param {number} max the max number of messages to flush + * @returns {Promise} the number of messages flushed + */ + const flushInboundQueue = async (max: number = Number.POSITIVE_INFINITY) => { + console.log('🚽'); + let i = 0; + for (i = 0; i < max; i += 1) { + const args = inboundQueue.shift(); + if (!args) break; + + await runUtils.queueAndRun(() => inbound(...args), true); + } + console.log('🧻'); + return i; + }; + return { advanceTimeBy, advanceTimeTo, buildProposal, bridgeInbound, controller, + flushInboundQueue, evalProposal, getCrankNumber, getOutboundMessages, diff --git a/packages/builders/scripts/testing/restart-sendAnywhere.js b/packages/builders/scripts/testing/restart-sendAnywhere.js new file mode 100644 index 00000000000..3fe56ee023b --- /dev/null +++ b/packages/builders/scripts/testing/restart-sendAnywhere.js @@ -0,0 +1,100 @@ +/** + * @file This is for use in tests in a3p-integration + * Unlike most builder scripts, this one includes the proposal exports as well. + */ +import { + deeplyFulfilledObject, + makeTracer, + NonNullish, +} from '@agoric/internal'; +import { E } from '@endo/far'; + +/// + +const trace = makeTracer('StartSA', true); + +/** + * @import {start as StartFn} from '@agoric/orchestration/src/examples/sendAnywhere.contract.js'; + */ + +/** + * @param {BootstrapPowers} powers + */ +export const restartSendAnywhere = async ({ + consume: { + agoricNames, + board, + chainStorage, + chainTimerService, + cosmosInterchainService, + localchain, + + contractKits, + }, + instance: instances, +}) => { + trace(restartSendAnywhere.name); + + // @ts-expect-error unknown instance + const instance = await instances.consume.sendAnywhere; + trace('instance', instance); + /** @type {StartedInstanceKit} */ + const kit = /** @type {any} */ (await E(contractKits).get(instance)); + + const marshaller = await E(board).getReadonlyMarshaller(); + + const privateArgs = await deeplyFulfilledObject( + harden({ + agoricNames, + localchain, + marshaller, + orchestrationService: cosmosInterchainService, + storageNode: E(NonNullish(await chainStorage)).makeChildNode( + 'sendAnywhere', + ), + timerService: chainTimerService, + }), + ); + + await E(kit.adminFacet).restartContract(privateArgs); + trace('done'); +}; +harden(restartSendAnywhere); + +export const getManifest = () => { + return { + manifest: { + [restartSendAnywhere.name]: { + consume: { + agoricNames: true, + board: true, + chainStorage: true, + chainTimerService: true, + cosmosInterchainService: true, + localchain: true, + + contractKits: true, + }, + instance: { + consume: { sendAnywhere: true }, + }, + }, + }, + }; +}; + +/** @type {import('@agoric/deploy-script-support/src/externalTypes.js').CoreEvalBuilder} */ +export const defaultProposalBuilder = async () => + harden({ + // Somewhat unorthodox, source the exports from this builder module + sourceSpec: '@agoric/builders/scripts/testing/restart-sendAnywhere.js', + getManifestCall: [getManifest.name], + }); + +export default async (homeP, endowments) => { + // import dynamically so the module can work in CoreEval environment + const dspModule = await import('@agoric/deploy-script-support'); + const { makeHelpers } = dspModule; + const { writeCoreEval } = await makeHelpers(homeP, endowments); + await writeCoreEval(restartSendAnywhere.name, defaultProposalBuilder); +}; diff --git a/packages/builders/scripts/testing/restart-stakeAtom.js b/packages/builders/scripts/testing/restart-stakeAtom.js new file mode 100644 index 00000000000..85fa18429c1 --- /dev/null +++ b/packages/builders/scripts/testing/restart-stakeAtom.js @@ -0,0 +1,89 @@ +/** + * @file This is for use in tests in a3p-integration + * Unlike most builder scripts, this one includes the proposal exports as well. + */ +import { deeplyFulfilledObject, makeTracer } from '@agoric/internal'; +import { makeStorageNodeChild } from '@agoric/internal/src/lib-chainStorage.js'; +import { E } from '@endo/far'; + +/// + +const trace = makeTracer('RestartSA', true); + +/** + * @import {start as StartFn} from '@agoric/orchestration/src/examples/stakeIca.contract.js'; + */ + +/** + * @param {BootstrapPowers} powers + */ +export const restartStakeAtom = async ({ + consume: { + board, + chainStorage, + chainTimerService, + cosmosInterchainService, + + contractKits, + }, + instance: instances, +}) => { + trace(restartStakeAtom.name); + + const instance = await instances.consume.stakeAtom; + trace('instance', instance); + + /** @type {StartedInstanceKit} */ + const kit = /** @type {any} */ (await E(contractKits).get(instance)); + + const marshaller = await E(board).getReadonlyMarshaller(); + + const privateArgs = await deeplyFulfilledObject( + harden({ + cosmosInterchainService, + storageNode: makeStorageNodeChild(chainStorage, 'stakeAtom'), + marshaller, + timer: chainTimerService, + }), + ); + + await E(kit.adminFacet).restartContract(privateArgs); + trace('done'); +}; +harden(restartStakeAtom); + +export const getManifest = () => { + return { + manifest: { + [restartStakeAtom.name]: { + consume: { + board: true, + chainStorage: true, + chainTimerService: true, + cosmosInterchainService: true, + + contractKits: true, + }, + instance: { + consume: { stakeAtom: true }, + }, + }, + }, + }; +}; + +/** @type {import('@agoric/deploy-script-support/src/externalTypes.js').CoreEvalBuilder} */ +export const defaultProposalBuilder = async () => + harden({ + // Somewhat unorthodox, source the exports from this builder module + sourceSpec: '@agoric/builders/scripts/testing/restart-stakeAtom.js', + getManifestCall: [getManifest.name], + }); + +export default async (homeP, endowments) => { + // import dynamically so the module can work in CoreEval environment + const dspModule = await import('@agoric/deploy-script-support'); + const { makeHelpers } = dspModule; + const { writeCoreEval } = await makeHelpers(homeP, endowments); + await writeCoreEval(restartStakeAtom.name, defaultProposalBuilder); +}; diff --git a/packages/builders/scripts/testing/start-sendAnywhere.js b/packages/builders/scripts/testing/start-sendAnywhere.js new file mode 100644 index 00000000000..87bc18cd2a7 --- /dev/null +++ b/packages/builders/scripts/testing/start-sendAnywhere.js @@ -0,0 +1,128 @@ +/** + * @file This is for use in tests in a3p-integration + * Unlike most builder scripts, this one includes the proposal exports as well. + */ +import { + deeplyFulfilledObject, + makeTracer, + NonNullish, +} from '@agoric/internal'; +import { E } from '@endo/far'; + +/// +/** + * @import {Installation} from '@agoric/zoe/src/zoeService/utils.js'; + */ + +const trace = makeTracer('StartSA', true); + +/** + * @import {start as StartFn} from '@agoric/orchestration/src/examples/sendAnywhere.contract.js'; + */ + +/** + * @param {BootstrapPowers & { + * installation: { + * consume: { + * sendAnywhere: Installation; + * }; + * }; + * }} powers + */ +export const startSendAnywhere = async ({ + consume: { + agoricNames, + board, + chainStorage, + chainTimerService, + cosmosInterchainService, + localchain, + startUpgradable, + }, + installation: { + consume: { sendAnywhere }, + }, + instance: { + // @ts-expect-error unknown instance + produce: { sendAnywhere: produceInstance }, + }, +}) => { + trace(startSendAnywhere.name); + + const marshaller = await E(board).getReadonlyMarshaller(); + + const privateArgs = await deeplyFulfilledObject( + harden({ + agoricNames, + localchain, + marshaller, + orchestrationService: cosmosInterchainService, + storageNode: E(NonNullish(await chainStorage)).makeChildNode( + 'sendAnywhere', + ), + timerService: chainTimerService, + }), + ); + + const { instance } = await E(startUpgradable)({ + label: 'sendAnywhere', + installation: sendAnywhere, + privateArgs, + }); + produceInstance.resolve(instance); + trace('done'); +}; +harden(startSendAnywhere); + +export const getManifest = ({ restoreRef }, { installationRef }) => { + return { + manifest: { + [startSendAnywhere.name]: { + consume: { + agoricNames: true, + board: true, + chainStorage: true, + chainTimerService: true, + cosmosInterchainService: true, + localchain: true, + + startUpgradable: true, + }, + installation: { + consume: { sendAnywhere: true }, + }, + instance: { + produce: { sendAnywhere: true }, + }, + }, + }, + installations: { + sendAnywhere: restoreRef(installationRef), + }, + }; +}; + +/** @type {import('@agoric/deploy-script-support/src/externalTypes.js').CoreEvalBuilder} */ +export const defaultProposalBuilder = async ({ publishRef, install }) => + harden({ + // Somewhat unorthodox, source the exports from this builder module + sourceSpec: '@agoric/builders/scripts/testing/start-sendAnywhere.js', + getManifestCall: [ + getManifest.name, + { + installationRef: publishRef( + install( + '@agoric/orchestration/src/examples/sendAnywhere.contract.js', + ), + ), + }, + ], + }); + +export default async (homeP, endowments) => { + // import dynamically so the module can work in CoreEval environment + const dspModule = await import('@agoric/deploy-script-support'); + const { makeHelpers } = dspModule; + const { writeCoreEval } = await makeHelpers(homeP, endowments); + await writeCoreEval(startSendAnywhere.name, defaultProposalBuilder); +}; diff --git a/packages/inter-protocol/test/vaultFactory/vault-collateralization.test.js b/packages/inter-protocol/test/vaultFactory/vault-collateralization.test.js index 7157de31682..3aebc47a106 100644 --- a/packages/inter-protocol/test/vaultFactory/vault-collateralization.test.js +++ b/packages/inter-protocol/test/vaultFactory/vault-collateralization.test.js @@ -20,9 +20,7 @@ test('excessive loan', async t => { const md = await makeManagerDriver(t); const threshold = 453n; - await t.notThrowsAsync( - md.makeVaultDriver(aeth.make(100n), run.make(threshold)), - ); + await md.makeVaultDriver(aeth.make(100n), run.make(threshold)); await t.throwsAsync( md.makeVaultDriver(aeth.make(100n), run.make(threshold + 1n)), diff --git a/packages/orchestration/src/examples/auto-stake-it.contract.js b/packages/orchestration/src/examples/auto-stake-it.contract.js index cc97565d0ba..3cf05d4485c 100644 --- a/packages/orchestration/src/examples/auto-stake-it.contract.js +++ b/packages/orchestration/src/examples/auto-stake-it.contract.js @@ -2,25 +2,20 @@ import { EmptyProposalShape, InvitationShape, } from '@agoric/zoe/src/typeGuards.js'; -import { Fail } from '@endo/errors'; import { M } from '@endo/patterns'; -import { withOrchestration } from '../utils/start-helper.js'; import { prepareChainHubAdmin } from '../exos/chain-hub-admin.js'; -import { prepareStakingTap } from './auto-stake-it-tap-kit.js'; import { preparePortfolioHolder } from '../exos/portfolio-holder-kit.js'; +import { withOrchestration } from '../utils/start-helper.js'; +import { prepareStakingTap } from './auto-stake-it-tap-kit.js'; +import * as flows from './auto-stake-it.flows.js'; /** * @import {TimerService} from '@agoric/time'; - * @import {ResolvedPublicTopic} from '@agoric/zoe/src/contractSupport/topics.js'; * @import {LocalChain} from '@agoric/vats/src/localchain.js'; * @import {NameHub} from '@agoric/vats'; * @import {Remote} from '@agoric/vow'; * @import {Zone} from '@agoric/zone'; - * @import {GuestInterface} from '@agoric/async-flow'; - * @import {CosmosValidatorAddress, Orchestrator, CosmosInterchainService, Denom, OrchestrationAccount, StakingAccountActions, OrchestrationFlow} from '@agoric/orchestration'; - * @import {MakeStakingTap} from './auto-stake-it-tap-kit.js'; - * @import {MakePortfolioHolder} from '../exos/portfolio-holder-kit.js'; - * @import {ChainHub} from '../exos/chain-hub.js'; + * @import {CosmosInterchainService} from '@agoric/orchestration'; * @import {OrchestrationTools} from '../utils/start-helper.js'; */ @@ -34,100 +29,6 @@ import { preparePortfolioHolder } from '../exos/portfolio-holder-kit.js'; * }} OrchestrationPowers */ -/** - * @satisfies {OrchestrationFlow} - * @param {Orchestrator} orch - * @param {{ - * makeStakingTap: MakeStakingTap; - * makePortfolioHolder: MakePortfolioHolder; - * chainHub: GuestInterface; - * }} ctx - * @param {ZCFSeat} seat - * @param {{ - * chainName: string; - * validator: CosmosValidatorAddress; - * localDenom: Denom; - * }} offerArgs - */ -const makeAccountsHandler = async ( - orch, - { makeStakingTap, makePortfolioHolder, chainHub }, - seat, - { - chainName, - validator, - // TODO localDenom is user supplied, until #9211 - localDenom, - }, -) => { - seat.exit(); // no funds exchanged - const [agoric, remoteChain] = await Promise.all([ - orch.getChain('agoric'), - orch.getChain(chainName), - ]); - const { chainId, stakingTokens } = await remoteChain.getChainInfo(); - const remoteDenom = stakingTokens[0].denom; - remoteDenom || - Fail`${chainId || chainName} does not have stakingTokens in config`; - if (chainId !== validator.chainId) { - Fail`validator chainId ${validator.chainId} does not match remote chainId ${chainId}`; - } - const [localAccount, stakingAccount] = await Promise.all([ - agoric.makeAccount(), - /** @type {Promise & StakingAccountActions>} */ ( - remoteChain.makeAccount() - ), - ]); - - const [localChainAddress, remoteChainAddress] = await Promise.all([ - localAccount.getAddress(), - stakingAccount.getAddress(), - ]); - const agoricChainId = (await agoric.getChainInfo()).chainId; - const { transferChannel } = await chainHub.getConnectionInfo( - agoricChainId, - chainId, - ); - assert(transferChannel.counterPartyChannelId, 'unable to find sourceChannel'); - - // Every time the `localAccount` receives `remoteDenom` over IBC, delegate it. - const tap = makeStakingTap({ - localAccount, - stakingAccount, - validator, - localChainAddress, - remoteChainAddress, - sourceChannel: transferChannel.counterPartyChannelId, - remoteDenom, - localDenom, - }); - // XXX consider storing appRegistration, so we can .revoke() or .updateTargetApp() - // @ts-expect-error tap.receiveUpcall: 'Vow | undefined' not assignable to 'Promise' - await localAccount.monitorTransfers(tap); - - const accountEntries = harden( - /** @type {[string, OrchestrationAccount][]} */ ([ - ['agoric', localAccount], - [chainName, stakingAccount], - ]), - ); - 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(); -}; - /** * AutoStakeIt allows users to to create an auto-forwarding address that * transfers and stakes tokens on a remote chain when received. @@ -145,7 +46,7 @@ const contract = async ( zcf, _privateArgs, zone, - { chainHub, orchestrate, vowTools }, + { chainHub, orchestrateAll, vowTools }, ) => { const makeStakingTap = prepareStakingTap( zone.subZone('stakingTap'), @@ -156,11 +57,11 @@ const contract = async ( vowTools, ); - const makeAccounts = orchestrate( - 'makeAccounts', - { makeStakingTap, makePortfolioHolder, chainHub }, - makeAccountsHandler, - ); + const { makeAccounts } = orchestrateAll(flows, { + makeStakingTap, + makePortfolioHolder, + chainHub, + }); const publicFacet = zone.exo( 'AutoStakeIt Public Facet', diff --git a/packages/orchestration/src/examples/auto-stake-it.flows.js b/packages/orchestration/src/examples/auto-stake-it.flows.js new file mode 100644 index 00000000000..21c75dcb97d --- /dev/null +++ b/packages/orchestration/src/examples/auto-stake-it.flows.js @@ -0,0 +1,104 @@ +import { Fail } from '@endo/errors'; + +/** + * @import {ResolvedPublicTopic} from '@agoric/zoe/src/contractSupport/topics.js'; + * @import {GuestInterface} from '@agoric/async-flow'; + * @import {CosmosValidatorAddress, Orchestrator, CosmosInterchainService, Denom, OrchestrationAccount, StakingAccountActions, OrchestrationFlow} from '@agoric/orchestration'; + * @import {MakeStakingTap} from './auto-stake-it-tap-kit.js'; + * @import {MakePortfolioHolder} from '../exos/portfolio-holder-kit.js'; + * @import {ChainHub} from '../exos/chain-hub.js'; + */ + +/** + * @satisfies {OrchestrationFlow} + * @param {Orchestrator} orch + * @param {{ + * makeStakingTap: MakeStakingTap; + * makePortfolioHolder: MakePortfolioHolder; + * chainHub: GuestInterface; + * }} ctx + * @param {ZCFSeat} seat + * @param {{ + * chainName: string; + * validator: CosmosValidatorAddress; + * localDenom: Denom; + * }} offerArgs + */ +export const makeAccounts = async ( + orch, + { makeStakingTap, makePortfolioHolder, chainHub }, + seat, + { + chainName, + validator, + // TODO localDenom is user supplied, until #9211 + localDenom, + }, +) => { + seat.exit(); // no funds exchanged + const [agoric, remoteChain] = await Promise.all([ + orch.getChain('agoric'), + orch.getChain(chainName), + ]); + const { chainId, stakingTokens } = await remoteChain.getChainInfo(); + const remoteDenom = stakingTokens[0].denom; + remoteDenom || + Fail`${chainId || chainName} does not have stakingTokens in config`; + if (chainId !== validator.chainId) { + Fail`validator chainId ${validator.chainId} does not match remote chainId ${chainId}`; + } + const [localAccount, stakingAccount] = await Promise.all([ + agoric.makeAccount(), + /** @type {Promise & StakingAccountActions>} */ ( + remoteChain.makeAccount() + ), + ]); + + const [localChainAddress, remoteChainAddress] = await Promise.all([ + localAccount.getAddress(), + stakingAccount.getAddress(), + ]); + const agoricChainId = (await agoric.getChainInfo()).chainId; + const { transferChannel } = await chainHub.getConnectionInfo( + agoricChainId, + chainId, + ); + assert(transferChannel.counterPartyChannelId, 'unable to find sourceChannel'); + + // Every time the `localAccount` receives `remoteDenom` over IBC, delegate it. + const tap = makeStakingTap({ + localAccount, + stakingAccount, + validator, + localChainAddress, + remoteChainAddress, + sourceChannel: transferChannel.counterPartyChannelId, + remoteDenom, + localDenom, + }); + // XXX consider storing appRegistration, so we can .revoke() or .updateTargetApp() + // @ts-expect-error tap.receiveUpcall: 'Vow | undefined' not assignable to 'Promise' + await localAccount.monitorTransfers(tap); + + const accountEntries = harden( + /** @type {[string, OrchestrationAccount][]} */ ([ + ['agoric', localAccount], + [chainName, stakingAccount], + ]), + ); + 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(); +}; diff --git a/packages/orchestration/src/examples/basic-flows.contract.js b/packages/orchestration/src/examples/basic-flows.contract.js index bbc428b6058..a430dc240c7 100644 --- a/packages/orchestration/src/examples/basic-flows.contract.js +++ b/packages/orchestration/src/examples/basic-flows.contract.js @@ -3,83 +3,17 @@ * 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 { withOrchestration } from '../utils/start-helper.js'; +import { M } from '@endo/patterns'; import { preparePortfolioHolder } from '../exos/portfolio-holder-kit.js'; +import { withOrchestration } from '../utils/start-helper.js'; +import * as flows from './basic-flows.flows.js'; /** * @import {Zone} from '@agoric/zone'; - * @import {OrchestrationAccount, OrchestrationFlow, 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'; */ -/** - * Create an account on a Cosmos chain and return a continuing offer with - * invitations makers for Delegate, WithdrawRewards, Transfer, etc. - * - * @satisfies {OrchestrationFlow} - * @param {Orchestrator} orch - * @param {undefined} _ctx - * @param {ZCFSeat} seat - * @param {{ chainName: string }} offerArgs - */ -const makeOrchAccountHandler = 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(); - 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. - * - * @satisfies {OrchestrationFlow} - * @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 & { @@ -88,23 +22,18 @@ const makePortfolioAcctHandler = async ( * @param {Zone} zone * @param {OrchestrationTools} tools */ -const contract = async (zcf, _privateArgs, zone, { orchestrate, vowTools }) => { +const contract = async ( + zcf, + _privateArgs, + zone, + { orchestrateAll, vowTools }, +) => { const makePortfolioHolder = preparePortfolioHolder( zone.subZone('portfolio'), vowTools, ); - const makeOrchAccount = orchestrate( - 'makeOrchAccount', - undefined, - makeOrchAccountHandler, - ); - - const makePortfolioAccount = orchestrate( - 'makePortfolioAccount', - makePortfolioHolder, - makePortfolioAcctHandler, - ); + const orchFns = orchestrateAll(flows, { makePortfolioHolder }); const publicFacet = zone.exo( 'Basic Flows Public Facet', @@ -115,13 +44,13 @@ const contract = async (zcf, _privateArgs, zone, { orchestrate, vowTools }) => { { makeOrchAccountInvitation() { return zcf.makeInvitation( - makeOrchAccount, + orchFns.makeOrchAccount, 'Make an Orchestration Account', ); }, makePortfolioAccountInvitation() { return zcf.makeInvitation( - makePortfolioAccount, + orchFns.makePortfolioAccount, 'Make an Orchestration Account', ); }, diff --git a/packages/orchestration/src/examples/basic-flows.flows.js b/packages/orchestration/src/examples/basic-flows.flows.js new file mode 100644 index 00000000000..d735e44d1e4 --- /dev/null +++ b/packages/orchestration/src/examples/basic-flows.flows.js @@ -0,0 +1,79 @@ +/** + * @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 { M, mustMatch } from '@endo/patterns'; + +/** + * @import {Zone} from '@agoric/zone'; + * @import {OrchestrationAccount, OrchestrationFlow, 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'; + */ + +/** + * Create an OrchestrationAccount for a specific chain and return a continuing + * offer with invitations makers for Delegate, WithdrawRewards, Transfer, etc. + * + * @satisfies {OrchestrationFlow} + * @param {Orchestrator} orch + * @param {any} _ctx + * @param {ZCFSeat} seat + * @param {{ chainName: string }} offerArgs + */ +export const makeOrchAccount = async (orch, _ctx, seat, { chainName }) => { + seat.exit(); // no funds exchanged + mustMatch(chainName, M.string()); + const remoteChain = await orch.getChain(chainName); + const orchAccount = await remoteChain.makeAccount(); + return orchAccount.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. + * + * @satisfies {OrchestrationFlow} + * @param {Orchestrator} orch + * @param {object} ctx + * @param {MakePortfolioHolder} ctx.makePortfolioHolder + * @param {ZCFSeat} seat + * @param {{ chainNames: string[] }} offerArgs + */ +export const makePortfolioAccount = 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(); +}; diff --git a/packages/orchestration/src/examples/sendAnywhere.contract.js b/packages/orchestration/src/examples/sendAnywhere.contract.js index ba635f22dc9..cbe1e573e74 100644 --- a/packages/orchestration/src/examples/sendAnywhere.contract.js +++ b/packages/orchestration/src/examples/sendAnywhere.contract.js @@ -54,7 +54,7 @@ const contract = async ( ) => { const contractState = makeStateRecord( /** @type {{ account: OrchestrationAccount | undefined }} */ { - account: undefined, + localAccount: undefined, }, ); diff --git a/packages/orchestration/src/examples/sendAnywhere.flows.js b/packages/orchestration/src/examples/sendAnywhere.flows.js index cdd44865a51..1c9e987ef2f 100644 --- a/packages/orchestration/src/examples/sendAnywhere.flows.js +++ b/packages/orchestration/src/examples/sendAnywhere.flows.js @@ -16,7 +16,7 @@ const { entries } = Object; * @satisfies {OrchestrationFlow} * @param {Orchestrator} orch * @param {object} ctx - * @param {{ account?: OrchestrationAccountI & LocalAccountMethods }} ctx.contractState + * @param {{ localAccount?: OrchestrationAccountI & LocalAccountMethods }} ctx.contractState * @param {GuestOf} ctx.localTransfer * @param {(brand: Brand) => Promise} ctx.findBrandInVBank * @param {ZCFSeat} seat @@ -36,18 +36,18 @@ export async function sendIt( const { denom } = await findBrandInVBank(amt.brand); const chain = await orch.getChain(chainName); - if (!contractState.account) { + if (!contractState.localAccount) { const agoricChain = await orch.getChain('agoric'); - contractState.account = await agoricChain.makeAccount(); + contractState.localAccount = await agoricChain.makeAccount(); } const info = await chain.getChainInfo(); const { chainId } = info; assert(typeof chainId === 'string', 'bad chainId'); - await localTransfer(seat, contractState.account, give); + await localTransfer(seat, contractState.localAccount, give); - await contractState.account.transfer( + await contractState.localAccount.transfer( { denom, value: amt.value }, { value: destAddr, diff --git a/packages/orchestration/src/utils/zoe-tools.js b/packages/orchestration/src/utils/zoe-tools.js index 32b7632bec0..b8aca53a790 100644 --- a/packages/orchestration/src/utils/zoe-tools.js +++ b/packages/orchestration/src/utils/zoe-tools.js @@ -1,5 +1,6 @@ import { Fail } from '@endo/errors'; import { atomicTransfer } from '@agoric/zoe/src/contractSupport/index.js'; +import { E } from '@endo/far'; /** * @import {InvitationMakers} from '@agoric/smart-wallet/src/types.js'; @@ -64,7 +65,7 @@ export const makeZoeTools = (zone, { zcf, vowTools }) => { // Now all the `give` are accessible, so we can move them to the localAccount` const promises = Object.entries(give).map(async ([kw, _amount]) => { - const pmt = await userSeat.getPayout(kw); + const pmt = await E(userSeat).getPayout(kw); // TODO arrange recovery on upgrade of pmt? return localAccount.deposit(pmt); }); diff --git a/packages/vats/src/core/lib-boot.js b/packages/vats/src/core/lib-boot.js index 5cf6771eb8d..486a7f12603 100644 --- a/packages/vats/src/core/lib-boot.js +++ b/packages/vats/src/core/lib-boot.js @@ -187,6 +187,7 @@ export const makeBootstrap = ( throw e; }); }, + /** @param {string} name } */ consumeItem: name => { assert.typeof(name, 'string'); return consume[name]; @@ -195,6 +196,7 @@ export const makeBootstrap = ( assert.typeof(name, 'string'); produce[name].resolve(resolution); }, + /** @param {string} name } */ resetItem: name => { assert.typeof(name, 'string'); produce[name].reset(); diff --git a/packages/vats/src/core/types-ambient.d.ts b/packages/vats/src/core/types-ambient.d.ts index ee27e97924b..f87f529db5a 100644 --- a/packages/vats/src/core/types-ambient.d.ts +++ b/packages/vats/src/core/types-ambient.d.ts @@ -371,8 +371,10 @@ type ChainBootstrapSpaceT = { namesByAddress: import('../types.js').NameHub; namesByAddressAdmin: import('../types.js').NamesByAddressAdmin; networkVat: NetworkVat; + orchestration?: CosmosInterchainService; pegasusConnections: import('@agoric/vats').NameHubKit; pegasusConnectionsAdmin: import('@agoric/vats').NameAdmin; + powerStore: MapStore; priceAuthorityVat: Awaited; priceAuthority: import('@agoric/zoe/tools/types.js').PriceAuthority; priceAuthorityAdmin: import('@agoric/vats/src/priceAuthorityRegistry').PriceAuthorityRegistryAdmin; diff --git a/packages/vats/test/localchain.test.js b/packages/vats/test/localchain.test.js index f8ea6048d1d..05abdadab11 100644 --- a/packages/vats/test/localchain.test.js +++ b/packages/vats/test/localchain.test.js @@ -99,8 +99,11 @@ test('localchain - deposit and withdraw', async t => { const boot = async () => { const { bankManager } = await t.context; - await t.notThrowsAsync( - E(bankManager).addAsset('ubld', 'BLD', 'Staking Token', bld.issuerKit), + await E(bankManager).addAsset( + 'ubld', + 'BLD', + 'Staking Token', + bld.issuerKit, ); }; await boot(); diff --git a/packages/xsnap/test/xs-limits.test.js b/packages/xsnap/test/xs-limits.test.js index 58aa10522ba..7f9b6613cd7 100644 --- a/packages/xsnap/test/xs-limits.test.js +++ b/packages/xsnap/test/xs-limits.test.js @@ -89,7 +89,7 @@ test.skip('property name space exhaustion: orderly fail-stop', async t => { const vat = await xsnap({ ...opts, parserBufferSize }); t.teardown(() => vat.terminate()); const expected = failure ? [failure] : [qty * 4 + 2]; - await t.notThrowsAsync(vat.evaluate(grow(qty))); + await vat.evaluate(grow(qty)); t.deepEqual( expected, opts.messages.map(txt => JSON.parse(txt)), diff --git a/packages/zoe/src/contractFacet/types-ambient.d.ts b/packages/zoe/src/contractFacet/types-ambient.d.ts index bb41fe7e823..255e7e81298 100644 --- a/packages/zoe/src/contractFacet/types-ambient.d.ts +++ b/packages/zoe/src/contractFacet/types-ambient.d.ts @@ -209,7 +209,7 @@ type ZCFSeat = import('@endo/pass-style').RemotableObject & { }; type ZcfSeatKit = { zcfSeat: ZCFSeat; - userSeat: ERef; + userSeat: Promise; }; type HandleOffer = (seat: ZCFSeat, offerArgs: OA) => OR; type OfferHandler = diff --git a/packages/zoe/src/zoeService/utils.d.ts b/packages/zoe/src/zoeService/utils.d.ts index 6ef63ce49a8..1b27843320c 100644 --- a/packages/zoe/src/zoeService/utils.d.ts +++ b/packages/zoe/src/zoeService/utils.d.ts @@ -7,6 +7,7 @@ import type { TagContainer } from '@agoric/internal/src/tagged.js'; import type { Baggage } from '@agoric/swingset-liveslots'; import type { VatUpgradeResults } from '@agoric/swingset-vat'; import type { RemotableObject } from '@endo/marshal'; +import type { FarRef } from '@endo/far'; // XXX https://github.com/Agoric/agoric-sdk/issues/4565 type SourceBundle = Record; @@ -33,7 +34,7 @@ export type ContractStartFunction = ( baggage?: Baggage, ) => ERef<{ creatorFacet?: {}; publicFacet?: {} }>; -export type AdminFacet = RemotableObject & { +export type AdminFacet = FarRef<{ // Completion, which is currently any getVatShutdownPromise: () => Promise; upgradeContract: Parameters[1] extends undefined @@ -45,7 +46,7 @@ export type AdminFacet = RemotableObject & { restartContract: Parameters[1] extends undefined ? () => Promise : (newPrivateArgs: Parameters[1]) => Promise; -}; +}>; export type StartParams = SF extends ContractStartFunction ? Parameters[1] extends undefined diff --git a/packages/zoe/src/zoeService/utils.test-d.ts b/packages/zoe/src/zoeService/utils.test-d.ts index f92a163e30f..90c61472899 100644 --- a/packages/zoe/src/zoeService/utils.test-d.ts +++ b/packages/zoe/src/zoeService/utils.test-d.ts @@ -1,3 +1,4 @@ +import { E } from '@endo/far'; import type { StartedInstanceKit } from './utils'; const someContractStartFn = ( @@ -10,24 +11,24 @@ type PsmInstanceKit = StartedInstanceKit; const psmInstanceKit: PsmInstanceKit = null as any; // @ts-expect-error missing privateArgs argument -void psmInstanceKit.adminFacet.restartContract(); +void E(psmInstanceKit.adminFacet).restartContract(); const partial = { someNumber: 1, }; // @ts-expect-error missing member of privateArgs argument -void psmInstanceKit.adminFacet.restartContract(partial); +void E(psmInstanceKit.adminFacet).restartContract(partial); // valid privateArgs now with 'marshaller' -void psmInstanceKit.adminFacet.restartContract({ +void E(psmInstanceKit.adminFacet).restartContract({ ...partial, someString: 'str', }); // @ts-expect-error missing member of privateArgs argument -void psmInstanceKit.adminFacet.upgradeContract('whatever', partial); +void E(psmInstanceKit.adminFacet).upgradeContract('whatever', partial); // valid privateArgs now with 'marshaller' -void psmInstanceKit.adminFacet.upgradeContract('whatever', { +void E(psmInstanceKit.adminFacet).upgradeContract('whatever', { ...partial, someString: 'str', });