From 0e2070754d6811acd40cb026792d4295189ae771 Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Sat, 30 Nov 2024 01:45:27 -0500 Subject: [PATCH 1/4] feat: register interchain bank assets proposal - create `register-interchain-bank-assets.js` for multichain-testing environment - allow users to submit offers using new brands like OSMO, ION - override existing brands like ATOM (ibc/toyatom) with starship denom - create `make-bank-asset-info.ts` to gather `InterchainAssetOptions` from starship env - update `make start` and ci `multichain-e2e-template.yaml` to call these scripts - deploy-cli.ts `deployBuilder` accepts `builderOpts` --- .github/workflows/multichain-e2e-template.yml | 4 + multichain-testing/.gitignore | 5 +- multichain-testing/Makefile | 7 +- multichain-testing/README.md | 2 +- multichain-testing/scripts/deploy-cli.ts | 17 +- .../scripts/make-bank-asset-info.ts | 30 +++ ...register-interchain-bank-assets.builder.js | 51 +++++ .../src/revise-chain-info.builder.js | 2 +- .../test/scripts/make-bank-asset-info.test.ts | 30 +++ .../test/tools/asset-info.test.ts | 2 + multichain-testing/tools/asset-info.ts | 29 +-- multichain-testing/tools/e2e-tools.js | 2 +- multichain-testing/tsconfig.json | 1 + .../register-interchain-bank-assets.js | 199 ++++++++++++++++++ 14 files changed, 359 insertions(+), 22 deletions(-) create mode 100755 multichain-testing/scripts/make-bank-asset-info.ts create mode 100644 multichain-testing/src/register-interchain-bank-assets.builder.js create mode 100644 multichain-testing/test/scripts/make-bank-asset-info.test.ts create mode 100644 packages/builders/scripts/testing/register-interchain-bank-assets.js diff --git a/.github/workflows/multichain-e2e-template.yml b/.github/workflows/multichain-e2e-template.yml index 58c7ee81013..58455e3b9b4 100644 --- a/.github/workflows/multichain-e2e-template.yml +++ b/.github/workflows/multichain-e2e-template.yml @@ -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 diff --git a/multichain-testing/.gitignore b/multichain-testing/.gitignore index bd550b4d526..a59e6798973 100644 --- a/multichain-testing/.gitignore +++ b/multichain-testing/.gitignore @@ -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-* diff --git a/multichain-testing/Makefile b/multichain-testing/Makefile index 5301808e631..0f2bd61450d 100644 --- a/multichain-testing/Makefile +++ b/multichain-testing/Makefile @@ -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 @@ -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 diff --git a/multichain-testing/README.md b/multichain-testing/README.md index cb090092cde..5a43a2aff24 100644 --- a/multichain-testing/README.md +++ b/multichain-testing/README.md @@ -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. diff --git a/multichain-testing/scripts/deploy-cli.ts b/multichain-testing/scripts/deploy-cli.ts index 2dfd7b94d06..65e2320da71 100755 --- a/multichain-testing/scripts/deploy-cli.ts +++ b/multichain-testing/scripts/deploy-cli.ts @@ -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 = {}; + for (const arg of rawArgs) { + const [key, value] = arg.split('='); + if (key && value) { + builderOpts[key] = value; + } + } if (!builder) { - console.error('USAGE: deploy-cli.ts '); + console.error( + 'USAGE: deploy-cli.ts [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); diff --git a/multichain-testing/scripts/make-bank-asset-info.ts b/multichain-testing/scripts/make-bank-asset-info.ts new file mode 100755 index 00000000000..257a81986b1 --- /dev/null +++ b/multichain-testing/scripts/make-bank-asset-info.ts @@ -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(); diff --git a/multichain-testing/src/register-interchain-bank-assets.builder.js b/multichain-testing/src/register-interchain-bank-assets.builder.js new file mode 100644 index 00000000000..0417c2b92d5 --- /dev/null +++ b/multichain-testing/src/register-interchain-bank-assets.builder.js @@ -0,0 +1,51 @@ +/* global harden */ +/// +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), + ); +}; diff --git a/multichain-testing/src/revise-chain-info.builder.js b/multichain-testing/src/revise-chain-info.builder.js index 10673129466..1269350a491 100644 --- a/multichain-testing/src/revise-chain-info.builder.js +++ b/multichain-testing/src/revise-chain-info.builder.js @@ -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); }; diff --git a/multichain-testing/test/scripts/make-bank-asset-info.test.ts b/multichain-testing/test/scripts/make-bank-asset-info.test.ts new file mode 100644 index 00000000000..d04adf4dbe8 --- /dev/null +++ b/multichain-testing/test/scripts/make-bank-asset-info.test.ts @@ -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); + } +}); diff --git a/multichain-testing/test/tools/asset-info.test.ts b/multichain-testing/test/tools/asset-info.test.ts index be054b65180..1806a3c4e26 100644 --- a/multichain-testing/test/tools/asset-info.test.ts +++ b/multichain-testing/test/tools/asset-info.test.ts @@ -106,6 +106,7 @@ test('makeAssetInfo', async t => { { baseDenom: 'uosmo', baseName: 'osmosis', + brandKey: 'OSMO', chainName: 'agoric', }, ], @@ -138,6 +139,7 @@ test('makeAssetInfo', async t => { { baseDenom: 'uatom', baseName: 'cosmoshub', + brandKey: 'ATOM', chainName: 'agoric', }, ], diff --git a/multichain-testing/tools/asset-info.ts b/multichain-testing/tools/asset-info.ts index f3e548d1968..ef4faf1d5ac 100644 --- a/multichain-testing/tools/asset-info.ts +++ b/multichain-testing/tools/asset-info.ts @@ -6,7 +6,12 @@ import { } from '@agoric/orchestration'; import type { IBCChannelID } from '@agoric/vats'; -/** make asset info for current env */ +/** + * Make asset info for the current environment. + * + * until #10580, the contract's `issuerKeywordRecord` must include 'ATOM', + * 'OSMO', 'IST', etc. for the local `chainHub` to know about brands. + */ export const makeAssetInfo = ( chainInfo: Record, tokenMap: Record = { @@ -37,13 +42,6 @@ export const makeAssetInfo = ( return `ibc/${denomHash({ denom, channelId })}`; }; - // `brandKey` instead of `brand` until #10580 - // only BLD, IST until #9966 - const BRAND_KEY_MAP: Record = { - ubld: 'BLD', - uist: 'IST', - }; - // only include chains present in `chainInfo` const tokens = Object.entries(tokenMap) .filter(([chain]) => chain in chainInfo) @@ -62,7 +60,11 @@ export const makeAssetInfo = ( { ...baseDetails, chainName: chain, - ...(BRAND_KEY_MAP[denom] && { brandKey: BRAND_KEY_MAP[denom] }), + ...(chain === 'agoric' && { + // `brandKey` instead of `brand` until #10580 + // assumes issuerKeywordRecord includes brand keywords like `IST`, `OSMO` + brandKey: denom.replace(/^u/, '').toUpperCase(), + }), }, ]); @@ -70,14 +72,15 @@ export const makeAssetInfo = ( const issuingChainId = chainInfo[chain].chainId; for (const holdingChain of Object.keys(chainInfo)) { if (holdingChain === chain) continue; - const denomHash = toDenomHash(denom, issuingChainId, holdingChain); assetInfo.push([ - denomHash, + toDenomHash(denom, issuingChainId, holdingChain), { ...baseDetails, chainName: holdingChain, - ...(BRAND_KEY_MAP[denomHash] && { - brandKey: BRAND_KEY_MAP[denomHash], + ...(holdingChain === 'agoric' && { + // `brandKey` instead of `brand` until #10580 + // assumes issuerKeywordRecord includes brand keywords like `IST`, `OSMO` + brandKey: denom.replace(/^u/, '').toUpperCase(), }), }, ]); diff --git a/multichain-testing/tools/e2e-tools.js b/multichain-testing/tools/e2e-tools.js index a3c4ce4207c..51583c006b8 100644 --- a/multichain-testing/tools/e2e-tools.js +++ b/multichain-testing/tools/e2e-tools.js @@ -169,7 +169,7 @@ export const provisionSmartWallet = async ( for await (const [name, qty] of Object.entries(balances)) { const info = byName[name]; if (!info) { - throw Error(name); + throw Error(`${name} not found in vbank assets`); } const { denom, displayInfo } = info; const { decimalPlaces } = displayInfo; diff --git a/multichain-testing/tsconfig.json b/multichain-testing/tsconfig.json index d08285caec4..8915139d881 100644 --- a/multichain-testing/tsconfig.json +++ b/multichain-testing/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "../tsconfig.json", "include": [ + "src", "tools", "test" ], diff --git a/packages/builders/scripts/testing/register-interchain-bank-assets.js b/packages/builders/scripts/testing/register-interchain-bank-assets.js new file mode 100644 index 00000000000..dc99315c52b --- /dev/null +++ b/packages/builders/scripts/testing/register-interchain-bank-assets.js @@ -0,0 +1,199 @@ +/** + * @file register-interchain-bank-assets.js Core Eval + * + * Used to populate vbank in testing environments. + */ +import { AssetKind } from '@agoric/ertp'; +import { makeTracer } from '@agoric/internal'; +import { E } from '@endo/far'; +import { makeMarshal } from '@endo/marshal'; + +const { Fail } = assert; + +const trace = makeTracer('RegisterInterchainBankAssets', true); + +/** @import {Board} from '@agoric/vats'; */ + +/** + * @typedef {object} InterchainAssetOptions + * @property {string} denom + * @property {number} decimalPlaces + * @property {string} issuerName + * @property {string} keyword - defaults to `issuerName` if not provided + * @property {string} [proposedName] - defaults to `issuerName` if not provided + */ + +// vstorage paths under published.* +const BOARD_AUX = 'boardAux'; + +const marshalData = makeMarshal(_val => Fail`data only`); + +/** + * Make a storage node for auxiliary data for a value on the board. + * + * @param {ERef} chainStorage + * @param {string} boardId + */ +const makeBoardAuxNode = async (chainStorage, boardId) => { + const boardAux = E(chainStorage).makeChildNode(BOARD_AUX); + return E(boardAux).makeChildNode(boardId); +}; + +/** + * see `publishAgoricBrandsDisplayInfo` {@link @agoric/smart-wallet/proposals/upgrade-walletFactory-proposal.js} + * + * @param {ERef} chainStorage + * @param {ERef} board + * @param {Brand<'nat'>} brand + */ +const publishBrandInfo = async (chainStorage, board, brand) => { + const [boardId, displayInfo, allegedName] = await Promise.all([ + E(board).getId(brand), + E(brand).getDisplayInfo(), + E(brand).getAllegedName(), + ]); + const node = makeBoardAuxNode(chainStorage, boardId); + const aux = marshalData.toCapData(harden({ allegedName, displayInfo })); + await E(node).setValue(JSON.stringify(aux)); +}; + +/** + * @param {BootstrapPowers} powers + * @param {object} config + * @param {object} config.options + * @param {InterchainAssetOptions[]} config.options.assets + */ +export const publishInterchainAssets = async ( + { + consume: { + agoricNamesAdmin, + bankManager, + board, + chainStorage, + startUpgradable, + }, + brand: { produce: produceBrands }, + issuer: { produce: produceIssuers }, + installation: { + consume: { mintHolder }, + }, + }, + { options: { assets } }, +) => { + trace(`${publishInterchainAssets.name} starting...`); + trace(assets); + await null; + for (const interchainAssetOptions of assets) { + const { + denom, + decimalPlaces, + issuerName, + keyword = issuerName, + proposedName = issuerName, + } = interchainAssetOptions; + + trace('interchainAssetOptions', { + denom, + decimalPlaces, + issuerName, + keyword, + proposedName, + }); + + assert.typeof(denom, 'string'); + assert.typeof(decimalPlaces, 'number'); + assert.typeof(keyword, 'string'); + assert.typeof(issuerName, 'string'); + assert.typeof(proposedName, 'string'); + + const terms = { + keyword: issuerName, // "keyword" is a misnomer in mintHolder terms + assetKind: AssetKind.NAT, + displayInfo: { + decimalPlaces, + assetKind: AssetKind.NAT, + }, + }; + const { creatorFacet: mint, publicFacet: issuer } = await E( + startUpgradable, + )({ + installation: mintHolder, + label: issuerName, + privateArgs: undefined, + terms, + }); + + const brand = await E(issuer).getBrand(); + const kit = /** @type {IssuerKit<'nat'>} */ ({ mint, issuer, brand }); + + /** + * `addAssetToVault.js` will register the issuer with the `reserveKit`, + * but we don't need to do that here. + */ + + await Promise.all([ + E(E(agoricNamesAdmin).lookupAdmin('issuer')).update(issuerName, issuer), + E(E(agoricNamesAdmin).lookupAdmin('brand')).update(issuerName, brand), + // triggers benign UnhandledPromiseRejection 'Error: keyword "ATOM" must + // be unique' in provisionPool in testing environments + E(bankManager).addAsset(denom, issuerName, proposedName, kit), + ]); + + // publish brands and issuers to Bootstrap space for use in proposals + produceBrands[keyword].reset(); + produceIssuers[keyword].reset(); + produceBrands[keyword].resolve(brand); + produceIssuers[keyword].resolve(issuer); + + // publish brand info / boardAux for offer legibility + await publishBrandInfo( + // @ts-expect-error 'Promise' is not assignable to + // parameter of type 'ERef' + chainStorage, + board, + brand, + ); + } + trace(`${publishInterchainAssets.name} complete`); +}; + +/** + * @param {unknown} _powers + * @param {{ assets: InterchainAssetOptions[] }} options + */ +export const getManifestCall = (_powers, options) => { + /** @type {Record} */ + const IssuerKws = options.assets.reduce( + /** + * @param {Record} acc + * @param {InterchainAssetOptions} assetOptions + */ + (acc, { issuerName }) => Object.assign(acc, { [issuerName]: true }), + {}, + ); + harden(IssuerKws); + + return { + manifest: { + [publishInterchainAssets.name]: { + consume: { + agoricNamesAdmin: true, + bankManager: true, + board: true, + chainStorage: true, + startUpgradable: true, + }, + brand: { + produce: IssuerKws, + }, + issuer: { + produce: IssuerKws, + }, + installation: { + consume: { mintHolder: true }, + }, + }, + }, + options, + }; +}; From 5cafda6636d79329fe6e1f4d5561ccdec95d3a0b Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Sat, 30 Nov 2024 04:19:51 -0500 Subject: [PATCH 2/4] chore: pfm timeout is Go time duration string --- packages/orchestration/src/cosmos-api.ts | 17 +++++++++++++++-- packages/orchestration/src/exos/chain-hub.js | 4 ++-- .../orchestration/test/exos/chain-hub.test.ts | 10 +++++----- .../local-orchestration-account-kit.test.ts | 6 +++--- 4 files changed, 25 insertions(+), 12 deletions(-) diff --git a/packages/orchestration/src/cosmos-api.ts b/packages/orchestration/src/cosmos-api.ts index 661d92a23d5..dad627c1382 100644 --- a/packages/orchestration/src/cosmos-api.ts +++ b/packages/orchestration/src/cosmos-api.ts @@ -370,8 +370,8 @@ export interface ForwardInfo { receiver: ChainAddress['value']; port: IBCPortID; channel: IBCChannelID; - /** e.g. '10min' */ - timeout: string; + /** e.g. '10m' */ + timeout: GoDuration; /** default is 3? */ retries: number; next?: { @@ -404,3 +404,16 @@ export type TransferRoute = { forwardInfo?: never; } ); + +/** Single units allowed in Go time duration strings */ +type GoDurationUnit = 'h' | 'm' | 's' | 'ms' | 'us' | 'ns'; + +/** + * Type for a time duration string in Go (cosmos-sdk). For example, "1h", "3m". + * + * Note: this does not support composite values like "1h30m", "1m30s", + * which are allowed in Go. + * + * @see https://pkg.go.dev/time#ParseDuration + */ +export type GoDuration = `${number}${GoDurationUnit}`; diff --git a/packages/orchestration/src/exos/chain-hub.js b/packages/orchestration/src/exos/chain-hub.js index 1477ca91de8..8f40d34b563 100644 --- a/packages/orchestration/src/exos/chain-hub.js +++ b/packages/orchestration/src/exos/chain-hub.js @@ -21,7 +21,7 @@ import { getBech32Prefix } from '../utils/address.js'; * @import {NameHub} from '@agoric/vats'; * @import {Vow, VowTools} from '@agoric/vow'; * @import {Zone} from '@agoric/zone'; - * @import {CosmosAssetInfo, CosmosChainInfo, ForwardInfo, IBCConnectionInfo, IBCMsgTransferOptions, TransferRoute} from '../cosmos-api.js'; + * @import {CosmosAssetInfo, CosmosChainInfo, ForwardInfo, IBCConnectionInfo, IBCMsgTransferOptions, TransferRoute, GoDuration} from '../cosmos-api.js'; * @import {ChainInfo, KnownChains} from '../chain-info.js'; * @import {ChainAddress, Denom, DenomAmount} from '../orchestration-api.js'; * @import {Remote, TypedPattern} from '@agoric/internal'; @@ -179,7 +179,7 @@ const ChainIdArgShape = M.or( const DefaultPfmTimeoutOpts = harden( /** @type {const} */ ({ retries: 3, - timeout: '10min', + timeout: /** @type {const} */ ('10m'), }), ); diff --git a/packages/orchestration/test/exos/chain-hub.test.ts b/packages/orchestration/test/exos/chain-hub.test.ts index 02505f41778..870d5c9672d 100644 --- a/packages/orchestration/test/exos/chain-hub.test.ts +++ b/packages/orchestration/test/exos/chain-hub.test.ts @@ -325,7 +325,7 @@ test('makeTransferRoute - through issuing chain', async t => { port: 'transfer', channel: 'channel-1', retries: 3, - timeout: '10min', + timeout: '10m', }, }, }); @@ -348,7 +348,7 @@ test('makeTransferRoute - through issuing chain', async t => { timeoutTimestamp: 0n, }); t.like(transferMsg, { - memo: '{"forward":{"receiver":"osmo1234","port":"transfer","channel":"channel-1","retries":3,"timeout":"10min"}}', + memo: '{"forward":{"receiver":"osmo1234","port":"transfer","channel":"channel-1","retries":3,"timeout":"10m"}}', receiver: 'pfm', }); }); @@ -367,7 +367,7 @@ test('makeTransferRoute - takes forwardOpts', t => { const amt: DenomAmount = harden({ denom: uusdcOnOsmosis, value: 100n }); const forwardOpts = harden({ retries: 1, - timeout: '3min', + timeout: '3m' as const, }); // 100 USDC on osmosis -> agoric @@ -386,11 +386,11 @@ test('makeTransferRoute - takes forwardOpts', t => { }); t.like( - chainHub.makeTransferRoute(dest, amt, 'osmosis', { timeout: '99min' }), + chainHub.makeTransferRoute(dest, amt, 'osmosis', { timeout: '99m' }), { forwardInfo: { forward: { - timeout: '99min', + timeout: '99m' as const, }, }, }, diff --git a/packages/orchestration/test/exos/local-orchestration-account-kit.test.ts b/packages/orchestration/test/exos/local-orchestration-account-kit.test.ts index afec573e19c..ed618734166 100644 --- a/packages/orchestration/test/exos/local-orchestration-account-kit.test.ts +++ b/packages/orchestration/test/exos/local-orchestration-account-kit.test.ts @@ -299,7 +299,7 @@ test('transfer', async t => { t.like(lastestTxMsg(), { receiver: PFM_RECEIVER, - memo: '{"forward":{"receiver":"dydx1test","port":"transfer","channel":"channel-33","retries":3,"timeout":"10min"}}', + memo: '{"forward":{"receiver":"dydx1test","port":"transfer","channel":"channel-33","retries":3,"timeout":"10m"}}', }); t.log('accepts pfm `forwardOpts`'); @@ -309,7 +309,7 @@ test('transfer', async t => { dydxDest, { forwardOpts: { - timeout: '999min', + timeout: '999m', }, }, fetchedChainInfo.agoric.connections['noble-1'].transferChannel.channelId, @@ -318,7 +318,7 @@ test('transfer', async t => { t.like(JSON.parse(lastestTxMsg().memo), { forward: { - timeout: '999min', + timeout: '999m', }, }); }); From 57cc060c1f11e69d9ef26a3a129213ab021cbc51 Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Sat, 30 Nov 2024 04:37:42 -0500 Subject: [PATCH 3/4] test: `faucetTools.fundFaucet` - adds helper to fund agoric faucet with interchain tokens - allows callers to request `OSMO`, `ATOM`, etc, via `provisionSmartWallet` --- multichain-testing/test/auto-stake-it.test.ts | 60 +++--------------- multichain-testing/test/support.ts | 8 +++ multichain-testing/tools/agd-lib.js | 15 ++++- multichain-testing/tools/e2e-tools.js | 7 ++- multichain-testing/tools/faucet-tools.ts | 39 ++++++++++++ multichain-testing/tools/ibc-transfer.ts | 62 ++++++++++++++++++- multichain-testing/tools/sleep.ts | 2 + 7 files changed, 135 insertions(+), 58 deletions(-) create mode 100644 multichain-testing/tools/faucet-tools.ts diff --git a/multichain-testing/test/auto-stake-it.test.ts b/multichain-testing/test/auto-stake-it.test.ts index 29b984ceec0..d4bfded2086 100644 --- a/multichain-testing/test/auto-stake-it.test.ts +++ b/multichain-testing/test/auto-stake-it.test.ts @@ -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'; @@ -37,53 +34,6 @@ test.after(async t => { deleteTestKeys(accounts); }); -const makeFundAndTransfer = (t: ExecutionContext) => { - 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) => { @@ -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]; diff --git a/multichain-testing/test/support.ts b/multichain-testing/test/support.ts index f3f8e752d96..18e8006f155 100644 --- a/multichain-testing/test/support.ts +++ b/multichain-testing/test/support.ts @@ -19,6 +19,7 @@ import { makeHermes } from '../tools/hermes-tools.js'; import { makeNobleTools } from '../tools/noble-tools.js'; import { makeAssetInfo } from '../tools/asset-info.js'; import starshipChainInfo from '../starship-chain-info.js'; +import { makeFaucetTools } from '../tools/faucet-tools.js'; export const FAUCET_POUR = 10_000n * 1_000_000n; @@ -84,6 +85,12 @@ export const commonSetup = async (t: ExecutionContext) => { const nobleTools = makeNobleTools(childProcess); const assetInfo = makeAssetInfo(starshipChainInfo); const chainInfo = withChainCapabilities(starshipChainInfo); + const faucetTools = makeFaucetTools( + t, + tools.agd, + retryUntilCondition, + useChain, + ); /** * Starts a contract if instance not found. Takes care of installing @@ -135,6 +142,7 @@ export const commonSetup = async (t: ExecutionContext) => { startContract, assetInfo, chainInfo, + faucetTools, }; }; diff --git a/multichain-testing/tools/agd-lib.js b/multichain-testing/tools/agd-lib.js index a301945f148..a28d408493b 100644 --- a/multichain-testing/tools/agd-lib.js +++ b/multichain-testing/tools/agd-lib.js @@ -164,6 +164,19 @@ export const makeAgd = ({ execFileSync }) => { }, ).toString(); }, + /** @param {string} name key name in keyring */ + showAddress: name => { + return execFileSync( + kubectlBinary, + [...binaryArgs, 'keys', 'show', name, '-a', ...keyringArgs], + { + encoding: 'utf-8', + stdio: ['pipe', 'pipe', 'ignore'], + }, + ) + .toString() + .trim(); + }, /** @param {string} name */ delete: name => { return exec([...keyringArgs, 'keys', 'delete', name, '-y'], { @@ -181,7 +194,7 @@ export const makeAgd = ({ execFileSync }) => { return make(); }; -/** @typedef {ReturnType} Agd */ +/** @typedef {ReturnType} Agd */ /** @param {{ execFileSync: typeof import('child_process').execFileSync, log: typeof console.log }} powers */ export const makeCopyFiles = ( diff --git a/multichain-testing/tools/e2e-tools.js b/multichain-testing/tools/e2e-tools.js index 51583c006b8..f820e7c8077 100644 --- a/multichain-testing/tools/e2e-tools.js +++ b/multichain-testing/tools/e2e-tools.js @@ -144,7 +144,11 @@ export const provisionSmartWallet = async ( // TODO: skip this query if balances is {} const vbankEntries = await q.queryData('published.agoricNames.vbankAsset'); const byName = Object.fromEntries( - vbankEntries.map(([_denom, info]) => [info.issuerName, info]), + vbankEntries.map(([denom, info]) => { + /// XXX better way to filter out old ATOM denom? + if (denom === 'ibc/toyatom') return [undefined, undefined]; + return [info.issuerName, info]; + }), ); progress({ send: balances, to: address }); @@ -543,6 +547,7 @@ export const makeE2ETools = async ( /** @param {string} name */ deleteKey: async name => agd.keys.delete(name), copyFiles, + agd, }; }; diff --git a/multichain-testing/tools/faucet-tools.ts b/multichain-testing/tools/faucet-tools.ts new file mode 100644 index 00000000000..272dd464cae --- /dev/null +++ b/multichain-testing/tools/faucet-tools.ts @@ -0,0 +1,39 @@ +import type { ExecutionContext } from 'ava'; +import type { Denom } from '@agoric/orchestration'; +import { makeFundAndTransfer } from './ibc-transfer.js'; +import type { MultichainRegistry } from './registry.js'; +import type { RetryUntilCondition } from './sleep.js'; +import type { AgdTools } from './agd-tools.js'; + +type ChainName = string; + +// 90% of default faucet pour +const DEFAULT_QTY = (10_000_000_000n * 9n) / 10n; + +/** + * Determines the agoric `faucet` address and sends funds to it. + * + * Allows use of brands like OSMO, ATOM, etc. with `provisionSmartWallet`. + */ +export const makeFaucetTools = ( + t: ExecutionContext, + agd: AgdTools['agd'], + retryUntilCondition: RetryUntilCondition, + useChain: MultichainRegistry['useChain'], +) => { + const fundAndTransfer = makeFundAndTransfer(t, retryUntilCondition, useChain); + return { + /** + * @param assets denom on the issuing chain + * @param [qty] number of tokens + */ + fundFaucet: async (assets: [ChainName, Denom][], qty = DEFAULT_QTY) => { + const faucetAddr = agd.keys.showAddress('faucet'); + console.log(`Faucet address: ${faucetAddr}`); + + for (const [chainName, denom] of assets) { + await fundAndTransfer(chainName, faucetAddr, qty, denom); + } + }, + }; +}; diff --git a/multichain-testing/tools/ibc-transfer.ts b/multichain-testing/tools/ibc-transfer.ts index 41db593cff2..3437080afd3 100644 --- a/multichain-testing/tools/ibc-transfer.ts +++ b/multichain-testing/tools/ibc-transfer.ts @@ -16,6 +16,7 @@ import { MsgTransfer } from '@agoric/cosmic-proto/ibc/applications/transfer/v1/t import { createWallet } from './wallet.js'; import chainInfo from '../starship-chain-info.js'; import type { MultichainRegistry } from './registry.js'; +import type { RetryUntilCondition } from './sleep.js'; interface MakeFeeObjectArgs { denom?: string; @@ -118,14 +119,15 @@ export const makeIBCTransferMsg = ( }; export const createFundedWalletAndClient = async ( - t: ExecutionContext, + log: (...args: unknown[]) => void, chainName: string, useChain: MultichainRegistry['useChain'], + mnemonic?: string, ) => { const { chain, creditFromFaucet, getRpcEndpoint } = useChain(chainName); - const wallet = await createWallet(chain.bech32_prefix); + const wallet = await createWallet(chain.bech32_prefix, mnemonic); const address = (await wallet.getAccounts())[0].address; - t.log(`Requesting faucet funds for ${address}`); + log(`Requesting faucet funds for ${address}`); await creditFromFaucet(address); // TODO use telescope generated rpc client from @agoric/cosmic-proto // https://github.com/Agoric/agoric-sdk/issues/9200 @@ -135,3 +137,57 @@ export const createFundedWalletAndClient = async ( ); return { client, wallet, address }; }; + +export const makeFundAndTransfer = ( + t: ExecutionContext, + retryUntilCondition: RetryUntilCondition, + useChain: MultichainRegistry['useChain'], +) => { + return async ( + chainName: string, + agoricAddr: string, + amount = 100n, + denom?: string, + ) => { + const { staking } = useChain(chainName).chainInfo.chain; + const denomToTransfer = denom || staking?.staking_tokens?.[0].denom; + if (!denomToTransfer) throw Error(`no denom for ${chainName}`); + + const { client, address, wallet } = await createFundedWalletAndClient( + t.log, + 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: denomToTransfer, 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, + }; + }; +}; diff --git a/multichain-testing/tools/sleep.ts b/multichain-testing/tools/sleep.ts index 66251a87dca..1b0d1a58807 100644 --- a/multichain-testing/tools/sleep.ts +++ b/multichain-testing/tools/sleep.ts @@ -75,3 +75,5 @@ export const makeRetryUntilCondition = (defaultOptions: RetryOptions = {}) => { ...options, }); }; + +export type RetryUntilCondition = ReturnType; From 748883d97b625a2a98f2253799d6e6baac3de4f1 Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Sat, 30 Nov 2024 04:38:44 -0500 Subject: [PATCH 4/4] test: send-anywhere pfm scenarios --- multichain-testing/test/send-anywhere.test.ts | 50 +++++++++++-------- .../src/proposals/start-send-anywhere.js | 29 +++++++++-- 2 files changed, 53 insertions(+), 26 deletions(-) diff --git a/multichain-testing/test/send-anywhere.test.ts b/multichain-testing/test/send-anywhere.test.ts index f1a8a720e5f..11508f22cc6 100644 --- a/multichain-testing/test/send-anywhere.test.ts +++ b/multichain-testing/test/send-anywhere.test.ts @@ -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 }; @@ -30,6 +31,11 @@ test.before(async t => { chainInfo, assetInfo, }); + + await faucetTools.fundFaucet([ + ['cosmoshub', 'uatom'], + ['osmosis', 'uosmo'], + ]); }); test.after(async t => { @@ -37,12 +43,14 @@ test.after(async t => { 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, @@ -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}`); @@ -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}.`); @@ -83,8 +91,8 @@ 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: { @@ -92,7 +100,7 @@ const sendAnywhereScenario = test.macro({ instancePath: [contractName], callPipe: [['makeSendInvitation']], }, - offerArgs: { destAddr: receiver.value, chainName }, + offerArgs: { destAddr: receiver.value, chainName: destChainName }, proposal: { give: { Send: amount } }, }); @@ -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) diff --git a/packages/orchestration/src/proposals/start-send-anywhere.js b/packages/orchestration/src/proposals/start-send-anywhere.js index b0db84c3f32..0599f077dd9 100644 --- a/packages/orchestration/src/proposals/start-send-anywhere.js +++ b/packages/orchestration/src/proposals/start-send-anywhere.js @@ -31,7 +31,6 @@ const trace = makeTracer('StartSA', true); * consume: { * BLD: Issuer<'nat'>; * IST: Issuer<'nat'>; - * USDC: Issuer<'nat'>; * }; * }; * }} powers @@ -84,13 +83,33 @@ export const startSendAnywhere = async ( }), ); + /** @param {() => Promise} p */ + const safeFulfill = async p => + E.when( + p(), + i => i, + () => undefined, + ); + + const atomIssuer = await safeFulfill(() => + E(agoricNames).lookup('issuer', 'ATOM'), + ); + const osmoIssuer = await safeFulfill(() => + E(agoricNames).lookup('issuer', 'OSMO'), + ); + + const issuerKeywordRecord = harden({ + BLD: await BLD, + IST: await IST, + ...(atomIssuer && { ATOM: atomIssuer }), + ...(osmoIssuer && { OSMO: osmoIssuer }), + }); + trace('issuerKeywordRecord', issuerKeywordRecord); + const { instance } = await E(startUpgradable)({ label: 'send-anywhere', installation: sendAnywhere, - issuerKeywordRecord: { - Stable: await IST, - Stake: await BLD, - }, + issuerKeywordRecord, privateArgs, }); produceInstance.resolve(instance);