Skip to content

Commit

Permalink
feat: cosmosOrchAccount.getBalances() (#10004)
Browse files Browse the repository at this point in the history
refs: #9610

## Description
- adds `.getBalances()` method to `cosmos-orchestration-accounts.js`
- adds e2e tests of `CosmosOrchAccount` `getBalance()` and `getBalances()` in `multichain-testing`
- moves `*query` invitations used for testing out of `src/examples/basic-flows` contract into its own contract stored in `src/fixtures/query-flows.contract.js`

### Security Considerations
n/a, using existing powers

### Scaling Considerations
n/a

### Documentation Considerations
n/a 

### Testing Considerations
Includes unit tests with high fidelity mocks and e2e tests.

### Upgrade Considerations
n/a, unreleased code
  • Loading branch information
mergify[bot] authored Sep 4, 2024
2 parents bc8900d + 11f1cde commit 9ced650
Show file tree
Hide file tree
Showing 18 changed files with 963 additions and 350 deletions.
190 changes: 190 additions & 0 deletions multichain-testing/test/account-balance-queries.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import anyTest from '@endo/ses-ava/prepare-endo.js';
import type { TestFn } from 'ava';
import type { CosmosChainInfo } from '@agoric/orchestration';
import {
commonSetup,
SetupContextWithWallets,
chainConfig,
} from './support.js';
import { makeDoOffer } from '../tools/e2e-tools.js';
import chainInfo from '../starship-chain-info.js';
import { MAKE_ACCOUNT_AND_QUERY_BALANCE_TIMEOUT } from './config.js';

const test = anyTest as TestFn<SetupContextWithWallets>;

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

const contractName = 'queryFlows';
const contractBuilder =
'../packages/builders/scripts/testing/start-query-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 };
const { startContract } = rest;
await startContract(contractName, contractBuilder);
});

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

