From 5d4f84c0d911e3d2745c0e3a2003b93a87dd2545 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Tue, 3 Dec 2024 00:22:13 -0600 Subject: [PATCH 01/11] refactor(fast-usdc): inject io to addOperatorCommands --- packages/fast-usdc/src/cli/cli.js | 15 +++++++++- .../fast-usdc/src/cli/operator-commands.js | 30 +++++++++---------- 2 files changed, 28 insertions(+), 17 deletions(-) diff --git a/packages/fast-usdc/src/cli/cli.js b/packages/fast-usdc/src/cli/cli.js index 56a74fbc25b..afaf1eadf9a 100644 --- a/packages/fast-usdc/src/cli/cli.js +++ b/packages/fast-usdc/src/cli/cli.js @@ -1,3 +1,5 @@ +/* eslint-env node */ +/* global globalThis */ import { assertParsableNumber } from '@agoric/zoe/src/contractSupport/ratio.js'; import { Command, @@ -34,6 +36,11 @@ export const initProgram = ( writeFile = writeAsync, mkdir = mkdirSync, exists = existsSync, + fetch = globalThis.fetch, + stdout = process.stdout, + stderr = process.stderr, + env = process.env, + now = () => Date.now(), ) => { const program = new Command(); @@ -56,7 +63,13 @@ export const initProgram = ( }; addConfigCommands(program, configHelpers, makeConfigFile); - addOperatorCommands(program); + addOperatorCommands(program, { + fetch, + stdout, + stderr, + env, + now, + }); /** @param {string} value */ const parseDecimal = value => { diff --git a/packages/fast-usdc/src/cli/operator-commands.js b/packages/fast-usdc/src/cli/operator-commands.js index 3542bdcf074..73f380b2460 100644 --- a/packages/fast-usdc/src/cli/operator-commands.js +++ b/packages/fast-usdc/src/cli/operator-commands.js @@ -1,4 +1,3 @@ -/* eslint-env node */ /** * @import {Command} from 'commander'; * @import {OfferSpec} from '@agoric/smart-wallet/src/offers.js'; @@ -11,8 +10,18 @@ import { outputActionAndHint } from './bridge-action.js'; /** * @param {Command} program + * @param {{ + * fetch: Window['fetch']; + * stdout: typeof process.stdout; + * stderr: typeof process.stderr; + * env: typeof process.env; + * now: typeof Date.now; + * }} io */ -export const addOperatorCommands = program => { +export const addOperatorCommands = ( + program, + { fetch, stderr, stdout, env, now }, +) => { const operator = program .command('operator') .description('Oracle operator commands'); @@ -24,17 +33,9 @@ export const addOperatorCommands = program => { 'after', '\nPipe the STDOUT to a file such as accept.json, then use the Agoric CLI to broadcast it:\n agoric wallet send --offer accept.json --from gov1 --keyring-backend="test"', ) - .option( - '--offerId ', - 'Offer id', - String, - `operatorAccept-${Date.now()}`, - ) + .option('--offerId ', 'Offer id', String, `operatorAccept-${now()}`) .action(async opts => { - const networkConfig = await fetchEnvNetworkConfig({ - env: process.env, - fetch, - }); + const networkConfig = await fetchEnvNetworkConfig({ env, fetch }); const vsk = await makeVstorageKit({ fetch }, networkConfig); const instance = vsk.agoricNames.instance.fastUsdc; assert(instance, 'fastUsdc instance not in agoricNames'); @@ -56,10 +57,7 @@ export const addOperatorCommands = program => { offer, }; - outputActionAndHint(bridgeAction, { - stderr: process.stderr, - stdout: process.stdout, - }); + outputActionAndHint(bridgeAction, { stderr, stdout }); }); operator From 448aa3a194b55ebeb5423f0027c543f8c6807239 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Tue, 3 Dec 2024 01:16:12 -0600 Subject: [PATCH 02/11] feat(fast-usdc): operator attest cli command using a LegibleCapData blob arg --- .../fast-usdc/src/cli/operator-commands.js | 38 +++++++- .../test/cli/operator-commands.test.ts | 96 +++++++++++++++++++ 2 files changed, 129 insertions(+), 5 deletions(-) create mode 100644 packages/fast-usdc/test/cli/operator-commands.test.ts diff --git a/packages/fast-usdc/src/cli/operator-commands.js b/packages/fast-usdc/src/cli/operator-commands.js index 73f380b2460..a9312cb4240 100644 --- a/packages/fast-usdc/src/cli/operator-commands.js +++ b/packages/fast-usdc/src/cli/operator-commands.js @@ -2,11 +2,22 @@ * @import {Command} from 'commander'; * @import {OfferSpec} from '@agoric/smart-wallet/src/offers.js'; * @import {ExecuteOfferAction} from '@agoric/smart-wallet/src/smartWallet.js'; + * @import {OperatorKit} from '../exos/operator-kit.js'; */ import { fetchEnvNetworkConfig, makeVstorageKit } from '@agoric/client-utils'; +import { mustMatch } from '@agoric/internal'; import { INVITATION_MAKERS_DESC } from '../exos/transaction-feed.js'; +import { CctpTxEvidenceShape } from '../type-guards.js'; import { outputActionAndHint } from './bridge-action.js'; +import { fromExternalConfig } from '../utils/config-marshal.js'; + +/** @param {string} arg */ +const parseCCTPEvidence = arg => { + const evidence = fromExternalConfig(JSON.parse(arg), {}); + mustMatch(evidence, CctpTxEvidenceShape); + return evidence; +}; /** * @param {Command} program @@ -64,11 +75,28 @@ export const addOperatorCommands = ( .command('attest') .description('Attest to an observed Fast USDC transfer') .requiredOption('--previousOfferId ', 'Offer id', String) - .action(async options => { - const { previousOfferId } = options; - console.error( - 'TODO: Implement attest logic for request:', - previousOfferId, + .requiredOption('--evidence ', 'CCTP evidence', parseCCTPEvidence) + .option('--offerId ', 'Offer id', String, `operatorAttest-${now()}`) + .action(async opts => { + const { previousOfferId, evidence } = opts; + + /** @type {OfferSpec} */ + const offer = { + id: opts.offerId, + invitationSpec: { + source: 'continuing', + previousOffer: previousOfferId, + /** @type {string & keyof OperatorKit['invitationMakers'] } */ + invitationMakerName: 'SubmitEvidence', + /** @type {Parameters } */ + invitationArgs: [evidence], + }, + proposal: {}, + }; + + outputActionAndHint( + { method: 'executeOffer', offer }, + { stderr, stdout }, ); }); diff --git a/packages/fast-usdc/test/cli/operator-commands.test.ts b/packages/fast-usdc/test/cli/operator-commands.test.ts new file mode 100644 index 00000000000..c8885b05029 --- /dev/null +++ b/packages/fast-usdc/test/cli/operator-commands.test.ts @@ -0,0 +1,96 @@ +import test from 'ava'; +import { Command } from 'commander'; +import type { Passable } from '@endo/pass-style'; +import { addOperatorCommands } from '../../src/cli/operator-commands.js'; +import { MockCctpTxEvidences } from '../fixtures.js'; +import { toExternalConfig } from '../../src/utils/config-marshal.js'; + +export const flags = ( + record: Record, +): string[] => { + // @ts-expect-error undefined is filtered out + const skipUndef: [string, string][] = Object.entries(record).filter( + ([_k, v]) => v !== undefined, + ); + return skipUndef.map(([k, v]) => [`--${k}`, v]).flat(); +}; + +test('fast-usdc operator attest sub-command', async t => { + const evidence = harden( + MockCctpTxEvidences.AGORIC_PLUS_DYDX(), + ) as unknown as Passable; + const argv = [ + ...`node fast-usdc operator attest`.split(' '), + ...flags({ + previousOfferId: 123, + evidence: JSON.stringify(toExternalConfig(evidence, {})), + }), + ]; + const program = new Command(); + program.exitOverride(); + const out = [] as string[]; + const err = [] as string[]; + + addOperatorCommands(program, { + fetch: null as unknown as Window['fetch'], + stdout: { + write: txt => { + out.push(txt); + return true; + }, + } as unknown as typeof process.stdout, + stderr: { + write: txt => { + err.push(txt); + return true; + }, + } as unknown as typeof process.stderr, + env: {}, + now: () => 1234, + }); + + await program.parseAsync(argv); + + t.deepEqual(out, [ + JSON.stringify({ + body: `#${JSON.stringify({ + method: 'executeOffer', + offer: { + id: 'operatorAttest-1234', + invitationSpec: { + invitationArgs: [ + { + aux: { + forwardingChannel: 'channel-21', + recipientAddress: + 'agoric16kv2g7snfc4q24vg3pjdlnnqgngtjpwtetd2h689nz09lcklvh5s8u37ek?EUD=dydx183dejcnmkka5dzcu9xw6mywq0p2m5peks28men', + }, + blockHash: + '0x80d7343e04f8160892e94f02d6a9b9f255663ed0ac34caca98544c8143fee699', + blockNumber: '+21037669', + blockTimestamp: '+1730762099', + chainId: 1, + tx: { + amount: '+300000000', + forwardingAddress: + 'noble1x0ydg69dh6fqvr27xjvp6maqmrldam6yfelktz', + }, + txHash: + '0xd81bc6105b60a234c7c50ac17816ebcd5561d366df8bf3be59ff387552761799', + }, + ], + invitationMakerName: 'SubmitEvidence', + previousOffer: '123', + source: 'continuing', + }, + proposal: {}, + }, + })}`, + slots: [], + }), + '\n', + ]); + t.deepEqual(err, [ + 'Now use `agoric wallet send ...` to sign and broadcast the offer.\n', + ]); +}); From c6a2123970a3687fe85c9a2d83f2ffe403cbc368 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Tue, 3 Dec 2024 01:38:27 -0600 Subject: [PATCH 03/11] chore(fast-usdc): use an arg for each evidence property --- packages/fast-usdc/package.json | 1 + .../fast-usdc/src/cli/operator-commands.js | 45 +++++++++++++++---- .../test/cli/operator-commands.test.ts | 16 +++++-- 3 files changed, 51 insertions(+), 11 deletions(-) diff --git a/packages/fast-usdc/package.json b/packages/fast-usdc/package.json index 007e1e90140..22759fefcd8 100644 --- a/packages/fast-usdc/package.json +++ b/packages/fast-usdc/package.json @@ -49,6 +49,7 @@ "@endo/far": "^1.1.9", "@endo/init": "^1.1.7", "@endo/marshal": "^1.6.2", + "@endo/nat": "^5.0.13", "@endo/pass-style": "^1.4.7", "@endo/patterns": "^1.4.7", "@endo/promise-kit": "^1.1.8", diff --git a/packages/fast-usdc/src/cli/operator-commands.js b/packages/fast-usdc/src/cli/operator-commands.js index a9312cb4240..06a3fdc01df 100644 --- a/packages/fast-usdc/src/cli/operator-commands.js +++ b/packages/fast-usdc/src/cli/operator-commands.js @@ -7,16 +7,22 @@ import { fetchEnvNetworkConfig, makeVstorageKit } from '@agoric/client-utils'; import { mustMatch } from '@agoric/internal'; +import { Nat } from '@endo/nat'; +import { InvalidArgumentError } from 'commander'; import { INVITATION_MAKERS_DESC } from '../exos/transaction-feed.js'; import { CctpTxEvidenceShape } from '../type-guards.js'; import { outputActionAndHint } from './bridge-action.js'; -import { fromExternalConfig } from '../utils/config-marshal.js'; /** @param {string} arg */ -const parseCCTPEvidence = arg => { - const evidence = fromExternalConfig(JSON.parse(arg), {}); - mustMatch(evidence, CctpTxEvidenceShape); - return evidence; +const parseNat = arg => { + const n = Nat(BigInt(arg)); + return n; +}; + +/** @param {string} arg */ +const parseHex = arg => { + if (!arg.startsWith('0x')) throw new InvalidArgumentError('not a hex string'); + return arg; }; /** @@ -75,14 +81,37 @@ export const addOperatorCommands = ( .command('attest') .description('Attest to an observed Fast USDC transfer') .requiredOption('--previousOfferId ', 'Offer id', String) - .requiredOption('--evidence ', 'CCTP evidence', parseCCTPEvidence) + .requiredOption('--forwardingChannel ', 'Channel id', String) + .requiredOption('--recipientAddress ', 'bech32 address', String) + .requiredOption('--blockHash <0xhex>', 'hex hash', parseHex) + .requiredOption('--blockNumber ', 'number', parseNat) + .requiredOption('--blockTimestamp ', 'number', parseNat) + .requiredOption('--chainId ', 'chain id', Number) + .requiredOption('--amount ', 'number', parseNat) + .requiredOption('--forwardingAddress ', 'bech32 address', String) + .requiredOption('--txHash <0xhexo>', 'hex hash', parseHex) .option('--offerId ', 'Offer id', String, `operatorAttest-${now()}`) .action(async opts => { - const { previousOfferId, evidence } = opts; + const { + offerId, + previousOfferId, + forwardingChannel, + recipientAddress, + amount, + forwardingAddress, + ...flat + } = opts; + + const evidence = harden({ + aux: { forwardingChannel, recipientAddress }, + tx: { amount, forwardingAddress }, + ...flat, + }); + mustMatch(evidence, CctpTxEvidenceShape); /** @type {OfferSpec} */ const offer = { - id: opts.offerId, + id: offerId, invitationSpec: { source: 'continuing', previousOffer: previousOfferId, diff --git a/packages/fast-usdc/test/cli/operator-commands.test.ts b/packages/fast-usdc/test/cli/operator-commands.test.ts index c8885b05029..02b7d38e0d8 100644 --- a/packages/fast-usdc/test/cli/operator-commands.test.ts +++ b/packages/fast-usdc/test/cli/operator-commands.test.ts @@ -1,9 +1,8 @@ +import type { Passable } from '@endo/pass-style'; import test from 'ava'; import { Command } from 'commander'; -import type { Passable } from '@endo/pass-style'; import { addOperatorCommands } from '../../src/cli/operator-commands.js'; import { MockCctpTxEvidences } from '../fixtures.js'; -import { toExternalConfig } from '../../src/utils/config-marshal.js'; export const flags = ( record: Record, @@ -23,7 +22,18 @@ test('fast-usdc operator attest sub-command', async t => { ...`node fast-usdc operator attest`.split(' '), ...flags({ previousOfferId: 123, - evidence: JSON.stringify(toExternalConfig(evidence, {})), + forwardingChannel: 'channel-21', + recipientAddress: + 'agoric16kv2g7snfc4q24vg3pjdlnnqgngtjpwtetd2h689nz09lcklvh5s8u37ek?EUD=dydx183dejcnmkka5dzcu9xw6mywq0p2m5peks28men', + blockHash: + '0x80d7343e04f8160892e94f02d6a9b9f255663ed0ac34caca98544c8143fee699', + blockNumber: 21037669, + blockTimestamp: 1730762099, + chainId: 1, + amount: 300000000, + forwardingAddress: 'noble1x0ydg69dh6fqvr27xjvp6maqmrldam6yfelktz', + txHash: + '0xd81bc6105b60a234c7c50ac17816ebcd5561d366df8bf3be59ff387552761799', }), ]; const program = new Command(); From f1df99f8d431d1b9a26f2687aa2c4c80a17537c0 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Tue, 3 Dec 2024 01:43:40 -0600 Subject: [PATCH 04/11] chore: don't kludge fromCapData --- .../test/cli/operator-commands.test.ts | 79 +++++++++---------- 1 file changed, 39 insertions(+), 40 deletions(-) diff --git a/packages/fast-usdc/test/cli/operator-commands.test.ts b/packages/fast-usdc/test/cli/operator-commands.test.ts index 02b7d38e0d8..ff0b7157888 100644 --- a/packages/fast-usdc/test/cli/operator-commands.test.ts +++ b/packages/fast-usdc/test/cli/operator-commands.test.ts @@ -3,6 +3,7 @@ import test from 'ava'; import { Command } from 'commander'; import { addOperatorCommands } from '../../src/cli/operator-commands.js'; import { MockCctpTxEvidences } from '../fixtures.js'; +import { makeMarshal } from '@endo/marshal'; export const flags = ( record: Record, @@ -14,6 +15,8 @@ export const flags = ( return skipUndef.map(([k, v]) => [`--${k}`, v]).flat(); }; +const marshalData = makeMarshal(_v => assert.fail('data only')); + test('fast-usdc operator attest sub-command', async t => { const evidence = harden( MockCctpTxEvidences.AGORIC_PLUS_DYDX(), @@ -61,46 +64,42 @@ test('fast-usdc operator attest sub-command', async t => { await program.parseAsync(argv); - t.deepEqual(out, [ - JSON.stringify({ - body: `#${JSON.stringify({ - method: 'executeOffer', - offer: { - id: 'operatorAttest-1234', - invitationSpec: { - invitationArgs: [ - { - aux: { - forwardingChannel: 'channel-21', - recipientAddress: - 'agoric16kv2g7snfc4q24vg3pjdlnnqgngtjpwtetd2h689nz09lcklvh5s8u37ek?EUD=dydx183dejcnmkka5dzcu9xw6mywq0p2m5peks28men', - }, - blockHash: - '0x80d7343e04f8160892e94f02d6a9b9f255663ed0ac34caca98544c8143fee699', - blockNumber: '+21037669', - blockTimestamp: '+1730762099', - chainId: 1, - tx: { - amount: '+300000000', - forwardingAddress: - 'noble1x0ydg69dh6fqvr27xjvp6maqmrldam6yfelktz', - }, - txHash: - '0xd81bc6105b60a234c7c50ac17816ebcd5561d366df8bf3be59ff387552761799', - }, - ], - invitationMakerName: 'SubmitEvidence', - previousOffer: '123', - source: 'continuing', + const action = marshalData.fromCapData(JSON.parse(out.join(''))); + t.deepEqual(action, { + method: 'executeOffer', + offer: { + id: 'operatorAttest-1234', + invitationSpec: { + invitationArgs: [ + { + aux: { + forwardingChannel: 'channel-21', + recipientAddress: + 'agoric16kv2g7snfc4q24vg3pjdlnnqgngtjpwtetd2h689nz09lcklvh5s8u37ek?EUD=dydx183dejcnmkka5dzcu9xw6mywq0p2m5peks28men', + }, + blockHash: + '0x80d7343e04f8160892e94f02d6a9b9f255663ed0ac34caca98544c8143fee699', + blockNumber: 21037669n, + blockTimestamp: 1730762099n, + chainId: 1, + tx: { + amount: 300000000n, + forwardingAddress: 'noble1x0ydg69dh6fqvr27xjvp6maqmrldam6yfelktz', + }, + txHash: + '0xd81bc6105b60a234c7c50ac17816ebcd5561d366df8bf3be59ff387552761799', }, - proposal: {}, - }, - })}`, - slots: [], - }), - '\n', - ]); - t.deepEqual(err, [ + ], + invitationMakerName: 'SubmitEvidence', + previousOffer: '123', + source: 'continuing', + }, + proposal: {}, + }, + }); + + t.is( + err.join(''), 'Now use `agoric wallet send ...` to sign and broadcast the offer.\n', - ]); + ); }); From 11df834abbc903ca49dc42501dd89aea034aaaaa Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Tue, 3 Dec 2024 01:48:06 -0600 Subject: [PATCH 05/11] chore: avoid magic strings in attest test --- .../test/cli/operator-commands.test.ts | 47 ++++--------------- 1 file changed, 10 insertions(+), 37 deletions(-) diff --git a/packages/fast-usdc/test/cli/operator-commands.test.ts b/packages/fast-usdc/test/cli/operator-commands.test.ts index ff0b7157888..3c58b9b5dab 100644 --- a/packages/fast-usdc/test/cli/operator-commands.test.ts +++ b/packages/fast-usdc/test/cli/operator-commands.test.ts @@ -6,37 +6,29 @@ import { MockCctpTxEvidences } from '../fixtures.js'; import { makeMarshal } from '@endo/marshal'; export const flags = ( - record: Record, + record: Record, ): string[] => { // @ts-expect-error undefined is filtered out const skipUndef: [string, string][] = Object.entries(record).filter( ([_k, v]) => v !== undefined, ); - return skipUndef.map(([k, v]) => [`--${k}`, v]).flat(); + return skipUndef.map(([k, v]) => [`--${k}`, `${v}`]).flat(); }; const marshalData = makeMarshal(_v => assert.fail('data only')); test('fast-usdc operator attest sub-command', async t => { - const evidence = harden( - MockCctpTxEvidences.AGORIC_PLUS_DYDX(), - ) as unknown as Passable; + const evidence = harden(MockCctpTxEvidences.AGORIC_PLUS_DYDX()); + const { aux, tx, ...flat } = evidence; const argv = [ ...`node fast-usdc operator attest`.split(' '), ...flags({ previousOfferId: 123, - forwardingChannel: 'channel-21', - recipientAddress: - 'agoric16kv2g7snfc4q24vg3pjdlnnqgngtjpwtetd2h689nz09lcklvh5s8u37ek?EUD=dydx183dejcnmkka5dzcu9xw6mywq0p2m5peks28men', - blockHash: - '0x80d7343e04f8160892e94f02d6a9b9f255663ed0ac34caca98544c8143fee699', - blockNumber: 21037669, - blockTimestamp: 1730762099, - chainId: 1, - amount: 300000000, - forwardingAddress: 'noble1x0ydg69dh6fqvr27xjvp6maqmrldam6yfelktz', - txHash: - '0xd81bc6105b60a234c7c50ac17816ebcd5561d366df8bf3be59ff387552761799', + forwardingChannel: aux.forwardingChannel, + recipientAddress: aux.recipientAddress, + amount: tx.amount, + forwardingAddress: tx.forwardingAddress, + ...flat, }), ]; const program = new Command(); @@ -70,26 +62,7 @@ test('fast-usdc operator attest sub-command', async t => { offer: { id: 'operatorAttest-1234', invitationSpec: { - invitationArgs: [ - { - aux: { - forwardingChannel: 'channel-21', - recipientAddress: - 'agoric16kv2g7snfc4q24vg3pjdlnnqgngtjpwtetd2h689nz09lcklvh5s8u37ek?EUD=dydx183dejcnmkka5dzcu9xw6mywq0p2m5peks28men', - }, - blockHash: - '0x80d7343e04f8160892e94f02d6a9b9f255663ed0ac34caca98544c8143fee699', - blockNumber: 21037669n, - blockTimestamp: 1730762099n, - chainId: 1, - tx: { - amount: 300000000n, - forwardingAddress: 'noble1x0ydg69dh6fqvr27xjvp6maqmrldam6yfelktz', - }, - txHash: - '0xd81bc6105b60a234c7c50ac17816ebcd5561d366df8bf3be59ff387552761799', - }, - ], + invitationArgs: [evidence], invitationMakerName: 'SubmitEvidence', previousOffer: '123', source: 'continuing', From 843ad24f5cc4becf547ae54acde4b4289d6033b2 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Tue, 3 Dec 2024 11:13:07 -0600 Subject: [PATCH 06/11] refactor: mockStream, concise flags --- .../test/cli/operator-commands.test.ts | 29 +++++-------------- 1 file changed, 8 insertions(+), 21 deletions(-) diff --git a/packages/fast-usdc/test/cli/operator-commands.test.ts b/packages/fast-usdc/test/cli/operator-commands.test.ts index 3c58b9b5dab..b0e25b26109 100644 --- a/packages/fast-usdc/test/cli/operator-commands.test.ts +++ b/packages/fast-usdc/test/cli/operator-commands.test.ts @@ -1,9 +1,9 @@ -import type { Passable } from '@endo/pass-style'; import test from 'ava'; import { Command } from 'commander'; import { addOperatorCommands } from '../../src/cli/operator-commands.js'; import { MockCctpTxEvidences } from '../fixtures.js'; import { makeMarshal } from '@endo/marshal'; +import type { Writable } from 'node:stream'; export const flags = ( record: Record, @@ -17,20 +17,17 @@ export const flags = ( const marshalData = makeMarshal(_v => assert.fail('data only')); +const mockStream = (buf: string[]): T => + ({ write: txt => (buf.push(txt), true) }) as T; + test('fast-usdc operator attest sub-command', async t => { const evidence = harden(MockCctpTxEvidences.AGORIC_PLUS_DYDX()); const { aux, tx, ...flat } = evidence; const argv = [ ...`node fast-usdc operator attest`.split(' '), - ...flags({ - previousOfferId: 123, - forwardingChannel: aux.forwardingChannel, - recipientAddress: aux.recipientAddress, - amount: tx.amount, - forwardingAddress: tx.forwardingAddress, - ...flat, - }), + ...flags({ previousOfferId: 123, ...aux, ...tx, ...flat }), ]; + t.log(...argv); const program = new Command(); program.exitOverride(); const out = [] as string[]; @@ -38,18 +35,8 @@ test('fast-usdc operator attest sub-command', async t => { addOperatorCommands(program, { fetch: null as unknown as Window['fetch'], - stdout: { - write: txt => { - out.push(txt); - return true; - }, - } as unknown as typeof process.stdout, - stderr: { - write: txt => { - err.push(txt); - return true; - }, - } as unknown as typeof process.stderr, + stdout: mockStream(out), + stderr: mockStream(err), env: {}, now: () => 1234, }); From ccb969e105689d5454f9519820bdf4b3eac97025 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Tue, 3 Dec 2024 13:43:09 -0600 Subject: [PATCH 07/11] refactor: move mockStream(...) to fast-usdc/tools/ --- packages/fast-usdc/package.json | 3 ++- .../fast-usdc/test/cli/operator-commands.test.ts | 7 ++----- packages/fast-usdc/tools/mock-io.ts | 14 ++++++++++++++ 3 files changed, 18 insertions(+), 6 deletions(-) create mode 100644 packages/fast-usdc/tools/mock-io.ts diff --git a/packages/fast-usdc/package.json b/packages/fast-usdc/package.json index 22759fefcd8..0643f9bf809 100644 --- a/packages/fast-usdc/package.json +++ b/packages/fast-usdc/package.json @@ -5,7 +5,8 @@ "type": "module", "files": [ "contract", - "src" + "src", + "tools" ], "bin": { "fast-usdc": "./src/cli/bin.js" diff --git a/packages/fast-usdc/test/cli/operator-commands.test.ts b/packages/fast-usdc/test/cli/operator-commands.test.ts index b0e25b26109..0dcc411cf73 100644 --- a/packages/fast-usdc/test/cli/operator-commands.test.ts +++ b/packages/fast-usdc/test/cli/operator-commands.test.ts @@ -1,9 +1,9 @@ +import { makeMarshal } from '@endo/marshal'; import test from 'ava'; import { Command } from 'commander'; import { addOperatorCommands } from '../../src/cli/operator-commands.js'; +import { mockStream } from '../../tools/mock-io.js'; import { MockCctpTxEvidences } from '../fixtures.js'; -import { makeMarshal } from '@endo/marshal'; -import type { Writable } from 'node:stream'; export const flags = ( record: Record, @@ -17,9 +17,6 @@ export const flags = ( const marshalData = makeMarshal(_v => assert.fail('data only')); -const mockStream = (buf: string[]): T => - ({ write: txt => (buf.push(txt), true) }) as T; - test('fast-usdc operator attest sub-command', async t => { const evidence = harden(MockCctpTxEvidences.AGORIC_PLUS_DYDX()); const { aux, tx, ...flat } = evidence; diff --git a/packages/fast-usdc/tools/mock-io.ts b/packages/fast-usdc/tools/mock-io.ts new file mode 100644 index 00000000000..0cb6ced28e6 --- /dev/null +++ b/packages/fast-usdc/tools/mock-io.ts @@ -0,0 +1,14 @@ +import type { Writable } from 'node:stream'; + +/** + * mock stdout / stderr, for example + * + * @param buf - caller-provided buffer for written data + */ +export const mockStream = (buf: string[]): T => + ({ + write: txt => { + buf.push(txt); + return true; + }, + }) as T; From 8674c750bd0db5a6e33049d4b994016b0b9e52b3 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Tue, 3 Dec 2024 13:47:16 -0600 Subject: [PATCH 08/11] refactor: move flags() to fast-usdc/tools/ --- packages/fast-usdc/test/cli/operator-commands.test.ts | 11 +---------- packages/fast-usdc/tools/cli-tools.ts | 9 +++++++++ 2 files changed, 10 insertions(+), 10 deletions(-) create mode 100644 packages/fast-usdc/tools/cli-tools.ts diff --git a/packages/fast-usdc/test/cli/operator-commands.test.ts b/packages/fast-usdc/test/cli/operator-commands.test.ts index 0dcc411cf73..36c645adb8a 100644 --- a/packages/fast-usdc/test/cli/operator-commands.test.ts +++ b/packages/fast-usdc/test/cli/operator-commands.test.ts @@ -2,19 +2,10 @@ import { makeMarshal } from '@endo/marshal'; import test from 'ava'; import { Command } from 'commander'; import { addOperatorCommands } from '../../src/cli/operator-commands.js'; +import { flags } from '../../tools/cli-tools.js'; import { mockStream } from '../../tools/mock-io.js'; import { MockCctpTxEvidences } from '../fixtures.js'; -export const flags = ( - record: Record, -): string[] => { - // @ts-expect-error undefined is filtered out - const skipUndef: [string, string][] = Object.entries(record).filter( - ([_k, v]) => v !== undefined, - ); - return skipUndef.map(([k, v]) => [`--${k}`, `${v}`]).flat(); -}; - const marshalData = makeMarshal(_v => assert.fail('data only')); test('fast-usdc operator attest sub-command', async t => { diff --git a/packages/fast-usdc/tools/cli-tools.ts b/packages/fast-usdc/tools/cli-tools.ts new file mode 100644 index 00000000000..12af12653e3 --- /dev/null +++ b/packages/fast-usdc/tools/cli-tools.ts @@ -0,0 +1,9 @@ +export const flags = ( + record: Record, +): string[] => { + // @ts-expect-error undefined is filtered out + const skipUndef: [string, string][] = Object.entries(record).filter( + ([_k, v]) => v !== undefined, + ); + return skipUndef.map(([k, v]) => [`--${k}`, `${v}`]).flat(); +}; From 13e68a99c21418368df9e4276cc10cd10190e54e Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Tue, 3 Dec 2024 13:59:26 -0600 Subject: [PATCH 09/11] chore: use AGORIC_NET for agoric RPC endpoint in transfer cmd --- packages/fast-usdc/src/cli/config.js | 1 - packages/fast-usdc/src/cli/transfer.js | 13 ++++++++++--- packages/fast-usdc/test/cli/transfer.test.ts | 1 + 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/fast-usdc/src/cli/config.js b/packages/fast-usdc/src/cli/config.js index 2dd63fdad2d..2b67a4868a3 100644 --- a/packages/fast-usdc/src/cli/config.js +++ b/packages/fast-usdc/src/cli/config.js @@ -6,7 +6,6 @@ import { stdin as input, stdout as output } from 'node:process'; nobleSeed: string, ethSeed: string, nobleToAgoricChannel: string, - agoricRpc: string, nobleApi: string, nobleRpc: string, ethRpc: string, diff --git a/packages/fast-usdc/src/cli/transfer.js b/packages/fast-usdc/src/cli/transfer.js index f27004f5cc0..9120f93be0a 100644 --- a/packages/fast-usdc/src/cli/transfer.js +++ b/packages/fast-usdc/src/cli/transfer.js @@ -1,13 +1,18 @@ +/* eslint-env node */ /* global globalThis */ -import { makeVStorage } from '@agoric/client-utils'; +import { + fetchEnvNetworkConfig, + makeVStorage, + pickEndpoint, +} from '@agoric/client-utils'; +import { queryFastUSDCLocalChainAccount } from '../util/agoric.js'; import { depositForBurn, makeProvider } from '../util/cctp.js'; import { makeSigner, queryForwardingAccount, registerFwdAccount, } from '../util/noble.js'; -import { queryFastUSDCLocalChainAccount } from '../util/agoric.js'; /** @import { File } from '../util/file' */ /** @import { VStorage } from '@agoric/client-utils' */ @@ -23,13 +28,15 @@ const transfer = async ( /** @type {VStorage | undefined} */ vstorage, /** @type {{signer: SigningStargateClient, address: string} | undefined} */ nobleSigner, /** @type {ethProvider | undefined} */ ethProvider, + env = process.env, ) => { const execute = async ( /** @type {import('./config').ConfigOpts} */ config, ) => { + const netConfig = await fetchEnvNetworkConfig({ env, fetch }); vstorage ||= makeVStorage( { fetch }, - { chainName: 'agoric', rpcAddrs: [config.agoricRpc] }, + { chainName: 'agoric', rpcAddrs: [pickEndpoint(netConfig)] }, ); const agoricAddr = await queryFastUSDCLocalChainAccount(vstorage, out); const appendedAddr = `${agoricAddr}?EUD=${destination}`; diff --git a/packages/fast-usdc/test/cli/transfer.test.ts b/packages/fast-usdc/test/cli/transfer.test.ts index d820492c2ed..d3c319f1f6b 100644 --- a/packages/fast-usdc/test/cli/transfer.test.ts +++ b/packages/fast-usdc/test/cli/transfer.test.ts @@ -142,6 +142,7 @@ test('Transfer signs and broadcasts the depositForBurn message on Ethereum', asy vstorageMock.vstorage, { signer: signerMock.signer, address: nobleSignerAddress }, mockEthProvider.provider, + {}, ); t.is(signerMock.getSigned(), undefined); From 4c92dbbac7cc59895ae747ea7ca1c3b8480186dd Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Tue, 3 Dec 2024 15:30:00 -0600 Subject: [PATCH 10/11] docs(fast-usdc): add cli help text re AGORIC_NET --- packages/fast-usdc/src/cli/cli.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/packages/fast-usdc/src/cli/cli.js b/packages/fast-usdc/src/cli/cli.js index afaf1eadf9a..9488bf574c2 100644 --- a/packages/fast-usdc/src/cli/cli.js +++ b/packages/fast-usdc/src/cli/cli.js @@ -62,6 +62,19 @@ export const initProgram = ( return makeFile(getConfigPath(), readFile, writeFile, mkdir, exists); }; + program.addHelpText( + 'afterAll', + ` + Agoric test networks provide configuration info at, for example, + + https://devnet.agoric.net/network-config + + To use RPC endpoints from such a configuration, use: + export AGORIC_NET=devnet + + Use AGORIC_NET=local or leave it unset to use localhost and chain id agoriclocal. + `, + ); addConfigCommands(program, configHelpers, makeConfigFile); addOperatorCommands(program, { fetch, From 918eabee99e26754f7d3b0dde1c84fbae68113c2 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Tue, 3 Dec 2024 19:12:42 -0600 Subject: [PATCH 11/11] chore(fast-usdc): regen snapshots with AGORIC_NET docs --- .../test/cli/snapshots/cli.test.ts.md | 96 ++++++++++++++++-- .../test/cli/snapshots/cli.test.ts.snap | Bin 1557 -> 1731 bytes 2 files changed, 88 insertions(+), 8 deletions(-) diff --git a/packages/fast-usdc/test/cli/snapshots/cli.test.ts.md b/packages/fast-usdc/test/cli/snapshots/cli.test.ts.md index ed8f08e0f82..07607d1eab1 100644 --- a/packages/fast-usdc/test/cli/snapshots/cli.test.ts.md +++ b/packages/fast-usdc/test/cli/snapshots/cli.test.ts.md @@ -25,7 +25,17 @@ Generated by [AVA](https://avajs.dev). withdraw [options] Withdraw assets from the liquidity pool␊ transfer Transfer USDC from Ethereum/L2 to Cosmos via Fast␊ USDC␊ - help [command] display help for command` + help [command] display help for command␊ + ␊ + Agoric test networks provide configuration info at, for example,␊ + ␊ + https://devnet.agoric.net/network-config␊ + ␊ + To use RPC endpoints from such a configuration, use:␊ + export AGORIC_NET=devnet␊ + ␊ + Use AGORIC_NET=local or leave it unset to use localhost and chain id agoriclocal.␊ + ` ## shows help for transfer command @@ -40,7 +50,17 @@ Generated by [AVA](https://avajs.dev). dest Destination address in Cosmos␊ ␊ Options:␊ - -h, --help display help for command` + -h, --help display help for command␊ + ␊ + Agoric test networks provide configuration info at, for example,␊ + ␊ + https://devnet.agoric.net/network-config␊ + ␊ + To use RPC endpoints from such a configuration, use:␊ + export AGORIC_NET=devnet␊ + ␊ + Use AGORIC_NET=local or leave it unset to use localhost and chain id agoriclocal.␊ + ` ## shows help for config command @@ -57,7 +77,17 @@ Generated by [AVA](https://avajs.dev). show Show current config␊ init [options] Set initial config values␊ update [options] Update config values␊ - help [command] display help for command` + help [command] display help for command␊ + ␊ + Agoric test networks provide configuration info at, for example,␊ + ␊ + https://devnet.agoric.net/network-config␊ + ␊ + To use RPC endpoints from such a configuration, use:␊ + export AGORIC_NET=devnet␊ + ␊ + Use AGORIC_NET=local or leave it unset to use localhost and chain id agoriclocal.␊ + ` ## shows help for config init command @@ -93,7 +123,17 @@ Generated by [AVA](https://avajs.dev). "0xbd3fa81b58ba92a82136038b25adec7066af3155")␊ --token-contract-address [address] Address of USDC token contract (default:␊ "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48")␊ - -h, --help display help for command` + -h, --help display help for command␊ + ␊ + Agoric test networks provide configuration info at, for example,␊ + ␊ + https://devnet.agoric.net/network-config␊ + ␊ + To use RPC endpoints from such a configuration, use:␊ + export AGORIC_NET=devnet␊ + ␊ + Use AGORIC_NET=local or leave it unset to use localhost and chain id agoriclocal.␊ + ` ## shows help for config update command @@ -120,7 +160,17 @@ Generated by [AVA](https://avajs.dev). --noble-to-agoric-channel [channel] Channel ID on Noble for Agoric␊ --token-messenger-address [address] Address of TokenMessenger contract␊ --token-contract-address [address] Address of USDC token contract␊ - -h, --help display help for command` + -h, --help display help for command␊ + ␊ + Agoric test networks provide configuration info at, for example,␊ + ␊ + https://devnet.agoric.net/network-config␊ + ␊ + To use RPC endpoints from such a configuration, use:␊ + export AGORIC_NET=devnet␊ + ␊ + Use AGORIC_NET=local or leave it unset to use localhost and chain id agoriclocal.␊ + ` ## shows help for config show command @@ -131,7 +181,17 @@ Generated by [AVA](https://avajs.dev). Show current config␊ ␊ Options:␊ - -h, --help display help for command` + -h, --help display help for command␊ + ␊ + Agoric test networks provide configuration info at, for example,␊ + ␊ + https://devnet.agoric.net/network-config␊ + ␊ + To use RPC endpoints from such a configuration, use:␊ + export AGORIC_NET=devnet␊ + ␊ + Use AGORIC_NET=local or leave it unset to use localhost and chain id agoriclocal.␊ + ` ## shows help for deposit command @@ -147,7 +207,17 @@ Generated by [AVA](https://avajs.dev). Options:␊ --id [offer-id] Offer ID␊ --fee [fee] Cosmos fee␊ - -h, --help display help for command` + -h, --help display help for command␊ + ␊ + Agoric test networks provide configuration info at, for example,␊ + ␊ + https://devnet.agoric.net/network-config␊ + ␊ + To use RPC endpoints from such a configuration, use:␊ + export AGORIC_NET=devnet␊ + ␊ + Use AGORIC_NET=local or leave it unset to use localhost and chain id agoriclocal.␊ + ` ## shows help for withdraw command @@ -163,7 +233,17 @@ Generated by [AVA](https://avajs.dev). Options:␊ --id [offer-id] Offer ID␊ --fee [fee] Cosmos fee␊ - -h, --help display help for command` + -h, --help display help for command␊ + ␊ + Agoric test networks provide configuration info at, for example,␊ + ␊ + https://devnet.agoric.net/network-config␊ + ␊ + To use RPC endpoints from such a configuration, use:␊ + export AGORIC_NET=devnet␊ + ␊ + Use AGORIC_NET=local or leave it unset to use localhost and chain id agoriclocal.␊ + ` ## shows error when deposit command is run without options diff --git a/packages/fast-usdc/test/cli/snapshots/cli.test.ts.snap b/packages/fast-usdc/test/cli/snapshots/cli.test.ts.snap index 2150beaaf1008567472a39ea2e1411ad7bedd15b..2d0eac4dcb4aaa55da830deb8489ef90b7b6a80e 100644 GIT binary patch literal 1731 zcmV;!20ZyeRzV=})o&-5b0}&8)7|yur7LrC z{uX~r6d#KS00000000B+S}d< z$j^6I?%ew9mL4njZhdk4-#aIg^l%4UB2_t(jtw0iD$1EWg^taGfH3FOvf3{WKyjc< zp&+)3HNi6?6`Z`;Z-Y>%Q{L#>{tNhAJ?EDGsq1dsW4KYJcM+f zML`#Zey!~B0AV*I%6pQ3?YW+Cs6bnBaj9WN62avHY|dQTgKr&lN#rZ7e!^J&b-gk$ zd-ai4>S?U8Plty?W$2U3#Nmmq*f*BdPX5Hk8UW+QOTtJG%?b;EhbV~RV$9MbL2Mtd zRv1;%0&p|80%exLrEd!3zshAeAh^NX!fl;vjqOM`JxeU03J|!`KCWlB%u^@5! zfQ?he1z@ko1+_t;RKQT3b8#ji6ueKJG!T&>+6_RN%Ymr1SOi{>AoOuf%kY#6<<9!L zgMEgoLSm5>hHBkNESrd00LKXvU%zStGAHDerEZjw?E%T_T+@p+Y49TCLc!h-N3Rds zzZ^b2{w`6bMV?5^PWap=K5zj(l0E`e5HX1=-2?Ho$E5#nVuHom?(;NN7j)Xv_t};7ixdg4(p^BeU;-{GV#E(#C^eA+Q z!tDB!izu~V0<>vGWh_W&Fwux%3(O9B@#@nuL4-DRBH^EoS#2O^H{rC! z@YPt=+kR(#y;k3GmgpZ zvf1$%(dBy0fKdDlSvf!{k@ZlN)0(bh{+t5rnMd4(W4-!ix>#prr9W(^RX;o4>P6St za7nAyZM3>%yG~m5+D5aw(dyP4#6f$j+H4Ycqt`8%*Gbd|LTuaBV3~ZSTB&l{= z&F$@)-K^JI-HoPQb86epCaL2#cB?jSG^XS9Tl_)$4R{qQ#1;4Kp9R;zN>?iA(tBvKI*usPQ)lg8= z`~Bieu`hE0y*qj-R@5S}tSLGK`Ab&)UT*uEogYsOl?Bv+4%Z6GRNqb|x(@c^ITsN+ zE~3ek=6csf9G5nhY+Bfj{-E1aOs5AqI(BC4y=w1W*LooY%cj>og$ z0LuEr*G9$tp;0Vh4olZyF_Ovy2=P}Clo(fKQcfDwC9NkFuX!+pw*rS;0RabX8W7*K zwJWI^t+~u_5l3yXb2gk98xr?JDYHXL8H`1yRNxsR1d&wGMIhJ19!@YHjs`kw3}~=B z6OfzXK@9jB9&FEN$Bh$VbQ_N8pG?h#=Q-p%2Kj&kS0;?KM@qoB0vV!BU5ZXIPlsFE zQ)Ao8VS98vPZN$!VCoc}Lgj(BZxqh69Gq6MtA;u@1&;cD9(2hOQ2_8D2k>z*z|p&o Z0*p`Q!5A~80OG>v_8-Ax@1c|>004MELcIV0 literal 1557 zcmV+w2I~1iRzV3h~XCGT;{y0t{og2#|qD7+g>l@I(O-qa;G6B$bTBGT=)7^49Z+ zB0p~Y@zL7tHQm-fUi)O@_l;9YMz{w)k*c0Z&xHYx73EBx!NBEFM3{HxIQ`=zP#h>z zD2S`lmEbXv3Qk`f_93LN6Y5cQ2{GrP?NcYVXYClOEdJ5CGQpGWFv6Qmm1S5vxU zQF^ybsj)Ei?6D)K3iC2ys^Qy0d!fI$i4e%tRg6+*g_<6RD*Z}Hm}5RCVS+j~lh`8) zi{nqTFOvhya-K){=5)eSe3h)Vi4N1$vM$WUjc3jqrUMBFIarFiLTu3TC4! zY-I-2IdT@|Vf#c`@Ef+-W!XOdh*_6=+^0&|XfXs_I zWopsfebqJtrQ*G4Erm@skiKA5mU z%p$^oICy?^-9+>Z1|66)X@ILh)LcBeMi{-${f5(J?rzKNwp+d7cGqortzGXvY2z*qn=Wp5n_V2<@6Eg3U$uFu_|EiHoaCv#A{pQM z%}U1aZdE1Y$ydY#KGH{2)p_^BPsQVIJ*+zIQX|Eu$CP*2s;|_~CvfMrlS<5g zTSxStYbDTC)fdNEp!i}x?e#os7t98x9TTTJ^F{$lVkb ze8g$HzihE&TrH}8*>6~!{Z;0yvf|`8i&mU$=Je#L%;hWH6ni@vdnkmaPS<276bDpV zWm-c>S%@7gcxtJ|J&353l#M{(>xAm`ke7L(rbScBaXeTC2T*oSLh6}XQyEKaVHp}M z#!`I@A-$GBjd4{c^{k3p)9d8sHD42W8}N)PAmE`(BNCe0ZzFf3w~`w^V%Z1R^5N9@ zkkJpdf(&W}j8(2Q;4vaZiBvE|AoF2QM$!9|fewwa4EC1-ax*$e0ngFF?rMJAxDd)= zIHhX0uoj+J$oCBLu?DVOC>7;uz_Vy?UeX|O>><(E0@W=xApc-KL@lk>C;VKwYrBpz?Ho5%+-)*F6 Hxf}oh{;Bw$