diff --git a/a3p-integration/proposals/s:stake-bld/test-lib/chain.js b/a3p-integration/proposals/s:stake-bld/test-lib/chain.js deleted file mode 100644 index 74cc0d3c14f..00000000000 --- a/a3p-integration/proposals/s:stake-bld/test-lib/chain.js +++ /dev/null @@ -1,140 +0,0 @@ -/** @file copied from packages/agoric-cli */ -// TODO DRY in https://github.com/Agoric/agoric-sdk/issues/9109 -// @ts-check -/* global process */ - -const agdBinary = 'agd'; - -/** - * @param {ReadonlyArray} swingsetArgs - * @param {import('./rpc.js').MinimalNetworkConfig & { - * from: string, - * fees?: string, - * dryRun?: boolean, - * verbose?: boolean, - * keyring?: {home?: string, backend: string} - * stdout?: Pick - * execFileSync: typeof import('child_process').execFileSync - * }} opts - */ -export const execSwingsetTransaction = (swingsetArgs, opts) => { - const { - from, - fees, - dryRun = false, - verbose = true, - keyring = undefined, - chainName, - rpcAddrs, - stdout = process.stdout, - execFileSync, - } = opts; - const homeOpt = keyring?.home ? [`--home=${keyring.home}`] : []; - const backendOpt = keyring?.backend - ? [`--keyring-backend=${keyring.backend}`] - : []; - const feeOpt = fees ? ['--fees', fees] : []; - const cmd = [`--node=${rpcAddrs[0]}`, `--chain-id=${chainName}`].concat( - homeOpt, - backendOpt, - feeOpt, - [`--from=${from}`, 'tx', 'swingset'], - swingsetArgs, - ); - - if (dryRun) { - stdout.write(`Run this interactive command in shell:\n\n`); - stdout.write(`${agdBinary} `); - stdout.write(cmd.join(' ')); - stdout.write('\n'); - } else { - const yesCmd = cmd.concat(['--yes']); - if (verbose) console.log('Executing ', agdBinary, yesCmd); - const out = execFileSync(agdBinary, yesCmd, { encoding: 'utf-8' }); - - // agd puts this diagnostic on stdout rather than stderr :-/ - // "Default sign-mode 'direct' not supported by Ledger, using sign-mode 'amino-json'. - if (out.startsWith('Default sign-mode')) { - const stripDiagnostic = out.replace(/^Default[^\n]+\n/, ''); - return stripDiagnostic; - } - return out; - } -}; -harden(execSwingsetTransaction); - -/** - * @param {import('./rpc.js').MinimalNetworkConfig & { - * execFileSync: typeof import('child_process').execFileSync, - * delay: (ms: number) => Promise, - * period?: number, - * retryMessage?: string, - * }} opts - * @returns {(l: (b: { time: string, height: string }) => Promise) => Promise} - */ -export const pollBlocks = opts => async lookup => { - const { execFileSync, delay, rpcAddrs, period = 3 * 1000 } = opts; - assert(execFileSync, 'missing execFileSync'); - const { retryMessage } = opts; - - const nodeArgs = [`--node=${rpcAddrs[0]}`]; - - await null; // separate sync prologue - - for (;;) { - const sTxt = execFileSync(agdBinary, ['status', ...nodeArgs]); - const status = JSON.parse(sTxt.toString()); - const { - SyncInfo: { latest_block_time: time, latest_block_height: height }, - } = status; - try { - // see await null above - const result = await lookup({ time, height }); - return result; - } catch (_err) { - console.error( - time, - retryMessage || 'not in block', - height, - 'retrying...', - ); - await delay(period); - } - } -}; - -/** - * @param {string} txhash - * @param {import('./rpc.js').MinimalNetworkConfig & { - * execFileSync: typeof import('child_process').execFileSync, - * delay: (ms: number) => Promise, - * period?: number, - * }} opts - */ -export const pollTx = async (txhash, opts) => { - const { execFileSync, rpcAddrs, chainName } = opts; - assert(execFileSync, 'missing execFileSync in pollTx'); - - const nodeArgs = [`--node=${rpcAddrs[0]}`]; - const outJson = ['--output', 'json']; - - const lookup = async () => { - const out = execFileSync( - agdBinary, - [ - 'query', - 'tx', - txhash, - `--chain-id=${chainName}`, - ...nodeArgs, - ...outJson, - ], - { stdio: ['ignore', 'pipe', 'ignore'] }, - ); - // XXX this type is defined in a .proto file somewhere - /** @type {{ height: string, txhash: string, code: number, timestamp: string }} */ - const info = JSON.parse(out.toString()); - return info; - }; - return pollBlocks({ ...opts, retryMessage: 'tx not in block' })(lookup); -}; diff --git a/a3p-integration/proposals/s:stake-bld/test-lib/index.js b/a3p-integration/proposals/s:stake-bld/test-lib/index.js index a35b9b94fe0..9c22b218e19 100644 --- a/a3p-integration/proposals/s:stake-bld/test-lib/index.js +++ b/a3p-integration/proposals/s:stake-bld/test-lib/index.js @@ -1,11 +1,9 @@ /* eslint-env node */ import { execFileSync } from 'child_process'; +import { LOCAL_CONFIG as networkConfig } from '@agoric/client-utils'; import { makeWalletUtils } from './wallet.js'; -export const networkConfig = { - rpcAddrs: ['http://0.0.0.0:26657'], - chainName: 'agoriclocal', -}; +export { networkConfig }; /** * Resolve after a delay in milliseconds. diff --git a/a3p-integration/proposals/s:stake-bld/test-lib/rpc.js b/a3p-integration/proposals/s:stake-bld/test-lib/rpc.js deleted file mode 100644 index a3ac266ee88..00000000000 --- a/a3p-integration/proposals/s:stake-bld/test-lib/rpc.js +++ /dev/null @@ -1,266 +0,0 @@ -/** @file copied from packages/agoric-cli */ -// TODO DRY in https://github.com/Agoric/agoric-sdk/issues/9109 -// @ts-check -/* eslint-env node */ - -import { Fail } from '@endo/errors'; -import { - boardSlottingMarshaller, - makeBoardRemote, -} from '@agoric/internal/src/marshal.js'; - -export { boardSlottingMarshaller }; - -export const boardValToSlot = val => { - if ('getBoardId' in val) { - return val.getBoardId(); - } - Fail`unknown obj in boardSlottingMarshaller.valToSlot ${val}`; -}; - -export const networkConfigUrl = agoricNetSubdomain => - `https://${agoricNetSubdomain}.agoric.net/network-config`; -export const rpcUrl = agoricNetSubdomain => - `https://${agoricNetSubdomain}.rpc.agoric.net:443`; - -/** - * @typedef {{ rpcAddrs: string[], chainName: string }} MinimalNetworkConfig - */ - -/** @type {MinimalNetworkConfig} */ -const networkConfig = { - rpcAddrs: ['http://0.0.0.0:26657'], - chainName: 'agoriclocal', -}; -export { networkConfig }; -// console.warn('networkConfig', networkConfig); - -/** - * @param {object} powers - * @param {typeof window.fetch} powers.fetch - * @param {MinimalNetworkConfig} config - */ -export const makeVStorage = (powers, config = networkConfig) => { - /** @param {string} path */ - const getJSON = path => { - const url = config.rpcAddrs[0] + path; - // console.warn('fetching', url); - return powers.fetch(url, { keepalive: true }).then(res => res.json()); - }; - // height=0 is the same as omitting height and implies the highest block - const url = (path = 'published', { kind = 'children', height = 0 } = {}) => - `/abci_query?path=%22/custom/vstorage/${kind}/${path}%22&height=${height}`; - - const readStorage = (path = 'published', { kind = 'children', height = 0 }) => - getJSON(url(path, { kind, height })) - .catch(err => { - throw Error(`cannot read ${kind} of ${path}: ${err.message}`); - }) - .then(data => { - const { - result: { response }, - } = data; - if (response?.code !== 0) { - /** @type {any} */ - const err = Error( - `error code ${response?.code} reading ${kind} of ${path}: ${response.log}`, - ); - err.code = response?.code; - err.codespace = response?.codespace; - throw err; - } - return data; - }); - - return { - url, - decode({ result: { response } }) { - const { code } = response; - if (code !== 0) { - throw response; - } - const { value } = response; - return Buffer.from(value, 'base64').toString(); - }, - /** - * - * @param {string} path - * @returns {Promise} latest vstorage value at path - */ - async readLatest(path = 'published') { - const raw = await readStorage(path, { kind: 'data' }); - return this.decode(raw); - }, - async keys(path = 'published') { - const raw = await readStorage(path, { kind: 'children' }); - return JSON.parse(this.decode(raw)).children; - }, - /** - * @param {string} path - * @param {number} [height] default is highest - * @returns {Promise<{blockHeight: number, values: string[]}>} - */ - async readAt(path, height = undefined) { - const raw = await readStorage(path, { kind: 'data', height }); - const txt = this.decode(raw); - /** @type {{ value: string }} */ - const { value } = JSON.parse(txt); - return JSON.parse(value); - }, - /** - * Read values going back as far as available - * - * @param {string} path - * @param {number | string} [minHeight] - * @returns {Promise} - */ - async readFully(path, minHeight = undefined) { - const parts = []; - // undefined the first iteration, to query at the highest - let blockHeight; - await null; - do { - // console.debug('READING', { blockHeight }); - let values; - try { - ({ blockHeight, values } = await this.readAt( - path, - blockHeight && Number(blockHeight) - 1, - )); - // console.debug('readAt returned', { blockHeight }); - } catch (err) { - if ( - // CosmosSDK ErrNotFound; there is no data at the path - (err.codespace === 'sdk' && err.code === 38) || - // CosmosSDK ErrUnknownRequest; misrepresentation of the same until - // https://github.com/Agoric/agoric-sdk/commit/dafc7c1708977aaa55e245dc09a73859cf1df192 - // TODO remove after upgrade-12 - err.message.match(/unknown request/) - ) { - // console.error(err); - break; - } - throw err; - } - parts.push(values); - // console.debug('PUSHED', values); - // console.debug('NEW', { blockHeight, minHeight }); - if (minHeight && Number(blockHeight) <= Number(minHeight)) break; - } while (blockHeight > 0); - return parts.flat(); - }, - }; -}; -/** @typedef {ReturnType} VStorage */ - -export const makeFromBoard = () => { - const cache = new Map(); - const convertSlotToVal = (boardId, iface) => { - if (cache.has(boardId)) { - return cache.get(boardId); - } - const val = makeBoardRemote({ boardId, iface }); - cache.set(boardId, val); - return val; - }; - return harden({ convertSlotToVal }); -}; -/** @typedef {ReturnType} IdMap */ - -export const storageHelper = { - /** @param { string } txt */ - parseCapData: txt => { - assert(typeof txt === 'string', typeof txt); - /** @type {{ value: string }} */ - const { value } = JSON.parse(txt); - const specimen = JSON.parse(value); - const { blockHeight, values } = specimen; - assert(values, `empty values in specimen ${value}`); - const capDatas = storageHelper.parseMany(values); - return { blockHeight, capDatas }; - }, - /** - * @param {string} txt - * @param {IdMap} ctx - */ - unserializeTxt: (txt, ctx) => { - const { capDatas } = storageHelper.parseCapData(txt); - return capDatas.map(capData => - boardSlottingMarshaller(ctx.convertSlotToVal).fromCapData(capData), - ); - }, - /** @param {string[]} capDataStrings array of stringified capData */ - parseMany: capDataStrings => { - assert(capDataStrings && capDataStrings.length); - /** @type {{ body: string, slots: string[] }[]} */ - const capDatas = capDataStrings.map(s => JSON.parse(s)); - for (const capData of capDatas) { - assert(typeof capData === 'object' && capData !== null); - assert('body' in capData && 'slots' in capData); - assert(typeof capData.body === 'string'); - assert(Array.isArray(capData.slots)); - } - return capDatas; - }, -}; -harden(storageHelper); - -/** - * @param {IdMap} ctx - * @param {VStorage} vstorage - * @returns {Promise} - */ -export const makeAgoricNames = async (ctx, vstorage) => { - const reverse = {}; - const entries = await Promise.all( - ['brand', 'instance', 'vbankAsset'].map(async kind => { - const content = await vstorage.readLatest( - `published.agoricNames.${kind}`, - ); - /** @type {Array<[string, import('@agoric/vats/tools/board-utils.js').BoardRemote]>} */ - const parts = storageHelper.unserializeTxt(content, ctx).at(-1); - for (const [name, remote] of parts) { - if ('getBoardId' in remote) { - reverse[remote.getBoardId()] = name; - } - } - return [kind, Object.fromEntries(parts)]; - }), - ); - return { ...Object.fromEntries(entries), reverse }; -}; - -/** - * @param {{ fetch: typeof window.fetch }} io - * @param {MinimalNetworkConfig} config - */ -export const makeVstorageKit = async ({ fetch }, config = networkConfig) => { - await null; - try { - const vstorage = makeVStorage({ fetch }, config); - const fromBoard = makeFromBoard(); - const agoricNames = await makeAgoricNames(fromBoard, vstorage); - - const marshaller = boardSlottingMarshaller(fromBoard.convertSlotToVal); - - /** @type {(txt: string) => unknown} */ - const unserializeHead = txt => - storageHelper.unserializeTxt(txt, fromBoard).at(-1); - - /** @type {(path: string) => Promise} */ - const readLatestHead = path => - vstorage.readLatest(path).then(unserializeHead); - - return { - agoricNames, - fromBoard, - marshaller, - readLatestHead, - unserializeHead, - vstorage, - }; - } catch (err) { - throw Error(`RPC failure (${config.rpcAddrs}): ${err.message}`); - } -}; -/** @typedef {Awaited>} RpcUtils */ diff --git a/a3p-integration/proposals/s:stake-bld/test-lib/wallet.js b/a3p-integration/proposals/s:stake-bld/test-lib/wallet.js index e9461522e1d..e3ef23af929 100644 --- a/a3p-integration/proposals/s:stake-bld/test-lib/wallet.js +++ b/a3p-integration/proposals/s:stake-bld/test-lib/wallet.js @@ -1,61 +1,8 @@ -/** - * @file copied from packages/agoric-cli, - * removing polling and coalescing features whose dependencies cause import problems here - */ -// TODO DRY in https://github.com/Agoric/agoric-sdk/issues/9109 // @ts-check -/* global */ +import { makeVstorageKit } from '@agoric/client-utils'; +import { sendAction } from 'agoric/src/lib/index.js'; import { inspect } from 'util'; -import { execSwingsetTransaction, pollTx } from './chain.js'; -import { makeVstorageKit } from './rpc.js'; - -/** - * Sign and broadcast a wallet-action. - * - * @throws { Error & { code: number } } if transaction fails - * @param {import('@agoric/smart-wallet/src/smartWallet.js').BridgeAction} bridgeAction - * @param {import('./rpc.js').MinimalNetworkConfig & { - * from: string, - * marshaller: import('@endo/marshal').Marshal<'string'>, - * fees?: string, - * verbose?: boolean, - * keyring?: {home?: string, backend: string}, - * stdout: Pick, - * execFileSync: typeof import('child_process').execFileSync, - * delay: (ms: number) => Promise, - * dryRun?: boolean, - * }} opts - */ -export const sendAction = async (bridgeAction, opts) => { - const { marshaller } = opts; - // @ts-expect-error BridgeAction has methods disallowed by Passable - const offerBody = JSON.stringify(marshaller.toCapData(harden(bridgeAction))); - - // tryExit should not require --allow-spend - // https://github.com/Agoric/agoric-sdk/issues/7291 - const spendMethods = ['executeOffer', 'tryExitOffer']; - const spendArg = spendMethods.includes(bridgeAction.method) - ? ['--allow-spend'] - : []; - - const act = ['wallet-action', ...spendArg, offerBody]; - const out = execSwingsetTransaction([...act, '--output', 'json'], opts); - if (opts.dryRun) { - return; - } - - assert(out); // not dry run - const tx = JSON.parse(out); - if (tx.code !== 0) { - const err = Error(`failed to send tx: ${tx.raw_log} code: ${tx.code}`); - // @ts-expect-error XXX how to add properties to an error? - err.code = tx.code; - throw err; - } - - return pollTx(tx.txhash, opts); -}; export const makeWalletUtils = async ( { delay, execFileSync, fetch },