From 39a980a64c899a746f1c5108d98440bdb3374d68 Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Thu, 2 May 2024 10:58:16 -0400 Subject: [PATCH] refactor(chainAccountKit): move to own file --- .../test/bootstrapTests/test-orchestration.ts | 2 +- .../bootstrapTests/test-vat-orchestration.ts | 1 - .../src/examples/stakeAtom.contract.js | 4 +- .../orchestration/src/exos/chainAccountKit.js | 200 +++++++++++++++++ packages/orchestration/src/service.js | 209 ++---------------- packages/orchestration/src/types.d.ts | 14 +- 6 files changed, 226 insertions(+), 204 deletions(-) create mode 100644 packages/orchestration/src/exos/chainAccountKit.js diff --git a/packages/boot/test/bootstrapTests/test-orchestration.ts b/packages/boot/test/bootstrapTests/test-orchestration.ts index 6a9ff4f6f5d..f1ab0b27b20 100644 --- a/packages/boot/test/bootstrapTests/test-orchestration.ts +++ b/packages/boot/test/bootstrapTests/test-orchestration.ts @@ -140,7 +140,7 @@ test.serial('stakeAtom - smart wallet', async t => { invitationSpec: { source: 'agoricContract', instancePath: ['stakeAtom'], - callPipe: [['makeMakeAccountInvitation']], + callPipe: [['makeAcountInvitationMaker']], }, proposal: {}, }); diff --git a/packages/boot/test/bootstrapTests/test-vat-orchestration.ts b/packages/boot/test/bootstrapTests/test-vat-orchestration.ts index fb643b16d0f..f4151cd6e10 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/examples/stakeAtom.contract.js b/packages/orchestration/src/examples/stakeAtom.contract.js index 0b9baaafeae..ca885cb12a0 100644 --- a/packages/orchestration/src/examples/stakeAtom.contract.js +++ b/packages/orchestration/src/examples/stakeAtom.contract.js @@ -70,14 +70,14 @@ export const start = async (zcf, privateArgs, baggage) => { 'StakeAtom', M.interface('StakeAtomI', { makeAccount: M.callWhen().returns(M.remotable('ChainAccount')), - makeMakeAccountInvitation: M.call().returns(M.promise()), + makeAcountInvitationMaker: M.call().returns(M.promise()), }), { async makeAccount() { trace('makeAccount'); return makeAccount().then(({ account }) => account); }, - makeMakeAccountInvitation() { + makeAcountInvitationMaker() { trace('makeCreateAccountInvitation'); return zcf.makeInvitation( async seat => { 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 */ diff --git a/packages/orchestration/src/types.d.ts b/packages/orchestration/src/types.d.ts index 5d33dc6a241..bb37e16ff72 100644 --- a/packages/orchestration/src/types.d.ts +++ b/packages/orchestration/src/types.d.ts @@ -4,7 +4,7 @@ import type { Invitation } from '@agoric/zoe/exported.js'; import type { Any } from '@agoric/cosmic-proto/google/protobuf/any'; import type { AnyJson } from '@agoric/cosmic-proto'; import type { - MsgCancelUnbondingDelegation, + MsgBeginRedelegateResponse, MsgUndelegateResponse, } from '@agoric/cosmic-proto/cosmos/staking/v1beta1/tx.js'; import type { @@ -18,6 +18,7 @@ import type { RemoteIbcAddress, } from '@agoric/vats/tools/ibc-utils.js'; import type { Port } from '@agoric/network'; +import { MsgTransferResponse } from '@agoric/cosmic-proto/ibc/applications/transfer/v1/tx.js'; /** * static declaration of known chain types will allow type support for @@ -367,14 +368,14 @@ export interface BaseOrchestrationAccount { srcValidator: CosmosValidatorAddress, dstValidator: CosmosValidatorAddress, amount: AmountArg, - ) => Promise; + ) => Promise; /** * Undelegate multiple delegations (concurrently). To delegate independently, pass an array with one item. * Resolves when the undelegation is complete and the tokens are no longer bonded. Note it may take weeks. * @param {Delegation[]} delegations - the delegation to undelegate */ - undelegate: (delegations: Delegation[]) => Promise; + undelegate: (delegations: Delegation[]) => Promise; /** * Withdraw rewards from all validators. The promise settles when the rewards are withdrawn. @@ -403,7 +404,7 @@ export interface BaseOrchestrationAccount { amount: AmountArg, destination: ChainAddress, memo?: string, - ) => Promise; + ) => Promise; /** * Transfer an amount to another account in multiple steps. The promise settles when @@ -412,7 +413,10 @@ export interface BaseOrchestrationAccount { * @param msg - the transfer message, including follow-up steps * @returns void */ - transferSteps: (amount: AmountArg, msg: TransferMsg) => Promise; + transferSteps: ( + amount: AmountArg, + msg: TransferMsg, + ) => Promise; /** * deposit payment from zoe to the account. For remote accounts, * an IBC Transfer will be executed to transfer funds there.