Skip to content

Commit

Permalink
Fast USDC CLI: Transfer (#10437)
Browse files Browse the repository at this point in the history
refs #10339

## Description

- Adds a `config` command for setting all the required variables relating to IBC channels, mnemonics, rpc endpoints, etc.
- Adds a `transfer` command for simulating the Noble Express frontend. It accepts an end user cosmos address destination, and a USDC amount as args. TODO: It should query the agoric Fast USDC LCA address, but for now it just stubs a hardcoded address there, because I'm not sure what the query should look like.
- Also TODO, add a command to query the status of pending transfers

### Documentation Considerations
Tried to make the `help` command as self-documenting as possible

### Testing Considerations
Manually tested on testnets with testnet tokens that:
- It queries the noble forwarding address for the "agoricXXX?EUD=dydxYYY" address 
- It registers the forwarding address on noble if it doesn't exist yet (I tested on noble mainnet for this part actually, not testnet)
- It sends a depositForBurn txn on ethereum (I used sepolia testnet) with the correctly encoded noble address: https://sepolia.etherscan.io/tx/0xde6d7ea6a6d2737e67524dca70372610c5ac0476d75d4c30fc4270e89e36de77

I put the config used for that test run in `demo/testnet` for reference.

Added unit tests for all the config stuff, but didn't unit test "transfer" yet. Maybe e2e tests would be better for that part...

### Upgrade Considerations
The CLI is a client program and doesn't need to be upgraded in production.
  • Loading branch information
mergify[bot] authored Nov 14, 2024
2 parents ff8c142 + d46cefd commit 511b789
Show file tree
Hide file tree
Showing 25 changed files with 1,689 additions and 94 deletions.
11 changes: 11 additions & 0 deletions packages/fast-usdc/demo/testnet/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"nobleSeed": "stamp later develop betray boss ranch abstract puzzle calm right bounce march orchard edge correct canal fault miracle void dutch lottery lucky observe armed",
"ethSeed": "a4b7f431465df5dc1458cd8a9be10c42da8e3729e3ce53f18814f48ae2a98a08",
"nobleToAgoricChannel": "channel-21",
"agoricRpc": "https://main.rpc.agoric.net",
"nobleRpc": "https://noble-rpc.polkachu.com",
"nobleApi": "https://noble-api.polkachu.com",
"ethRpc": "https://sepolia.drpc.org",
"tokenMessengerAddress": "0x9f3B8679c73C2Fef8b59B4f3444d4e156fb70AA5",
"tokenAddress": "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238"
}
15 changes: 10 additions & 5 deletions packages/fast-usdc/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"src"
],
"bin": {
"fast-usdc": "./src/cli.js"
"fast-usdc": "./src/cli/index.js"
},
"scripts": {
"build": "exit 0",
Expand All @@ -31,6 +31,7 @@
"ts-blank-space": "^0.4.1"
},
"dependencies": {
"@agoric/client-utils": "^0.1.0",
"@agoric/ertp": "^0.16.2",
"@agoric/internal": "^0.3.2",
"@agoric/notifier": "^0.6.2",
Expand All @@ -47,7 +48,14 @@
"@endo/pass-style": "^1.4.6",
"@endo/patterns": "^1.4.6",
"@endo/promise-kit": "^1.1.7",
"commander": "^12.1.0"
"@cosmjs/proto-signing": "^0.32.4",
"@cosmjs/stargate": "^0.32.4",
"@endo/init": "^1.1.6",
"@nick134-bit/noblejs": "0.0.2",
"agoric": "^0.21.1",
"bech32": "^2.0.0",
"commander": "^12.1.0",
"ethers": "^6.13.4"
},
"ava": {
"extensions": {
Expand All @@ -61,9 +69,6 @@
"--import=ts-blank-space/register",
"--no-warnings"
],
"require": [
"@endo/init/debug.js"
],
"timeout": "20m"
}
}
38 changes: 0 additions & 38 deletions packages/fast-usdc/src/cli.js

This file was deleted.

163 changes: 163 additions & 0 deletions packages/fast-usdc/src/cli/cli.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import { Command } from 'commander';
import { existsSync, mkdirSync, readFileSync } from 'fs';
import { fileURLToPath } from 'url';
import { dirname, resolve } from 'path';
import { homedir } from 'os';
import {
readFile as readAsync,
writeFile as writeAsync,
} from 'node:fs/promises';
import configLib from './config.js';
import transferLib from './transfer.js';
import { makeFile } from '../util/file.js';

const packageJson = JSON.parse(
readFileSync(
resolve(dirname(fileURLToPath(import.meta.url)), '../../package.json'),
'utf8',
),
);

const defaultHome = homedir();

