diff --git a/src/account/command.ts b/src/account/command.ts index 4c9db8bf..06cf3a58 100644 --- a/src/account/command.ts +++ b/src/account/command.ts @@ -4,7 +4,7 @@ import { EntropyAccount } from "./main"; import { selectAndPersistNewAccount } from "./utils"; import { ACCOUNTS_CONTENT } from './constants' import * as config from '../config' -import { cliWrite, currentAccountAddressOption, endpointOption, loadEntropy, passwordOption } from "../common/utils-cli"; +import { cliWrite, accountOption, endpointOption, loadEntropy, passwordOption } from "../common/utils-cli"; import { findAccountByAddressOrName } from "src/common/utils"; export function entropyAccountCommand () { @@ -87,7 +87,7 @@ function entropyAccountRegister () { .description('Register an entropy account with a program') .addOption(passwordOption()) .addOption(endpointOption()) - .addOption(currentAccountAddressOption()) + .addOption(accountOption()) // 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 0a169888..84d36b31 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -4,7 +4,8 @@ import { Command, Option } from 'commander' import { EntropyTuiOptions } from './types' -import { currentAccountAddressOption, endpointOption, loadEntropy } from './common/utils-cli' +import { accountOption, endpointOption, loadEntropy } from './common/utils-cli' +import * as config from './config' import launchTui from './tui' import { entropyAccountCommand } from './account/command' @@ -18,9 +19,8 @@ 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(currentAccountAddressOption()) + .addOption(accountOption()) .addOption(endpointOption()) - // NOTE: I think this is currently unused .addOption( new Option( '-d, --dev', @@ -33,10 +33,17 @@ program .addCommand(entropyAccountCommand()) .addCommand(entropyTransferCommand()) .addCommand(entropySignCommand()) - .action(async (options: EntropyTuiOptions) => { - const { account, endpoint } = options - const entropy = await loadEntropy(account, endpoint) - launchTui(entropy, options) + .action(async (opts: EntropyTuiOptions) => { + const { account, endpoint } = opts + const entropy = account + ? await loadEntropy(account, endpoint) + : undefined + // NOTE: on initial startup you have no account + launchTui(entropy, opts) + }) + .hook('preAction', async () => { + // set up config file, run migrations + return config.init() }) -program.parseAsync().then(() => {}) +program.parseAsync() diff --git a/src/common/utils-cli.ts b/src/common/utils-cli.ts index eae015cb..3aec350b 100644 --- a/src/common/utils-cli.ts +++ b/src/common/utils-cli.ts @@ -1,7 +1,7 @@ +import Entropy from '@entropyxyz/sdk' import { Option } from 'commander' import { findAccountByAddressOrName, stringify } from './utils' import * as config from '../config' -import Entropy from '@entropyxyz/sdk' import { initializeEntropy } from './initializeEntropy' export function cliWrite (result) { @@ -9,6 +9,15 @@ export function cliWrite (result) { process.stdout.write(prettyResult) } +function getConfigOrNull () { + try { + return config.getSync() + } catch (err) { + if (config.isDangerousReadError(err)) throw err + return null + } +} + export function endpointOption () { return new Option( '-e, --endpoint ', @@ -17,14 +26,14 @@ export function endpointOption () { 'Can also be given a stored endpoint name from config eg: `entropy --endpoint test-net`.' ].join(' ') ) - .env('ENDPOINT') + .env('ENTROPY_ENDPOINT') .argParser(aliasOrEndpoint => { /* see if it's a raw endpoint */ if (aliasOrEndpoint.match(/^wss?:\/\//)) return aliasOrEndpoint /* look up endpoint-alias */ - const storedConfig = config.getSync() - const endpoint = storedConfig.endpoints[aliasOrEndpoint] + const storedConfig = getConfigOrNull() + const endpoint = storedConfig?.endpoints?.[aliasOrEndpoint] if (!endpoint) throw Error('unknown endpoint alias: ' + aliasOrEndpoint) return endpoint @@ -40,31 +49,37 @@ export function passwordOption (description?: string) { ) } -export function currentAccountAddressOption () { - const storedConfig = config.getSync() +export function accountOption () { + const storedConfig = getConfigOrNull() return new Option( - '-a, --account
', - 'Sets the current account for the session or defaults to the account stored in the config' + '-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).' + ].join(' ') ) - .env('ACCOUNT_ADDRESS') + .env('ENTROPY_ACCOUNT') .argParser(async (account) => { - if (account === storedConfig.selectedAccount) return account - // Updated selected account in config with new address from this option - const newConfigUpdates = { selectedAccount: account } - await config.set({ ...storedConfig, ...newConfigUpdates }) + if (storedConfig && storedConfig.selectedAccount !== account) { + // Updated selected account in config with new address from this option + await config.set({ + ...storedConfig, + selectedAccount: account + }) + } return account }) - .default(storedConfig.selectedAccount) + .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 { - const storedConfig = config.getSync() - const selectedAccount = findAccountByAddressOrName(storedConfig.accounts, addressOrName) - if (!selectedAccount) throw new Error(`AddressError: No account with name or address "${addressOrName}"`) + const accounts = getConfigOrNull()?.accounts || [] + const selectedAccount = findAccountByAddressOrName(accounts, addressOrName) + if (!selectedAccount) throw new Error(`No account with name or address: "${addressOrName}"`) // check if data is encrypted + we have a password if (typeof selectedAccount.data === 'string' && !password) { diff --git a/src/config/index.ts b/src/config/index.ts index 73c91e27..049b6775 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -1,5 +1,5 @@ import { readFile, writeFile, rm } from 'node:fs/promises' -import { readFileSync, writeFileSync } from 'node:fs' +import { readFileSync } from 'node:fs' import { mkdirp } from 'mkdirp' import { join, dirname } from 'path' import envPaths from 'env-paths' @@ -35,9 +35,10 @@ function hasRunMigration (config: any, version: number) { export async function init (configPath = CONFIG_PATH, oldConfigPath = OLD_CONFIG_PATH) { const currentConfig = await get(configPath) - .catch(async (err) => { - if (err.code !== 'ENOENT') throw err + .catch(async (err ) => { + if (isDangerousReadError(err)) throw err + // If there is no current config, try loading the old one const oldConfig = await get(oldConfigPath).catch(noop) // drop errors if (oldConfig) { // move the config @@ -58,33 +59,30 @@ export async function init (configPath = CONFIG_PATH, oldConfigPath = OLD_CONFIG export async function get (configPath = CONFIG_PATH) { return readFile(configPath, 'utf-8') .then(deserialize) - .catch(makeGetErrorHandler(configPath)) } -export function getSync (configPath = CONFIG_PATH): EntropyConfig { - try { - const configBuffer = readFileSync(configPath, 'utf8') - return deserialize(configBuffer) - } catch (err) { - return makeGetErrorHandler(configPath)(err) - } +export function getSync (configPath = CONFIG_PATH) { + const configStr = readFileSync(configPath, 'utf8') + return deserialize(configStr) } export async function set (config: EntropyConfig, configPath = CONFIG_PATH) { + assertConfigPath(configPath) + await mkdirp(dirname(configPath)) await writeFile(configPath, serialize(config)) } /* util */ function noop () {} - -function makeGetErrorHandler (configPath) { - return function getErrorHandler (err) { - if (err.code !== 'ENOENT') throw err - - const newConfig = migrateData(allMigrations, {}) - mkdirp.sync(dirname(configPath)) - writeFileSync(configPath, serialize(newConfig)) - return newConfig +function assertConfigPath (configPath) { + if (!configPath.endsWith('.json')) { + throw Error(`configPath must be of form *.json, got ${configPath}`) } } +export function isDangerousReadError (err) { + // file not found: + if (err.code === 'ENOENT') return false + + return true +} diff --git a/src/sign/command.ts b/src/sign/command.ts index db116d74..fc574228 100644 --- a/src/sign/command.ts +++ b/src/sign/command.ts @@ -1,5 +1,5 @@ import { Command, /* Option */ } from 'commander' -import { cliWrite, currentAccountAddressOption, endpointOption, loadEntropy, passwordOption } from '../common/utils-cli' +import { cliWrite, accountOption, endpointOption, loadEntropy, passwordOption } from '../common/utils-cli' import { EntropySign } from './main' export function entropySignCommand () { @@ -8,7 +8,7 @@ export function entropySignCommand () { .argument('msg', 'Message you would like to sign (string)') .addOption(passwordOption('Password for the source account (if required)')) .addOption(endpointOption()) - .addOption(currentAccountAddressOption()) + .addOption(accountOption()) // .addOption( // new Option( // '-r, --raw', diff --git a/src/transfer/command.ts b/src/transfer/command.ts index 537f2149..b814e2f9 100644 --- a/src/transfer/command.ts +++ b/src/transfer/command.ts @@ -1,5 +1,5 @@ import { Command } from "commander" -import { currentAccountAddressOption, endpointOption, loadEntropy, passwordOption } from "src/common/utils-cli" +import { accountOption, endpointOption, loadEntropy, passwordOption } from "src/common/utils-cli" import { EntropyTransfer } from "./main" export function entropyTransferCommand () { @@ -10,7 +10,7 @@ export function entropyTransferCommand () { .argument('amount', 'Amount of funds to be moved') .addOption(passwordOption('Password for the source account (if required)')) .addOption(endpointOption()) - .addOption(currentAccountAddressOption()) + .addOption(accountOption()) .action(async (destination, amount, opts) => { const entropy = await loadEntropy(opts.account, opts.endpoint) const transferService = new EntropyTransfer(entropy, opts.endpoint) diff --git a/tests/config.test.ts b/tests/config.test.ts index 8f28396b..5e2d0215 100644 --- a/tests/config.test.ts +++ b/tests/config.test.ts @@ -81,10 +81,11 @@ test('config - get', async t => { const result = await get(configPath) t.deepEqual(result, config, 'get works') + const MSG = 'path that does not exist fails' await get('/tmp/junk') - .then(() => t.fail('bad path should fail')) + .then(() => t.fail(MSG)) .catch(err => { - t.match(err.message, /no such file/, 'bad path should fail') + t.match(err.message, /ENOENT/, MSG) }) }) @@ -95,6 +96,7 @@ test('config - set', async t => { dog: true, secretKey: makeKey() } + // @ts-expect-error : this is a breaking test await set(config, configPath) const actual = await get(configPath)