-
Notifications
You must be signed in to change notification settings - Fork 207
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
Showing
25 changed files
with
1,689 additions
and
94 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); |
Oops, something went wrong.