diff --git a/packages/orchestration/src/examples/stakeIca.contract.js b/packages/orchestration/src/examples/stakeIca.contract.js index af8eeeac585..0f168f052b8 100644 --- a/packages/orchestration/src/examples/stakeIca.contract.js +++ b/packages/orchestration/src/examples/stakeIca.contract.js @@ -119,7 +119,7 @@ export const start = async (zcf, privateArgs, baggage) => { const publicFacet = zone.exo( 'StakeAtom', M.interface('StakeAtomI', { - makeAccount: M.callWhen().returns(M.remotable('ChainAccount')), + makeAccount: M.callWhen().returns(M.remotable('OrchestrationAccountKit')), makeAccountInvitationMaker: M.callWhen().returns(InvitationShape), }), { diff --git a/packages/orchestration/src/exos/README.md b/packages/orchestration/src/exos/README.md index 6e9dbf88a44..902421a4e7a 100644 --- a/packages/orchestration/src/exos/README.md +++ b/packages/orchestration/src/exos/README.md @@ -1,56 +1,123 @@ # Exo structure -As of 2024-05-29… +Last verified 2024-07-31 ```mermaid classDiagram %% Orchestration vat business logic (Zoe) - LCAKit --* LocalchainAccount - ICQConnectionKit --* Port - ICQConnectionKit --* Connection - ChainAccountKit --* Port - ChainAccountKit --* Connection - StakingAccountKit --* IcaAccount - - class ChainAccountKit { + ICQConnection --* Port + ICQConnection --* Connection + IcaAccount --* Port + IcaAccount --* Connection + IcaAccount --* CosmosInterchainService + ICQConnection --* CosmosInterchainService + CosmosInterchainService --* PortAllocator + PortAllocator --* NetworkVat + LocalChainAccount --* LocalChainVat + + class IcaAccount { port: Port connection: Connection localAddress: LocalIbcAddress requestedRemoteAddress: string remoteAddress: RemoteIbcAddress chainAddress: ChainAddress + getAddress() + getLocalAddress() + getRemoteAddress() + getPort() + executeTx() + executeEncodedTx() + close() } - class ICQConnectionKit { + class ICQConnection { port: Port connection: Connection localAddress: LocalIbcAddress remoteAddress: RemoteIbcAddress + getLocalAddress() + getRemoteAddress() + query() } - class StakingAccountKit { - chainAddress: ChainAddress - bondDenom: string - account: ICAAccount - timer: Timer - topicKit: TopicKit - makeTransferInvitation() + + class CosmosInterchainService { + portAllocator: PortAllocator + icqConnections: MapStore + sharedICQPort: Port + makeAccount() + provideICQConnection() } %% In other vats - class LCAKit { - account: LocalChainAccount - address: ChainAddress - topicKit: RecorderKit + class Port { + getLocalAddress() + addListener() + connect() + removeListener() + revoke() } - class LocalchainAccount { - executeTx() - deposit() - withdraw() + + class Connection { + getLocalAddress() + getRemoteAddress() + send() + close() } - class IcaAccount { - executeTx() - deposit() - getPurse() - close() + + class PortAllocator { + allocateCustomIBCPort() + allocateICAControllerPort() + allocateICQControllerPort() + } + + class LocalChainAccount { + deposit() + executeTx() + getBalance() + withdraw() + executeTx() + monitorTransfers() + } + +%% In api consumer vats + + LocalOrchestrationAccount --* LocalChainAccount + CosmosOrchestrationAccount --* IcaAccount + + class LocalOrchestrationAccount { + account: LocalChainAccount + address: ChainAddress + topicKit: RecorderKit + asContinuingOffer() + delegate() + deposit() + executeTx() + getAddress() + getBalance() + getPublicTopics() + monitorTransfers() + send() + transfer() + undelegate() + withdraw() + } + + class CosmosOrchestrationAccount { + account: LocalChainAccount + bondDenom: string + chainAddress: ChainAddress + icqConnection: ICQConnection | undefined + timer: Timer + topicKit: RecorderKit + asContinuingOffer() + delegate() + executeEncodedTx() + getAddress() + getBalance() + getPublicTopics() + redelegate() + undelegate() + withdrawReward() } ``` diff --git a/packages/orchestration/src/exos/cosmos-interchain-service.js b/packages/orchestration/src/exos/cosmos-interchain-service.js index ba56d40e055..ec2bffb9f41 100644 --- a/packages/orchestration/src/exos/cosmos-interchain-service.js +++ b/packages/orchestration/src/exos/cosmos-interchain-service.js @@ -3,7 +3,7 @@ import { E } from '@endo/far'; import { M, mustMatch } from '@endo/patterns'; import { Shape as NetworkShape } from '@agoric/network'; -import { prepareChainAccountKit } from './chain-account-kit.js'; +import { prepareIcaAccountKit } from './ica-account-kit.js'; import { prepareICQConnectionKit } from './icq-connection-kit.js'; import { DEFAULT_ICQ_VERSION, @@ -18,7 +18,7 @@ import { * @import {IBCConnectionID} from '@agoric/vats'; * @import {RemoteIbcAddress} from '@agoric/vats/tools/ibc-utils.js'; * @import {Vow, VowTools} from '@agoric/vow'; - * @import {ICQConnection, IcaAccount, ICQConnectionKit, ChainAccountKit} from '../types.js'; + * @import {ICQConnection, IcaAccount, ICQConnectionKit, IcaAccountKit} from '../types.js'; * @import {ICAChannelAddressOpts} from '../utils/address.js'; */ @@ -32,7 +32,7 @@ const { Vow$ } = NetworkShape; // TODO #9611 /** @typedef {MapStore} ICQConnectionStore */ -/** @typedef {ChainAccountKit | ICQConnectionKit} ConnectionKit */ +/** @typedef {IcaAccountKit | ICQConnectionKit} ConnectionKit */ /** * @typedef {{ @@ -56,13 +56,13 @@ const getICQConnectionKey = (controllerConnectionId, version) => { /** * @param {Zone} zone * @param {VowTools} vowTools - * @param {ReturnType} makeChainAccountKit + * @param {ReturnType} makeIcaAccountKit * @param {ReturnType} makeICQConnectionKit */ const prepareCosmosOrchestrationServiceKit = ( zone, { watch, asVow }, - makeChainAccountKit, + makeIcaAccountKit, makeICQConnectionKit, ) => zone.exoClassKit( @@ -94,7 +94,7 @@ const prepareCosmosOrchestrationServiceKit = ( public: M.interface('CosmosInterchainService', { makeAccount: M.call(M.string(), M.string(), M.string()) .optional(M.record()) - .returns(Vow$(M.remotable('ChainAccountKit'))), + .returns(Vow$(M.remotable('IcaAccountKit'))), provideICQConnection: M.call(M.string()) .optional(M.string()) .returns(Vow$(M.remotable('ICQConnection'))), @@ -121,15 +121,15 @@ const prepareCosmosOrchestrationServiceKit = ( * }} watchContext */ onFulfilled(port, { chainId, remoteConnAddr }) { - const chainAccountKit = makeChainAccountKit( + const connectionKit = makeIcaAccountKit( chainId, port, remoteConnAddr, ); return watch( - E(port).connect(remoteConnAddr, chainAccountKit.connectionHandler), + E(port).connect(remoteConnAddr, connectionKit.connectionHandler), this.facets.channelOpenWatcher, - { returnFacet: 'account', connectionKit: chainAccountKit }, + { returnFacet: 'account', connectionKit }, ); }, }, @@ -252,13 +252,13 @@ const prepareCosmosOrchestrationServiceKit = ( * @param {VowTools} vowTools */ export const prepareCosmosInterchainService = (zone, vowTools) => { - const makeChainAccountKit = prepareChainAccountKit(zone, vowTools); + const makeIcaAccountKit = prepareIcaAccountKit(zone, vowTools); const makeICQConnectionKit = prepareICQConnectionKit(zone, vowTools); const makeCosmosOrchestrationServiceKit = prepareCosmosOrchestrationServiceKit( zone, vowTools, - makeChainAccountKit, + makeIcaAccountKit, makeICQConnectionKit, ); diff --git a/packages/orchestration/src/exos/chain-account-kit.js b/packages/orchestration/src/exos/ica-account-kit.js similarity index 79% rename from packages/orchestration/src/exos/chain-account-kit.js rename to packages/orchestration/src/exos/ica-account-kit.js index 1fce0a19ab8..ac60e2856e5 100644 --- a/packages/orchestration/src/exos/chain-account-kit.js +++ b/packages/orchestration/src/exos/ica-account-kit.js @@ -1,4 +1,4 @@ -/** @file ChainAccount exo */ +/** @file IcaAccount exo */ import { Fail } from '@endo/errors'; import { E } from '@endo/far'; import { M } from '@endo/patterns'; @@ -22,15 +22,13 @@ import { makeTxPacket, parseTxPacket } from '../utils/packet.js'; * @import {ChainAddress} from '../types.js'; */ -const trace = makeTracer('ChainAccountKit'); +const trace = makeTracer('IcaAccountKit'); /** @typedef {'UNPARSABLE_CHAIN_ADDRESS'} UnparsableChainAddress */ const UNPARSABLE_CHAIN_ADDRESS = 'UNPARSABLE_CHAIN_ADDRESS'; -export const ChainAccountI = M.interface('ChainAccount', { +export const IcaAccountI = M.interface('IcaAccount', { getAddress: M.call().returns(ChainAddressShape), - 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')), @@ -39,7 +37,6 @@ export const ChainAccountI = M.interface('ChainAccount', { .optional(M.record()) .returns(VowShape), close: M.call().returns(VowShape), - getPurse: M.call().returns(VowShape), }); /** @@ -58,11 +55,11 @@ export const ChainAccountI = M.interface('ChainAccount', { * @param {Zone} zone * @param {VowTools} vowTools */ -export const prepareChainAccountKit = (zone, { watch, asVow }) => +export const prepareIcaAccountKit = (zone, { watch, asVow }) => zone.exoClassKit( - 'ChainAccountKit', + 'IcaAccountKit', { - account: ChainAccountI, + account: IcaAccountI, connectionHandler: OutboundConnectionHandlerI, parseTxPacketWatcher: M.interface('ParseTxPacketWatcher', { onFulfilled: M.call(M.string()) @@ -100,16 +97,6 @@ export const prepareChainAccountKit = (zone, { watch, asVow }) => 'ICA channel creation acknowledgement not yet received.', ); }, - 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`); - }, getLocalAddress() { return NonNullish( this.state.localAddress, @@ -164,15 +151,6 @@ export const prepareChainAccountKit = (zone, { watch, asVow }) => return E(connection).close(); }); }, - /** - * get Purse for a brand to .withdraw() a Payment from the account - * - * @param {Brand} brand - */ - getPurse(brand) { - console.log('getPurse got', brand); - return asVow(() => Fail`not yet implemented`); - }, }, connectionHandler: { /** @@ -185,10 +163,12 @@ export const prepareChainAccountKit = (zone, { watch, asVow }) => this.state.connection = connection; this.state.remoteAddress = remoteAddr; this.state.localAddress = localAddr; + const address = findAddressField(remoteAddr); + if (!address) { + console.error('⚠️ failed to parse chain address', remoteAddr); + } this.state.chainAddress = harden({ - // FIXME need a fallback value like icacontroller-1-connection-1 if this fails - // https://github.com/Agoric/agoric-sdk/issues/9066 - value: findAddressField(remoteAddr) || UNPARSABLE_CHAIN_ADDRESS, + value: address || UNPARSABLE_CHAIN_ADDRESS, chainId: this.state.chainId, encoding: 'bech32', }); @@ -206,4 +186,4 @@ export const prepareChainAccountKit = (zone, { watch, asVow }) => }, ); -/** @typedef {ReturnType>} ChainAccountKit */ +/** @typedef {ReturnType>} IcaAccountKit */ diff --git a/packages/orchestration/src/exos/local-chain-facade.js b/packages/orchestration/src/exos/local-chain-facade.js index f00eb5e418e..3677024c507 100644 --- a/packages/orchestration/src/exos/local-chain-facade.js +++ b/packages/orchestration/src/exos/local-chain-facade.js @@ -1,4 +1,4 @@ -/** @file ChainAccount exo */ +/** @file Localchain Facade exo */ import { E } from '@endo/far'; // eslint-disable-next-line no-restricted-syntax -- just the import import { heapVowE } from '@agoric/vow/vat.js'; diff --git a/packages/orchestration/src/exos/orchestrator.js b/packages/orchestration/src/exos/orchestrator.js index 10ed1daf665..6aad87ef1ea 100644 --- a/packages/orchestration/src/exos/orchestrator.js +++ b/packages/orchestration/src/exos/orchestrator.js @@ -1,4 +1,4 @@ -/** @file ChainAccount exo */ +/** @file Orchestrator exo */ import { AmountShape } from '@agoric/ertp'; import { pickFacet } from '@agoric/vat-data'; import { makeTracer } from '@agoric/internal'; diff --git a/packages/orchestration/src/exos/remote-chain-facade.js b/packages/orchestration/src/exos/remote-chain-facade.js index e33f14b6ae2..c68abb389e4 100644 --- a/packages/orchestration/src/exos/remote-chain-facade.js +++ b/packages/orchestration/src/exos/remote-chain-facade.js @@ -1,4 +1,4 @@ -/** @file ChainAccount exo */ +/** @file Remote Chain Facade exo */ import { makeTracer } from '@agoric/internal'; import { E } from '@endo/far'; import { M } from '@endo/patterns'; diff --git a/packages/orchestration/src/types.ts b/packages/orchestration/src/types.ts index 9297dee237e..b86d499442f 100644 --- a/packages/orchestration/src/types.ts +++ b/packages/orchestration/src/types.ts @@ -3,7 +3,7 @@ export type * from './chain-info.js'; export type * from './cosmos-api.js'; export type * from './ethereum-api.js'; -export type * from './exos/chain-account-kit.js'; +export type * from './exos/ica-account-kit.js'; export type * from './exos/icq-connection-kit.js'; export type * from './orchestration-api.js'; export type * from './exos/cosmos-interchain-service.js'; diff --git a/packages/orchestration/src/utils/address.js b/packages/orchestration/src/utils/address.js index f3a857c8aa5..1735040c401 100644 --- a/packages/orchestration/src/utils/address.js +++ b/packages/orchestration/src/utils/address.js @@ -77,7 +77,8 @@ export const findAddressField = remoteAddressString => { // Extract JSON version string assuming it's always surrounded by {} const jsonStr = remoteAddressString?.match(/{.*?}/)?.[0]; const jsonObj = jsonStr ? JSON.parse(jsonStr) : undefined; - return jsonObj?.address ?? undefined; + if (!jsonObj?.address?.length) return undefined; + return jsonObj.address; } catch (error) { return undefined; } diff --git a/packages/orchestration/test/service.test.ts b/packages/orchestration/test/service.test.ts index 1244a680897..ab73095146c 100644 --- a/packages/orchestration/test/service.test.ts +++ b/packages/orchestration/test/service.test.ts @@ -13,6 +13,8 @@ import { matches } from '@endo/patterns'; import { heapVowE as E } from '@agoric/vow/vat.js'; import { decodeBase64 } from '@endo/base64'; import type { LocalIbcAddress } from '@agoric/vats/tools/ibc-utils.js'; +import { getMethodNames } from '@agoric/internal'; +import { Port } from '@agoric/network'; import { commonSetup } from './supports.js'; import { ChainAddressShape } from '../src/typeGuards.js'; import { tryDecodeResponse } from '../src/utils/cosmos.js'; @@ -97,9 +99,16 @@ test('makeICQConnection returns an ICQConnection', async t => { ); t.regex([...uniquePortIds][0], /icqcontroller-\d+/); t.is(uniquePortIds.size, 1, 'all connections share same port'); + + await t.throwsAsync( + // @ts-expect-error icqConnectionKit doesn't have a port method + () => E(icqConnection).getPort(), + undefined, + 'ICQConnection Kit does not expose its port', + ); }); -test('makeAccount returns a ChainAccount', async t => { +test('makeAccount returns an IcaAccountKit', async t => { const { bootstrap: { cosmosInterchainService }, } = await commonSetup(t); @@ -109,10 +118,11 @@ test('makeAccount returns a ChainAccount', async t => { HOST_CONNECTION_ID, CONTROLLER_CONNECTION_ID, ); - const [localAddr, remoteAddr, chainAddr] = await Promise.all([ + const [localAddr, remoteAddr, chainAddr, port] = await Promise.all([ E(account).getLocalAddress(), E(account).getRemoteAddress(), E(account).getAddress(), + E(account).getPort(), ]); t.log(account, { localAddr, @@ -135,6 +145,12 @@ test('makeAccount returns a ChainAccount', async t => { /"version":"ics27-1"(.*)"encoding":"proto3"/, 'remote address contains version and encoding in version string', ); + t.true( + ( + ['addListener', 'removeListener', 'connect', 'revoke'] as (keyof Port)[] + ).every(method => getMethodNames(port).includes(method)), + 'IcaAccountKit returns a Port remotable', + ); t.true(matches(chainAddr, ChainAddressShape)); t.regex(chainAddr.value, /cosmos1test/); diff --git a/packages/orchestration/test/utils/address.test.ts b/packages/orchestration/test/utils/address.test.ts index e19a7c2cf13..9012edc9da3 100644 --- a/packages/orchestration/test/utils/address.test.ts +++ b/packages/orchestration/test/utils/address.test.ts @@ -54,8 +54,8 @@ test('findAddressField', t => { findAddressField( '/ibc-hop/connection-0/ibc-port/icahost/ordered/{"version":"ics27-1","controllerConnectionId":"connection-0","hostConnectionId":"connection-1","address":"","encoding":"proto3","txType":"sdk_multi_msg"}', ), - '', - 'returns empty string if address is an empty string', + undefined, + 'returns undefined if address is an empty string', ); t.is( findAddressField( @@ -71,6 +71,13 @@ test('findAddressField', t => { 'osmo1m30khedzqy9msu4502u74ugmep30v69pzee370jkas57xhmjfgjqe67ayq', 'returns address when localAddrr is appended to version string', ); + t.is( + findAddressField( + '/ibc-hop/connection-0/ibc-port/icahost/ordered/{not valid JSON}', + ), + undefined, + 'returns undefined when JSON is malformed', + ); }); test('makeICQChannelAddress', t => {