Skip to content

Commit

Permalink
feat: close, reopen, and automatically reopen ICA channels
Browse files Browse the repository at this point in the history
- adds .reopen() method to IcaAccountKit['holder'] to re-establish an ICA channel using the original requestedRemoteAddress. it's
  intended to be used internally to automatically re-establish a channel, but also exposed to holders if they wish to re-open their
  account after calling .close(). includes corresponding 'ReopenAccount' invitationMaker on CosmosOrchestrationAccount.
- performs cleanup after a connection is closed - namely resetting localAddr, remoteAddr, and connection in state. chainAddress and
  port are preserved.
- adds logic to onClose() handler to automatically re-establish the ICA channel. `reopen() will only fire when external factors
  force a channel closure (iow - if .close() is called by the holder, this will not fire)
- updates network-fakes.ts to cache mockChainAddresses based on PortID:ConnectionID to mimic ICS-27 protocol

- refs: #9192
- refs: #9068
  • Loading branch information
0xpatrickdev committed Aug 16, 2024
1 parent 295aa5b commit b808d43
Show file tree
Hide file tree
Showing 8 changed files with 329 additions and 40 deletions.
17 changes: 15 additions & 2 deletions packages/orchestration/src/cosmos-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,9 +213,22 @@ export interface IcaAccount {
opts?: Partial<Omit<TxBody, 'messages'>>,
) => Promise<string>;
/**
* Close the remote account
* Deactivates the ICA account by closing the ICA channel. The `Port` is
* persisted so holders can always call `.reactivate()` to re-establish a new
* channel with the same chain address.
* CAVEAT: Does not retrieve assets so they may be lost if left.
* @throws {Error} if connection is not available or already deactivated
*/
close: () => Promise<void>;
deactivate: () => Promise<void>;
/**
* Reactivates the ICA account by re-establishing a new channel with the
* original Port and requested address.
* If a channel is closed for an unexpected reason, such as a packet timeout,
* an automatic attempt to re will be made and the holder should not need
* to call `.reactivate()`.
* @throws {Error} if connection is currently active
*/
reactivate: () => Promise<void>;
/** @returns the address of the remote channel */
getRemoteAddress: () => RemoteIbcAddress;
/** @returns the address of the local channel */
Expand Down
3 changes: 2 additions & 1 deletion packages/orchestration/src/exos/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ classDiagram
getPort()
executeTx()
executeEncodedTx()
close()
deactivate()
reactivate()
}
class ICQConnection {
port: Port
Expand Down
26 changes: 23 additions & 3 deletions packages/orchestration/src/exos/cosmos-orchestration-account.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ export const IcaAccountHolderI = M.interface('IcaAccountHolder', {
),
withdrawRewards: M.call().returns(Vow$(M.arrayOf(DenomAmountShape))),
undelegate: M.call(M.arrayOf(DelegationShape)).returns(VowShape),
deactivate: M.call().returns(VowShape),
reactivate: M.call().returns(VowShape),
});

/** @type {{ [name: string]: [description: string, valueShape: Matcher] }} */
Expand Down Expand Up @@ -167,7 +169,8 @@ export const prepareCosmosOrchestrationAccountKit = (
).returns(M.promise()),
WithdrawReward: M.call(ChainAddressShape).returns(M.promise()),
Undelegate: M.call(M.arrayOf(DelegationShape)).returns(M.promise()),
CloseAccount: M.call().returns(M.promise()),
DeactivateAccount: M.call().returns(M.promise()),
ReactivateAccount: M.call().returns(M.promise()),
TransferAccount: M.call().returns(M.promise()),
Send: M.call().returns(M.promise()),
SendAll: M.call().returns(M.promise()),
Expand Down Expand Up @@ -358,8 +361,17 @@ export const prepareCosmosOrchestrationAccountKit = (
return watch(this.facets.holder.undelegate(delegations));
}, 'Undelegate');
},
CloseAccount() {
throw Error('not yet implemented');
DeactivateAccount() {
return zcf.makeInvitation(seat => {
seat.exit();
return watch(this.facets.holder.deactivate());
}, 'DeactivateAccount');
},
ReactivateAccount() {
return zcf.makeInvitation(seat => {
seat.exit();
return watch(this.facets.holder.reactivate());
}, 'ReactivateAccount');
},
Send() {
/**
Expand Down Expand Up @@ -660,6 +672,14 @@ export const prepareCosmosOrchestrationAccountKit = (
return watch(undelegateV, this.facets.returnVoidWatcher);
});
},
/** @type {HostOf<IcaAccount['deactivate']>} */
deactivate() {
return watch(E(this.facets.helper.owned()).deactivate());
},
/** @type {HostOf<IcaAccount['reactivate']>} */
reactivate() {
return watch(E(this.facets.helper.owned()).reactivate());
},
},
},
);
Expand Down
67 changes: 51 additions & 16 deletions packages/orchestration/src/exos/ica-account-kit.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,14 @@ import { findAddressField } from '../utils/address.js';
import { makeTxPacket, parseTxPacket } from '../utils/packet.js';

/**
* @import {HostOf} from '@agoric/async-flow';
* @import {Zone} from '@agoric/base-zone';
* @import {Connection, Port} from '@agoric/network';
* @import {Remote, Vow, VowTools} from '@agoric/vow';
* @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';
* @import {ChainAddress, IcaAccount} from '../types.js';
*/

const trace = makeTracer('IcaAccountKit');
Expand All @@ -37,7 +38,8 @@ export const IcaAccountI = M.interface('IcaAccount', {
executeEncodedTx: M.call(M.arrayOf(Proto3Shape))
.optional(TxBodyOptsShape)
.returns(VowShape),
close: M.call().returns(VowShape),
deactivate: M.call().returns(VowShape),
reactivate: M.call().returns(VowShape),
});

/**
Expand All @@ -49,6 +51,7 @@ export const IcaAccountI = M.interface('IcaAccount', {
* requestedRemoteAddress: string;
* remoteAddress: RemoteIbcAddress | undefined;
* chainAddress: ChainAddress | undefined;
* isInitiatingClose: boolean;
* }} State
*/

Expand Down Expand Up @@ -82,6 +85,7 @@ export const prepareIcaAccountKit = (zone, { watch, asVow }) =>
remoteAddress: undefined,
chainAddress: undefined,
localAddress: undefined,
isInitiatingClose: false,
}),
{
parseTxPacketWatcher: {
Expand Down Expand Up @@ -129,29 +133,39 @@ export const prepareIcaAccountKit = (zone, { watch, asVow }) =>
executeEncodedTx(msgs, opts) {
return asVow(() => {
const { connection } = this.state;
if (!connection) throw Fail`connection not available`;
if (!connection) {
throw Fail`Connection not available or deactivated.`;
}
return watch(
E(connection).send(makeTxPacket(msgs, opts)),
this.facets.parseTxPacketWatcher,
);
});
},
/**
* Close the remote account
*
* @returns {Vow<void>}
* @throws {Error} if connection is not available or already closed
*/
close() {
/** @type {HostOf<IcaAccount['deactivate']>} */
deactivate() {
return asVow(() => {
/// TODO #9192 what should the behavior be here? and `onClose`?
// - retrieve assets?
// - revoke the port?
const { connection } = this.state;
if (!connection) throw Fail`connection not available`;
if (!connection) throw Fail`Connection not available`;
this.state.isInitiatingClose = true;
return E(connection).close();
});
},
/** @type {HostOf<IcaAccount['reactivate']>} */
reactivate() {
return asVow(() => {
const { connection, port, requestedRemoteAddress } = this.state;
if (connection) {
throw Fail`Connection already active. Call .deactivate() first.`;
}
return watch(
E(port).connect(
requestedRemoteAddress,
this.facets.connectionHandler,
),
);
});
},
},
connectionHandler: {
/**
Expand All @@ -175,13 +189,34 @@ export const prepareIcaAccountKit = (zone, { watch, asVow }) =>
});
},
/**
* This handler fires any time the connection (channel) closes. This
* could be due to external factors (e.g. a packet timeout), or a holder
* initiated action (`.deactivate()`).
*
* Here, we clear the connection and addresses from state as they will
* change - a new channel will be established if the connection is
* reopened.
*
* If the holder did not initiate the closure, a connection is
* re-established using the original requested remote address. This will
* result in a new channelID but the ChainAddress will be preserved.
*
* @param {Remote<Connection>} _connection
* @param {unknown} reason
* @see {@link https://docs.cosmos.network/v0.45/ibc/overview.html#:~:text=In%20ORDERED%20channels%2C%20a%20timeout%20of%20a%20single%20packet%20in%20the%20channel%20closes%20the%20channel.}
*/
async onClose(_connection, reason) {
trace(`ICA Channel closed. Reason: ${reason}`);
// FIXME handle connection closing https://github.com/Agoric/agoric-sdk/issues/9192
// XXX is there a scenario where a connection will unexpectedly close? _I think yes_
this.state.connection = undefined;
this.state.localAddress = undefined;
this.state.remoteAddress = undefined;
if (this.state.isInitiatingClose === true) {
trace('Account deactivated by holder. Skipping reactivation.');
this.state.isInitiatingClose = false;
} else {
trace('Automatically reactivating the account.');
void watch(this.facets.account.reactivate());
}
},
},
},
Expand Down
Loading

0 comments on commit b808d43

Please sign in to comment.