Skip to content

Commit

Permalink
test: e2e test of ica channel close flows
Browse files Browse the repository at this point in the history
- automatically reopen a closed channel
- CosmosOrchAccount (IcaAccout) holder can close their account
- CosmosOrchAccount (IcaAccount) holder can reopen their account
  • Loading branch information
0xpatrickdev committed Aug 8, 2024
1 parent 8364fdf commit 4a69097
Show file tree
Hide file tree
Showing 2 changed files with 311 additions and 0 deletions.
306 changes: 306 additions & 0 deletions multichain-testing/test/ica-channel-close.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,306 @@
import anyTest from '@endo/ses-ava/prepare-endo.js';
import type { TestFn } from 'ava';
import type { CosmosOrchestrationAccountStorageState } from '@agoric/orchestration/src/exos/cosmos-orchestration-account.js';
import { makeDoOffer } from '../tools/e2e-tools.js';
import {
commonSetup,
SetupContextWithWallets,
chainConfig,
} from './support.js';
import { makeQueryClient } from '../tools/query.js';
import { parseLocalAddress, parseRemoteAddress } from '../tools/address.js';
import chainInfo from '../starship-chain-info.js';

const test = anyTest as TestFn<SetupContextWithWallets>;

const accounts = ['cosmoshub', 'osmosis'];

const contractName = 'basicFlows';
const contractBuilder =
'../packages/builders/scripts/orchestration/init-basic-flows.js';

test.before(async t => {
const { deleteTestKeys, setupTestKeys, ...rest } = await commonSetup(t);
deleteTestKeys(accounts).catch();
const wallets = await setupTestKeys(accounts);
t.context = { ...rest, wallets, deleteTestKeys };

t.log('bundle and install contract', contractName);
await t.context.deployBuilder(contractBuilder);
const vstorageClient = t.context.makeQueryTool();
await t.context.retryUntilCondition(
() => vstorageClient.queryData(`published.agoricNames.instance`),
res => contractName in Object.fromEntries(res),
`${contractName} instance is available`,
);
});

test.after(async t => {
const { deleteTestKeys } = t.context;
deleteTestKeys(accounts);
});

