From ed28c2c722a1ad25006ca158fa0e1d5baa11234c Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Tue, 18 Jun 2024 13:14:42 -0700 Subject: [PATCH] feat(orchestration): cosmosOrchestrationAccount returns unwrapped vows --- .../src/examples/stakeIca.contract.js | 5 +- .../src/exos/chain-account-kit.js | 4 +- .../src/exos/cosmos-orchestration-account.js | 217 ++++++++++++------ .../src/exos/local-orchestration-account.js | 2 +- packages/orchestration/src/facade.js | 2 +- .../orchestration/src/utils/start-helper.js | 1 + .../orchestration/test/staking-ops.test.ts | 87 +++++-- packages/orchestration/test/types.test-d.ts | 1 + 8 files changed, 218 insertions(+), 101 deletions(-) diff --git a/packages/orchestration/src/examples/stakeIca.contract.js b/packages/orchestration/src/examples/stakeIca.contract.js index e700f2754687..e257764d9141 100644 --- a/packages/orchestration/src/examples/stakeIca.contract.js +++ b/packages/orchestration/src/examples/stakeIca.contract.js @@ -2,7 +2,7 @@ import { makeTracer, StorageNodeShape } from '@agoric/internal'; import { TimerServiceShape } from '@agoric/time'; -import { V as E } from '@agoric/vow/vat.js'; +import { V as E, prepareVowTools } from '@agoric/vow/vat.js'; import { prepareRecorderKitMakers, provideAll, @@ -76,9 +76,12 @@ export const start = async (zcf, privateArgs, baggage) => { const { makeRecorderKit } = prepareRecorderKitMakers(baggage, marshaller); + const vowTools = prepareVowTools(zone.subZone('vows')); + const makeCosmosOrchestrationAccount = prepareCosmosOrchestrationAccount( zone, makeRecorderKit, + vowTools, zcf, ); diff --git a/packages/orchestration/src/exos/chain-account-kit.js b/packages/orchestration/src/exos/chain-account-kit.js index 6cec4150694c..4d14e1410eb8 100644 --- a/packages/orchestration/src/exos/chain-account-kit.js +++ b/packages/orchestration/src/exos/chain-account-kit.js @@ -137,7 +137,7 @@ export const prepareChainAccountKit = (zone, { watch, when }) => * decoded using the corresponding `Msg*Response` object. * @throws {Error} if packet fails to send or an error is returned */ - executeEncodedTx(msgs, opts) { + async executeEncodedTx(msgs, opts) { const { connection } = this.state; // TODO #9281 do not throw synchronously when returning a promise; return a rejected Vow /// see https://github.com/Agoric/agoric-sdk/pull/9454#discussion_r1626898694 @@ -150,7 +150,7 @@ export const prepareChainAccountKit = (zone, { watch, when }) => ); }, /** Close the remote account */ - close() { + async close() { /// TODO #9192 what should the behavior be here? and `onClose`? // - retrieve assets? // - revoke the port? diff --git a/packages/orchestration/src/exos/cosmos-orchestration-account.js b/packages/orchestration/src/exos/cosmos-orchestration-account.js index 529f35c77d40..9fce78f8acc9 100644 --- a/packages/orchestration/src/exos/cosmos-orchestration-account.js +++ b/packages/orchestration/src/exos/cosmos-orchestration-account.js @@ -11,7 +11,6 @@ import { import { MsgBeginRedelegate, MsgDelegate, - MsgDelegateResponse, MsgUndelegate, MsgUndelegateResponse, } from '@agoric/cosmic-proto/cosmos/staking/v1beta1/tx.js'; @@ -26,13 +25,10 @@ import { AmountArgShape, ChainAddressShape, ChainAmountShape, + CoinShape, DelegationShape, } from '../typeGuards.js'; -import { - encodeTxResponse, - maxClockSkew, - tryDecodeResponse, -} from '../utils/cosmos.js'; +import { maxClockSkew, tryDecodeResponse } from '../utils/cosmos.js'; import { orchestrationAccountMethods } from '../utils/orchestrationAccount.js'; import { dateInSeconds } from '../utils/time.js'; @@ -43,7 +39,10 @@ import { dateInSeconds } from '../utils/time.js'; * @import {Delegation} from '@agoric/cosmic-proto/cosmos/staking/v1beta1/staking.js'; * @import {Remote} from '@agoric/internal'; * @import {TimerService} from '@agoric/time'; + * @import {VowTools} from '@agoric/vow'; * @import {Zone} from '@agoric/zone'; + * @import {ResponseQuery} from '@agoric/cosmic-proto/tendermint/abci/types.js'; + * @import {JsonSafe} from '@agoric/cosmic-proto'; */ const trace = makeTracer('ComosOrchestrationAccountHolder'); @@ -94,28 +93,19 @@ const PUBLIC_TOPICS = { account: ['Staking Account holder status', M.any()], }; -export const trivialDelegateResponse = encodeTxResponse( - {}, - MsgDelegateResponse.toProtoMsg, -); - -const expect = (actual, expected, message) => { - if (actual !== expected) { - console.log(message, { actual, expected }); - } -}; - /** @type {(c: { denom: string; amount: string }) => DenomAmount} */ const toDenomAmount = c => ({ denom: c.denom, value: BigInt(c.amount) }); /** * @param {Zone} zone * @param {MakeRecorderKit} makeRecorderKit + * @param {VowTools} vowTools * @param {ZCF} zcf */ export const prepareCosmosOrchestrationAccountKit = ( zone, makeRecorderKit, + { when, watch }, zcf, ) => { const makeCosmosOrchestrationAccountKit = zone.exoClassKit( @@ -126,6 +116,26 @@ export const prepareCosmosOrchestrationAccountKit = ( getUpdater: M.call().returns(M.remotable()), amountToCoin: M.call(AmountArgShape).returns(M.record()), }), + returnVoidWatcher: M.interface('returnVoidWatcher', { + onFulfilled: M.call(M.or(M.string(), M.record())) + .optional(M.arrayOf(M.undefined())) + .returns(M.undefined()), + }), + balanceQueryWatcher: M.interface('balanceQueryWatcher', { + onFulfilled: M.call(M.arrayOf(M.record())) + .optional(M.arrayOf(M.undefined())) // empty context + .returns(M.or(M.record(), M.undefined())), + }), + undelegateWatcher: M.interface('undelegateWatcher', { + onFulfilled: M.call(M.string()) + .optional(M.arrayOf(M.undefined())) // empty context + .returns(M.promise()), + }), + withdrawRewardWatcher: M.interface('withdrawRewardWatcher', { + onFulfilled: M.call(M.string()) + .optional(M.arrayOf(M.undefined())) // empty context + .returns(M.arrayOf(CoinShape)), + }), holder: IcaAccountHolderI, invitationMakers: M.interface('invitationMakers', { Delegate: M.callWhen(ChainAddressShape, AmountArgShape).returns( @@ -195,6 +205,59 @@ export const prepareCosmosOrchestrationAccountKit = ( }); }, }, + balanceQueryWatcher: { + /** + * @param {JsonSafe[]} results + */ + onFulfilled([result]) { + if (!result?.key) throw Fail`Error parsing result ${result}`; + const { balance } = QueryBalanceResponse.decode( + decodeBase64(result.key), + ); + if (!balance) throw Fail`Result lacked balance key: ${result}`; + return harden(toDenomAmount(balance)); + }, + }, + undelegateWatcher: { + /** + * @param {string} result + */ + onFulfilled(result) { + const response = tryDecodeResponse( + result, + MsgUndelegateResponse.fromProtoMsg, + ); + trace('undelegate response', response); + const { completionTime } = response; + completionTime || Fail`No completion time result ${result}`; + return E(this.state.timer).wakeAt( + dateInSeconds(completionTime) + maxClockSkew, + ); + }, + }, + /** + * takes an array of results (from `executeEncodedTx`) and returns void + * since we are not interested in the result + */ + returnVoidWatcher: { + /** @param {string | Record} result */ + onFulfilled(result) { + trace('Result', result); + return undefined; + }, + }, + withdrawRewardWatcher: { + /** @param {string} result */ + onFulfilled(result) { + const response = tryDecodeResponse( + result, + MsgWithdrawDelegatorRewardResponse.fromProtoMsg, + ); + trace('withdrawReward response', response); + const { amount: coins } = response; + return harden(coins.map(toDenomAmount)); + }, + }, invitationMakers: { /** * @param {CosmosValidatorAddress} validator @@ -292,17 +355,20 @@ export const prepareCosmosOrchestrationAccountKit = ( const { helper } = this.facets; const { chainAddress } = this.state; - const result = await E(helper.owned()).executeEncodedTx([ - Any.toJSON( - MsgDelegate.toProtoMsg({ - delegatorAddress: chainAddress.address, - validatorAddress: validator.address, - amount: helper.amountToCoin(amount), - }), + return when( + watch( + E(helper.owned()).executeEncodedTx([ + Any.toJSON( + MsgDelegate.toProtoMsg({ + delegatorAddress: chainAddress.address, + validatorAddress: validator.address, + amount: helper.amountToCoin(amount), + }), + ), + ]), + this.facets.returnVoidWatcher, ), - ]); - - expect(result, trivialDelegateResponse, 'MsgDelegateResponse'); + ); }, async deposit(payment) { trace('deposit', payment); @@ -325,19 +391,23 @@ export const prepareCosmosOrchestrationAccountKit = ( const { helper } = this.facets; const { chainAddress } = this.state; - // NOTE: response, including completionTime, is currently discarded. - await E(helper.owned()).executeEncodedTx([ - Any.toJSON( - MsgBeginRedelegate.toProtoMsg({ - delegatorAddress: chainAddress.address, - validatorSrcAddress: srcValidator.address, - validatorDstAddress: dstValidator.address, - amount: helper.amountToCoin(amount), - }), + return when( + watch( + E(helper.owned()).executeEncodedTx([ + Any.toJSON( + MsgBeginRedelegate.toProtoMsg({ + delegatorAddress: chainAddress.address, + validatorSrcAddress: srcValidator.address, + validatorDstAddress: dstValidator.address, + amount: helper.amountToCoin(amount), + }), + ), + ]), + // NOTE: response, including completionTime, is currently discarded. + this.facets.returnVoidWatcher, ), - ]); + ); }, - /** * @param {CosmosValidatorAddress} validator * @returns {Promise} @@ -351,14 +421,13 @@ export const prepareCosmosOrchestrationAccountKit = ( validatorAddress: validator.address, }); const account = helper.owned(); - const result = await E(account).executeEncodedTx([Any.toJSON(msg)]); - const response = tryDecodeResponse( - result, - MsgWithdrawDelegatorRewardResponse.fromProtoMsg, + + return when( + watch( + E(account).executeEncodedTx([Any.toJSON(msg)]), + this.facets.withdrawRewardWatcher, + ), ); - trace('withdrawReward response', response); - const { amount: coins } = response; - return harden(coins.map(toDenomAmount)); }, /** * @param {DenomArg} denom @@ -372,20 +441,19 @@ export const prepareCosmosOrchestrationAccountKit = ( // TODO #9211 lookup denom from brand assert.typeof(denom, 'string'); - const [result] = await E(icqConnection).query([ - toRequestQueryJson( - QueryBalanceRequest.toProtoMsg({ - address: chainAddress.address, - denom, - }), + return when( + watch( + E(icqConnection).query([ + toRequestQueryJson( + QueryBalanceRequest.toProtoMsg({ + address: chainAddress.address, + denom, + }), + ), + ]), + this.facets.balanceQueryWatcher, ), - ]); - if (!result?.key) throw Fail`Error parsing result ${result}`; - const { balance } = QueryBalanceResponse.decode( - decodeBase64(result.key), ); - if (!balance) throw Fail`Result lacked balance key: ${result}`; - return harden(toDenomAmount(balance)); }, send(toAccount, amount) { @@ -411,28 +479,24 @@ export const prepareCosmosOrchestrationAccountKit = ( async undelegate(delegations) { trace('undelegate', delegations); const { helper } = this.facets; - const { chainAddress, bondDenom, timer } = this.state; - - const result = await E(helper.owned()).executeEncodedTx( - delegations.map(d => - Any.toJSON( - MsgUndelegate.toProtoMsg({ - delegatorAddress: chainAddress.address, - validatorAddress: d.validatorAddress, - amount: { denom: bondDenom, amount: d.shares }, - }), + const { chainAddress, bondDenom } = this.state; + + const undelegateV = watch( + E(helper.owned()).executeEncodedTx( + delegations.map(d => + Any.toJSON( + MsgUndelegate.toProtoMsg({ + delegatorAddress: chainAddress.address, + validatorAddress: d.validatorAddress, + amount: { denom: bondDenom, amount: d.shares }, + }), + ), ), ), + this.facets.undelegateWatcher, ); - const response = tryDecodeResponse( - result, - MsgUndelegateResponse.fromProtoMsg, - ); - trace('undelegate response', response); - const { completionTime } = response; - - await E(timer).wakeAt(dateInSeconds(completionTime) + maxClockSkew); + return when(watch(undelegateV, this.facets.returnVoidWatcher)); }, }, }, @@ -450,6 +514,7 @@ export const prepareCosmosOrchestrationAccountKit = ( /** * @param {Zone} zone * @param {MakeRecorderKit} makeRecorderKit + * @param {VowTools} vowTools * @param {ZCF} zcf * @returns {( * ...args: Parameters< @@ -460,11 +525,13 @@ export const prepareCosmosOrchestrationAccountKit = ( export const prepareCosmosOrchestrationAccount = ( zone, makeRecorderKit, + vowTools, zcf, ) => { const makeKit = prepareCosmosOrchestrationAccountKit( zone, makeRecorderKit, + vowTools, zcf, ); return (...args) => makeKit(...args).holder; diff --git a/packages/orchestration/src/exos/local-orchestration-account.js b/packages/orchestration/src/exos/local-orchestration-account.js index 65cb95277c43..df1cb5578342 100644 --- a/packages/orchestration/src/exos/local-orchestration-account.js +++ b/packages/orchestration/src/exos/local-orchestration-account.js @@ -110,7 +110,7 @@ export const prepareLocalOrchestrationAccountKit = ( .optional(M.arrayOf(M.undefined())) .returns(M.record()), }), - returnVoidWatcher: M.interface('extractFirstResultWatcher', { + returnVoidWatcher: M.interface('returnVoidWatcher', { onFulfilled: M.call(M.arrayOf(M.record())) .optional(M.arrayOf(M.undefined())) .returns(M.undefined()), diff --git a/packages/orchestration/src/facade.js b/packages/orchestration/src/facade.js index 2043346b14f2..6de79caa23f7 100644 --- a/packages/orchestration/src/facade.js +++ b/packages/orchestration/src/facade.js @@ -7,7 +7,7 @@ import { prepareOrchestrator } from './exos/orchestrator.js'; /** * @import {AsyncFlowTools} from '@agoric/async-flow'; * @import {Zone} from '@agoric/zone'; - * @import {Vow} from '@agoric/vow'; + * @import {Vow, VowTools} from '@agoric/vow'; * @import {TimerService} from '@agoric/time'; * @import {IBCConnectionID} from '@agoric/vats'; * @import {LocalChain} from '@agoric/vats/src/localchain.js'; diff --git a/packages/orchestration/src/utils/start-helper.js b/packages/orchestration/src/utils/start-helper.js index 3c301281a182..8ec73eb8bead 100644 --- a/packages/orchestration/src/utils/start-helper.js +++ b/packages/orchestration/src/utils/start-helper.js @@ -68,6 +68,7 @@ export const provideOrchestration = ( // FIXME what zone? zone, makeRecorderKit, + vowTools, zcf, ); diff --git a/packages/orchestration/test/staking-ops.test.ts b/packages/orchestration/test/staking-ops.test.ts index 8bbe71c63885..ed8f99868c4c 100644 --- a/packages/orchestration/test/staking-ops.test.ts +++ b/packages/orchestration/test/staking-ops.test.ts @@ -1,5 +1,7 @@ import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js'; +import type { AnyJson } from '@agoric/cosmic-proto'; +import type { Coin } from '@agoric/cosmic-proto/cosmos/base/v1beta1/coin.js'; import { MsgWithdrawDelegatorRewardResponse } from '@agoric/cosmic-proto/cosmos/distribution/v1beta1/tx.js'; import { MsgBeginRedelegateResponse, @@ -7,28 +9,29 @@ import { MsgDelegateResponse, MsgUndelegateResponse, } from '@agoric/cosmic-proto/cosmos/staking/v1beta1/tx.js'; +import { makeFakeStorageKit } from '@agoric/internal/src/storage-test-utils.js'; +import { eventLoopIteration } from '@agoric/internal/src/testing-utils.js'; +import { makeNotifierFromSubscriber } from '@agoric/notifier'; +import type { TimestampRecord, TimestampValue } from '@agoric/time'; 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 { prepareVowTools } from '@agoric/vow/vat.js'; +import { prepareRecorderKitMakers } from '@agoric/zoe/src/contractSupport/recorder.js'; import { buildZoeManualTimer } from '@agoric/zoe/tools/manualTimer.js'; -import { eventLoopIteration } from '@agoric/internal/src/testing-utils.js'; -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, -} from '../src/exos/cosmos-orchestration-account.js'; +import { decodeBase64 } from '@endo/base64'; +import { E, Far } from '@endo/far'; +import { prepareCosmosOrchestrationAccountKit } from '../src/exos/cosmos-orchestration-account.js'; +import type { ChainAddress, IcaAccount, ICQConnection } from '../src/types.js'; import { encodeTxResponse } from '../src/utils/cosmos.js'; -import type { IcaAccount, ChainAddress, ICQConnection } from '../src/types.js'; const { Fail } = assert; +const trivialDelegateResponse = encodeTxResponse( + {}, + MsgDelegateResponse.toProtoMsg, +); + test('MsgDelegateResponse trivial response', t => { t.is( trivialDelegateResponse, @@ -189,6 +192,8 @@ const makeScenario = () => { sequence: false, }); + const vowTools = prepareVowTools(zone.subZone('VowTools')); + const icqConnection = Far('ICQConnection', {}) as ICQConnection; const timer = buildZoeManualTimer(undefined, time.parse(startTime), { @@ -203,6 +208,7 @@ const makeScenario = () => { storageNode: rootNode, timer, icqConnection, + vowTools, ...mockZCF(), }; }; @@ -210,8 +216,14 @@ const makeScenario = () => { test('makeAccount() writes to storage', async t => { const s = makeScenario(); const { account, timer } = s; - const { makeRecorderKit, storageNode, zcf, icqConnection, zone } = s; - const make = prepareCosmosOrchestrationAccountKit(zone, makeRecorderKit, zcf); + const { makeRecorderKit, storageNode, zcf, icqConnection, vowTools, zone } = + s; + const make = prepareCosmosOrchestrationAccountKit( + zone, + makeRecorderKit, + vowTools, + zcf, + ); const { holder } = make(account.getAddress(), 'uatom', { account, @@ -233,8 +245,14 @@ test('makeAccount() writes to storage', async t => { test('withdrawRewards() on StakingAccountHolder formats message correctly', async t => { const s = makeScenario(); const { account, calls, timer } = s; - const { makeRecorderKit, storageNode, zcf, icqConnection, zone } = s; - const make = prepareCosmosOrchestrationAccountKit(zone, makeRecorderKit, zcf); + const { makeRecorderKit, storageNode, zcf, icqConnection, vowTools, zone } = + s; + const make = prepareCosmosOrchestrationAccountKit( + zone, + makeRecorderKit, + vowTools, + zcf, + ); // Higher fidelity tests below use invitationMakers. const { holder } = make(account.getAddress(), 'uatom', { @@ -256,11 +274,20 @@ test('withdrawRewards() on StakingAccountHolder formats message correctly', asyn test(`delegate; redelegate using invitationMakers`, async t => { const s = makeScenario(); const { account, calls, timer } = s; - const { makeRecorderKit, storageNode, zcf, zoe, icqConnection, zone } = s; + const { + makeRecorderKit, + storageNode, + zcf, + zoe, + icqConnection, + vowTools, + zone, + } = s; const aBrand = Far('Token') as Brand<'nat'>; const makeAccountKit = prepareCosmosOrchestrationAccountKit( zone, makeRecorderKit, + vowTools, zcf, ); @@ -329,10 +356,19 @@ test(`delegate; redelegate using invitationMakers`, async t => { test(`withdraw rewards using invitationMakers`, async t => { const s = makeScenario(); const { account, calls, timer } = s; - const { makeRecorderKit, storageNode, zcf, zoe, icqConnection, zone } = s; + const { + makeRecorderKit, + storageNode, + zcf, + zoe, + icqConnection, + vowTools, + zone, + } = s; const makeAccountKit = prepareCosmosOrchestrationAccountKit( zone, makeRecorderKit, + vowTools, zcf, ); @@ -359,10 +395,19 @@ test(`withdraw rewards using invitationMakers`, async t => { test(`undelegate waits for unbonding period`, async t => { const s = makeScenario(); const { account, calls, timer } = s; - const { makeRecorderKit, storageNode, zcf, zoe, icqConnection, zone } = s; + const { + makeRecorderKit, + storageNode, + zcf, + zoe, + icqConnection, + vowTools, + zone, + } = s; const makeAccountKit = prepareCosmosOrchestrationAccountKit( zone, makeRecorderKit, + vowTools, zcf, ); diff --git a/packages/orchestration/test/types.test-d.ts b/packages/orchestration/test/types.test-d.ts index cfc5989588b4..a880f300038d 100644 --- a/packages/orchestration/test/types.test-d.ts +++ b/packages/orchestration/test/types.test-d.ts @@ -60,6 +60,7 @@ expectNotType(chainAddr); anyVal, anyVal, anyVal, + anyVal, ); makeCosmosOrchestrationAccount( anyVal,