From fb4691b6582d130b894d21a40b3c9aecff2e27ff Mon Sep 17 00:00:00 2001 From: mixmix Date: Thu, 3 Oct 2024 13:33:08 +1300 Subject: [PATCH 1/6] fix cli opts bug with multiple accountOptions --- src/account/command.ts | 6 +++--- src/cli.ts | 4 +--- src/common/utils-cli.ts | 7 ++++--- src/sign/command.ts | 6 +++--- src/transfer/command.ts | 6 +++--- 5 files changed, 14 insertions(+), 15 deletions(-) diff --git a/src/account/command.ts b/src/account/command.ts index a502a850..8447af16 100644 --- a/src/account/command.ts +++ b/src/account/command.ts @@ -4,7 +4,7 @@ import { EntropyAccount } from "./main"; import { selectAndPersistNewAccount, addVerifyingKeyToAccountAndSelect } from "./utils"; import { ACCOUNTS_CONTENT } from './constants' import * as config from '../config' -import { cliWrite, accountOption, endpointOption, loadEntropy, passwordOption } from "../common/utils-cli"; +import { accountOption, endpointOption, passwordOption, cliWrite, loadEntropy } from "../common/utils-cli"; export function entropyAccountCommand () { return new Command('account') @@ -84,9 +84,9 @@ function entropyAccountList () { function entropyAccountRegister () { return new Command('register') .description('Register an entropy account with a program') - .addOption(passwordOption()) - .addOption(endpointOption()) .addOption(accountOption()) + .addOption(endpointOption()) + .addOption(passwordOption()) // Removing these options for now until we update the design to accept program configs // .addOption( // new Option( diff --git a/src/cli.ts b/src/cli.ts index b531efa8..cc004d79 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -4,7 +4,7 @@ import { Command, Option } from 'commander' import { EntropyTuiOptions } from './types' -import { accountOption, endpointOption, loadEntropy } from './common/utils-cli' +import { loadEntropy } from './common/utils-cli' import * as config from './config' import launchTui from './tui' @@ -20,8 +20,6 @@ const program = new Command() program .name('entropy') .description('CLI interface for interacting with entropy.xyz. Running this binary without any commands or arguments starts a text-based interface.') - .addOption(accountOption()) - .addOption(endpointOption()) .addOption( new Option( '-d, --dev', diff --git a/src/common/utils-cli.ts b/src/common/utils-cli.ts index 3aec350b..00fc8c34 100644 --- a/src/common/utils-cli.ts +++ b/src/common/utils-cli.ts @@ -20,7 +20,7 @@ function getConfigOrNull () { export function endpointOption () { return new Option( - '-e, --endpoint ', + '-e, --endpoint ', [ 'Runs entropy with the given endpoint and ignores network endpoints in config.', 'Can also be given a stored endpoint name from config eg: `entropy --endpoint test-net`.' @@ -44,16 +44,17 @@ export function endpointOption () { export function passwordOption (description?: string) { return new Option( - '-p, --password ', + '-p, --password ', description || 'Password for the account' ) + .hideHelp(true) } export function accountOption () { const storedConfig = getConfigOrNull() return new Option( - '-a, --account ', + '-a, --account ', [ 'Sets the account for the session.', 'Defaults to the last set account (or the first account if one has not been set before).' diff --git a/src/sign/command.ts b/src/sign/command.ts index fc574228..1c67f4d6 100644 --- a/src/sign/command.ts +++ b/src/sign/command.ts @@ -1,14 +1,14 @@ import { Command, /* Option */ } from 'commander' -import { cliWrite, accountOption, endpointOption, loadEntropy, passwordOption } from '../common/utils-cli' +import { accountOption, endpointOption, passwordOption, cliWrite, loadEntropy } from '../common/utils-cli' import { EntropySign } from './main' export function entropySignCommand () { const signCommand = new Command('sign') .description('Sign a message using the Entropy network. Output is a JSON { verifyingKey, signature }') .argument('msg', 'Message you would like to sign (string)') - .addOption(passwordOption('Password for the source account (if required)')) - .addOption(endpointOption()) .addOption(accountOption()) + .addOption(endpointOption()) + .addOption(passwordOption('Password for the source account (if required)')) // .addOption( // new Option( // '-r, --raw', diff --git a/src/transfer/command.ts b/src/transfer/command.ts index b814e2f9..e8ef3ceb 100644 --- a/src/transfer/command.ts +++ b/src/transfer/command.ts @@ -1,5 +1,5 @@ import { Command } from "commander" -import { accountOption, endpointOption, loadEntropy, passwordOption } from "src/common/utils-cli" +import { accountOption, endpointOption, passwordOption, loadEntropy } from "../common/utils-cli" import { EntropyTransfer } from "./main" export function entropyTransferCommand () { @@ -8,9 +8,9 @@ export function entropyTransferCommand () { .description('Transfer funds between two Entropy accounts.') // TODO: name the output .argument('destination', 'Account address funds will be sent to') .argument('amount', 'Amount of funds to be moved') - .addOption(passwordOption('Password for the source account (if required)')) - .addOption(endpointOption()) .addOption(accountOption()) + .addOption(endpointOption()) + .addOption(passwordOption('Password for the source account (if required)')) .action(async (destination, amount, opts) => { const entropy = await loadEntropy(opts.account, opts.endpoint) const transferService = new EntropyTransfer(entropy, opts.endpoint) From 6eccbba805096e430b0c75645b8b692376ad7b4c Mon Sep 17 00:00:00 2001 From: mixmix Date: Thu, 3 Oct 2024 13:51:23 +1300 Subject: [PATCH 2/6] drop async argParser function! (unsupported) --- src/common/utils-cli.ts | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/src/common/utils-cli.ts b/src/common/utils-cli.ts index 00fc8c34..f8e7be9b 100644 --- a/src/common/utils-cli.ts +++ b/src/common/utils-cli.ts @@ -61,20 +61,29 @@ export function accountOption () { ].join(' ') ) .env('ENTROPY_ACCOUNT') - .argParser(async (account) => { - if (storedConfig && storedConfig.selectedAccount !== account) { - // Updated selected account in config with new address from this option - await config.set({ - ...storedConfig, - selectedAccount: account - }) - } + .argParser(addressOrName => { + // We try to map addressOrName to an account we have stored + if (!storedConfig) return addressOrName - return account + const account = findAccountByAddressOrName(storedConfig.accounts, addressOrName) + if (!account) return addressOrName + + // If we find one, we set this account as the future default + config.set({ + ...storedConfig, + selectedAccount: account.name + }) + // NOTE: argParser cannot be an async function, so we cannot await this call + // WARNING: this will lead to a race-condition if functions are called in quick succession + // and assume the selectedAccount has been persisted + // + // RISK: doesn't seem likely as most of our functions will await at slow other steps.... + // SOLUTION: write a scynchronous version? + + // We finally return the account name to be as consistent as possible (using name, not address) + return account.name }) .default(storedConfig?.selectedAccount) - // TODO: display the *name* not address - // TODO: standardise whether selectedAccount is name or address. } export async function loadEntropy (addressOrName: string, endpoint: string, password?: string): Promise { From ea2388148d7658dcb01ed455c754c8b99036c391 Mon Sep 17 00:00:00 2001 From: mixmix Date: Thu, 3 Oct 2024 14:01:13 +1300 Subject: [PATCH 3/6] add setSelectedAccount --- src/account/interaction.ts | 5 +---- src/account/utils.ts | 6 +++--- src/common/utils-cli.ts | 5 +---- src/config/index.ts | 20 +++++++++++++++++--- src/config/types.ts | 9 +++++++++ src/tui.ts | 6 +----- tests/account.test.ts | 2 +- 7 files changed, 33 insertions(+), 20 deletions(-) diff --git a/src/account/interaction.ts b/src/account/interaction.ts index bf1c599f..b26103dd 100644 --- a/src/account/interaction.ts +++ b/src/account/interaction.ts @@ -45,10 +45,7 @@ export async function entropyAccount (endpoint: string, storedConfig: EntropyCon return } const { selectedAccount } = await inquirer.prompt(accountSelectQuestions(accounts)) - await config.set({ - ...storedConfig, - selectedAccount: selectedAccount.address - }) + await config.setSelectedAccount(selectedAccount) print('Current selected account is ' + selectedAccount) return diff --git a/src/account/utils.ts b/src/account/utils.ts index cf51603a..c8efef2e 100644 --- a/src/account/utils.ts +++ b/src/account/utils.ts @@ -16,7 +16,8 @@ export async function selectAndPersistNewAccount (newAccount: EntropyAccountConf throw Error(`An account with address "${newAccount.address}" already exists.`) } - accounts.push(newAccount) + // persist to config, set selectedAccount + accounts.push(newAccount) await config.set({ ...storedConfig, selectedAccount: newAccount.address @@ -28,9 +29,8 @@ export async function addVerifyingKeyToAccountAndSelect (verifyingKey: string, a const account = findAccountByAddressOrName(storedConfig.accounts, accountNameOrAddress) if (!account) throw Error(`Unable to persist verifyingKey "${verifyingKey}" to unknown account "${accountNameOrAddress}"`) - account.data.registration.verifyingKeys.push(verifyingKey) - // persist to config, set selectedAccount + account.data.registration.verifyingKeys.push(verifyingKey) await config.set({ ...storedConfig, selectedAccount: account.address diff --git a/src/common/utils-cli.ts b/src/common/utils-cli.ts index f8e7be9b..96f8bed7 100644 --- a/src/common/utils-cli.ts +++ b/src/common/utils-cli.ts @@ -69,10 +69,7 @@ export function accountOption () { if (!account) return addressOrName // If we find one, we set this account as the future default - config.set({ - ...storedConfig, - selectedAccount: account.name - }) + config.setSelectedAccount(account) // NOTE: argParser cannot be an async function, so we cannot await this call // WARNING: this will lead to a race-condition if functions are called in quick succession // and assume the selectedAccount has been persisted diff --git a/src/config/index.ts b/src/config/index.ts index 049b6775..a38859a4 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -6,7 +6,7 @@ import envPaths from 'env-paths' import allMigrations from './migrations' import { serialize, deserialize } from './encoding' -import { EntropyConfig } from './types' +import { EntropyConfig, EntropyAccountConfig } from './types' const paths = envPaths('entropy-cryptography', { suffix: '' }) const CONFIG_PATH = join(paths.config, 'entropy-cli.json') @@ -73,14 +73,28 @@ export async function set (config: EntropyConfig, configPath = CONFIG_PATH) { await writeFile(configPath, serialize(config)) } +export async function setSelectedAccount (account: EntropyAccountConfig, configPath = CONFIG_PATH) { + const storedConfig = await get(configPath) + + if (storedConfig.selectedAccount === account.name) return storedConfig + // no need for update + + const newConfig = { + ...storedConfig, + selectedAccount: account.name + } + await set(newConfig, configPath) + return newConfig +} + /* util */ function noop () {} -function assertConfigPath (configPath) { +function assertConfigPath (configPath: string) { if (!configPath.endsWith('.json')) { throw Error(`configPath must be of form *.json, got ${configPath}`) } } -export function isDangerousReadError (err) { +export function isDangerousReadError (err: any) { // file not found: if (err.code === 'ENOENT') return false diff --git a/src/config/types.ts b/src/config/types.ts index 7d4deb78..26aa6678 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -4,6 +4,7 @@ export interface EntropyConfig { dev: string; 'test-net': string } + // selectedAccount is account.name (alias) for the account selectedAccount: string 'migration-version': string } @@ -14,6 +15,14 @@ export interface EntropyAccountConfig { data: EntropyAccountData } +// Safe output format +export interface EntropyAccountConfigFormatted { + name: string + address: string + verifyingKeys: string[] +} + +// TODO: document this whole thing export interface EntropyAccountData { debug?: boolean seed: string diff --git a/src/tui.ts b/src/tui.ts index bd7780cf..f02d82da 100644 --- a/src/tui.ts +++ b/src/tui.ts @@ -19,11 +19,7 @@ async function setupConfig () { // set selectedAccount if we can if (!storedConfig.selectedAccount && storedConfig.accounts.length) { - await config.set({ - ...storedConfig, - selectedAccount: storedConfig.accounts[0].address - }) - storedConfig = await config.get() + storedConfig = await config.setSelectedAccount(storedConfig.accounts[0]) } return storedConfig diff --git a/tests/account.test.ts b/tests/account.test.ts index 9f0288f6..220f101a 100644 --- a/tests/account.test.ts +++ b/tests/account.test.ts @@ -32,7 +32,7 @@ test('Account - list', async t => { dev: 'ws://127.0.0.1:9944', 'test-net': 'wss://testnet.entropy.xyz', }, - selectedAccount: account.address, + selectedAccount: account.name, 'migration-version': '0' } From ef005b3c1fcd54d2b0d91b897b5fb469d62c827e Mon Sep 17 00:00:00 2001 From: mixmix Date: Tue, 15 Oct 2024 09:56:39 +1300 Subject: [PATCH 4/6] make custom config possible for CLI usage --- src/account/command.ts | 25 ++++++++++++++---------- src/account/interaction.ts | 2 +- src/account/utils.ts | 30 +++++++++++++++++------------ src/balance/command.ts | 8 +++++--- src/cli.ts | 12 ++++++++---- src/common/utils-cli.ts | 39 +++++++++++++++++++++++++++++++++----- src/common/utils.ts | 13 +++++++++++++ src/config/index.ts | 2 +- src/program/command.ts | 6 +++--- src/sign/command.ts | 5 +++-- src/transfer/command.ts | 5 +++-- tests/common.test.ts | 31 ++++++++++++++++++++++++++++++ 12 files changed, 135 insertions(+), 43 deletions(-) diff --git a/src/account/command.ts b/src/account/command.ts index 8447af16..ed2de74f 100644 --- a/src/account/command.ts +++ b/src/account/command.ts @@ -4,7 +4,7 @@ import { EntropyAccount } from "./main"; import { selectAndPersistNewAccount, addVerifyingKeyToAccountAndSelect } from "./utils"; import { ACCOUNTS_CONTENT } from './constants' import * as config from '../config' -import { accountOption, endpointOption, passwordOption, cliWrite, loadEntropy } from "../common/utils-cli"; +import { accountOption, configOption, endpointOption, passwordOption, cliWrite, loadEntropy } from "../common/utils-cli"; export function entropyAccountCommand () { return new Command('account') @@ -19,19 +19,20 @@ function entropyAccountCreate () { return new Command('create') .alias('new') .description('Create a new entropy account from scratch. Output is JSON of form {name, address}') - .addOption(passwordOption()) .argument('', 'A user friendly name for your new account.') + .addOption(configOption()) .addOption( new Option( '--path', 'Derivation path' ).default(ACCOUNTS_CONTENT.path.default) ) + .addOption(passwordOption()) .action(async (name, opts) => { - const { path } = opts + const { config: configPath, path } = opts const newAccount = await EntropyAccount.create({ name, path }) - await selectAndPersistNewAccount(newAccount) + await selectAndPersistNewAccount(configPath, newAccount) cliWrite({ name: newAccount.name, @@ -47,6 +48,7 @@ function entropyAccountImport () { .addOption(passwordOption()) .argument('', 'A user friendly name for your new account.') .argument('', 'The seed for the account you are importing') + .addOption(configOption()) .addOption( new Option( '--path', @@ -54,10 +56,10 @@ function entropyAccountImport () { ).default(ACCOUNTS_CONTENT.path.default) ) .action(async (name, seed, opts) => { - const { path } = opts + const { config: configPath, path } = opts const newAccount = await EntropyAccount.import({ name, seed, path }) - await selectAndPersistNewAccount(newAccount) + await selectAndPersistNewAccount(configPath, newAccount) cliWrite({ name: newAccount.name, @@ -71,9 +73,11 @@ function entropyAccountList () { return new Command('list') .alias('ls') .description('List all accounts. Output is JSON of form [{ name, address, verifyingKeys }]') - .action(async () => { + .addOption(configOption()) + .action(async (opts) => { + const { config: configPath } = opts // TODO: test if it's an encrypted account, if no password provided, throw because later on there's no protection from a prompt coming up - const storedConfig = await config.get() + const storedConfig = await config.get(configPath) const accounts = EntropyAccount.list(storedConfig) cliWrite(accounts) process.exit(0) @@ -85,6 +89,7 @@ function entropyAccountRegister () { return new Command('register') .description('Register an entropy account with a program') .addOption(accountOption()) + .addOption(configOption()) .addOption(endpointOption()) .addOption(passwordOption()) // Removing these options for now until we update the design to accept program configs @@ -102,11 +107,11 @@ function entropyAccountRegister () { // ) .action(async (opts) => { // NOTE: loadEntropy throws if it can't find opts.account - const entropy: Entropy = await loadEntropy(opts.account, opts.endpoint) + const entropy: Entropy = await loadEntropy(opts) const accountService = new EntropyAccount(entropy, opts.endpoint) const verifyingKey = await accountService.register() - await addVerifyingKeyToAccountAndSelect(verifyingKey, opts.account) + await addVerifyingKeyToAccountAndSelect(opts.config, verifyingKey, opts.account) cliWrite(verifyingKey) process.exit(0) diff --git a/src/account/interaction.ts b/src/account/interaction.ts index b26103dd..952da74f 100644 --- a/src/account/interaction.ts +++ b/src/account/interaction.ts @@ -35,7 +35,7 @@ export async function entropyAccount (endpoint: string, storedConfig: EntropyCon ? await EntropyAccount.import({ seed, name, path }) : await EntropyAccount.create({ name, path }) - await selectAndPersistNewAccount(newAccount) + await selectAndPersistNewAccount(config.CONFIG_PATH, newAccount) return } diff --git a/src/account/utils.ts b/src/account/utils.ts index c8efef2e..89b741a4 100644 --- a/src/account/utils.ts +++ b/src/account/utils.ts @@ -3,8 +3,8 @@ import { EntropyAccountConfig } from "../config/types"; import * as config from "../config"; import { generateAccountChoices, findAccountByAddressOrName } from '../common/utils'; -export async function selectAndPersistNewAccount (newAccount: EntropyAccountConfig) { - const storedConfig = await config.get() +export async function selectAndPersistNewAccount (configPath: string, newAccount: EntropyAccountConfig) { + const storedConfig = await config.get(configPath) const { accounts } = storedConfig const isExistingName = accounts.find(account => account.name === newAccount.name) @@ -18,23 +18,29 @@ export async function selectAndPersistNewAccount (newAccount: EntropyAccountConf // persist to config, set selectedAccount accounts.push(newAccount) - await config.set({ - ...storedConfig, - selectedAccount: newAccount.address - }) + await config.set( + { + ...storedConfig, + selectedAccount: newAccount.address + }, + configPath + ) } -export async function addVerifyingKeyToAccountAndSelect (verifyingKey: string, accountNameOrAddress: string) { - const storedConfig = await config.get() +export async function addVerifyingKeyToAccountAndSelect (configPath, verifyingKey: string, accountNameOrAddress: string) { + const storedConfig = await config.get(configPath) const account = findAccountByAddressOrName(storedConfig.accounts, accountNameOrAddress) if (!account) throw Error(`Unable to persist verifyingKey "${verifyingKey}" to unknown account "${accountNameOrAddress}"`) // persist to config, set selectedAccount account.data.registration.verifyingKeys.push(verifyingKey) - await config.set({ - ...storedConfig, - selectedAccount: account.address - }) + await config.set( + { + ...storedConfig, + selectedAccount: account.address + }, + configPath + ) } function validateSeedInput (seed) { diff --git a/src/balance/command.ts b/src/balance/command.ts index 4d0b023a..79094010 100644 --- a/src/balance/command.ts +++ b/src/balance/command.ts @@ -1,17 +1,19 @@ import { Command } from "commander"; import Entropy from "@entropyxyz/sdk"; -import { cliWrite, endpointOption, loadEntropy, passwordOption } from "src/common/utils-cli"; + import { EntropyBalance } from "./main"; +import { configOption, endpointOption, passwordOption, loadEntropy, cliWrite } from "../common/utils-cli"; export function entropyBalanceCommand () { const balanceCommand = new Command('balance') balanceCommand .description('Command to retrieive the balance of an account on the Entropy Network') .argument('address', 'Account address whose balance you want to query') - .addOption(passwordOption()) + .addOption(configOption()) .addOption(endpointOption()) + .addOption(passwordOption()) .action(async (address, opts) => { - const entropy: Entropy = await loadEntropy(address, opts.endpoint) + const entropy: Entropy = await loadEntropy({ account: address, ...opts }) const BalanceService = new EntropyBalance(entropy, opts.endpoint) const balance = await BalanceService.getBalance(address) cliWrite(`${balance.toLocaleString('en-US')} BITS`) diff --git a/src/cli.ts b/src/cli.ts index cc004d79..2a88c106 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -36,14 +36,18 @@ program .action(async (opts: EntropyTuiOptions) => { const { account, endpoint } = opts const entropy = account - ? await loadEntropy(account, endpoint) + ? await loadEntropy(account, config.CONFIG_PATH, endpoint) : undefined - // NOTE: on initial startup you have no account + // NOTE: + // - on initial startup you have no account + // - no custom config for the TUI at moment (opt.name collisions) launchTui(entropy, opts) }) - .hook('preAction', async () => { + .hook('preAction', async (thisCommand, actionCommand) => { + const { config: configPath } = actionCommand.opts() + + if (configPath) await config.init(configPath) // set up config file, run migrations - return config.init() }) program.parseAsync() diff --git a/src/common/utils-cli.ts b/src/common/utils-cli.ts index 96f8bed7..2d04af0c 100644 --- a/src/common/utils-cli.ts +++ b/src/common/utils-cli.ts @@ -1,6 +1,6 @@ import Entropy from '@entropyxyz/sdk' import { Option } from 'commander' -import { findAccountByAddressOrName, stringify } from './utils' +import { absolutePath, findAccountByAddressOrName, stringify } from './utils' import * as config from '../config' import { initializeEntropy } from './initializeEntropy' @@ -9,9 +9,9 @@ export function cliWrite (result) { process.stdout.write(prettyResult) } -function getConfigOrNull () { +function getConfigOrNull (configPath) { try { - return config.getSync() + return config.getSync(configPath) } catch (err) { if (config.isDangerousReadError(err)) throw err return null @@ -33,6 +33,12 @@ export function endpointOption () { /* look up endpoint-alias */ const storedConfig = getConfigOrNull() + // WIP: ... ahhh, we need --config + // + // ideas: + // - do arg-parsing actions in e.g. loadEntropy + // - try to mutate opts state in a gook :skull: + // const endpoint = storedConfig?.endpoints?.[aliasOrEndpoint] if (!endpoint) throw Error('unknown endpoint alias: ' + aliasOrEndpoint) @@ -51,6 +57,7 @@ export function passwordOption (description?: string) { } export function accountOption () { + // WIP: ... ahhh, we need --config const storedConfig = getConfigOrNull() return new Option( @@ -83,8 +90,30 @@ export function accountOption () { .default(storedConfig?.selectedAccount) } -export async function loadEntropy (addressOrName: string, endpoint: string, password?: string): Promise { - const accounts = getConfigOrNull()?.accounts || [] +export function configOption () { + return new Option( + '-c, --config ', + 'Set the path to your Entropy config file (JSON).', + ) + .env('ENTROPY_CONFIG') + .argParser(configPath => absolutePath(configPath)) + .default(config.CONFIG_PATH) +} + +export async function loadEntropy ( + { + account: addressOrName, + config: configPath, + endpoint, + password + }: { + account: string, + config: string, + endpoint: string, + password?: string + } +): Promise { + const accounts = getConfigOrNull(configPath)?.accounts || [] const selectedAccount = findAccountByAddressOrName(accounts, addressOrName) if (!selectedAccount) throw new Error(`No account with name or address: "${addressOrName}"`) diff --git a/src/common/utils.ts b/src/common/utils.ts index b4dbc7ea..e7960ff5 100644 --- a/src/common/utils.ts +++ b/src/common/utils.ts @@ -1,4 +1,6 @@ import { Buffer } from 'buffer' +import { join } from 'path' +import { homedir } from 'os' import { EntropyAccountConfig } from "../config/types" export function stripHexPrefix (str: string): string { @@ -73,3 +75,14 @@ export function findAccountByAddressOrName (accounts: EntropyAccountConfig[], al accounts.find(account => account.name === aliasOrAddress) ) } + +export function absolutePath (somePath: string) { + switch (somePath.charAt(0)) { + case '.': + return join(process.cwd(), somePath) + case '~': + return join(homedir(), somePath.slice(1)) + default: + return somePath + } +} diff --git a/src/config/index.ts b/src/config/index.ts index a38859a4..3c5a8210 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -9,9 +9,9 @@ import { serialize, deserialize } from './encoding' import { EntropyConfig, EntropyAccountConfig } from './types' const paths = envPaths('entropy-cryptography', { suffix: '' }) -const CONFIG_PATH = join(paths.config, 'entropy-cli.json') const OLD_CONFIG_PATH = join(process.env.HOME, '.entropy-cli.config') +export const CONFIG_PATH = join(paths.config, 'entropy-cli.json') export const VERSION = 'migration-version' export function migrateData (migrations, currentConfig = {}) { diff --git a/src/program/command.ts b/src/program/command.ts index 0f02ffb5..042b7757 100644 --- a/src/program/command.ts +++ b/src/program/command.ts @@ -1,7 +1,7 @@ import { Command } from 'commander' import { EntropyProgram } from './main' -import { accountOption, endpointOption, cliWrite, loadEntropy } from '../common/utils-cli' +import { accountOption, configOption, endpointOption, cliWrite, loadEntropy } from '../common/utils-cli' export function entropyProgramCommand () { return new Command('program') @@ -43,11 +43,11 @@ function entropyProgramDeploy () { ].join(' ') ) .addOption(accountOption()) + .addOption(configOption()) .addOption(endpointOption()) .action(async (bytecodePath, configurationSchemaPath, auxillaryDataSchemaPath, opts) => { // eslint-disable-line - const entropy = await loadEntropy(opts.account, opts.endpoint) - + const entropy = await loadEntropy(opts) const program = new EntropyProgram(entropy, opts.endpoint) const pointer = await program.deploy({ diff --git a/src/sign/command.ts b/src/sign/command.ts index 1c67f4d6..0a245995 100644 --- a/src/sign/command.ts +++ b/src/sign/command.ts @@ -1,5 +1,5 @@ import { Command, /* Option */ } from 'commander' -import { accountOption, endpointOption, passwordOption, cliWrite, loadEntropy } from '../common/utils-cli' +import { accountOption, configOption, endpointOption, passwordOption, cliWrite, loadEntropy } from '../common/utils-cli' import { EntropySign } from './main' export function entropySignCommand () { @@ -7,6 +7,7 @@ export function entropySignCommand () { .description('Sign a message using the Entropy network. Output is a JSON { verifyingKey, signature }') .argument('msg', 'Message you would like to sign (string)') .addOption(accountOption()) + .addOption(configOption()) .addOption(endpointOption()) .addOption(passwordOption('Password for the source account (if required)')) // .addOption( @@ -16,7 +17,7 @@ export function entropySignCommand () { // ) // ) .action(async (msg, opts) => { - const entropy = await loadEntropy(opts.account, opts.endpoint) + const entropy = await loadEntropy(opts) const SigningService = new EntropySign(entropy, opts.endpoint) // TO-DO: Add ability for raw signing here, maybe? new raw option can be used for the conditional /** diff --git a/src/transfer/command.ts b/src/transfer/command.ts index e8ef3ceb..edadd3c1 100644 --- a/src/transfer/command.ts +++ b/src/transfer/command.ts @@ -1,5 +1,5 @@ import { Command } from "commander" -import { accountOption, endpointOption, passwordOption, loadEntropy } from "../common/utils-cli" +import { accountOption, configOption, endpointOption, passwordOption, loadEntropy } from "../common/utils-cli" import { EntropyTransfer } from "./main" export function entropyTransferCommand () { @@ -9,10 +9,11 @@ export function entropyTransferCommand () { .argument('destination', 'Account address funds will be sent to') .argument('amount', 'Amount of funds to be moved') .addOption(accountOption()) + .addOption(configOption()) .addOption(endpointOption()) .addOption(passwordOption('Password for the source account (if required)')) .action(async (destination, amount, opts) => { - const entropy = await loadEntropy(opts.account, opts.endpoint) + const entropy = await loadEntropy(opts) const transferService = new EntropyTransfer(entropy, opts.endpoint) await transferService.transfer(destination, amount) // cliWrite(??) // TODO: write the output diff --git a/tests/common.test.ts b/tests/common.test.ts index 8ec05314..6734f53a 100644 --- a/tests/common.test.ts +++ b/tests/common.test.ts @@ -1,6 +1,9 @@ import test from 'tape' +import { join } from 'path' +import { homedir } from 'os' import { maskPayload } from '../src/common/masking' +import { absolutePath } from '../src/common/utils' test('common/masking', async (t) => { @@ -51,3 +54,31 @@ test('common/masking', async (t) => { t.end() }) + +test.only('common/utils', (t) => { + t.equal( + absolutePath('/tmp/things.json'), + '/tmp/things.json', + 'absolute path (unix)' + ) + + t.equal( + absolutePath('C:\folder\things.json'), + 'C:\folder\things.json', + 'absolute path (win)' + ) + + t.equal( + absolutePath('../things.json'), + join(process.cwd(), '../things.json'), + 'relative path (to cwd)' + ) + + t.equal( + absolutePath('~/things.json'), + join(homedir(), './things.json'), + 'relative path (home)' + ) + + t.end() +}) From 8236b29c37437a7b3f552b78e8584744c0dc9874 Mon Sep 17 00:00:00 2001 From: mixmix Date: Wed, 23 Oct 2024 15:18:17 +1300 Subject: [PATCH 5/6] cli descriptions refactor --- src/cli.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index 3511232f..10d9f09b 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -19,11 +19,17 @@ const program = new Command() /* no command */ program .name('entropy') - .description('CLI interface for interacting with entropy.xyz. Running this binary without any commands or arguments starts a text-based interface.') + .description([ + 'CLI interface for interacting with entropy.xyz.', + 'Running this binary without any commands or arguments starts a text-based interface.' + ].join(' ')) .addOption( new Option( '-d, --dev', - 'Runs entropy in a developer mode uses the dev endpoint as the main endpoint and allows for faucet option to be available in the main menu' + [ + 'Runs entropy in a developer mode uses the dev endpoint as the main endpoint and', + 'allows for faucet option to be available in the main menu' + ].join(' ') ) .env('DEV_MODE') .hideHelp() @@ -42,7 +48,8 @@ program : undefined // NOTE: // - on initial startup you have no account - // - no custom config for the TUI at moment (opt.name collisions) + // - no custom config for the TUI at moment + // - option name collisions, see: https://github.com/tj/commander.js/issues/2260 launchTui(entropy, opts) }) .hook('preAction', async (thisCommand, actionCommand) => { From 61df302b97baa1c138e37915cac1e49fcb22b5cf Mon Sep 17 00:00:00 2001 From: mixmix Date: Wed, 23 Oct 2024 16:54:35 +1300 Subject: [PATCH 6/6] wip --- src/account/interaction.ts | 4 +- src/cli.ts | 12 ++--- src/common/constants.ts | 4 ++ src/common/utils-cli.ts | 105 ++++++++++++++++--------------------- src/common/utils.ts | 7 +-- src/config/index.ts | 12 ++--- src/program/utils.ts | 15 +++--- src/tui.ts | 51 +++++++++++------- src/types/index.ts | 7 ++- tests/e2e.cli.sh | 3 +- 10 files changed, 111 insertions(+), 109 deletions(-) create mode 100644 src/common/constants.ts diff --git a/src/account/interaction.ts b/src/account/interaction.ts index a041f384..d16bdf41 100644 --- a/src/account/interaction.ts +++ b/src/account/interaction.ts @@ -35,7 +35,7 @@ export async function entropyAccount (endpoint: string, storedConfig: EntropyCon ? await EntropyAccount.import({ seed, name, path }) : await EntropyAccount.create({ name, path }) - await selectAndPersistNewAccount(config.CONFIG_PATH, newAccount) + await selectAndPersistNewAccount(config.CONFIG_PATH_DEFAULT, newAccount) return } @@ -82,7 +82,7 @@ export async function entropyRegister (entropy: Entropy, endpoint: string, store print("Attempting to register the address:", account.address) const verifyingKey = await accountService.register() - await addVerifyingKeyToAccountAndSelect(config.CONFIG_PATH, verifyingKey, account.address) + await addVerifyingKeyToAccountAndSelect(config.CONFIG_PATH_DEFAULT, verifyingKey, account.address) print("Your address", account.address, "has been successfully registered.") } diff --git a/src/cli.ts b/src/cli.ts index 10d9f09b..ddafe0a0 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -4,7 +4,6 @@ import { Command, Option } from 'commander' import { EntropyTuiOptions } from './types' -import { loadEntropy } from './common/utils-cli' import * as config from './config' import launchTui from './tui' @@ -42,15 +41,10 @@ program .addCommand(entropyProgramCommand()) .action(async (opts: EntropyTuiOptions) => { - const { account, endpoint } = opts - const entropy = account - ? await loadEntropy({ account, config: config.CONFIG_PATH, endpoint }) - : undefined // NOTE: - // - on initial startup you have no account - // - no custom config for the TUI at moment - // - option name collisions, see: https://github.com/tj/commander.js/issues/2260 - launchTui(entropy, opts) + // because of option name collisions (https://github.com/entropyxyz/cli/issues/265) + // we currently do not support options [account, endpoint, config] in Tui + launchTui(opts) }) .hook('preAction', async (thisCommand, actionCommand) => { const { config: configPath } = actionCommand.opts() diff --git a/src/common/constants.ts b/src/common/constants.ts new file mode 100644 index 00000000..1a474f44 --- /dev/null +++ b/src/common/constants.ts @@ -0,0 +1,4 @@ + + +export const ENTROPY_ENDPOINT_DEFAULT = 'ws://testnet.entropy.xyz:9944/' +// TODO: update this to be wss? diff --git a/src/common/utils-cli.ts b/src/common/utils-cli.ts index 4fd80684..d89c445b 100644 --- a/src/common/utils-cli.ts +++ b/src/common/utils-cli.ts @@ -1,8 +1,11 @@ import Entropy from '@entropyxyz/sdk' import { Option } from 'commander' + import { absolutePath, findAccountByAddressOrName, stringify } from './utils' -import * as config from '../config' import { initializeEntropy } from './initializeEntropy' +import * as config from '../config' +import { EntropyConfig } from "../config/types"; +import { ENTROPY_ENDPOINT_DEFAULT } from '../common/constants' export function cliWrite (result) { const prettyResult = stringify(result, 0) @@ -27,32 +30,10 @@ export function endpointOption () { ].join(' ') ) .env('ENTROPY_ENDPOINT') - .argParser(aliasOrEndpoint => { - /* see if it's a raw endpoint */ - if (aliasOrEndpoint.match(/^wss?:\/\//)) return aliasOrEndpoint - - /* look up endpoint-alias */ - const storedConfig = getConfigOrNull() - // WIP: ... ahhh, we need --config - // - // ideas: - // - do arg-parsing actions in e.g. loadEntropy - // - try to mutate opts state in a gook :skull: - // - const endpoint = storedConfig?.endpoints?.[aliasOrEndpoint] - if (!endpoint) throw Error('unknown endpoint alias: ' + aliasOrEndpoint) - - return endpoint - }) - .default('ws://testnet.entropy.xyz:9944/') - // NOTE: default cannot be "test-net" as argParser only runs if the -e/--endpoint flag - // or ENTROPY_ENDPOINT env set + .default(ENTROPY_ENDPOINT_DEFAULT) } export function accountOption () { - // WIP: ... ahhh, we need --config - const storedConfig = getConfigOrNull() - return new Option( '-a, --account ', [ @@ -61,26 +42,6 @@ export function accountOption () { ].join(' ') ) .env('ENTROPY_ACCOUNT') - .argParser(addressOrName => { - // We try to map addressOrName to an account we have stored - if (!storedConfig) return addressOrName - - const account = findAccountByAddressOrName(storedConfig.accounts, addressOrName) - if (!account) return addressOrName - - // If we find one, we set this account as the future default - config.setSelectedAccount(account) - // NOTE: argParser cannot be an async function, so we cannot await this call - // WARNING: this will lead to a race-condition if functions are called in quick succession - // and assume the selectedAccount has been persisted - // - // RISK: doesn't seem likely as most of our functions will await at slow other steps.... - // SOLUTION: write a scynchronous version? - - // We finally return the account name to be as consistent as possible (using name, not address) - return account.name - }) - .default(storedConfig?.selectedAccount) } export function configOption () { @@ -90,28 +51,54 @@ export function configOption () { ) .env('ENTROPY_CONFIG') .argParser(configPath => absolutePath(configPath)) - .default(config.CONFIG_PATH) + .default(config.CONFIG_PATH_DEFAULT) } -export async function loadEntropy ( - { - account: addressOrName, - config: configPath, - endpoint, - }: { - account: string, - config: string, - endpoint: string, +export async function loadEntropy (options: { + account: string, + config: string, + endpoint: string, +}): Promise { + const storedConfig = getConfigOrNull(options.config) + + const account = parseAccountOption(storedConfig, options.account) + // if this account is not the default selectedAccount, make it so + if (storedConfig.selectedAccount !== account.name) { + await config.set({ + ...storedConfig, + selectedAccount: account.name + }) } -): Promise { - const accounts = getConfigOrNull(configPath)?.accounts || [] - const selectedAccount = findAccountByAddressOrName(accounts, addressOrName) - if (!selectedAccount) throw new Error(`No account with name or address: "${addressOrName}"`) - const entropy = await initializeEntropy({ keyMaterial: selectedAccount.data, endpoint }) + const endpoint = parseEndpointOption(storedConfig, options.endpoint) + + const entropy = await initializeEntropy({ keyMaterial: account.data, endpoint }) if (!entropy?.keyring?.accounts?.registration?.pair) { throw new Error("Signer keypair is undefined or not properly initialized.") } return entropy } + +function parseEndpointOption (config: EntropyConfig, aliasOrEndpoint: string) { + // if raw endpoint + if (aliasOrEndpoint.match(/^wss?:\/\//)) { + return aliasOrEndpoint + } + // else an alias + else { + const endpoint = config.endpoints[aliasOrEndpoint] + if (!endpoint) throw Error('unknown endpoint alias: ' + aliasOrEndpoint) + + return endpoint + } +} + +function parseAccountOption (config: EntropyConfig, addressOrName: string) { + const accounts = config?.accounts || [] + const account = findAccountByAddressOrName(accounts, addressOrName) + if (!account) throw new Error(`No account with name or address: "${addressOrName}"`) + + return account +} + diff --git a/src/common/utils.ts b/src/common/utils.ts index e7960ff5..42f2d019 100644 --- a/src/common/utils.ts +++ b/src/common/utils.ts @@ -1,6 +1,7 @@ -import { Buffer } from 'buffer' -import { join } from 'path' -import { homedir } from 'os' +import { Buffer } from 'node:buffer' +import { homedir } from 'node:os' +import { join } from 'node:path' + import { EntropyAccountConfig } from "../config/types" export function stripHexPrefix (str: string): string { diff --git a/src/config/index.ts b/src/config/index.ts index 3c5a8210..5bdc8f39 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -11,7 +11,7 @@ import { EntropyConfig, EntropyAccountConfig } from './types' const paths = envPaths('entropy-cryptography', { suffix: '' }) const OLD_CONFIG_PATH = join(process.env.HOME, '.entropy-cli.config') -export const CONFIG_PATH = join(paths.config, 'entropy-cli.json') +export const CONFIG_PATH_DEFAULT = join(paths.config, 'entropy-cli.json') export const VERSION = 'migration-version' export function migrateData (migrations, currentConfig = {}) { @@ -33,7 +33,7 @@ function hasRunMigration (config: any, version: number) { return Number(currentVersion) >= Number(version) } -export async function init (configPath = CONFIG_PATH, oldConfigPath = OLD_CONFIG_PATH) { +export async function init (configPath = CONFIG_PATH_DEFAULT, oldConfigPath = OLD_CONFIG_PATH) { const currentConfig = await get(configPath) .catch(async (err ) => { if (isDangerousReadError(err)) throw err @@ -56,24 +56,24 @@ export async function init (configPath = CONFIG_PATH, oldConfigPath = OLD_CONFIG } } -export async function get (configPath = CONFIG_PATH) { +export async function get (configPath = CONFIG_PATH_DEFAULT) { return readFile(configPath, 'utf-8') .then(deserialize) } -export function getSync (configPath = CONFIG_PATH) { +export function getSync (configPath = CONFIG_PATH_DEFAULT) { const configStr = readFileSync(configPath, 'utf8') return deserialize(configStr) } -export async function set (config: EntropyConfig, configPath = CONFIG_PATH) { +export async function set (config: EntropyConfig, configPath = CONFIG_PATH_DEFAULT) { assertConfigPath(configPath) await mkdirp(dirname(configPath)) await writeFile(configPath, serialize(config)) } -export async function setSelectedAccount (account: EntropyAccountConfig, configPath = CONFIG_PATH) { +export async function setSelectedAccount (account: EntropyAccountConfig, configPath = CONFIG_PATH_DEFAULT) { const storedConfig = await get(configPath) if (storedConfig.selectedAccount === account.name) return storedConfig diff --git a/src/program/utils.ts b/src/program/utils.ts index 3cc60746..1c49ebaa 100644 --- a/src/program/utils.ts +++ b/src/program/utils.ts @@ -1,23 +1,20 @@ import Entropy from "@entropyxyz/sdk" import fs from "node:fs/promises" -import { isAbsolute, join } from "node:path" import { u8aToHex } from "@polkadot/util" -import { print } from "../common/utils" +import { print, absolutePath } from "../common/utils" -export async function loadFile (path?: string, encoding?: string) { - if (path === undefined) return +export async function loadFile (somePath?: string, encoding?: string) { + if (somePath === undefined) return - const absolutePath = isAbsolute(path) - ? path - : join(process.cwd(), path) + const path = absolutePath(somePath) switch (encoding) { case undefined: - return fs.readFile(absolutePath) + return fs.readFile(path) case 'json': - return fs.readFile(absolutePath, 'utf-8') + return fs.readFile(path, 'utf-8') .then(string => JSON.parse(string)) default: diff --git a/src/tui.ts b/src/tui.ts index e7adbb02..2cc877c6 100644 --- a/src/tui.ts +++ b/src/tui.ts @@ -1,9 +1,11 @@ import inquirer from 'inquirer' import Entropy from '@entropyxyz/sdk' import * as config from './config' +import { CONFIG_PATH_DEFAULT } from './config' import { EntropyTuiOptions } from './types' import { logo } from './common/ascii' import { print } from './common/utils' +import { ENTROPY_ENDPOINT_DEFAULT } from './common/constants' import { loadEntropy } from './common/utils-cli' import { EntropyLogger } from './common/logger' @@ -14,25 +16,27 @@ import { entropyTransfer } from './transfer/interaction' import { entropyFaucet } from './faucet/interaction' import { entropyProgram, entropyProgramDev } from './program/interaction' -async function setupConfig () { - let storedConfig = await config.get() - - // set selectedAccount if we can - if (!storedConfig.selectedAccount && storedConfig.accounts.length) { - storedConfig = await config.setSelectedAccount(storedConfig.accounts[0]) +// tui = text user interface +export default async function tui (opts: EntropyTuiOptions) { + const options = { + ...opts, + config: CONFIG_PATH_DEFAULT, + endpoint: ENTROPY_ENDPOINT_DEFAULT } - return storedConfig -} - -// tui = text user interface -export default function tui (entropy: Entropy, options: EntropyTuiOptions) { const logger = new EntropyLogger('TUI', options.endpoint) console.clear() console.log(logo) // the Entropy logo logger.debug(options) - let choices = [ + const storedConfig = await setupConfig(options.config) + const entropy = await loadEntropy({ + account: storedConfig.selectedAccount, + config: storedConfig, + endpoint: options.endpoint + }) + + const choices = [ 'Manage Accounts', 'Entropy Faucet', 'Balance', @@ -42,19 +46,29 @@ export default function tui (entropy: Entropy, options: EntropyTuiOptions) { // TODO: design programs in TUI (merge deploy+user programs) 'Deploy Program', 'User Programs', + 'Exit' ] - // assign exit so its last - choices = [...choices, 'Exit'] main(entropy, choices, options, logger) } +async function setupConfig (configPath) { + let storedConfig = await config.get(configPath) + + // set selectedAccount if we can + if (!storedConfig.selectedAccount && storedConfig.accounts.length) { + storedConfig = await config.setSelectedAccount(storedConfig.accounts[0]) + } + + return storedConfig +} + async function main (entropy: Entropy, choices, options, logger: EntropyLogger) { - const storedConfig = await setupConfig() + const storedConfig = await setupConfig(options.config) - // Entropy is undefined on initial install, after user creates their first account, - // entropy should be loaded + // Entropy is undefined on initial install + // However, after user creates their first account, entropy can be loaded if (storedConfig.selectedAccount && !entropy) { entropy = await loadEntropy({ account: storedConfig.selectedAccount, @@ -62,7 +76,8 @@ async function main (entropy: Entropy, choices, options, logger: EntropyLogger) endpoint: options.endpoint }) } - // If the selected account changes within the TUI we need to reset the entropy instance being used + + // If the selected account changes within the TUI we reload entropy to use that account const currentAccount = entropy?.keyring?.accounts?.registration?.address if (currentAccount && currentAccount !== storedConfig.selectedAccount) { await entropy.close() diff --git a/src/types/index.ts b/src/types/index.ts index d1411368..9ad97529 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,6 +1,9 @@ export interface EntropyTuiOptions { - account: string - endpoint: string + /* + * NOTE: we can't currently set account, endpoint, see cli.ts notes + */ + // account: string + // endpoint: string dev: boolean } diff --git a/tests/e2e.cli.sh b/tests/e2e.cli.sh index 119f1ad7..74bd26e6 100755 --- a/tests/e2e.cli.sh +++ b/tests/e2e.cli.sh @@ -9,7 +9,8 @@ # Run # $ yarn build && ./tests/e2e.cli.sh -rm ~/.config/entropy-cryptography/entropy-cli.json +CURRENT_DATE=$(date +%s%N) +export ENTROPY_CONFIG="/tmp/entropy-cli-${CURRENT_DATE}.e2e.json" print () { COLOR='\033[0;35m'