diff --git a/packages/boot/test/bootstrapTests/test-vat-orchestration.ts b/packages/boot/test/bootstrapTests/test-vat-orchestration.ts index 4f1646770ab..f7a91ab8256 100644 --- a/packages/boot/test/bootstrapTests/test-vat-orchestration.ts +++ b/packages/boot/test/bootstrapTests/test-vat-orchestration.ts @@ -150,7 +150,6 @@ test('ICA connection can send msg with proto3', async t => { const txWithOptions = await EV(account).executeEncodedTx( [delegateMsgSuccess], - // @ts-expect-error XXX TxBody interface { memo: 'TESTING', timeoutHeight: 1_000_000_000n, diff --git a/packages/orchestration/src/exos/chainAccountKit.js b/packages/orchestration/src/exos/chainAccountKit.js new file mode 100644 index 00000000000..1013b21f769 --- /dev/null +++ b/packages/orchestration/src/exos/chainAccountKit.js @@ -0,0 +1,200 @@ +// @ts-check +/** @file Orchestration service */ +import { NonNullish } from '@agoric/assert'; +import { makeTracer } from '@agoric/internal'; + +// XXX ambient types runtime imports until https://github.com/Agoric/agoric-sdk/issues/6512 +import '@agoric/network/exported.js'; + +import { V as E } from '@agoric/vat-data/vow.js'; +import { M } from '@endo/patterns'; +import { PaymentShape, PurseShape } from '@agoric/ertp'; +import { InvitationShape } from '@agoric/zoe/src/typeGuards.js'; +import { parseAddress } from '../utils/address.js'; +import { makeTxPacket, parsePacketAck } from '../utils/tx.js'; + +/** + * @import { Connection, Port } from '@agoric/network'; + * @import { Remote } from '@agoric/vow'; + * @import { Zone } from '@agoric/base-zone'; + * @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'; + * @import { ChainAddress } from '../types.js'; + */ + +const { Fail } = assert; +const trace = makeTracer('ChainAccount'); + +export const Proto3Shape = { + typeUrl: M.string(), + value: M.string(), +}; + +export const ChainAddressShape = { + address: M.string(), + chainId: M.string(), + addressEncoding: M.string(), +}; + +/** @typedef {'UNPARSABLE_CHAIN_ADDRESS'} UnparsableChainAddress */ +const UNPARSABLE_CHAIN_ADDRESS = 'UNPARSABLE_CHAIN_ADDRESS'; + +export const ChainAccountI = M.interface('ChainAccount', { + getAddress: M.call().returns(ChainAddressShape), + 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()), + executeEncodedTx: M.call(M.arrayOf(Proto3Shape)) + .optional(M.record()) + .returns(M.promise()), + close: M.callWhen().returns(M.undefined()), + deposit: M.callWhen(PaymentShape).returns(M.undefined()), + getPurse: M.callWhen().returns(PurseShape), + prepareTransfer: M.callWhen().returns(InvitationShape), +}); + +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()), +}); + +/** @param {Zone} zone */ +export const prepareChainAccountKit = zone => + zone.exoClassKit( + 'ChainAccount', + { account: ChainAccountI, connectionHandler: ConnectionHandlerI }, + /** + * @param {Port} port + * @param {string} requestedRemoteAddress + */ + (port, requestedRemoteAddress) => + /** + * @type {{ + * port: Port; + * connection: Remote | undefined; + * localAddress: LocalIbcAddress | undefined; + * requestedRemoteAddress: string; + * remoteAddress: RemoteIbcAddress | undefined; + * chainAddress: ChainAddress | undefined; + * }} + */ ( + harden({ + port, + connection: undefined, + requestedRemoteAddress, + remoteAddress: undefined, + chainAddress: undefined, + localAddress: undefined, + }) + ), + { + account: { + /** + * @returns {ChainAddress} + */ + getAddress() { + return NonNullish( + this.state.chainAddress, + 'ICA channel creation acknowledgement not yet received.', + ); + }, + getLocalAddress() { + return NonNullish( + this.state.localAddress, + 'local address not available', + ); + }, + getRemoteAddress() { + return NonNullish( + this.state.remoteAddress, + 'remote address not available', + ); + }, + getPort() { + return this.state.port; + }, + executeTx() { + throw new Error('not yet implemented'); + }, + /** + * Submit a transaction on behalf of the remote account for execution on the remote chain. + * @param {AnyJson[]} msgs + * @param {Omit} [opts] + * @returns {Promise} - 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 + */ + executeEncodedTx(msgs, opts) { + const { connection } = this.state; + if (!connection) throw Fail`connection not available`; + return E.when( + E(connection).send(makeTxPacket(msgs, opts)), + // if parsePacketAck cannot find a `result` key, it throws + ack => parsePacketAck(ack), + ); + }, + /** + * Close the remote account + */ + async close() { + /// XXX what should the behavior be here? and `onClose`? + // - retrieve assets? + // - revoke the port? + const { connection } = this.state; + if (!connection) throw Fail`connection not available`; + await E(connection).close(); + }, + async deposit(payment) { + console.log('deposit got', payment); + throw new Error('not yet implemented'); + }, + /** + * get Purse for a brand to .withdraw() a Payment from the account + * @param {Brand} brand + */ + async getPurse(brand) { + console.log('getPurse got', brand); + throw new Error('not yet implemented'); + }, + + /* transfer account to new holder */ + async prepareTransfer() { + throw new Error('not yet implemented'); + }, + }, + connectionHandler: { + /** + * @param {Remote} connection + * @param {LocalIbcAddress} localAddr + * @param {RemoteIbcAddress} remoteAddr + */ + async onOpen(connection, localAddr, remoteAddr) { + trace(`ICA Channel Opened for ${localAddr} at ${remoteAddr}`); + this.state.connection = connection; + this.state.remoteAddress = remoteAddr; + this.state.localAddress = localAddr; + // XXX parseAddress currently throws, should it return '' instead? + this.state.chainAddress = harden({ + address: parseAddress(remoteAddr) || UNPARSABLE_CHAIN_ADDRESS, + // TODO get this from `Chain` object #9063 + // XXX how do we get a chainId for an unknown chain? seems it may need to be a user supplied arg + chainId: 'FIXME', + addressEncoding: 'bech32', + }); + }, + async onClose(_connection, reason) { + trace(`ICA Channel closed. Reason: ${reason}`); + // XXX handle connection closing + // 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 ''; + }, + }, + }, + ); + +/** @typedef {ReturnType>} ChainAccountKit */ diff --git a/packages/orchestration/src/service.js b/packages/orchestration/src/service.js index b68d00a4bd9..58baaa3a63a 100644 --- a/packages/orchestration/src/service.js +++ b/packages/orchestration/src/service.js @@ -1,32 +1,22 @@ // @ts-check /** @file Orchestration service */ -import { NonNullish } from '@agoric/assert'; -import { makeTracer } from '@agoric/internal'; // XXX ambient types runtime imports until https://github.com/Agoric/agoric-sdk/issues/6512 import '@agoric/network/exported.js'; import { V as E } from '@agoric/vat-data/vow.js'; import { M } from '@endo/patterns'; -import { PaymentShape, PurseShape } from '@agoric/ertp'; -import { InvitationShape } from '@agoric/zoe/src/typeGuards.js'; -import { makeICAConnectionAddress, parseAddress } from './utils/address.js'; -import { makeTxPacket, parsePacketAck } from './utils/tx.js'; +import { prepareChainAccountKit } from './exos/chainAccountKit.js'; +import { makeICAConnectionAddress } from './utils/address.js'; /** - * @import {Connection, Port, PortAllocator} from '@agoric/network'; - * @import {Remote} from '@agoric/vow'; + * @import { PortAllocator} from '@agoric/network'; * @import { IBCConnectionID } from '@agoric/vats'; * @import { Zone } from '@agoric/base-zone'; - * @import { TxBody } from '@agoric/cosmic-proto/cosmos/tx/v1beta1/tx.js'; - * @import { ChainAccount, ChainAddress } from './types.js'; - * @import { LocalIbcAddress, RemoteIbcAddress } from '@agoric/vats/tools/ibc-utils.js'; + * @import { ChainAccount } from './types.js'; */ const { Fail, bare } = assert; -const trace = makeTracer('Orchestration'); - -/** @import {AnyJson} from '@agoric/cosmic-proto'; */ /** * @typedef {object} OrchestrationPowers @@ -54,179 +44,6 @@ const getPower = (powers, name) => { return /** @type {OrchestrationPowers[K]} */ (powers.get(name)); }; -export const Proto3Shape = { - typeUrl: M.string(), - value: M.string(), -}; - -export const ChainAddressShape = { - address: M.string(), - chainId: M.string(), - addressEncoding: M.string(), -}; - -/** @typedef {'UNPARSABLE_CHAIN_ADDRESS'} UnparsableChainAddress */ -const UNPARSABLE_CHAIN_ADDRESS = 'UNPARSABLE_CHAIN_ADDRESS'; - -export const ChainAccountI = M.interface('ChainAccount', { - getAddress: M.call().returns(ChainAddressShape), - 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()), - executeEncodedTx: M.call(M.arrayOf(Proto3Shape)) - .optional(M.record()) - .returns(M.promise()), - close: M.callWhen().returns(M.undefined()), - deposit: M.callWhen(PaymentShape).returns(M.undefined()), - getPurse: M.callWhen().returns(PurseShape), - prepareTransfer: M.callWhen().returns(InvitationShape), -}); - -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()), -}); - -/** @param {Zone} zone */ -const prepareChainAccount = zone => - zone.exoClassKit( - 'ChainAccount', - { account: ChainAccountI, connectionHandler: ConnectionHandlerI }, - /** - * @param {Port} port - * @param {string} requestedRemoteAddress - */ - (port, requestedRemoteAddress) => - /** - * @type {{ - * port: Port; - * connection: Remote | undefined; - * localAddress: LocalIbcAddress | undefined; - * requestedRemoteAddress: string; - * remoteAddress: RemoteIbcAddress | undefined; - * chainAddress: ChainAddress | undefined; - * }} - */ ( - harden({ - port, - connection: undefined, - requestedRemoteAddress, - remoteAddress: undefined, - chainAddress: undefined, - localAddress: undefined, - }) - ), - { - account: { - /** - * @returns {ChainAddress} - */ - getAddress() { - return NonNullish( - this.state.chainAddress, - 'ICA channel creation acknowledgement not yet received.', - ); - }, - getLocalAddress() { - return NonNullish( - this.state.localAddress, - 'local address not available', - ); - }, - getRemoteAddress() { - return NonNullish( - this.state.remoteAddress, - 'remote address not available', - ); - }, - getPort() { - return this.state.port; - }, - executeTx() { - throw new Error('not yet implemented'); - }, - /** - * Submit a transaction on behalf of the remote account for execution on the remote chain. - * @param {AnyJson[]} msgs - * @param {Omit} [opts] - * @returns {Promise} - 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 - */ - executeEncodedTx(msgs, opts) { - const { connection } = this.state; - if (!connection) throw Fail`connection not available`; - return E.when( - E(connection).send(makeTxPacket(msgs, opts)), - // if parsePacketAck cannot find a `result` key, it throws - ack => parsePacketAck(ack), - ); - }, - /** - * Close the remote account - */ - async close() { - /// XXX what should the behavior be here? and `onClose`? - // - retrieve assets? - // - revoke the port? - const { connection } = this.state; - if (!connection) throw Fail`connection not available`; - await E(connection).close(); - }, - async deposit(payment) { - console.log('deposit got', payment); - throw new Error('not yet implemented'); - }, - /** - * get Purse for a brand to .withdraw() a Payment from the account - * @param {Brand} brand - */ - async getPurse(brand) { - console.log('getPurse got', brand); - throw new Error('not yet implemented'); - }, - - /* transfer account to new holder */ - async prepareTransfer() { - throw new Error('not yet implemented'); - }, - }, - connectionHandler: { - /** - * @param {Remote} connection - * @param {LocalIbcAddress} localAddr - * @param {RemoteIbcAddress} remoteAddr - */ - async onOpen(connection, localAddr, remoteAddr) { - trace(`ICA Channel Opened for ${localAddr} at ${remoteAddr}`); - this.state.connection = connection; - this.state.remoteAddress = remoteAddr; - this.state.localAddress = localAddr; - // XXX parseAddress currently throws, should it return '' instead? - this.state.chainAddress = harden({ - address: parseAddress(remoteAddr) || UNPARSABLE_CHAIN_ADDRESS, - // TODO get this from `Chain` object #9063 - // XXX how do we get a chainId for an unknown chain? seems it may need to be a user supplied arg - chainId: 'FIXME', - addressEncoding: 'bech32', - }); - trace('got chainAddress', this.state.chainAddress); - trace('parseAddress(remoteAddr)', parseAddress(remoteAddr)); - }, - async onClose(_connection, reason) { - trace(`ICA Channel closed. Reason: ${reason}`); - // XXX handle connection closing - // 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 ''; - }, - }, - }, - ); - export const OrchestrationI = M.interface('Orchestration', { makeAccount: M.callWhen(M.string(), M.string()).returns( M.remotable('ChainAccount'), @@ -235,9 +52,9 @@ export const OrchestrationI = M.interface('Orchestration', { /** * @param {Zone} zone - * @param {ReturnType} createChainAccount + * @param {ReturnType} makeChainAccountKit */ -const prepareOrchestration = (zone, createChainAccount) => +const prepareOrchestration = (zone, makeChainAccountKit) => zone.exoClassKit( 'Orchestration', { @@ -279,13 +96,16 @@ const prepareOrchestration = (zone, createChainAccount) => hostConnectionId, controllerConnectionId, ); - const chainAccount = createChainAccount(port, remoteConnAddr); + const chainAccountKit = makeChainAccountKit(port, remoteConnAddr); // await so we do not return a ChainAccount before it successfully instantiates - await E(port).connect(remoteConnAddr, chainAccount.connectionHandler); + await E(port).connect( + remoteConnAddr, + chainAccountKit.connectionHandler, + ); // XXX if we fail, should we close the port (if it was created in this flow)? - return chainAccount.account; + return chainAccountKit.account; }, }, }, @@ -293,14 +113,13 @@ const prepareOrchestration = (zone, createChainAccount) => /** @param {Zone} zone */ export const prepareOrchestrationTools = zone => { - const createChainAccount = prepareChainAccount(zone); - const makeOrchestration = prepareOrchestration(zone, createChainAccount); + const makeChainAccountKit = prepareChainAccountKit(zone); + const makeOrchestration = prepareOrchestration(zone, makeChainAccountKit); return harden({ makeOrchestration }); }; harden(prepareOrchestrationTools); -/** @typedef {ReturnType>} ChainAccountKit */ /** @typedef {ReturnType} OrchestrationTools */ /** @typedef {ReturnType} OrchestrationKit */ /** @typedef {OrchestrationKit['public']} OrchestrationService */