export const initProgram = (
configHelpers = configLib,
transferHelpers = transferLib,
readFile = readAsync,
writeFile = writeAsync,
mkdir = mkdirSync,
exists = existsSync,
) => {
const program = new Command();

program
.name('fast-usdc')
.description('CLI to interact with Fast USDC liquidity pool')
.version(packageJson.version)
.option(
'--home <path>',
`Home directory to use for config`,
`${defaultHome}/.fast-usdc/`,
);

const config = program.command('config').description('Manage config');

const configFilename = 'config.json';
const getConfigPath = () => {
const { home: configDir } = program.opts();
return configDir + configFilename;
};

const makeConfigFile = () =>
makeFile(getConfigPath(), readFile, writeFile, mkdir, exists);

config
.command('show')
.description('Show current config')
.action(async () => {
await configHelpers.show(makeConfigFile());
});

config
.command('init')
.description('Set initial config values')
.requiredOption(
'--noble-seed <seed>',
'Seed phrase for Noble account. CAUTION: Stored unencrypted in file system',
)
.requiredOption(
'--eth-seed <seed>',
'Seed phrase for Ethereum account. CAUTION: Stored unencrypted in file system',
)
.option(
'--agoric-rpc [url]',
'Agoric RPC endpoint',
'http://127.0.0.1:1317',
)
.option('--noble-api [url]', 'Noble API endpoint', 'http://127.0.0.1:1318')
.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',
// Default to ETH mainnet contract address. For ETH sepolia, use 0x9f3B8679c73C2Fef8b59B4f3444d4e156fb70AA5
'0xbd3fa81b58ba92a82136038b25adec7066af3155',
)
.option(
'--token-contract-address [address]',
'Address of USDC token contract',
// Detault to ETH mainnet token address. For ETH sepolia, use 0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238
'0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
)
.action(async options => {
await configHelpers.init(makeConfigFile(), options);
});

config
.command('update')
.description('Update config values')
.option(
'--noble-seed [string]',
'Seed phrase for Noble account. CAUTION: Stored unencrypted in file system',
)
.option(
'--eth-seed [string]',
'Seed phrase for Ethereum account. CAUTION: Stored unencrypted in file system',
)
.option('--agoric-rpc [url]', 'Agoric RPC endpoint')
.option('--noble-rpc [url]', 'Noble RPC endpoint')
.option('--eth-rpc [url]', 'Ethereum RPC Endpoint')
.option('--noble-api [url]', 'Noble API endpoint')
.option(
'--noble-to-agoric-channel [channel]',
'Channel ID on Noble for Agoric',
)
.option(
'--token-messenger-address [address]',
'Address of TokenMessenger contract',
)
.option(
'--token-contract-address [address]',
'Address of USDC token contract',
)
.action(async options => {
await configHelpers.update(makeConfigFile(), options);
});

program
.command('deposit')
.description('Offer assets to the liquidity pool')
.action(() => {
console.error('TODO actually send deposit');
// TODO: Implement deposit logic
});

program
.command('withdraw')
.description('Withdraw assets from the liquidity pool')
.action(() => {
console.error('TODO actually send withdrawal');
// TODO: Implement withdraw logic
});

program
.command('transfer')
.description('Transfer USDC from Ethereum/L2 to Cosmos via Fast USDC')
.argument('amount', 'Amount to transfer denominated in uusdc')
.argument('dest', 'Destination address in Cosmos')
.action(
async (
/** @type {string} */ amount,
/** @type {string} */ destination,
) => {
await transferHelpers.transfer(makeConfigFile(), amount, destination);
},
);

return program;
};
101 changes: 101 additions & 0 deletions packages/fast-usdc/src/cli/config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import * as readline from 'node:readline/promises';
import { stdin as input, stdout as output } from 'node:process';

/**
@typedef {{
nobleSeed: string,
ethSeed: string,
nobleToAgoricChannel: string,
agoricRpc: string,
nobleApi: string,
nobleRpc: string,
ethRpc: string,
tokenMessengerAddress: string,
tokenAddress: string
}} ConfigOpts
*/

/** @import { file } from '../util/file' */

const init = async (
/** @type {file} */ configFile,
/** @type {ConfigOpts} */ options,
out = console,
rl = readline.createInterface({ input, output }),
) => {
const showOverrideWarning = async () => {
const answer = await rl.question(
`Config at ${configFile.path} already exists. Override it? (To partially update, use "update", or set "--home" to use a different config path.) y/N: `,
);
rl.close();
const confirmed = ['y', 'yes'].includes(answer.toLowerCase());
if (!confirmed) {
throw new Error('User cancelled');
}
};

const writeConfig = async () => {
await null;
try {
await configFile.write(JSON.stringify(options, null, 2));
out.log(`Config initialized at ${configFile.path}`);
} catch (error) {
out.error(`An unexpected error has occurred: ${error}`);
throw error;
}
};

await null;
if (configFile.exists()) {
await showOverrideWarning();
}
await writeConfig();
};

const update = async (
/** @type {file} */ configFile,
/** @type {Partial<ConfigOpts>} */ options,
out = console,
) => {
const updateConfig = async (/** @type {ConfigOpts} */ data) => {
await null;
const stringified = JSON.stringify(data, null, 2);
try {
await configFile.write(stringified);
out.log(`Config updated at ${configFile.path}`);
out.log(stringified);
} catch (error) {
out.error(`An unexpected error has occurred: ${error}`);
throw error;
}
};

let file;
await null;
try {
file = await configFile.read();
} catch {
out.error(
`No config found at ${configFile.path}. Use "init" to create one, or "--home" to specify config location.`,
);
throw new Error();
}
await updateConfig({ ...JSON.parse(file), ...options });
};

const show = async (/** @type {file} */ configFile, out = console) => {
let contents;
await null;
try {
contents = await configFile.read();
} catch {
out.error(
`No config found at ${configFile.path}. Use "init" to create one, or "--home" to specify config location.`,
);
throw new Error();
}
out.log(`Config found at ${configFile.path}:`);
out.log(contents);
};

export default { init, update, show };
6 changes: 6 additions & 0 deletions packages/fast-usdc/src/cli/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/usr/bin/env node
import '@endo/init/legacy.js';
import { initProgram } from './cli.js';

const program = initProgram();
program.parse();
Loading

0 comments on commit 511b789

Please sign in to comment.