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/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, ); diff --git a/packages/orchestration/src/exos/chain-account-kit.js b/packages/orchestration/src/exos/chain-account-kit.js index 4d14e1410eb..d39adb451b8 100644 --- a/packages/orchestration/src/exos/chain-account-kit.js +++ b/packages/orchestration/src/exos/chain-account-kit.js @@ -1,12 +1,12 @@ /** @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 { ChainAddressShape, - ConnectionHandlerI, + OutboundConnectionHandlerI, Proto3Shape, } from '../typeGuards.js'; import { findAddressField } from '../utils/address.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 {Remote, 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'; @@ -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,12 +59,12 @@ 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', { account: ChainAccountI, - connectionHandler: ConnectionHandlerI, + connectionHandler: OutboundConnectionHandlerI, parseTxPacketWatcher: M.interface('ParseTxPacketWatcher', { onFulfilled: M.call(M.string()) .optional(M.arrayOf(M.undefined())) // does not need watcherContext @@ -103,11 +103,11 @@ export const prepareChainAccountKit = (zone, { watch, when }) => }, 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,7 +125,7 @@ export const prepareChainAccountKit = (zone, { watch, when }) => return this.state.port; }, executeTx() { - throw new Error('not yet implemented'); + return asVow(() => Fail`'not yet implemented'`); }, /** * Submit a transaction on behalf of the remote account for execution on @@ -133,41 +133,44 @@ 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; + 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; + if (!connection) throw Fail`connection not available`; + return 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(() => Fail`'not yet implemented'`); }, }, connectionHandler: { @@ -189,15 +192,15 @@ export const prepareChainAccountKit = (zone, { watch, when }) => addressEncoding: 'bech32', }); }, + /** + * @param {Remote} _connection + * @param {unknown} 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_ }, - async onReceive(connection, bytes) { - trace(`ICA Channel onReceive`, connection, bytes); - return ''; - }, }, }, ); 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..e01063c7bb6 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 transactions. If + * your channel expects incoming transactions, please extend this interface to + * include the `onReceive` handler. + */ +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(), 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', }, 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 => {