-
Notifications
You must be signed in to change notification settings - Fork 212
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
test: e2e test of ica channel close flows
- 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
1 parent
a779e08
commit b0cfcb1
Showing
3 changed files
with
241 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters