From f76fb10cd4b7d76b97fcef86b180542e2abd576e Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Tue, 25 Jun 2024 18:06:57 -0400 Subject: [PATCH 01/19] lint: warn when importing heapVowE in resumable code --- .eslintrc.cjs | 10 ++++++++++ packages/orchestration/src/exos/chain-hub.js | 2 ++ 2 files changed, 12 insertions(+) diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 8d8b6c3ad30..005f64f6b21 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -46,6 +46,16 @@ const resumable = [ message: 'callWhen wraps the function in a promise; instead immediately return a vow', }, + { + selector: "Identifier[name='heapVowE']", + message: + 'heapVowE shortens vows to promises; instead use `E` from `@endo/far` with `watch` from durable vowTools', + }, + { + selector: "Identifier[name='heapVowTools']", + message: + 'heapVowTools are not durable; instead use `prepareVowTools` with a durable zone', + }, ]; module.exports = { diff --git a/packages/orchestration/src/exos/chain-hub.js b/packages/orchestration/src/exos/chain-hub.js index e59e6c7e45b..55a178cc610 100644 --- a/packages/orchestration/src/exos/chain-hub.js +++ b/packages/orchestration/src/exos/chain-hub.js @@ -1,4 +1,5 @@ import { VowShape } from '@agoric/vow'; +// eslint-disable-next-line no-restricted-syntax import { heapVowTools } from '@agoric/vow/vat.js'; import { makeHeapZone } from '@agoric/zone'; import { E } from '@endo/far'; @@ -6,6 +7,7 @@ import { M } from '@endo/patterns'; import { CosmosChainInfoShape, IBCConnectionInfoShape } from '../typeGuards.js'; // FIXME test thoroughly whether heap suffices for ChainHub +// eslint-disable-next-line no-restricted-syntax const { allVows, watch } = heapVowTools; const { Fail } = assert; From 9404b43785c92b70764d2ed9aa9370e0956aa2fa Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Tue, 25 Jun 2024 18:08:20 -0400 Subject: [PATCH 02/19] types(orchestration): PromiseToVow helper - allows us to reference the idealized API which typically returns Promises in code that returns Vows --- packages/orchestration/src/internal.ts | 5 +++++ packages/orchestration/src/types.ts | 1 + 2 files changed, 6 insertions(+) create mode 100644 packages/orchestration/src/internal.ts diff --git a/packages/orchestration/src/internal.ts b/packages/orchestration/src/internal.ts new file mode 100644 index 00000000000..19a2daf54e1 --- /dev/null +++ b/packages/orchestration/src/internal.ts @@ -0,0 +1,5 @@ +import type { Vow } from '@agoric/vow'; + +export type PromiseToVow = T extends (...args: infer A) => Promise + ? (...args: A) => Vow + : never; diff --git a/packages/orchestration/src/types.ts b/packages/orchestration/src/types.ts index 42c0f81af80..a9a17cc8905 100644 --- a/packages/orchestration/src/types.ts +++ b/packages/orchestration/src/types.ts @@ -5,6 +5,7 @@ export type * from './cosmos-api.js'; export type * from './ethereum-api.js'; export type * from './exos/chain-account-kit.js'; export type * from './exos/icq-connection-kit.js'; +export type * from './internal.js'; export type * from './orchestration-api.js'; export type * from './service.js'; export type * from './vat-orchestration.js'; From e8bbaec3bf57746c40d73abf1d15d83b37bba45b Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Tue, 25 Jun 2024 18:35:31 -0400 Subject: [PATCH 03/19] chore(orchestration): ensure local-orchestration-account is resumable --- .../src/exos/local-orchestration-account.js | 256 ++++++++++-------- .../src/utils/orchestrationAccount.js | 16 +- 2 files changed, 157 insertions(+), 115 deletions(-) diff --git a/packages/orchestration/src/exos/local-orchestration-account.js b/packages/orchestration/src/exos/local-orchestration-account.js index c183bbd06d8..ecf516a9993 100644 --- a/packages/orchestration/src/exos/local-orchestration-account.js +++ b/packages/orchestration/src/exos/local-orchestration-account.js @@ -2,14 +2,17 @@ import { typedJson } from '@agoric/cosmic-proto/vatsafe'; import { AmountShape, PaymentShape } from '@agoric/ertp'; import { makeTracer } from '@agoric/internal'; +import { Shape as NetworkShape } from '@agoric/network'; import { M } from '@agoric/vat-data'; import { VowShape } from '@agoric/vow'; -import { heapVowE as E } from '@agoric/vow/vat.js'; import { TopicsRecordShape } from '@agoric/zoe/src/contractSupport/index.js'; import { InvitationShape } from '@agoric/zoe/src/typeGuards.js'; +import { E } from '@endo/far'; import { ChainAddressShape, ChainAmountShape, + DenomAmountShape, + DenomShape, IBCTransferOptionsShape, } from '../typeGuards.js'; import { maxClockSkew } from '../utils/cosmos.js'; @@ -18,13 +21,13 @@ import { dateInSeconds, makeTimestampHelper } from '../utils/time.js'; /** * @import {LocalChainAccount} from '@agoric/vats/src/localchain.js'; - * @import {AmountArg, ChainAddress, DenomAmount, IBCMsgTransferOptions, OrchestrationAccount, ChainInfo, IBCConnectionInfo} from '@agoric/orchestration'; + * @import {AmountArg, ChainAddress, DenomAmount, IBCMsgTransferOptions, OrchestrationAccount, ChainInfo, IBCConnectionInfo, PromiseToVow} from '@agoric/orchestration'; * @import {RecorderKit, MakeRecorderKit} from '@agoric/zoe/src/contractSupport/recorder.js'. * @import {Zone} from '@agoric/zone'; * @import {Remote} from '@agoric/internal'; * @import {TimerService, TimerBrand, TimestampRecord} from '@agoric/time'; - * @import {PromiseVow, VowTools} from '@agoric/vow'; - * @import {TypedJson} from '@agoric/cosmic-proto'; + * @import {PromiseVow, Vow, VowTools} from '@agoric/vow'; + * @import {TypedJson, JsonSafe} from '@agoric/cosmic-proto'; * @import {ChainHub} from './chain-hub.js'; */ @@ -47,10 +50,12 @@ const { Fail } = assert; const HolderI = M.interface('holder', { ...orchestrationAccountMethods, getPublicTopics: M.call().returns(TopicsRecordShape), - delegate: M.call(M.string(), AmountShape).returns(M.promise()), - undelegate: M.call(M.string(), AmountShape).returns(M.promise()), - withdraw: M.callWhen(AmountShape).returns(PaymentShape), - executeTx: M.callWhen(M.arrayOf(M.record())).returns(M.arrayOf(M.record())), + delegate: M.call(M.string(), AmountShape).returns(VowShape), + undelegate: M.call(M.string(), AmountShape).returns(VowShape), + withdraw: M.call(AmountShape).returns(NetworkShape.Vow$(PaymentShape)), + executeTx: M.call(M.arrayOf(M.record())).returns( + NetworkShape.Vow$(M.record()), + ), }); /** @type {{ [name: string]: [description: string, valueShape: Pattern] }} */ @@ -71,7 +76,7 @@ export const prepareLocalOrchestrationAccountKit = ( makeRecorderKit, zcf, timerService, - { watch, when, allVows }, + { watch, allVows, asVow }, chainHub, ) => { const timestampHelper = makeTimestampHelper(timerService); @@ -82,7 +87,7 @@ export const prepareLocalOrchestrationAccountKit = ( { holder: HolderI, undelegateWatcher: M.interface('undelegateWatcher', { - onFulfilled: M.call(M.arrayOf(M.record())) // XXX consider specifying `completionTime` + onFulfilled: M.call(M.arrayOf(M.record())) .optional(M.arrayOf(M.undefined())) // empty context .returns(M.promise()), }), @@ -111,10 +116,17 @@ export const prepareLocalOrchestrationAccountKit = ( .returns(M.record()), }), returnVoidWatcher: M.interface('returnVoidWatcher', { - onFulfilled: M.call(M.arrayOf(M.record())) + onFulfilled: M.call(M.any()) .optional(M.arrayOf(M.undefined())) .returns(M.undefined()), }), + getBalanceWatcher: M.interface('getBalanceWatcher', { + onFulfilled: M.call(AmountShape) + .optional({ + denom: DenomShape, + }) + .returns(DenomAmountShape), + }), invitationMakers: M.interface('invitationMakers', { Delegate: M.callWhen(M.string(), AmountShape).returns(InvitationShape), Undelegate: M.callWhen(M.string(), AmountShape).returns( @@ -173,14 +185,18 @@ export const prepareLocalOrchestrationAccountKit = ( undelegateWatcher: { /** * @param {[ - * TypedJson<'/cosmos.staking.v1beta1.MsgUndelegateResponse'>, + * JsonSafe< + * TypedJson<'/cosmos.staking.v1beta1.MsgUndelegateResponse'> + * >, * ]} response */ onFulfilled(response) { const { completionTime } = response[0]; - return E(timerService).wakeAt( - // TODO clean up date handling once we have real data - dateInSeconds(new Date(completionTime)) + maxClockSkew, + return watch( + E(timerService).wakeAt( + // TODO clean up date handling once we have real data + dateInSeconds(new Date(completionTime)) + maxClockSkew, + ), ); }, }, @@ -214,7 +230,7 @@ export const prepareLocalOrchestrationAccountKit = ( * @param {[ * { transferChannel: IBCConnectionInfo['transferChannel'] }, * bigint, - * ]} params + * ]} results * @param {{ * destination: ChainAddress; * opts: IBCMsgTransferOptions; @@ -225,24 +241,26 @@ export const prepareLocalOrchestrationAccountKit = ( [{ transferChannel }, timeoutTimestamp], { opts, amount, destination }, ) { - return E(this.state.account).executeTx([ - typedJson('/ibc.applications.transfer.v1.MsgTransfer', { - sourcePort: transferChannel.portId, - sourceChannel: transferChannel.channelId, - token: { - amount: String(amount.value), - denom: amount.denom, - }, - sender: this.state.address.address, - receiver: destination.address, - timeoutHeight: opts?.timeoutHeight ?? { - revisionHeight: 0n, - revisionNumber: 0n, - }, - timeoutTimestamp, - memo: opts?.memo ?? '', - }), - ]); + return watch( + E(this.state.account).executeTx([ + typedJson('/ibc.applications.transfer.v1.MsgTransfer', { + sourcePort: transferChannel.portId, + sourceChannel: transferChannel.channelId, + token: { + amount: String(amount.value), + denom: amount.denom, + }, + sender: this.state.address.address, + receiver: destination.address, + timeoutHeight: opts?.timeoutHeight ?? { + revisionHeight: 0n, + revisionNumber: 0n, + }, + timeoutTimestamp, + memo: opts?.memo ?? '', + }), + ]), + ); }, }, /** @@ -265,18 +283,33 @@ export const prepareLocalOrchestrationAccountKit = ( */ returnVoidWatcher: { /** - * @param {Record[]} results + * @param {any} _result */ - onFulfilled(results) { - results.length === 1 || - Fail`expected exactly one result; got ${results}`; - trace('Result', results[0]); + onFulfilled(_result) { return undefined; }, }, + /** + * handles a request for balance from a bank purse and returns the balance + * as a Chain Amount + */ + getBalanceWatcher: { + /** + * @param {Amount<'nat'>} natAmount + * @param {{ denom: DenomAmount['denom'] }} ctx + * @returns {DenomAmount} + */ + onFulfilled(natAmount, { denom }) { + return harden({ denom, value: natAmount.value }); + }, + }, holder: { - /** @type {OrchestrationAccount['getBalance']} */ - async getBalance(denomArg) { + /** + * TODO: balance lookups for non-vbank assets + * + * @type {PromiseToVow['getBalance']>} + */ + getBalance(denomArg) { // FIXME look up real values // UNTIL https://github.com/Agoric/agoric-sdk/issues/9211 const [brand, denom] = @@ -284,10 +317,11 @@ export const prepareLocalOrchestrationAccountKit = ( ? [/** @type {any} */ (null), denomArg] : [denomArg, 'FIXME']; - const natAmount = await E.when( + return watch( E(this.state.account).getBalance(brand), + this.facets.getBalanceWatcher, + { denom }, ); - return harden({ denom, value: natAmount.value }); }, getBalances() { throw new Error('not yet implemented'); @@ -307,7 +341,7 @@ export const prepareLocalOrchestrationAccountKit = ( * @param {string} validatorAddress * @param {Amount<'nat'>} ertpAmount */ - async delegate(validatorAddress, ertpAmount) { + delegate(validatorAddress, ertpAmount) { // TODO #9211 lookup denom from brand const amount = { amount: String(ertpAmount.value), @@ -315,68 +349,70 @@ export const prepareLocalOrchestrationAccountKit = ( }; const { account: lca } = this.state; - const results = E(lca).executeTx([ - typedJson('/cosmos.staking.v1beta1.MsgDelegate', { - amount, - validatorAddress, - delegatorAddress: this.state.address.address, - }), - ]); - - return when(watch(results, this.facets.extractFirstResultWatcher)); + return watch( + E(lca).executeTx([ + typedJson('/cosmos.staking.v1beta1.MsgDelegate', { + amount, + validatorAddress, + delegatorAddress: this.state.address.address, + }), + ]), + this.facets.extractFirstResultWatcher, + ); }, /** * @param {string} validatorAddress * @param {Amount<'nat'>} ertpAmount - * @returns {PromiseVow} + * @returns {Vow} */ - async undelegate(validatorAddress, ertpAmount) { + undelegate(validatorAddress, ertpAmount) { // TODO #9211 lookup denom from brand const amount = { amount: String(ertpAmount.value), denom: 'ubld', }; const { account: lca } = this.state; - /** @type {any} XXX heapVowE */ - const results = E(lca).executeTx([ - typedJson('/cosmos.staking.v1beta1.MsgUndelegate', { - amount, - validatorAddress, - delegatorAddress: this.state.address.address, - }), - ]); - return when(watch(results, this.facets.undelegateWatcher)); + return watch( + E(lca).executeTx([ + typedJson('/cosmos.staking.v1beta1.MsgUndelegate', { + amount, + validatorAddress, + delegatorAddress: this.state.address.address, + }), + ]), + this.facets.undelegateWatcher, + ); }, /** * Starting a transfer revokes the account holder. The associated * updater will get a special notification that the account is being * transferred. */ - /** @type {OrchestrationAccount['deposit']} */ - async deposit(payment) { - return when( - watch( - E(this.state.account) - .deposit(payment) - .then(() => {}), - ), + /** @type {PromiseToVow['deposit']>} */ + deposit(payment) { + return watch( + E(this.state.account).deposit(payment), + this.facets.returnVoidWatcher, ); }, - /** @type {LocalChainAccount['withdraw']} */ - async withdraw(amount) { - return when(watch(E(this.state.account).withdraw(amount))); + /** @type {PromiseToVow} */ + withdraw(amount) { + return watch(E(this.state.account).withdraw(amount)); }, - /** @type {LocalChainAccount['executeTx']} */ - async executeTx(messages) { - return when(watch(E(this.state.account).executeTx(messages))); + /** @type {PromiseToVow} */ + executeTx(messages) { + return watch(E(this.state.account).executeTx(messages)); }, /** @returns {ChainAddress} */ getAddress() { return this.state.address; }, - async send(toAccount, amount) { - // FIXME implement - console.log('send got', toAccount, amount); + send(toAccount, amount) { + return asVow(() => { + // FIXME implement + console.log('send got', toAccount, amount); + throw Fail`send not yet implemented`; + }); }, /** * @param {AmountArg} amount an ERTP {@link Amount} or a @@ -385,39 +421,43 @@ export const prepareLocalOrchestrationAccountKit = ( * @param {IBCMsgTransferOptions} [opts] if either timeoutHeight or * timeoutTimestamp are not supplied, a default timeoutTimestamp will * be set for 5 minutes in the future - * @returns {Promise} + * @returns {Vow} */ - async transfer(amount, destination, opts) { - trace('Transferring funds from LCA over IBC'); - // TODO #9211 lookup denom from brand - if ('brand' in amount) throw Fail`ERTP Amounts not yet supported`; + transfer(amount, destination, opts) { + return asVow(() => { + trace('Transferring funds from LCA over IBC'); + // TODO #9211 lookup denom from brand + if ('brand' in amount) throw Fail`ERTP Amounts not yet supported`; - const connectionInfoV = watch( - chainHub.getChainInfo('agoric'), - this.facets.getChainInfoWatcher, - { destination }, - ); + const connectionInfoV = watch( + chainHub.getChainInfo('agoric'), + this.facets.getChainInfoWatcher, + { destination }, + ); - // set a `timeoutTimestamp` if caller does not supply either `timeoutHeight` or `timeoutTimestamp` - // TODO #9324 what's a reasonable default? currently 5 minutes - // FIXME: do not call `getTimeoutTimestampNS` if `opts.timeoutTimestamp` or `opts.timeoutHeight` is provided - const timeoutTimestampV = watch( - timestampHelper.getTimeoutTimestampNS(), - this.facets.getTimeoutTimestampWatcher, - { opts }, - ); + // set a `timeoutTimestamp` if caller does not supply either `timeoutHeight` or `timeoutTimestamp` + // TODO #9324 what's a reasonable default? currently 5 minutes + // FIXME: do not call `getTimeoutTimestampNS` if `opts.timeoutTimestamp` or `opts.timeoutHeight` is provided + const timeoutTimestampV = watch( + timestampHelper.getTimeoutTimestampNS(), + this.facets.getTimeoutTimestampWatcher, + { opts }, + ); - const transferV = watch( - allVows([connectionInfoV, timeoutTimestampV]), - this.facets.transferWatcher, - { opts, amount, destination }, - ); - return when(watch(transferV, this.facets.returnVoidWatcher)); + const transferV = watch( + allVows([connectionInfoV, timeoutTimestampV]), + this.facets.transferWatcher, + { opts, amount, destination }, + ); + return watch(transferV, this.facets.returnVoidWatcher); + }); }, - /** @type {OrchestrationAccount['transferSteps']} */ + /** @type {PromiseToVow['transferSteps']>} */ transferSteps(amount, msg) { - console.log('transferSteps got', amount, msg); - return Promise.resolve(); + return asVow(() => { + console.log('transferSteps got', amount, msg); + throw Fail`not yet implemented`; + }); }, }, }, diff --git a/packages/orchestration/src/utils/orchestrationAccount.js b/packages/orchestration/src/utils/orchestrationAccount.js index b4824919706..f0d5bc1bdf0 100644 --- a/packages/orchestration/src/utils/orchestrationAccount.js +++ b/packages/orchestration/src/utils/orchestrationAccount.js @@ -1,5 +1,7 @@ import { M } from '@endo/patterns'; import { PaymentShape } from '@agoric/ertp'; +import { Shape as NetworkShape } from '@agoric/network'; +import { VowShape } from '@agoric/vow'; import { AmountArgShape, ChainAddressShape, CoinShape } from '../typeGuards.js'; /** @import {OrchestrationAccountI} from '../orchestration-api.js'; */ @@ -7,12 +9,12 @@ import { AmountArgShape, ChainAddressShape, CoinShape } from '../typeGuards.js'; /** @see {OrchestrationAccountI} */ export const orchestrationAccountMethods = { getAddress: M.call().returns(ChainAddressShape), - getBalance: M.callWhen(M.any()).returns(CoinShape), - getBalances: M.callWhen().returns(M.arrayOf(CoinShape)), - send: M.callWhen(ChainAddressShape, AmountArgShape).returns(M.undefined()), - transfer: M.callWhen(AmountArgShape, ChainAddressShape) + getBalance: M.call(M.any()).returns(NetworkShape.Vow$(CoinShape)), + getBalances: M.call().returns(NetworkShape.Vow$(M.arrayOf(CoinShape))), + send: M.call(ChainAddressShape, AmountArgShape).returns(VowShape), + transfer: M.call(AmountArgShape, ChainAddressShape) .optional(M.record()) - .returns(M.undefined()), - transferSteps: M.callWhen(AmountArgShape, M.any()).returns(M.undefined()), - deposit: M.callWhen(PaymentShape).returns(M.undefined()), + .returns(VowShape), + transferSteps: M.call(AmountArgShape, M.any()).returns(VowShape), + deposit: M.call(PaymentShape).returns(VowShape), }; From 042766fb8ab16f90c823190fe101e9cd8e619011 Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Tue, 25 Jun 2024 20:49:51 -0400 Subject: [PATCH 04/19] chore(orchestration): ensure local-orchestration-account offerHandlers are resumable --- .../src/exos/local-orchestration-account.js | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/packages/orchestration/src/exos/local-orchestration-account.js b/packages/orchestration/src/exos/local-orchestration-account.js index ecf516a9993..aa0be123175 100644 --- a/packages/orchestration/src/exos/local-orchestration-account.js +++ b/packages/orchestration/src/exos/local-orchestration-account.js @@ -128,10 +128,8 @@ export const prepareLocalOrchestrationAccountKit = ( .returns(DenomAmountShape), }), invitationMakers: M.interface('invitationMakers', { - Delegate: M.callWhen(M.string(), AmountShape).returns(InvitationShape), - Undelegate: M.callWhen(M.string(), AmountShape).returns( - InvitationShape, - ), + Delegate: M.call(M.string(), AmountShape).returns(InvitationShape), + Undelegate: M.call(M.string(), AmountShape).returns(InvitationShape), CloseAccount: M.call().returns(M.promise()), }), }, @@ -155,27 +153,28 @@ export const prepareLocalOrchestrationAccountKit = ( * @param {string} validatorAddress * @param {Amount<'nat'>} ertpAmount */ - async Delegate(validatorAddress, ertpAmount) { + Delegate(validatorAddress, ertpAmount) { trace('Delegate', validatorAddress, ertpAmount); - return zcf.makeInvitation(async seat => { - // TODO should it allow delegating more BLD? + return zcf.makeInvitation(seat => { seat.exit(); - return this.facets.holder.delegate(validatorAddress, ertpAmount); + return watch( + this.facets.holder.delegate(validatorAddress, ertpAmount), + ); }, 'Delegate'); }, - /** * @param {string} validatorAddress * @param {Amount<'nat'>} ertpAmount */ - async Undelegate(validatorAddress, ertpAmount) { + Undelegate(validatorAddress, ertpAmount) { trace('Undelegate', validatorAddress, ertpAmount); - return zcf.makeInvitation(async seat => { - // TODO should it allow delegating more BLD? + return zcf.makeInvitation(seat => { seat.exit(); - return this.facets.holder.undelegate(validatorAddress, ertpAmount); + return watch( + this.facets.holder.undelegate(validatorAddress, ertpAmount), + ); }, 'Undelegate'); }, CloseAccount() { From c162426f6a20b375113fae9ab82c0ba4ab87841d Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Tue, 25 Jun 2024 20:57:56 -0400 Subject: [PATCH 05/19] fix(orchestration): do not call `getTimeoutTimestampNS` if `opts.timeoutTimestamp` is provided --- .../src/exos/local-chain-facade.js | 10 ++--- .../src/exos/local-orchestration-account.js | 44 +++++-------------- packages/vow/test/watch-utils.test.js | 12 +++++ 3 files changed, 29 insertions(+), 37 deletions(-) diff --git a/packages/orchestration/src/exos/local-chain-facade.js b/packages/orchestration/src/exos/local-chain-facade.js index 4a6defd1f74..154b8931549 100644 --- a/packages/orchestration/src/exos/local-chain-facade.js +++ b/packages/orchestration/src/exos/local-chain-facade.js @@ -1,5 +1,5 @@ /** @file ChainAccount exo */ -import { heapVowE as E, heapVowTools } from '@agoric/vow/vat.js'; +import { E } from '@endo/far'; import { ChainFacadeI } from '../typeGuards.js'; @@ -31,7 +31,7 @@ export const prepareLocalChainFacade = ( makeLocalOrchestrationAccountKit, localchain, storageNode, - vowTools: { allVows }, + vowTools: { allVows, watch }, }, ) => zone.exoClass( @@ -45,7 +45,7 @@ export const prepareLocalChainFacade = ( }, { getChainInfo() { - return heapVowTools.watch(this.state.localChainInfo); + return watch(this.state.localChainInfo); }, // FIXME parameterize on the remoteChainInfo to make() @@ -54,8 +54,8 @@ export const prepareLocalChainFacade = ( makeAccount() { const { localChainInfo } = this.state; const lcaP = E(localchain).makeAccount(); - // FIXME use watch() from vowTools - return heapVowTools.watch(allVows([lcaP, E(lcaP).getAddress()]), { + // @ts-expect-error E does not understand Vow pipelining + return watch(allVows([lcaP, E(lcaP).getAddress()]), { onFulfilled: ([lca, address]) => { const { holder: account } = makeLocalOrchestrationAccountKit({ account: lca, diff --git a/packages/orchestration/src/exos/local-orchestration-account.js b/packages/orchestration/src/exos/local-orchestration-account.js index aa0be123175..32eff5b689f 100644 --- a/packages/orchestration/src/exos/local-orchestration-account.js +++ b/packages/orchestration/src/exos/local-orchestration-account.js @@ -6,7 +6,6 @@ import { Shape as NetworkShape } from '@agoric/network'; import { M } from '@agoric/vat-data'; import { VowShape } from '@agoric/vow'; import { TopicsRecordShape } from '@agoric/zoe/src/contractSupport/index.js'; -import { InvitationShape } from '@agoric/zoe/src/typeGuards.js'; import { E } from '@endo/far'; import { ChainAddressShape, @@ -89,18 +88,13 @@ export const prepareLocalOrchestrationAccountKit = ( undelegateWatcher: M.interface('undelegateWatcher', { onFulfilled: M.call(M.arrayOf(M.record())) .optional(M.arrayOf(M.undefined())) // empty context - .returns(M.promise()), + .returns(VowShape), }), getChainInfoWatcher: M.interface('getChainInfoWatcher', { onFulfilled: M.call(M.record()) // agoric chain info .optional({ destination: ChainAddressShape }) // empty context .returns(VowShape), // transfer channel }), - getTimeoutTimestampWatcher: M.interface('getTimeoutTimestampWatcher', { - onFulfilled: M.call(M.bigint()) - .optional(IBCTransferOptionsShape) - .returns(M.bigint()), - }), transferWatcher: M.interface('transferWatcher', { onFulfilled: M.call(M.any()) .optional({ @@ -108,12 +102,12 @@ export const prepareLocalOrchestrationAccountKit = ( opts: M.or(M.undefined(), IBCTransferOptionsShape), amount: ChainAmountShape, }) - .returns(M.promise()), + .returns(VowShape), }), extractFirstResultWatcher: M.interface('extractFirstResultWatcher', { onFulfilled: M.call(M.arrayOf(M.record())) .optional(M.arrayOf(M.undefined())) - .returns(M.record()), + .returns(M.any()), }), returnVoidWatcher: M.interface('returnVoidWatcher', { onFulfilled: M.call(M.any()) @@ -128,8 +122,8 @@ export const prepareLocalOrchestrationAccountKit = ( .returns(DenomAmountShape), }), invitationMakers: M.interface('invitationMakers', { - Delegate: M.call(M.string(), AmountShape).returns(InvitationShape), - Undelegate: M.call(M.string(), AmountShape).returns(InvitationShape), + Delegate: M.call(M.string(), AmountShape).returns(M.promise()), + Undelegate: M.call(M.string(), AmountShape).returns(M.promise()), CloseAccount: M.call().returns(M.promise()), }), }, @@ -211,19 +205,6 @@ export const prepareLocalOrchestrationAccountKit = ( ); }, }, - getTimeoutTimestampWatcher: { - /** - * @param {bigint} timeoutTimestamp - * @param {{ opts: IBCMsgTransferOptions }} ctx - */ - onFulfilled(timeoutTimestamp, { opts }) { - // FIXME: do not call `getTimeoutTimestampNS` if `opts.timeoutTimestamp` or `opts.timeoutHeight` is provided - return ( - opts?.timeoutTimestamp ?? - (opts?.timeoutHeight ? 0n : timeoutTimestamp) - ); - }, - }, transferWatcher: { /** * @param {[ @@ -323,7 +304,7 @@ export const prepareLocalOrchestrationAccountKit = ( ); }, getBalances() { - throw new Error('not yet implemented'); + return asVow(() => Fail`not yet implemented`); }, getPublicTopics() { @@ -436,15 +417,14 @@ export const prepareLocalOrchestrationAccountKit = ( // set a `timeoutTimestamp` if caller does not supply either `timeoutHeight` or `timeoutTimestamp` // TODO #9324 what's a reasonable default? currently 5 minutes - // FIXME: do not call `getTimeoutTimestampNS` if `opts.timeoutTimestamp` or `opts.timeoutHeight` is provided - const timeoutTimestampV = watch( - timestampHelper.getTimeoutTimestampNS(), - this.facets.getTimeoutTimestampWatcher, - { opts }, - ); + const timeoutTimestampVowOrValue = + opts?.timeoutTimestamp ?? + (opts?.timeoutHeight + ? 0n + : E(timestampHelper).getTimeoutTimestampNS()); const transferV = watch( - allVows([connectionInfoV, timeoutTimestampV]), + allVows([connectionInfoV, timeoutTimestampVowOrValue]), this.facets.transferWatcher, { opts, amount, destination }, ); diff --git a/packages/vow/test/watch-utils.test.js b/packages/vow/test/watch-utils.test.js index b0c79c597a3..9706bb6c8f9 100644 --- a/packages/vow/test/watch-utils.test.js +++ b/packages/vow/test/watch-utils.test.js @@ -112,3 +112,15 @@ test('allVows - watch promises mixed with vows', async t => { t.is(result.length, 2); t.like(result, ['vow', 'promise']); }); + +test('allVows can accept passables', async t => { + const zone = makeHeapZone(); + const { watch, when, allVows } = prepareVowTools(zone); + + const testPromiseP = Promise.resolve('vow'); + const vowA = watch(testPromiseP); + + const result = await when(allVows([vowA, 'string', 1n, { obj: true }])); + t.is(result.length, 4); + t.deepEqual(result, ['vow', 'string', 1n, { obj: true }]); +}); From ef193ac302f9af2fe4d19e3f1318793097fbba57 Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Tue, 25 Jun 2024 22:45:20 -0400 Subject: [PATCH 06/19] chore(orchestration): ensure cosmos-orchestration-account methods from shared `orchestrationAccountMethods` are resumable --- .../src/exos/chain-account-kit.js | 8 ++-- .../src/exos/cosmos-orchestration-account.js | 44 ++++++++++--------- .../src/exos/local-orchestration-account.js | 12 ++--- .../src/exos/remote-chain-facade.js | 10 ++--- .../test/examples/stake-atom.contract.test.ts | 5 +-- 5 files changed, 39 insertions(+), 40 deletions(-) diff --git a/packages/orchestration/src/exos/chain-account-kit.js b/packages/orchestration/src/exos/chain-account-kit.js index d39adb451b8..a735afac469 100644 --- a/packages/orchestration/src/exos/chain-account-kit.js +++ b/packages/orchestration/src/exos/chain-account-kit.js @@ -103,11 +103,11 @@ export const prepareChainAccountKit = (zone, { watch, asVow }) => }, getBalance(_denom) { // UNTIL https://github.com/Agoric/agoric-sdk/issues/9326 - return asVow(() => Fail`'not yet implemented'`); + return asVow(() => Fail`not yet implemented`); }, getBalances() { // UNTIL https://github.com/Agoric/agoric-sdk/issues/9326 - return asVow(() => Fail`'not yet implemented'`); + return asVow(() => Fail`not yet implemented`); }, getLocalAddress() { return NonNullish( @@ -125,7 +125,7 @@ export const prepareChainAccountKit = (zone, { watch, asVow }) => return this.state.port; }, executeTx() { - return asVow(() => Fail`'not yet implemented'`); + return asVow(() => Fail`not yet implemented`); }, /** * Submit a transaction on behalf of the remote account for execution on @@ -170,7 +170,7 @@ export const prepareChainAccountKit = (zone, { watch, asVow }) => */ getPurse(brand) { console.log('getPurse got', brand); - return asVow(() => Fail`'not yet implemented'`); + return asVow(() => Fail`not yet implemented`); }, }, connectionHandler: { diff --git a/packages/orchestration/src/exos/cosmos-orchestration-account.js b/packages/orchestration/src/exos/cosmos-orchestration-account.js index 46ddd1e6bcb..68d97482127 100644 --- a/packages/orchestration/src/exos/cosmos-orchestration-account.js +++ b/packages/orchestration/src/exos/cosmos-orchestration-account.js @@ -39,7 +39,7 @@ 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 {Vow, 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'; @@ -105,7 +105,7 @@ const toDenomAmount = c => ({ denom: c.denom, value: BigInt(c.amount) }); export const prepareCosmosOrchestrationAccountKit = ( zone, makeRecorderKit, - { when, watch }, + { when, watch, asVow }, zcf, ) => { const makeCosmosOrchestrationAccountKit = zone.exoClassKit( @@ -372,8 +372,8 @@ export const prepareCosmosOrchestrationAccountKit = ( 'FIXME deposit noop until https://github.com/Agoric/agoric-sdk/issues/9193', ); }, - async getBalances() { - throw Error('not yet implemented'); + getBalances() { + return asVow(() => Fail`not yet implemented`); }, /** * _Assumes users has already sent funds to their ICA, until #9193 @@ -425,25 +425,27 @@ export const prepareCosmosOrchestrationAccountKit = ( }, /** * @param {DenomArg} denom - * @returns {Promise} + * @returns {Vow} */ - async getBalance(denom) { - const { chainAddress, icqConnection } = this.state; - if (!icqConnection) { - throw Fail`Queries not available for chain ${chainAddress.chainId}`; - } - // TODO #9211 lookup denom from brand - assert.typeof(denom, 'string'); + getBalance(denom) { + return asVow(() => { + const { chainAddress, icqConnection } = this.state; + if (!icqConnection) { + throw Fail`Queries not available for chain ${chainAddress.chainId}`; + } + // TODO #9211 lookup denom from brand + assert.typeof(denom, 'string'); - const results = E(icqConnection).query([ - toRequestQueryJson( - QueryBalanceRequest.toProtoMsg({ - address: chainAddress.address, - denom, - }), - ), - ]); - return when(watch(results, this.facets.balanceQueryWatcher)); + const results = E(icqConnection).query([ + toRequestQueryJson( + QueryBalanceRequest.toProtoMsg({ + address: chainAddress.address, + denom, + }), + ), + ]); + return watch(results, this.facets.balanceQueryWatcher); + }); }, send(toAccount, amount) { diff --git a/packages/orchestration/src/exos/local-orchestration-account.js b/packages/orchestration/src/exos/local-orchestration-account.js index 32eff5b689f..3a42ac8f4d0 100644 --- a/packages/orchestration/src/exos/local-orchestration-account.js +++ b/packages/orchestration/src/exos/local-orchestration-account.js @@ -86,26 +86,26 @@ export const prepareLocalOrchestrationAccountKit = ( { holder: HolderI, undelegateWatcher: M.interface('undelegateWatcher', { - onFulfilled: M.call(M.arrayOf(M.record())) + onFulfilled: M.call([M.splitRecord({ completionTime: M.string() })]) .optional(M.arrayOf(M.undefined())) // empty context .returns(VowShape), }), getChainInfoWatcher: M.interface('getChainInfoWatcher', { onFulfilled: M.call(M.record()) // agoric chain info - .optional({ destination: ChainAddressShape }) // empty context - .returns(VowShape), // transfer channel + .optional({ destination: ChainAddressShape }) + .returns(NetworkShape.Vow$(M.record())), // connection info }), transferWatcher: M.interface('transferWatcher', { - onFulfilled: M.call(M.any()) + onFulfilled: M.call([M.record(), M.nat()]) .optional({ destination: ChainAddressShape, opts: M.or(M.undefined(), IBCTransferOptionsShape), amount: ChainAmountShape, }) - .returns(VowShape), + .returns(NetworkShape.Vow$(M.record())), }), extractFirstResultWatcher: M.interface('extractFirstResultWatcher', { - onFulfilled: M.call(M.arrayOf(M.record())) + onFulfilled: M.call([M.record()]) .optional(M.arrayOf(M.undefined())) .returns(M.any()), }), diff --git a/packages/orchestration/src/exos/remote-chain-facade.js b/packages/orchestration/src/exos/remote-chain-facade.js index b5f18fb5a1e..ecfcf1b4592 100644 --- a/packages/orchestration/src/exos/remote-chain-facade.js +++ b/packages/orchestration/src/exos/remote-chain-facade.js @@ -1,6 +1,6 @@ /** @file ChainAccount exo */ import { makeTracer } from '@agoric/internal'; -import { heapVowE as E, heapVowTools } from '@agoric/vow/vat.js'; +import { E } from '@endo/far'; import { ChainFacadeI } from '../typeGuards.js'; @@ -39,7 +39,7 @@ export const prepareRemoteChainFacade = ( orchestration, storageNode, timer, - vowTools: { allVows }, + vowTools: { allVows, watch }, }, ) => zone.exoClass( @@ -55,7 +55,7 @@ export const prepareRemoteChainFacade = ( }, { getChainInfo() { - return heapVowTools.watch(this.state.remoteChainInfo); + return watch(this.state.remoteChainInfo); }, // FIXME parameterize on the remoteChainInfo to make() @@ -75,8 +75,8 @@ export const prepareRemoteChainFacade = ( connectionInfo.counterparty.connection_id, ); - // FIXME use watch() from vowTools - return heapVowTools.watch(allVows([icaP, E(icaP).getAddress()]), { + // @ts-expect-error E does not understand Vow pipelining + return watch(allVows([icaP, E(icaP).getAddress()]), { onFulfilled: ([account, address]) => { return makeCosmosOrchestrationAccount(address, stakingDenom, { account, diff --git a/packages/orchestration/test/examples/stake-atom.contract.test.ts b/packages/orchestration/test/examples/stake-atom.contract.test.ts index 5df7dc624de..64f1c312907 100644 --- a/packages/orchestration/test/examples/stake-atom.contract.test.ts +++ b/packages/orchestration/test/examples/stake-atom.contract.test.ts @@ -1,7 +1,7 @@ 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 { heapVowE as E } from '@agoric/vow/vat.js'; import path from 'path'; import { makeNotifierFromSubscriber } from '@agoric/notifier'; import type { Installation } from '@agoric/zoe/src/zoeService/utils.js'; @@ -62,9 +62,6 @@ test('makeAccount, getAddress, getBalances, getBalance', async t => { // t.regex(address.address, /cosmos1/); t.like(chainAddress, { chainId: 'cosmoshub-4', addressEncoding: 'bech32' }); - t.log('deposit 100 bld to account'); - await E(account).deposit(await utils.pourPayment(ist.units(100))); - await t.throwsAsync(E(account).getBalances(), { message: 'not yet implemented', }); From c9263dd51f99c5fe0bb27984b37dc68b495703cc Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Wed, 26 Jun 2024 15:37:34 -0400 Subject: [PATCH 07/19] chore(orchestration): ensure chain facades are membrane friendly --- .../src/exos/local-chain-facade.js | 100 ++++++++++----- .../src/exos/local-orchestration-account.js | 2 +- .../src/exos/remote-chain-facade.js | 114 ++++++++++++------ packages/vow/test/watch-utils.test.js | 2 +- 4 files changed, 143 insertions(+), 75 deletions(-) diff --git a/packages/orchestration/src/exos/local-chain-facade.js b/packages/orchestration/src/exos/local-chain-facade.js index 154b8931549..694ef809224 100644 --- a/packages/orchestration/src/exos/local-chain-facade.js +++ b/packages/orchestration/src/exos/local-chain-facade.js @@ -1,5 +1,7 @@ /** @file ChainAccount exo */ import { E } from '@endo/far'; +import { M } from '@endo/patterns'; +import { pickFacet } from '@agoric/vat-data'; import { ChainFacadeI } from '../typeGuards.js'; @@ -7,25 +9,29 @@ import { ChainFacadeI } from '../typeGuards.js'; * @import {Zone} from '@agoric/base-zone'; * @import {TimerService} from '@agoric/time'; * @import {Remote} from '@agoric/internal'; - * @import {LocalChain} from '@agoric/vats/src/localchain.js'; + * @import {LocalChain, LocalChainAccount} from '@agoric/vats/src/localchain.js'; * @import {Vow, VowTools} from '@agoric/vow'; * @import {OrchestrationService} from '../service.js'; * @import {MakeLocalOrchestrationAccountKit} from './local-orchestration-account.js'; - * @import {ChainInfo, CosmosChainInfo, IBCConnectionInfo, OrchestrationAccount} from '../types.js'; + * @import {ChainAddress, ChainInfo, CosmosChainInfo, IBCConnectionInfo, OrchestrationAccount, PromiseToVow} from '../types.js'; */ /** - * @param {Zone} zone - * @param {{ + * @typedef {{ * makeLocalOrchestrationAccountKit: MakeLocalOrchestrationAccountKit; * orchestration: Remote; * storageNode: Remote; * timer: Remote; * localchain: Remote; * vowTools: VowTools; - * }} powers + * }} LocalChainFacadePowers */ -export const prepareLocalChainFacade = ( + +/** + * @param {Zone} zone + * @param {LocalChainFacadePowers} powers + */ +const prepareLocalChainFacadeKit = ( zone, { makeLocalOrchestrationAccountKit, @@ -34,9 +40,16 @@ export const prepareLocalChainFacade = ( vowTools: { allVows, watch }, }, ) => - zone.exoClass( + zone.exoClassKit( 'LocalChainFacade', - ChainFacadeI, + { + public: ChainFacadeI, + makeAccountWatcher: M.interface('undelegateWatcher', { + onFulfilled: M.call([M.remotable('LCA Account'), M.string()]) + .optional(M.arrayOf(M.undefined())) // empty context + .returns(M.remotable()), + }), + }, /** * @param {CosmosChainInfo} localChainInfo */ @@ -44,34 +57,55 @@ export const prepareLocalChainFacade = ( return { localChainInfo }; }, { - getChainInfo() { - return watch(this.state.localChainInfo); - }, + public: { + getChainInfo() { + return watch(this.state.localChainInfo); + }, - // FIXME parameterize on the remoteChainInfo to make() - // That used to work but got lost in the migration to Exo - /** @returns {Vow>} */ - makeAccount() { - const { localChainInfo } = this.state; - const lcaP = E(localchain).makeAccount(); - // @ts-expect-error E does not understand Vow pipelining - return watch(allVows([lcaP, E(lcaP).getAddress()]), { - onFulfilled: ([lca, address]) => { - const { holder: account } = makeLocalOrchestrationAccountKit({ - account: lca, - address: harden({ - address, - addressEncoding: 'bech32', - chainId: localChainInfo.chainId, - }), - // FIXME storage path https://github.com/Agoric/agoric-sdk/issues/9066 - storageNode, - }); - return account; - }, - }); + // FIXME parameterize on the remoteChainInfo to make() + // That used to work but got lost in the migration to Exo + /** @returns {Vow>>} */ + makeAccount() { + const lcaP = E(localchain).makeAccount(); + // @ts-expect-error 'Vow> is not assignable to type 'Vow' + return watch( + // @ts-expect-error Property 'getAddress' does not exist on type 'EMethods { + const makeLocalChainFacadeKit = prepareLocalChainFacadeKit(zone, powers); + return pickFacet(makeLocalChainFacadeKit, 'public'); +}; harden(prepareLocalChainFacade); + /** @typedef {ReturnType} MakeLocalChainFacade */ diff --git a/packages/orchestration/src/exos/local-orchestration-account.js b/packages/orchestration/src/exos/local-orchestration-account.js index 3a42ac8f4d0..087430527ef 100644 --- a/packages/orchestration/src/exos/local-orchestration-account.js +++ b/packages/orchestration/src/exos/local-orchestration-account.js @@ -383,7 +383,7 @@ export const prepareLocalOrchestrationAccountKit = ( executeTx(messages) { return watch(E(this.state.account).executeTx(messages)); }, - /** @returns {ChainAddress} */ + /** @type {OrchestrationAccount['getAddress']} */ getAddress() { return this.state.address; }, diff --git a/packages/orchestration/src/exos/remote-chain-facade.js b/packages/orchestration/src/exos/remote-chain-facade.js index ecfcf1b4592..e6f3ebfd7c6 100644 --- a/packages/orchestration/src/exos/remote-chain-facade.js +++ b/packages/orchestration/src/exos/remote-chain-facade.js @@ -1,7 +1,9 @@ /** @file ChainAccount exo */ import { makeTracer } from '@agoric/internal'; import { E } from '@endo/far'; +import { M } from '@endo/patterns'; +import { pickFacet } from '@agoric/vat-data'; import { ChainFacadeI } from '../typeGuards.js'; /** @@ -11,7 +13,7 @@ import { ChainFacadeI } from '../typeGuards.js'; * @import {Vow, VowTools} from '@agoric/vow'; * @import {OrchestrationService} from '../service.js'; * @import {prepareCosmosOrchestrationAccount} from './cosmos-orchestration-account.js'; - * @import {ChainInfo, CosmosChainInfo, IBCConnectionInfo, OrchestrationAccount} from '../types.js'; + * @import {ChainInfo, CosmosChainInfo, IBCConnectionInfo, OrchestrationAccount, ChainAddress, IcaAccount, PromiseToVow, Denom} from '../types.js'; */ const { Fail } = assert; @@ -21,8 +23,7 @@ const trace = makeTracer('RemoteChainFacade'); const anyVal = null; /** - * @param {Zone} zone - * @param {{ + * @typedef {{ * makeCosmosOrchestrationAccount: ReturnType< * typeof prepareCosmosOrchestrationAccount * >; @@ -30,65 +31,98 @@ const anyVal = null; * storageNode: Remote; * timer: Remote; * vowTools: VowTools; - * }} powers + * }} RemoteChainFacadePowers + */ + +/** + * @param {Zone} zone + * @param {RemoteChainFacadePowers} powers */ -export const prepareRemoteChainFacade = ( +const prepareRemoteChainFacadeKit = ( zone, { makeCosmosOrchestrationAccount, orchestration, storageNode, timer, - vowTools: { allVows, watch }, + vowTools: { allVows, asVow, watch }, }, ) => - zone.exoClass( + zone.exoClassKit( 'RemoteChainFacade', - ChainFacadeI, + { + public: ChainFacadeI, + makeAccountWatcher: M.interface('makeAccountWatcher', { + onFulfilled: M.call([M.remotable(), M.record()]) + .optional({ stakingDenom: M.string() }) + .returns(M.remotable()), + }), + }, /** * @param {CosmosChainInfo} remoteChainInfo * @param {IBCConnectionInfo} connectionInfo */ (remoteChainInfo, connectionInfo) => { - trace('making an RemoteChainFacade'); + trace('making a RemoteChainFacade'); return { remoteChainInfo, connectionInfo }; }, { - getChainInfo() { - return watch(this.state.remoteChainInfo); - }, + public: { + getChainInfo() { + return watch(this.state.remoteChainInfo); + }, - // FIXME parameterize on the remoteChainInfo to make() - // That used to work but got lost in the migration to Exo - /** @returns {Vow>} */ - makeAccount() { - const { remoteChainInfo, connectionInfo } = this.state; - const stakingDenom = remoteChainInfo.stakingTokens?.[0]?.denom; - if (!stakingDenom) { - // FIXME methods that return vows must not throw synchronously - throw Fail`chain info lacks staking denom`; - } + /** @returns {Vow>>} */ + makeAccount() { + // @ts-expect-error 'Vow> is not assignable to type 'Vow' + return asVow(() => { + const { remoteChainInfo, connectionInfo } = this.state; + const stakingDenom = remoteChainInfo.stakingTokens?.[0]?.denom; + if (!stakingDenom) { + throw Fail`chain info lacks staking denom`; + } - const icaP = E(orchestration).makeAccount( - remoteChainInfo.chainId, - connectionInfo.id, - connectionInfo.counterparty.connection_id, - ); - - // @ts-expect-error E does not understand Vow pipelining - return watch(allVows([icaP, E(icaP).getAddress()]), { - onFulfilled: ([account, address]) => { - return makeCosmosOrchestrationAccount(address, stakingDenom, { - account, - storageNode, - // FIXME provide real ICQ connection - icqConnection: anyVal, - timer, - }); - }, - }); + const icaP = E(orchestration).makeAccount( + remoteChainInfo.chainId, + connectionInfo.id, + connectionInfo.counterparty.connection_id, + ); + return watch( + allVows([icaP, E(icaP).getAddress()]), + this.facets.makeAccountWatcher, + { stakingDenom }, + ); + }); + }, + }, + makeAccountWatcher: { + /** + * @param {[IcaAccount, ChainAddress]} results + * @param {{ stakingDenom: Denom }} ctx + */ + onFulfilled([account, chainAddress], { stakingDenom }) { + return makeCosmosOrchestrationAccount(chainAddress, stakingDenom, { + account, + storageNode, + // FIXME provide real ICQ connection + // FIXME make Query Connection available via chain, not orchestrationAccount + icqConnection: anyVal, + timer, + }); + }, }, }, ); +harden(prepareRemoteChainFacadeKit); + +/** + * @param {Zone} zone + * @param {RemoteChainFacadePowers} powers + */ +export const prepareRemoteChainFacade = (zone, powers) => { + const makeLocalChainFacadeKit = prepareRemoteChainFacadeKit(zone, powers); + return pickFacet(makeLocalChainFacadeKit, 'public'); +}; harden(prepareRemoteChainFacade); + /** @typedef {ReturnType} MakeRemoteChainFacade */ diff --git a/packages/vow/test/watch-utils.test.js b/packages/vow/test/watch-utils.test.js index 9706bb6c8f9..ea5393e55cd 100644 --- a/packages/vow/test/watch-utils.test.js +++ b/packages/vow/test/watch-utils.test.js @@ -113,7 +113,7 @@ test('allVows - watch promises mixed with vows', async t => { t.like(result, ['vow', 'promise']); }); -test('allVows can accept passables', async t => { +test('allVows can accept passable data (PureData)', async t => { const zone = makeHeapZone(); const { watch, when, allVows } = prepareVowTools(zone); From 7e3fdfef5b4e6ee6bc115725f2720ef96f58863c Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Wed, 26 Jun 2024 20:59:26 -0400 Subject: [PATCH 08/19] test(boot): local orch account offer handler rejection --- packages/boot/test/bootstrapTests/lca.test.ts | 22 ++++++++++ packages/boot/tools/supports.ts | 20 ++++++++-- .../test/examples/stake-bld.contract.test.ts | 40 +++++++++++++------ packages/vats/tools/fake-bridge.js | 3 ++ 4 files changed, 70 insertions(+), 15 deletions(-) diff --git a/packages/boot/test/bootstrapTests/lca.test.ts b/packages/boot/test/bootstrapTests/lca.test.ts index 07020bcf9d4..051d359700f 100644 --- a/packages/boot/test/bootstrapTests/lca.test.ts +++ b/packages/boot/test/bootstrapTests/lca.test.ts @@ -89,4 +89,26 @@ test.serial('stakeBld', async t => { }, }, }); + t.like(wd.getLatestUpdateRecord(), { + status: { id: 'request-delegate', numWantsSatisfied: 1 }, + }); + + await t.throwsAsync( + wd.executeOffer({ + id: 'request-delegate-504', + invitationSpec: { + source: 'continuing', + previousOffer: 'request-stake', + invitationMakerName: 'Delegate', + invitationArgs: ['agoric1validator1', { brand: BLD, value: 504n }], + }, + proposal: { + give: { + // @ts-expect-error XXX BoardRemote + In: { brand: BLD, value: 504n }, + }, + }, + }), + // TODO should receive "simulated packet timeout" error + ); }); diff --git a/packages/boot/tools/supports.ts b/packages/boot/tools/supports.ts index f5ba1c82b6a..1ce41655f49 100644 --- a/packages/boot/tools/supports.ts +++ b/packages/boot/tools/supports.ts @@ -416,9 +416,23 @@ export const makeSwingsetTestKit = async ( switch (obj.type) { case 'VLOCALCHAIN_ALLOCATE_ADDRESS': return 'agoric1mockVlocalchainAddress'; - case 'VLOCALCHAIN_EXECUTE_TX': - // returns one empty object per message - return obj.messages.map(() => ({})); + case 'VLOCALCHAIN_EXECUTE_TX': { + return obj.messages.map(message => { + switch (message['@type']) { + case '/cosmos.staking.v1beta1.MsgDelegate': { + if (message.amount.amount === '504') { + // FIXME - how can we propagate the error? + // this results in `syscall.callNow failed: device.invoke failed, see logs for details` + throw Error('simulated packet timeout'); + } + return /** @type {JsonSafe} */ {}; + } + // returns one empty object per message unless specified + default: + return {}; + } + }); + } default: throw Error(`VLOCALCHAIN message of unknown type ${obj.type}`); } diff --git a/packages/orchestration/test/examples/stake-bld.contract.test.ts b/packages/orchestration/test/examples/stake-bld.contract.test.ts index 0fcdd37c7f9..16bbc79956c 100644 --- a/packages/orchestration/test/examples/stake-bld.contract.test.ts +++ b/packages/orchestration/test/examples/stake-bld.contract.test.ts @@ -97,18 +97,34 @@ test('makeStakeBldInvitation', async t => { t.truthy(invitationMakers, 'received continuing invitation'); t.log('make Delegate offer using invitationMakers'); - const delegateInv = await E(invitationMakers).Delegate('agoric1validator1', { - brand: bld.brand, - value: 1_000_000_000n, - }); - const delegateOffer = await E(zoe).offer( - delegateInv, - { give: { In: hundred } }, - { In: utils.pourPayment(hundred) }, - ); - const res = await E(delegateOffer).getOfferResult(); - t.deepEqual(res, {}); - t.log('Successfully delegated'); + { + const delegateInv = await E(invitationMakers).Delegate( + 'agoric1validator1', + { + brand: bld.brand, + value: 1_000_000_000n, + }, + ); + const delegateOffer = await E(zoe).offer(delegateInv); + const res = await E(delegateOffer).getOfferResult(); + t.deepEqual(res, {}); + t.log('Successfully delegated'); + } + + t.log('example Delegate offer rejection'); + { + const delegateInv = await E(invitationMakers).Delegate( + 'agoric1validator1', + { + brand: bld.brand, + value: 504n, + }, + ); + const delegateOffer = await E(zoe).offer(delegateInv); + await t.throwsAsync(E(delegateOffer).getOfferResult(), { + message: 'simualted packet timeout', + }); + } await t.throwsAsync(() => E(invitationMakers).CloseAccount(), { message: 'not yet implemented', diff --git a/packages/vats/tools/fake-bridge.js b/packages/vats/tools/fake-bridge.js index 307cfae3dab..1a45a636660 100644 --- a/packages/vats/tools/fake-bridge.js +++ b/packages/vats/tools/fake-bridge.js @@ -193,6 +193,9 @@ export const makeFakeLocalchainBridge = (zone, onToBridge = () => {}) => { }; } case '/cosmos.staking.v1beta1.MsgDelegate': { + if (message.amount.amount === '504') { + throw Error('simualted packet timeout'); + } return /** @type {JsonSafe} */ ({}); } case '/cosmos.staking.v1beta1.MsgUndelegate': { From fc2f5eada5d2bba0e196d53e36d2283a34b43f1d Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Wed, 26 Jun 2024 17:58:44 -0400 Subject: [PATCH 09/19] chore: cosmos-orchestration-account is resumable - adds VowifyAll helper for satisfies types test --- .../src/exos/cosmos-orchestration-account.js | 207 +++++++++--------- packages/orchestration/src/internal.ts | 4 + .../orchestration/test/staking-ops.test.ts | 4 +- packages/orchestration/test/types.test-d.ts | 3 +- 4 files changed, 114 insertions(+), 104 deletions(-) diff --git a/packages/orchestration/src/exos/cosmos-orchestration-account.js b/packages/orchestration/src/exos/cosmos-orchestration-account.js index 68d97482127..d4e6d4b5f47 100644 --- a/packages/orchestration/src/exos/cosmos-orchestration-account.js +++ b/packages/orchestration/src/exos/cosmos-orchestration-account.js @@ -16,9 +16,10 @@ import { } from '@agoric/cosmic-proto/cosmos/staking/v1beta1/tx.js'; import { Any } from '@agoric/cosmic-proto/google/protobuf/any.js'; import { makeTracer } from '@agoric/internal'; +import { Shape as NetworkShape } from '@agoric/network'; import { M } from '@agoric/vat-data'; +import { VowShape } from '@agoric/vow'; import { TopicsRecordShape } from '@agoric/zoe/src/contractSupport/index.js'; -import { InvitationShape } from '@agoric/zoe/src/typeGuards.js'; import { decodeBase64 } from '@endo/base64'; import { E } from '@endo/far'; import { @@ -73,19 +74,19 @@ export const IcaAccountHolderI = M.interface('IcaAccountHolder', { holder: M.any(), }), getPublicTopics: M.call().returns(TopicsRecordShape), - delegate: M.callWhen(ChainAddressShape, AmountArgShape).returns( - M.undefined(), - ), - redelegate: M.callWhen( + delegate: M.call(ChainAddressShape, AmountArgShape).returns(VowShape), + redelegate: M.call( ChainAddressShape, ChainAddressShape, AmountArgShape, - ).returns(M.undefined()), - withdrawReward: M.callWhen(ChainAddressShape).returns( - M.arrayOf(ChainAmountShape), + ).returns(VowShape), + withdrawReward: M.call(ChainAddressShape).returns( + NetworkShape.Vow$(M.arrayOf(ChainAmountShape)), + ), + withdrawRewards: M.call().returns( + NetworkShape.Vow$(M.arrayOf(ChainAmountShape)), ), - withdrawRewards: M.callWhen().returns(M.arrayOf(ChainAmountShape)), - undelegate: M.callWhen(M.arrayOf(DelegationShape)).returns(M.undefined()), + undelegate: M.call(M.arrayOf(DelegationShape)).returns(VowShape), }); /** @type {{ [name: string]: [description: string, valueShape: Pattern] }} */ @@ -105,7 +106,7 @@ const toDenomAmount = c => ({ denom: c.denom, value: BigInt(c.amount) }); export const prepareCosmosOrchestrationAccountKit = ( zone, makeRecorderKit, - { when, watch, asVow }, + { watch, asVow }, zcf, ) => { const makeCosmosOrchestrationAccountKit = zone.exoClassKit( @@ -129,7 +130,7 @@ export const prepareCosmosOrchestrationAccountKit = ( undelegateWatcher: M.interface('undelegateWatcher', { onFulfilled: M.call(M.string()) .optional(M.arrayOf(M.undefined())) // empty context - .returns(M.promise()), + .returns(NetworkShape.Vow$(M.promise())), }), withdrawRewardWatcher: M.interface('withdrawRewardWatcher', { onFulfilled: M.call(M.string()) @@ -138,20 +139,18 @@ export const prepareCosmosOrchestrationAccountKit = ( }), holder: IcaAccountHolderI, invitationMakers: M.interface('invitationMakers', { - Delegate: M.callWhen(ChainAddressShape, AmountArgShape).returns( - InvitationShape, + Delegate: M.call(ChainAddressShape, AmountArgShape).returns( + M.promise(), ), - Redelegate: M.callWhen( + Redelegate: M.call( ChainAddressShape, ChainAddressShape, AmountArgShape, - ).returns(InvitationShape), - WithdrawReward: M.callWhen(ChainAddressShape).returns(InvitationShape), - Undelegate: M.callWhen(M.arrayOf(DelegationShape)).returns( - InvitationShape, - ), - CloseAccount: M.callWhen().returns(InvitationShape), - TransferAccount: M.callWhen().returns(InvitationShape), + ).returns(M.promise()), + WithdrawReward: M.call(ChainAddressShape).returns(M.promise()), + Undelegate: M.call(M.arrayOf(DelegationShape)).returns(M.promise()), + CloseAccount: M.call().returns(M.promise()), + TransferAccount: M.call().returns(M.promise()), }), }, /** @@ -230,8 +229,10 @@ export const prepareCosmosOrchestrationAccountKit = ( trace('undelegate response', response); const { completionTime } = response; completionTime || Fail`No completion time result ${result}`; - return E(this.state.timer).wakeAt( - dateInSeconds(completionTime) + maxClockSkew, + return watch( + E(this.state.timer).wakeAt( + dateInSeconds(completionTime) + maxClockSkew, + ), ); }, }, @@ -266,9 +267,9 @@ export const prepareCosmosOrchestrationAccountKit = ( Delegate(validator, amount) { trace('Delegate', validator, amount); - return zcf.makeInvitation(async seat => { + return zcf.makeInvitation(seat => { seat.exit(); - return this.facets.holder.delegate(validator, amount); + return watch(this.facets.holder.delegate(validator, amount)); }, 'Delegate'); }, /** @@ -279,12 +280,10 @@ export const prepareCosmosOrchestrationAccountKit = ( Redelegate(srcValidator, dstValidator, amount) { trace('Redelegate', srcValidator, dstValidator, amount); - return zcf.makeInvitation(async seat => { + return zcf.makeInvitation(seat => { seat.exit(); - return this.facets.holder.redelegate( - srcValidator, - dstValidator, - amount, + return watch( + this.facets.holder.redelegate(srcValidator, dstValidator, amount), ); }, 'Redelegate'); }, @@ -292,18 +291,18 @@ export const prepareCosmosOrchestrationAccountKit = ( WithdrawReward(validator) { trace('WithdrawReward', validator); - return zcf.makeInvitation(async seat => { + return zcf.makeInvitation(seat => { seat.exit(); - return this.facets.holder.withdrawReward(validator); + return watch(this.facets.holder.withdrawReward(validator)); }, 'WithdrawReward'); }, /** @param {Omit[]} delegations */ Undelegate(delegations) { trace('Undelegate', delegations); - return zcf.makeInvitation(async seat => { + return zcf.makeInvitation(seat => { seat.exit(); - return this.facets.holder.undelegate(delegations); + return watch(this.facets.holder.undelegate(delegations)); }, 'Undelegate'); }, CloseAccount() { @@ -350,23 +349,25 @@ export const prepareCosmosOrchestrationAccountKit = ( * @param {CosmosValidatorAddress} validator * @param {AmountArg} amount */ - async delegate(validator, amount) { - trace('delegate', validator, amount); - const { helper } = this.facets; - const { chainAddress } = this.state; + delegate(validator, amount) { + return asVow(() => { + trace('delegate', validator, amount); + const { helper } = this.facets; + const { chainAddress } = this.state; - const results = E(helper.owned()).executeEncodedTx([ - Any.toJSON( - MsgDelegate.toProtoMsg({ - delegatorAddress: chainAddress.address, - validatorAddress: validator.address, - amount: helper.amountToCoin(amount), - }), - ), - ]); - return when(watch(results, this.facets.returnVoidWatcher)); + const results = E(helper.owned()).executeEncodedTx([ + Any.toJSON( + MsgDelegate.toProtoMsg({ + delegatorAddress: chainAddress.address, + validatorAddress: validator.address, + amount: helper.amountToCoin(amount), + }), + ), + ]); + return watch(results, this.facets.returnVoidWatcher); + }); }, - async deposit(payment) { + deposit(payment) { trace('deposit', payment); console.error( 'FIXME deposit noop until https://github.com/Agoric/agoric-sdk/issues/9193', @@ -382,46 +383,48 @@ export const prepareCosmosOrchestrationAccountKit = ( * @param {CosmosValidatorAddress} dstValidator * @param {AmountArg} amount */ - async redelegate(srcValidator, dstValidator, amount) { - trace('redelegate', srcValidator, dstValidator, amount); - const { helper } = this.facets; - const { chainAddress } = this.state; + redelegate(srcValidator, dstValidator, amount) { + return asVow(() => { + trace('redelegate', srcValidator, dstValidator, amount); + const { helper } = this.facets; + const { chainAddress } = this.state; - const results = E(helper.owned()).executeEncodedTx([ - Any.toJSON( - MsgBeginRedelegate.toProtoMsg({ - delegatorAddress: chainAddress.address, - validatorSrcAddress: srcValidator.address, - validatorDstAddress: dstValidator.address, - amount: helper.amountToCoin(amount), - }), - ), - ]); + const results = E(helper.owned()).executeEncodedTx([ + Any.toJSON( + MsgBeginRedelegate.toProtoMsg({ + delegatorAddress: chainAddress.address, + validatorSrcAddress: srcValidator.address, + validatorDstAddress: dstValidator.address, + amount: helper.amountToCoin(amount), + }), + ), + ]); - return when( - watch( + return watch( results, // NOTE: response, including completionTime, is currently discarded. this.facets.returnVoidWatcher, - ), - ); + ); + }); }, /** * @param {CosmosValidatorAddress} validator - * @returns {Promise} + * @returns {Vow} */ - async withdrawReward(validator) { - trace('withdrawReward', validator); - const { helper } = this.facets; - const { chainAddress } = this.state; - const msg = MsgWithdrawDelegatorReward.toProtoMsg({ - delegatorAddress: chainAddress.address, - validatorAddress: validator.address, - }); - const account = helper.owned(); + withdrawReward(validator) { + return asVow(() => { + trace('withdrawReward', validator); + const { helper } = this.facets; + const { chainAddress } = this.state; + const msg = MsgWithdrawDelegatorReward.toProtoMsg({ + delegatorAddress: chainAddress.address, + validatorAddress: validator.address, + }); + const account = helper.owned(); - const results = E(account).executeEncodedTx([Any.toJSON(msg)]); - return when(watch(results, this.facets.withdrawRewardWatcher)); + const results = E(account).executeEncodedTx([Any.toJSON(msg)]); + return watch(results, this.facets.withdrawRewardWatcher); + }); }, /** * @param {DenomArg} denom @@ -450,44 +453,46 @@ export const prepareCosmosOrchestrationAccountKit = ( send(toAccount, amount) { console.log('send got', toAccount, amount); - throw Error('not yet implemented'); + return asVow(() => Fail`not yet implemented`); }, transfer(amount, msg) { console.log('transferSteps got', amount, msg); - throw Error('not yet implemented'); + return asVow(() => Fail`not yet implemented`); }, transferSteps(amount, msg) { console.log('transferSteps got', amount, msg); - throw Error('not yet implemented'); + return asVow(() => Fail`not yet implemented`); }, withdrawRewards() { - throw assert.error('Not implemented'); + return asVow(() => Fail`Not Implemented. Try using withdrawReward.`); }, /** @param {Omit[]} delegations */ - async undelegate(delegations) { - trace('undelegate', delegations); - const { helper } = this.facets; - const { chainAddress, bondDenom } = this.state; + undelegate(delegations) { + return asVow(() => { + trace('undelegate', delegations); + const { helper } = this.facets; + 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 }, - }), + 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, - ); - return when(watch(undelegateV, this.facets.returnVoidWatcher)); + this.facets.undelegateWatcher, + ); + return watch(undelegateV, this.facets.returnVoidWatcher); + }); }, }, }, diff --git a/packages/orchestration/src/internal.ts b/packages/orchestration/src/internal.ts index 19a2daf54e1..04c4266ebfe 100644 --- a/packages/orchestration/src/internal.ts +++ b/packages/orchestration/src/internal.ts @@ -3,3 +3,7 @@ import type { Vow } from '@agoric/vow'; export type PromiseToVow = T extends (...args: infer A) => Promise ? (...args: A) => Vow : never; + +export type VowifyAll = { + [K in keyof T]: PromiseToVow; +}; diff --git a/packages/orchestration/test/staking-ops.test.ts b/packages/orchestration/test/staking-ops.test.ts index ed8f99868c4..b1de3fc4b82 100644 --- a/packages/orchestration/test/staking-ops.test.ts +++ b/packages/orchestration/test/staking-ops.test.ts @@ -15,12 +15,12 @@ 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 { prepareVowTools } from '@agoric/vow/vat.js'; +import { prepareVowTools, heapVowE as E } from '@agoric/vow/vat.js'; import { prepareRecorderKitMakers } from '@agoric/zoe/src/contractSupport/recorder.js'; import { buildZoeManualTimer } from '@agoric/zoe/tools/manualTimer.js'; import { makeDurableZone } from '@agoric/zone/durable.js'; import { decodeBase64 } from '@endo/base64'; -import { E, Far } from '@endo/far'; +import { 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'; diff --git a/packages/orchestration/test/types.test-d.ts b/packages/orchestration/test/types.test-d.ts index a880f300038..4e2b67e6d5b 100644 --- a/packages/orchestration/test/types.test-d.ts +++ b/packages/orchestration/test/types.test-d.ts @@ -12,6 +12,7 @@ import type { } from '../src/types.js'; import type { LocalOrchestrationAccountKit } from '../src/exos/local-orchestration-account.js'; import { prepareCosmosOrchestrationAccount } from '../src/exos/cosmos-orchestration-account.js'; +import type { VowifyAll } from '../src/internal.js'; const anyVal = null as any; @@ -66,5 +67,5 @@ expectNotType(chainAddr); anyVal, anyVal, anyVal, - ) satisfies StakingAccountActions; + ) satisfies VowifyAll; } From 612c0ff44a22614821bdce1467c6689f21ae804b Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Wed, 26 Jun 2024 19:57:18 -0400 Subject: [PATCH 10/19] chore(orchestration): orchestration service is resumable --- .../src/exos/remote-chain-facade.js | 42 ++++++++++---- packages/orchestration/src/service.js | 57 +++++++++---------- packages/orchestration/test/service.test.ts | 3 +- 3 files changed, 57 insertions(+), 45 deletions(-) diff --git a/packages/orchestration/src/exos/remote-chain-facade.js b/packages/orchestration/src/exos/remote-chain-facade.js index e6f3ebfd7c6..c9b06e69d1d 100644 --- a/packages/orchestration/src/exos/remote-chain-facade.js +++ b/packages/orchestration/src/exos/remote-chain-facade.js @@ -2,8 +2,8 @@ import { makeTracer } from '@agoric/internal'; import { E } from '@endo/far'; import { M } from '@endo/patterns'; - import { pickFacet } from '@agoric/vat-data'; +import { VowShape } from '@agoric/vow'; import { ChainFacadeI } from '../typeGuards.js'; /** @@ -45,7 +45,7 @@ const prepareRemoteChainFacadeKit = ( orchestration, storageNode, timer, - vowTools: { allVows, asVow, watch }, + vowTools: { asVow, watch }, }, ) => zone.exoClassKit( @@ -53,8 +53,13 @@ const prepareRemoteChainFacadeKit = ( { public: ChainFacadeI, makeAccountWatcher: M.interface('makeAccountWatcher', { - onFulfilled: M.call([M.remotable(), M.record()]) + onFulfilled: M.call(M.remotable()) .optional({ stakingDenom: M.string() }) + .returns(VowShape), + }), + getAddressWatcher: M.interface('makeAccountWatcher', { + onFulfilled: M.call(M.record()) + .optional({ stakingDenom: M.string(), account: M.remotable() }) .returns(M.remotable()), }), }, @@ -82,25 +87,38 @@ const prepareRemoteChainFacadeKit = ( throw Fail`chain info lacks staking denom`; } - const icaP = E(orchestration).makeAccount( - remoteChainInfo.chainId, - connectionInfo.id, - connectionInfo.counterparty.connection_id, - ); return watch( - allVows([icaP, E(icaP).getAddress()]), + E(orchestration).makeAccount( + remoteChainInfo.chainId, + connectionInfo.id, + connectionInfo.counterparty.connection_id, + ), this.facets.makeAccountWatcher, - { stakingDenom }, + { + stakingDenom, + }, ); }); }, }, makeAccountWatcher: { /** - * @param {[IcaAccount, ChainAddress]} results + * @param {IcaAccount} account * @param {{ stakingDenom: Denom }} ctx */ - onFulfilled([account, chainAddress], { stakingDenom }) { + onFulfilled(account, { stakingDenom }) { + return watch(E(account).getAddress(), this.facets.getAddressWatcher, { + stakingDenom, + account, + }); + }, + }, + getAddressWatcher: { + /** + * @param {ChainAddress} chainAddress + * @param {{ stakingDenom: Denom; account: IcaAccount }} ctx + */ + onFulfilled(chainAddress, { account, stakingDenom }) { return makeCosmosOrchestrationAccount(chainAddress, stakingDenom, { account, storageNode, diff --git a/packages/orchestration/src/service.js b/packages/orchestration/src/service.js index 88d7bc8ccff..56019b381c3 100644 --- a/packages/orchestration/src/service.js +++ b/packages/orchestration/src/service.js @@ -16,7 +16,7 @@ import { * @import {Connection, Port, PortAllocator} from '@agoric/network'; * @import {IBCConnectionID} from '@agoric/vats'; * @import {RemoteIbcAddress} from '@agoric/vats/tools/ibc-utils.js'; - * @import {VowTools} from '@agoric/vow'; + * @import {Vow, VowTools} from '@agoric/vow'; * @import {ICQConnection, IcaAccount, ICQConnectionKit, ChainAccountKit} from './types.js'; */ @@ -62,7 +62,7 @@ const getPower = (powers, name) => { */ const prepareOrchestrationKit = ( zone, - { when, watch }, + { watch }, makeChainAccountKit, makeICQConnectionKit, ) => @@ -93,11 +93,11 @@ const prepareOrchestrationKit = ( .returns(M.remotable('ConnectionKit Holder facet')), }), public: M.interface('OrchestrationService', { - makeAccount: M.callWhen(M.string(), M.string(), M.string()).returns( - M.remotable('ChainAccountKit'), + makeAccount: M.call(M.string(), M.string(), M.string()).returns( + NetworkShape.Vow$(M.remotable('ChainAccountKit')), ), - provideICQConnection: M.callWhen(M.string()).returns( - M.remotable('ICQConnection'), + provideICQConnection: M.call(M.string()).returns( + NetworkShape.Vow$(M.remotable('ICQConnection')), ), }), }, @@ -192,7 +192,7 @@ const prepareOrchestrationKit = ( * @param {IBCConnectionID} hostConnectionId the counterparty * connection_id * @param {IBCConnectionID} controllerConnectionId self connection_id - * @returns {Promise} + * @returns {Vow} */ makeAccount(chainId, hostConnectionId, controllerConnectionId) { const remoteConnAddr = makeICAChannelAddress( @@ -200,41 +200,36 @@ const prepareOrchestrationKit = ( controllerConnectionId, ); const portAllocator = getPower(this.state.powers, 'portAllocator'); - return when( - watch( - E(portAllocator).allocateICAControllerPort(), - this.facets.requestICAChannelWatcher, - { - chainId, - remoteConnAddr, - }, - ), + return watch( + E(portAllocator).allocateICAControllerPort(), + this.facets.requestICAChannelWatcher, + { + chainId, + remoteConnAddr, + }, ); }, /** * @param {IBCConnectionID} controllerConnectionId - * @returns {Promise} + * @returns {Vow | ICQConnection} */ provideICQConnection(controllerConnectionId) { if (this.state.icqConnections.has(controllerConnectionId)) { // TODO #9281 do not return synchronously. see https://github.com/Agoric/agoric-sdk/pull/9454#discussion_r1626898694 - return when( - this.state.icqConnections.get(controllerConnectionId).connection, - ); + return this.state.icqConnections.get(controllerConnectionId) + .connection; } const remoteConnAddr = makeICQChannelAddress(controllerConnectionId); const portAllocator = getPower(this.state.powers, 'portAllocator'); - return when( - watch( - // allocate a new Port for every Connection - // TODO #9317 optimize ICQ port allocation - E(portAllocator).allocateICQControllerPort(), - this.facets.requestICQChannelWatcher, - { - remoteConnAddr, - controllerConnectionId, - }, - ), + return watch( + // allocate a new Port for every Connection + // TODO #9317 optimize ICQ port allocation + E(portAllocator).allocateICQControllerPort(), + this.facets.requestICQChannelWatcher, + { + remoteConnAddr, + controllerConnectionId, + }, ); }, }, diff --git a/packages/orchestration/test/service.test.ts b/packages/orchestration/test/service.test.ts index a3e6febfabc..ab82977b121 100644 --- a/packages/orchestration/test/service.test.ts +++ b/packages/orchestration/test/service.test.ts @@ -1,11 +1,10 @@ import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js'; -import { E } from '@endo/far'; import { toRequestQueryJson } from '@agoric/cosmic-proto'; import { QueryBalanceRequest } from '@agoric/cosmic-proto/cosmos/bank/v1beta1/query.js'; import { MsgDelegate } from '@agoric/cosmic-proto/cosmos/staking/v1beta1/tx.js'; import { Any } from '@agoric/cosmic-proto/google/protobuf/any.js'; import { matches } from '@endo/patterns'; -import { heapVowTools } from '@agoric/vow/vat.js'; +import { heapVowTools, heapVowE as E } from '@agoric/vow/vat.js'; import { commonSetup } from './supports.js'; import { ChainAddressShape } from '../src/typeGuards.js'; From 166c6b959cc551e849876eeeeaa437b888052290 Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Thu, 27 Jun 2024 16:41:52 -0400 Subject: [PATCH 11/19] chore(orchestration): orchestrator is resumable --- .../orchestration/src/exos/orchestrator.js | 33 +++++++++---------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/packages/orchestration/src/exos/orchestrator.js b/packages/orchestration/src/exos/orchestrator.js index 5c5923af6b1..8047085a2ed 100644 --- a/packages/orchestration/src/exos/orchestrator.js +++ b/packages/orchestration/src/exos/orchestrator.js @@ -1,6 +1,7 @@ /** @file ChainAccount exo */ import { AmountShape } from '@agoric/ertp'; import { makeTracer } from '@agoric/internal'; +import { Shape as NetworkShape } from '@agoric/network'; import { E } from '@endo/far'; import { M } from '@endo/patterns'; import { @@ -25,7 +26,7 @@ import { * @import {MakeLocalOrchestrationAccountKit} from './local-orchestration-account.js'; * @import {MakeLocalChainFacade} from './local-chain-facade.js'; * @import {MakeRemoteChainFacade} from './remote-chain-facade.js'; - * @import {Chain, ChainInfo, CosmosChainInfo, IBCConnectionInfo, OrchestrationAccount, Orchestrator} from '../types.js'; + * @import {Chain, ChainInfo, CosmosChainInfo, IBCConnectionInfo, OrchestrationAccount, Orchestrator, PromiseToVow} from '../types.js'; */ const { Fail } = assert; @@ -33,8 +34,8 @@ const trace = makeTracer('Orchestrator'); /** @see {Orchestrator} */ export const OrchestratorI = M.interface('Orchestrator', { - getChain: M.callWhen(M.string()).returns(ChainInfoShape), - makeLocalAccount: M.callWhen().returns(LocalChainAccountShape), + getChain: M.call(M.string()).returns(NetworkShape.Vow$(ChainInfoShape)), + makeLocalAccount: M.call().returns(NetworkShape.Vow$(LocalChainAccountShape)), getBrandInfo: M.call(DenomShape).returns(BrandInfoShape), asAmount: M.call(DenomAmountShape).returns(AmountShape), }); @@ -62,7 +63,7 @@ export const prepareOrchestratorKit = ( localchain, makeLocalChainFacade, makeRemoteChainFacade, - vowTools: { watch, when }, + vowTools: { watch }, }, ) => zone.exoClassKit( @@ -111,27 +112,23 @@ export const prepareOrchestratorKit = ( }, }, orchestrator: { - /** @type {Orchestrator['getChain']} */ + /** @type {PromiseToVow} */ getChain(name) { if (name === 'agoric') { - // XXX when() until membrane - return when( - watch( - chainHub.getChainInfo('agoric'), - this.facets.makeLocalChainFacadeWatcher, - ), + // @ts-expect-error Type 'Vow' is not assignable to type 'Vow>'. + return watch( + chainHub.getChainInfo('agoric'), + this.facets.makeLocalChainFacadeWatcher, ); } - // XXX when() until membrane - return when( - watch( - chainHub.getChainsAndConnection('agoric', name), - this.facets.makeRemoteChainFacadeWatcher, - ), + // @ts-expect-error Type 'Vow' is not assignable to type 'Vow>'. + return watch( + chainHub.getChainsAndConnection('agoric', name), + this.facets.makeRemoteChainFacadeWatcher, ); }, makeLocalAccount() { - return when(watch(E(localchain).makeAccount())); + return watch(E(localchain).makeAccount()); }, getBrandInfo: () => Fail`not yet implemented`, asAmount: () => Fail`not yet implemented`, From 933ab299ee30c14530f92a9548fd79a35de3d0ff Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Fri, 28 Jun 2024 10:46:25 -0400 Subject: [PATCH 12/19] feat(smart-wallet): accept `Vow` for offerResult * Make smart wallet use `when` to accept a Vow for the result of an offer NOTE: this enables upgrade of the contract, NOT upgrade of the smart-wallet refs: #9308 Co-authored-by: Dean Tribble --- packages/boot/test/bootstrapTests/lca.test.ts | 4 +++- .../test/examples/stake-bld.contract.test.ts | 2 +- packages/smart-wallet/package.json | 1 + packages/smart-wallet/src/offerWatcher.js | 19 +++++++++++++------ packages/vats/tools/fake-bridge.js | 2 +- 5 files changed, 19 insertions(+), 9 deletions(-) diff --git a/packages/boot/test/bootstrapTests/lca.test.ts b/packages/boot/test/bootstrapTests/lca.test.ts index 051d359700f..cf30fe8ae24 100644 --- a/packages/boot/test/bootstrapTests/lca.test.ts +++ b/packages/boot/test/bootstrapTests/lca.test.ts @@ -109,6 +109,8 @@ test.serial('stakeBld', async t => { }, }, }), - // TODO should receive "simulated packet timeout" error + // TODO propagate error message through bridge + // FIXME should receive "simulated packet timeout" error + // { message: 'simulated packet timeout' }, ); }); diff --git a/packages/orchestration/test/examples/stake-bld.contract.test.ts b/packages/orchestration/test/examples/stake-bld.contract.test.ts index 16bbc79956c..b6053e47f70 100644 --- a/packages/orchestration/test/examples/stake-bld.contract.test.ts +++ b/packages/orchestration/test/examples/stake-bld.contract.test.ts @@ -122,7 +122,7 @@ test('makeStakeBldInvitation', async t => { ); const delegateOffer = await E(zoe).offer(delegateInv); await t.throwsAsync(E(delegateOffer).getOfferResult(), { - message: 'simualted packet timeout', + message: 'simulated packet timeout', }); } diff --git a/packages/smart-wallet/package.json b/packages/smart-wallet/package.json index f2f805cafb1..e1bca3a44d9 100644 --- a/packages/smart-wallet/package.json +++ b/packages/smart-wallet/package.json @@ -34,6 +34,7 @@ "@agoric/store": "^0.9.2", "@agoric/vat-data": "^0.5.2", "@agoric/vats": "^0.15.1", + "@agoric/vow": "^0.1.0", "@agoric/zoe": "^0.26.2", "@endo/eventual-send": "^1.2.2", "@endo/far": "^1.1.2", diff --git a/packages/smart-wallet/src/offerWatcher.js b/packages/smart-wallet/src/offerWatcher.js index 3b2bc004f62..baf17775ea2 100644 --- a/packages/smart-wallet/src/offerWatcher.js +++ b/packages/smart-wallet/src/offerWatcher.js @@ -9,9 +9,12 @@ import { } from '@agoric/zoe/src/typeGuards.js'; import { AmountShape } from '@agoric/ertp/src/typeGuards.js'; import { deeplyFulfilledObject, objectMap } from '@agoric/internal'; +import { heapVowTools } from '@agoric/vow/vat.js'; import { UNPUBLISHED_RESULT } from './offers.js'; +const { when, allVows } = heapVowTools; + /** * @import {OfferSpec} from "./offers.js"; * @import {ContinuingOfferResult} from "./types.js"; @@ -37,7 +40,8 @@ import { UNPUBLISHED_RESULT } from './offers.js'; * @param {UserSeat} seat */ const watchForOfferResult = ({ resultWatcher }, seat) => { - const p = E(seat).getOfferResult(); + // NOTE: this enables upgrade of the contract, NOT upgrade of the smart-wallet. See #9308 + const p = when(E(seat).getOfferResult()); watchPromise(p, resultWatcher, seat); return p; }; @@ -67,11 +71,14 @@ const watchForPayout = ({ paymentWatcher }, seat) => { * @param {UserSeat} seat */ export const watchOfferOutcomes = (watchers, seat) => { - return Promise.all([ - watchForOfferResult(watchers, seat), - watchForNumWants(watchers, seat), - watchForPayout(watchers, seat), - ]); + // NOTE: this enables upgrade of the contract, NOT upgrade of the smart-wallet. See #9308 + return when( + allVows([ + watchForOfferResult(watchers, seat), + watchForNumWants(watchers, seat), + watchForPayout(watchers, seat), + ]), + ); }; const offerWatcherGuard = harden({ diff --git a/packages/vats/tools/fake-bridge.js b/packages/vats/tools/fake-bridge.js index 1a45a636660..7920c570f79 100644 --- a/packages/vats/tools/fake-bridge.js +++ b/packages/vats/tools/fake-bridge.js @@ -194,7 +194,7 @@ export const makeFakeLocalchainBridge = (zone, onToBridge = () => {}) => { } case '/cosmos.staking.v1beta1.MsgDelegate': { if (message.amount.amount === '504') { - throw Error('simualted packet timeout'); + throw Error('simulated packet timeout'); } return /** @type {JsonSafe} */ ({}); } From 6f6b8917f5145ad5d81ed761f88e7c9142b1ef79 Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Fri, 28 Jun 2024 10:47:52 -0400 Subject: [PATCH 13/19] fix(orchestration): disable type casts in skipped boot tests --- .../bootstrapTests/vat-orchestration.test.ts | 23 +++++++------------ 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/packages/boot/test/bootstrapTests/vat-orchestration.test.ts b/packages/boot/test/bootstrapTests/vat-orchestration.test.ts index 9e156718911..fed0336a3c9 100644 --- a/packages/boot/test/bootstrapTests/vat-orchestration.test.ts +++ b/packages/boot/test/bootstrapTests/vat-orchestration.test.ts @@ -1,5 +1,5 @@ import { test as anyTest } from '@agoric/zoe/tools/prepare-test-env-ava.js'; -import type { ExecutionContext, TestFn } from 'ava'; +import type { TestFn } from 'ava'; import { toRequestQueryJson } from '@agoric/cosmic-proto'; import { @@ -11,10 +11,7 @@ import { MsgDelegate, MsgDelegateResponse, } from '@agoric/cosmic-proto/cosmos/staking/v1beta1/tx.js'; -import type { - OrchestrationService, - ICQConnection, -} from '@agoric/orchestration'; +import type { OrchestrationService } from '@agoric/orchestration'; import { decodeBase64 } from '@endo/base64'; import { M, matches } from '@endo/patterns'; import { @@ -75,8 +72,7 @@ test.skip('makeAccount returns an ICA connection', async t => { runUtils: { EV }, } = t.context; - const orchestration: OrchestrationService = - await EV.vat('bootstrap').consumeItem('orchestration'); + const orchestration = await EV.vat('bootstrap').consumeItem('orchestration'); const account = await EV(orchestration).makeAccount( 'somechain-1', @@ -112,8 +108,7 @@ test.skip('ICA connection can be closed', async t => { runUtils: { EV }, } = t.context; - const orchestration: OrchestrationService = - await EV.vat('bootstrap').consumeItem('orchestration'); + const orchestration = await EV.vat('bootstrap').consumeItem('orchestration'); const account = await EV(orchestration).makeAccount( 'somechain-1', @@ -134,8 +129,7 @@ test.skip('ICA connection can send msg with proto3', async t => { runUtils: { EV }, } = t.context; - const orchestration: OrchestrationService = - await EV.vat('bootstrap').consumeItem('orchestration'); + const orchestration = await EV.vat('bootstrap').consumeItem('orchestration'); const account = await EV(orchestration).makeAccount( 'somechain-1', @@ -144,7 +138,6 @@ test.skip('ICA connection can send msg with proto3', async t => { ); t.truthy(account, 'makeAccount returns an account'); - // @ts-expect-error intentional await t.throwsAsync(EV(account).executeEncodedTx('malformed'), { message: 'In "executeEncodedTx" method of (ChainAccountKit account): arg 0: string "malformed" - Must be a copyArray', @@ -195,7 +188,7 @@ test.skip('Query connection can be created', async t => { } = t.context; type Powers = { orchestration: OrchestrationService }; - const contract = async ({ orchestration }: Powers) => { + const contract = async ({ orchestration }) => { const connection = await EV(orchestration).provideICQConnection('connection-0'); t.log('Query Connection', connection); @@ -221,8 +214,8 @@ test.skip('Query connection can send a query', async t => { } = t.context; type Powers = { orchestration: OrchestrationService }; - const contract = async ({ orchestration }: Powers) => { - const queryConnection: ICQConnection = + const contract = async ({ orchestration }) => { + const queryConnection = await EV(orchestration).provideICQConnection('connection-0'); const [result] = await EV(queryConnection).query([balanceQuery]); From 26d450899c86d874fac2475b0829735902bb1b3b Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Fri, 28 Jun 2024 10:48:18 -0400 Subject: [PATCH 14/19] lint: tighten resumable restricted syntax to error --- .eslintrc.cjs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 005f64f6b21..b134ca606ac 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -170,8 +170,7 @@ module.exports = { // Modules with exports that must be resumable files: ['packages/orchestration/src/exos/**'], rules: { - // TODO tighten to error - 'no-restricted-syntax': ['warn', ...resumable], + 'no-restricted-syntax': ['error', ...resumable], }, }, { From 3660944b5d9076e2ff39d7d24ac014cc152b6cdd Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Fri, 28 Jun 2024 12:01:42 -0400 Subject: [PATCH 15/19] chore(orchestration): document TODOs with ticket numbers --- packages/orchestration/src/exos/chain-account-kit.js | 2 ++ .../orchestration/src/exos/cosmos-orchestration-account.js | 1 + packages/orchestration/src/exos/local-chain-facade.js | 2 ++ packages/orchestration/src/exos/local-orchestration-account.js | 1 + packages/orchestration/src/exos/orchestrator.js | 2 ++ packages/orchestration/src/exos/remote-chain-facade.js | 3 ++- 6 files changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/orchestration/src/exos/chain-account-kit.js b/packages/orchestration/src/exos/chain-account-kit.js index a735afac469..f4cae4c2867 100644 --- a/packages/orchestration/src/exos/chain-account-kit.js +++ b/packages/orchestration/src/exos/chain-account-kit.js @@ -102,10 +102,12 @@ export const prepareChainAccountKit = (zone, { watch, asVow }) => ); }, getBalance(_denom) { + // TODO https://github.com/Agoric/agoric-sdk/issues/9610 // UNTIL https://github.com/Agoric/agoric-sdk/issues/9326 return asVow(() => Fail`not yet implemented`); }, getBalances() { + // TODO https://github.com/Agoric/agoric-sdk/issues/9610 // UNTIL https://github.com/Agoric/agoric-sdk/issues/9326 return asVow(() => Fail`not yet implemented`); }, diff --git a/packages/orchestration/src/exos/cosmos-orchestration-account.js b/packages/orchestration/src/exos/cosmos-orchestration-account.js index d4e6d4b5f47..53a42671b9e 100644 --- a/packages/orchestration/src/exos/cosmos-orchestration-account.js +++ b/packages/orchestration/src/exos/cosmos-orchestration-account.js @@ -374,6 +374,7 @@ export const prepareCosmosOrchestrationAccountKit = ( ); }, getBalances() { + // TODO https://github.com/Agoric/agoric-sdk/issues/9610 return asVow(() => Fail`not yet implemented`); }, /** diff --git a/packages/orchestration/src/exos/local-chain-facade.js b/packages/orchestration/src/exos/local-chain-facade.js index 694ef809224..5684f7de769 100644 --- a/packages/orchestration/src/exos/local-chain-facade.js +++ b/packages/orchestration/src/exos/local-chain-facade.js @@ -67,8 +67,10 @@ const prepareLocalChainFacadeKit = ( /** @returns {Vow>>} */ makeAccount() { const lcaP = E(localchain).makeAccount(); + // TODO #9449 fix types // @ts-expect-error 'Vow> is not assignable to type 'Vow' return watch( + // TODO #9449 fix types // @ts-expect-error Property 'getAddress' does not exist on type 'EMethods Fail`not yet implemented`); }, diff --git a/packages/orchestration/src/exos/orchestrator.js b/packages/orchestration/src/exos/orchestrator.js index 8047085a2ed..8f1ca88ee75 100644 --- a/packages/orchestration/src/exos/orchestrator.js +++ b/packages/orchestration/src/exos/orchestrator.js @@ -115,12 +115,14 @@ export const prepareOrchestratorKit = ( /** @type {PromiseToVow} */ getChain(name) { if (name === 'agoric') { + // TODO #9449 fix types // @ts-expect-error Type 'Vow' is not assignable to type 'Vow>'. return watch( chainHub.getChainInfo('agoric'), this.facets.makeLocalChainFacadeWatcher, ); } + // TODO #9449 fix types // @ts-expect-error Type 'Vow' is not assignable to type 'Vow>'. return watch( chainHub.getChainsAndConnection('agoric', name), diff --git a/packages/orchestration/src/exos/remote-chain-facade.js b/packages/orchestration/src/exos/remote-chain-facade.js index c9b06e69d1d..b2d7039c5e3 100644 --- a/packages/orchestration/src/exos/remote-chain-facade.js +++ b/packages/orchestration/src/exos/remote-chain-facade.js @@ -79,7 +79,8 @@ const prepareRemoteChainFacadeKit = ( /** @returns {Vow>>} */ makeAccount() { - // @ts-expect-error 'Vow> is not assignable to type 'Vow' + // TODO #9449 fix types + // @ts-expect-error 'Vow> is not assignable to type 'Vow' #9449 return asVow(() => { const { remoteChainInfo, connectionInfo } = this.state; const stakingDenom = remoteChainInfo.stakingTokens?.[0]?.denom; From d7bf05d5b4ab8d6a69b730713355fd607480a424 Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Fri, 28 Jun 2024 16:46:12 -0400 Subject: [PATCH 16/19] test(vow): allVows pipelining behavior adds tests that demonstrate: 1. allVows supporting pipelining if the operand is a Promise 2. allVows (original E?) does not support pipelining if the operand is a Vow --- packages/vow/test/watch-utils.test.js | 75 +++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/packages/vow/test/watch-utils.test.js b/packages/vow/test/watch-utils.test.js index ea5393e55cd..d0abc41abac 100644 --- a/packages/vow/test/watch-utils.test.js +++ b/packages/vow/test/watch-utils.test.js @@ -2,6 +2,7 @@ import test from 'ava'; import { makeHeapZone } from '@agoric/base-zone/heap.js'; +import { E, getInterfaceOf } from '@endo/far'; import { prepareVowTools } from '../src/tools.js'; @@ -124,3 +125,77 @@ test('allVows can accept passable data (PureData)', async t => { t.is(result.length, 4); t.deepEqual(result, ['vow', 'string', 1n, { obj: true }]); }); + +const prepareAccount = zone => + zone.exoClass('Account', undefined, address => ({ address }), { + getAddress() { + return Promise.resolve(this.state.address); + }, + }); + +test('allVows supports Promise pipelining', async t => { + const zone = makeHeapZone(); + const { watch, when, allVows } = prepareVowTools(zone); + + // makeAccount returns a Promise + const prepareLocalChain = makeAccount => { + const localchainMock = zone.exoClass( + 'Localchain', + undefined, + () => ({ accountIndex: 0 }), + { + makeAccount() { + this.state.accountIndex += 1; + return Promise.resolve( + makeAccount(`agoric1foo${this.state.accountIndex}`), + ); + }, + }, + ); + return localchainMock(); + }; + + const Localchain = prepareLocalChain(prepareAccount(zone)); + const lcaP = E(Localchain).makeAccount(); + const results = await when(watch(allVows([lcaP, E(lcaP).getAddress()]))); + t.is(results.length, 2); + const [acct, address] = results; + t.is(getInterfaceOf(acct), 'Alleged: Account'); + t.is( + address, + 'agoric1foo1', + 'pipelining does not result in multiple instantiations', + ); +}); + +test('allVows does NOT support Vow pipelining', async t => { + const zone = makeHeapZone(); + const { watch, when, allVows } = prepareVowTools(zone); + + // makeAccount returns a Vow + const prepareLocalChainVowish = makeAccount => { + const localchainMock = zone.exoClass( + 'Localchain', + undefined, + () => ({ accountIndex: 0 }), + { + makeAccount() { + this.state.accountIndex += 1; + return watch( + Promise.resolve( + makeAccount(`agoric1foo${this.state.accountIndex}`), + ), + ); + }, + }, + ); + return localchainMock(); + }; + const Localchain = prepareLocalChainVowish(prepareAccount(zone)); + const lcaP = E(Localchain).makeAccount(); + // @ts-expect-error Property 'getAddress' does not exist on type + // 'EMethods & { payload: VowPayload; }>>'. + await t.throwsAsync(when(watch(allVows([lcaP, E(lcaP).getAddress()]))), { + message: 'target has no method "getAddress", has []', + }); +}); From ff47e1cd10ece84093663b9ed388428bc29384fa Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Fri, 28 Jun 2024 16:51:50 -0400 Subject: [PATCH 17/19] chore(orchestration): reduce noise around Vow$ shape --- .../src/exos/cosmos-orchestration-account.js | 10 +++++----- .../src/exos/local-orchestration-account.js | 12 ++++++------ packages/orchestration/src/exos/orchestrator.js | 5 +++-- packages/orchestration/src/service.js | 10 +++++----- .../orchestration/src/utils/orchestrationAccount.js | 6 ++++-- 5 files changed, 23 insertions(+), 20 deletions(-) diff --git a/packages/orchestration/src/exos/cosmos-orchestration-account.js b/packages/orchestration/src/exos/cosmos-orchestration-account.js index 53a42671b9e..2a9bed7625b 100644 --- a/packages/orchestration/src/exos/cosmos-orchestration-account.js +++ b/packages/orchestration/src/exos/cosmos-orchestration-account.js @@ -49,6 +49,8 @@ import { dateInSeconds } from '../utils/time.js'; const trace = makeTracer('ComosOrchestrationAccountHolder'); const { Fail } = assert; +const { Vow$ } = NetworkShape; // TODO #9611 + /** * @typedef {object} ComosOrchestrationAccountNotification * @property {ChainAddress} chainAddress @@ -81,11 +83,9 @@ export const IcaAccountHolderI = M.interface('IcaAccountHolder', { AmountArgShape, ).returns(VowShape), withdrawReward: M.call(ChainAddressShape).returns( - NetworkShape.Vow$(M.arrayOf(ChainAmountShape)), - ), - withdrawRewards: M.call().returns( - NetworkShape.Vow$(M.arrayOf(ChainAmountShape)), + Vow$(M.arrayOf(ChainAmountShape)), ), + withdrawRewards: M.call().returns(Vow$(M.arrayOf(ChainAmountShape))), undelegate: M.call(M.arrayOf(DelegationShape)).returns(VowShape), }); @@ -130,7 +130,7 @@ export const prepareCosmosOrchestrationAccountKit = ( undelegateWatcher: M.interface('undelegateWatcher', { onFulfilled: M.call(M.string()) .optional(M.arrayOf(M.undefined())) // empty context - .returns(NetworkShape.Vow$(M.promise())), + .returns(Vow$(M.promise())), }), withdrawRewardWatcher: M.interface('withdrawRewardWatcher', { onFulfilled: M.call(M.string()) diff --git a/packages/orchestration/src/exos/local-orchestration-account.js b/packages/orchestration/src/exos/local-orchestration-account.js index 0d6660f2a04..a99ef53164e 100644 --- a/packages/orchestration/src/exos/local-orchestration-account.js +++ b/packages/orchestration/src/exos/local-orchestration-account.js @@ -33,6 +33,8 @@ import { dateInSeconds, makeTimestampHelper } from '../utils/time.js'; const trace = makeTracer('LOA'); const { Fail } = assert; +const { Vow$ } = NetworkShape; // TODO #9611 + /** * @typedef {object} LocalChainAccountNotification * @property {string} address @@ -51,10 +53,8 @@ const HolderI = M.interface('holder', { getPublicTopics: M.call().returns(TopicsRecordShape), delegate: M.call(M.string(), AmountShape).returns(VowShape), undelegate: M.call(M.string(), AmountShape).returns(VowShape), - withdraw: M.call(AmountShape).returns(NetworkShape.Vow$(PaymentShape)), - executeTx: M.call(M.arrayOf(M.record())).returns( - NetworkShape.Vow$(M.record()), - ), + withdraw: M.call(AmountShape).returns(Vow$(PaymentShape)), + executeTx: M.call(M.arrayOf(M.record())).returns(Vow$(M.record())), }); /** @type {{ [name: string]: [description: string, valueShape: Pattern] }} */ @@ -93,7 +93,7 @@ export const prepareLocalOrchestrationAccountKit = ( getChainInfoWatcher: M.interface('getChainInfoWatcher', { onFulfilled: M.call(M.record()) // agoric chain info .optional({ destination: ChainAddressShape }) - .returns(NetworkShape.Vow$(M.record())), // connection info + .returns(Vow$(M.record())), // connection info }), transferWatcher: M.interface('transferWatcher', { onFulfilled: M.call([M.record(), M.nat()]) @@ -102,7 +102,7 @@ export const prepareLocalOrchestrationAccountKit = ( opts: M.or(M.undefined(), IBCTransferOptionsShape), amount: ChainAmountShape, }) - .returns(NetworkShape.Vow$(M.record())), + .returns(Vow$(M.record())), }), extractFirstResultWatcher: M.interface('extractFirstResultWatcher', { onFulfilled: M.call([M.record()]) diff --git a/packages/orchestration/src/exos/orchestrator.js b/packages/orchestration/src/exos/orchestrator.js index 8f1ca88ee75..8703f6aaf04 100644 --- a/packages/orchestration/src/exos/orchestrator.js +++ b/packages/orchestration/src/exos/orchestrator.js @@ -30,12 +30,13 @@ import { */ const { Fail } = assert; +const { Vow$ } = NetworkShape; // TODO #9611 const trace = makeTracer('Orchestrator'); /** @see {Orchestrator} */ export const OrchestratorI = M.interface('Orchestrator', { - getChain: M.call(M.string()).returns(NetworkShape.Vow$(ChainInfoShape)), - makeLocalAccount: M.call().returns(NetworkShape.Vow$(LocalChainAccountShape)), + getChain: M.call(M.string()).returns(Vow$(ChainInfoShape)), + makeLocalAccount: M.call().returns(Vow$(LocalChainAccountShape)), getBrandInfo: M.call(DenomShape).returns(BrandInfoShape), asAmount: M.call(DenomAmountShape).returns(AmountShape), }); diff --git a/packages/orchestration/src/service.js b/packages/orchestration/src/service.js index 56019b381c3..15bb84a736d 100644 --- a/packages/orchestration/src/service.js +++ b/packages/orchestration/src/service.js @@ -21,7 +21,7 @@ import { */ const { Fail, bare } = assert; - +const { Vow$ } = NetworkShape; // TODO #9611 /** * @typedef {object} OrchestrationPowers * @property {Remote} portAllocator @@ -72,7 +72,7 @@ const prepareOrchestrationKit = ( requestICAChannelWatcher: M.interface('RequestICAChannelWatcher', { onFulfilled: M.call(M.remotable('Port')) .optional({ chainId: M.string(), remoteConnAddr: M.string() }) - .returns(NetworkShape.Vow$(NetworkShape.Connection)), + .returns(Vow$(NetworkShape.Connection)), }), requestICQChannelWatcher: M.interface('RequestICQChannelWatcher', { onFulfilled: M.call(M.remotable('Port')) @@ -80,7 +80,7 @@ const prepareOrchestrationKit = ( remoteConnAddr: M.string(), controllerConnectionId: M.string(), }) - .returns(NetworkShape.Vow$(NetworkShape.Connection)), + .returns(Vow$(NetworkShape.Connection)), }), channelOpenWatcher: M.interface('ChannelOpenWatcher', { onFulfilled: M.call(M.remotable('Connection')) @@ -94,10 +94,10 @@ const prepareOrchestrationKit = ( }), public: M.interface('OrchestrationService', { makeAccount: M.call(M.string(), M.string(), M.string()).returns( - NetworkShape.Vow$(M.remotable('ChainAccountKit')), + Vow$(M.remotable('ChainAccountKit')), ), provideICQConnection: M.call(M.string()).returns( - NetworkShape.Vow$(M.remotable('ICQConnection')), + Vow$(M.remotable('ICQConnection')), ), }), }, diff --git a/packages/orchestration/src/utils/orchestrationAccount.js b/packages/orchestration/src/utils/orchestrationAccount.js index f0d5bc1bdf0..728fab3acd3 100644 --- a/packages/orchestration/src/utils/orchestrationAccount.js +++ b/packages/orchestration/src/utils/orchestrationAccount.js @@ -6,11 +6,13 @@ import { AmountArgShape, ChainAddressShape, CoinShape } from '../typeGuards.js'; /** @import {OrchestrationAccountI} from '../orchestration-api.js'; */ +const { Vow$ } = NetworkShape; // TODO #9611 + /** @see {OrchestrationAccountI} */ export const orchestrationAccountMethods = { getAddress: M.call().returns(ChainAddressShape), - getBalance: M.call(M.any()).returns(NetworkShape.Vow$(CoinShape)), - getBalances: M.call().returns(NetworkShape.Vow$(M.arrayOf(CoinShape))), + getBalance: M.call(M.any()).returns(Vow$(CoinShape)), + getBalances: M.call().returns(Vow$(M.arrayOf(CoinShape))), send: M.call(ChainAddressShape, AmountArgShape).returns(VowShape), transfer: M.call(AmountArgShape, ChainAddressShape) .optional(M.record()) From 194ac48dcbc756242972eebf46567f3c1d9d0c15 Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Fri, 28 Jun 2024 17:24:37 -0400 Subject: [PATCH 18/19] test(types): PromiseToVow behavior --- .../src/exos/local-chain-facade.js | 2 +- .../src/exos/remote-chain-facade.js | 2 +- packages/orchestration/src/internal.ts | 17 +++++- packages/orchestration/test/types.test-d.ts | 57 ++++++++++++++++++- 4 files changed, 72 insertions(+), 6 deletions(-) diff --git a/packages/orchestration/src/exos/local-chain-facade.js b/packages/orchestration/src/exos/local-chain-facade.js index 5684f7de769..492fa457ecc 100644 --- a/packages/orchestration/src/exos/local-chain-facade.js +++ b/packages/orchestration/src/exos/local-chain-facade.js @@ -68,7 +68,7 @@ const prepareLocalChainFacadeKit = ( makeAccount() { const lcaP = E(localchain).makeAccount(); // TODO #9449 fix types - // @ts-expect-error 'Vow> is not assignable to type 'Vow' + // @ts-expect-error Type 'Vow' is not assignable to type 'Vow'. return watch( // TODO #9449 fix types // @ts-expect-error Property 'getAddress' does not exist on type 'EMethods>>} */ makeAccount() { // TODO #9449 fix types - // @ts-expect-error 'Vow> is not assignable to type 'Vow' #9449 + // @ts-expect-error Type 'Vow' is not assignable to type 'Vow' return asVow(() => { const { remoteChainInfo, connectionInfo } = this.state; const stakingDenom = remoteChainInfo.stakingTokens?.[0]?.denom; diff --git a/packages/orchestration/src/internal.ts b/packages/orchestration/src/internal.ts index 04c4266ebfe..e9960c97f5b 100644 --- a/packages/orchestration/src/internal.ts +++ b/packages/orchestration/src/internal.ts @@ -1,8 +1,19 @@ import type { Vow } from '@agoric/vow'; -export type PromiseToVow = T extends (...args: infer A) => Promise - ? (...args: A) => Vow - : never; +/** + * Converts a function type that returns a Promise to a function type that + * returns a Vow. If the input is not a function returning a Promise, it + * preserves the original type. + * + * @template T - The type to transform + */ +export type PromiseToVow = T extends ( + ...args: infer Args +) => Promise + ? (...args: Args) => Vow + : T extends (...args: infer Args) => infer R + ? (...args: Args) => R + : T; export type VowifyAll = { [K in keyof T]: PromiseToVow; diff --git a/packages/orchestration/test/types.test-d.ts b/packages/orchestration/test/types.test-d.ts index 4e2b67e6d5b..30b947e0634 100644 --- a/packages/orchestration/test/types.test-d.ts +++ b/packages/orchestration/test/types.test-d.ts @@ -5,14 +5,16 @@ import { expectNotType, expectType } from 'tsd'; import { typedJson } from '@agoric/cosmic-proto'; import type { MsgDelegateResponse } from '@agoric/cosmic-proto/cosmos/staking/v1beta1/tx.js'; import type { QueryAllBalancesResponse } from '@agoric/cosmic-proto/cosmos/bank/v1beta1/query.js'; +import type { Vow } from '@agoric/vow'; import type { ChainAddress, CosmosValidatorAddress, StakingAccountActions, + OrchestrationAccount, } from '../src/types.js'; import type { LocalOrchestrationAccountKit } from '../src/exos/local-orchestration-account.js'; import { prepareCosmosOrchestrationAccount } from '../src/exos/cosmos-orchestration-account.js'; -import type { VowifyAll } from '../src/internal.js'; +import type { PromiseToVow, VowifyAll } from '../src/internal.js'; const anyVal = null as any; @@ -69,3 +71,56 @@ expectNotType(chainAddr); anyVal, ) satisfies VowifyAll; } + +// PromiseToVow +{ + type PromiseFn = () => Promise; + type SyncFn = () => number; + + type VowFn = PromiseToVow; + type StillSyncFn = PromiseToVow; + + // Use type assertion instead of casting + const vowFn: VowFn = (() => ({}) as Vow) as VowFn; + expectType<() => Vow>(vowFn); + + const syncFn: StillSyncFn = (() => 42) as StillSyncFn; + expectType<() => number>(syncFn); + + // Negative test + expectNotType<() => Promise>(vowFn); +} + +// PromiseToVow with TransferSteps +{ + type TransferStepsVow = PromiseToVow< + OrchestrationAccount['transferSteps'] + >; + + const transferStepsVow: TransferStepsVow = (...args: any[]): Vow => + ({}) as any; + expectType<(...args: any[]) => Vow>(transferStepsVow); +} + +// VowifyAll +{ + type PromiseObject = { + foo: () => Promise; + bar: (x: string) => Promise; + bizz: () => Record; + }; + + type VowObject = VowifyAll; + + const vowObject: VowObject = { + foo: () => ({}) as Vow, + bar: (x: string) => ({}) as Vow, + bizz: () => ({ foo: 1 }), + }; + + expectType<{ + foo: () => Vow; + bar: (x: string) => Vow; + bizz: () => Record; + }>(vowObject); +} From 100e0a3a496e985b372e7ce7a92c25e9c2f6135b Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Fri, 28 Jun 2024 17:44:35 -0400 Subject: [PATCH 19/19] chore: limit unnecessary allocation to watcher ctx --- .../src/exos/agoric-names-tools.js | 8 ++++---- .../src/exos/local-orchestration-account.js | 20 +++++++++---------- .../src/exos/remote-chain-facade.js | 10 ++++------ 3 files changed, 17 insertions(+), 21 deletions(-) diff --git a/packages/orchestration/src/exos/agoric-names-tools.js b/packages/orchestration/src/exos/agoric-names-tools.js index cc36ee065f5..b6280b77180 100644 --- a/packages/orchestration/src/exos/agoric-names-tools.js +++ b/packages/orchestration/src/exos/agoric-names-tools.js @@ -41,7 +41,7 @@ export const makeResumableAgoricNamesHack = ( }), vbankAssetEntriesWatcher: M.interface('vbankAssetEntriesWatcher', { onFulfilled: M.call(M.arrayOf(M.record())) - .optional({ brand: BrandShape }) + .optional(BrandShape) .returns(VowShape), }), }, @@ -55,9 +55,9 @@ export const makeResumableAgoricNamesHack = ( vbankAssetEntriesWatcher: { /** * @param {AssetInfo[]} assets - * @param {{ brand: Brand<'nat'> }} ctx + * @param {Brand<'nat'>} brand */ - onFulfilled(assets, { brand }) { + onFulfilled(assets, brand) { return asVow(() => { const { vbankAssetsByBrand } = this.state; vbankAssetsByBrand.addAll( @@ -96,7 +96,7 @@ export const makeResumableAgoricNamesHack = ( return watch( vbankAssetEntriesP, this.facets.vbankAssetEntriesWatcher, - { brand }, + brand, ); }); }, diff --git a/packages/orchestration/src/exos/local-orchestration-account.js b/packages/orchestration/src/exos/local-orchestration-account.js index a99ef53164e..be4edc4efaf 100644 --- a/packages/orchestration/src/exos/local-orchestration-account.js +++ b/packages/orchestration/src/exos/local-orchestration-account.js @@ -92,7 +92,7 @@ export const prepareLocalOrchestrationAccountKit = ( }), getChainInfoWatcher: M.interface('getChainInfoWatcher', { onFulfilled: M.call(M.record()) // agoric chain info - .optional({ destination: ChainAddressShape }) + .optional(ChainAddressShape) .returns(Vow$(M.record())), // connection info }), transferWatcher: M.interface('transferWatcher', { @@ -116,9 +116,7 @@ export const prepareLocalOrchestrationAccountKit = ( }), getBalanceWatcher: M.interface('getBalanceWatcher', { onFulfilled: M.call(AmountShape) - .optional({ - denom: DenomShape, - }) + .optional(DenomShape) .returns(DenomAmountShape), }), invitationMakers: M.interface('invitationMakers', { @@ -196,9 +194,9 @@ export const prepareLocalOrchestrationAccountKit = ( getChainInfoWatcher: { /** * @param {ChainInfo} agoricChainInfo - * @param {{ destination: ChainAddress }} ctx + * @param {ChainAddress} destination */ - onFulfilled(agoricChainInfo, { destination }) { + onFulfilled(agoricChainInfo, destination) { return chainHub.getConnectionInfo( agoricChainInfo.chainId, destination.chainId, @@ -263,7 +261,7 @@ export const prepareLocalOrchestrationAccountKit = ( */ returnVoidWatcher: { /** - * @param {any} _result + * @param {unknown} _result */ onFulfilled(_result) { return undefined; @@ -276,10 +274,10 @@ export const prepareLocalOrchestrationAccountKit = ( getBalanceWatcher: { /** * @param {Amount<'nat'>} natAmount - * @param {{ denom: DenomAmount['denom'] }} ctx + * @param {DenomAmount['denom']} denom * @returns {DenomAmount} */ - onFulfilled(natAmount, { denom }) { + onFulfilled(natAmount, denom) { return harden({ denom, value: natAmount.value }); }, }, @@ -300,7 +298,7 @@ export const prepareLocalOrchestrationAccountKit = ( return watch( E(this.state.account).getBalance(brand), this.facets.getBalanceWatcher, - { denom }, + denom, ); }, getBalances() { @@ -413,7 +411,7 @@ export const prepareLocalOrchestrationAccountKit = ( const connectionInfoV = watch( chainHub.getChainInfo('agoric'), this.facets.getChainInfoWatcher, - { destination }, + destination, ); // set a `timeoutTimestamp` if caller does not supply either `timeoutHeight` or `timeoutTimestamp` diff --git a/packages/orchestration/src/exos/remote-chain-facade.js b/packages/orchestration/src/exos/remote-chain-facade.js index 0b6c4efa833..a88d903d60c 100644 --- a/packages/orchestration/src/exos/remote-chain-facade.js +++ b/packages/orchestration/src/exos/remote-chain-facade.js @@ -54,7 +54,7 @@ const prepareRemoteChainFacadeKit = ( public: ChainFacadeI, makeAccountWatcher: M.interface('makeAccountWatcher', { onFulfilled: M.call(M.remotable()) - .optional({ stakingDenom: M.string() }) + .optional(M.string()) .returns(VowShape), }), getAddressWatcher: M.interface('makeAccountWatcher', { @@ -95,9 +95,7 @@ const prepareRemoteChainFacadeKit = ( connectionInfo.counterparty.connection_id, ), this.facets.makeAccountWatcher, - { - stakingDenom, - }, + stakingDenom, ); }); }, @@ -105,9 +103,9 @@ const prepareRemoteChainFacadeKit = ( makeAccountWatcher: { /** * @param {IcaAccount} account - * @param {{ stakingDenom: Denom }} ctx + * @param {Denom} stakingDenom */ - onFulfilled(account, { stakingDenom }) { + onFulfilled(account, stakingDenom) { return watch(E(account).getAddress(), this.facets.getAddressWatcher, { stakingDenom, account,