Skip to content

Commit

Permalink
test: send-anywhere pfm scenarios (#10591)
Browse files Browse the repository at this point in the history
closes: #9966
closes: #10445

## Description
Adds multi-hop (PFM) scenarios to the `examples/send-anywhere.contract.js` multichain (e2e) test.  To support this change, this PR also includes:
  - a proposal for registering interchain assets in vbank (closes #9966). aims for production quality but is only used in tests 
  - a `fundFaucet` helper in `multichain-testing` so developers can request ATOM, OSMO, etc in `provisionSmartWallet`
  - a `GoDuration` type in `@agoric/orchestration` that captures basic Go [time duration strings](https://pkg.go.dev/time#ParseDuration) and an update to `DefaultPfmTimeoutOpts` (10min -> 10m)


### Security Considerations

`@agoric/builders/scripts/testing/register-interchain-bank-assets.js` allows callers overwrite assets in `vbank` and `agoricNames`. It's only intended for testing, and shouldn't be used in production. A production version might guard against accidental overrides.

### Scaling Considerations
None, mostly test code. Adds a little CI time to `multichain-testing` for the extra CoreEval.

### Documentation Considerations
None

### Testing Considerations
Includes an E2E to test in `multichain-testing` that leverages `register-interchain-bank-assets.js`. Also includes the first E2E test of PFM functionality added in #10584 and #10571.

### Upgrade Considerations
None, library code an NPM orch or FUSDC release.
  • Loading branch information
mergify[bot] authored Dec 4, 2024
2 parents ce195ab + 748883d commit cf1d435
Show file tree
Hide file tree
Showing 26 changed files with 572 additions and 118 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/multichain-e2e-template.yml
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,10 @@ jobs:
run: make override-chain-registry
working-directory: ./agoric-sdk/multichain-testing

- name: Register Interchain Bank Assets
run: make register-bank-assets
working-directory: ./agoric-sdk/multichain-testing

- name: Run @agoric/multichain-testing E2E Tests
run: yarn ${{ inputs.test_command }}
working-directory: ./agoric-sdk/multichain-testing
Expand Down
5 changes: 3 additions & 2 deletions multichain-testing/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
!.yarn/patches/*
# fetched chain info from running starship
starship-chain-info.js
# output of build script to get update running chain info
revise-chain-info*
# builder prefix for contract starters
start*
# builder prefix for core evals
eval-*
7 changes: 6 additions & 1 deletion multichain-testing/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,11 @@ override-chain-registry:
scripts/fetch-starship-chain-info.ts && \
scripts/deploy-cli.ts src/revise-chain-info.builder.js

register-bank-assets:
scripts/fetch-starship-chain-info.ts && \
scripts/deploy-cli.ts src/register-interchain-bank-assets.builder.js \
assets="$$(scripts/make-bank-asset-info.ts)"

ADDR=agoric1ldmtatp24qlllgxmrsjzcpe20fvlkp448zcuce
COIN=1000000000uist

Expand All @@ -101,5 +106,5 @@ wait-for-pods:
scripts/pod-readiness.ts

.PHONY: start
start: install wait-for-pods port-forward fund-provision-pool override-chain-registry
start: install wait-for-pods port-forward fund-provision-pool override-chain-registry register-bank-assets

2 changes: 1 addition & 1 deletion multichain-testing/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ make wait-for-pods
make port-forward

# set up Agoric testing environment
make fund-provision-pool override-chain-registry
make fund-provision-pool override-chain-registry register-bank-assets
```

If you get an error like "connection refused", you need to wait longer, until all the pods are Running.
Expand Down
17 changes: 14 additions & 3 deletions multichain-testing/scripts/deploy-cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,28 @@ import { makeAgdTools } from '../tools/agd-tools.js';
import { makeDeployBuilder } from '../tools/deploy.js';

async function main() {
const builder = process.argv[2];
const [builder, ...rawArgs] = process.argv.slice(2);

// Parse builder options from command line arguments
const builderOpts: Record<string, string> = {};
for (const arg of rawArgs) {
const [key, value] = arg.split('=');
if (key && value) {
builderOpts[key] = value;
}
}

if (!builder) {
console.error('USAGE: deploy-cli.ts <builder script>');
console.error(
'USAGE: deploy-cli.ts <builder script> [key1=value1] [key2=value2]',
);
process.exit(1);
}

try {
const agdTools = await makeAgdTools(console.log, childProcess);
const deployBuilder = makeDeployBuilder(agdTools, fse.readJSON, execa);
await deployBuilder(builder);
await deployBuilder(builder, builderOpts);
} catch (err) {
console.error(err);
process.exit(1);
Expand Down
30 changes: 30 additions & 0 deletions multichain-testing/scripts/make-bank-asset-info.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#!/usr/bin/env -S node --import ts-blank-space/register
/* eslint-env node */

import '@endo/init';
import starshipChainInfo from '../starship-chain-info.js';
import { makeAssetInfo } from '../tools/asset-info.ts';

const main = () => {
if (!starshipChainInfo) {
throw new Error(
'starshipChainInfo not found. run `./scripts/fetch-starship-chain-info.ts` first.',
);
}

const assetInfo = makeAssetInfo(starshipChainInfo)
.filter(
([_, { chainName, baseName }]) =>
chainName === 'agoric' && baseName !== 'agoric',
)
.map(([denom, { baseDenom }]) => ({
denom,
issuerName: baseDenom.replace(/^u/, '').toUpperCase(),
decimalPlaces: 6, // TODO do not assume 6
}));

// Directly output JSON string for proposal builder options
process.stdout.write(JSON.stringify(assetInfo));
};

main();
51 changes: 51 additions & 0 deletions multichain-testing/src/register-interchain-bank-assets.builder.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/* global harden */
/// <reference types="ses" />
import { makeHelpers } from '@agoric/deploy-script-support';
import { parseArgs } from 'node:util';

/**
* @import {ParseArgsConfig} from 'node:util';
* @import {CoreEvalBuilder, DeployScriptFunction} from '@agoric/deploy-script-support/src/externalTypes.js';
*/

/** @type {ParseArgsConfig['options']} */
const parserOpts = {
assets: { type: 'string' },
};

/** @type {CoreEvalBuilder} */
export const defaultProposalBuilder = async (_, options) => {
return harden({
sourceSpec:
'@agoric/builders/scripts/testing/register-interchain-bank-assets.js',
getManifestCall: ['getManifestCall', options],
});
};

/** @type {DeployScriptFunction} */
export default async (homeP, endowments) => {
const { scriptArgs } = endowments;

const {
values: { assets },
} = parseArgs({
args: scriptArgs,
options: parserOpts,
});

const parseAssets = () => {
if (typeof assets !== 'string') {
throw Error(
'must provide --assets=JSON.stringify({ denom: Denom; issuerName: string; decimalPlaces: number; }[])',
);
}
return JSON.parse(assets);
};

const opts = harden({ assets: parseAssets() });

const { writeCoreEval } = await makeHelpers(homeP, endowments);
await writeCoreEval('eval-register-interchain-bank-assets', utils =>
defaultProposalBuilder(utils, opts),
);
};
2 changes: 1 addition & 1 deletion multichain-testing/src/revise-chain-info.builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,5 @@ export const defaultProposalBuilder = async () =>
/** @type {import('@agoric/deploy-script-support/src/externalTypes.js').DeployScriptFunction} */
export default async (homeP, endowments) => {
const { writeCoreEval } = await makeHelpers(homeP, endowments);
await writeCoreEval('revise-chain-info', defaultProposalBuilder);
await writeCoreEval('eval-revise-chain-info', defaultProposalBuilder);
};
60 changes: 7 additions & 53 deletions multichain-testing/test/auto-stake-it.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
import anyTest from '@endo/ses-ava/prepare-endo.js';
import type { ExecutionContext, TestFn } from 'ava';
import type { TestFn } from 'ava';
import starshipChainInfo from '../starship-chain-info.js';
import { makeDoOffer } from '../tools/e2e-tools.js';
import {
createFundedWalletAndClient,
makeIBCTransferMsg,
} from '../tools/ibc-transfer.js';
import { makeFundAndTransfer } from '../tools/ibc-transfer.js';
import { makeQueryClient } from '../tools/query.js';
import type { SetupContextWithWallets } from './support.js';
import { chainConfig, commonSetup } from './support.js';
Expand Down Expand Up @@ -37,53 +34,6 @@ test.after(async t => {
deleteTestKeys(accounts);
});

const makeFundAndTransfer = (t: ExecutionContext<SetupContextWithWallets>) => {
const { retryUntilCondition, useChain } = t.context;
return async (chainName: string, agoricAddr: string, amount = 100n) => {
const { staking } = useChain(chainName).chainInfo.chain;
const denom = staking?.staking_tokens?.[0].denom;
if (!denom) throw Error(`no denom for ${chainName}`);

const { client, address, wallet } = await createFundedWalletAndClient(
t,
chainName,
useChain,
);
const balancesResult = await retryUntilCondition(
() => client.getAllBalances(address),
coins => !!coins?.length,
`Faucet balances found for ${address}`,
);

console.log('Balances:', balancesResult);

const transferArgs = makeIBCTransferMsg(
{ denom, value: amount },
{ address: agoricAddr, chainName: 'agoric' },
{ address: address, chainName },
Date.now(),
useChain,
);
console.log('Transfer Args:', transferArgs);
// TODO #9200 `sendIbcTokens` does not support `memo`
// @ts-expect-error spread argument for concise code
const txRes = await client.sendIbcTokens(...transferArgs);
if (txRes && txRes.code !== 0) {
console.error(txRes);
throw Error(`failed to ibc transfer funds to ${chainName}`);
}
const { events: _events, ...txRest } = txRes;
console.log(txRest);
t.is(txRes.code, 0, `Transaction succeeded`);
t.log(`Funds transferred to ${agoricAddr}`);
return {
client,
address,
wallet,
};
};
};

const autoStakeItScenario = test.macro({
title: (_, chainName: string) => `auto-stake-it on ${chainName}`,
exec: async (t, chainName: string) => {
Expand All @@ -96,7 +46,11 @@ const autoStakeItScenario = test.macro({
useChain,
} = t.context;

const fundAndTransfer = makeFundAndTransfer(t);
const fundAndTransfer = makeFundAndTransfer(
t,
retryUntilCondition,
useChain,
);

// 2. Find 'stakingDenom' denom on agoric
const remoteChainInfo = starshipChainInfo[chainName];
Expand Down
30 changes: 30 additions & 0 deletions multichain-testing/test/scripts/make-bank-asset-info.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import test from 'ava';
import { execFileSync } from 'node:child_process';

test('make-bank-asset-info', async t => {
const stdout = execFileSync('./scripts/make-bank-asset-info.ts', {
encoding: 'utf8',
});

const assetInfo = JSON.parse(stdout);

t.like(assetInfo, [
{
issuerName: 'ATOM',
decimalPlaces: 6,
},
{
issuerName: 'OSMO',
decimalPlaces: 6,
},
{
issuerName: 'ION',
decimalPlaces: 6,
},
]);

for (const { denom } of assetInfo) {
t.regex(denom, /^ibc\//);
t.is(denom.length, 68);
}
});
50 changes: 29 additions & 21 deletions multichain-testing/test/send-anywhere.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ const contractBuilder =

test.before(async t => {
const { setupTestKeys, ...common } = await commonSetup(t);
const { assetInfo, chainInfo, deleteTestKeys, startContract } = common;
const { assetInfo, chainInfo, deleteTestKeys, faucetTools, startContract } =
common;
deleteTestKeys(accounts).catch();
const wallets = await setupTestKeys(accounts);
t.context = { ...common, wallets };
Expand All @@ -30,19 +31,26 @@ test.before(async t => {
chainInfo,
assetInfo,
});

await faucetTools.fundFaucet([
['cosmoshub', 'uatom'],
['osmosis', 'uosmo'],
]);
});

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

type BrandKW = 'IST' | 'OSMO' | 'ATOM';

const sendAnywhereScenario = test.macro({
title: (_, chainName: string, acctIdx: number) =>
`send-anywhere ${chainName}${acctIdx}`,
exec: async (t, chainName: string, acctIdx: number) => {
const config = chainConfig[chainName];
if (!config) return t.fail(`Unknown chain: ${chainName}`);
title: (_, destChainName: string, acctIdx: number, brandKw: BrandKW) =>
`send-anywhere ${brandKw} from agoric to ${destChainName}${acctIdx}`,
exec: async (t, destChainName: string, acctIdx: number, brandKw: BrandKW) => {
const config = chainConfig[destChainName];
if (!config) return t.fail(`Unknown chain: ${destChainName}`);

const {
wallets,
Expand All @@ -53,13 +61,13 @@ const sendAnywhereScenario = test.macro({
} = t.context;

t.log('Create a receiving wallet for the send-anywhere transfer');
const chain = useChain(chainName).chain;
const chain = useChain(destChainName).chain;

t.log('Create an agoric smart wallet to initiate send-anywhere transfer');
const agoricAddr = wallets[`${chainName}${acctIdx}`];
const agoricAddr = wallets[`${destChainName}${acctIdx}`];
const wdUser1 = await provisionSmartWallet(agoricAddr, {
BLD: 100_000n,
IST: 100_000n,
BLD: 1_000n,
[brandKw]: 1_000n,
});
t.log(`provisioning agoric smart wallet for ${agoricAddr}`);

Expand All @@ -68,11 +76,11 @@ const sendAnywhereScenario = test.macro({
const brands = await vstorageClient.queryData(
'published.agoricNames.brand',
);
const istBrand = Object.fromEntries(brands).IST;
const brand = Object.fromEntries(brands)[brandKw];

const apiUrl = await useChain(chainName).getRestEndpoint();
const apiUrl = await useChain(destChainName).getRestEndpoint();
const queryClient = makeQueryClient(apiUrl);
t.log(`Made ${chainName} query client`);
t.log(`Made ${destChainName} query client`);

const doSendAnywhere = async (amount: Amount) => {
t.log(`Sending ${amount.value} ${amount.brand}.`);
Expand All @@ -83,16 +91,16 @@ const sendAnywhereScenario = test.macro({
encoding: 'bech32',
};
t.log('Will send payment to:', receiver);
t.log(`${chainName} offer`);
const offerId = `${chainName}-makeSendInvitation-${Date.now()}`;
t.log(`${destChainName} offer`);
const offerId = `${destChainName}-makeSendInvitation-${Date.now()}`;
await doOffer({
id: offerId,
invitationSpec: {
source: 'agoricContract',
instancePath: [contractName],
callPipe: [['makeSendInvitation']],
},
offerArgs: { destAddr: receiver.value, chainName },
offerArgs: { destAddr: receiver.value, chainName: destChainName },
proposal: { give: { Send: amount } },
});

Expand Down Expand Up @@ -123,12 +131,12 @@ const sendAnywhereScenario = test.macro({
console.log(`${agoricAddr} offer amounts:`, offerAmounts);

for (const value of offerAmounts) {
await doSendAnywhere(AmountMath.make(istBrand, value));
await doSendAnywhere(AmountMath.make(brand, value));
}
},
});

test.serial(sendAnywhereScenario, 'osmosis', 1);
test.serial(sendAnywhereScenario, 'osmosis', 2);
test.serial(sendAnywhereScenario, 'cosmoshub', 1);
test.serial(sendAnywhereScenario, 'cosmoshub', 2);
test.serial(sendAnywhereScenario, 'osmosis', 1, 'IST');
test.serial(sendAnywhereScenario, 'osmosis', 2, 'ATOM'); // exercises PFM (agoric -> cosmoshub -> osmosis)
test.serial(sendAnywhereScenario, 'cosmoshub', 1, 'IST');
test.serial(sendAnywhereScenario, 'cosmoshub', 2, 'OSMO'); // exercises PFM (agoric -> osmosis -> cosmoshub)
Loading

0 comments on commit cf1d435

Please sign in to comment.