Skip to content

Commit

Permalink
feat(fast-usdc): cli for lp deposit and withdraw
Browse files Browse the repository at this point in the history
  • Loading branch information
samsiegart committed Dec 9, 2024
1 parent 56d1c14 commit 9ec5c44
Show file tree
Hide file tree
Showing 7 changed files with 282 additions and 158 deletions.
16 changes: 12 additions & 4 deletions packages/fast-usdc/src/cli/bridge-action.js
Original file line number Diff line number Diff line change
Expand Up @@ -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>} BoardSlottingMarshaller */

/**
* @param {BridgeAction} bridgeAction
* @param {Pick<import('stream').Writable,'write'>} 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');
Expand All @@ -25,8 +28,13 @@ export const sendHint =
* stdout: Pick<import('stream').Writable,'write'>,
* stderr: Pick<import('stream').Writable,'write'>,
* }} 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);
};
54 changes: 3 additions & 51 deletions packages/fast-usdc/src/cli/cli.js
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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(
Expand Down Expand Up @@ -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('<give>', '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('<want>', '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')
Expand Down
135 changes: 135 additions & 0 deletions packages/fast-usdc/src/cli/lp-commands.js
Original file line number Diff line number Diff line change
@@ -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<ReturnType<typeof makeVstorageKit>>;
* 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<typeof loadVsk>} */
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 <number>', 'uUSDC amount', parseNat)
.option('--offerId <string>', '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 <number>', 'FastLP amount', parseNat)
.option('--offerId <string>', '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;
};
28 changes: 4 additions & 24 deletions packages/fast-usdc/test/cli/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});

Expand Down
Loading

0 comments on commit 9ec5c44

Please sign in to comment.