Skip to content

Commit cf1d435

Browse files
authored
test: send-anywhere pfm scenarios (#10591)
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.
2 parents ce195ab + 748883d commit cf1d435

26 files changed

+572
-118
lines changed

.github/workflows/multichain-e2e-template.yml

+4
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,10 @@ jobs:
9292
run: make override-chain-registry
9393
working-directory: ./agoric-sdk/multichain-testing
9494

95+
- name: Register Interchain Bank Assets
96+
run: make register-bank-assets
97+
working-directory: ./agoric-sdk/multichain-testing
98+
9599
- name: Run @agoric/multichain-testing E2E Tests
96100
run: yarn ${{ inputs.test_command }}
97101
working-directory: ./agoric-sdk/multichain-testing

multichain-testing/.gitignore

+3-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
!.yarn/patches/*
33
# fetched chain info from running starship
44
starship-chain-info.js
5-
# output of build script to get update running chain info
6-
revise-chain-info*
5+
# builder prefix for contract starters
76
start*
7+
# builder prefix for core evals
8+
eval-*

multichain-testing/Makefile

+6-1
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,11 @@ override-chain-registry:
7979
scripts/fetch-starship-chain-info.ts && \
8080
scripts/deploy-cli.ts src/revise-chain-info.builder.js
8181

82+
register-bank-assets:
83+
scripts/fetch-starship-chain-info.ts && \
84+
scripts/deploy-cli.ts src/register-interchain-bank-assets.builder.js \
85+
assets="$$(scripts/make-bank-asset-info.ts)"
86+
8287
ADDR=agoric1ldmtatp24qlllgxmrsjzcpe20fvlkp448zcuce
8388
COIN=1000000000uist
8489

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

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

multichain-testing/README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ make wait-for-pods
5959
make port-forward
6060

6161
# set up Agoric testing environment
62-
make fund-provision-pool override-chain-registry
62+
make fund-provision-pool override-chain-registry register-bank-assets
6363
```
6464

6565
If you get an error like "connection refused", you need to wait longer, until all the pods are Running.

multichain-testing/scripts/deploy-cli.ts

+14-3
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,28 @@ import { makeAgdTools } from '../tools/agd-tools.js';
99
import { makeDeployBuilder } from '../tools/deploy.js';
1010

1111
async function main() {
12-
const builder = process.argv[2];
12+
const [builder, ...rawArgs] = process.argv.slice(2);
13+
14+
// Parse builder options from command line arguments
15+
const builderOpts: Record<string, string> = {};
16+
for (const arg of rawArgs) {
17+
const [key, value] = arg.split('=');
18+
if (key && value) {
19+
builderOpts[key] = value;
20+
}
21+
}
1322

1423
if (!builder) {
15-
console.error('USAGE: deploy-cli.ts <builder script>');
24+
console.error(
25+
'USAGE: deploy-cli.ts <builder script> [key1=value1] [key2=value2]',
26+
);
1627
process.exit(1);
1728
}
1829

1930
try {
2031
const agdTools = await makeAgdTools(console.log, childProcess);
2132
const deployBuilder = makeDeployBuilder(agdTools, fse.readJSON, execa);
22-
await deployBuilder(builder);
33+
await deployBuilder(builder, builderOpts);
2334
} catch (err) {
2435
console.error(err);
2536
process.exit(1);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
#!/usr/bin/env -S node --import ts-blank-space/register
2+
/* eslint-env node */
3+
4+
import '@endo/init';
5+
import starshipChainInfo from '../starship-chain-info.js';
6+
import { makeAssetInfo } from '../tools/asset-info.ts';
7+
8+
const main = () => {
9+
if (!starshipChainInfo) {
10+
throw new Error(
11+
'starshipChainInfo not found. run `./scripts/fetch-starship-chain-info.ts` first.',
12+
);
13+
}
14+
15+
const assetInfo = makeAssetInfo(starshipChainInfo)
16+
.filter(
17+
([_, { chainName, baseName }]) =>
18+
chainName === 'agoric' && baseName !== 'agoric',
19+
)
20+
.map(([denom, { baseDenom }]) => ({
21+
denom,
22+
issuerName: baseDenom.replace(/^u/, '').toUpperCase(),
23+
decimalPlaces: 6, // TODO do not assume 6
24+
}));
25+
26+
// Directly output JSON string for proposal builder options
27+
process.stdout.write(JSON.stringify(assetInfo));
28+
};
29+
30+
main();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/* global harden */
2+
/// <reference types="ses" />
3+
import { makeHelpers } from '@agoric/deploy-script-support';
4+
import { parseArgs } from 'node:util';
5+
6+
/**
7+
* @import {ParseArgsConfig} from 'node:util';
8+
* @import {CoreEvalBuilder, DeployScriptFunction} from '@agoric/deploy-script-support/src/externalTypes.js';
9+
*/
10+
11+
/** @type {ParseArgsConfig['options']} */
12+
const parserOpts = {
13+
assets: { type: 'string' },
14+
};
15+
16+
/** @type {CoreEvalBuilder} */
17+
export const defaultProposalBuilder = async (_, options) => {
18+
return harden({
19+
sourceSpec:
20+
'@agoric/builders/scripts/testing/register-interchain-bank-assets.js',
21+
getManifestCall: ['getManifestCall', options],
22+
});
23+
};
24+
25+
/** @type {DeployScriptFunction} */
26+
export default async (homeP, endowments) => {
27+
const { scriptArgs } = endowments;
28+
29+
const {
30+
values: { assets },
31+
} = parseArgs({
32+
args: scriptArgs,
33+
options: parserOpts,
34+
});
35+
36+
const parseAssets = () => {
37+
if (typeof assets !== 'string') {
38+
throw Error(
39+
'must provide --assets=JSON.stringify({ denom: Denom; issuerName: string; decimalPlaces: number; }[])',
40+
);
41+
}
42+
return JSON.parse(assets);
43+
};
44+
45+
const opts = harden({ assets: parseAssets() });
46+
47+
const { writeCoreEval } = await makeHelpers(homeP, endowments);
48+
await writeCoreEval('eval-register-interchain-bank-assets', utils =>
49+
defaultProposalBuilder(utils, opts),
50+
);
51+
};

multichain-testing/src/revise-chain-info.builder.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,5 @@ export const defaultProposalBuilder = async () =>
1919
/** @type {import('@agoric/deploy-script-support/src/externalTypes.js').DeployScriptFunction} */
2020
export default async (homeP, endowments) => {
2121
const { writeCoreEval } = await makeHelpers(homeP, endowments);
22-
await writeCoreEval('revise-chain-info', defaultProposalBuilder);
22+
await writeCoreEval('eval-revise-chain-info', defaultProposalBuilder);
2323
};

multichain-testing/test/auto-stake-it.test.ts

+7-53
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
11
import anyTest from '@endo/ses-ava/prepare-endo.js';
2-
import type { ExecutionContext, TestFn } from 'ava';
2+
import type { TestFn } from 'ava';
33
import starshipChainInfo from '../starship-chain-info.js';
44
import { makeDoOffer } from '../tools/e2e-tools.js';
5-
import {
6-
createFundedWalletAndClient,
7-
makeIBCTransferMsg,
8-
} from '../tools/ibc-transfer.js';
5+
import { makeFundAndTransfer } from '../tools/ibc-transfer.js';
96
import { makeQueryClient } from '../tools/query.js';
107
import type { SetupContextWithWallets } from './support.js';
118
import { chainConfig, commonSetup } from './support.js';
@@ -37,53 +34,6 @@ test.after(async t => {
3734
deleteTestKeys(accounts);
3835
});
3936

40-
const makeFundAndTransfer = (t: ExecutionContext<SetupContextWithWallets>) => {
41-
const { retryUntilCondition, useChain } = t.context;
42-
return async (chainName: string, agoricAddr: string, amount = 100n) => {
43-
const { staking } = useChain(chainName).chainInfo.chain;
44-
const denom = staking?.staking_tokens?.[0].denom;
45-
if (!denom) throw Error(`no denom for ${chainName}`);
46-
47-
const { client, address, wallet } = await createFundedWalletAndClient(
48-
t,
49-
chainName,
50-
useChain,
51-
);
52-
const balancesResult = await retryUntilCondition(
53-
() => client.getAllBalances(address),
54-
coins => !!coins?.length,
55-
`Faucet balances found for ${address}`,
56-
);
57-
58-
console.log('Balances:', balancesResult);
59-
60-
const transferArgs = makeIBCTransferMsg(
61-
{ denom, value: amount },
62-
{ address: agoricAddr, chainName: 'agoric' },
63-
{ address: address, chainName },
64-
Date.now(),
65-
useChain,
66-
);
67-
console.log('Transfer Args:', transferArgs);
68-
// TODO #9200 `sendIbcTokens` does not support `memo`
69-
// @ts-expect-error spread argument for concise code
70-
const txRes = await client.sendIbcTokens(...transferArgs);
71-
if (txRes && txRes.code !== 0) {
72-
console.error(txRes);
73-
throw Error(`failed to ibc transfer funds to ${chainName}`);
74-
}
75-
const { events: _events, ...txRest } = txRes;
76-
console.log(txRest);
77-
t.is(txRes.code, 0, `Transaction succeeded`);
78-
t.log(`Funds transferred to ${agoricAddr}`);
79-
return {
80-
client,
81-
address,
82-
wallet,
83-
};
84-
};
85-
};
86-
8737
const autoStakeItScenario = test.macro({
8838
title: (_, chainName: string) => `auto-stake-it on ${chainName}`,
8939
exec: async (t, chainName: string) => {
@@ -96,7 +46,11 @@ const autoStakeItScenario = test.macro({
9646
useChain,
9747
} = t.context;
9848

99-
const fundAndTransfer = makeFundAndTransfer(t);
49+
const fundAndTransfer = makeFundAndTransfer(
50+
t,
51+
retryUntilCondition,
52+
useChain,
53+
);
10054

10155
// 2. Find 'stakingDenom' denom on agoric
10256
const remoteChainInfo = starshipChainInfo[chainName];
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import test from 'ava';
2+
import { execFileSync } from 'node:child_process';
3+
4+
test('make-bank-asset-info', async t => {
5+
const stdout = execFileSync('./scripts/make-bank-asset-info.ts', {
6+
encoding: 'utf8',
7+
});
8+
9+
const assetInfo = JSON.parse(stdout);
10+
11+
t.like(assetInfo, [
12+
{
13+
issuerName: 'ATOM',
14+
decimalPlaces: 6,
15+
},
16+
{
17+
issuerName: 'OSMO',
18+
decimalPlaces: 6,
19+
},
20+
{
21+
issuerName: 'ION',
22+
decimalPlaces: 6,
23+
},
24+
]);
25+
26+
for (const { denom } of assetInfo) {
27+
t.regex(denom, /^ibc\//);
28+
t.is(denom.length, 68);
29+
}
30+
});

multichain-testing/test/send-anywhere.test.ts

+29-21
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ const contractBuilder =
2121

2222
test.before(async t => {
2323
const { setupTestKeys, ...common } = await commonSetup(t);
24-
const { assetInfo, chainInfo, deleteTestKeys, startContract } = common;
24+
const { assetInfo, chainInfo, deleteTestKeys, faucetTools, startContract } =
25+
common;
2526
deleteTestKeys(accounts).catch();
2627
const wallets = await setupTestKeys(accounts);
2728
t.context = { ...common, wallets };
@@ -30,19 +31,26 @@ test.before(async t => {
3031
chainInfo,
3132
assetInfo,
3233
});
34+
35+
await faucetTools.fundFaucet([
36+
['cosmoshub', 'uatom'],
37+
['osmosis', 'uosmo'],
38+
]);
3339
});
3440

3541
test.after(async t => {
3642
const { deleteTestKeys } = t.context;
3743
deleteTestKeys(accounts);
3844
});
3945

46+
type BrandKW = 'IST' | 'OSMO' | 'ATOM';
47+
4048
const sendAnywhereScenario = test.macro({
41-
title: (_, chainName: string, acctIdx: number) =>
42-
`send-anywhere ${chainName}${acctIdx}`,
43-
exec: async (t, chainName: string, acctIdx: number) => {
44-
const config = chainConfig[chainName];
45-
if (!config) return t.fail(`Unknown chain: ${chainName}`);
49+
title: (_, destChainName: string, acctIdx: number, brandKw: BrandKW) =>
50+
`send-anywhere ${brandKw} from agoric to ${destChainName}${acctIdx}`,
51+
exec: async (t, destChainName: string, acctIdx: number, brandKw: BrandKW) => {
52+
const config = chainConfig[destChainName];
53+
if (!config) return t.fail(`Unknown chain: ${destChainName}`);
4654

4755
const {
4856
wallets,
@@ -53,13 +61,13 @@ const sendAnywhereScenario = test.macro({
5361
} = t.context;
5462

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

5866
t.log('Create an agoric smart wallet to initiate send-anywhere transfer');
59-
const agoricAddr = wallets[`${chainName}${acctIdx}`];
67+
const agoricAddr = wallets[`${destChainName}${acctIdx}`];
6068
const wdUser1 = await provisionSmartWallet(agoricAddr, {
61-
BLD: 100_000n,
62-
IST: 100_000n,
69+
BLD: 1_000n,
70+
[brandKw]: 1_000n,
6371
});
6472
t.log(`provisioning agoric smart wallet for ${agoricAddr}`);
6573

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

73-
const apiUrl = await useChain(chainName).getRestEndpoint();
81+
const apiUrl = await useChain(destChainName).getRestEndpoint();
7482
const queryClient = makeQueryClient(apiUrl);
75-
t.log(`Made ${chainName} query client`);
83+
t.log(`Made ${destChainName} query client`);
7684

7785
const doSendAnywhere = async (amount: Amount) => {
7886
t.log(`Sending ${amount.value} ${amount.brand}.`);
@@ -83,16 +91,16 @@ const sendAnywhereScenario = test.macro({
8391
encoding: 'bech32',
8492
};
8593
t.log('Will send payment to:', receiver);
86-
t.log(`${chainName} offer`);
87-
const offerId = `${chainName}-makeSendInvitation-${Date.now()}`;
94+
t.log(`${destChainName} offer`);
95+
const offerId = `${destChainName}-makeSendInvitation-${Date.now()}`;
8896
await doOffer({
8997
id: offerId,
9098
invitationSpec: {
9199
source: 'agoricContract',
92100
instancePath: [contractName],
93101
callPipe: [['makeSendInvitation']],
94102
},
95-
offerArgs: { destAddr: receiver.value, chainName },
103+
offerArgs: { destAddr: receiver.value, chainName: destChainName },
96104
proposal: { give: { Send: amount } },
97105
});
98106

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

125133
for (const value of offerAmounts) {
126-
await doSendAnywhere(AmountMath.make(istBrand, value));
134+
await doSendAnywhere(AmountMath.make(brand, value));
127135
}
128136
},
129137
});
130138

131-
test.serial(sendAnywhereScenario, 'osmosis', 1);
132-
test.serial(sendAnywhereScenario, 'osmosis', 2);
133-
test.serial(sendAnywhereScenario, 'cosmoshub', 1);
134-
test.serial(sendAnywhereScenario, 'cosmoshub', 2);
139+
test.serial(sendAnywhereScenario, 'osmosis', 1, 'IST');
140+
test.serial(sendAnywhereScenario, 'osmosis', 2, 'ATOM'); // exercises PFM (agoric -> cosmoshub -> osmosis)
141+
test.serial(sendAnywhereScenario, 'cosmoshub', 1, 'IST');
142+
test.serial(sendAnywhereScenario, 'cosmoshub', 2, 'OSMO'); // exercises PFM (agoric -> osmosis -> cosmoshub)

0 commit comments

Comments
 (0)