const queryAccountBalances = test.macro({
title: (_, chainName: string) => `Query Account Balances on ${chainName}`,
exec: async (t, chainName: string) => {
const config = chainConfig[chainName];
if (!config) return t.fail(`Unknown chain: ${chainName}`);
const {
wallets,
provisionSmartWallet,
vstorageClient,
retryUntilCondition,
} = 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} makeAccountAndGetBalancesQuery offer`);
const offerId = `${chainName}-makeAccountAndGetBalancesQuery-${Date.now()}`;

await doOffer({
id: offerId,
invitationSpec: {
source: 'agoricContract',
instancePath: [contractName],
callPipe: [['makeAccountAndGetBalancesQueryInvitation']],
},
offerArgs: { chainName },
proposal: {},
});

const offerResult = await retryUntilCondition(
() => vstorageClient.queryData(`published.wallet.${agoricAddr}`),
({ status }) => status.id === offerId && (status.result || status.error),
`${offerId} offer result is in vstorage`,
MAKE_ACCOUNT_AND_QUERY_BALANCE_TIMEOUT,
);
t.log('Account Balances Query Offer Result', offerResult);

const { icqEnabled } = (chainInfo as Record<string, CosmosChainInfo>)[
chainName
];
t.log(
icqEnabled
? 'ICQ Enabled expecting offer result.'
: 'ICQ Disabled expecting offer error',
);

const {
status: { result, error },
} = offerResult;
if (icqEnabled) {
t.is(error, undefined, 'No error observed for supported chain');
const balances = JSON.parse(result);
t.truthy(balances, 'Result is parsed successfully');
t.true(Array.isArray(balances), 'Balances is an array');
t.is(balances.length, 0, 'Balances are empty');
} else {
t.truthy(error, 'Error observed for unsupported chain');
t.regex(
error,
/Queries not available for chain/i,
'Correct error message for unsupported chain',
);
}
},
});

const queryAccountBalance = test.macro({
title: (_, chainName: string) => `Query Account Balance 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 {
chainInfo: {
chain: { staking },
},
} = useChain(chainName);
const denom = staking?.staking_tokens?.[0].denom;
if (!denom) throw Error(`no denom for ${chainName}`);

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} makeAccountAndGetBalanceQuery offer`);
const offerId = `${chainName}-makeAccountAndGetBalanceQuery-${Date.now()}`;

await doOffer({
id: offerId,
invitationSpec: {
source: 'agoricContract',
instancePath: [contractName],
callPipe: [['makeAccountAndGetBalanceQueryInvitation']],
},
offerArgs: { chainName, denom },
proposal: {},
});

const offerResult = await retryUntilCondition(
() => vstorageClient.queryData(`published.wallet.${agoricAddr}`),
({ status }) => status.id === offerId && (status.result || status.error),
`${offerId} offer result is in vstorage`,
MAKE_ACCOUNT_AND_QUERY_BALANCE_TIMEOUT,
);
t.log('Account Balance Query Offer Result', offerResult);
const { icqEnabled } = (chainInfo as Record<string, CosmosChainInfo>)[
chainName
];
t.log(
icqEnabled
? 'ICQ Enabled, expecting offer result.'
: 'ICQ Disabled, expecting offer error',
);

const {
status: { result, error },
} = offerResult;
if (icqEnabled) {
t.is(error, undefined, 'No error observed for supported chain');
const parsedBalance = JSON.parse(result);
t.truthy(parsedBalance, 'Result is parsed successfully');

t.truthy(parsedBalance, 'Balance object exists');
t.is(parsedBalance.denom, denom, 'Correct denom in balance');
t.is(parsedBalance.value, '[0n]', 'Balance amount is 0n');
} else {
t.truthy(error, 'Error observed for unsupported chain');
t.regex(
error,
/Queries not available for chain/i,
'Correct error message for unsupported chain',
);
}
},
});

test.serial(queryAccountBalances, 'osmosis');
test.serial(queryAccountBalances, 'cosmoshub');
test.serial(queryAccountBalance, 'osmosis');
test.serial(queryAccountBalance, 'cosmoshub');
6 changes: 3 additions & 3 deletions multichain-testing/test/chain-queries.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ const test = anyTest as TestFn<SetupContextWithWallets>;

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

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

test.before(async t => {
const { deleteTestKeys, setupTestKeys, ...rest } = await commonSetup(t);
Expand Down Expand Up @@ -110,7 +110,7 @@ const queryICQChain = test.macro({
const offerResult = await retryUntilCondition(
() => vstorageClient.queryData(`published.wallet.${agoricAddr}`),
({ status }) => status.id === offerId && (status.result || status.error),
`${offerId} continuing invitation is in vstorage`,
`${offerId} offer result is in vstorage`,
{
maxRetries: 15,
},
Expand Down
10 changes: 10 additions & 0 deletions multichain-testing/test/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,13 @@ export const AUTO_STAKE_IT_DELEGATIONS_TIMEOUT: RetryOptions = {
retryIntervalMs: 5000,
maxRetries: 24,
};

/**
* Wait about 90s to ensure:
* - ICA Account is created
* - ICQ Connection is established (in some instances)
* - Query is executed (sometimes local, sometimes via ICQ)
*/
export const MAKE_ACCOUNT_AND_QUERY_BALANCE_TIMEOUT: RetryOptions = {
maxRetries: 25,
};
134 changes: 134 additions & 0 deletions packages/builders/scripts/testing/start-query-flows.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/**
* @file A proposal to start the query-flows contract.
*
* QueryFlows is a testing fixture that publishes query results to vstorage.
* It's purpose is to support E2E testing.
*/
import { deeplyFulfilledObject, makeTracer } from '@agoric/internal';
import { makeStorageNodeChild } from '@agoric/internal/src/lib-chainStorage.js';
import { E } from '@endo/far';

/**
* @import {QueryFlowsSF as StartFn} from '@agoric/orchestration/src/fixtures/query-flows.contract.js';
*/

const contractName = 'queryFlows';
const trace = makeTracer(contractName, true);

/**
* See `@agoric/builders/builders/scripts/orchestration/init-query-flows.js` for
* the accompanying proposal builder. Run `agoric run
* packages/builders/scripts/orchestration/init-query-flows.js` to build the
* contract and proposal files.
*
* @param {BootstrapPowers & {
* installation: {
* consume: {
* queryFlows: Installation<StartFn>;
* };
* };
* }} powers
*/
export const startQueryFlows = async ({
consume: {
agoricNames,
board,
chainStorage,
chainTimerService,
cosmosInterchainService,
localchain,
startUpgradable,
},
installation: {
consume: { [contractName]: installation },
},
instance: {
// @ts-expect-error unknown instance
produce: { [contractName]: produceInstance },
},
}) => {
trace(`start ${contractName}`);

const storageNode = await makeStorageNodeChild(chainStorage, contractName);
const marshaller = await E(board).getPublishingMarshaller();

/** @type {StartUpgradableOpts<StartFn>} */
const startOpts = {
label: 'queryFlows',
installation,
terms: undefined,
privateArgs: await deeplyFulfilledObject(
harden({
agoricNames,
orchestrationService: cosmosInterchainService,
localchain,
storageNode,
marshaller,
timerService: chainTimerService,
}),
),
};

const { instance } = await E(startUpgradable)(startOpts);
produceInstance.resolve(instance);
};
harden(startQueryFlows);

export const getManifestForContract = (
{ restoreRef },
{ installKeys, ...options },
) => {
return {
manifest: {
[startQueryFlows.name]: {
consume: {
agoricNames: true,
board: true,
chainStorage: true,
chainTimerService: true,
cosmosInterchainService: true,
localchain: true,
startUpgradable: true,
},
installation: {
consume: { [contractName]: true },
},
instance: {
produce: { [contractName]: true },
},
},
},
installations: {
[contractName]: restoreRef(installKeys[contractName]),
},
options,
};
};

/** @type {import('@agoric/deploy-script-support/src/externalTypes.js').CoreEvalBuilder} */
export const defaultProposalBuilder = async ({ publishRef, install }) => {
return harden({
// Somewhat unorthodox, source the exports from this builder module
sourceSpec: '@agoric/builders/scripts/testing/start-query-flows.js',
getManifestCall: [
'getManifestForContract',
{
installKeys: {
queryFlows: publishRef(
install(
'@agoric/orchestration/src/fixtures/query-flows.contract.js',
),
),
},
},
],
});
};

export default async (homeP, endowments) => {
// import dynamically so the module can work in CoreEval environment
const dspModule = await import('@agoric/deploy-script-support');
const { makeHelpers } = dspModule;
const { writeCoreEval } = await makeHelpers(homeP, endowments);
await writeCoreEval(startQueryFlows.name, defaultProposalBuilder);
};
22 changes: 0 additions & 22 deletions packages/orchestration/src/examples/basic-flows.contract.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,6 @@ const contract = async (
M.interface('Basic Flows PF', {
makeOrchAccountInvitation: M.callWhen().returns(InvitationShape),
makePortfolioAccountInvitation: M.callWhen().returns(InvitationShape),
makeSendICQQueryInvitation: M.callWhen().returns(InvitationShape),
makeAccountAndSendBalanceQueryInvitation:
M.callWhen().returns(InvitationShape),
makeSendLocalQueryInvitation: M.callWhen().returns(InvitationShape),
}),
{
makeOrchAccountInvitation() {
Expand All @@ -58,24 +54,6 @@ const contract = async (
'Make an Orchestration Account',
);
},
makeSendICQQueryInvitation() {
return zcf.makeInvitation(
orchFns.sendICQQuery,
'Submit a query to a remote chain',
);
},
makeAccountAndSendBalanceQueryInvitation() {
return zcf.makeInvitation(
orchFns.makeAccountAndSendBalanceQuery,
'Make an account and submit a balance query',
);
},
makeSendLocalQueryInvitation() {
return zcf.makeInvitation(
orchFns.sendLocalQuery,
'Submit a query to the local chain',
);
},
},
);

Expand Down
Loading

0 comments on commit 9ced650

Please sign in to comment.