diff --git a/packages/orchestration/src/exos/ica-account-kit.js b/packages/orchestration/src/exos/ica-account-kit.js index 5cf67e007334..524643b90c7a 100644 --- a/packages/orchestration/src/exos/ica-account-kit.js +++ b/packages/orchestration/src/exos/ica-account-kit.js @@ -4,6 +4,7 @@ import { E } from '@endo/far'; import { M } from '@endo/patterns'; import { NonNullish, makeTracer } from '@agoric/internal'; import { VowShape } from '@agoric/vow'; +import { CLOSE_REASON_FINALIZER } from '@agoric/network'; import { ChainAddressShape, OutboundConnectionHandlerI, @@ -187,12 +188,16 @@ export const prepareIcaAccountKit = (zone, { watch, asVow }) => }); }, /** - * This handler fires if the connection is closed due to external - * factors (e.g. a packet timeout). It will not fire if a holder calls - * `.close()`. + * This handler fires any time the connection closes. This could be due + * to external factors (e.g. a packet timeout), or a holder initated + * action (`.close()`). * - * Here, we clear the connection and addresses from state as we expect - * them to change and call reopen() to re-establish the connection. + * Here, we clear the connection and addresses from state as they will + * change - a new chanel will be established if the connection is + * reopened. + * + * If the close reason is not {@link CLOSE_REASON_FINALIZER}, we + * automatically reopen the connection. * * @param {Remote} _connection * @param {unknown} reason @@ -203,7 +208,12 @@ export const prepareIcaAccountKit = (zone, { watch, asVow }) => this.state.connection = undefined; this.state.localAddress = undefined; this.state.remoteAddress = undefined; - void watch(this.facets.account.reopen()); + if (reason !== CLOSE_REASON_FINALIZER) { + trace('Automatically reopening the connection.'); + void watch(this.facets.account.reopen()); + } else { + trace('Channel closed by the user action. Not reopening.'); + } }, }, }, diff --git a/packages/orchestration/test/service.test.ts b/packages/orchestration/test/service.test.ts index 95bc0ba8ec12..1db27e3d222e 100644 --- a/packages/orchestration/test/service.test.ts +++ b/packages/orchestration/test/service.test.ts @@ -26,7 +26,7 @@ const CHAIN_ID = 'cosmoshub-99'; const HOST_CONNECTION_ID = 'connection-0'; const CONTROLLER_CONNECTION_ID = 'connection-1'; -test('makeICQConnection returns an ICQConnection', async t => { +test.serial('makeICQConnection returns an ICQConnection', async t => { const { bootstrap: { cosmosInterchainService }, } = await commonSetup(t); @@ -111,7 +111,7 @@ test('makeICQConnection returns an ICQConnection', async t => { ); }); -test('makeAccount returns an IcaAccountKit', async t => { +test.serial('makeAccount returns an IcaAccountKit', async t => { const { bootstrap: { cosmosInterchainService }, } = await commonSetup(t); @@ -182,206 +182,182 @@ test('makeAccount returns an IcaAccountKit', async t => { ); }); -test('makeAccount accepts opts (version, ordering, encoding)', async t => { - const { - bootstrap: { cosmosInterchainService }, - } = await commonSetup(t); - - const account = await E(cosmosInterchainService).makeAccount( - CHAIN_ID, - HOST_CONNECTION_ID, - CONTROLLER_CONNECTION_ID, - { version: 'ics27-2', ordering: 'unordered', encoding: 'json' }, - ); - const [localAddr, remoteAddr] = await Promise.all([ - E(account).getLocalAddress(), - E(account).getRemoteAddress(), - ]); - t.log({ - localAddr, - remoteAddr, - }); - for (const addr of [localAddr, remoteAddr]) { - t.regex(addr, /unordered/, 'remote address contains unordered ordering'); - t.regex( - addr, - /"version":"ics27-2"(.*)"encoding":"json"/, - 'remote address contains version and encoding in version string', +test.serial( + 'makeAccount accepts opts (version, ordering, encoding)', + async t => { + const { + bootstrap: { cosmosInterchainService }, + } = await commonSetup(t); + + const account = await E(cosmosInterchainService).makeAccount( + CHAIN_ID, + HOST_CONNECTION_ID, + CONTROLLER_CONNECTION_ID, + { version: 'ics27-2', ordering: 'unordered', encoding: 'json' }, + ); + const [localAddr, remoteAddr] = await Promise.all([ + E(account).getLocalAddress(), + E(account).getRemoteAddress(), + ]); + t.log({ + localAddr, + remoteAddr, + }); + for (const addr of [localAddr, remoteAddr]) { + t.regex(addr, /unordered/, 'remote address contains unordered ordering'); + t.regex( + addr, + /"version":"ics27-2"(.*)"encoding":"json"/, + 'remote address contains version and encoding in version string', + ); + } + }, +); + +test.serial( + 'connection automatically reopens when close is not user-initiated', + async t => { + const { + bootstrap: { cosmosInterchainService }, + mocks: { ibcBridge }, + utils: { inspectDibcBridge }, + } = await commonSetup(t); + + await E(cosmosInterchainService).makeAccount( + CHAIN_ID, + HOST_CONNECTION_ID, + CONTROLLER_CONNECTION_ID, + { version: 'ics27-2', ordering: 'unordered', encoding: 'json' }, ); - } -}); - -test('.close() sends a downcall to the ibc bridge handler', async t => { - const { - bootstrap: { cosmosInterchainService }, - utils: { inspectDibcBridge }, - } = await commonSetup(t); - - const account = await E(cosmosInterchainService).makeAccount( - CHAIN_ID, - HOST_CONNECTION_ID, - CONTROLLER_CONNECTION_ID, - ); - await eventLoopIteration(); // ensure there's an account to close - await E(account).close(); - await eventLoopIteration(); - - const { bridgeEvents, bridgeDowncalls } = await inspectDibcBridge(); - t.is(bridgeEvents.length, 1, 'bridge received 1 event'); - t.is( - bridgeEvents[0].event, - 'channelOpenAck', - 'bridged received channelOpenAck event', - ); - - t.is(bridgeDowncalls.length, 3, 'bridge received 3 downcalls'); - t.is( - bridgeDowncalls[0].method, - 'bindPort', - 'bridge received bindPort downcall', - ); - t.is( - bridgeDowncalls[1].method, - 'startChannelOpenInit', - 'bridge received startChannelOpenInit downcall', - ); - t.is( - bridgeDowncalls[2].method, - 'startChannelCloseInit', - 'bridge received startChannelCloseInit downcall', - ); -}); - -test('onClose handler is called when channelCloseConfirm event is received', async t => { - const { - bootstrap: { cosmosInterchainService }, - mocks: { ibcBridge }, - utils: { inspectDibcBridge }, - } = await commonSetup(t); - - await E(cosmosInterchainService).makeAccount( - CHAIN_ID, - HOST_CONNECTION_ID, - CONTROLLER_CONNECTION_ID, - { version: 'ics27-2', ordering: 'unordered', encoding: 'json' }, - ); - - const { bridgeEvents: bridgeEvents0, bridgeDowncalls: bridgeDowncalls0 } = - await inspectDibcBridge(); - t.is(bridgeEvents0.length, 1, 'bridge received 1 event'); - t.is( - bridgeEvents0[0].event, - 'channelOpenAck', - 'bridged received channelOpenAck event', - ); - t.is(bridgeDowncalls0.length, 2, 'bridge received 2 downcalls'); - t.is( - bridgeDowncalls0[0].method, - 'bindPort', - 'bridge received bindPort downcall', - ); - t.is( - bridgeDowncalls0[1].method, - 'startChannelOpenInit', - 'bridge received startChannelOpenInit downcall', - ); - - // get channelInfo from `channelOpenAck` event - const { event, ...channelInfo } = bridgeEvents0[0]; - // simulate channel closing from remote chain - await E(ibcBridge).fromBridge(buildChannelCloseConfirmEvent(channelInfo)); - await eventLoopIteration(); - - const { bridgeEvents: bridgeEvents1, bridgeDowncalls: bridgeDowncalls1 } = - await inspectDibcBridge(); - t.is(bridgeEvents1.length, 3, 'bridge received an additional 2 events'); - t.is( - bridgeEvents1[bridgeEvents1.length - 2].event, - 'channelCloseConfirm', - 'bridged received channelCloseInit event', - ); - t.is( - bridgeEvents1[bridgeEvents1.length - 1].event, - 'channelOpenAck', - 'onCloe handler automatically reopens the channel', - ); -}); - -test('reopen a close account(channel) after choosing to close it', async t => { - const { - bootstrap: { cosmosInterchainService }, - utils: { inspectDibcBridge }, - } = await commonSetup(t); - - const account = await E(cosmosInterchainService).makeAccount( - CHAIN_ID, - HOST_CONNECTION_ID, - CONTROLLER_CONNECTION_ID, - ); - - const [chainAddress0, remoteAddress0, localAddress0] = await Promise.all([ - E(account).getAddress(), - E(account).getRemoteAddress(), - E(account).getLocalAddress(), - ]); - await eventLoopIteration(); // ensure there's an account to close - // close the account - await E(account).close(); - await eventLoopIteration(); + const { bridgeEvents: bridgeEvents0, bridgeDowncalls: bridgeDowncalls0 } = + await inspectDibcBridge(); - const { bridgeDowncalls: bridgeDowncalls0 } = await inspectDibcBridge(); - t.is( - bridgeDowncalls0[2].method, - 'startChannelCloseInit', - 'bridge received startChannelCloseInit downcall', - ); + t.is( + bridgeEvents0[0].event, + 'channelOpenAck', + 'bridged received channelOpenAck event', + ); + t.is(bridgeEvents0.length, 1, 'bridge received 1 event'); - // reopen the account - await E(account).reopen(); - await eventLoopIteration(); + t.is( + bridgeDowncalls0[0].method, + 'bindPort', + 'bridge received bindPort downcall', + ); + t.is( + bridgeDowncalls0[1].method, + 'startChannelOpenInit', + 'bridge received startChannelOpenInit downcall', + ); + t.is(bridgeDowncalls0.length, 2, 'bridge received 2 downcalls'); + + // get channelInfo from `channelOpenAck` event + const { event, ...channelInfo } = bridgeEvents0[0]; + // simulate channel closing from remote chain + await E(ibcBridge).fromBridge(buildChannelCloseConfirmEvent(channelInfo)); + await eventLoopIteration(); + + const { bridgeEvents: bridgeEvents1, bridgeDowncalls: bridgeDowncalls1 } = + await inspectDibcBridge(); + t.is(bridgeEvents1.length, 3, 'bridge received an additional 2 events'); + t.is( + bridgeEvents1[bridgeEvents1.length - 2].event, + 'channelCloseConfirm', + 'bridged received channelCloseInit event', + ); + t.is( + bridgeEvents1[bridgeEvents1.length - 1].event, + 'channelOpenAck', + 'onClose handler automatically reopens the channel', + ); + t.log('bridgeEvents', bridgeEvents1); + t.log('bridgeDowncalls', bridgeDowncalls1); + }, +); + +test.serial( + 'reopen a closed account(channel) after choosing to close it', + async t => { + const { + bootstrap: { cosmosInterchainService }, + utils: { inspectDibcBridge }, + } = await commonSetup(t); + + const account = await E(cosmosInterchainService).makeAccount( + CHAIN_ID, + HOST_CONNECTION_ID, + CONTROLLER_CONNECTION_ID, + ); - const { bridgeDowncalls } = await inspectDibcBridge(); - t.is( - bridgeDowncalls[3].method, - 'startChannelOpenInit', - 'bridge received startChannelOpenInit to re-establish the channel', - ); + const [chainAddress0, remoteAddress0, localAddress0] = await Promise.all([ + E(account).getAddress(), + E(account).getRemoteAddress(), + E(account).getLocalAddress(), + ]); + + await eventLoopIteration(); // ensure there's an account to close + // close the account + await E(account).close(); + await eventLoopIteration(); + + const { bridgeDowncalls: bridgeDowncalls0 } = await inspectDibcBridge(); + // FIXME: shouldn't we expect to see a `startChannelCloseInit` downcall? + t.is( + bridgeDowncalls0?.[2]?.method, + 'startChannelCloseInit', + 'bridge received startChannelCloseInit downcall', + ); - const getPortAndConnectionIDs = (obj: IBCMethod<'startChannelOpenInit'>) => { - const { hops, packet } = obj; - const { source_port: sourcePort } = packet; - return { hops, sourcePort }; - }; + // reopen the account + await E(account).reopen(); + await eventLoopIteration(); - t.deepEqual( - getPortAndConnectionIDs( - bridgeDowncalls[3] as IBCMethod<'startChannelOpenInit'>, - ), - getPortAndConnectionIDs( - bridgeDowncalls[1] as IBCMethod<'startChannelOpenInit'>, - ), - 'same port and connection id are used to re-stablish the channel', - ); + const { bridgeDowncalls } = await inspectDibcBridge(); + t.log('bridgeDowncalls', bridgeDowncalls); + t.is( + bridgeDowncalls?.[3]?.method, + 'startChannelOpenInit', + 'bridge received startChannelOpenInit to re-establish the channel', + ); - const [chainAddress, remoteAddress, localAddress] = await Promise.all([ - E(account).getAddress(), - E(account).getRemoteAddress(), - E(account).getLocalAddress(), - ]); + const getPortAndConnectionIDs = ( + obj: IBCMethod<'startChannelOpenInit'>, + ) => { + const { hops, packet } = obj; + const { source_port: sourcePort } = packet; + return { hops, sourcePort }; + }; + + t.deepEqual( + getPortAndConnectionIDs( + bridgeDowncalls[3] as IBCMethod<'startChannelOpenInit'>, + ), + getPortAndConnectionIDs( + bridgeDowncalls[1] as IBCMethod<'startChannelOpenInit'>, + ), + 'same port and connection id are used to re-stablish the channel', + ); - t.deepEqual(chainAddress, chainAddress0, 'chain address is unchanged'); - t.notDeepEqual( - remoteAddress, - remoteAddress0, - 'remote ibc address is changed', - ); - t.notDeepEqual(localAddress, localAddress0, 'local ibc address is changed'); - const getChannelID = (lAddr: LocalIbcAddress) => - lAddr.split('/ibc-channel/')[1]; - t.not( - getChannelID(localAddress), - getChannelID(localAddress0), - 'channel id is changed', - ); -}); + const [chainAddress, remoteAddress, localAddress] = await Promise.all([ + E(account).getAddress(), + E(account).getRemoteAddress(), + E(account).getLocalAddress(), + ]); + + t.deepEqual(chainAddress, chainAddress0, 'chain address is unchanged'); + t.notDeepEqual( + remoteAddress, + remoteAddress0, + 'remote ibc address is changed', + ); + t.notDeepEqual(localAddress, localAddress0, 'local ibc address is changed'); + const getChannelID = (lAddr: LocalIbcAddress) => + lAddr.split('/ibc-channel/')[1]; + t.not( + getChannelID(localAddress), + getChannelID(localAddress0), + 'channel id is changed', + ); + }, +);