Skip to content

Commit

Permalink
test: e2e test of ica channel close flows
Browse files Browse the repository at this point in the history
- CosmosOrchAccount (IcaAccout) holder can deactivate their account (close channel)
- CosmosOrchAccount (IcaAccount) holder can reactivate their account (open new channel w same portID)
  • Loading branch information
0xpatrickdev committed Aug 19, 2024
1 parent a779e08 commit b0cfcb1
Show file tree
Hide file tree
Showing 3 changed files with 241 additions and 6 deletions.
20 changes: 14 additions & 6 deletions multichain-testing/test/basic-flows.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,21 @@ test.before(async t => {
t.context = { ...rest, wallets, deleteTestKeys };

t.log('bundle and install contract', contractName);
await t.context.deployBuilder(contractBuilder);
const { vstorageClient } = t.context;
await t.context.retryUntilCondition(
() => vstorageClient.queryData(`published.agoricNames.instance`),
res => contractName in Object.fromEntries(res),
`${contractName} instance is available`,
const { vstorageClient, deployBuilder, retryUntilCondition } = t.context;
const installedContracts = await vstorageClient.queryData(
`published.agoricNames.instance`,
);
const isInstalled = contractName in Object.fromEntries(installedContracts);
if (!isInstalled) {
await deployBuilder(contractBuilder);
await retryUntilCondition(
() => vstorageClient.queryData(`published.agoricNames.instance`),
res => contractName in Object.fromEntries(res),
`${contractName} instance is available`,
);
} else {
t.log('Contract found. Skipping installation...');
}
});

test.after(async t => {
Expand Down
219 changes: 219 additions & 0 deletions multichain-testing/test/ica-channel-close.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
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 type { IdentifiedChannelSDKType } from '@agoric/cosmic-proto/ibc/core/channel/v1/channel.js';
import type { IBCPortID } from '@agoric/vats';
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';

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);
const { vstorageClient, deployBuilder, retryUntilCondition } = t.context;
const installedContracts = await vstorageClient.queryData(
`published.agoricNames.instance`,
);
const isInstalled = contractName in Object.fromEntries(installedContracts);
if (!isInstalled) {
await deployBuilder(contractBuilder);
await retryUntilCondition(
() => vstorageClient.queryData(`published.agoricNames.instance`),
res => contractName in Object.fromEntries(res),
`${contractName} instance is available`,
);
} else {
t.log('Contract found. Skipping installation...');
}
});

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

// XXX until new localAddr + remoteAddr are published to vstorage, use
// original port to determine new channelID
const findNewChannel = (
channels: IdentifiedChannelSDKType[],
{ rPortID, lPortID }: { rPortID: IBCPortID; lPortID: IBCPortID },
) =>
channels.find(
c =>
c.port_id === rPortID &&
c.counterparty.port_id === lPortID &&
// @ts-expect-error ChannelSDKType.state is a string not a number
c.state === 'STATE_OPEN',
);

/** 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,
vstorageClient,
retryUntilCondition,
useChain,
} = t.context;

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}-deactivateAccount-${Date.now()}`;
await doOffer({
id: closeAccountOfferId,
invitationSpec: {
source: 'continuing',
previousOffer: offerId,
invitationMakerName: 'DeactivateAccount',
},
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}-reactivateAccount-${Date.now()}`;
await doOffer({
id: reopenAccountOfferId,
invitationSpec: {
source: 'continuing',
previousOffer: offerId,
invitationMakerName: 'ReactivateAccount',
},
proposal: {},
});

const { channels } = await retryUntilCondition(
() => remoteQueryClient.queryChannels(),
({ channels }) => !!findNewChannel(channels, { rPortID, lPortID }),
`ICA channel is reopened on ${chainName} Host`,
);
const newChannel = findNewChannel(channels, { rPortID, lPortID });
t.log('New Channel after Reactivate', newChannel);
if (!newChannel) throw new Error('Channel not found');
const newAddress = JSON.parse(newChannel.version).address;
t.is(newAddress, address, `same chain address is returned - ${chainName}`);
t.is(
newChannel.state,
// @ts-expect-error ChannelSDKType.state is a string not a number
'STATE_OPEN',
`channel is open on ${chainName} Host`,
);
t.not(newChannel.channel_id, rChannelID, 'remote channel id changed');
t.not(
newChannel.counterparty.channel_id,
lChannelID,
'local channel id changed',
);
},
});

test.serial(intentionalCloseAccountScenario, 'cosmoshub');
test.serial(intentionalCloseAccountScenario, 'osmosis');
8 changes: 8 additions & 0 deletions multichain-testing/tools/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ 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';
import { QueryChannelsResponseSDKType } 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 +54,11 @@ 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}`,
),
queryChannels: () =>
query<QueryChannelsResponseSDKType>(`/ibc/core/channel/v1/channels`),
};
}

0 comments on commit b0cfcb1

Please sign in to comment.