/** The account holder chooses to close their ICA account (channel) */
const intentionalCloseAccountScenario = test.macro({
title: (_, chainName: string) => `Close and reopen account on ${chainName}`,
exec: async (t, chainName: string) => {
const config = chainConfig[chainName];
if (!config) return t.fail(`Unknown chain: ${chainName}`);

const {
wallets,
provisionSmartWallet,
makeQueryTool,
retryUntilCondition,
useChain,
} = t.context;

const vstorageClient = makeQueryTool();

const agoricAddr = wallets[chainName];
const wdUser1 = await provisionSmartWallet(agoricAddr, {
BLD: 100n,
IST: 100n,
});
t.log(`provisioning agoric smart wallet for ${agoricAddr}`);

const doOffer = makeDoOffer(wdUser1);
t.log(`${chainName} makeAccount offer`);
const offerId = `${chainName}-makeAccount-${Date.now()}`;

await doOffer({
id: offerId,
invitationSpec: {
source: 'agoricContract',
instancePath: [contractName],
callPipe: [['makeOrchAccountInvitation']],
},
offerArgs: { chainName },
proposal: {},
});
const currentWalletRecord = await retryUntilCondition(
() => vstorageClient.queryData(`published.wallet.${agoricAddr}.current`),
({ offerToPublicSubscriberPaths }) =>
Object.fromEntries(offerToPublicSubscriberPaths)[offerId],
`${offerId} continuing invitation is in vstorage`,
);
const offerToPublicSubscriberMap = Object.fromEntries(
currentWalletRecord.offerToPublicSubscriberPaths,
);

const accountStoragePath = offerToPublicSubscriberMap[offerId]?.account;
t.assert(accountStoragePath, 'account storage path returned');
const address = accountStoragePath.split('.').pop();
t.log('Got address:', address);

const {
remoteAddress,
localAddress,
}: CosmosOrchestrationAccountStorageState =
await vstorageClient.queryData(accountStoragePath);
const { rPortID, rChannelID } = parseRemoteAddress(remoteAddress);

const remoteQueryClient = makeQueryClient(
await useChain(chainName).getRestEndpoint(),
);
const localQueryClient = makeQueryClient(
await useChain('agoric').getRestEndpoint(),
);

const { channel } = await retryUntilCondition(
() => remoteQueryClient.queryChannel(rPortID, rChannelID),
// @ts-expect-error ChannelSDKType.state is a string not a number
({ channel }) => channel?.state === 'STATE_OPEN',
`ICA channel is open on Host - ${chainName}`,
);
t.log('Channel State Before', channel);
// @ts-expect-error ChannelSDKType.state is a string not a number
t.is(channel?.state, 'STATE_OPEN', 'channel is open');

const closeAccountOfferId = `${chainName}-closeAccount-${Date.now()}`;
await doOffer({
id: closeAccountOfferId,
invitationSpec: {
source: 'continuing',
previousOffer: offerId,
invitationMakerName: 'CloseAccount',
},
proposal: {},
});

const { channel: rChannelAfterClose } = await retryUntilCondition(
() => remoteQueryClient.queryChannel(rPortID, rChannelID),
// @ts-expect-error ChannelSDKType.state is a string not a number
({ channel }) => channel?.state === 'STATE_CLOSED',
`ICA channel is closed on Host - ${chainName}`,
);
t.log('Remote Channel State After', rChannelAfterClose);
t.is(
rChannelAfterClose?.state,
// @ts-expect-error ChannelSDKType.state is a string not a number
'STATE_CLOSED',
`channel is closed from host perspective - ${chainName}`,
);

const { lPortID, lChannelID } = parseLocalAddress(localAddress);
const { channel: lChannelAfterClose } = await retryUntilCondition(
() => localQueryClient.queryChannel(lPortID, lChannelID),
// @ts-expect-error ChannelSDKType.state is a string not a number
({ channel }) => channel?.state === 'STATE_CLOSED',
`ICA channel is closed on Controller - ${chainName}`,
);
t.log('Local Channel State After', lChannelAfterClose);
if (!lChannelAfterClose?.state) throw Error('channel state is available');
t.is(
lChannelAfterClose.state,
// @ts-expect-error ChannelSDKType.state is a string not a number
'STATE_CLOSED',
`channel is closed from controller perspective - ${chainName}`,
);

const reopenAccountOfferId = `${chainName}-reopenAccount-${Date.now()}`;
await doOffer({
id: reopenAccountOfferId,
invitationSpec: {
source: 'continuing',
previousOffer: offerId,
invitationMakerName: 'ReopenAccount',
},
proposal: {},
});

const { channel: rChannelAfterCloseReopen } = await retryUntilCondition(
() => remoteQueryClient.queryChannel(rPortID, rChannelID),
// @ts-expect-error ChannelSDKType.state is a string not a number
({ channel }) => channel?.state === 'STATE_OPEN',
`ICA channel is reopened on ${chainName} Host`,
);
t.log('Remote Channel State After Reopening', rChannelAfterCloseReopen);
t.is(
rChannelAfterCloseReopen?.state,
// @ts-expect-error ChannelSDKType.state is a string not a number
'STATE_OPEN',
`ICA channel is reopened on ${chainName} Host`,
);
},
});

