From 4383aec170da7e1243257a6e6ca6cd8d2d9b9440 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Fri, 21 Jun 2024 13:19:54 -0700 Subject: [PATCH 1/4] feat: ChainAccountKit returns vows Co-authored-by: 0xPatrick --- .../src/exos/chain-account-kit.js | 98 ++++++++++++------- packages/orchestration/test/service.test.ts | 23 +++-- 2 files changed, 74 insertions(+), 47 deletions(-) diff --git a/packages/orchestration/src/exos/chain-account-kit.js b/packages/orchestration/src/exos/chain-account-kit.js index 4d14e1410eb..a525b75a172 100644 --- a/packages/orchestration/src/exos/chain-account-kit.js +++ b/packages/orchestration/src/exos/chain-account-kit.js @@ -1,7 +1,7 @@ /** @file ChainAccount exo */ import { NonNullish } from '@agoric/assert'; -import { PurseShape } from '@agoric/ertp'; import { makeTracer } from '@agoric/internal'; +import { VowShape } from '@agoric/vow'; import { E } from '@endo/far'; import { M } from '@endo/patterns'; import { @@ -15,7 +15,7 @@ import { makeTxPacket, parseTxPacket } from '../utils/packet.js'; /** * @import {Zone} from '@agoric/base-zone'; * @import {Connection, Port} from '@agoric/network'; - * @import {Remote, VowTools} from '@agoric/vow'; + * @import {PromiseVow, Remote, Vow, VowTools} from '@agoric/vow'; * @import {AnyJson} from '@agoric/cosmic-proto'; * @import {TxBody} from '@agoric/cosmic-proto/cosmos/tx/v1beta1/tx.js'; * @import {LocalIbcAddress, RemoteIbcAddress} from '@agoric/vats/tools/ibc-utils.js'; @@ -30,17 +30,17 @@ const UNPARSABLE_CHAIN_ADDRESS = 'UNPARSABLE_CHAIN_ADDRESS'; export const ChainAccountI = M.interface('ChainAccount', { getAddress: M.call().returns(ChainAddressShape), - getBalance: M.callWhen(M.string()).returns(M.any()), - getBalances: M.callWhen().returns(M.any()), + getBalance: M.call(M.string()).returns(VowShape), + getBalances: M.call().returns(VowShape), getLocalAddress: M.call().returns(M.string()), getRemoteAddress: M.call().returns(M.string()), getPort: M.call().returns(M.remotable('Port')), - executeTx: M.call(M.arrayOf(M.record())).returns(M.promise()), + executeTx: M.call(M.arrayOf(M.record())).returns(VowShape), executeEncodedTx: M.call(M.arrayOf(Proto3Shape)) .optional(M.record()) - .returns(M.promise()), - close: M.callWhen().returns(M.undefined()), - getPurse: M.callWhen().returns(PurseShape), + .returns(VowShape), + close: M.call().returns(VowShape), + getPurse: M.call().returns(VowShape), }); /** @@ -59,7 +59,7 @@ export const ChainAccountI = M.interface('ChainAccount', { * @param {Zone} zone * @param {VowTools} vowTools */ -export const prepareChainAccountKit = (zone, { watch, when }) => +export const prepareChainAccountKit = (zone, { watch, asVow }) => zone.exoClassKit( 'ChainAccountKit', { @@ -125,7 +125,9 @@ export const prepareChainAccountKit = (zone, { watch, when }) => return this.state.port; }, executeTx() { - throw new Error('not yet implemented'); + return asVow(() => { + throw new Error('not yet implemented'); + }); }, /** * Submit a transaction on behalf of the remote account for execution on @@ -133,41 +135,50 @@ export const prepareChainAccountKit = (zone, { watch, when }) => * * @param {AnyJson[]} msgs * @param {Omit} [opts] - * @returns {Promise} - base64 encoded bytes string. Can be - * decoded using the corresponding `Msg*Response` object. + * @returns {Vow} - base64 encoded bytes string. Can be decoded + * using the corresponding `Msg*Response` object. * @throws {Error} if packet fails to send or an error is returned */ - async executeEncodedTx(msgs, opts) { - const { connection } = this.state; - // TODO #9281 do not throw synchronously when returning a promise; return a rejected Vow - /// see https://github.com/Agoric/agoric-sdk/pull/9454#discussion_r1626898694 - if (!connection) throw Fail`connection not available`; - return when( - watch( + executeEncodedTx(msgs, opts) { + return asVow(() => { + const { connection } = this.state; + // TODO #9281 do not throw synchronously when returning a promise; return a rejected Vow + /// see https://github.com/Agoric/agoric-sdk/pull/9454#discussion_r1626898694 + if (!connection) throw Fail`connection not available`; + return watch( E(connection).send(makeTxPacket(msgs, opts)), this.facets.parseTxPacketWatcher, - ), - ); + ); + }); }, - /** Close the remote account */ - async close() { - /// TODO #9192 what should the behavior be here? and `onClose`? - // - retrieve assets? - // - revoke the port? - const { connection } = this.state; - // TODO #9281 do not throw synchronously when returning a promise; return a rejected Vow - /// see https://github.com/Agoric/agoric-sdk/pull/9454#discussion_r1626898694 - if (!connection) throw Fail`connection not available`; - return when(watch(E(connection).close())); + /** + * Close the remote account + * + * @returns {Vow} + * @throws {Error} if connection is not available or already closed + */ + close() { + return asVow(() => { + /// TODO #9192 what should the behavior be here? and `onClose`? + // - retrieve assets? + // - revoke the port? + const { connection } = this.state; + // TODO #9281 do not throw synchronously when returning a promise; return a rejected Vow + /// see https://github.com/Agoric/agoric-sdk/pull/9454#discussion_r1626898694 + if (!connection) throw Fail`connection not available`; + return watch(E(connection).close()); + }); }, /** * get Purse for a brand to .withdraw() a Payment from the account * * @param {Brand} brand */ - async getPurse(brand) { + getPurse(brand) { console.log('getPurse got', brand); - throw new Error('not yet implemented'); + return asVow(() => { + throw new Error('not yet implemented'); + }); }, }, connectionHandler: { @@ -175,8 +186,9 @@ export const prepareChainAccountKit = (zone, { watch, when }) => * @param {Remote} connection * @param {LocalIbcAddress} localAddr * @param {RemoteIbcAddress} remoteAddr + * @returns {PromiseVow} */ - async onOpen(connection, localAddr, remoteAddr) { + onOpen(connection, localAddr, remoteAddr) { trace(`ICA Channel Opened for ${localAddr} at ${remoteAddr}`); this.state.connection = connection; this.state.remoteAddress = remoteAddr; @@ -188,15 +200,27 @@ export const prepareChainAccountKit = (zone, { watch, when }) => chainId: this.state.chainId, addressEncoding: 'bech32', }); + return Promise.resolve(watch(undefined)); }, - async onClose(_connection, reason) { + /** + * @param {Remote} _connection + * @param {unknown} reason + * @returns {PromiseVow} + */ + onClose(_connection, reason) { trace(`ICA Channel closed. Reason: ${reason}`); // FIXME handle connection closing https://github.com/Agoric/agoric-sdk/issues/9192 // XXX is there a scenario where a connection will unexpectedly close? _I think yes_ + return Promise.resolve(watch(undefined)); }, - async onReceive(connection, bytes) { + /** + * @param {Remote} connection + * @param bytes + * @returns {PromiseVow} + */ + onReceive(connection, bytes) { trace(`ICA Channel onReceive`, connection, bytes); - return ''; + return Promise.resolve(watch('')); }, }, }, diff --git a/packages/orchestration/test/service.test.ts b/packages/orchestration/test/service.test.ts index b8ac167eeaf..b2829f146f2 100644 --- a/packages/orchestration/test/service.test.ts +++ b/packages/orchestration/test/service.test.ts @@ -5,6 +5,7 @@ import { QueryBalanceRequest } from '@agoric/cosmic-proto/cosmos/bank/v1beta1/qu 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 { commonSetup } from './supports.js'; import { ChainAddressShape } from '../src/typeGuards.js'; @@ -45,14 +46,16 @@ test('makeICQConnection returns an ICQConnection', async t => { t.is(localAddr, localAddr2, 'provideICQConnection is idempotent'); await t.throwsAsync( - E(icqConnection).query([ - toRequestQueryJson( - QueryBalanceRequest.toProtoMsg({ - address: 'cosmos1test', - denom: 'uatom', - }), - ), - ]), + heapVowTools.when( + E(icqConnection).query([ + toRequestQueryJson( + QueryBalanceRequest.toProtoMsg({ + address: 'cosmos1test', + denom: 'uatom', + }), + ), + ]), + ), { message: /"data":"(.*)"memo":""/ }, 'TODO do not use echo connection', ); @@ -114,14 +117,14 @@ test('makeAccount returns a ChainAccount', async t => { }), ); await t.throwsAsync( - E(account).executeEncodedTx([delegateMsg]), + heapVowTools.when(E(account).executeEncodedTx([delegateMsg])), { message: /"type":1(.*)"data":"(.*)"memo":""/ }, 'TODO do not use echo connection', ); await E(account).close(); await t.throwsAsync( - E(account).executeEncodedTx([delegateMsg]), + heapVowTools.when(E(account).executeEncodedTx([delegateMsg])), { message: 'Connection closed', }, From 465b415345ba81f201aee8524f4fac91ba722acc Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Tue, 25 Jun 2024 14:36:56 -0400 Subject: [PATCH 2/4] refactor(ConnectionHandler): remove unused, optional onReceive handler - ICA and ICQ channels only send outbound requests; they do not receive incoming requests they need to respond to - renames ConnectionHandlerI to OutboundConnectionHandlerI to better reflect the behavior --- .../src/exos/chain-account-kit.js | 13 ++----------- .../src/exos/icq-connection-kit.js | 8 ++------ packages/orchestration/src/typeGuards.js | 19 ++++++++++++++----- 3 files changed, 18 insertions(+), 22 deletions(-) diff --git a/packages/orchestration/src/exos/chain-account-kit.js b/packages/orchestration/src/exos/chain-account-kit.js index a525b75a172..4219feecbf8 100644 --- a/packages/orchestration/src/exos/chain-account-kit.js +++ b/packages/orchestration/src/exos/chain-account-kit.js @@ -6,7 +6,7 @@ import { E } from '@endo/far'; import { M } from '@endo/patterns'; import { ChainAddressShape, - ConnectionHandlerI, + OutboundConnectionHandlerI, Proto3Shape, } from '../typeGuards.js'; import { findAddressField } from '../utils/address.js'; @@ -64,7 +64,7 @@ export const prepareChainAccountKit = (zone, { watch, asVow }) => 'ChainAccountKit', { account: ChainAccountI, - connectionHandler: ConnectionHandlerI, + connectionHandler: OutboundConnectionHandlerI, parseTxPacketWatcher: M.interface('ParseTxPacketWatcher', { onFulfilled: M.call(M.string()) .optional(M.arrayOf(M.undefined())) // does not need watcherContext @@ -213,15 +213,6 @@ export const prepareChainAccountKit = (zone, { watch, asVow }) => // XXX is there a scenario where a connection will unexpectedly close? _I think yes_ return Promise.resolve(watch(undefined)); }, - /** - * @param {Remote} connection - * @param bytes - * @returns {PromiseVow} - */ - onReceive(connection, bytes) { - trace(`ICA Channel onReceive`, connection, bytes); - return Promise.resolve(watch('')); - }, }, }, ); diff --git a/packages/orchestration/src/exos/icq-connection-kit.js b/packages/orchestration/src/exos/icq-connection-kit.js index e54d4599652..a327540b0f4 100644 --- a/packages/orchestration/src/exos/icq-connection-kit.js +++ b/packages/orchestration/src/exos/icq-connection-kit.js @@ -4,7 +4,7 @@ import { makeTracer } from '@agoric/internal'; import { E } from '@endo/far'; import { M } from '@endo/patterns'; import { makeQueryPacket, parseQueryPacket } from '../utils/packet.js'; -import { ConnectionHandlerI } from '../typeGuards.js'; +import { OutboundConnectionHandlerI } from '../typeGuards.js'; /** * @import {Zone} from '@agoric/base-zone'; @@ -59,7 +59,7 @@ export const prepareICQConnectionKit = (zone, { watch, when }) => 'ICQConnectionKit', { connection: ICQConnectionI, - connectionHandler: ConnectionHandlerI, + connectionHandler: OutboundConnectionHandlerI, parseQueryPacketWatcher: M.interface('ParseQueryPacketWatcher', { onFulfilled: M.call(M.string()) .optional(M.arrayOf(M.undefined())) // does not need watcherContext @@ -127,10 +127,6 @@ export const prepareICQConnectionKit = (zone, { watch, when }) => async onClose(_connection, reason) { trace(`ICQ Channel closed. Reason: ${reason}`); }, - async onReceive(connection, bytes) { - trace(`ICQ Channel onReceive`, connection, bytes); - return ''; - }, }, }, ); diff --git a/packages/orchestration/src/typeGuards.js b/packages/orchestration/src/typeGuards.js index 13f59166071..677d71b77a7 100644 --- a/packages/orchestration/src/typeGuards.js +++ b/packages/orchestration/src/typeGuards.js @@ -2,11 +2,20 @@ import { AmountShape } from '@agoric/ertp'; import { VowShape } from '@agoric/vow'; import { M } from '@endo/patterns'; -export const ConnectionHandlerI = M.interface('ConnectionHandler', { - onOpen: M.callWhen(M.any(), M.string(), M.string(), M.any()).returns(M.any()), - onClose: M.callWhen(M.any(), M.any(), M.any()).returns(M.any()), - onReceive: M.callWhen(M.any(), M.string()).returns(M.any()), -}); +/** + * Used for IBC Channel Connections that only send outgoing packets. If your + * channel expects incoming packets, please extend this interface to include + * `onReceive`. + */ +export const OutboundConnectionHandlerI = M.interface( + 'OutboundConnectionHandler', + { + onOpen: M.callWhen(M.any(), M.string(), M.string(), M.any()).returns( + M.any(), + ), + onClose: M.callWhen(M.any(), M.any(), M.any()).returns(M.any()), + }, +); export const ChainAddressShape = { address: M.string(), From f409a8dd899cd0eb8c24ba2dba12724dafaae03c Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Tue, 25 Jun 2024 15:29:18 -0400 Subject: [PATCH 3/4] feat(lint): exempt connectionHandler notification methods from resumable rule - onOpen and onClose are event listeners that handle incoming notifications - these methods do not return values and are not expected to be resumable --- .eslintrc.cjs | 4 ++- .../src/exos/chain-account-kit.js | 28 ++++++------------- packages/orchestration/src/typeGuards.js | 6 ++-- packages/vow/src/vow-utils.js | 4 +-- 4 files changed, 16 insertions(+), 26 deletions(-) diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 5b12991ba4a..8d8b6c3ad30 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -32,7 +32,9 @@ const deprecatedTerminology = Object.fromEntries( */ const resumable = [ { - selector: 'FunctionExpression[async=true]', + // all async function expressions, except `onOpen` and `onClose` when they are properties of `connectionHandler` + selector: + 'FunctionExpression[async=true]:not(Property[key.name="connectionHandler"] > ObjectExpression > Property[key.name=/^(onOpen|onClose)$/] > FunctionExpression[async=true])', message: 'Non-immediate functions must return vows, not promises', }, { diff --git a/packages/orchestration/src/exos/chain-account-kit.js b/packages/orchestration/src/exos/chain-account-kit.js index 4219feecbf8..d39adb451b8 100644 --- a/packages/orchestration/src/exos/chain-account-kit.js +++ b/packages/orchestration/src/exos/chain-account-kit.js @@ -15,7 +15,7 @@ import { makeTxPacket, parseTxPacket } from '../utils/packet.js'; /** * @import {Zone} from '@agoric/base-zone'; * @import {Connection, Port} from '@agoric/network'; - * @import {PromiseVow, Remote, Vow, VowTools} from '@agoric/vow'; + * @import {Remote, Vow, VowTools} from '@agoric/vow'; * @import {AnyJson} from '@agoric/cosmic-proto'; * @import {TxBody} from '@agoric/cosmic-proto/cosmos/tx/v1beta1/tx.js'; * @import {LocalIbcAddress, RemoteIbcAddress} from '@agoric/vats/tools/ibc-utils.js'; @@ -103,11 +103,11 @@ export const prepareChainAccountKit = (zone, { watch, asVow }) => }, getBalance(_denom) { // UNTIL https://github.com/Agoric/agoric-sdk/issues/9326 - throw new Error('not yet implemented'); + return asVow(() => Fail`'not yet implemented'`); }, getBalances() { // UNTIL https://github.com/Agoric/agoric-sdk/issues/9326 - throw new Error('not yet implemented'); + return asVow(() => Fail`'not yet implemented'`); }, getLocalAddress() { return NonNullish( @@ -125,9 +125,7 @@ export const prepareChainAccountKit = (zone, { watch, asVow }) => return this.state.port; }, executeTx() { - return asVow(() => { - throw new Error('not yet implemented'); - }); + return asVow(() => Fail`'not yet implemented'`); }, /** * Submit a transaction on behalf of the remote account for execution on @@ -142,8 +140,6 @@ export const prepareChainAccountKit = (zone, { watch, asVow }) => executeEncodedTx(msgs, opts) { return asVow(() => { const { connection } = this.state; - // TODO #9281 do not throw synchronously when returning a promise; return a rejected Vow - /// see https://github.com/Agoric/agoric-sdk/pull/9454#discussion_r1626898694 if (!connection) throw Fail`connection not available`; return watch( E(connection).send(makeTxPacket(msgs, opts)), @@ -163,10 +159,8 @@ export const prepareChainAccountKit = (zone, { watch, asVow }) => // - retrieve assets? // - revoke the port? const { connection } = this.state; - // TODO #9281 do not throw synchronously when returning a promise; return a rejected Vow - /// see https://github.com/Agoric/agoric-sdk/pull/9454#discussion_r1626898694 if (!connection) throw Fail`connection not available`; - return watch(E(connection).close()); + return E(connection).close(); }); }, /** @@ -176,9 +170,7 @@ export const prepareChainAccountKit = (zone, { watch, asVow }) => */ getPurse(brand) { console.log('getPurse got', brand); - return asVow(() => { - throw new Error('not yet implemented'); - }); + return asVow(() => Fail`'not yet implemented'`); }, }, connectionHandler: { @@ -186,9 +178,8 @@ export const prepareChainAccountKit = (zone, { watch, asVow }) => * @param {Remote} connection * @param {LocalIbcAddress} localAddr * @param {RemoteIbcAddress} remoteAddr - * @returns {PromiseVow} */ - onOpen(connection, localAddr, remoteAddr) { + async onOpen(connection, localAddr, remoteAddr) { trace(`ICA Channel Opened for ${localAddr} at ${remoteAddr}`); this.state.connection = connection; this.state.remoteAddress = remoteAddr; @@ -200,18 +191,15 @@ export const prepareChainAccountKit = (zone, { watch, asVow }) => chainId: this.state.chainId, addressEncoding: 'bech32', }); - return Promise.resolve(watch(undefined)); }, /** * @param {Remote} _connection * @param {unknown} reason - * @returns {PromiseVow} */ - onClose(_connection, reason) { + async onClose(_connection, reason) { trace(`ICA Channel closed. Reason: ${reason}`); // FIXME handle connection closing https://github.com/Agoric/agoric-sdk/issues/9192 // XXX is there a scenario where a connection will unexpectedly close? _I think yes_ - return Promise.resolve(watch(undefined)); }, }, }, diff --git a/packages/orchestration/src/typeGuards.js b/packages/orchestration/src/typeGuards.js index 677d71b77a7..e01063c7bb6 100644 --- a/packages/orchestration/src/typeGuards.js +++ b/packages/orchestration/src/typeGuards.js @@ -3,9 +3,9 @@ import { VowShape } from '@agoric/vow'; import { M } from '@endo/patterns'; /** - * Used for IBC Channel Connections that only send outgoing packets. If your - * channel expects incoming packets, please extend this interface to include - * `onReceive`. + * Used for IBC Channel Connections that only send outgoing transactions. If + * your channel expects incoming transactions, please extend this interface to + * include the `onReceive` handler. */ export const OutboundConnectionHandlerI = M.interface( 'OutboundConnectionHandler', diff --git a/packages/vow/src/vow-utils.js b/packages/vow/src/vow-utils.js index 2da8ccda1c5..feb2cd0e815 100644 --- a/packages/vow/src/vow-utils.js +++ b/packages/vow/src/vow-utils.js @@ -5,7 +5,7 @@ import { M, matches } from '@endo/patterns'; /** * @import {PassableCap} from '@endo/pass-style'; - * @import {VowPayload, Vow} from './types.js'; + * @import {VowPayload, Vow, PromiseVow} from './types.js'; * @import {MakeVowKit} from './vow.js'; */ @@ -81,7 +81,7 @@ export const makeAsVow = makeVowKit => { * Helper function that coerces the result of a function to a Vow. Helpful * for scenarios like a synchronously thrown error. * @template {any} T - * @param {(...args: any[]) => Vow> | Awaited} fn + * @param {(...args: any[]) => Vow> | Awaited | PromiseVow} fn * @returns {Vow>} */ const asVow = fn => { From 9bc7afb011c5e0c1bfcf52d98fd2cda620ba23b2 Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Tue, 25 Jun 2024 19:28:16 -0400 Subject: [PATCH 4/4] fix(orchestrate): makeStateRecord type annotation error TS2741: Property 'account' is missing in type '{}' but required in type '{ account: OrchestrationAccount | undefined; }' --- .../orchestration/src/examples/sendAnywhere.contract.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/orchestration/src/examples/sendAnywhere.contract.js b/packages/orchestration/src/examples/sendAnywhere.contract.js index 0bd32082285..1183553bea6 100644 --- a/packages/orchestration/src/examples/sendAnywhere.contract.js +++ b/packages/orchestration/src/examples/sendAnywhere.contract.js @@ -108,14 +108,16 @@ export const start = async (zcf, privateArgs, baggage) => { vowTools, }); - /** @type {{ account: OrchestrationAccount | undefined }} */ - const contractState = makeStateRecord({ account: undefined }); + const contractState = makeStateRecord( + /** @type {{ account: OrchestrationAccount | undefined }} */ { + account: undefined, + }, + ); /** @type {OfferHandler} */ const sendIt = orchestrate( 'sendIt', { zcf, agoricNamesTools, contractState }, - // eslint-disable-next-line no-shadow -- this `zcf` is enclosed in a membrane sendItFn, );