diff --git a/packages/orchestration/src/service.js b/packages/orchestration/src/service.js index 74c32958039a..2a81bb200c73 100644 --- a/packages/orchestration/src/service.js +++ b/packages/orchestration/src/service.js @@ -1,6 +1,6 @@ /** @file Orchestration service */ -import { V as E } from '@agoric/vow/vat.js'; +import { E } from '@endo/far'; import { M } from '@endo/patterns'; import { Shape as NetworkShape } from '@agoric/network'; import { prepareChainAccountKit } from './exos/chainAccountKit.js'; @@ -13,10 +13,11 @@ import { /** * @import {Zone} from '@agoric/base-zone'; * @import {Remote} from '@agoric/internal'; - * @import {Port, PortAllocator} from '@agoric/network'; + * @import {Connection, Port, PortAllocator} from '@agoric/network'; * @import {IBCConnectionID} from '@agoric/vats'; + * @import {RemoteIbcAddress} from '@agoric/vats/tools/ibc-utils.js'; * @import {VowTools} from '@agoric/vow'; - * @import {ICQConnection, IcaAccount, ICQConnectionKit} from './types.js'; + * @import {ICQConnection, IcaAccount, ICQConnectionKit, ChainAccountKit} from './types.js'; */ const { Fail, bare } = assert; @@ -37,9 +38,9 @@ const { Fail, bare } = assert; * >} PowerStore */ -/** - * @typedef {MapStore} ICQConnectionStore - */ +/** @typedef {MapStore} ICQConnectionStore */ + +/** @typedef {ChainAccountKit | ICQConnectionKit} ConnectionKit */ /** * @template {keyof OrchestrationPowers} K @@ -52,37 +53,62 @@ const getPower = (powers, name) => { }; export const OrchestrationI = M.interface('Orchestration', { - makeAccount: M.callWhen(M.string(), M.string()).returns( - M.remotable('ChainAccount'), - ), - provideICQConnection: M.callWhen(M.string()).returns( - M.remotable('Connection'), + makeAccount: M.call(M.string(), M.string()).returns(M.promise()), + provideICQConnection: M.call(M.string()).returns( + M.or(M.promise(), M.remotable('ICQConnection')), ), }); +const requestICAConnectionWatcherI = M.interface( + 'requestICAConnectionWatcher', + { + onFulfilled: M.call(M.remotable('Port')) + .optional({ remoteConnAddr: M.string() }) + .returns(NetworkShape.Vow$(NetworkShape.Connection)), + }, +); +const requestICQConnectionWatcherI = M.interface( + 'requestICQConnectionWatcher', + { + onFulfilled: M.call(M.remotable('Port')) + .optional({ + remoteConnAddr: M.string(), + controllerConnectionId: M.string(), + }) + .returns(NetworkShape.Vow$(NetworkShape.Connection)), + }, +); +const channelOpenWatcherI = M.interface('channelOpenWatcher', { + onFulfilled: M.call(M.remotable('Connection')) + .optional( + M.splitRecord( + { connectionKit: M.record(), returnFacet: M.string() }, + { saveICQConnection: M.string() }, + ), + ) + .returns(M.remotable('ConnectionKit holder facet')), +}); + /** @typedef {{ powers: PowerStore; icqConnections: ICQConnectionStore }} OrchestrationState */ /** * @param {Zone} zone + * @param {VowTools} vowTools * @param {ReturnType} makeChainAccountKit * @param {ReturnType} makeICQConnectionKit */ const prepareOrchestrationKit = ( zone, + { when, watch }, makeChainAccountKit, makeICQConnectionKit, ) => zone.exoClassKit( 'Orchestration', { - self: M.interface('OrchestrationSelf', { - allocateICAControllerPort: M.callWhen().returns( - NetworkShape.Vow$(NetworkShape.Port), - ), - allocateICQControllerPort: M.callWhen().returns( - NetworkShape.Vow$(NetworkShape.Port), - ), - }), + requestICAConnectionWatcher: requestICAConnectionWatcherI, + requestICQConnectionWatcher: requestICQConnectionWatcherI, + channelOpenWatcher: channelOpenWatcherI, public: OrchestrationI, }, /** @param {Partial} [initialPowers] */ @@ -98,15 +124,72 @@ const prepareOrchestrationKit = ( return /** @type {OrchestrationState} */ ({ powers, icqConnections }); }, { - self: { - async allocateICAControllerPort() { - const portAllocator = getPower(this.state.powers, 'portAllocator'); - return E(portAllocator).allocateICAControllerPort(); + requestICAConnectionWatcher: { + /** + * @param {Port} port + * @param {{ + * remoteConnAddr: RemoteIbcAddress; + * }} watchContext + */ + onFulfilled(port, { remoteConnAddr }) { + const chainAccountKit = makeChainAccountKit(port, remoteConnAddr); + return watch( + E(port).connect(remoteConnAddr, chainAccountKit.connectionHandler), + this.facets.channelOpenWatcher, + { returnFacet: 'account', connectionKit: chainAccountKit }, + ); }, - async allocateICQControllerPort() { - const portAllocator = getPower(this.state.powers, 'portAllocator'); - return E(portAllocator).allocateICQControllerPort(); + }, + requestICQConnectionWatcher: { + /** + * @param {Port} port + * @param {{ + * remoteConnAddr: RemoteIbcAddress; + * controllerConnectionId: IBCConnectionID; + * }} watchContext + */ + onFulfilled(port, { remoteConnAddr, controllerConnectionId }) { + const connectionKit = makeICQConnectionKit(port); + /** @param {ICQConnectionKit} kit */ + return watch( + E(port).connect(remoteConnAddr, connectionKit.connectionHandler), + this.facets.channelOpenWatcher, + { + connectionKit, + returnFacet: 'connection', + saveICQConnection: controllerConnectionId, + }, + ); + }, + }, + /** + * Waits for a channel (ICA, ICQ) to open and returns the consumer-facing + * facet of the ConnectionKit, specified by `returnFacet`. Saves the + * ConnectionKit if `saveICQConnection` is provided. + */ + channelOpenWatcher: { + /** + * @param {Connection} _connection + * @param {{ + * connectionKit: ConnectionKit; + * returnFacet: string; + * saveICQConnection?: IBCConnectionID; + * }} watchContext + */ + onFulfilled( + _connection, + { connectionKit, returnFacet, saveICQConnection }, + ) { + if (saveICQConnection) { + this.state.icqConnections.init( + saveICQConnection, + /** @type {ICQConnectionKit} */ (connectionKit), + ); + } + return connectionKit[returnFacet]; }, + // TODO #9317 if we fail, should we revoke the port (if it was created in this flow)? + // onRejected() {} }, public: { /** @@ -115,50 +198,47 @@ const prepareOrchestrationKit = ( * @param {IBCConnectionID} controllerConnectionId self connection_id * @returns {Promise} */ - async makeAccount(hostConnectionId, controllerConnectionId) { - const port = await this.facets.self.allocateICAControllerPort(); - + makeAccount(hostConnectionId, controllerConnectionId) { const remoteConnAddr = makeICAChannelAddress( hostConnectionId, controllerConnectionId, ); - const chainAccountKit = makeChainAccountKit(port, remoteConnAddr); - - // await so we do not return a ChainAccount before it successfully instantiates - await E(port).connect( - remoteConnAddr, - chainAccountKit.connectionHandler, + const portAllocator = getPower(this.state.powers, 'portAllocator'); + return when( + watch( + E(portAllocator).allocateICAControllerPort(), + this.facets.requestICAConnectionWatcher, + { + remoteConnAddr, + }, + ), ); - // XXX if we fail, should we close the port (if it was created in this flow)? - return chainAccountKit.account; }, /** * @param {IBCConnectionID} controllerConnectionId - * @returns {Promise} + * @returns {ICQConnection | Promise} */ - async provideICQConnection(controllerConnectionId) { + provideICQConnection(controllerConnectionId) { if (this.state.icqConnections.has(controllerConnectionId)) { return this.state.icqConnections.get(controllerConnectionId) .connection; } - // allocate a new Port for every Connection - // TODO #9317 optimize ICQ port allocation - const port = await this.facets.self.allocateICQControllerPort(); - const remoteConnAddr = makeICQChannelAddress(controllerConnectionId); - const icqConnectionKit = makeICQConnectionKit(port); - - // await so we do not return/save a ICQConnection before it successfully instantiates - await E(port).connect( - remoteConnAddr, - icqConnectionKit.connectionHandler, + const remoteConnAddr = harden( + makeICQChannelAddress(controllerConnectionId), ); - - this.state.icqConnections.init( - controllerConnectionId, - icqConnectionKit, + const portAllocator = getPower(this.state.powers, 'portAllocator'); + return when( + watch( + // allocate a new Port for every Connection + // TODO #9317 optimize ICQ port allocation + E(portAllocator).allocateICQControllerPort(), + this.facets.requestICQConnectionWatcher, + { + remoteConnAddr, + controllerConnectionId, + }, + ), ); - - return icqConnectionKit.connection; }, }, }, @@ -173,6 +253,7 @@ export const prepareOrchestrationTools = (zone, vowTools) => { const makeICQConnectionKit = prepareICQConnectionKit(zone, vowTools); const makeOrchestrationKit = prepareOrchestrationKit( zone, + vowTools, makeChainAccountKit, makeICQConnectionKit, ); diff --git a/packages/orchestration/test/service.test.ts b/packages/orchestration/test/service.test.ts index a91f427a5916..0a324843eeb7 100644 --- a/packages/orchestration/test/service.test.ts +++ b/packages/orchestration/test/service.test.ts @@ -1,7 +1,9 @@ import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js'; +import { E } from '@endo/far'; import { toRequestQueryJson } from '@agoric/cosmic-proto'; import { QueryBalanceRequest } from '@agoric/cosmic-proto/cosmos/bank/v1beta1/query.js'; -import { E } from '@endo/far'; +import { MsgDelegate } from '@agoric/cosmic-proto/cosmos/staking/v1beta1/tx.js'; +import { Any } from '@agoric/cosmic-proto/google/protobuf/any.js'; import { commonSetup } from './supports.js'; test('makeICQConnection returns an ICQConnection', async t => { @@ -9,10 +11,11 @@ test('makeICQConnection returns an ICQConnection', async t => { bootstrap: { orchestration }, } = await commonSetup(t); - const CONNECTION_ID = 'connection-0'; + const CONTROLLER_CONNECTION_ID = 'connection-0'; - const icqConnection = - await E(orchestration).provideICQConnection(CONNECTION_ID); + const icqConnection = await E(orchestration).provideICQConnection( + CONTROLLER_CONNECTION_ID, + ); const [localAddr, remoteAddr] = await Promise.all([ E(icqConnection).getLocalAddress(), E(icqConnection).getRemoteAddress(), @@ -24,7 +27,7 @@ test('makeICQConnection returns an ICQConnection', async t => { t.regex(localAddr, /ibc-port\/icqcontroller-\d+/); t.regex( remoteAddr, - new RegExp(`/ibc-hop/${CONNECTION_ID}`), + new RegExp(`/ibc-hop/${CONTROLLER_CONNECTION_ID}`), 'remote address contains provided connectionId', ); t.regex( @@ -47,4 +50,56 @@ test('makeICQConnection returns an ICQConnection', async t => { ); }); -test.todo('makeAccount'); +test('makeAccount returns a ChainAccount', async t => { + const { + bootstrap: { orchestration }, + } = await commonSetup(t); + + const HOST_CONNECTION_ID = 'connection-0'; + const CONTROLLER_CONNECTION_ID = 'connection-1'; + + const account = await E(orchestration).makeAccount( + HOST_CONNECTION_ID, + CONTROLLER_CONNECTION_ID, + ); + const [localAddr, remoteAddr, chainAddr] = await Promise.all([ + E(account).getLocalAddress(), + E(account).getRemoteAddress(), + E(account).getAddress(), + ]); + t.log(account, { + localAddr, + remoteAddr, + chainAddr, + }); + t.regex(localAddr, /ibc-port\/icacontroller-\d+/); + t.regex( + remoteAddr, + new RegExp(`/ibc-hop/${CONTROLLER_CONNECTION_ID}`), + 'remote address contains provided connectionId', + ); + t.regex( + remoteAddr, + /icahost\/ordered/, + 'remote address contains icahost port, ordered ordering', + ); + t.regex( + remoteAddr, + /"version":"ics27-1"(.*)"encoding":"proto3"/, + 'remote address contains version and encoding in version string', + ); + + await t.throwsAsync( + E(account).executeEncodedTx([ + Any.toJSON( + MsgDelegate.toProtoMsg({ + delegatorAddress: 'cosmos1test', + validatorAddress: 'cosmosvaloper1test', + amount: { denom: 'uatom', amount: '10' }, + }), + ), + ]), + { message: /"type":1(.*)"data":"(.*)"memo":""/ }, + 'TODO do not use echo connection', + ); +});