/** The channel is closed for an unexpected reason and should automatically reopen */
const unintentionalCloseAccountScenario = test.macro({
title: (_, chainName: string) =>
`Automatically reopen closed channel on ${chainName}`,
exec: async (t, chainName: string) => {
const config = chainConfig[chainName];
if (!config) return t.fail(`Unknown chain: ${chainName}`);

const {
wallets,
provisionSmartWallet,
makeQueryTool,
retryUntilCondition,
useChain,
hermes,
} = t.context;

const vstorageClient = makeQueryTool();

const agoricAddr = wallets[chainName];
const wdUser1 = await provisionSmartWallet(agoricAddr, {
BLD: 100n,
IST: 100n,
});
t.log(`provisioning agoric smart wallet for ${agoricAddr}`);

const doOffer = makeDoOffer(wdUser1);
t.log(`${chainName} makeAccount offer`);
const offerId = `${chainName}-makeAccount-${Date.now()}`;

await doOffer({
id: offerId,
invitationSpec: {
source: 'agoricContract',
instancePath: [contractName],
callPipe: [['makeOrchAccountInvitation']],
},
offerArgs: { chainName },
proposal: {},
});
const currentWalletRecord = await retryUntilCondition(
() => vstorageClient.queryData(`published.wallet.${agoricAddr}.current`),
({ offerToPublicSubscriberPaths }) =>
Object.fromEntries(offerToPublicSubscriberPaths)[offerId],
`${offerId} continuing invitation is in vstorage`,
);
const offerToPublicSubscriberMap = Object.fromEntries(
currentWalletRecord.offerToPublicSubscriberPaths,
);

const accountStoragePath = offerToPublicSubscriberMap[offerId]?.account;
t.assert(accountStoragePath, 'account storage path returned');
const address = accountStoragePath.split('.').pop();
t.log('Got address:', address);

const {
remoteAddress,
localAddress,
}: CosmosOrchestrationAccountStorageState =
await vstorageClient.queryData(accountStoragePath);
const { rPortID, rChannelID } = parseRemoteAddress(remoteAddress);
const { lPortID, lChannelID, lConnectionID } =
parseLocalAddress(localAddress);

const dst = {
chainId: chainInfo['agoric'].chainId,
channelID: lChannelID,
portID: lPortID,
connectionID: lConnectionID,
};
const src = {
chainId: useChain(chainName).chainInfo.chain.chain_id,
channelID: rChannelID,
portID: rPortID,
};
console.log(`Initiating channelCloseInit for dst: ${dst} src: ${src}`);
const closeChannelTx = hermes.channelCloseInit(chainName, dst, src);
console.log('closeChannelExec', closeChannelTx);

const remoteQueryClient = makeQueryClient(
await useChain(chainName).getRestEndpoint(),
);
const { channel } = await retryUntilCondition(
() => remoteQueryClient.queryChannel(rPortID, rChannelID),
// @ts-expect-error ChannelSDKType.state is a string not a number
({ channel }) => channel?.state === 'STATE_CLOSED',
'ICA channel closed from Hermes closeChannelInit',
{
retryIntervalMs: 300,
maxRetries: 10,
},
);
t.is(
channel?.state,
// @ts-expect-error ChannelSDKType.state is a string not a number
'STATE_CLOSED',
'closed state is observed',
);

const { channel: channel2 } = await retryUntilCondition(
() => remoteQueryClient.queryChannel(rPortID, rChannelID),
// @ts-expect-error ChannelSDKType.state is a string not a number
({ channel }) => channel?.state === 'STATE_CLOSED',
`ICA channel closed from Hermes closeChannelInit on ${chainName}`,
);
t.is(
channel2?.state,
// @ts-expect-error ChannelSDKType.state is a string not a number
'STATE_OPEN',
`channel is automatically reopened on ${chainName}`,
);
},
});

test.serial(intentionalCloseAccountScenario, 'cosmoshub');
test.serial(intentionalCloseAccountScenario, 'osmosis');

test.serial(unintentionalCloseAccountScenario, 'cosmoshub');
test.serial(unintentionalCloseAccountScenario, 'osmosis');
5 changes: 5 additions & 0 deletions multichain-testing/tools/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type { QueryValidatorsResponseSDKType } from '@agoric/cosmic-proto/cosmos
import type { QueryDelegatorDelegationsResponseSDKType } from '@agoric/cosmic-proto/cosmos/staking/v1beta1/query.js';
import type { QueryDelegatorUnbondingDelegationsResponseSDKType } from '@agoric/cosmic-proto/cosmos/staking/v1beta1/query.js';
import type { QueryDenomHashResponseSDKType } from '@agoric/cosmic-proto/ibc/applications/transfer/v1/query.js';
import type { QueryChannelResponseSDKType } from '@agoric/cosmic-proto/ibc/core/channel/v1/query.js';

// TODO use telescope generated query client from @agoric/cosmic-proto
// https://github.com/Agoric/agoric-sdk/issues/9200
Expand Down Expand Up @@ -52,5 +53,9 @@ export function makeQueryClient(apiUrl: string) {
query<QueryDenomHashResponseSDKType>(
`/ibc/apps/transfer/v1/denom_hashes/${path}/${baseDenom}`,
),
queryChannel: (portID: string, channelID: string) =>
query<QueryChannelResponseSDKType>(
`/ibc/core/channel/v1/channels/${channelID}/ports/${portID}`,
),
};
}

0 comments on commit 4a69097

Please sign in to comment.