Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mixmix/custom config #264

Draft
wants to merge 8 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 13 additions & 10 deletions src/account/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, cliWrite, loadEntropy } from "../common/utils-cli";
import { accountOption, configOption, endpointOption, cliWrite, loadEntropy } from "../common/utils-cli";

export function entropyAccountCommand () {
return new Command('account')
Expand All @@ -23,17 +23,18 @@ function entropyAccountCreate () {
.alias('new')
.description('Create a new entropy account from scratch. Output is JSON of form {name, address}')
.argument('<name>', 'A user friendly name for your new account.')
.addOption(configOption())
.addOption(
new Option(
'--path',
'Derivation path'
).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({
name: newAccount.name,
Expand All @@ -49,17 +50,18 @@ function entropyAccountImport () {
.description('Import an existing entropy account from seed. Output is JSON of form {name, address}')
.argument('<name>', 'A user friendly name for your new account.')
.argument('<seed>', 'The seed for the account you are importing')
.addOption(configOption())
.addOption(
new Option(
'--path',
'Derivation path'
).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,
Expand All @@ -74,9 +76,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 []
Expand All @@ -94,6 +96,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(
Expand All @@ -110,11 +113,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)
Expand Down
4 changes: 2 additions & 2 deletions src/account/interaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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_DEFAULT, newAccount)
return
}

Expand Down Expand Up @@ -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(verifyingKey, account.address)
await addVerifyingKeyToAccountAndSelect(config.CONFIG_PATH_DEFAULT, verifyingKey, account.address)

print("Your address", account.address, "has been successfully registered.")
}
34 changes: 19 additions & 15 deletions src/account/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -18,25 +18,29 @@ export async function selectAndPersistNewAccount (newAccount: EntropyAccountConf

// persist to config, set selectedAccount
accounts.push(newAccount)
await config.set({
...storedConfig,
selectedAccount: newAccount.name
})
await config.set(
{
...storedConfig,
selectedAccount: newAccount.address
},
configPath
)
}

export async function addVerifyingKeyToAccountAndSelect (verifyingKey: string, accountNameOrAddress: string) {
const storedConfig = await config.get()
const { accounts } = storedConfig

const account = findAccountByAddressOrName(accounts, accountNameOrAddress)
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,
setSelectedAccount: account.name
})
await config.set(
{
...storedConfig,
setSelectedAccount: account.name
},
configPath
)
}

function validateSeedInput (seed) {
Expand Down
5 changes: 3 additions & 2 deletions src/balance/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Command } from "commander";
import Entropy from "@entropyxyz/sdk";

import { EntropyBalance } from "./main";
import { endpointOption, cliWrite, loadEntropy } from "../common/utils-cli";
import { configOption, endpointOption, loadEntropy, cliWrite } from "../common/utils-cli";
import { findAccountByAddressOrName } from "../common/utils";
import * as config from "../config";

Expand All @@ -11,9 +11,10 @@ export function entropyBalanceCommand () {
balanceCommand
.description('Command to retrieive the balance of an account on the Entropy Network')
.argument('account <address|name>', 'Account address whose balance you want to query')
.addOption(configOption())
.addOption(endpointOption())
.action(async (account, opts) => {
const entropy: Entropy = await loadEntropy(account, opts.endpoint)
const entropy: Entropy = await loadEntropy({ account, ...opts })
const BalanceService = new EntropyBalance(entropy, opts.endpoint)

const { accounts } = await config.get()
Expand Down
27 changes: 16 additions & 11 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -19,11 +18,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()
Expand All @@ -36,16 +41,16 @@ program
.addCommand(entropyProgramCommand())

.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)
// NOTE:
// 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 () => {
.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()
4 changes: 4 additions & 0 deletions src/common/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@


export const ENTROPY_ENDPOINT_DEFAULT = 'ws://testnet.entropy.xyz:9944/'
// TODO: update this to be wss?
104 changes: 59 additions & 45 deletions src/common/utils-cli.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
import Entropy from '@entropyxyz/sdk'
import { Option } from 'commander'
import { findAccountByAddressOrName, stringify } from './utils'
import * as config from '../config'

import { absolutePath, findAccountByAddressOrName, stringify } from './utils'
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)
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
Expand All @@ -27,25 +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()
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 () {
const storedConfig = getConfigOrNull()

return new Option(
'-a, --account <name|address>',
[
Expand All @@ -54,37 +42,63 @@ 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 async function loadEntropy (addressOrName: string, endpoint: string): Promise<Entropy> {
const accounts = getConfigOrNull()?.accounts || []
const selectedAccount = findAccountByAddressOrName(accounts, addressOrName)
if (!selectedAccount) throw new Error(`No account with name or address: "${addressOrName}"`)
export function configOption () {
return new Option(
'-c, --config <path>',
'Set the path to your Entropy config file (JSON).',
)
.env('ENTROPY_CONFIG')
.argParser(configPath => absolutePath(configPath))
.default(config.CONFIG_PATH_DEFAULT)
}

export async function loadEntropy (options: {
account: string,
config: string,
endpoint: string,
}): Promise<Entropy> {
const storedConfig = getConfigOrNull(options.config)

const entropy = await initializeEntropy({ keyMaterial: selectedAccount.data, endpoint })
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
})
}

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
}

Loading
Loading