diff --git a/packages/fast-usdc/src/cli/bridge-action.js b/packages/fast-usdc/src/cli/bridge-action.js index 582028f23bf..45a53351b8c 100644 --- a/packages/fast-usdc/src/cli/bridge-action.js +++ b/packages/fast-usdc/src/cli/bridge-action.js @@ -4,13 +4,16 @@ import { boardSlottingMarshaller } from '@agoric/client-utils'; * @import {BridgeAction} from '@agoric/smart-wallet/src/smartWallet.js'; */ -const marshaller = boardSlottingMarshaller(); +const defaultMarshaller = boardSlottingMarshaller(); + +/** @typedef {ReturnType} BoardSlottingMarshaller */ /** * @param {BridgeAction} bridgeAction * @param {Pick} stdout + * @param {BoardSlottingMarshaller} marshaller */ -const outputAction = (bridgeAction, stdout) => { +const outputAction = (bridgeAction, stdout, marshaller) => { const capData = marshaller.toCapData(harden(bridgeAction)); stdout.write(JSON.stringify(capData)); stdout.write('\n'); @@ -25,8 +28,13 @@ export const sendHint = * stdout: Pick, * stderr: Pick, * }} io + * @param {BoardSlottingMarshaller | undefined} marshaller */ -export const outputActionAndHint = (bridgeAction, { stdout, stderr }) => { - outputAction(bridgeAction, stdout); +export const outputActionAndHint = ( + bridgeAction, + { stdout, stderr }, + marshaller = defaultMarshaller, +) => { + outputAction(bridgeAction, stdout, marshaller); stderr.write(sendHint); }; diff --git a/packages/fast-usdc/src/cli/cli.js b/packages/fast-usdc/src/cli/cli.js index 9488bf574c2..8676ef955e2 100644 --- a/packages/fast-usdc/src/cli/cli.js +++ b/packages/fast-usdc/src/cli/cli.js @@ -1,11 +1,6 @@ /* eslint-env node */ /* global globalThis */ -import { assertParsableNumber } from '@agoric/zoe/src/contractSupport/ratio.js'; -import { - Command, - InvalidArgumentError, - InvalidOptionArgumentError, -} from 'commander'; +import { Command } from 'commander'; import { existsSync, mkdirSync, readFileSync } from 'fs'; import { fileURLToPath } from 'url'; import { dirname, resolve } from 'path'; @@ -19,6 +14,7 @@ import { addOperatorCommands } from './operator-commands.js'; import * as configLib from './config.js'; import transferLib from './transfer.js'; import { makeFile } from '../util/file.js'; +import { addLPCommands } from './lp-commands.js'; const packageJson = JSON.parse( readFileSync( @@ -83,51 +79,7 @@ export const initProgram = ( env, now, }); - - /** @param {string} value */ - const parseDecimal = value => { - try { - assertParsableNumber(value); - } catch { - throw new InvalidArgumentError('Not a decimal number.'); - } - return value; - }; - - /** - * @param {string} str - * @returns {'auto' | number} - */ - const parseFee = str => { - if (str === 'auto') return 'auto'; - const num = parseFloat(str); - if (Number.isNaN(num)) { - throw new InvalidOptionArgumentError('Fee must be a number.'); - } - return num; - }; - - program - .command('deposit') - .description('Offer assets to the liquidity pool') - .argument('', 'USDC to give', parseDecimal) - .option('--id [offer-id]', 'Offer ID') - .option('--fee [fee]', 'Cosmos fee', parseFee) - .action(() => { - console.error('TODO actually send deposit'); - // TODO: Implement deposit logic - }); - - program - .command('withdraw') - .description('Withdraw assets from the liquidity pool') - .argument('', 'USDC to withdraw', parseDecimal) - .option('--id [offer-id]', 'Offer ID') - .option('--fee [fee]', 'Cosmos fee', parseFee) - .action(() => { - console.error('TODO actually send withdrawal'); - // TODO: Implement withdraw logic - }); + addLPCommands(program, { fetch, stdout, stderr, env, now }); program .command('transfer') diff --git a/packages/fast-usdc/src/cli/lp-commands.js b/packages/fast-usdc/src/cli/lp-commands.js new file mode 100644 index 00000000000..4947d02d7ac --- /dev/null +++ b/packages/fast-usdc/src/cli/lp-commands.js @@ -0,0 +1,135 @@ +/** + * @import {Command} from 'commander'; + * @import {OfferSpec} from '@agoric/smart-wallet/src/offers.js'; + * @import {ExecuteOfferAction} from '@agoric/smart-wallet/src/smartWallet.js'; + */ + +import { fetchEnvNetworkConfig, makeVstorageKit } from '@agoric/client-utils'; +import { Nat } from '@endo/nat'; +import { InvalidArgumentError } from 'commander'; +import { outputActionAndHint } from './bridge-action.js'; + +/** @param {string} arg */ +const parseNat = arg => { + try { + const n = Nat(BigInt(arg)); + return n; + } catch { + throw new InvalidArgumentError('Not a number'); + } +}; + +/** + * @param {Command} program + * @param {{ + * fetch?: Window['fetch']; + * vstorageKit?: Awaited>; + * stdout: typeof process.stdout; + * stderr: typeof process.stderr; + * env: typeof process.env; + * now: typeof Date.now; + * }} io + */ +export const addLPCommands = ( + program, + { fetch, vstorageKit, stderr, stdout, env, now }, +) => { + const operator = program + .command('lp') + .description('Liquidity Provider commands'); + + const loadVsk = async () => { + if (vstorageKit) { + return vstorageKit; + } + assert(fetch); + const networkConfig = await fetchEnvNetworkConfig({ env, fetch }); + return makeVstorageKit({ fetch }, networkConfig); + }; + /** @type {undefined | ReturnType} */ + let vskP; + + operator + .command('deposit') + .description('Deposit USDC into pool') + .addHelpText( + 'after', + '\nPipe the STDOUT to a file such as deposit.json, then use the Agoric CLI to broadcast it:\n agoric wallet send --offer accept.json --from gov1 --keyring-backend="test"', + ) + .requiredOption('--amount ', 'uUSDC amount', parseNat) + .option('--offerId ', 'Offer id', String, `lpDeposit-${now()}`) + .action(async opts => { + vskP ||= loadVsk(); + const vsk = await vskP; + + /** @type {Brand<'nat'>} */ + // @ts-expect-error it doesnt recognize usdc as a Brand type + const usdc = vsk.agoricNames.brand.USDC; + assert(usdc, 'USDC brand not in agoricNames'); + + /** @type {OfferSpec} */ + const offer = { + id: opts.offerId, + invitationSpec: { + source: 'agoricContract', + instancePath: ['fastUsdc'], + callPipe: [['makeDepositInvitation', []]], + }, + proposal: { + give: { + USDC: { brand: usdc, value: opts.amount }, + }, + }, + }; + + /** @type {ExecuteOfferAction} */ + const bridgeAction = { + method: 'executeOffer', + offer, + }; + + outputActionAndHint(bridgeAction, { stderr, stdout }, vsk.marshaller); + }); + + operator + .command('withdraw') + .description("Withdraw USDC from the LP's pool share") + .addHelpText( + 'after', + '\nPipe the STDOUT to a file such as withdraw.json, then use the Agoric CLI to broadcast it:\n agoric wallet send --offer accept.json --from gov1 --keyring-backend="test"', + ) + .requiredOption('--amount ', 'FastLP amount', parseNat) + .option('--offerId ', 'Offer id', String, `lpWithdraw-${now()}`) + .action(async opts => { + vskP ||= loadVsk(); + const vsk = await vskP; + + /** @type {Brand<'nat'>} */ + // @ts-expect-error it doesnt recognize usdc as a Brand type + const poolShare = vsk.agoricNames.brand.FastLP; + assert(poolShare, 'FastLP brand not in agoricNames'); + + /** @type {OfferSpec} */ + const offer = { + id: opts.offerId, + invitationSpec: { + source: 'agoricContract', + instancePath: ['fastUsdc'], + callPipe: [['makeWithdrawInvitation', []]], + }, + proposal: { + give: { + PoolShare: { brand: poolShare, value: opts.amount }, + }, + }, + }; + + outputActionAndHint( + { method: 'executeOffer', offer }, + { stderr, stdout }, + vsk.marshaller, + ); + }); + + return operator; +}; diff --git a/packages/fast-usdc/test/cli/cli.test.ts b/packages/fast-usdc/test/cli/cli.test.ts index cfde8387677..7eae770761e 100644 --- a/packages/fast-usdc/test/cli/cli.test.ts +++ b/packages/fast-usdc/test/cli/cli.test.ts @@ -81,43 +81,23 @@ test('shows help for config show command', async t => { t.snapshot(output); }); -test('shows help for deposit command', async t => { - const output = await runCli(['deposit', '-h']); - t.snapshot(output); -}); - -test('shows help for withdraw command', async t => { - const output = await runCli(['withdraw', '-h']); - t.snapshot(output); -}); - test('shows error when deposit command is run without options', async t => { - const output = await runCli(['deposit']); + const output = await runCli(['lp', 'deposit']); t.snapshot(output); }); test('shows error when deposit command is run with invalid amount', async t => { - const output = await runCli(['deposit', 'not-a-number']); - t.snapshot(output); -}); - -test('shows error when deposit command is run with invalid fee', async t => { - const output = await runCli(['deposit', '50', '--fee', 'not-a-number']); + const output = await runCli(['lp', 'deposit', '--amount', 'not-a-number']); t.snapshot(output); }); test('shows error when withdraw command is run without options', async t => { - const output = await runCli(['withdraw']); + const output = await runCli(['lp', 'withdraw']); t.snapshot(output); }); test('shows error when withdraw command is run with invalid amount', async t => { - const output = await runCli(['withdraw', 'not-a-number']); - t.snapshot(output); -}); - -test('shows error when withdraw command is run with invalid fee', async t => { - const output = await runCli(['withdraw', '50', '--fee', 'not-a-number']); + const output = await runCli(['lp', 'withdraw', '--amount', 'not-a-number']); t.snapshot(output); }); diff --git a/packages/fast-usdc/test/cli/lp-commands.test.ts b/packages/fast-usdc/test/cli/lp-commands.test.ts new file mode 100644 index 00000000000..b87c8281faf --- /dev/null +++ b/packages/fast-usdc/test/cli/lp-commands.test.ts @@ -0,0 +1,114 @@ +import { makeMarshal } from '@endo/marshal'; +import anyTest, { type TestFn } from 'ava'; +import { Command } from 'commander'; +import { flags } from '../../tools/cli-tools.js'; +import { mockStream } from '../../tools/mock-io.js'; +import { addLPCommands } from '../../src/cli/lp-commands.js'; + +const makeTestContext = () => { + const program = new Command(); + program.exitOverride(); + const out = [] as string[]; + const err = [] as string[]; + + const USDC = 'usdcbrand'; + const FastLP = 'fastlpbrand'; + const slotToVal = { + '0': USDC, + '1': FastLP, + }; + const valToSlot = { + USDC: '0', + FastLP: '1', + }; + const marshaller = makeMarshal( + val => valToSlot[val], + slot => slotToVal[slot], + ); + const now = () => 1234; + + addLPCommands(program, { + vstorageKit: { + // @ts-expect-error fake brands + agoricNames: { brand: { FastLP, USDC } }, + marshaller, + }, + stdout: mockStream(out), + stderr: mockStream(err), + env: {}, + now, + }); + + return { program, marshaller, out, err, USDC, FastLP, now }; +}; + +const test = anyTest as TestFn>>; +test.beforeEach(async t => (t.context = await makeTestContext())); + +test('fast-usdc lp deposit sub-command', async t => { + const { program, marshaller, out, err, USDC, now } = t.context; + const amount = 100; + const argv = [ + ...`node fast-usdc lp deposit`.split(' '), + ...flags({ amount }), + ]; + t.log(...argv); + await program.parseAsync(argv); + + const action = marshaller.fromCapData(JSON.parse(out.join(''))); + t.deepEqual(action, { + method: 'executeOffer', + offer: { + id: `lpDeposit-${now()}`, + invitationSpec: { + source: 'agoricContract', + instancePath: ['fastUsdc'], + callPipe: [['makeDepositInvitation', []]], + }, + proposal: { + give: { + USDC: { brand: USDC, value: BigInt(amount) }, + }, + }, + }, + }); + + t.is( + err.join(''), + 'Now use `agoric wallet send ...` to sign and broadcast the offer.\n', + ); +}); + +test('fast-usdc lp withdraw sub-command', async t => { + const { program, marshaller, out, err, FastLP, now } = t.context; + const amount = 100; + const argv = [ + ...`node fast-usdc lp withdraw`.split(' '), + ...flags({ amount }), + ]; + t.log(...argv); + await program.parseAsync(argv); + + const action = marshaller.fromCapData(JSON.parse(out.join(''))); + t.deepEqual(action, { + method: 'executeOffer', + offer: { + id: `lpWithdraw-${now()}`, + invitationSpec: { + source: 'agoricContract', + instancePath: ['fastUsdc'], + callPipe: [['makeWithdrawInvitation', []]], + }, + proposal: { + give: { + PoolShare: { brand: FastLP, value: BigInt(amount) }, + }, + }, + }, + }); + + t.is( + err.join(''), + 'Now use `agoric wallet send ...` to sign and broadcast the offer.\n', + ); +}); 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 07607d1eab1..855006de77e 100644 --- a/packages/fast-usdc/test/cli/snapshots/cli.test.ts.md +++ b/packages/fast-usdc/test/cli/snapshots/cli.test.ts.md @@ -13,19 +13,18 @@ Generated by [AVA](https://avajs.dev). CLI to interact with Fast USDC liquidity pool␊ ␊ Options:␊ - -V, --version output the version number␊ - --home Home directory to use for config (default:␊ - "~/.fast-usdc")␊ - -h, --help display help for command␊ + -V, --version output the version number␊ + --home Home directory to use for config (default:␊ + "~/.fast-usdc")␊ + -h, --help display help for command␊ ␊ Commands:␊ - config Manage config␊ - operator Oracle operator commands␊ - deposit [options] Offer assets to the liquidity pool␊ - 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␊ + config Manage config␊ + operator Oracle operator commands␊ + lp Liquidity Provider commands␊ + transfer Transfer USDC from Ethereum/L2 to Cosmos via Fast␊ + USDC␊ + help [command] display help for command␊ ␊ Agoric test networks provide configuration info at, for example,␊ ␊ @@ -193,93 +192,29 @@ Generated by [AVA](https://avajs.dev). Use AGORIC_NET=local or leave it unset to use localhost and chain id agoriclocal.␊ ` -## shows help for deposit command - -> Snapshot 1 - - `Usage: fast-usdc deposit [options] ␊ - ␊ - Offer assets to the liquidity pool␊ - ␊ - Arguments:␊ - give USDC to give␊ - ␊ - Options:␊ - --id [offer-id] Offer ID␊ - --fee [fee] Cosmos fee␊ - -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 - -> Snapshot 1 - - `Usage: fast-usdc withdraw [options] ␊ - ␊ - Withdraw assets from the liquidity pool␊ - ␊ - Arguments:␊ - want USDC to withdraw␊ - ␊ - Options:␊ - --id [offer-id] Offer ID␊ - --fee [fee] Cosmos fee␊ - -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 > Snapshot 1 - 'error: missing required argument \'give\'' + 'error: required option \'--amount \' not specified' ## shows error when deposit command is run with invalid amount > Snapshot 1 - 'error: command-argument value \'not-a-number\' is invalid for argument \'give\'. Not a decimal number.' - -## shows error when deposit command is run with invalid fee - -> Snapshot 1 - - 'error: option \'--fee [fee]\' argument \'not-a-number\' is invalid. Fee must be a number.' + 'error: option \'--amount \' argument \'not-a-number\' is invalid. Not a number' ## shows error when withdraw command is run without options > Snapshot 1 - 'error: missing required argument \'want\'' + 'error: required option \'--amount \' not specified' ## shows error when withdraw command is run with invalid amount > Snapshot 1 - 'error: command-argument value \'not-a-number\' is invalid for argument \'want\'. Not a decimal number.' - -## shows error when withdraw command is run with invalid fee - -> Snapshot 1 - - 'error: option \'--fee [fee]\' argument \'not-a-number\' is invalid. Fee must be a number.' + 'error: option \'--amount \' argument \'not-a-number\' is invalid. Not a number' ## shows error when config init 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 2d0eac4dcb4..eee94a8427c 100644 Binary files a/packages/fast-usdc/test/cli/snapshots/cli.test.ts.snap and b/packages/fast-usdc/test/cli/snapshots/cli.test.ts.snap differ