From c2021ba0e59de5942c1286b2db70f8becb201730 Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Tue, 11 Jun 2024 11:01:44 -0400 Subject: [PATCH] feat(stakeAtom): publish address to vstorage --- .../test/bootstrapTests/orchestration.test.ts | 12 +++- .../src/examples/sendAnywhere.contract.js | 1 + .../src/examples/stakeIca.contract.js | 14 +++- .../src/examples/swapExample.contract.js | 9 ++- .../src/examples/unbondExample.contract.js | 1 + .../src/exos/cosmosOrchestrationAccount.js | 3 + packages/orchestration/src/facade.js | 12 +++- .../test/examples/stake-atom.contract.test.ts | 65 ++++++++++++++++--- packages/orchestration/test/facade.test.ts | 6 ++ .../orchestration/test/staking-ops.test.ts | 43 ++++++++++-- 10 files changed, 140 insertions(+), 26 deletions(-) diff --git a/packages/boot/test/bootstrapTests/orchestration.test.ts b/packages/boot/test/bootstrapTests/orchestration.test.ts index d5fc69792bb..1d468e8abc9 100644 --- a/packages/boot/test/bootstrapTests/orchestration.test.ts +++ b/packages/boot/test/bootstrapTests/orchestration.test.ts @@ -123,7 +123,7 @@ test.serial('stakeAtom - repl-style', async t => { }); test.serial('stakeAtom - smart wallet', async t => { - const { agoricNamesRemotes } = t.context; + const { agoricNamesRemotes, readLatest } = t.context; const wd = await t.context.walletFactoryDriver.provideSmartWallet( 'agoric1testStakAtom', @@ -140,12 +140,20 @@ test.serial('stakeAtom - smart wallet', async t => { }); t.like(wd.getCurrentWalletRecord(), { offerToPublicSubscriberPaths: [ - ['request-account', { account: 'published.stakeAtom' }], + [ + 'request-account', + { + account: 'published.stakeAtom.accounts.cosmos1test', + }, + ], ], }); t.like(wd.getLatestUpdateRecord(), { status: { id: 'request-account', numWantsSatisfied: 1 }, }); + t.like(readLatest('published.stakeAtom.accounts.cosmos1test'), { + sequence: 0n, + }); const { ATOM } = agoricNamesRemotes.brand; ATOM || Fail`ATOM missing from agoricNames`; diff --git a/packages/orchestration/src/examples/sendAnywhere.contract.js b/packages/orchestration/src/examples/sendAnywhere.contract.js index ed49472cee2..301f6da3f2d 100644 --- a/packages/orchestration/src/examples/sendAnywhere.contract.js +++ b/packages/orchestration/src/examples/sendAnywhere.contract.js @@ -68,6 +68,7 @@ export const start = async (zcf, privateArgs, baggage) => { zone, chainHub, makeLocalChainAccountKit, + makeRecorderKit, ...orchPowers, }); diff --git a/packages/orchestration/src/examples/stakeIca.contract.js b/packages/orchestration/src/examples/stakeIca.contract.js index cc0a6731067..5c5b14ffb1c 100644 --- a/packages/orchestration/src/examples/stakeIca.contract.js +++ b/packages/orchestration/src/examples/stakeIca.contract.js @@ -3,7 +3,10 @@ import { makeTracer, StorageNodeShape } from '@agoric/internal'; import { TimerServiceShape } from '@agoric/time'; import { V as E } from '@agoric/vow/vat.js'; -import { prepareRecorderKitMakers } from '@agoric/zoe/src/contractSupport'; +import { + prepareRecorderKitMakers, + provideAll, +} from '@agoric/zoe/src/contractSupport'; import { InvitationShape } from '@agoric/zoe/src/typeGuards.js'; import { makeDurableZone } from '@agoric/zone/durable.js'; import { M } from '@endo/patterns'; @@ -67,6 +70,10 @@ export const start = async (zcf, privateArgs, baggage) => { const zone = makeDurableZone(baggage); + const { accountsStorageNode } = await provideAll(baggage, { + accountsStorageNode: () => E(storageNode).makeChildNode('accounts'), + }); + const { makeRecorderKit } = prepareRecorderKitMakers(baggage, marshaller); const makeCosmosOrchestrationAccount = prepareCosmosOrchestrationAccount( @@ -88,9 +95,12 @@ export const start = async (zcf, privateArgs, baggage) => { const accountAddress = await E(account).getAddress(); trace('account address', accountAddress); + const accountNode = await E(accountsStorageNode).makeChildNode( + accountAddress.address, + ); const holder = makeCosmosOrchestrationAccount(accountAddress, bondDenom, { account, - storageNode, + storageNode: accountNode, icqConnection, timer, }); diff --git a/packages/orchestration/src/examples/swapExample.contract.js b/packages/orchestration/src/examples/swapExample.contract.js index a7ba4c8db2e..9b36e37d91a 100644 --- a/packages/orchestration/src/examples/swapExample.contract.js +++ b/packages/orchestration/src/examples/swapExample.contract.js @@ -2,9 +2,10 @@ import { StorageNodeShape } from '@agoric/internal'; import { TimerServiceShape } from '@agoric/time'; import { withdrawFromSeat } from '@agoric/zoe/src/contractSupport/zoeHelpers.js'; import { makeDurableZone } from '@agoric/zone/durable.js'; -import { Far } from '@endo/far'; +import { E, Far } from '@endo/far'; import { deeplyFulfilled } from '@endo/marshal'; import { M, objectMap } from '@endo/patterns'; +import { provideAll } from '@agoric/zoe/src/contractSupport'; import { prepareRecorderKitMakers } from '@agoric/zoe/src/contractSupport/recorder.js'; import { makeOrchestrationFacade } from '../facade.js'; import { orcUtils } from '../utils/orc.js'; @@ -79,16 +80,20 @@ export const start = async (zcf, privateArgs, baggage) => { timerService, chainHub, ); + const { accountsStorageNode } = await provideAll(baggage, { + accountsStorageNode: () => E(storageNode).makeChildNode('accounts'), + }); const { orchestrate } = makeOrchestrationFacade({ localchain, orchestrationService, - storageNode, + storageNode: accountsStorageNode, timerService, zcf, zone, chainHub, makeLocalChainAccountKit, + makeRecorderKit, }); /** deprecated historical example */ diff --git a/packages/orchestration/src/examples/unbondExample.contract.js b/packages/orchestration/src/examples/unbondExample.contract.js index 7bd5d2053e6..8f707e2f4ac 100644 --- a/packages/orchestration/src/examples/unbondExample.contract.js +++ b/packages/orchestration/src/examples/unbondExample.contract.js @@ -57,6 +57,7 @@ export const start = async (zcf, privateArgs, baggage) => { zone, chainHub: makeChainHub(agoricNames), makeLocalChainAccountKit, + makeRecorderKit, }); /** @type {OfferHandler} */ diff --git a/packages/orchestration/src/exos/cosmosOrchestrationAccount.js b/packages/orchestration/src/exos/cosmosOrchestrationAccount.js index 6ef7ea2ec8a..136489ac752 100644 --- a/packages/orchestration/src/exos/cosmosOrchestrationAccount.js +++ b/packages/orchestration/src/exos/cosmosOrchestrationAccount.js @@ -158,6 +158,9 @@ export const prepareCosmosOrchestrationAccountKit = ( // must be the fully synchronous maker because the kit is held in durable state // @ts-expect-error XXX Patterns const topicKit = makeRecorderKit(storageNode, PUBLIC_TOPICS.account[1]); + // TODO https://github.com/Agoric/agoric-sdk/issues/9066 + // update sequence after successful executeEncodedTx, _if we want sequence in `vstorage`_ + void E(topicKit.recorder).write(harden({ sequence: 0n })); return { chainAddress, bondDenom, topicKit, ...rest }; }, diff --git a/packages/orchestration/src/facade.js b/packages/orchestration/src/facade.js index b2e3beb3384..9567b974db9 100644 --- a/packages/orchestration/src/facade.js +++ b/packages/orchestration/src/facade.js @@ -8,6 +8,7 @@ import { prepareCosmosOrchestrationAccount } from './exos/cosmosOrchestrationAcc * @import {TimerService} from '@agoric/time'; * @import {IBCConnectionID} from '@agoric/vats'; * @import {LocalChain} from '@agoric/vats/src/localchain.js'; + * @import {RecorderKit, MakeRecorderKit} from '@agoric/zoe/src/contractSupport/recorder.js'. * @import {Remote} from '@agoric/internal'; * @import {OrchestrationService} from './service.js'; * @import {Chain, ChainInfo, CosmosChainInfo, IBCConnectionInfo, OrchestrationAccount, Orchestrator} from './types.js'; @@ -95,6 +96,8 @@ const makeLocalChainFacade = ( * @param {IBCConnectionInfo} connectionInfo * @param {object} io * @param {Remote} io.orchestration + * @param {MakeRecorderKit} io.makeRecorderKit + * @param {Remote} io.storageNode * @param {Remote} io.timer * @param {ZCF} io.zcf * @param {Zone} io.zone @@ -103,9 +106,8 @@ const makeLocalChainFacade = ( const makeRemoteChainFacade = ( chainInfo, connectionInfo, - { orchestration, timer, zcf, zone }, + { orchestration, makeRecorderKit, storageNode, timer, zcf, zone }, ) => { - const makeRecorderKit = () => anyVal; const makeCosmosOrchestrationAccount = prepareCosmosOrchestrationAccount( zone.subZone(chainInfo.chainId), makeRecorderKit, @@ -133,7 +135,7 @@ const makeRemoteChainFacade = ( // @ts-expect-error XXX dynamic method availability return makeCosmosOrchestrationAccount(address, bondDenom, { account: icaAccount, - storageNode: anyVal, + storageNode, icqConnection: anyVal, timer, }); @@ -153,6 +155,7 @@ const makeRemoteChainFacade = ( * makeLocalChainAccountKit: ReturnType< * typeof import('./exos/local-chain-account-kit.js').prepareLocalChainAccountKit * >; + * makeRecorderKit: MakeRecorderKit; * }} powers */ export const makeOrchestrationFacade = ({ @@ -164,6 +167,7 @@ export const makeOrchestrationFacade = ({ localchain, chainHub, makeLocalChainAccountKit, + makeRecorderKit, }) => { console.log('makeOrchestrationFacade got', { zone, @@ -205,6 +209,8 @@ export const makeOrchestrationFacade = ({ return makeRemoteChainFacade(remoteChainInfo, connectionInfo, { orchestration: orchestrationService, + makeRecorderKit, + storageNode, timer: timerService, zcf, zone, diff --git a/packages/orchestration/test/examples/stake-atom.contract.test.ts b/packages/orchestration/test/examples/stake-atom.contract.test.ts index 3dea9945d04..53570a3e9f9 100644 --- a/packages/orchestration/test/examples/stake-atom.contract.test.ts +++ b/packages/orchestration/test/examples/stake-atom.contract.test.ts @@ -3,9 +3,10 @@ import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js'; import { setUpZoeForTest } from '@agoric/zoe/tools/setup-zoe.js'; import { E } from '@endo/far'; import path from 'path'; +import { makeNotifierFromSubscriber } from '@agoric/notifier'; import type { Installation } from '@agoric/zoe/src/zoeService/utils.js'; import { commonSetup } from '../supports.js'; -import { type StakeAtomTerms } from '../../src/examples/stakeAtom.contract.js'; +import { type StakeIcaTerms } from '../../src/examples/stakeIca.contract.js'; const dirname = path.dirname(new URL(import.meta.url).pathname); @@ -20,11 +21,12 @@ const startContract = async ({ storage, issuerKeywordRecord, terms = { + chainId: 'cosmoshub-4', hostConnectionId: 'connection-1', controllerConnectionId: 'connection-2', bondDenom: 'uatom', icqEnabled: false, - } as StakeAtomTerms, + } as StakeIcaTerms, }) => { const { zoe, bundleAndInstall } = await setUpZoeForTest(); const installation: Installation = @@ -32,17 +34,12 @@ const startContract = async ({ const { publicFacet } = await E(zoe).startInstance( installation, - { In: bld.issuer }, - { - chainId: 'cosmoshub-4', - hostConnectionId: 'connection-1', - controllerConnectionId: 'connection-2', - bondDenom: 'uatom', - }, + issuerKeywordRecord, + terms, { marshaller, orchestration, - storageNode: storage.rootNode, + storageNode: storage.rootNode.makeChildNode('stakeAtom'), timer, }, ); @@ -78,3 +75,51 @@ test('makeAccount, getAddress, getBalances, getBalance', async t => { message: 'Queries not enabled.', }); }); + +test('makeAccountInvitationMaker', async t => { + const { + bootstrap, + brands: { ist }, + } = await commonSetup(t); + const { publicFacet, zoe } = await startContract({ + ...bootstrap, + issuerKeywordRecord: { In: ist.issuer }, + }); + const inv = await E(publicFacet).makeAccountInvitationMaker(); + t.log('make an offer for ICA account'); + t.log('inv', inv); + + const seat = await E(zoe).offer(inv); + t.log('seat', seat); + const offerResult = await E(seat).getOfferResult(); + + t.like(offerResult, { + publicSubscribers: { + account: { + description: 'Staking Account holder status', + }, + }, + }); + + const accountNotifier = makeNotifierFromSubscriber( + offerResult.publicSubscribers.account.subscriber, + ); + const storageUpdate = await E(accountNotifier).getUpdateSince(); + t.deepEqual(storageUpdate, { + updateCount: 1n, + value: { + sequence: 0n, + }, + }); + + // FIXME mock remoteAddress in ibc bridge + const storagePath = + 'mockChainStorageRoot.stakeAtom.accounts.UNPARSABLE_CHAIN_ADDRESS'; + const vstorageEntry = bootstrap.storage.data.get(storagePath); + if (typeof vstorageEntry !== 'string') { + t.fail('vstorageEntry not found'); + } else { + t.log(storagePath, vstorageEntry); + t.regex(vstorageEntry, /sequence/); + } +}); diff --git a/packages/orchestration/test/facade.test.ts b/packages/orchestration/test/facade.test.ts index 28ede33b678..e68a3cdd54f 100644 --- a/packages/orchestration/test/facade.test.ts +++ b/packages/orchestration/test/facade.test.ts @@ -1,6 +1,7 @@ import { test as anyTest } from '@agoric/zoe/tools/prepare-test-env-ava.js'; import { setupZCFTest } from '@agoric/zoe/test/unitTests/zcf/setupZcfTest.js'; +import { prepareRecorderKitMakers } from '@agoric/zoe/src/contractSupport/recorder.js'; import type { CosmosChainInfo, IBCConnectionInfo } from '../src/cosmos-api.js'; import { makeOrchestrationFacade } from '../src/facade.js'; import type { Chain } from '../src/orchestration-api.js'; @@ -48,6 +49,10 @@ test('chain info', async t => { const { zcf } = await setupZCFTest(); const chainHub = makeChainHub(facadeServices.agoricNames); + const { makeRecorderKit } = prepareRecorderKitMakers( + zone.mapStore('recorder'), + bootstrap.marshaller, + ); const { orchestrate } = makeOrchestrationFacade({ ...facadeServices, @@ -56,6 +61,7 @@ test('chain info', async t => { zone, chainHub, makeLocalChainAccountKit, + makeRecorderKit, }); chainHub.registerChain('mock', mockChainInfo); diff --git a/packages/orchestration/test/staking-ops.test.ts b/packages/orchestration/test/staking-ops.test.ts index 4059ac372f8..b1c2058b115 100644 --- a/packages/orchestration/test/staking-ops.test.ts +++ b/packages/orchestration/test/staking-ops.test.ts @@ -8,6 +8,7 @@ import { MsgUndelegateResponse, } from '@agoric/cosmic-proto/cosmos/staking/v1beta1/tx.js'; import { makeScalarBigMapStore, type Baggage } from '@agoric/vat-data'; +import { makeFakeBoard } from '@agoric/vats/tools/board-utils.js'; import { decodeBase64 } from '@endo/base64'; import { E, Far } from '@endo/far'; import { buildZoeManualTimer } from '@agoric/zoe/tools/manualTimer.js'; @@ -16,6 +17,9 @@ import type { Coin } from '@agoric/cosmic-proto/cosmos/base/v1beta1/coin.js'; import type { TimestampRecord, TimestampValue } from '@agoric/time'; import type { AnyJson } from '@agoric/cosmic-proto'; import { makeDurableZone } from '@agoric/zone/durable.js'; +import { makeFakeStorageKit } from '@agoric/internal/src/storage-test-utils.js'; +import { makeNotifierFromSubscriber } from '@agoric/notifier'; +import { prepareRecorderKitMakers } from '@agoric/zoe/src/contractSupport/recorder.js'; import { prepareCosmosOrchestrationAccountKit, trivialDelegateResponse, @@ -174,17 +178,16 @@ const makeScenario = () => { return { zcf, zoe }; }; - const makeRecorderKit = () => harden({}) as any; - const baggage = makeScalarBigMapStore('b1') as Baggage; const zone = makeDurableZone(baggage); + const marshaller = makeFakeBoard().getReadonlyMarshaller(); + const { makeRecorderKit } = prepareRecorderKitMakers(baggage, marshaller); const { delegations, startTime } = configStaking; - // TODO: when we write to chainStorage, test it. - // const { rootNode } = makeFakeStorageKit('mockChainStorageRoot'); - - const storageNode = Far('StorageNode', {}) as unknown as StorageNode; + const { rootNode } = makeFakeStorageKit('mockChainStorageRoot', { + sequence: false, + }); const icqConnection = Far('ICQConnection', {}) as ICQConnection; @@ -197,13 +200,39 @@ const makeScenario = () => { zone, makeRecorderKit, ...mockAccount(undefined, delegations), - storageNode, + storageNode: rootNode, timer, icqConnection, ...mockZCF(), }; }; +test('makeAccount() writes to storage', async t => { + const s = makeScenario(); + const { account, calls, timer } = s; + const { makeRecorderKit, storageNode, zcf, icqConnection, zone } = s; + const make = prepareCosmosOrchestrationAccountKit(zone, makeRecorderKit, zcf); + + // Higher fidelity tests below use invitationMakers. + const { holder } = make(account.getAddress(), 'uatom', { + account, + storageNode, + icqConnection, + timer, + }); + const { publicSubscribers } = holder.asContinuingOffer(); + const accountNotifier = makeNotifierFromSubscriber( + publicSubscribers.account.subscriber, + ); + const storageUpdate = await E(accountNotifier).getUpdateSince(); + t.deepEqual(storageUpdate, { + updateCount: 1n, + value: { + sequence: 0n, + }, + }); +}); + test('withdrawRewards() on StakingAccountHolder formats message correctly', async t => { const s = makeScenario(); const { account, calls, timer } = s;