From fb2d05c0d755e1ad68aed1ae1112ea4973aad92e Mon Sep 17 00:00:00 2001 From: Samuel Siegart Date: Thu, 14 Nov 2024 20:20:40 -0800 Subject: [PATCH] feat(fast-usdc): add cli config and args for deposit and withdraw (#10487) refs https://github.com/Agoric/agoric-sdk/issues/10328 --- packages/fast-usdc/src/cli/cli.js | 56 +++++++- packages/fast-usdc/test/cli/cli.test.ts | 114 ++++++++++++++- .../test/cli/snapshots/cli.test.ts.md | 136 ++++++++++++++++-- .../test/cli/snapshots/cli.test.ts.snap | Bin 1193 -> 1538 bytes 4 files changed, 286 insertions(+), 20 deletions(-) diff --git a/packages/fast-usdc/src/cli/cli.js b/packages/fast-usdc/src/cli/cli.js index c4504830833..42d63045f56 100644 --- a/packages/fast-usdc/src/cli/cli.js +++ b/packages/fast-usdc/src/cli/cli.js @@ -1,4 +1,9 @@ -import { Command } from 'commander'; +import { assertParsableNumber } from '@agoric/zoe/src/contractSupport/ratio.js'; +import { + Command, + InvalidArgumentError, + InvalidOptionArgumentError, +} from 'commander'; import { existsSync, mkdirSync, readFileSync } from 'fs'; import { fileURLToPath } from 'url'; import { dirname, resolve } from 'path'; @@ -69,19 +74,28 @@ export const initProgram = ( '--eth-seed ', 'Seed phrase for Ethereum account. CAUTION: Stored unencrypted in file system', ) + .requiredOption( + '--agoric-seed ', + 'Seed phrase for Agoric LP account. CAUTION: Stored unencrypted in file system', + ) .option( '--agoric-rpc [url]', 'Agoric RPC endpoint', + 'http://127.0.0.1:26656', + ) + .option( + '--agoric-api [url]', + 'Agoric RPC endpoint', 'http://127.0.0.1:1317', ) + .option('--noble-rpc [url]', 'Noble RPC endpoint', 'http://127.0.0.1:26657') .option('--noble-api [url]', 'Noble API endpoint', 'http://127.0.0.1:1318') + .option('--eth-rpc [url]', 'Ethereum RPC Endpoint', 'http://127.0.0.1:8545') .option( '--noble-to-agoric-channel [channel]', 'Channel ID on Noble for Agoric', 'channel-21', ) - .option('--noble-rpc [url]', 'Noble RPC endpoint', 'http://127.0.0.1:26657') - .option('--eth-rpc [url]', 'Ethereum RPC Endpoint', 'http://127.0.0.1:8545') .option( '--token-messenger-address [address]', 'Address of TokenMessenger contract', @@ -109,10 +123,15 @@ export const initProgram = ( '--eth-seed [string]', 'Seed phrase for Ethereum account. CAUTION: Stored unencrypted in file system', ) + .option( + '--agoric-seed ', + 'Seed phrase for Agoric LP account. CAUTION: Stored unencrypted in file system', + ) .option('--agoric-rpc [url]', 'Agoric RPC endpoint') + .option('--agoric-api [url]', 'Agoric API endpoint') .option('--noble-rpc [url]', 'Noble RPC endpoint') - .option('--eth-rpc [url]', 'Ethereum RPC Endpoint') .option('--noble-api [url]', 'Noble API endpoint') + .option('--eth-rpc [url]', 'Ethereum RPC Endpoint') .option( '--noble-to-agoric-channel [channel]', 'Channel ID on Noble for Agoric', @@ -129,9 +148,35 @@ export const initProgram = ( await configHelpers.update(makeConfigFile(), options); }); + /** @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 @@ -140,6 +185,9 @@ export const initProgram = ( 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 diff --git a/packages/fast-usdc/test/cli/cli.test.ts b/packages/fast-usdc/test/cli/cli.test.ts index 6726da3d775..ae0d53216a5 100644 --- a/packages/fast-usdc/test/cli/cli.test.ts +++ b/packages/fast-usdc/test/cli/cli.test.ts @@ -105,12 +105,78 @@ test('shows help for config show command', async t => { t.snapshot(output); }); +test('shows help for deposit command', async t => { + const output = await collectStdOut([CLI_PATH, 'deposit', '-h']); + + t.snapshot(output); +}); + +test('shows help for withdraw command', async t => { + const output = await collectStdOut([CLI_PATH, 'withdraw', '-h']); + + t.snapshot(output); +}); + +test('shows error when deposit command is run without options', async t => { + const output = await collectStdErr([CLI_PATH, 'deposit']); + + t.snapshot(output); +}); + +test('shows error when deposit command is run with invalid amount', async t => { + const output = await collectStdErr([CLI_PATH, 'deposit', 'not-a-number']); + + t.snapshot(output); +}); + +test('shows error when deposit command is run with invalid fee', async t => { + const output = await collectStdErr([ + CLI_PATH, + 'deposit', + '50', + '--fee', + 'not-a-number', + ]); + + t.snapshot(output); +}); + +test('shows error when withdraw command is run without options', async t => { + const output = await collectStdErr([CLI_PATH, 'withdraw']); + + t.snapshot(output); +}); + +test('shows error when withdraw command is run with invalid amount', async t => { + const output = await collectStdErr([CLI_PATH, 'withdraw', 'not-a-number']); + + t.snapshot(output); +}); + +test('shows error when withdraw command is run with invalid fee', async t => { + const output = await collectStdErr([ + CLI_PATH, + 'withdraw', + '50', + '--fee', + 'not-a-number', + ]); + + t.snapshot(output); +}); + test('shows error when config init command is run without options', async t => { const output = await collectStdErr([CLI_PATH, 'config', 'init']); t.snapshot(output); }); +test('shows error when transfer command is run without options', async t => { + const output = await collectStdErr([CLI_PATH, 'transfer']); + + t.snapshot(output); +}); + test('shows error when config init command is run without eth seed', async t => { const output = await collectStdErr([ CLI_PATH, @@ -118,6 +184,36 @@ test('shows error when config init command is run without eth seed', async t => 'init', '--noble-seed', 'foo', + '--agoric-seed', + 'bar', + ]); + + t.snapshot(output); +}); + +test('shows error when config init command is run without agoric seed', async t => { + const output = await collectStdErr([ + CLI_PATH, + 'config', + 'init', + '--noble-seed', + 'foo', + '--eth-seed', + 'bar', + ]); + + t.snapshot(output); +}); + +test('shows error when config init command is run without noble seed', async t => { + const output = await collectStdErr([ + CLI_PATH, + 'config', + 'init', + '--agoric-seed', + 'foo', + '--eth-seed', + 'bar', ]); t.snapshot(output); @@ -139,13 +235,17 @@ test('calls config init with default args', t => { 'foo', '--eth-seed', 'bar', + '--agoric-seed', + 'bazinga', ]); const args = config.getInitArgs(); t.is(args.shift().path, `${homeDir}config.json`); t.deepEqual(args, [ { - agoricRpc: 'http://127.0.0.1:1317', + agoricSeed: 'bazinga', + agoricApi: 'http://127.0.0.1:1317', + agoricRpc: 'http://127.0.0.1:26656', ethRpc: 'http://127.0.0.1:8545', ethSeed: 'bar', nobleRpc: 'http://127.0.0.1:26657', @@ -174,6 +274,10 @@ test('calls config init with optional args', t => { 'foo', '--eth-seed', 'bar', + '--agoric-seed', + 'bazinga', + '--agoric-api', + '127.0.0.1:0000', '--agoric-rpc', '127.0.0.1:1111', '--eth-rpc', @@ -194,6 +298,8 @@ test('calls config init with optional args', t => { t.is(args.shift().path, `${homeDir}config.json`); t.deepEqual(args, [ { + agoricApi: '127.0.0.1:0000', + agoricSeed: 'bazinga', agoricRpc: '127.0.0.1:1111', ethRpc: '127.0.0.1:2222', ethSeed: 'bar', @@ -223,6 +329,10 @@ test('calls config update with args', t => { 'foo', '--eth-seed', 'bar', + '--agoric-seed', + 'bazinga', + '--agoric-api', + '127.0.0.1:0000', '--agoric-rpc', '127.0.0.1:1111', '--eth-rpc', @@ -243,6 +353,8 @@ test('calls config update with args', t => { t.is(args.shift().path, `${homeDir}config.json`); t.deepEqual(args, [ { + agoricSeed: 'bazinga', + agoricApi: '127.0.0.1:0000', agoricRpc: '127.0.0.1:1111', ethRpc: '127.0.0.1:2222', ethSeed: 'bar', 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 a96f75d613c..b044f515a6a 100644 --- a/packages/fast-usdc/test/cli/snapshots/cli.test.ts.md +++ b/packages/fast-usdc/test/cli/snapshots/cli.test.ts.md @@ -13,18 +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␊ - deposit Offer assets to the liquidity pool␊ - withdraw 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␊ + 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␊ ` ## shows help for transfer command @@ -75,16 +75,21 @@ Generated by [AVA](https://avajs.dev). --eth-seed Seed phrase for Ethereum account.␊ CAUTION: Stored unencrypted in file␊ system␊ + --agoric-seed Seed phrase for Agoric LP account.␊ + CAUTION: Stored unencrypted in file␊ + system␊ --agoric-rpc [url] Agoric RPC endpoint (default:␊ + "http://127.0.0.1:26656")␊ + --agoric-api [url] Agoric RPC endpoint (default:␊ "http://127.0.0.1:1317")␊ - --noble-api [url] Noble API endpoint (default:␊ - "http://127.0.0.1:1318")␊ - --noble-to-agoric-channel [channel] Channel ID on Noble for Agoric (default:␊ - "channel-21")␊ --noble-rpc [url] Noble RPC endpoint (default:␊ "http://127.0.0.1:26657")␊ + --noble-api [url] Noble API endpoint (default:␊ + "http://127.0.0.1:1318")␊ --eth-rpc [url] Ethereum RPC Endpoint (default:␊ "http://127.0.0.1:8545")␊ + --noble-to-agoric-channel [channel] Channel ID on Noble for Agoric (default:␊ + "channel-21")␊ --token-messenger-address [address] Address of TokenMessenger contract␊ (default:␊ "0xbd3fa81b58ba92a82136038b25adec7066af3155")␊ @@ -107,10 +112,14 @@ Generated by [AVA](https://avajs.dev). --eth-seed [string] Seed phrase for Ethereum account.␊ CAUTION: Stored unencrypted in file␊ system␊ + --agoric-seed Seed phrase for Agoric LP account.␊ + CAUTION: Stored unencrypted in file␊ + system␊ --agoric-rpc [url] Agoric RPC endpoint␊ + --agoric-api [url] Agoric API endpoint␊ --noble-rpc [url] Noble RPC endpoint␊ - --eth-rpc [url] Ethereum RPC Endpoint␊ --noble-api [url] Noble API endpoint␊ + --eth-rpc [url] Ethereum RPC Endpoint␊ --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␊ @@ -129,6 +138,82 @@ Generated by [AVA](https://avajs.dev). -h, --help display help for command␊ ` +## 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␊ + ` + +## 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␊ + ` + +## shows error when deposit command is run without options + +> Snapshot 1 + + `error: missing required argument 'give'␊ + ` + +## 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.␊ + ` + +## shows error when withdraw command is run without options + +> Snapshot 1 + + `error: missing required argument 'want'␊ + ` + +## 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.␊ + ` + ## shows error when config init command is run without options > Snapshot 1 @@ -136,9 +221,30 @@ Generated by [AVA](https://avajs.dev). `error: required option '--noble-seed ' not specified␊ ` +## shows error when transfer command is run without options + +> Snapshot 1 + + `error: missing required argument 'amount'␊ + ` + ## shows error when config init command is run without eth seed > Snapshot 1 `error: required option '--eth-seed ' not specified␊ ` + +## shows error when config init command is run without agoric seed + +> Snapshot 1 + + `error: required option '--agoric-seed ' not specified␊ + ` + +## shows error when config init command is run without noble seed + +> Snapshot 1 + + `error: required option '--noble-seed ' not specified␊ + ` 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 c2e3eb0d1bdbabeb9219c8f5608f0c1130f3463c..ab682b37ff170ba9f0ee9ad2b29658c7d25af758 100644 GIT binary patch literal 1538 zcmV+d2L1U#RzVhHWW_U4h6KU-457x5Ohd8%#MF*$4-L`LDFs+`iHgY zHW;~}#UqQTM5-c{G#D^o&#)(G_XOKx?Rq!68z@nZXj%SS+U#PyFbt7A{LaVk@Q~+? zHw=XPQvUiD6;lDe{#^RvOoER=1ZO^Sh!YNHO!*>FK;lsnB3F`0P9o_GC4YHCDN*Fd zYrkJ#ySb*@`Uh(tU3+`&SdtO$fJdaNC6c-@5Rqbn%M%#5A`A(qCysOQ@CcLu#udiI zRq0CbfJg<$&kheDV6PHJnL39^h`@24WF~eTfZB^Mp;kM^STe!EY9dilqCoiwc|T9W zA;x-I%@-lUZbX#7XHWY^cTg6it3-URVI>m56EV1gdu#-^DS9LclwN*D+xU5_K6ZQK zj$YK)SW`|X8)Y&I$azM1wpPlG;~ZpvQfCc-Y3&i=WQ1mf13)nnlBojSyCZgr+SezZ zhcOT-QAzEdCTN0TNOyq7G!>coHCLsVK&*?iUS#aV_hxAa4z}J3X<3TWAJ8RS?uc{ zTVnrky(IRrjRK1{6^^qs6PdHlejbHd8JW?{)BWtf-r3wVMJ__di9!mDL!!Z@B9SWR z=MbgRBXpP$N@J8#56#xLL#`m>xvYLN!K#sPHK#p`(>rBOjfJV_h#du0pqB|b4d0g9 zGyTO);uyKQh&RTqVAJDJrC%8da?GbBNKiTeNkoal;`rn2%jCeapyx8aF`e-gU?uH0 zA6c~T-K?VB_S12yy|mb`aWM?ACJ`y@>feb&HzN!wM1D-nar; zZ|=0Z-A*@4&N&JavGT8L62 zA7Na}^Yg&`IRV%=pM(d`_2@_WV0|hpeW_k_`Q`C8UJq&8BfaLZ(;Je0i}YH}cDK>) z4O<;T(cNxzyTofZI~~iP9N08>ivFbcqIAmGmz6-0#<17z_nU6F)$9%1UAIY_eR`L) zu#dxri|uZsi^IFUd3W`THa8dFnx2c3JlR)d;~Rfi+4#2`RoQs*9^p7s?-7-$cpz2G z_^1?LSDT8fQ2n2Z-cZH%2RCRm+Gez!1D@9CFsAcL;BCP66&fNjN>d&g0bKCdY-ip zW((7fiSwON7zn*Ujp-+2w${;M+UFs{z(X{GHtD>*Y9_N;YF&UUR6b}A%Xq%F@bs(6nkX7pQT5d- o@UjbK1;TGFguB%U%P)~CkbA3uOx03>@$w}152Fb~J(e5*00pV#Gynhq literal 1193 zcmV;a1XlY&RzV1VM0o_`#VQa#f}jFRV?C~U4p33 zSJP2=C?AUm00000000BsSW9aoM-X1y*#su1TmvD6N!TnzwnnmL$!iQk-Xw>c=W3LGWa?4N1bps)PAb&0Q(DRUIq#5}UK_GoFLw9xcS6_cs)z&+I z5-RVt`So`sK_R~V+yv^oV_|4K`7oNf(yEWSgFu)PBIt!4nXU>FQL`CKy8GQP#!9wI3b__p;(qlG(l~5 zwE`6(9K?)*!^*C2>^v75JyQD0f=Ud6uQYf{`eF)SaP(Ocl0Ce6Z2r8xHH&%ku{|=d zKvP2J1{Wp{*;Ph%c2dfVF`n(>@THpSe)0pJ)bBdEsm#P?AHGX}}n&@4A| z+(ODd*X(j}`}f64?!nh8s`o*fNn_7CV3A5B9l`-eLx%vT#Y%dysfVW&pvENHe%7&| zM#@CWz=dF`0+!Njoj8y&H*1~27<6NUL)-*f2 znrJ2c-pJ}FbFAj#ZRWIJ;q>t$r@4pP>QPOACeU|DHVeP1v{!bhdm1=vXY`|nj~|NCJR?bSFPr#Z?B z`z@&^A+`)6hXeb!T5B_6?P45gmY3NVb_+1ivvS^o(eUi__~ga%YFlZV>+WS8Ex%qCi$)Cjbq-%1uNp(Yf`L@Q zh!-#^F@$mcaSkxbr;d+6$sCcr(wv~XLSs-)QMZb=GR{BF0fzaM^5N84KQD}JF(LZ`ye*QuLvO#G z@UGAL?qs(=VS^6qcie8T-R)00yNsi^*Y5S0-*tDZ@f5)7!YRj-w)1q$ES7tLWbH}6 zHyF5HujBS7-Ja)icffa82M0K5d)V!@dpOzY-|)69jT>9YXZ0;4DSA{x6WREy(nS8) zXlf$!?ay&)+h1b`85`0?ewvCoc$7`Q;+VvKY~EC88&( z*L~3VCJ{E{vVHKV=0*Dmq#`gedcqfYjql(-_>cmyUKas=T>&^~1~~V$orm`iGG@)T H>Jb0{o8(K8