diff --git a/src/account/command.ts b/src/account/command.ts index 4d9963c..30b69dd 100644 --- a/src/account/command.ts +++ b/src/account/command.ts @@ -1,10 +1,11 @@ import Entropy from "@entropyxyz/sdk" import { Command, Option } from 'commander' + import { EntropyAccount } from "./main"; import { selectAndPersistNewAccount, persistVerifyingKeyToAccount, generateAccountDataForPrint } from "./utils"; import { ACCOUNTS_CONTENT } from './constants' import * as config from '../config' -import { accountOption, endpointOption, cliWrite } from "../common/utils-cli"; +import { accountOption, configOption, endpointOption, cliWrite } from "../common/utils-cli"; import { loadEntropyCli } from "../common/load-entropy" export function entropyAccountCommand () { @@ -24,6 +25,7 @@ function entropyAccountCreate () { .alias('new') .description('Create a new entropy account from scratch. Output is JSON of form {name, address}') .argument('', 'A user friendly name for your new account.') + .addOption(configOption()) .addOption( new Option( '--path', @@ -31,10 +33,10 @@ function entropyAccountCreate () { ).default(ACCOUNTS_CONTENT.path.default) ) .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(generateAccountDataForPrint(newAccount)) @@ -47,6 +49,7 @@ function entropyAccountImport () { .description('Import an existing entropy account from seed. Output is JSON of form {name, address}') .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 +57,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, @@ -72,9 +75,9 @@ function entropyAccountList () { return new Command('list') .alias('ls') .description('List all accounts. Output is JSON of form [{ name, address, verifyingKeys }]') - .action(async () => { - // 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 accounts = await config.get() + .addOption(configOption()) + .action(async (opts) => { + const accounts = await config.get(opts.config) .then(storedConfig => EntropyAccount.list(storedConfig)) .catch((err) => { if (err.message.includes('currently no accounts')) return [] @@ -92,6 +95,7 @@ function entropyAccountRegister () { return new Command('register') .description('Register an entropy account with a program') .addOption(accountOption()) + .addOption(configOption()) .addOption(endpointOption()) // Removing these options for now until we update the design to accept program configs // .addOption( @@ -111,7 +115,8 @@ function entropyAccountRegister () { const accountService = new EntropyAccount(entropy, opts.endpoint) const verifyingKey = await accountService.register() - await persistVerifyingKeyToAccount(verifyingKey, opts.account) + // TODO: handle error + await persistVerifyingKeyToAccount(opts.config, verifyingKey, opts.account) cliWrite(verifyingKey) process.exit(0) diff --git a/src/account/interaction.ts b/src/account/interaction.ts index b1bd6d6..680016e 100644 --- a/src/account/interaction.ts +++ b/src/account/interaction.ts @@ -1,7 +1,9 @@ import inquirer from "inquirer"; import Entropy from "@entropyxyz/sdk"; +import yoctoSpinner from "yocto-spinner"; import { EntropyAccount } from './main' +import { EntropyTuiOptions } from '../types' import { selectAndPersistNewAccount, persistVerifyingKeyToAccount, generateAccountDataForPrint } from "./utils"; import { findAccountByAddressOrName, print } from "../common/utils" import { EntropyConfig } from "../config/types"; @@ -12,13 +14,11 @@ import { accountNewQuestions, accountSelectQuestions } from "./utils" -import { ERROR_RED } from "src/common/constants"; -import yoctoSpinner from "yocto-spinner"; /* * @returns partialConfigUpdate | "exit" | undefined */ -export async function entropyAccount (endpoint: string, storedConfig: EntropyConfig) { +export async function entropyAccount (opts: EntropyTuiOptions, storedConfig: EntropyConfig) { const { accounts } = storedConfig const { interactionChoice } = await inquirer.prompt(accountManageQuestions) @@ -37,7 +37,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(opts.config, newAccount) print(generateAccountDataForPrint(newAccount)) return } @@ -48,8 +48,8 @@ export async function entropyAccount (endpoint: string, storedConfig: EntropyCon return } const { selectedAccount } = await inquirer.prompt(accountSelectQuestions(accounts)) - - await config.setSelectedAccount(selectedAccount) + + await config.setSelectedAccount(opts.config, selectedAccount) print('Current selected account is:') print({ name: selectedAccount.name, address: selectedAccount.address }) @@ -77,8 +77,9 @@ export async function entropyAccount (endpoint: string, storedConfig: EntropyCon const registrationSpinner = yoctoSpinner() const SPINNER_TEXT = 'Registering account…' -export async function entropyRegister (entropy: Entropy, endpoint: string, storedConfig: EntropyConfig): Promise> { - const accountService = new EntropyAccount(entropy, endpoint) + +export async function entropyRegister (entropy: Entropy, opts: EntropyTuiOptions, storedConfig: EntropyConfig): Promise> { + const accountService = new EntropyAccount(entropy, opts.endpoint) const { accounts, selectedAccount } = storedConfig const account = findAccountByAddressOrName(accounts, selectedAccount) @@ -94,7 +95,8 @@ export async function entropyRegister (entropy: Entropy, endpoint: string, store try { if (!registrationSpinner.isSpinning) registrationSpinner.start() const verifyingKey = await accountService.register() - await persistVerifyingKeyToAccount(verifyingKey, account.address) + await persistVerifyingKeyToAccount(opts.config, verifyingKey, account.address) + if (registrationSpinner.isSpinning) registrationSpinner.stop() print("Your address", account.address, "has been successfully registered.") } catch (error) { @@ -102,9 +104,9 @@ export async function entropyRegister (entropy: Entropy, endpoint: string, store registrationSpinner.text = 'Registration has failed...' if (registrationSpinner.isSpinning) registrationSpinner.stop() if (error.message.includes(endpointErrorMessageToMatch)) { - console.error(ERROR_RED + 'GenericError: Incompatible endpoint, expected core version 0.3.0, got 0.2.0') + print.error('GenericError: Incompatible endpoint, expected core version 0.3.0, got 0.2.0') return } - console.error(ERROR_RED + 'RegisterError:', error.message); + print.error('RegisterError:', error.message) } } diff --git a/src/account/utils.ts b/src/account/utils.ts index a50760b..d8ef02c 100644 --- a/src/account/utils.ts +++ b/src/account/utils.ts @@ -7,8 +7,8 @@ import { EntropyConfigAccount } from "../config/types"; import * as config from "../config"; import { generateAccountChoices, findAccountByAddressOrName } from '../common/utils'; -export async function selectAndPersistNewAccount (newAccount: EntropyConfigAccount) { - const storedConfig = await config.get() +export async function selectAndPersistNewAccount (configPath: string, newAccount: EntropyConfigAccount) { + const storedConfig = await config.get(configPath) const { accounts } = storedConfig const isExistingName = accounts.find(account => account.name === newAccount.name) @@ -22,14 +22,14 @@ export async function selectAndPersistNewAccount (newAccount: EntropyConfigAccou // persist to config, set selectedAccount accounts.push(newAccount) - await config.set({ + await config.set(configPath, { ...storedConfig, - selectedAccount: newAccount.name + selectedAccount: newAccount.address }) } -export async function persistVerifyingKeyToAccount (verifyingKey: string, accountNameOrAddress: string) { - const storedConfig = await config.get() +export async function persistVerifyingKeyToAccount (configPath: string, verifyingKey: string, accountNameOrAddress: string) { + const storedConfig = await config.get(configPath) const { accounts } = storedConfig const account = findAccountByAddressOrName(accounts, accountNameOrAddress) @@ -37,7 +37,10 @@ export async function persistVerifyingKeyToAccount (verifyingKey: string, accoun // persist to config, set selectedAccount account.data.registration.verifyingKeys.push(verifyingKey) - await config.set(storedConfig) + await config.set(configPath, { + ...storedConfig, + selectedAccount: account.name + }) } function validateSeedInput (seed) { diff --git a/src/balance/command.ts b/src/balance/command.ts index 97f13a8..a4744c7 100644 --- a/src/balance/command.ts +++ b/src/balance/command.ts @@ -2,7 +2,7 @@ import { Command } from "commander"; import Entropy from "@entropyxyz/sdk"; import { EntropyBalance } from "./main"; -import { endpointOption, cliWrite } from "../common/utils-cli"; +import { configOption, endpointOption, cliWrite } from "../common/utils-cli"; import { findAccountByAddressOrName, getTokenDetails, nanoBitsToBits, round } from "../common/utils"; import { loadEntropyCli } from "../common/load-entropy" import * as config from "../config"; @@ -11,17 +11,18 @@ export function entropyBalanceCommand () { const balanceCommand = new Command('balance') balanceCommand .description('Command to retrieive the balance of an account on the Entropy Network') - .argument('', [ + .argument('account ', [ 'The address an account address whose balance you want to query.', 'Can also be the human-readable name of one of your accounts' ].join(' ')) + .addOption(configOption()) .addOption(endpointOption()) .action(async (account, opts) => { const entropy: Entropy = await loadEntropyCli({ account, ...opts }) const balanceService = new EntropyBalance(entropy, opts.endpoint) const { decimals, symbol } = await getTokenDetails(entropy) - const { accounts } = await config.get() + const { accounts } = await config.get(opts.config) const address = findAccountByAddressOrName(accounts, account)?.address const nanoBalance = await balanceService.getBalance(address) diff --git a/src/balance/interaction.ts b/src/balance/interaction.ts index c37aa31..c904f88 100644 --- a/src/balance/interaction.ts +++ b/src/balance/interaction.ts @@ -1,11 +1,13 @@ -import { findAccountByAddressOrName, getTokenDetails, print, round, nanoBitsToBits } from "src/common/utils" import { EntropyBalance } from "./main" +import { findAccountByAddressOrName, getTokenDetails, print, round, nanoBitsToBits } from "../common/utils" -export async function entropyBalance (entropy, endpoint, storedConfig) { +import { EntropyTuiOptions } from '../types' + +export async function entropyBalance (entropy, opts: EntropyTuiOptions, storedConfig) { try { // grabbing decimals from chain spec as that is the source of truth for the value const { decimals, symbol } = await getTokenDetails(entropy) - const balanceService = new EntropyBalance(entropy, endpoint) + const balanceService = new EntropyBalance(entropy, opts.endpoint) const address = findAccountByAddressOrName(storedConfig.accounts, storedConfig.selectedAccount)?.address const nanoBalance = await balanceService.getBalance(address) const balance = round(nanoBitsToBits(nanoBalance, decimals)) diff --git a/src/cli.ts b/src/cli.ts index c7064e3..d9f5557 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -44,15 +44,13 @@ cli // print entropy help and exit cli.help() - - // tuiAction(opts) - // NOTE: this doesn't quite work, because -a, -e are not defined as options - // and if we do put them in here it gets a bit confusing }) // set up config file, run migrations - .hook('preAction', async () => { - return config.init() + .hook('preAction', async (thisCommand, actionCommand) => { + const { config: configPath } = actionCommand.opts() + + if (configPath) await config.init(configPath) }) cli.parseAsync() diff --git a/src/common/constants.ts b/src/common/constants.ts index 5c78824..9218384 100644 --- a/src/common/constants.ts +++ b/src/common/constants.ts @@ -1,4 +1,6 @@ +export const ENTROPY_ENDPOINT_DEFAULT = 'wss://testnet.entropy.xyz/' + // ASCII Colors for Logging to Console export const ERROR_RED = '\u001b[31m' export const SUCCESS_GREEN = '\u001b[32m' -export const INFO_BLUE = '\u001b[34m' \ No newline at end of file +export const INFO_BLUE = '\u001b[34m' diff --git a/src/common/initializeEntropy.ts b/src/common/initializeEntropy.ts deleted file mode 100644 index 8a5cded..0000000 --- a/src/common/initializeEntropy.ts +++ /dev/null @@ -1,134 +0,0 @@ -import Entropy, { wasmGlobalsReady } from "@entropyxyz/sdk" -// TODO: fix importing of types from @entropy/sdk/keys -// @ts-ignore -import Keyring from "@entropyxyz/sdk/keys" -import * as config from "../config" -import { EntropyConfigAccountData } from "../config/types" -import { EntropyLogger } from "./logger" - -// TODO: unused -// let defaultAccount // have a main account to use -// let entropys - -// a cache of keyrings -const keyrings = { - default: undefined // this is the "selected account" keyring -} - -export function getKeyring (address?: string) { - - if (!address && keyrings.default) return keyrings.default - if (address && keyrings[address]) return keyrings[address] - // explicitly return undefined so there is no confusion around what is selected - return undefined -} - -interface InitializeEntropyOpts { - keyMaterial: MaybeKeyMaterial, - endpoint: string, - configPath?: string // for testing -} -type MaybeKeyMaterial = EntropyConfigAccountData | string - -// WARNING: in programatic cli mode this function should NEVER prompt users - -export const initializeEntropy = async ({ keyMaterial, endpoint, configPath }: InitializeEntropyOpts): Promise => { - const logger = new EntropyLogger('initializeEntropy', endpoint) - try { - await wasmGlobalsReady() - - const { accountData } = await getAccountData(keyMaterial) - // check if there is no admin account and no seed so that we can throw an error - if (!accountData.seed && !accountData.admin) { - throw new Error("Data format is not recognized as either encrypted or unencrypted") - } - - if (accountData && accountData.admin && !accountData.registration) { - accountData.registration = accountData.admin - accountData.registration.used = true // TODO: is this even used? - const store = await config.get(configPath) - store.accounts = store.accounts.map((account) => { - if (account.address === accountData.admin.address) { - account = { - ...account, - data: accountData, - } - } - return account - }) - // re save the entire config - await config.set(store, configPath) - } - - let selectedAccount - const storedKeyring = getKeyring(accountData.admin.address) - - if(!storedKeyring) { - const keyring = new Keyring({ ...accountData, debug: true }) - keyring.accounts.on('account-update', async (newAccountData) => { - const store = await config.get(configPath) - store.accounts = store.accounts.map((account) => { - if (account.address === store.selectedAccount) { - const newAccount = { - ...account, - data: newAccountData, - } - return newAccount - } - return account - }) - - // re save the entire config - await config.set(store, configPath) - - }) - keyrings.default = keyring - logger.debug(keyring) - - // TO-DO: fix in sdk: admin should be on kering.accounts by default - // /*WANT*/ keyrings[keyring.admin.address] = keyring - keyrings[keyring.getAccount().admin.address] = keyring - selectedAccount = keyring - } else { - keyrings.default = storedKeyring - selectedAccount = storedKeyring - } - - const entropy = new Entropy({ keyring: selectedAccount, endpoint }) - await entropy.ready - - if (!entropy?.keyring?.accounts?.registration?.seed) { - throw new Error("Keys are undefined") - } - - return entropy - } catch (error) { - logger.error('Error while initializing entropy', error) - console.error(error.message) - if (error.message.includes('TimeError')) { - process.exit(1) - } - } -} - - -// NOTE: frankie this was prettier before I had to refactor it for merge conflicts, promise -async function getAccountData (keyMaterial: MaybeKeyMaterial): Promise<{ accountData: EntropyConfigAccountData }> { - if (isEntropyAccountData(keyMaterial)) { - return { - accountData: keyMaterial as EntropyConfigAccountData - } - } - - if (typeof keyMaterial !== 'string') { - throw new Error("Data format is not recognized as either encrypted or unencrypted") - } -} - -function isEntropyAccountData (maybeAccountData: any) { - return ( - maybeAccountData && - typeof maybeAccountData === 'object' && - 'seed' in maybeAccountData - ) -} diff --git a/src/common/load-entropy.ts b/src/common/load-entropy.ts index 9b76be0..230490d 100644 --- a/src/common/load-entropy.ts +++ b/src/common/load-entropy.ts @@ -23,7 +23,7 @@ export async function loadEntropyCli (opts: LoadEntropyCliOpts) { return fsConfig.get(opts.config) }, async set (config: EntropyConfig) { - return fsConfig.set(config, opts.config) + return fsConfig.set(opts.config, config) } } }) @@ -47,7 +47,7 @@ export async function loadEntropyTui (opts: LoadEntropyTuiOpts) { return fsConfig.get(opts.config) }, async set (config: EntropyConfig) { - return fsConfig.set(config, opts.config) + return fsConfig.set(opts.config, config) } } }) @@ -70,14 +70,17 @@ export async function loadEntropyTest (opts: LoadEntropyTestOpts) { const keyring = new Keyring({ seed: opts.seed, debug: true }) const account = keyring.getAccount() + + const accountName = 'test-account' config.accounts.push({ - name: 'test-account', + name: accountName, address: account.admin.address, data: account }) - config.selectedAccount = 'test-account' + config.selectedAccount = accountName return loadEntropy({ + account: accountName, ...opts, config: { async get () { @@ -163,15 +166,18 @@ function resolveEndpoint (config: EntropyConfig, aliasOrEndpoint: string) { } } -function resolveAccount (config: EntropyConfig, addressOrName?: string): EntropyConfigAccount|null { +function resolveAccount (config: EntropyConfig, addressOrName: string): EntropyConfigAccount { if (!config.accounts) throw Error('no accounts') - const account = findAccountByAddressOrName(config.accounts, addressOrName || config.selectedAccount) + const account = findAccountByAddressOrName(config.accounts, addressOrName) if (!account) { // there are accounts, but not match found - print(`AccountError: No account with name or address "${addressOrName}"`) + const msg = `AccountError: No account with name or address "${addressOrName}"` + // TODO: move printing out to loadEntropyCli, loadEntropyTui + // could bolt hint on to error: error.hint = "Available acounts..." + print.error(msg) print(bold('!! Available accounts can be found using `entropy account list` !!')) - process.exit(1) + throw Error(msg) } // there are accounts, we found a match diff --git a/src/common/utils-cli.ts b/src/common/utils-cli.ts index 270d95e..ae3ae3f 100644 --- a/src/common/utils-cli.ts +++ b/src/common/utils-cli.ts @@ -1,22 +1,14 @@ import { Option } from 'commander' -import { bold, findAccountByAddressOrName, print, stringify } from './utils' +import { stringify, absolutePath } from './utils' import * as config from '../config' +import { ENTROPY_ENDPOINT_DEFAULT } from '../common/constants' export function cliWrite (result) { const prettyResult = stringify(result, 0) 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 ', @@ -26,25 +18,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() - const endpoint = storedConfig?.endpoints?.[aliasOrEndpoint] - if (!endpoint) throw Error('unknown endpoint alias: ' + aliasOrEndpoint) - - return endpoint - }) - .default('wss://testnet.entropy.xyz/') - // 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 () { - const storedConfig = getConfigOrNull() - return new Option( '-a, --account ', [ @@ -53,30 +30,18 @@ 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) { - console.error(`AccountError: [${addressOrName}] is not a valid argument for the account option.`) - print(bold('!! Available accounts can be found using entropy account list || entropy account ls !!')) - process.exit(1) - } - - // 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 +export function configOption () { + return new Option( + '-c, --config ', + 'Set the path to your Entropy config file (JSON).', + ) + .env('ENTROPY_CONFIG') + .argParser(configPath => { + return absolutePath(configPath) }) - .default(storedConfig?.selectedAccount) + .default(config.CONFIG_PATH_DEFAULT) } export function verifyingKeyOption () { diff --git a/src/common/utils.ts b/src/common/utils.ts index 544cde5..4cf2e8e 100644 --- a/src/common/utils.ts +++ b/src/common/utils.ts @@ -1,8 +1,11 @@ import { Entropy } from '@entropyxyz/sdk' -import { Buffer } from 'buffer' -import { EntropyConfigAccount } from "../config/types" +import { Buffer } from 'node:buffer' +import { homedir } from 'node:os' +import { join } from 'node:path' + import { EntropyLogger } from './logger' -import { TokenDetails } from 'src/types' +import { EntropyConfigAccount } from '../config/types' +import { TokenDetails } from '../types' export function stripHexPrefix (str: string): string { if (str.startsWith('0x')) return str.slice(2) @@ -22,10 +25,26 @@ export function replacer (key, value) { else return value } + export function print (...args) { console.log(...args.map(arg => stringify(arg))) } +// ASCII color codes +const GREEN = '\u001b[32m' +const RED = '\u001b[31m' +const BLUE = '\u001b[34m' +const RESET = '\u001b[0m' +print.success = function printSuccess (...args) { + return print(GREEN, ...args, RESET) +} +print.error = function printError (...args) { + return print(RED, ...args, RESET) +} +print.info = function printInfo (...args) { + return print(BLUE, ...args, RESET) +} + export function bold (text) { return `\x1b[1m${text}\x1b[0m` } @@ -81,6 +100,17 @@ export function findAccountByAddressOrName (accounts: EntropyConfigAccount[], al ) } +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 + } +} + export function formatDispatchError (entropy: Entropy, dispatchError) { let msg: string if (dispatchError.isModule) { @@ -146,4 +176,4 @@ export function bitsToNanoBits (numOfBits: number, decimals: number): bigint { export function round (num: number, decimals: number = 4): number { return parseFloat(num.toFixed(decimals)) -} \ No newline at end of file +} diff --git a/src/config/index.ts b/src/config/index.ts index 3ecef86..07cce4f 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -1,5 +1,4 @@ import { readFile, writeFile, rm } from 'node:fs/promises' -import { readFileSync } from 'node:fs' import { mkdirp } from 'mkdirp' import { join, dirname } from 'path' import envPaths from 'env-paths' @@ -9,9 +8,9 @@ import { serialize, deserialize } from './encoding' import { EntropyConfig, EntropyConfigAccount } from './types' const paths = envPaths('entropy-cryptography', { suffix: '' }) -export const CONFIG_PATH = join(paths.config, 'entropy-cli.json') const OLD_CONFIG_PATH = join(process.env.HOME, '.entropy-cli.config') +export const CONFIG_PATH_DEFAULT = join(paths.config, 'entropy-cli.json') export const VERSION = 'migration-version' export function migrateData (migrations, currentConfig = {}) { @@ -33,7 +32,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: string, oldConfigPath = OLD_CONFIG_PATH) { const currentConfig = await get(configPath) .catch(async (err ) => { if (isDangerousReadError(err)) throw err @@ -42,7 +41,7 @@ export async function init (configPath = CONFIG_PATH, oldConfigPath = OLD_CONFIG const oldConfig = await get(oldConfigPath).catch(noop) // drop errors if (oldConfig) { // move the config - await set(oldConfig, configPath) + await set(configPath, oldConfig) await rm(oldConfigPath) return oldConfig } @@ -52,21 +51,16 @@ export async function init (configPath = CONFIG_PATH, oldConfigPath = OLD_CONFIG const newConfig = migrateData(allMigrations, currentConfig) if (newConfig[VERSION] !== currentConfig[VERSION]) { - await set(newConfig, configPath) + await set(configPath, newConfig) } } -export async function get (configPath = CONFIG_PATH) { +export async function get (configPath) { return readFile(configPath, 'utf-8') .then(deserialize) } -export function getSync (configPath = CONFIG_PATH) { - const configStr = readFileSync(configPath, 'utf8') - return deserialize(configStr) -} - -export async function set (config: EntropyConfig, configPath = CONFIG_PATH) { +export async function set (configPath: string, config: EntropyConfig) { assertConfig(config) assertConfigPath(configPath) @@ -74,7 +68,7 @@ export async function set (config: EntropyConfig, configPath = CONFIG_PATH) { await writeFile(configPath, serialize(config)) } -export async function setSelectedAccount (account: EntropyConfigAccount, configPath = CONFIG_PATH) { +export async function setSelectedAccount (configPath: string, account: EntropyConfigAccount) { const storedConfig = await get(configPath) if (storedConfig.selectedAccount === account.name) return storedConfig @@ -84,7 +78,7 @@ export async function setSelectedAccount (account: EntropyConfigAccount, configP ...storedConfig, selectedAccount: account.name } - await set(newConfig, configPath) + await set(configPath, newConfig) return newConfig } diff --git a/src/faucet/interaction.ts b/src/faucet/interaction.ts index e6fc92e..3cdf026 100644 --- a/src/faucet/interaction.ts +++ b/src/faucet/interaction.ts @@ -1,9 +1,11 @@ import Entropy from "@entropyxyz/sdk" import yoctoSpinner from 'yocto-spinner'; + import { EntropyLogger } from '../common/logger' import { FAUCET_PROGRAM_POINTER } from "./utils" import { EntropyFaucet } from "./main" -import { bitsToNanoBits, getTokenDetails, nanoBitsToBits, print, round } from "src/common/utils" +import { EntropyTuiOptions } from '../types' +import { bitsToNanoBits, getTokenDetails, nanoBitsToBits, print, round } from "../common/utils" let chosenVerifyingKeys = [] // Sending only 1e10 nanoBITS does not allow user's to register after receiving funds @@ -14,19 +16,19 @@ let chosenVerifyingKeys = [] const FLOW_CONTEXT = 'ENTROPY_FAUCET_INTERACTION' const SPINNER_TEXT = 'Funding account…' const faucetSpinner = yoctoSpinner() -export async function entropyFaucet (entropy: Entropy, options, logger: EntropyLogger) { + +export async function entropyFaucet (entropy: Entropy, opts: EntropyTuiOptions, logger: EntropyLogger) { faucetSpinner.text = SPINNER_TEXT if (faucetSpinner.isSpinning) { faucetSpinner.stop() } - const { endpoint } = options if (!entropy.registrationManager.signer.pair) { throw new Error("Keys are undefined") } const { decimals } = await getTokenDetails(entropy) const amount = bitsToNanoBits(2, decimals) - const faucetService = new EntropyFaucet(entropy, endpoint) + const faucetService = new EntropyFaucet(entropy, opts.endpoint) const verifyingKeys = await faucetService.getAllFaucetVerifyingKeys() // @ts-expect-error return sendMoneyFromRandomFaucet(entropy, options.endpoint, verifyingKeys, amount.toString(), logger) @@ -66,4 +68,4 @@ async function sendMoneyFromRandomFaucet (entropy: Entropy, endpoint: string, ve await sendMoneyFromRandomFaucet(entropy, endpoint, verifyingKeys, amount, logger) } } -} \ No newline at end of file +} diff --git a/src/program/command.ts b/src/program/command.ts index 5db31de..03f5a71 100644 --- a/src/program/command.ts +++ b/src/program/command.ts @@ -2,8 +2,8 @@ import { Command } from 'commander' import { EntropyProgram } from './main' import { - accountOption, endpointOption, verifyingKeyOption, programModKeyOption, - cliWrite + accountOption, endpointOption, configOption, verifyingKeyOption, programModKeyOption, + cliWrite, } from '../common/utils-cli' import { loadEntropyCli } from '../common/load-entropy' @@ -51,6 +51,7 @@ function entropyProgramDeploy () { ].join(' ') ) .addOption(accountOption()) + .addOption(configOption()) .addOption(endpointOption()) .action(async (bytecodePath, configurationSchemaPath, auxillaryDataSchemaPath, opts) => { // eslint-disable-line diff --git a/src/program/interaction.ts b/src/program/interaction.ts index 24f066f..abae02a 100644 --- a/src/program/interaction.ts +++ b/src/program/interaction.ts @@ -8,12 +8,13 @@ import { writeFile } from "node:fs/promises" import { displayPrograms, addQuestions, getProgramPointerInput, verifyingKeyQuestion } from "./utils"; import { EntropyProgram } from "./main"; import { print } from "../common/utils" +import { EntropyTuiOptions } from '../types' let verifyingKey: string; const paths = envPaths('entropy-cryptography', { suffix: '' }) -export async function entropyProgram (entropy: Entropy, endpoint: string) { +export async function entropyProgram (entropy: Entropy, opts: EntropyTuiOptions) { const actionChoice = await inquirer.prompt([ { type: "list", @@ -33,7 +34,7 @@ export async function entropyProgram (entropy: Entropy, endpoint: string) { throw new Error("Keys are undefined") } - const program = new EntropyProgram(entropy, endpoint) + const program = new EntropyProgram(entropy, opts.endpoint) switch (actionChoice.action) { case "View My Programs": { diff --git a/src/program/utils.ts b/src/program/utils.ts index 3cc6074..1c49eba 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/sign/command.ts b/src/sign/command.ts index 0cc5454..3dc19fd 100644 --- a/src/sign/command.ts +++ b/src/sign/command.ts @@ -1,7 +1,7 @@ import { Command, /* Option */ } from 'commander' import { EntropySign } from './main' -import { accountOption, endpointOption, cliWrite } from '../common/utils-cli' +import { accountOption, configOption, endpointOption, cliWrite } from '../common/utils-cli' import { loadEntropyCli } from '../common/load-entropy' export function entropySignCommand () { @@ -9,6 +9,7 @@ export function entropySignCommand () { .description('Sign a message using the Entropy network. Output is a JSON { verifyingKey, signature }') .argument('', 'Message you would like to sign (string)') .addOption(accountOption()) + .addOption(configOption()) .addOption(endpointOption()) // .addOption( // new Option( diff --git a/src/sign/interaction.ts b/src/sign/interaction.ts index 1cec6a6..e2ac5eb 100644 --- a/src/sign/interaction.ts +++ b/src/sign/interaction.ts @@ -1,11 +1,13 @@ -import { print } from "src/common/utils" -import { getMsgFromUser, /* interactionChoiceQuestions */ } from "./utils" import inquirer from "inquirer" import Entropy from "@entropyxyz/sdk" + import { EntropySign } from "./main" +import { print } from "../common/utils" +import { getMsgFromUser, /* interactionChoiceQuestions */ } from "./utils" +import { EntropyTuiOptions } from '../types' -export async function entropySign (entropy: Entropy, endpoint: string) { - const signingService = new EntropySign(entropy, endpoint) +export async function entropySign (entropy: Entropy, opts: EntropyTuiOptions) { + const signingService = new EntropySign(entropy, opts.endpoint) // const { interactionChoice } = await inquirer.prompt(interactionChoiceQuestions) // switch (interactionChoice) { // case 'Raw Sign': { diff --git a/src/transfer/command.ts b/src/transfer/command.ts index c62cb40..62601dc 100644 --- a/src/transfer/command.ts +++ b/src/transfer/command.ts @@ -1,9 +1,9 @@ import { Command } from "commander" import { EntropyTransfer } from "./main" -import { accountOption, cliWrite, endpointOption } from "src/common/utils-cli" +import { accountOption, configOption, endpointOption, cliWrite } from "../common/utils-cli" import { loadEntropyCli } from "../common/load-entropy" -import { getTokenDetails } from "src/common/utils" +import { getTokenDetails } from "../common/utils" export function entropyTransferCommand () { const transferCommand = new Command('transfer') @@ -12,6 +12,7 @@ export function entropyTransferCommand () { .argument('', 'Account address funds will be sent to') .argument('', 'Amount of funds (in "BITS") to be moved') .addOption(accountOption()) + .addOption(configOption()) .addOption(endpointOption()) .action(async (destination, amount, opts) => { // TODO: destination as ? diff --git a/src/transfer/interaction.ts b/src/transfer/interaction.ts index 2a538a1..59ba6af 100644 --- a/src/transfer/interaction.ts +++ b/src/transfer/interaction.ts @@ -1,27 +1,32 @@ import inquirer from "inquirer" +import yoctoSpinner from "yocto-spinner" + import { getTokenDetails, print } from "../common/utils" import { EntropyTransfer } from "./main" import { transferInputQuestions } from "./utils" -import yoctoSpinner from "yocto-spinner" -import { ERROR_RED } from "src/common/constants" + +import { EntropyTuiOptions } from '../types' const transferSpinner = yoctoSpinner() const SPINNER_TEXT = 'Transferring funds...' -export async function entropyTransfer (entropy, endpoint) { + +export async function entropyTransfer (entropy, opts: EntropyTuiOptions) { transferSpinner.text = SPINNER_TEXT if (transferSpinner.isSpinning) transferSpinner.stop() try { const { symbol } = await getTokenDetails(entropy) - const transferService = new EntropyTransfer(entropy, endpoint) + const transferService = new EntropyTransfer(entropy, opts.endpoint) const { amount, recipientAddress } = await inquirer.prompt(transferInputQuestions) if (!transferSpinner.isSpinning) transferSpinner.start() await transferService.transfer(recipientAddress, amount) if (transferSpinner.isSpinning) transferSpinner.stop() print('') print(`Transaction successful: Sent ${amount} ${symbol} to ${recipientAddress}`) + print('') + print('Press enter to return to main menu') } catch (error) { transferSpinner.text = 'Transfer failed...' if (transferSpinner.isSpinning) transferSpinner.stop() - console.error(ERROR_RED + 'TransferError:', error.message); + print.error('TransferError:', error.message); } } diff --git a/src/tui.ts b/src/tui.ts index 45f4276..35f3f00 100644 --- a/src/tui.ts +++ b/src/tui.ts @@ -7,7 +7,7 @@ import * as config from './config' import { EntropyTuiOptions } from './types' import { printLogo } from './common/ascii' import { jumpStartNetwork, print, findAccountByAddressOrName } from './common/utils' -import { accountOption, endpointOption } from './common/utils-cli' +import { accountOption, endpointOption, configOption } from './common/utils-cli' import { loadEntropyTui } from "./common/load-entropy" import { EntropyLogger } from './common/logger' @@ -24,6 +24,7 @@ export function entropyTuiCommand () { .addOption(accountOption()) .addOption(endpointOption()) + .addOption(configOption()) .addOption( new Option( '-d, --dev', @@ -40,10 +41,6 @@ export async function tuiAction (opts: EntropyTuiOptions) { const logger = new EntropyLogger('TUI', opts.endpoint) logger.debug(opts) - if (!opts.config) { - opts.config = config.CONFIG_PATH // TEMP - } - const entropyPromise = opts.account ? loadEntropyTui(opts) : Promise.resolve(undefined) @@ -83,12 +80,14 @@ export async function tuiAction (opts: EntropyTuiOptions) { } const loader = yoctoSpinner() -async function setupConfig () { - let storedConfig = await config.get() +// Loads the config, AND tries to ensure a selectedAccount is set +// NOTE: this should disappear with TUI Redesign +async function setupConfig (configPath: string) { + let storedConfig = await config.get(configPath) // set selectedAccount if we can if (!storedConfig.selectedAccount && storedConfig.accounts.length) { - storedConfig = await config.setSelectedAccount(storedConfig.accounts[0]) + storedConfig = await config.setSelectedAccount(configPath, storedConfig.accounts[0]) } return storedConfig @@ -96,10 +95,10 @@ async function setupConfig () { async function main (entropy: Entropy, choices: string[], opts: EntropyTuiOptions, logger: EntropyLogger) { if (loader.isSpinning) loader.stop() - const storedConfig = await setupConfig() + const storedConfig = await setupConfig(opts.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 loadEntropyTui({ account: storedConfig.selectedAccount, @@ -147,26 +146,26 @@ async function main (entropy: Entropy, choices: string[], opts: EntropyTuiOption switch (answers.choice) { case 'Manage Accounts': { - const response = await entropyAccount(opts.endpoint, storedConfig) + const response = await entropyAccount(opts, storedConfig) if (response === 'exit') { returnToMain = true } break } case 'Register': { - await entropyRegister(entropy, opts.endpoint, storedConfig) + await entropyRegister(entropy, opts, storedConfig) break } case 'Balance': { - await entropyBalance(entropy, opts.endpoint, storedConfig) + await entropyBalance(entropy, opts, storedConfig) .catch(err => console.error('There was an error retrieving balance', err)) break } case 'Transfer': { - await entropyTransfer(entropy, opts.endpoint) + await entropyTransfer(entropy, opts) .catch(err => console.error('There was an error sending the transfer', err)) break } case 'Sign': { - await entropySign(entropy, opts.endpoint) + await entropySign(entropy, opts) .catch(err => console.error('There was an issue with signing', err)) break } @@ -179,12 +178,12 @@ async function main (entropy: Entropy, choices: string[], opts: EntropyTuiOption break } case 'User Programs': { - await entropyProgram(entropy, opts.endpoint) + await entropyProgram(entropy, opts) .catch(err => console.error('There was an error with programs', err)) break } case 'Deploy Program': { - await entropyProgramDev(entropy, opts.endpoint) + await entropyProgramDev(entropy, opts) .catch(err => console.error('There was an error with program dev', err)) break } diff --git a/src/types/index.ts b/src/types/index.ts index fe8d6d4..ea62a05 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -3,8 +3,6 @@ export interface EntropyTuiOptions { config: string endpoint: string dev: boolean - version: string - coreVersion: string } type EntropyLoggerLogLevel = 'error' | 'warn' | 'info' | 'debug' diff --git a/tests/common.test.ts b/tests/common.test.ts index 8ec0531..61f4a90 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('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() +}) diff --git a/tests/config.test.ts b/tests/config.test.ts index a352e22..cee0796 100644 --- a/tests/config.test.ts +++ b/tests/config.test.ts @@ -95,7 +95,7 @@ test('config - set', async t => { { const message = 'set does not allow empty config' // @ts-expect-error : wrong types - await set(undefined, configPath) + await set(configPath) .then(() => t.fail(message)) .catch(err => { t.match(err.message, /config must be an object/, message) @@ -112,7 +112,7 @@ test('config - set', async t => { 'migration-version': 1200 } // @ts-expect-error : wrong types - await set(config, configPath) + await set(configPath, config) const actual = await get(configPath) t.deepEqual(config, actual, 'set works') @@ -138,7 +138,7 @@ test('config - init', async t => { ...config, manualAddition: 'boop' } - await set(newConfig, configPath) + await set(configPath, newConfig) await init(configPath, fakeOldConfigPath) config = await get(configPath) t.deepEqual(config, newConfig, 'init does not over-write manual changes') @@ -162,7 +162,7 @@ test('config - init (migration)', async t => { ...config, manualAddition: 'boop' } - await set(newConfig, oldConfigPath) + await set(oldConfigPath, newConfig) // init with new path diff --git a/tests/e2e.cli.sh b/tests/e2e.cli.sh index 119f1ad..30bcab5 100755 --- a/tests/e2e.cli.sh +++ b/tests/e2e.cli.sh @@ -6,10 +6,23 @@ # - internet connection # - jq - see https://jqlang.github.io/jq # -# Run -# $ yarn build && ./tests/e2e.cli.sh +# Build + install the CLI: +# ``` +# yarn build +# npm install -g +# ``` +# +# Run the tests: +# ``` +# yarn test:network:up +# ./tests/e2e.cli.sh +# yarn test:network:down +# ``` +# -rm ~/.config/entropy-cryptography/entropy-cli.json +CURRENT_DATE=$(date +%s%N) +export ENTROPY_CONFIG="/tmp/entropy-cli-${CURRENT_DATE}.e2e.json" +export ENTROPY_ENDPOINT=dev print () { COLOR='\033[0;35m' @@ -18,6 +31,9 @@ print () { echo -e "${COLOR}> $1${RESET}" } +print "Entropy Config:" +print $ENTROPY_CONFIG + print "// ACCOUNT /////////////////////////////////////////////////" print "account ls" @@ -27,7 +43,7 @@ print "account create" entropy account create naynay | jq print "account import" -entropy account import faucet 0x358f394d157e31be23313a1500f5e2c8871e514e530a35aa5c05334be7a39ba6 | jq +entropy account import faucet 0x66256c4e2f90e273bf387923a9a7860f2e9f47a1848d6263de512f7fb110fc08 | jq print "account list" entropy account list | jq @@ -40,7 +56,7 @@ print "balance (name)" entropy balance naynay print "balance (address)" -entropy balance 5CqJyjALDFz4sKjQgK8NXBQGHCWAiV63xXn2Dye393Y6Vghz +entropy balance 5Ck5SLSHYac6WFt5UZRSsdJjwmpSZq85fd5TRNAdZQVzEAPT @@ -74,7 +90,7 @@ print "// SIGN ////////////////////////////////////////////////////" print "entropy sign" -entropy sign -a naynay "some content!\nNICE&SIMPLE" +entropy sign -a naynay "some content!\nNICE&SIMPLE" | jq