From 4e0532a5f65149676371e67fb43a58c903887936 Mon Sep 17 00:00:00 2001 From: Nayyir Jutha Date: Mon, 5 Aug 2024 13:23:15 -0400 Subject: [PATCH 01/28] [NayNay] File Restructure: Accounts Restructure - refactoring file structure + flow of accounts in cli/tui --- src/accounts/command.ts | 0 src/accounts/constants.ts | 12 +++++ src/accounts/types.ts | 1 + src/accounts/utils.ts | 47 +++++++++++++++++++ .../manage-accounts/helpers/create-account.ts | 25 ---------- .../manage-accounts/helpers/import-account.ts | 40 ---------------- 6 files changed, 60 insertions(+), 65 deletions(-) create mode 100644 src/accounts/command.ts create mode 100644 src/accounts/constants.ts create mode 100644 src/accounts/types.ts create mode 100644 src/accounts/utils.ts delete mode 100644 src/flows/manage-accounts/helpers/create-account.ts delete mode 100644 src/flows/manage-accounts/helpers/import-account.ts diff --git a/src/accounts/command.ts b/src/accounts/command.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/accounts/constants.ts b/src/accounts/constants.ts new file mode 100644 index 00000000..379a9d25 --- /dev/null +++ b/src/accounts/constants.ts @@ -0,0 +1,12 @@ +export const IMPORT_CONTENT = { + seed: { + name: 'seed', + message: 'Enter seed:', + invalidSeed: 'Seed provided is not valid' + }, + path: { + name: 'path', + message: 'derivation path:', + default: 'none', + } +} \ No newline at end of file diff --git a/src/accounts/types.ts b/src/accounts/types.ts new file mode 100644 index 00000000..c5552c04 --- /dev/null +++ b/src/accounts/types.ts @@ -0,0 +1 @@ +export interface CreateAccountParams { name: string, seed: string, path?: string } \ No newline at end of file diff --git a/src/accounts/utils.ts b/src/accounts/utils.ts new file mode 100644 index 00000000..31c53280 --- /dev/null +++ b/src/accounts/utils.ts @@ -0,0 +1,47 @@ +// @ts-expect-error +import Keyring from '@entropyxyz/sdk/keys' +import { EntropyAccountConfig } from "src/config/types"; +import { CreateAccountParams } from './types'; +import { IMPORT_CONTENT } from './constants'; + +const validateSeedInput = (seed) => { + if (seed.includes('#debug')) return true + if (seed.length === 66 && seed.startsWith('0x')) return true + if (seed.length === 64) return true + return IMPORT_CONTENT.seed.invalidSeed +} + +export const importQuestions = [ + { + type: 'input', + name: IMPORT_CONTENT.seed.name, + message: IMPORT_CONTENT.seed.message, + validate: validateSeedInput, + when: ({ importKey }) => importKey + }, + { + type: 'input', + name: IMPORT_CONTENT.path.name, + message: IMPORT_CONTENT.path.message, + default: IMPORT_CONTENT.path.default, + when: ({ importKey }) => importKey + }, +] + +export async function createAccount ({ name, seed, path }: CreateAccountParams): Promise { + const keyring = new Keyring({ seed, path, debug: true }) + const fullAccount = keyring.getAccount() + // TO-DO: sdk should create account on constructor + const { admin } = keyring.getAccount() + + const data = fullAccount + delete admin.pair + // const encryptedData = password ? passwordFlow.encrypt(data, password) : data + + return { + name: name, + address: admin.address, + // TODO: replace with data: encryptedData once pasword input is added back + data, + } +} \ No newline at end of file diff --git a/src/flows/manage-accounts/helpers/create-account.ts b/src/flows/manage-accounts/helpers/create-account.ts deleted file mode 100644 index 0e9a66fa..00000000 --- a/src/flows/manage-accounts/helpers/create-account.ts +++ /dev/null @@ -1,25 +0,0 @@ -// @ts-ignore -import Keyring from '@entropyxyz/sdk/keys' -import { EntropyLogger } from 'src/common/logger'; -import { EntropyAccountConfig } from "src/config/types"; - -export async function createAccount ({ name, seed, path }: { name: string, seed: string, path?: string }, logger?: EntropyLogger): Promise { - const FLOW_CONTEXT = 'MANAGE_ACCOUNTS::CREATE_ACCOUNT' - const keyring = new Keyring({ seed, path, debug: true }) - const fullAccount = keyring.getAccount() - // TO-DO: sdk should create account on constructor - const { admin } = keyring.getAccount() - logger?.debug('fullAccount:', FLOW_CONTEXT) - logger?.debug(fullAccount, FLOW_CONTEXT) - - const data = fullAccount - delete admin.pair - // const encryptedData = password ? passwordFlow.encrypt(data, password) : data - - return { - name: name, - address: admin.address, - // TODO: replace with data: encryptedData once pasword input is added back - data, - } -} \ No newline at end of file diff --git a/src/flows/manage-accounts/helpers/import-account.ts b/src/flows/manage-accounts/helpers/import-account.ts deleted file mode 100644 index 4000c60c..00000000 --- a/src/flows/manage-accounts/helpers/import-account.ts +++ /dev/null @@ -1,40 +0,0 @@ -// import { mnemonicValidate, mnemonicToMiniSecret } from '@polkadot/util-crypto' - -export const importQuestions = [ - // { - // type: 'list', - // name: 'secretType', - // message: 'select secret type:', - // choices: ['seed'], - // when: ({ importKey }) => importKey - // }, - { - type: 'input', - name: 'secret', - // message: ({ secretType }) => `${secretType}:`, - message: 'Enter seed:', - validate: (secret) => { - // validate: (secret, { secretType }) => { - // if (secretType === 'mnemonic') return mnemonicValidate(secret) ? true : 'not a valid mnemonic' - if (secret.includes('#debug')) return true - if (secret.length === 66 && secret.startsWith('0x')) return true - if (secret.length === 64) return true - return 'not a valid seed' - }, - filter: (secret) => { - // filter: (secret, { secretType }) => { - // if (secretType === 'mnemonic') { - // return mnemonicToMiniSecret(secret) - // } - return secret - }, - when: ({ importKey }) => importKey - }, - { - type: 'input', - name: 'path', - message: 'derivation path:', - default: 'none', - when: ({ importKey }) => importKey - }, -] From e9411ce41f6eac927dff6bf56419294193c504e5 Mon Sep 17 00:00:00 2001 From: Nayyir Jutha Date: Tue, 6 Aug 2024 01:15:29 -0400 Subject: [PATCH 02/28] updated new account, list account and selected account with new file structure; still need to update tests --- src/accounts/command.ts | 110 ++++++++++++++++++++ src/accounts/constants.ts | 26 ++++- src/accounts/types.ts | 8 +- src/accounts/utils.ts | 58 +++++++++-- src/cli.ts | 54 ++++++++-- src/common/utils.ts | 16 ++- src/flows/entropyFaucet/index.ts | 4 +- src/flows/index.ts | 1 - src/flows/manage-accounts/cli.ts | 8 -- src/flows/manage-accounts/index.ts | 39 ------- src/flows/manage-accounts/list.ts | 14 --- src/flows/manage-accounts/new-account.ts | 72 ------------- src/flows/manage-accounts/select-account.ts | 15 --- src/flows/register/index.ts | 4 +- src/tui.ts | 12 ++- 15 files changed, 266 insertions(+), 175 deletions(-) delete mode 100644 src/flows/manage-accounts/cli.ts delete mode 100644 src/flows/manage-accounts/index.ts delete mode 100644 src/flows/manage-accounts/list.ts delete mode 100644 src/flows/manage-accounts/new-account.ts delete mode 100644 src/flows/manage-accounts/select-account.ts diff --git a/src/accounts/command.ts b/src/accounts/command.ts index e69de29b..fe2c632f 100644 --- a/src/accounts/command.ts +++ b/src/accounts/command.ts @@ -0,0 +1,110 @@ +import inquirer from "inquirer"; +import Entropy from "@entropyxyz/sdk"; +import { randomAsHex } from '@polkadot/util-crypto' +import { BaseCommand } from "../common/base-command"; +import { print } from "../common/utils"; +import { EntropyAccountConfig } from "../config/types"; +import * as config from '../config' +import { FLOW_CONTEXT } from "./constants"; +import { + createAccount, + formatAccountsList, + manageAccountsQuestions, + newAccountQuestions, + selectAccountQuestions +} from "./utils"; +import { CreateAccountParams } from "./types"; + +export class AccountsCommand extends BaseCommand { + constructor (entropy: Entropy, endpoint: string) { + super(entropy, endpoint, FLOW_CONTEXT) + } + + public async newAccount (params?: CreateAccountParams): Promise { + let { seed, name, path } = params + let importKey: boolean + + if (!seed && !name && !path) { + ({ seed, name, path, importKey } = await inquirer.prompt(newAccountQuestions)) + } + + if (importKey && seed.includes('#debug')) { + seed = seed.split('#debug')[0] + } + + return createAccount({ name, seed, path }) + } + + public async updateConfig (newAccount: EntropyAccountConfig): Promise { + const storedConfig = await config.get() + const { accounts } = storedConfig + accounts.push(newAccount) + await config.set({ + ...storedConfig, + accounts, + selectedAccount: newAccount.address + }) + } + + public async selectAccount (accounts: EntropyAccountConfig[]) { + const answers = await inquirer.prompt([selectAccountQuestions(accounts)]) + + return { selectedAccount: answers.selectedAccount.address } + } + + public listAccounts (accounts) { + const accountsArray = Array.isArray(accounts) && accounts.length ? accounts : [] + if (!accountsArray.length) + throw new Error( + 'There are currently no accounts available, please create or import your new account using the Manage Accounts feature' + ) + return formatAccountsList(accountsArray) + } + + public async getUserInput (): Promise { + const answers = await inquirer.prompt(newAccountQuestions) + const { secret, name, path, importKey } = answers + let seed: string + + // never create debug keys only ever import them + if (importKey && secret.includes('#debug')) { + // isDebugMode = true + seed = secret.split('#debug')[0] + } else { + seed = importKey ? secret : randomAsHex(32) + } + + return { seed, name, path } + } + + public async runInteraction (config): Promise { + const { accounts } = config + const { interactionChoice } = await inquirer.prompt(manageAccountsQuestions) + + switch (interactionChoice) { + case 'create-account': { + const createAccountParams = await this.getUserInput() + const newAccount = await this.newAccount(createAccountParams) + print('New Account:') + print({ name: newAccount.name, address: newAccount.address }) + accounts.push(newAccount) + return { accounts, selectedAccount: newAccount.address } + } + case 'select-account': { + const response = await this.selectAccount(config.accounts) + print('Current selected account is ' + response.selectedAccount) + return response + } + case 'list-account': { + const list = this.listAccounts(accounts) + list?.forEach(account => print(account)) + return + } + case 'exit': { + return 'exit' + } + default: + throw new Error('AccountsError: Unknown interaction action') + } + } +} \ No newline at end of file diff --git a/src/accounts/constants.ts b/src/accounts/constants.ts index 379a9d25..b8c407a0 100644 --- a/src/accounts/constants.ts +++ b/src/accounts/constants.ts @@ -1,4 +1,6 @@ -export const IMPORT_CONTENT = { +export const FLOW_CONTEXT = 'ENTROPY_ACCOUNTS' + +export const ACCOUNTS_CONTENT = { seed: { name: 'seed', message: 'Enter seed:', @@ -8,5 +10,27 @@ export const IMPORT_CONTENT = { name: 'path', message: 'derivation path:', default: 'none', + }, + importKey: { + name: 'importKey', + message: 'Would you like to import your own seed?', + default: false + }, + name: { + name: 'name', + default: 'My Key', + }, + selectAccount: { + name: "selectedAccount", + message: "Choose account:", + }, + interactionChoice: { + name: 'choice', + choices: [ + { name: 'Create/Import Account', value: 'create-import' }, + { name: 'Select Account', value: 'select-account' }, + { name: 'List Accounts', value: 'list-account' }, + { name: 'Exit to Main Menu', value: 'exit' } + ] } } \ No newline at end of file diff --git a/src/accounts/types.ts b/src/accounts/types.ts index c5552c04..c3b92350 100644 --- a/src/accounts/types.ts +++ b/src/accounts/types.ts @@ -1 +1,7 @@ -export interface CreateAccountParams { name: string, seed: string, path?: string } \ No newline at end of file +export interface CreateAccountParams { name: string, seed: string, path?: string } + +export type ListedAccount = { + name: string + address: string + verifyingKeys: string[] +} \ No newline at end of file diff --git a/src/accounts/utils.ts b/src/accounts/utils.ts index 31c53280..c9cbfa84 100644 --- a/src/accounts/utils.ts +++ b/src/accounts/utils.ts @@ -1,33 +1,65 @@ // @ts-expect-error import Keyring from '@entropyxyz/sdk/keys' -import { EntropyAccountConfig } from "src/config/types"; -import { CreateAccountParams } from './types'; -import { IMPORT_CONTENT } from './constants'; +import { EntropyAccountConfig } from "../config/types"; +import { CreateAccountParams, ListedAccount } from './types'; +import { ACCOUNTS_CONTENT } from './constants'; +import { generateAccountChoices } from 'src/common/utils'; const validateSeedInput = (seed) => { if (seed.includes('#debug')) return true if (seed.length === 66 && seed.startsWith('0x')) return true if (seed.length === 64) return true - return IMPORT_CONTENT.seed.invalidSeed + return ACCOUNTS_CONTENT.seed.invalidSeed } export const importQuestions = [ { type: 'input', - name: IMPORT_CONTENT.seed.name, - message: IMPORT_CONTENT.seed.message, + name: ACCOUNTS_CONTENT.seed.name, + message: ACCOUNTS_CONTENT.seed.message, validate: validateSeedInput, when: ({ importKey }) => importKey }, { type: 'input', - name: IMPORT_CONTENT.path.name, - message: IMPORT_CONTENT.path.message, - default: IMPORT_CONTENT.path.default, + name: ACCOUNTS_CONTENT.path.name, + message: ACCOUNTS_CONTENT.path.message, + default: ACCOUNTS_CONTENT.path.default, when: ({ importKey }) => importKey }, ] +export const newAccountQuestions = [ + { + type: 'confirm', + name: ACCOUNTS_CONTENT.importKey.name, + message: ACCOUNTS_CONTENT.importKey.message, + default: ACCOUNTS_CONTENT.importKey.default, + }, + ...importQuestions, + { + type: 'input', + name: ACCOUNTS_CONTENT.name.name, + default: ACCOUNTS_CONTENT.name.default, + }, +] + +export const selectAccountQuestions = (accounts: EntropyAccountConfig[]) => [{ + type: 'list', + name: ACCOUNTS_CONTENT.selectAccount.name, + message: ACCOUNTS_CONTENT.selectAccount.message, + choices: generateAccountChoices(accounts) +}] + +export const manageAccountsQuestions = [ + { + type: 'list', + name: ACCOUNTS_CONTENT.interactionChoice.name, + pageSize: ACCOUNTS_CONTENT.interactionChoice.choices.length, + choices: ACCOUNTS_CONTENT.interactionChoice.choices + } +] + export async function createAccount ({ name, seed, path }: CreateAccountParams): Promise { const keyring = new Keyring({ seed, path, debug: true }) const fullAccount = keyring.getAccount() @@ -44,4 +76,12 @@ export async function createAccount ({ name, seed, path }: CreateAccountParams): // TODO: replace with data: encryptedData once pasword input is added back data, } +} + +export function formatAccountsList (accounts: EntropyAccountConfig[]): ListedAccount[] { + return accounts.map((account: EntropyAccountConfig) => ({ + name: account.name, + address: account.address, + verifyingKeys: account?.data?.admin?.verifyingKeys + })) } \ No newline at end of file diff --git a/src/cli.ts b/src/cli.ts index 5af9da0a..1ba0c5ee 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -2,21 +2,22 @@ /* NOTE: calling this file entropy.ts helps commander parse process.argv */ import { Command, Option } from 'commander' +import { randomAsHex } from '@polkadot/util-crypto' import launchTui from './tui' import * as config from './config' import { EntropyTuiOptions } from './types' -import { cliListAccounts } from './flows/manage-accounts/cli' import { cliEntropyTransfer } from './flows/entropyTransfer/cli' import { cliSign } from './flows/sign/cli' import { getSelectedAccount, stringify } from './common/utils' import Entropy from '@entropyxyz/sdk' import { initializeEntropy } from './common/initializeEntropy' import { BalanceCommand } from './balance/command' +import { AccountsCommand } from './accounts/command' const program = new Command() // Array of restructured commands to make it easier to migrate them to the new "flow" -const RESTRUCTURED_COMMANDS = ['balance'] +const RESTRUCTURED_COMMANDS = ['balance', 'new-account'] function endpointOption (){ return new Option( @@ -71,7 +72,7 @@ function currentAccountAddressOption () { let entropy: Entropy async function loadEntropy (address: string, endpoint: string, password: string) { - const storedConfig = config.getSync() + const storedConfig = await config.get() const selectedAccount = getSelectedAccount(storedConfig.accounts, address) if (!selectedAccount) throw Error(`No account with address ${address}`) @@ -117,14 +118,55 @@ program /* list */ program.command('list') .alias('ls') - .description('List all accounts. Output is JSON of form [{ name, address, data }]') - .action(async () => { + .description('List all accounts. Output is JSON of form [{ name, address, verifyingKeys }]') + .addOption(endpointOption()) + .action(async (options) => { // 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 cliListAccounts() + const storedConfig = await config.get() + const accountsCommand = new AccountsCommand(entropy, options.endpoint) + const accounts = accountsCommand.listAccounts(storedConfig.accounts) writeOut(accounts) process.exit(0) }) +/* new account */ +program.command('new-account') + .alias('new') + .description('Create new entropy account from imported seed or from scratch. Output is JSON of form [{name, address}]') + .addOption(endpointOption()) + .addOption(passwordOption()) + .addOption( + new Option( + '-s, --seed', + 'Seed used to create entropy account' + ) + .makeOptionMandatory(true) + .default(randomAsHex(32)) + ) + .addOption( + new Option( + '-n, --name', + 'Name of entropy account' + ) + .makeOptionMandatory(true) + ) + .addOption( + new Option( + '-p, --path', + 'Derivation path' + ) + .makeOptionMandatory(true) + ) + .action(async (opts) => { + const { seed, name, path, endpoint } = opts + const accountsCommand = new AccountsCommand(entropy, endpoint) + + const newAccount = await accountsCommand.newAccount({ seed, name, path }) + await accountsCommand.updateConfig(newAccount) + writeOut(newAccount) + process.exit(0) + }) + /* balance */ program.command('balance') .description('Get the balance of an Entropy account. Output is a number') diff --git a/src/common/utils.ts b/src/common/utils.ts index ace2af19..81053888 100644 --- a/src/common/utils.ts +++ b/src/common/utils.ts @@ -1,5 +1,6 @@ +import * as config from '../config' import { Buffer } from 'buffer' -import { EntropyAccountConfig } from "../config/types" +import { EntropyAccountConfig, EntropyConfig } from "../config/types" export function stripHexPrefix (str: string): string { if (str.startsWith('0x')) return str.slice(2) @@ -52,7 +53,7 @@ export function buf2hex (buffer: ArrayBuffer): string { return Buffer.from(buffer).toString("hex") } -export function accountChoices (accounts: EntropyAccountConfig[]) { +export function generateAccountChoices (accounts: EntropyAccountConfig[]) { return accounts .map((account) => ({ name: `${account.name} (${account.address})`, @@ -61,10 +62,19 @@ export function accountChoices (accounts: EntropyAccountConfig[]) { } export function accountChoicesWithOther (accounts: EntropyAccountConfig[]) { - return accountChoices(accounts) + return generateAccountChoices(accounts) .concat([{ name: "Other", value: null }]) } export function getSelectedAccount (accounts: EntropyAccountConfig[], address: string) { return accounts.find(account => account.address === address) } + +export async function updateConfig (storedConfig: EntropyConfig, newUpdates: any) { + if (typeof newUpdates === 'string' && newUpdates === 'exit') { + return true + } else if (newUpdates) { + await config.set({ ...storedConfig, ...newUpdates }) + } + return false +} \ No newline at end of file diff --git a/src/flows/entropyFaucet/index.ts b/src/flows/entropyFaucet/index.ts index 3a38942c..f48ed557 100644 --- a/src/flows/entropyFaucet/index.ts +++ b/src/flows/entropyFaucet/index.ts @@ -1,5 +1,5 @@ import inquirer from "inquirer" -import { print, accountChoices } from "../../common/utils" +import { print, generateAccountChoices } from "../../common/utils" import { initializeEntropy } from "../../common/initializeEntropy" export async function entropyFaucet ({ accounts }, options) { @@ -9,7 +9,7 @@ export async function entropyFaucet ({ accounts }, options) { type: "list", name: "selectedAccount", message: "Choose account:", - choices: accountChoices(accounts), + choices: generateAccountChoices(accounts), } const answers = await inquirer.prompt([accountQuestion]) diff --git a/src/flows/index.ts b/src/flows/index.ts index 1aac99a8..c024ac3c 100644 --- a/src/flows/index.ts +++ b/src/flows/index.ts @@ -3,4 +3,3 @@ export { entropyRegister } from './register' export { userPrograms, devPrograms } from './programs' export { sign } from './sign' export { entropyTransfer } from './entropyTransfer' -export { manageAccounts } from './manage-accounts' diff --git a/src/flows/manage-accounts/cli.ts b/src/flows/manage-accounts/cli.ts deleted file mode 100644 index 390f6fe1..00000000 --- a/src/flows/manage-accounts/cli.ts +++ /dev/null @@ -1,8 +0,0 @@ -import * as config from '../../config' -import { listAccounts } from './list' - -export async function cliListAccounts () { - const storedConfig = await config.get() - - return listAccounts(storedConfig) -} diff --git a/src/flows/manage-accounts/index.ts b/src/flows/manage-accounts/index.ts deleted file mode 100644 index 300ceb1b..00000000 --- a/src/flows/manage-accounts/index.ts +++ /dev/null @@ -1,39 +0,0 @@ -import inquirer from 'inquirer' -import { print } from '../../common/utils' -import { newAccount } from './new-account' -import { selectAccount } from './select-account' -import { listAccounts } from './list' -import { EntropyTuiOptions } from 'src/types' -import { EntropyLogger } from 'src/common/logger' - -const actions = { - 'Create/Import Account': newAccount, - 'Select Account': selectAccount, - 'List Accounts': (config) => { - try { - const accountsArray = listAccounts(config) - accountsArray?.forEach(account => print(account)) - return - } catch (error) { - console.error(error.message); - } - }, -} - -const choices = Object.keys(actions) - -const questions = [{ - type: 'list', - name: 'choice', - pageSize: choices.length, - choices, -}] - -export async function manageAccounts (config, _options: EntropyTuiOptions, logger: EntropyLogger) { - const FLOW_CONTEXT = 'MANAGE_ACCOUNTS' - const { choice } = await inquirer.prompt(questions) - const responses = await actions[choice](config, logger) || {} - logger.debug('returned config update', FLOW_CONTEXT) - logger.debug({ accounts: responses.accounts ? responses.accounts : config.accounts, selectedAccount: responses.selectedAccount || config.selectedAccount }, FLOW_CONTEXT) - return { accounts: responses.accounts ? responses.accounts : config.accounts, selectedAccount: responses.selectedAccount || config.selectedAccount } -} diff --git a/src/flows/manage-accounts/list.ts b/src/flows/manage-accounts/list.ts deleted file mode 100644 index 74b43db8..00000000 --- a/src/flows/manage-accounts/list.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { EntropyAccountConfig } from "src/config/types" - -export function listAccounts (config) { - const accountsArray = Array.isArray(config.accounts) ? config.accounts : [config.accounts] - if (!accountsArray.length) - throw new Error( - 'There are currently no accounts available, please create or import your new account using the Manage Accounts feature' - ) - return accountsArray.map((account: EntropyAccountConfig) => ({ - name: account.name, - address: account.address, - verifyingKeys: account?.data?.admin?.verifyingKeys - })) -} \ No newline at end of file diff --git a/src/flows/manage-accounts/new-account.ts b/src/flows/manage-accounts/new-account.ts deleted file mode 100644 index 2c2c6268..00000000 --- a/src/flows/manage-accounts/new-account.ts +++ /dev/null @@ -1,72 +0,0 @@ -import inquirer from 'inquirer' -import { randomAsHex } from '@polkadot/util-crypto' -import { importQuestions } from './helpers/import-account' -// import * as passwordFlow from '../password' -import { print } from '../../common/utils' -import { createAccount } from './helpers/create-account' -import { EntropyLogger } from 'src/common/logger' - -export async function newAccount ({ accounts }, logger: EntropyLogger) { - accounts = Array.isArray(accounts) ? accounts : [] - - const questions = [ - { - type: 'confirm', - name: 'importKey', - message: 'Would you like to import a key?', - default: false, - }, - ...importQuestions, - { - type: 'input', - name: 'name', - default: 'My Key' - }, - // { - // type: 'confirm', - // name: 'newPassword', - // message: 'Would you like to password protect this key?', - // default: true, - // } - ] - - const answers = await inquirer.prompt(questions) - - // if (answers.newPassword) { - // const passwordAnswer = await inquirer.prompt([ - // { - // type: 'password', - // name: 'password', - // mask: '*', - // message: 'Enter a password for the key:', - // } - // ]) - // answers = { ...answers, ...passwordAnswer } - // } - // The below conditional resolves as true, but the passwordFlow questions never get asked - // most likely due to the when field criteria not being satified on the individual questions - // if (passwordFlow.questions.length > 0) { - // const passwordFlowAnswers = await inquirer.prompt(passwordFlow.questions) - // answers = { ...answers, ...passwordFlowAnswers } - // } - - // const { secret, name, path, password, importKey } = answers - const { secret, name, path, importKey } = answers - // let isDebugMode = false - let seed - // never create debug keys only ever import them - if (importKey && secret.includes('#debug')) { - // isDebugMode = true - seed = secret.split('#debug')[0] - } else { - seed = importKey ? secret : randomAsHex(32) - } - - const newAccount = await createAccount({ name, seed, path }, logger) - - print('New account:') - print({ name: newAccount.name, address: newAccount.address }) - - accounts.push(newAccount) - return { accounts, selectedAccount: newAccount.address } -} diff --git a/src/flows/manage-accounts/select-account.ts b/src/flows/manage-accounts/select-account.ts deleted file mode 100644 index c0746c15..00000000 --- a/src/flows/manage-accounts/select-account.ts +++ /dev/null @@ -1,15 +0,0 @@ -import inquirer from "inquirer"; -import { accountChoices } from "../../common/utils"; - -export async function selectAccount ({ accounts }) { - const accountQuestion = { - type: "list", - name: "selectedAccount", - message: "Choose account:", - choices: accountChoices(accounts) - } - - const answers = await inquirer.prompt([accountQuestion]) - - return { selectedAccount: answers.selectedAccount.address } -} \ No newline at end of file diff --git a/src/flows/register/index.ts b/src/flows/register/index.ts index 05247578..850cedbb 100644 --- a/src/flows/register/index.ts +++ b/src/flows/register/index.ts @@ -1,5 +1,5 @@ // import inquirer from "inquirer" -import { getSelectedAccount, print, /*accountChoices*/ } from "../../common/utils" +import { getSelectedAccount, print } from "../../common/utils" import { initializeEntropy } from "../../common/initializeEntropy" import { EntropyLogger } from "src/common/logger"; import { register } from "./register"; @@ -14,7 +14,7 @@ export async function entropyRegister (storedConfig, options, logger: EntropyLog const entropy = await initializeEntropy({ keyMaterial: selectedAccount.data, endpoint }) // TO-DO: investigate this a little more - // const filteredAccountChoices = accountChoices(accounts) + // const filteredAccountChoices = generateAccountChoices(accounts) // Not going to ask for a pointer from the user just yet // const { programPointer } = await inquirer.prompt([{ // type: 'input', diff --git a/src/tui.ts b/src/tui.ts index e22f71ac..c9f03b9e 100644 --- a/src/tui.ts +++ b/src/tui.ts @@ -4,9 +4,10 @@ import * as config from './config' import * as flows from './flows' import { EntropyTuiOptions } from './types' import { logo } from './common/ascii' -import { print } from './common/utils' +import { print, updateConfig } from './common/utils' import { EntropyLogger } from './common/logger' import { BalanceCommand } from './balance/command' +import { AccountsCommand } from './accounts/command' let shouldInit = true @@ -18,7 +19,7 @@ export default function tui (entropy: Entropy, options: EntropyTuiOptions) { logger.debug(options) const choices = { - 'Manage Accounts': flows.manageAccounts, + 'Manage Accounts': () => {}, // leaving as a noop function until all flows are restructured 'Balance': () => {}, 'Register': flows.entropyRegister, @@ -83,6 +84,13 @@ async function main (entropy: Entropy, choices, options, logger: EntropyLogger) print(`Address ${storedConfig.selectedAccount} has a balance of: ${balanceString}`) break; } + case 'Manage Accounts': { + const accountsCommand = new AccountsCommand(entropy, options.endpoint) + const response = await accountsCommand.runInteraction(storedConfig) + returnToMain = await updateConfig(storedConfig, response) + storedConfig = await config.get() + break; + } default: { const newConfigUpdates = await choices[answers.choice](storedConfig, options, logger) if (typeof newConfigUpdates === 'string' && newConfigUpdates === 'exit') { From 6d4683193eeb8be8986deae4603c7fd317309a68 Mon Sep 17 00:00:00 2001 From: Nayyir Jutha Date: Tue, 6 Aug 2024 14:34:18 -0400 Subject: [PATCH 03/28] finished updated manage accoutns stuff, updated tests too --- src/accounts/command.ts | 26 ++++++++------------------ src/accounts/utils.ts | 13 +++++++++++-- src/cli.ts | 16 +++++++--------- tests/manage-accounts.test.ts | 9 +++------ 4 files changed, 29 insertions(+), 35 deletions(-) diff --git a/src/accounts/command.ts b/src/accounts/command.ts index fe2c632f..a3b8432d 100644 --- a/src/accounts/command.ts +++ b/src/accounts/command.ts @@ -2,13 +2,12 @@ import inquirer from "inquirer"; import Entropy from "@entropyxyz/sdk"; import { randomAsHex } from '@polkadot/util-crypto' import { BaseCommand } from "../common/base-command"; -import { print } from "../common/utils"; -import { EntropyAccountConfig } from "../config/types"; -import * as config from '../config' +import { print, updateConfig } from "../common/utils"; +import { EntropyAccountConfig, EntropyConfig } from "../config/types"; import { FLOW_CONTEXT } from "./constants"; import { createAccount, - formatAccountsList, + listAccounts, manageAccountsQuestions, newAccountQuestions, selectAccountQuestions @@ -35,15 +34,11 @@ export class AccountsCommand extends BaseCommand { return createAccount({ name, seed, path }) } - public async updateConfig (newAccount: EntropyAccountConfig): Promise { - const storedConfig = await config.get() + public async updateConfig (storedConfig: EntropyConfig, newAccount: EntropyAccountConfig): Promise { const { accounts } = storedConfig accounts.push(newAccount) - await config.set({ - ...storedConfig, - accounts, - selectedAccount: newAccount.address - }) + + return updateConfig(storedConfig, { accounts, selectedAccount: newAccount.address }) } public async selectAccount (accounts: EntropyAccountConfig[]) { @@ -52,13 +47,8 @@ export class AccountsCommand extends BaseCommand { return { selectedAccount: answers.selectedAccount.address } } - public listAccounts (accounts) { - const accountsArray = Array.isArray(accounts) && accounts.length ? accounts : [] - if (!accountsArray.length) - throw new Error( - 'There are currently no accounts available, please create or import your new account using the Manage Accounts feature' - ) - return formatAccountsList(accountsArray) + public listAccounts (accounts: EntropyAccountConfig[]) { + return listAccounts({ accounts }) } public async getUserInput (): Promise { diff --git a/src/accounts/utils.ts b/src/accounts/utils.ts index c9cbfa84..5e4862bb 100644 --- a/src/accounts/utils.ts +++ b/src/accounts/utils.ts @@ -1,6 +1,6 @@ // @ts-expect-error import Keyring from '@entropyxyz/sdk/keys' -import { EntropyAccountConfig } from "../config/types"; +import { EntropyAccountConfig, EntropyConfig } from "../config/types"; import { CreateAccountParams, ListedAccount } from './types'; import { ACCOUNTS_CONTENT } from './constants'; import { generateAccountChoices } from 'src/common/utils'; @@ -78,10 +78,19 @@ export async function createAccount ({ name, seed, path }: CreateAccountParams): } } -export function formatAccountsList (accounts: EntropyAccountConfig[]): ListedAccount[] { +function formatAccountsList (accounts: EntropyAccountConfig[]): ListedAccount[] { return accounts.map((account: EntropyAccountConfig) => ({ name: account.name, address: account.address, verifyingKeys: account?.data?.admin?.verifyingKeys })) +} + +export function listAccounts ({ accounts }: Partial) { + const accountsArray = Array.isArray(accounts) && accounts.length ? accounts : [] + if (!accountsArray.length) + throw new Error( + 'There are currently no accounts available, please create or import your new account using the Manage Accounts feature' + ) + return formatAccountsList(accountsArray) } \ No newline at end of file diff --git a/src/cli.ts b/src/cli.ts index 1ba0c5ee..808ed709 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -14,6 +14,7 @@ import Entropy from '@entropyxyz/sdk' import { initializeEntropy } from './common/initializeEntropy' import { BalanceCommand } from './balance/command' import { AccountsCommand } from './accounts/command' +import { ACCOUNTS_CONTENT } from './accounts/constants' const program = new Command() // Array of restructured commands to make it easier to migrate them to the new "flow" @@ -139,31 +140,28 @@ program.command('new-account') new Option( '-s, --seed', 'Seed used to create entropy account' - ) - .makeOptionMandatory(true) - .default(randomAsHex(32)) + ).default(randomAsHex(32)) ) .addOption( new Option( '-n, --name', 'Name of entropy account' - ) - .makeOptionMandatory(true) + ).makeOptionMandatory(true) ) .addOption( new Option( '-p, --path', 'Derivation path' - ) - .makeOptionMandatory(true) + ).default(ACCOUNTS_CONTENT.path.default) ) .action(async (opts) => { + const storedConfig = await config.get() const { seed, name, path, endpoint } = opts const accountsCommand = new AccountsCommand(entropy, endpoint) const newAccount = await accountsCommand.newAccount({ seed, name, path }) - await accountsCommand.updateConfig(newAccount) - writeOut(newAccount) + await accountsCommand.updateConfig(storedConfig, newAccount) + writeOut({ name: newAccount.name, address: newAccount.address }) process.exit(0) }) diff --git a/tests/manage-accounts.test.ts b/tests/manage-accounts.test.ts index 6b3521c3..479c4cbe 100644 --- a/tests/manage-accounts.test.ts +++ b/tests/manage-accounts.test.ts @@ -6,8 +6,7 @@ import { isValidSubstrateAddress } from '@entropyxyz/sdk/utils' import Keyring from '@entropyxyz/sdk/keys' import { randomAsHex } from '@polkadot/util-crypto' import { EntropyAccountConfig, EntropyConfig } from '../src/config/types' -import { listAccounts } from '../src/flows/manage-accounts/list' -import { createAccount } from '../src/flows/manage-accounts/helpers/create-account' +import { createAccount, listAccounts } from '../src/accounts/utils' import * as config from '../src/config' import { promiseRunner, sleep } from './testing-utils' import { charlieStashAddress, charlieStashSeed } from './testing-utils/constants' @@ -40,7 +39,7 @@ test('List Accounts', async t => { t.deepEqual(accountsArray, [{ name: account.name, address: account.address, - verifyingKeys: account.data.admin.verifyingKeys + verifyingKeys: account?.data?.admin?.verifyingKeys }]) // Resetting accounts on config to test for empty list @@ -55,8 +54,6 @@ test('List Accounts', async t => { t.end() }) -const networkType = 'two-nodes' - let counter = 0 test('Create Account', async t => { const configPath = `/tmp/entropy-cli-${Date.now()}_${counter++}.json` @@ -74,6 +71,6 @@ test('Create Account', async t => { const isValidAddress = isValidSubstrateAddress(newAccount.address) t.ok(isValidAddress, 'Valid address created') - t.equal(newAccount.address, admin.address, 'Generated Account matches Account created by Keyring') + t.equal(newAccount.address, admin?.address, 'Generated Account matches Account created by Keyring') t.end() }) From 80636777c83c000937251805fe483f23ad35213e Mon Sep 17 00:00:00 2001 From: Nayyir Jutha Date: Tue, 6 Aug 2024 16:05:48 -0400 Subject: [PATCH 04/28] updated register flow and moved methods to accounts namespace --- src/accounts/command.ts | 21 +++++++++++++++++ src/accounts/types.ts | 6 +++++ src/accounts/utils.ts | 30 +++++++++++++++++++++++-- src/config/types.ts | 1 + src/flows/index.ts | 1 - src/flows/register/index.ts | 41 ---------------------------------- src/flows/register/register.ts | 28 ----------------------- src/flows/register/types.ts | 5 ----- src/tui.ts | 24 ++++++++++++++++---- 9 files changed, 76 insertions(+), 81 deletions(-) delete mode 100644 src/flows/register/index.ts delete mode 100644 src/flows/register/register.ts delete mode 100644 src/flows/register/types.ts diff --git a/src/accounts/command.ts b/src/accounts/command.ts index a3b8432d..5b9c6a01 100644 --- a/src/accounts/command.ts +++ b/src/accounts/command.ts @@ -10,6 +10,7 @@ import { listAccounts, manageAccountsQuestions, newAccountQuestions, + registerAccount, selectAccountQuestions } from "./utils"; import { CreateAccountParams } from "./types"; @@ -67,6 +68,26 @@ export class AccountsCommand extends BaseCommand { return { seed, name, path } } + public async registerAccount (account: EntropyAccountConfig): Promise { + this.logger.debug( + 'about to register selectedAccount.address' + + account.address + 'keyring:' + + // @ts-expect-error Type export of ChildKey still not available from SDK + this.entropy.keyring.getLazyLoadAccountProxy('registration').pair.address, + 'REGISTER' + ) + + try { + const verifyingKey = await registerAccount(this.entropy) + + account?.data?.registration?.verifyingKeys?.push(verifyingKey) + return account + } catch (error) { + this.logger.error('There was a problem registering', error) + throw error + } + } + public async runInteraction (config): Promise { const { accounts } = config const { interactionChoice } = await inquirer.prompt(manageAccountsQuestions) diff --git a/src/accounts/types.ts b/src/accounts/types.ts index c3b92350..274350aa 100644 --- a/src/accounts/types.ts +++ b/src/accounts/types.ts @@ -4,4 +4,10 @@ export type ListedAccount = { name: string address: string verifyingKeys: string[] +} + +export interface RegisterParams { + programModAddress?: string + // TODO: Export ProgramInstance type from sdk + programData?: any } \ No newline at end of file diff --git a/src/accounts/utils.ts b/src/accounts/utils.ts index 5e4862bb..873f5f1e 100644 --- a/src/accounts/utils.ts +++ b/src/accounts/utils.ts @@ -1,9 +1,10 @@ // @ts-expect-error import Keyring from '@entropyxyz/sdk/keys' import { EntropyAccountConfig, EntropyConfig } from "../config/types"; -import { CreateAccountParams, ListedAccount } from './types'; +import { CreateAccountParams, ListedAccount, RegisterParams } from './types'; import { ACCOUNTS_CONTENT } from './constants'; -import { generateAccountChoices } from 'src/common/utils'; +import { generateAccountChoices, print } from 'src/common/utils'; +import Entropy from '@entropyxyz/sdk'; const validateSeedInput = (seed) => { if (seed.includes('#debug')) return true @@ -93,4 +94,29 @@ export function listAccounts ({ accounts }: Partial) { 'There are currently no accounts available, please create or import your new account using the Manage Accounts feature' ) return formatAccountsList(accountsArray) +} + +export async function registerAccount (entropy: Entropy, params?: RegisterParams): Promise { + let verifyingKey: string + try { + const registerParams = params?.programModAddress && params?.programData ? { programDeployer: params.programModAddress, programData: params.programData } : undefined + + verifyingKey = await entropy.register(registerParams) + return verifyingKey + } catch (error) { + if (!verifyingKey) { + try { + const tx = entropy.substrate.tx.registry.pruneRegistration() + await tx.signAndSend(entropy.keyring.accounts.registration.pair, ({ status }) => { + if (status.isFinalized) { + print('Successfully pruned registration'); + } + }) + } catch (error) { + console.error('Unable to prune registration due to:', error.message); + throw error + } + } + throw error + } } \ No newline at end of file diff --git a/src/config/types.ts b/src/config/types.ts index 74d52ffc..1ca883fc 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -1,6 +1,7 @@ export interface EntropyConfig { accounts: EntropyAccountConfig[] endpoints: { dev: string; 'test-net': string } + selectedAccount: string 'migration-version': string } diff --git a/src/flows/index.ts b/src/flows/index.ts index c024ac3c..c5e1549f 100644 --- a/src/flows/index.ts +++ b/src/flows/index.ts @@ -1,5 +1,4 @@ export { entropyFaucet } from './entropyFaucet' -export { entropyRegister } from './register' export { userPrograms, devPrograms } from './programs' export { sign } from './sign' export { entropyTransfer } from './entropyTransfer' diff --git a/src/flows/register/index.ts b/src/flows/register/index.ts deleted file mode 100644 index 850cedbb..00000000 --- a/src/flows/register/index.ts +++ /dev/null @@ -1,41 +0,0 @@ -// import inquirer from "inquirer" -import { getSelectedAccount, print } from "../../common/utils" -import { initializeEntropy } from "../../common/initializeEntropy" -import { EntropyLogger } from "src/common/logger"; -import { register } from "./register"; - -export async function entropyRegister (storedConfig, options, logger: EntropyLogger) { - const FLOW_CONTEXT = 'REGISTER' - const { accounts, selectedAccount: selectedFromConfig } = storedConfig; - const { endpoint } = options - - if (!selectedFromConfig) return - const selectedAccount = getSelectedAccount(accounts, selectedFromConfig) - - const entropy = await initializeEntropy({ keyMaterial: selectedAccount.data, endpoint }) - // TO-DO: investigate this a little more - // const filteredAccountChoices = generateAccountChoices(accounts) - // Not going to ask for a pointer from the user just yet - // const { programPointer } = await inquirer.prompt([{ - // type: 'input', - // message: 'Enter the program pointer here:', - // name: 'programPointer', - // // Setting default to default key proxy program - // default: '0x0000000000000000000000000000000000000000000000000000000000000000' - // }]) - // @ts-expect-error: Expecting error here as method expects typeof ChildKey enum from sdk - // export from sdk is not working as intended currently - logger.debug('about to register selectedAccount.address' + selectedAccount.address + 'keyring:' + entropy.keyring.getLazyLoadAccountProxy('registration').pair.address, FLOW_CONTEXT) - print("Attempting to register the address:", selectedAccount.address, ) - - try { - const verifyingKey = await register(entropy) - print("Your address", selectedAccount.address, "has been successfully registered.") - selectedAccount?.data?.registration?.verifyingKeys?.push(verifyingKey) - const arrIdx = accounts.indexOf(selectedAccount) - accounts.splice(arrIdx, 1, selectedAccount) - return { accounts, selectedAccount: selectedAccount.address } - } catch (error) { - logger.error('There was a problem registering', error) - } -} diff --git a/src/flows/register/register.ts b/src/flows/register/register.ts deleted file mode 100644 index d859d92b..00000000 --- a/src/flows/register/register.ts +++ /dev/null @@ -1,28 +0,0 @@ -import Entropy from "@entropyxyz/sdk"; -import { RegisterParams } from "./types"; -import { print } from "src/common/utils"; - -export async function register (entropy: Entropy, params?: RegisterParams): Promise { - let verifyingKey: string - try { - const registerParams = params?.programModAddress && params?.programData ? { programDeployer: params.programModAddress, programData: params.programData } : undefined - - verifyingKey = await entropy.register(registerParams) - return verifyingKey - } catch (error) { - if (!verifyingKey) { - try { - const tx = entropy.substrate.tx.registry.pruneRegistration() - await tx.signAndSend(entropy.keyring.accounts.registration.pair, ({ status }) => { - if (status.isFinalized) { - print('Successfully pruned registration'); - } - }) - } catch (error) { - console.error('Unable to prune registration due to:', error.message); - throw error - } - } - throw error - } -} \ No newline at end of file diff --git a/src/flows/register/types.ts b/src/flows/register/types.ts deleted file mode 100644 index 1a1dc573..00000000 --- a/src/flows/register/types.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface RegisterParams { - programModAddress?: string - // TODO: Export ProgramInstance type from sdk - programData?: any -} \ No newline at end of file diff --git a/src/tui.ts b/src/tui.ts index c9f03b9e..f1612c8d 100644 --- a/src/tui.ts +++ b/src/tui.ts @@ -4,7 +4,7 @@ import * as config from './config' import * as flows from './flows' import { EntropyTuiOptions } from './types' import { logo } from './common/ascii' -import { print, updateConfig } from './common/utils' +import { getSelectedAccount, print, updateConfig } from './common/utils' import { EntropyLogger } from './common/logger' import { BalanceCommand } from './balance/command' import { AccountsCommand } from './accounts/command' @@ -22,7 +22,7 @@ export default function tui (entropy: Entropy, options: EntropyTuiOptions) { 'Manage Accounts': () => {}, // leaving as a noop function until all flows are restructured 'Balance': () => {}, - 'Register': flows.entropyRegister, + 'Register': () => {}, 'Sign': flows.sign, 'Transfer': flows.entropyTransfer, // TODO: design programs in TUI (merge deploy+user programs) @@ -48,6 +48,8 @@ async function main (entropy: Entropy, choices, options, logger: EntropyLogger) await config.init() shouldInit = false } + const balanceCommand = new BalanceCommand(entropy, options.endpoint) + const accountsCommand = new AccountsCommand(entropy, options.endpoint) let storedConfig = await config.get() @@ -79,18 +81,32 @@ async function main (entropy: Entropy, choices, options, logger: EntropyLogger) logger.debug(answers) switch (answers.choice) { case "Balance": { - const balanceCommand = new BalanceCommand(entropy, options.endpoint) const balanceString = await balanceCommand.getBalance(storedConfig.selectedAccount) print(`Address ${storedConfig.selectedAccount} has a balance of: ${balanceString}`) break; } case 'Manage Accounts': { - const accountsCommand = new AccountsCommand(entropy, options.endpoint) const response = await accountsCommand.runInteraction(storedConfig) returnToMain = await updateConfig(storedConfig, response) storedConfig = await config.get() break; } + case 'Register': { + const { accounts, selectedAccount } = storedConfig + const currentAccount = getSelectedAccount(accounts, selectedAccount) + if (!currentAccount) { + print("No account selected to register") + break; + } + print("Attempting to register the address:", currentAccount.address) + const updatedAccount = await accountsCommand.registerAccount(currentAccount) + const arrIdx = accounts.indexOf(currentAccount) + accounts.splice(arrIdx, 1, updatedAccount) + print("Your address", updatedAccount.address, "has been successfully registered.") + returnToMain = await updateConfig(storedConfig, { accounts, selectedAccount: updatedAccount.address }) + storedConfig = await config.get() + break; + } default: { const newConfigUpdates = await choices[answers.choice](storedConfig, options, logger) if (typeof newConfigUpdates === 'string' && newConfigUpdates === 'exit') { From 118c5a7ba8b475159ccd94666af8edff662d7455 Mon Sep 17 00:00:00 2001 From: Nayyir Jutha Date: Tue, 6 Aug 2024 17:02:43 -0400 Subject: [PATCH 05/28] updated register tests --- tests/register.test.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/register.test.ts b/tests/register.test.ts index f093df6f..67167a0d 100644 --- a/tests/register.test.ts +++ b/tests/register.test.ts @@ -1,15 +1,14 @@ import test from 'tape' - +import { registerAccount } from '../src/accounts/utils' import { charlieStashSeed, setupTest } from './testing-utils' -import { register } from '../src/flows/register/register' import { readFileSync } from 'node:fs' const networkType = 'two-nodes' -test('Regsiter - Default Program', async (t) => { +test('Register - Default Program', async (t) => { const { run, entropy } = await setupTest(t, { networkType, seed: charlieStashSeed }) - const verifyingKey = await run('register account', register(entropy)) + const verifyingKey = await run('register account', registerAccount(entropy)) const fullAccount = entropy.keyring.getAccount() @@ -30,7 +29,7 @@ test('Register - Barebones Program', async t => { const verifyingKey = await run( 'register - using custom params', - register(entropy, { + registerAccount(entropy, { programModAddress: entropy.keyring.accounts.registration.address, programData: [{ program_pointer: pointer, program_config: '0x' }], }) From 6dfd8b2297af2e26dbd8b827629c72edb05ae983 Mon Sep 17 00:00:00 2001 From: Nayyir Jutha Date: Tue, 6 Aug 2024 19:08:12 -0400 Subject: [PATCH 06/28] cleanup from smoke test --- src/accounts/command.ts | 4 ++-- src/accounts/constants.ts | 2 +- src/cli.ts | 39 +++++++++++++++++++++++++++++++++++++-- 3 files changed, 40 insertions(+), 5 deletions(-) diff --git a/src/accounts/command.ts b/src/accounts/command.ts index 5b9c6a01..f85c1d1f 100644 --- a/src/accounts/command.ts +++ b/src/accounts/command.ts @@ -43,7 +43,7 @@ export class AccountsCommand extends BaseCommand { } public async selectAccount (accounts: EntropyAccountConfig[]) { - const answers = await inquirer.prompt([selectAccountQuestions(accounts)]) + const answers = await inquirer.prompt(selectAccountQuestions(accounts)) return { selectedAccount: answers.selectedAccount.address } } @@ -91,7 +91,7 @@ export class AccountsCommand extends BaseCommand { public async runInteraction (config): Promise { const { accounts } = config const { interactionChoice } = await inquirer.prompt(manageAccountsQuestions) - + switch (interactionChoice) { case 'create-account': { const createAccountParams = await this.getUserInput() diff --git a/src/accounts/constants.ts b/src/accounts/constants.ts index b8c407a0..4c815477 100644 --- a/src/accounts/constants.ts +++ b/src/accounts/constants.ts @@ -25,7 +25,7 @@ export const ACCOUNTS_CONTENT = { message: "Choose account:", }, interactionChoice: { - name: 'choice', + name: 'interactionChoice', choices: [ { name: 'Create/Import Account', value: 'create-import' }, { name: 'Select Account', value: 'select-account' }, diff --git a/src/cli.ts b/src/cli.ts index 808ed709..8833cfd0 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -9,7 +9,7 @@ import { EntropyTuiOptions } from './types' import { cliEntropyTransfer } from './flows/entropyTransfer/cli' import { cliSign } from './flows/sign/cli' -import { getSelectedAccount, stringify } from './common/utils' +import { getSelectedAccount, stringify, updateConfig } from './common/utils' import Entropy from '@entropyxyz/sdk' import { initializeEntropy } from './common/initializeEntropy' import { BalanceCommand } from './balance/command' @@ -150,7 +150,7 @@ program.command('new-account') ) .addOption( new Option( - '-p, --path', + '-pa, --path', 'Derivation path' ).default(ACCOUNTS_CONTENT.path.default) ) @@ -165,6 +165,41 @@ program.command('new-account') process.exit(0) }) +/* register */ +program.command('register') + .description('Register an entropy account with a program') + .argument('address', 'Address of existing entropy account') + .addOption(passwordOption()) + .addOption(endpointOption()) + .addOption( + new Option( + '-pointer, --pointer', + 'Program pointer of program to be used for registering' + ) + ) + .addOption( + new Option( + '-data, --program-data', + 'Path to file containing program data in JSON format' + ) + ) + .action(async (address, opts) => { + const storedConfig = await config.get() + const { accounts } = storedConfig + const accountsCommand = new AccountsCommand(entropy, opts.endpoint) + writeOut('Attempting to register account with addtess: ' + address) + const accountToRegister = getSelectedAccount(accounts, address) + if (!accountToRegister) { + throw new Error('AccountError: Unable to register non-existent account') + } + const updatedAccount = await accountsCommand.registerAccount(accountToRegister) + const arrIdx = accounts.indexOf(accountToRegister) + accounts.splice(arrIdx, 1, updatedAccount) + await updateConfig(storedConfig, { accounts, selectedAccount: updatedAccount.address }) + writeOut("Your address" + updatedAccount.address + "has been successfully registered.") + process.exit(0) + }) + /* balance */ program.command('balance') .description('Get the balance of an Entropy account. Output is a number') From 9d22f6a659bb6dc9ff4a269fd0f6c99e21abebec Mon Sep 17 00:00:00 2001 From: Nayyir Jutha Date: Wed, 7 Aug 2024 13:39:44 -0400 Subject: [PATCH 07/28] updated changelog --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a554011..d1ab1259 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,9 @@ Version header format: `[version] Name - year-month-day (entropy-core compatibil - new: './src/balance' - new file structure for our CLI/TUI flows - new: './src/balance/command.ts' - main entry file for balance command for tui/cli - new: './src/balance/utils.ts' - utilities and helper methods for all things balance +- new: './src/accounts' - new file structure for our CLI/TUI flows + - new: './src/accounts/command.ts' - main entry file for accounts command for tui/cli + - new: './src/accounts/utils.ts' - utilities and helper methods for all things accounts ### Changed @@ -39,6 +42,8 @@ Version header format: `[version] Name - year-month-day (entropy-core compatibil - logger to handle nested contexts for better organization of logs - merged user + dev program folders + tests - removed flows/balance/*.ts directory with file restructure +- removed flows/manage-accounts/*/*.ts directory with file restructure +- removed flows/register/*.ts directory with file restructure ### Broke From 1b7aa6f41badb96a125e28e262778eb9e2c3359b Mon Sep 17 00:00:00 2001 From: mixmix Date: Tue, 27 Aug 2024 16:54:03 +1200 Subject: [PATCH 08/28] start refactor --- src/account/command.ts | 68 ++++++++++++ src/{accounts => account}/constants.ts | 0 src/{accounts/command.ts => account/main.ts} | 4 +- src/{accounts => account}/types.ts | 0 src/{accounts => account}/utils.ts | 0 src/cli.ts | 107 +------------------ src/common/utils-cli.ts | 105 ++++++++++++++++++ src/tui.ts | 4 +- 8 files changed, 182 insertions(+), 106 deletions(-) create mode 100644 src/account/command.ts rename src/{accounts => account}/constants.ts (100%) rename src/{accounts/command.ts => account/main.ts} (98%) rename src/{accounts => account}/types.ts (100%) rename src/{accounts => account}/utils.ts (100%) create mode 100644 src/common/utils-cli.ts diff --git a/src/account/command.ts b/src/account/command.ts new file mode 100644 index 00000000..a428aaf5 --- /dev/null +++ b/src/account/command.ts @@ -0,0 +1,68 @@ +import Entropy from "@entropyxyz/sdk"; +import { Command, Option } from 'commander' +import { randomAsHex } from '@polkadot/util-crypto' +import { EntropyAccount } from "./main"; +import { ACCOUNTS_CONTENT } from './constants' +import * as config from '../config' +import { cliWrite, endpointOption, passwordOption } from "../common/utils-cli"; + +export async function entropyAccountCommand (entropy: Entropy, rootCommand: Command) { + const accountCommand = rootCommand.command('account') + .description('Commands to work with accounts on the Entropy Network') + + entropyAccountList(entropy, accountCommand) + entropyAccountNew(entropy, accountCommand) +} + +function entropyAccountList (entropy: Entropy, accountCommand: Command) { + accountCommand.command('list') + .alias('ls') + .description('List all accounts. Output is JSON of form [{ name, address, verifyingKeys }]') + .addOption(endpointOption()) + .action(async (options) => { + // 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 accountsCommand = new EntropyAccount(entropy, options.endpoint) + const accounts = accountsCommand.listAccounts(storedConfig.accounts) + cliWrite(accounts) + process.exit(0) + }) +} + +function entropyAccountNew (entropy: Entropy, accountCommand: Command) { + accountCommand.command('new') + .alias('new-account') + .alias('create') + .description('Create new entropy account from imported seed or from scratch. Output is JSON of form [{name, address}]') + .addOption(endpointOption()) + .addOption(passwordOption()) + .addOption( + new Option( + '-s, --seed', + 'Seed used to create entropy account' + ).default(randomAsHex(32)) + ) + .addOption( + new Option( + '-n, --name', + 'Name of entropy account' + ).makeOptionMandatory(true) + ) + .addOption( + new Option( + '-pa, --path', + 'Derivation path' + ).default(ACCOUNTS_CONTENT.path.default) + ) + .action(async (opts) => { + const storedConfig = await config.get() + const { seed, name, path, endpoint } = opts + const accountsCommand = new EntropyAccount(entropy, endpoint) + + const newAccount = await accountsCommand.newAccount({ seed, name, path }) + await accountsCommand.updateConfig(storedConfig, newAccount) + cliWrite({ name: newAccount.name, address: newAccount.address }) + process.exit(0) + }) + +} diff --git a/src/accounts/constants.ts b/src/account/constants.ts similarity index 100% rename from src/accounts/constants.ts rename to src/account/constants.ts diff --git a/src/accounts/command.ts b/src/account/main.ts similarity index 98% rename from src/accounts/command.ts rename to src/account/main.ts index f85c1d1f..6f38670f 100644 --- a/src/accounts/command.ts +++ b/src/account/main.ts @@ -15,7 +15,7 @@ import { } from "./utils"; import { CreateAccountParams } from "./types"; -export class AccountsCommand extends BaseCommand { +export class EntropyAccount extends BaseCommand { constructor (entropy: Entropy, endpoint: string) { super(entropy, endpoint, FLOW_CONTEXT) } @@ -118,4 +118,4 @@ export class AccountsCommand extends BaseCommand { throw new Error('AccountsError: Unknown interaction action') } } -} \ No newline at end of file +} diff --git a/src/accounts/types.ts b/src/account/types.ts similarity index 100% rename from src/accounts/types.ts rename to src/account/types.ts diff --git a/src/accounts/utils.ts b/src/account/utils.ts similarity index 100% rename from src/accounts/utils.ts rename to src/account/utils.ts diff --git a/src/cli.ts b/src/cli.ts index d0093ead..65fd10bc 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -2,74 +2,24 @@ /* NOTE: calling this file entropy.ts helps commander parse process.argv */ import { Command, Option } from 'commander' -import { randomAsHex } from '@polkadot/util-crypto' import launchTui from './tui' import * as config from './config' import { EntropyTuiOptions } from './types' import { cliSign } from './flows/sign/cli' import { getSelectedAccount, stringify, updateConfig } from './common/utils' +import { endpointOption, currentAccountAddressOption, passwordOption } from './common/utils-cli' import Entropy from '@entropyxyz/sdk' import { initializeEntropy } from './common/initializeEntropy' +import { entropyAccountCommand } from './account/command' +import { EntropyAccount } from './account/main' import { BalanceCommand } from './balance/command' -import { AccountsCommand } from './accounts/command' -import { ACCOUNTS_CONTENT } from './accounts/constants' import { TransferCommand } from './transfer/command' const program = new Command() // Array of restructured commands to make it easier to migrate them to the new "flow" const RESTRUCTURED_COMMANDS = ['balance', 'new-account'] -function endpointOption (){ - return new Option( - '-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`.' - ].join(' ') - ) - .env('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] - if (!endpoint) throw Error('unknown endpoint alias: ' + aliasOrEndpoint) - - return endpoint - }) - .default('ws://testnet.entropy.xyz:9944/') - // NOTE: argParser is only run IF an option is provided, so this cannot be 'test-net' -} - -function passwordOption (description?: string) { - return new Option( - '-p, --password ', - description || 'Password for the account' - ) -} - -function currentAccountAddressOption () { - const storedConfig = config.getSync() - return new Option( - '-a, --account ', - 'Sets the current account for the session or defaults to the account stored in the config' - ) - .env('ACCOUNT_ADDRESS') - .argParser(async (address) => { - if (address === storedConfig.selectedAccount) return address - // Updated selected account in config with new address from this option - const newConfigUpdates = { selectedAccount: address } - await config.set({ ...storedConfig, ...newConfigUpdates }) - - return address - }) - .hideHelp() - .default(storedConfig.selectedAccount) -} - let entropy: Entropy export async function loadEntropy (address: string, endpoint: string, password?: string): Promise { @@ -122,54 +72,7 @@ program launchTui(entropy, options) }) -/* list */ -program.command('list') - .alias('ls') - .description('List all accounts. Output is JSON of form [{ name, address, verifyingKeys }]') - .addOption(endpointOption()) - .action(async (options) => { - // 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 accountsCommand = new AccountsCommand(entropy, options.endpoint) - const accounts = accountsCommand.listAccounts(storedConfig.accounts) - writeOut(accounts) - process.exit(0) - }) - -/* new account */ -program.command('new-account') - .alias('new') - .description('Create new entropy account from imported seed or from scratch. Output is JSON of form [{name, address}]') - .addOption(endpointOption()) - .addOption(passwordOption()) - .addOption( - new Option( - '-s, --seed', - 'Seed used to create entropy account' - ).default(randomAsHex(32)) - ) - .addOption( - new Option( - '-n, --name', - 'Name of entropy account' - ).makeOptionMandatory(true) - ) - .addOption( - new Option( - '-pa, --path', - 'Derivation path' - ).default(ACCOUNTS_CONTENT.path.default) - ) - .action(async (opts) => { - const storedConfig = await config.get() - const { seed, name, path, endpoint } = opts - const accountsCommand = new AccountsCommand(entropy, endpoint) - - const newAccount = await accountsCommand.newAccount({ seed, name, path }) - await accountsCommand.updateConfig(storedConfig, newAccount) - writeOut({ name: newAccount.name, address: newAccount.address }) - process.exit(0) - }) +entropyAccountCommand(entropy, program) /* register */ program.command('register') @@ -192,7 +95,7 @@ program.command('register') .action(async (address, opts) => { const storedConfig = await config.get() const { accounts } = storedConfig - const accountsCommand = new AccountsCommand(entropy, opts.endpoint) + const accountsCommand = new EntropyAccount(entropy, opts.endpoint) writeOut('Attempting to register account with addtess: ' + address) const accountToRegister = getSelectedAccount(accounts, address) if (!accountToRegister) { diff --git a/src/common/utils-cli.ts b/src/common/utils-cli.ts new file mode 100644 index 00000000..28f73eb0 --- /dev/null +++ b/src/common/utils-cli.ts @@ -0,0 +1,105 @@ +import { Option } from 'commander' +import { getSelectedAccount, stringify } from './utils' +import * as config from '../config' +import Entropy from '@entropyxyz/sdk' +import { initializeEntropy } from './initializeEntropy' + +export function cliWrite (result) { + const prettyResult = stringify(result) + process.stdout.write(prettyResult) +} + +export function endpointOption () { + return new Option( + '-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`.' + ].join(' ') + ) + .env('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] + if (!endpoint) throw Error('unknown endpoint alias: ' + aliasOrEndpoint) + + return endpoint + }) + .default('ws://testnet.entropy.xyz:9944/') + // NOTE: argParser is only run IF an option is provided, so this cannot be 'test-net' +} + +export function passwordOption (description?: string) { + return new Option( + '-p, --password ', + description || 'Password for the account' + ) +} + +export function currentAccountAddressOption () { + const storedConfig = config.getSync() + return new Option( + '-a, --account ', + 'Sets the current account for the session or defaults to the account stored in the config' + ) + .env('ACCOUNT_ADDRESS') + .argParser(async (address) => { + if (address === storedConfig.selectedAccount) return address + // Updated selected account in config with new address from this option + const newConfigUpdates = { selectedAccount: address } + await config.set({ ...storedConfig, ...newConfigUpdates }) + + return address + }) + .hideHelp() + .default(storedConfig.selectedAccount) +} + + +export function aliasOrAddressOption () { + return new Option( + '-a, --address ', + 'The alias or address of the verifying key to use for this command. Can be an alias or hex address.' + // TODO: describe default behaviour when "sessions" are introduced? + ) + // QUESTION: as this is a function, this could be a viable way to set the VK? + // .default(process.env.ENTROPY_SESSION) +} + +export async function loadEntropy (entropy: Entropy | undefined, address: string, endpoint: string, password?: string): Promise { + const storedConfig = config.getSync() + const selectedAccount = getSelectedAccount(storedConfig.accounts, address) + + if (!selectedAccount) throw new Error(`AddressError: No account with address ${address}`) + + // check if data is encrypted + we have a password + if (typeof selectedAccount.data === 'string' && !password) { + throw new Error('AuthError: This account requires a password, add --password ') + } + + entropy = await initializeEntropy({ keyMaterial: selectedAccount.data, endpoint, password }) + + if (!entropy?.keyring?.accounts?.registration?.pair) { + throw new Error("Signer keypair is undefined or not properly initialized.") + } + + return entropy +} + +export async function reloadEntropy (entropy: Entropy, newAddress: string, oldAddress: string, endpoint: string): Promise { + try { + entropy = await loadEntropy(entropy, newAddress, endpoint) + } catch (error) { + if (error.message.includes('AddressError')) { + entropy = await loadEntropy(entropy, oldAddress, endpoint) + return entropy + } + throw error + } + + return entropy +} \ No newline at end of file diff --git a/src/tui.ts b/src/tui.ts index 7e0c51b0..17f3e993 100644 --- a/src/tui.ts +++ b/src/tui.ts @@ -7,7 +7,7 @@ import { logo } from './common/ascii' import { getSelectedAccount, print, updateConfig } from './common/utils' import { EntropyLogger } from './common/logger' import { BalanceCommand } from './balance/command' -import { AccountsCommand } from './accounts/command' +import { EntropyAccount } from './account/main' import { TransferCommand } from './transfer/command' import { loadEntropy } from './cli' @@ -51,7 +51,7 @@ async function main (entropy: Entropy, choices, options, logger: EntropyLogger) shouldInit = false } const balanceCommand = new BalanceCommand(entropy, options.endpoint) - const accountsCommand = new AccountsCommand(entropy, options.endpoint) + const accountsCommand = new EntropyAccount(entropy, options.endpoint) const transferCommand = new TransferCommand(entropy, options.endpoint) let storedConfig = await config.get() From ec0609f5a854e747d9393f574f952fc2715a2b11 Mon Sep 17 00:00:00 2001 From: mixmix Date: Wed, 28 Aug 2024 17:11:32 +1200 Subject: [PATCH 09/28] WIP: part way refactored account stuff --- src/account/command.ts | 74 ++++++------ src/account/interaction.ts | 35 ++++++ src/account/main.ts | 225 +++++++++++++++++++++---------------- src/account/types.ts | 11 +- src/account/utils.ts | 41 +------ src/cli.ts | 55 ++++----- src/common/utils-cli.ts | 43 ++----- src/common/utils.ts | 3 +- src/tui.ts | 2 +- 9 files changed, 239 insertions(+), 250 deletions(-) create mode 100644 src/account/interaction.ts diff --git a/src/account/command.ts b/src/account/command.ts index a428aaf5..79e85a4b 100644 --- a/src/account/command.ts +++ b/src/account/command.ts @@ -1,6 +1,5 @@ import Entropy from "@entropyxyz/sdk"; import { Command, Option } from 'commander' -import { randomAsHex } from '@polkadot/util-crypto' import { EntropyAccount } from "./main"; import { ACCOUNTS_CONTENT } from './constants' import * as config from '../config' @@ -14,55 +13,52 @@ export async function entropyAccountCommand (entropy: Entropy, rootCommand: Comm entropyAccountNew(entropy, accountCommand) } -function entropyAccountList (entropy: Entropy, accountCommand: Command) { - accountCommand.command('list') - .alias('ls') - .description('List all accounts. Output is JSON of form [{ name, address, verifyingKeys }]') - .addOption(endpointOption()) - .action(async (options) => { - // 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 accountsCommand = new EntropyAccount(entropy, options.endpoint) - const accounts = accountsCommand.listAccounts(storedConfig.accounts) - cliWrite(accounts) - process.exit(0) - }) -} - function entropyAccountNew (entropy: Entropy, accountCommand: Command) { - accountCommand.command('new') - .alias('new-account') - .alias('create') - .description('Create new entropy account from imported seed or from scratch. Output is JSON of form [{name, address}]') + accountCommand.command('create') + .alias('new') + .description('Create a new entropy account from scratch. Output is JSON of form {name, address}') .addOption(endpointOption()) .addOption(passwordOption()) + .argument('', 'A user friendly name for your nem account.') .addOption( new Option( - '-s, --seed', - 'Seed used to create entropy account' - ).default(randomAsHex(32)) - ) - .addOption( - new Option( - '-n, --name', - 'Name of entropy account' - ).makeOptionMandatory(true) - ) - .addOption( - new Option( - '-pa, --path', + '-p, --path', 'Derivation path' ).default(ACCOUNTS_CONTENT.path.default) ) - .action(async (opts) => { + .action(async (name, opts) => { + const { endpoint, path } = opts + + const service = new EntropyAccount(entropy, endpoint) + const newAccount = await service.create({ + name, + path + }) + const storedConfig = await config.get() - const { seed, name, path, endpoint } = opts - const accountsCommand = new EntropyAccount(entropy, endpoint) + // WIP - sort out the updateConfig stuff + await service.updateConfig(storedConfig, newAccount) - const newAccount = await accountsCommand.newAccount({ seed, name, path }) - await accountsCommand.updateConfig(storedConfig, newAccount) - cliWrite({ name: newAccount.name, address: newAccount.address }) + cliWrite({ + name: newAccount.name, + address: newAccount.address + }) process.exit(0) }) } + +function entropyAccountList (entropy: Entropy, accountCommand: Command) { + accountCommand.command('list') + .alias('ls') + .description('List all accounts. Output is JSON of form [{ name, address, verifyingKeys }]') + .addOption(endpointOption()) + .action(async (options) => { + // 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 service = new EntropyAccount(entropy, options.endpoint) + const accounts = service.list(storedConfig.accounts) + cliWrite(accounts) + process.exit(0) + }) +} diff --git a/src/account/interaction.ts b/src/account/interaction.ts new file mode 100644 index 00000000..af3554b3 --- /dev/null +++ b/src/account/interaction.ts @@ -0,0 +1,35 @@ +import inquirer from "inquirer"; +import { EntropyAccount } from './main' +import { print } from "src/common/utils" + +import { + manageAccountsQuestions, + newAccountQuestions, + registerAccount, + selectAccountQuestions +} from "./utils"; + + +export async function entropyAccountCreate (entropy, endpoint) { + const { name, path } = await inquirer.prompt(newAccountQuestions) + + const service = new EntropyAccount(entropy, endpoint) + const account = await service.create({ name, path }) + + print(({ + name: account.name, + address: account.address + })) +} + +export async function entropyAccountCreate (entropy, endpoint) { + const { name, path } = await inquirer.prompt(newAccountQuestions) + + const service = new EntropyAccount(entropy, endpoint) + const account = await service.create({ name, path }) + + print(({ + name: account.name, + address: account.address + })) +} diff --git a/src/account/main.ts b/src/account/main.ts index 6f38670f..615fa01e 100644 --- a/src/account/main.ts +++ b/src/account/main.ts @@ -1,121 +1,150 @@ -import inquirer from "inquirer"; import Entropy from "@entropyxyz/sdk"; +// @ts-expect-error +import Keyring from '@entropyxyz/sdk/keys' import { randomAsHex } from '@polkadot/util-crypto' + import { BaseCommand } from "../common/base-command"; import { print, updateConfig } from "../common/utils"; import { EntropyAccountConfig, EntropyConfig } from "../config/types"; import { FLOW_CONTEXT } from "./constants"; -import { - createAccount, - listAccounts, - manageAccountsQuestions, - newAccountQuestions, - registerAccount, - selectAccountQuestions -} from "./utils"; -import { CreateAccountParams } from "./types"; +import { AccountCreateParams, AccountListResults } from "./types"; export class EntropyAccount extends BaseCommand { - constructor (entropy: Entropy, endpoint: string) { + // NOTE: this class is different - it doesn't need an entropy instance + constructor (entropy: Entropy | null, endpoint: string) { super(entropy, endpoint, FLOW_CONTEXT) } - public async newAccount (params?: CreateAccountParams): Promise { - let { seed, name, path } = params - let importKey: boolean + async create ({ name, path }: AccountCreateParams): Promise { + const seed = randomAsHex(32) + const keyring = new Keyring({ seed, path, debug: true }) + const fullAccount = keyring.getAccount() + // TODO: sdk should create account on constructor + const { admin } = keyring.getAccount() - if (!seed && !name && !path) { - ({ seed, name, path, importKey } = await inquirer.prompt(newAccountQuestions)) - } + const data = fullAccount + delete admin.pair + // const encryptedData = password ? passwordFlow.encrypt(data, password) : data - if (importKey && seed.includes('#debug')) { - seed = seed.split('#debug')[0] + return { + name: name, + address: admin.address, + data + // data: encryptedData // TODO: replace once password input is added back } - - return createAccount({ name, seed, path }) } - public async updateConfig (storedConfig: EntropyConfig, newAccount: EntropyAccountConfig): Promise { - const { accounts } = storedConfig - accounts.push(newAccount) - - return updateConfig(storedConfig, { accounts, selectedAccount: newAccount.address }) + list ({ accounts }: { accounts: EntropyAccountConfig[] }) { + const accountsArray = Array.isArray(accounts) && accounts.length + ? accounts + : [] + if (!accountsArray.length) + throw new Error( + 'There are currently no accounts available, please create or import a new account' + ) + return formatAccountsList(accountsArray) } - public async selectAccount (accounts: EntropyAccountConfig[]) { - const answers = await inquirer.prompt(selectAccountQuestions(accounts)) - - return { selectedAccount: answers.selectedAccount.address } - } + // WIP: Extract all these things into => interaction.ts - public listAccounts (accounts: EntropyAccountConfig[]) { - return listAccounts({ accounts }) - } + // public async newAccount (params?: AccountCreateParams): Promise { + // let { seed, name, path } = params + // let importKey: boolean - public async getUserInput (): Promise { - const answers = await inquirer.prompt(newAccountQuestions) - const { secret, name, path, importKey } = answers - let seed: string - - // never create debug keys only ever import them - if (importKey && secret.includes('#debug')) { - // isDebugMode = true - seed = secret.split('#debug')[0] - } else { - seed = importKey ? secret : randomAsHex(32) - } - - return { seed, name, path } - } + // if (!seed && !name && !path) { + // } - public async registerAccount (account: EntropyAccountConfig): Promise { - this.logger.debug( - 'about to register selectedAccount.address' + - account.address + 'keyring:' + - // @ts-expect-error Type export of ChildKey still not available from SDK - this.entropy.keyring.getLazyLoadAccountProxy('registration').pair.address, - 'REGISTER' - ) - - try { - const verifyingKey = await registerAccount(this.entropy) - - account?.data?.registration?.verifyingKeys?.push(verifyingKey) - return account - } catch (error) { - this.logger.error('There was a problem registering', error) - throw error - } - } + // if (importKey && seed.includes('#debug')) { + // seed = seed.split('#debug')[0] + // } - public async runInteraction (config): Promise { - const { accounts } = config - const { interactionChoice } = await inquirer.prompt(manageAccountsQuestions) - - switch (interactionChoice) { - case 'create-account': { - const createAccountParams = await this.getUserInput() - const newAccount = await this.newAccount(createAccountParams) - print('New Account:') - print({ name: newAccount.name, address: newAccount.address }) - accounts.push(newAccount) - return { accounts, selectedAccount: newAccount.address } - } - case 'select-account': { - const response = await this.selectAccount(config.accounts) - print('Current selected account is ' + response.selectedAccount) - return response - } - case 'list-account': { - const list = this.listAccounts(accounts) - list?.forEach(account => print(account)) - return - } - case 'exit': { - return 'exit' - } - default: - throw new Error('AccountsError: Unknown interaction action') - } - } + // } + + // public async updateConfig (storedConfig: EntropyConfig, newAccount: EntropyAccountConfig): Promise { + // const { accounts } = storedConfig + // accounts.push(newAccount) + // + // return updateConfig(storedConfig, { accounts, selectedAccount: newAccount.address }) + // } + + // public async selectAccount (accounts: EntropyAccountConfig[]) { + // const answers = await inquirer.prompt(selectAccountQuestions(accounts)) + // + // return { selectedAccount: answers.selectedAccount.address } + // } + + // public async getUserInput (): Promise { + // const answers = await inquirer.prompt(newAccountQuestions) + // const { secret, name, path, importKey } = answers + // let seed: string + // + // // never create debug keys only ever import them + // if (importKey && secret.includes('#debug')) { + // // isDebugMode = true + // seed = secret.split('#debug')[0] + // } else { + // seed = importKey ? secret : randomAsHex(32) + // } + // + // return { seed, name, path } + // } + + // public async registerAccount (account: EntropyAccountConfig): Promise { + // this.logger.debug( + // 'about to register selectedAccount.address' + + // account.address + 'keyring:' + + // // @ts-expect-error Type export of ChildKey still not available from SDK + // this.entropy.keyring.getLazyLoadAccountProxy('registration').pair.address, + // 'REGISTER' + // ) + + // try { + // const verifyingKey = await registerAccount(this.entropy) + // + // account?.data?.registration?.verifyingKeys?.push(verifyingKey) + // return account + // } catch (error) { + // this.logger.error('There was a problem registering', error) + // throw error + // } + // } + + // public async runInteraction (config): Promise { + // const { accounts } = config + // const { interactionChoice } = await inquirer.prompt(manageAccountsQuestions) + // + // switch (interactionChoice) { + // case 'create-account': { + // const createAccountParams = await this.getUserInput() + // const newAccount = await this.newAccount(createAccountParams) + // print('New Account:') + // print({ name: newAccount.name, address: newAccount.address }) + // accounts.push(newAccount) + // return { accounts, selectedAccount: newAccount.address } + // } + // case 'select-account': { + // const response = await this.selectAccount(config.accounts) + // print('Current selected account is ' + response.selectedAccount) + // return response + // } + // case 'list-account': { + // const list = this.list(accounts) + // list?.forEach(account => print(account)) + // return + // } + // case 'exit': { + // return 'exit' + // } + // default: + // throw new Error('AccountsError: Unknown interaction action') + // } + // } +} + +function formatAccountsList (accounts: EntropyAccountConfig[]): AccountListResults[] { + return accounts.map((account: EntropyAccountConfig) => ({ + name: account.name, + address: account.address, + verifyingKeys: account?.data?.admin?.verifyingKeys + })) } diff --git a/src/account/types.ts b/src/account/types.ts index 274350aa..66048778 100644 --- a/src/account/types.ts +++ b/src/account/types.ts @@ -1,13 +1,16 @@ -export interface CreateAccountParams { name: string, seed: string, path?: string } +export interface AccountCreateParams { + name: string + path?: string +} -export type ListedAccount = { +export type AccountListResults = { name: string address: string verifyingKeys: string[] } -export interface RegisterParams { +export interface AccountRegisterParams { programModAddress?: string // TODO: Export ProgramInstance type from sdk programData?: any -} \ No newline at end of file +} diff --git a/src/account/utils.ts b/src/account/utils.ts index 873f5f1e..605ce7ab 100644 --- a/src/account/utils.ts +++ b/src/account/utils.ts @@ -1,7 +1,5 @@ -// @ts-expect-error -import Keyring from '@entropyxyz/sdk/keys' import { EntropyAccountConfig, EntropyConfig } from "../config/types"; -import { CreateAccountParams, ListedAccount, RegisterParams } from './types'; +import { RegisterParams } from './types'; import { ACCOUNTS_CONTENT } from './constants'; import { generateAccountChoices, print } from 'src/common/utils'; import Entropy from '@entropyxyz/sdk'; @@ -61,41 +59,6 @@ export const manageAccountsQuestions = [ } ] -export async function createAccount ({ name, seed, path }: CreateAccountParams): Promise { - const keyring = new Keyring({ seed, path, debug: true }) - const fullAccount = keyring.getAccount() - // TO-DO: sdk should create account on constructor - const { admin } = keyring.getAccount() - - const data = fullAccount - delete admin.pair - // const encryptedData = password ? passwordFlow.encrypt(data, password) : data - - return { - name: name, - address: admin.address, - // TODO: replace with data: encryptedData once pasword input is added back - data, - } -} - -function formatAccountsList (accounts: EntropyAccountConfig[]): ListedAccount[] { - return accounts.map((account: EntropyAccountConfig) => ({ - name: account.name, - address: account.address, - verifyingKeys: account?.data?.admin?.verifyingKeys - })) -} - -export function listAccounts ({ accounts }: Partial) { - const accountsArray = Array.isArray(accounts) && accounts.length ? accounts : [] - if (!accountsArray.length) - throw new Error( - 'There are currently no accounts available, please create or import your new account using the Manage Accounts feature' - ) - return formatAccountsList(accountsArray) -} - export async function registerAccount (entropy: Entropy, params?: RegisterParams): Promise { let verifyingKey: string try { @@ -119,4 +82,4 @@ export async function registerAccount (entropy: Entropy, params?: RegisterParams } throw error } -} \ No newline at end of file +} diff --git a/src/cli.ts b/src/cli.ts index 65fd10bc..c2b25c4f 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -8,40 +8,32 @@ import { EntropyTuiOptions } from './types' import { cliSign } from './flows/sign/cli' import { getSelectedAccount, stringify, updateConfig } from './common/utils' -import { endpointOption, currentAccountAddressOption, passwordOption } from './common/utils-cli' +import { endpointOption, currentAccountAddressOption, loadEntropy, passwordOption } from './common/utils-cli' import Entropy from '@entropyxyz/sdk' -import { initializeEntropy } from './common/initializeEntropy' import { entropyAccountCommand } from './account/command' import { EntropyAccount } from './account/main' import { BalanceCommand } from './balance/command' import { TransferCommand } from './transfer/command' -const program = new Command() -// Array of restructured commands to make it easier to migrate them to the new "flow" -const RESTRUCTURED_COMMANDS = ['balance', 'new-account'] - let entropy: Entropy - -export async function loadEntropy (address: string, endpoint: string, password?: string): Promise { - const storedConfig = config.getSync() - const selectedAccount = getSelectedAccount(storedConfig.accounts, address) - - if (!selectedAccount) throw Error(`No account with address ${address}`) - - // check if data is encrypted + we have a password - if (typeof selectedAccount.data === 'string' && !password) { - throw Error('This account requires a password, add --password ') +async function setEntropyGlobal (address: string, endpoint: string, password?: string) { + if (entropy) { + const currentAddress = entropy?.keyring?.accounts?.registration?.address + if (address !== currentAddress) { + // Is it possible to hit this? + // - programmatic usage kills process after function call + // - tui usage manages mutation of entropy instance itself + await entropy.close() + entropy = await loadEntropy(address, endpoint, password) + } } - - entropy = await initializeEntropy({ keyMaterial: selectedAccount.data, endpoint, password }) - - if (!entropy?.keyring?.accounts?.registration?.pair) { - throw new Error("Signer keypair is undefined or not properly initialized.") + else { + entropy = await loadEntropy(address, endpoint, password) } - - return entropy } +const program = new Command() + /* no command */ program .name('entropy') @@ -57,16 +49,13 @@ program .hideHelp() ) .hook('preAction', async (_thisCommand, actionCommand) => { - if (!entropy || (entropy.keyring.accounts.registration.address !== actionCommand.args[0] || entropy.keyring.accounts.registration.address !== actionCommand.opts().account)) { - // balance includes an address argument, use that address to instantiate entropy - // can keep the conditional to check for length of args, and use the first index since it is our pattern to have the address as the first argument - if (RESTRUCTURED_COMMANDS.includes(actionCommand.name()) && actionCommand.args.length) { - await loadEntropy(actionCommand.args[0], actionCommand.opts().endpoint, actionCommand.opts().password) - } else { - // if address is not an argument, use the address from the option - await loadEntropy(actionCommand.opts().account, actionCommand.opts().endpoint, actionCommand.opts().password) - } - } + const { account, endpoint, password } = actionCommand.opts() + const address = actionCommand.name() === 'balance' + ? actionCommand.args[0] + : account + + console.log(_thisCommand.name(), actionCommand.name()) + await setEntropyGlobal(address, endpoint, password) }) .action((options: EntropyTuiOptions) => { launchTui(entropy, options) diff --git a/src/common/utils-cli.ts b/src/common/utils-cli.ts index 28f73eb0..22cfa012 100644 --- a/src/common/utils-cli.ts +++ b/src/common/utils-cli.ts @@ -43,63 +43,36 @@ export function passwordOption (description?: string) { export function currentAccountAddressOption () { const storedConfig = config.getSync() return new Option( - '-a, --account ', + '-a, --account
', 'Sets the current account for the session or defaults to the account stored in the config' ) .env('ACCOUNT_ADDRESS') - .argParser(async (address) => { - if (address === storedConfig.selectedAccount) return address + .argParser(async (account) => { + if (account === storedConfig.selectedAccount) return account // Updated selected account in config with new address from this option - const newConfigUpdates = { selectedAccount: address } + const newConfigUpdates = { selectedAccount: account } await config.set({ ...storedConfig, ...newConfigUpdates }) - return address + return account }) .hideHelp() .default(storedConfig.selectedAccount) } - -export function aliasOrAddressOption () { - return new Option( - '-a, --address ', - 'The alias or address of the verifying key to use for this command. Can be an alias or hex address.' - // TODO: describe default behaviour when "sessions" are introduced? - ) - // QUESTION: as this is a function, this could be a viable way to set the VK? - // .default(process.env.ENTROPY_SESSION) -} - -export async function loadEntropy (entropy: Entropy | undefined, address: string, endpoint: string, password?: string): Promise { +export async function loadEntropy (address: string, endpoint: string, password?: string): Promise { const storedConfig = config.getSync() const selectedAccount = getSelectedAccount(storedConfig.accounts, address) - - if (!selectedAccount) throw new Error(`AddressError: No account with address ${address}`) + if (!selectedAccount) throw new Error(`AddressError: No account with name or address "${address}"`) // check if data is encrypted + we have a password if (typeof selectedAccount.data === 'string' && !password) { throw new Error('AuthError: This account requires a password, add --password ') } - entropy = await initializeEntropy({ keyMaterial: selectedAccount.data, endpoint, password }) - + const entropy = await initializeEntropy({ keyMaterial: selectedAccount.data, endpoint, password }) if (!entropy?.keyring?.accounts?.registration?.pair) { throw new Error("Signer keypair is undefined or not properly initialized.") } return entropy } - -export async function reloadEntropy (entropy: Entropy, newAddress: string, oldAddress: string, endpoint: string): Promise { - try { - entropy = await loadEntropy(entropy, newAddress, endpoint) - } catch (error) { - if (error.message.includes('AddressError')) { - entropy = await loadEntropy(entropy, oldAddress, endpoint) - return entropy - } - throw error - } - - return entropy -} \ No newline at end of file diff --git a/src/common/utils.ts b/src/common/utils.ts index 81053888..5a3347bd 100644 --- a/src/common/utils.ts +++ b/src/common/utils.ts @@ -66,6 +66,7 @@ export function accountChoicesWithOther (accounts: EntropyAccountConfig[]) { .concat([{ name: "Other", value: null }]) } +// TODO: rename => findAccountByNameAddress export function getSelectedAccount (accounts: EntropyAccountConfig[], address: string) { return accounts.find(account => account.address === address) } @@ -77,4 +78,4 @@ export async function updateConfig (storedConfig: EntropyConfig, newUpdates: any await config.set({ ...storedConfig, ...newUpdates }) } return false -} \ No newline at end of file +} diff --git a/src/tui.ts b/src/tui.ts index 17f3e993..99b787fb 100644 --- a/src/tui.ts +++ b/src/tui.ts @@ -5,11 +5,11 @@ import * as flows from './flows' import { EntropyTuiOptions } from './types' import { logo } from './common/ascii' import { getSelectedAccount, print, updateConfig } from './common/utils' +import { loadEntropy } from './common/utils-cli' import { EntropyLogger } from './common/logger' import { BalanceCommand } from './balance/command' import { EntropyAccount } from './account/main' import { TransferCommand } from './transfer/command' -import { loadEntropy } from './cli' let shouldInit = true From 3aa419bbcb063ef224bc935c4aeff0c64b847919 Mon Sep 17 00:00:00 2001 From: Nayyir Jutha Date: Wed, 28 Aug 2024 14:47:53 -0400 Subject: [PATCH 10/28] cleaning up the cleanup for accounts restructure; --- src/account/interaction.ts | 53 ++++++++++--------- src/account/main.ts | 9 ++-- src/account/types.ts | 1 + src/balance/command.ts | 2 +- .../{base-command.ts => entropy-base.ts} | 2 +- src/transfer/command.ts | 2 +- 6 files changed, 37 insertions(+), 32 deletions(-) rename src/common/{base-command.ts => entropy-base.ts} (89%) diff --git a/src/account/interaction.ts b/src/account/interaction.ts index af3554b3..11715d0f 100644 --- a/src/account/interaction.ts +++ b/src/account/interaction.ts @@ -1,6 +1,7 @@ import inquirer from "inquirer"; import { EntropyAccount } from './main' import { print } from "src/common/utils" +import * as config from '../config' import { manageAccountsQuestions, @@ -8,28 +9,32 @@ import { registerAccount, selectAccountQuestions } from "./utils"; - - -export async function entropyAccountCreate (entropy, endpoint) { - const { name, path } = await inquirer.prompt(newAccountQuestions) - - const service = new EntropyAccount(entropy, endpoint) - const account = await service.create({ name, path }) - - print(({ - name: account.name, - address: account.address - })) -} - -export async function entropyAccountCreate (entropy, endpoint) { - const { name, path } = await inquirer.prompt(newAccountQuestions) - - const service = new EntropyAccount(entropy, endpoint) - const account = await service.create({ name, path }) - - print(({ - name: account.name, - address: account.address - })) +import Entropy from "@entropyxyz/sdk"; +import { EntropyConfig } from "src/config/types"; + +export async function entropyManageAccounts (entropy: Entropy, endpoint: string, storedConfig: EntropyConfig) { + const AccountService = new EntropyAccount(entropy, endpoint) + const { interactionChoice } = await inquirer.prompt(manageAccountsQuestions) + switch (interactionChoice) { + case 'create-import': { + let { seed, name, path, importKey } = await inquirer.prompt(newAccountQuestions) + if (importKey && secret.includes('#debug')) { + // isDebugMode = true + seed = secret.split('#debug')[0] + } else { + seed = importKey ? secret : randomAsHex(32) + } + + } + case 'list-account': { + + } + case 'select-account': { + + } + case 'exit': { + + } + } + return { accounts: responses.accounts ? responses.accounts : storedConfig.accounts, selectedAccount: responses.selectedAccount || storedConfig.selectedAccount } } diff --git a/src/account/main.ts b/src/account/main.ts index 615fa01e..e64e3389 100644 --- a/src/account/main.ts +++ b/src/account/main.ts @@ -3,20 +3,19 @@ import Entropy from "@entropyxyz/sdk"; import Keyring from '@entropyxyz/sdk/keys' import { randomAsHex } from '@polkadot/util-crypto' -import { BaseCommand } from "../common/base-command"; +import { EntropyBase } from "../common/entropy-base"; import { print, updateConfig } from "../common/utils"; import { EntropyAccountConfig, EntropyConfig } from "../config/types"; import { FLOW_CONTEXT } from "./constants"; import { AccountCreateParams, AccountListResults } from "./types"; -export class EntropyAccount extends BaseCommand { +export class EntropyAccount extends EntropyBase { // NOTE: this class is different - it doesn't need an entropy instance constructor (entropy: Entropy | null, endpoint: string) { super(entropy, endpoint, FLOW_CONTEXT) } - async create ({ name, path }: AccountCreateParams): Promise { - const seed = randomAsHex(32) + async create ({ seed = randomAsHex(32), name, path }: AccountCreateParams): Promise { const keyring = new Keyring({ seed, path, debug: true }) const fullAccount = keyring.getAccount() // TODO: sdk should create account on constructor @@ -40,7 +39,7 @@ export class EntropyAccount extends BaseCommand { : [] if (!accountsArray.length) throw new Error( - 'There are currently no accounts available, please create or import a new account' + 'Accounts Error: There are currently no accounts available, please create or import a new account' ) return formatAccountsList(accountsArray) } diff --git a/src/account/types.ts b/src/account/types.ts index 66048778..e5a833a4 100644 --- a/src/account/types.ts +++ b/src/account/types.ts @@ -1,5 +1,6 @@ export interface AccountCreateParams { name: string + seed: string path?: string } diff --git a/src/balance/command.ts b/src/balance/command.ts index d3c1a70f..06c576d2 100644 --- a/src/balance/command.ts +++ b/src/balance/command.ts @@ -1,5 +1,5 @@ import Entropy from "@entropyxyz/sdk" -import { BaseCommand } from "../common/base-command" +import { BaseCommand } from "../common/entropy-base" import * as BalanceUtils from "./utils" const FLOW_CONTEXT = 'ENTROPY-BALANCE' diff --git a/src/common/base-command.ts b/src/common/entropy-base.ts similarity index 89% rename from src/common/base-command.ts rename to src/common/entropy-base.ts index ded342e4..1e14bf3b 100644 --- a/src/common/base-command.ts +++ b/src/common/entropy-base.ts @@ -1,7 +1,7 @@ import Entropy from "@entropyxyz/sdk"; import { EntropyLogger } from "./logger"; -export abstract class BaseCommand { +export abstract class EntropyBase { protected logger: EntropyLogger protected entropy: Entropy diff --git a/src/transfer/command.ts b/src/transfer/command.ts index 7fee0ff1..0fe1277a 100644 --- a/src/transfer/command.ts +++ b/src/transfer/command.ts @@ -1,5 +1,5 @@ import Entropy from "@entropyxyz/sdk"; -import { BaseCommand } from "../common/base-command"; +import { BaseCommand } from "../common/entropy-base"; import { setupProgress } from "../common/progress"; import * as TransferUtils from './utils' import inquirer from "inquirer"; From 9cc1c671528bfdaedae7a62744aef12526d5299a Mon Sep 17 00:00:00 2001 From: Nayyir Jutha Date: Wed, 28 Aug 2024 14:49:12 -0400 Subject: [PATCH 11/28] forgot something --- src/account/interaction.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/account/interaction.ts b/src/account/interaction.ts index 11715d0f..da5c9e1b 100644 --- a/src/account/interaction.ts +++ b/src/account/interaction.ts @@ -18,13 +18,11 @@ export async function entropyManageAccounts (entropy: Entropy, endpoint: string, switch (interactionChoice) { case 'create-import': { let { seed, name, path, importKey } = await inquirer.prompt(newAccountQuestions) - if (importKey && secret.includes('#debug')) { + if (importKey && seed.includes('#debug')) { // isDebugMode = true - seed = secret.split('#debug')[0] - } else { - seed = importKey ? secret : randomAsHex(32) + seed = seed.split('#debug')[0] } - + const newAccount = await AccountService.create({ seed, name, path }) } case 'list-account': { From 249ca3e7f8d4f1bad83bf88f41849667ca5ee662 Mon Sep 17 00:00:00 2001 From: Nayyir Jutha Date: Wed, 28 Aug 2024 17:57:51 -0400 Subject: [PATCH 12/28] updated accounts restructure, continuing from mixs changes --- src/account/command.ts | 55 ++++++++++-- src/account/interaction.ts | 79 ++++++++++++----- src/account/main.ts | 162 ++++++++++++---------------------- src/account/types.ts | 2 +- src/account/utils.ts | 38 +++----- src/balance/command.ts | 6 +- src/cli.ts | 39 +------- src/common/entropy-base.ts | 2 +- src/transfer/command.ts | 6 +- src/tui.ts | 21 ++--- tests/manage-accounts.test.ts | 14 +-- 11 files changed, 193 insertions(+), 231 deletions(-) diff --git a/src/account/command.ts b/src/account/command.ts index 79e85a4b..2e1353a5 100644 --- a/src/account/command.ts +++ b/src/account/command.ts @@ -4,16 +4,17 @@ import { EntropyAccount } from "./main"; import { ACCOUNTS_CONTENT } from './constants' import * as config from '../config' import { cliWrite, endpointOption, passwordOption } from "../common/utils-cli"; +import { updateConfig } from "src/common/utils"; export async function entropyAccountCommand (entropy: Entropy, rootCommand: Command) { const accountCommand = rootCommand.command('account') .description('Commands to work with accounts on the Entropy Network') - entropyAccountList(entropy, accountCommand) - entropyAccountNew(entropy, accountCommand) + entropyAccountList(accountCommand) + entropyAccountNew(accountCommand) } -function entropyAccountNew (entropy: Entropy, accountCommand: Command) { +function entropyAccountNew (accountCommand: Command) { accountCommand.command('create') .alias('new') .description('Create a new entropy account from scratch. Output is JSON of form {name, address}') @@ -29,15 +30,20 @@ function entropyAccountNew (entropy: Entropy, accountCommand: Command) { .action(async (name, opts) => { const { endpoint, path } = opts - const service = new EntropyAccount(entropy, endpoint) + const service = new EntropyAccount({ endpoint }) const newAccount = await service.create({ name, path }) const storedConfig = await config.get() + const { accounts } = storedConfig + accounts.push(newAccount) // WIP - sort out the updateConfig stuff - await service.updateConfig(storedConfig, newAccount) + await updateConfig(storedConfig, { + accounts, + selectedAccount: newAccount.address + }) cliWrite({ name: newAccount.name, @@ -48,7 +54,7 @@ function entropyAccountNew (entropy: Entropy, accountCommand: Command) { } -function entropyAccountList (entropy: Entropy, accountCommand: Command) { +function entropyAccountList (accountCommand: Command) { accountCommand.command('list') .alias('ls') .description('List all accounts. Output is JSON of form [{ name, address, verifyingKeys }]') @@ -56,9 +62,44 @@ function entropyAccountList (entropy: Entropy, accountCommand: Command) { .action(async (options) => { // 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 service = new EntropyAccount(entropy, options.endpoint) + const service = new EntropyAccount({ endpoint: options.endpoint }) const accounts = service.list(storedConfig.accounts) cliWrite(accounts) process.exit(0) }) } + +/* register */ +// program.command('register') +// .description('Register an entropy account with a program') +// .argument('address', 'Address of existing entropy account') +// .addOption(passwordOption()) +// .addOption(endpointOption()) +// .addOption( +// new Option( +// '-pointer, --pointer', +// 'Program pointer of program to be used for registering' +// ) +// ) +// .addOption( +// new Option( +// '-data, --program-data', +// 'Path to file containing program data in JSON format' +// ) +// ) +// .action(async (address, opts) => { +// const storedConfig = await config.get() +// const { accounts } = storedConfig +// const accountsCommand = new EntropyAccount(entropy, opts.endpoint) +// writeOut('Attempting to register account with addtess: ' + address) +// const accountToRegister = getSelectedAccount(accounts, address) +// if (!accountToRegister) { +// throw new Error('AccountError: Unable to register non-existent account') +// } +// const updatedAccount = await accountsCommand.registerAccount(accountToRegister) +// const arrIdx = accounts.indexOf(accountToRegister) +// accounts.splice(arrIdx, 1, updatedAccount) +// await updateConfig(storedConfig, { accounts, selectedAccount: updatedAccount.address }) +// writeOut("Your address" + updatedAccount.address + "has been successfully registered.") +// process.exit(0) +// }) diff --git a/src/account/interaction.ts b/src/account/interaction.ts index da5c9e1b..f1f27b60 100644 --- a/src/account/interaction.ts +++ b/src/account/interaction.ts @@ -1,38 +1,73 @@ import inquirer from "inquirer"; +import Entropy from "@entropyxyz/sdk"; + +import { getSelectedAccount, print } from "../common/utils" +import { EntropyAccountConfig, EntropyConfig } from "../config/types"; import { EntropyAccount } from './main' -import { print } from "src/common/utils" -import * as config from '../config' import { manageAccountsQuestions, newAccountQuestions, - registerAccount, selectAccountQuestions -} from "./utils"; -import Entropy from "@entropyxyz/sdk"; -import { EntropyConfig } from "src/config/types"; +} from "./utils" + -export async function entropyManageAccounts (entropy: Entropy, endpoint: string, storedConfig: EntropyConfig) { - const AccountService = new EntropyAccount(entropy, endpoint) +export async function entropyManageAccounts (endpoint: string, storedConfig: EntropyConfig) { + const AccountService = new EntropyAccount({ endpoint }) + const { accounts } = storedConfig const { interactionChoice } = await inquirer.prompt(manageAccountsQuestions) switch (interactionChoice) { - case 'create-import': { - let { seed, name, path, importKey } = await inquirer.prompt(newAccountQuestions) - if (importKey && seed.includes('#debug')) { - // isDebugMode = true - seed = seed.split('#debug')[0] - } - const newAccount = await AccountService.create({ seed, name, path }) + case 'create-import': { + const answers = await inquirer.prompt(newAccountQuestions) + const { name, path, importKey } = answers + let { seed } = answers + if (importKey && seed.includes('#debug')) { + // isDebugMode = true + seed = seed.split('#debug')[0] } - case 'list-account': { - + const newAccount = await AccountService.create({ seed, name, path }) + accounts.push(newAccount) + return { + accounts, + selectedAccount: newAccount.address } - case 'select-account': { - + } + case 'select-account': { + const { selectedAccount } = await inquirer.prompt(selectAccountQuestions(accounts)) + + print('Current selected account is ' + selectedAccount) + return { + accounts: storedConfig.accounts, + selectedAccount: selectedAccount.address } - case 'exit': { + } + case 'list-account': { + const list = this.list(accounts) + list?.forEach((account: EntropyAccountConfig)=> print(account)) + return + } + case 'exit': { + return 'exit' + } + default: + throw new Error('AccountsError: Unknown interaction action') + } +} - } +export async function entropyRegister (entropy: Entropy, endpoint: string, storedConfig: EntropyConfig): Promise> { + const AccountService = new EntropyAccount({ entropy, endpoint }) + + const { accounts, selectedAccount } = storedConfig + const currentAccount = getSelectedAccount(accounts, selectedAccount) + if (!currentAccount) { + print("No account selected to register") + return; } - return { accounts: responses.accounts ? responses.accounts : storedConfig.accounts, selectedAccount: responses.selectedAccount || storedConfig.selectedAccount } + print("Attempting to register the address:", currentAccount.address) + const updatedAccount = await AccountService.registerAccount(currentAccount) + const arrIdx = accounts.indexOf(currentAccount) + accounts.splice(arrIdx, 1, updatedAccount) + print("Your address", updatedAccount.address, "has been successfully registered.") + + return { accounts, selectedAccount } } diff --git a/src/account/main.ts b/src/account/main.ts index e64e3389..270b575f 100644 --- a/src/account/main.ts +++ b/src/account/main.ts @@ -4,15 +4,20 @@ import Keyring from '@entropyxyz/sdk/keys' import { randomAsHex } from '@polkadot/util-crypto' import { EntropyBase } from "../common/entropy-base"; -import { print, updateConfig } from "../common/utils"; -import { EntropyAccountConfig, EntropyConfig } from "../config/types"; +import { EntropyAccountConfig } from "../config/types"; + import { FLOW_CONTEXT } from "./constants"; -import { AccountCreateParams, AccountListResults } from "./types"; +import { AccountCreateParams, AccountRegisterParams } from "./types"; +import { print } from "src/common/utils"; +import { formatAccountsList } from "./utils"; export class EntropyAccount extends EntropyBase { - // NOTE: this class is different - it doesn't need an entropy instance - constructor (entropy: Entropy | null, endpoint: string) { - super(entropy, endpoint, FLOW_CONTEXT) + // Entropy does not need to be required, as only register needs it + // Idea: We could use entropy as an argument for the register method, + // the base class has been updated to optionally require entropy in the + // constructor. + constructor ({ entropy, endpoint }: { entropy?: Entropy, endpoint: string }) { + super({ entropy, endpoint, flowContext: FLOW_CONTEXT }) } async create ({ seed = randomAsHex(32), name, path }: AccountCreateParams): Promise { @@ -44,106 +49,49 @@ export class EntropyAccount extends EntropyBase { return formatAccountsList(accountsArray) } - // WIP: Extract all these things into => interaction.ts - - // public async newAccount (params?: AccountCreateParams): Promise { - // let { seed, name, path } = params - // let importKey: boolean - - // if (!seed && !name && !path) { - // } - - // if (importKey && seed.includes('#debug')) { - // seed = seed.split('#debug')[0] - // } - - // } - - // public async updateConfig (storedConfig: EntropyConfig, newAccount: EntropyAccountConfig): Promise { - // const { accounts } = storedConfig - // accounts.push(newAccount) - // - // return updateConfig(storedConfig, { accounts, selectedAccount: newAccount.address }) - // } - - // public async selectAccount (accounts: EntropyAccountConfig[]) { - // const answers = await inquirer.prompt(selectAccountQuestions(accounts)) - // - // return { selectedAccount: answers.selectedAccount.address } - // } - - // public async getUserInput (): Promise { - // const answers = await inquirer.prompt(newAccountQuestions) - // const { secret, name, path, importKey } = answers - // let seed: string - // - // // never create debug keys only ever import them - // if (importKey && secret.includes('#debug')) { - // // isDebugMode = true - // seed = secret.split('#debug')[0] - // } else { - // seed = importKey ? secret : randomAsHex(32) - // } - // - // return { seed, name, path } - // } - - // public async registerAccount (account: EntropyAccountConfig): Promise { - // this.logger.debug( - // 'about to register selectedAccount.address' + - // account.address + 'keyring:' + - // // @ts-expect-error Type export of ChildKey still not available from SDK - // this.entropy.keyring.getLazyLoadAccountProxy('registration').pair.address, - // 'REGISTER' - // ) - - // try { - // const verifyingKey = await registerAccount(this.entropy) - // - // account?.data?.registration?.verifyingKeys?.push(verifyingKey) - // return account - // } catch (error) { - // this.logger.error('There was a problem registering', error) - // throw error - // } - // } - - // public async runInteraction (config): Promise { - // const { accounts } = config - // const { interactionChoice } = await inquirer.prompt(manageAccountsQuestions) - // - // switch (interactionChoice) { - // case 'create-account': { - // const createAccountParams = await this.getUserInput() - // const newAccount = await this.newAccount(createAccountParams) - // print('New Account:') - // print({ name: newAccount.name, address: newAccount.address }) - // accounts.push(newAccount) - // return { accounts, selectedAccount: newAccount.address } - // } - // case 'select-account': { - // const response = await this.selectAccount(config.accounts) - // print('Current selected account is ' + response.selectedAccount) - // return response - // } - // case 'list-account': { - // const list = this.list(accounts) - // list?.forEach(account => print(account)) - // return - // } - // case 'exit': { - // return 'exit' - // } - // default: - // throw new Error('AccountsError: Unknown interaction action') - // } - // } -} + private async register (params?: AccountRegisterParams): Promise { + const { programModAddress, programData } = params + let verifyingKey: string + try { + const registerParams = programModAddress && programData ? { programDeployer: programModAddress, programData } : undefined + + verifyingKey = await this.entropy.register(registerParams) + return verifyingKey + } catch (error) { + if (!verifyingKey) { + try { + const tx = this.entropy.substrate.tx.registry.pruneRegistration() + await tx.signAndSend(this.entropy.keyring.accounts.registration.pair, ({ status }) => { + if (status.isFinalized) { + print('Successfully pruned registration'); + } + }) + } catch (error) { + console.error('Unable to prune registration due to:', error.message); + throw error + } + } + throw error + } + } -function formatAccountsList (accounts: EntropyAccountConfig[]): AccountListResults[] { - return accounts.map((account: EntropyAccountConfig) => ({ - name: account.name, - address: account.address, - verifyingKeys: account?.data?.admin?.verifyingKeys - })) + async registerAccount (account: EntropyAccountConfig, registerParams?: AccountRegisterParams): Promise { + this.logger.debug( + 'about to register selectedAccount.address' + + account.address + 'keyring:' + + // @ts-expect-error Type export of ChildKey still not available from SDK + this.entropy.keyring.getLazyLoadAccountProxy('registration').pair.address, + 'REGISTER' + ) + // Register params to be defined from user input (arguments/options or inquirer prompts) + try { + const verifyingKey = await this.register(registerParams) + + account?.data?.registration?.verifyingKeys?.push(verifyingKey) + return account + } catch (error) { + this.logger.error('There was a problem registering', error) + throw error + } + } } diff --git a/src/account/types.ts b/src/account/types.ts index e5a833a4..a9727bc2 100644 --- a/src/account/types.ts +++ b/src/account/types.ts @@ -1,6 +1,6 @@ export interface AccountCreateParams { name: string - seed: string + seed?: string path?: string } diff --git a/src/account/utils.ts b/src/account/utils.ts index 605ce7ab..c9de44e7 100644 --- a/src/account/utils.ts +++ b/src/account/utils.ts @@ -1,8 +1,7 @@ -import { EntropyAccountConfig, EntropyConfig } from "../config/types"; -import { RegisterParams } from './types'; +import { EntropyAccountConfig } from "../config/types"; +import { AccountListResults } from './types'; import { ACCOUNTS_CONTENT } from './constants'; -import { generateAccountChoices, print } from 'src/common/utils'; -import Entropy from '@entropyxyz/sdk'; +import { generateAccountChoices } from 'src/common/utils'; const validateSeedInput = (seed) => { if (seed.includes('#debug')) return true @@ -59,27 +58,10 @@ export const manageAccountsQuestions = [ } ] -export async function registerAccount (entropy: Entropy, params?: RegisterParams): Promise { - let verifyingKey: string - try { - const registerParams = params?.programModAddress && params?.programData ? { programDeployer: params.programModAddress, programData: params.programData } : undefined - - verifyingKey = await entropy.register(registerParams) - return verifyingKey - } catch (error) { - if (!verifyingKey) { - try { - const tx = entropy.substrate.tx.registry.pruneRegistration() - await tx.signAndSend(entropy.keyring.accounts.registration.pair, ({ status }) => { - if (status.isFinalized) { - print('Successfully pruned registration'); - } - }) - } catch (error) { - console.error('Unable to prune registration due to:', error.message); - throw error - } - } - throw error - } -} +export function formatAccountsList (accounts: EntropyAccountConfig[]): AccountListResults[] { + return accounts.map((account: EntropyAccountConfig) => ({ + name: account.name, + address: account.address, + verifyingKeys: account?.data?.admin?.verifyingKeys + })) +} \ No newline at end of file diff --git a/src/balance/command.ts b/src/balance/command.ts index 06c576d2..8056ef85 100644 --- a/src/balance/command.ts +++ b/src/balance/command.ts @@ -1,11 +1,11 @@ import Entropy from "@entropyxyz/sdk" -import { BaseCommand } from "../common/entropy-base" +import { EntropyBase } from "../common/entropy-base" import * as BalanceUtils from "./utils" const FLOW_CONTEXT = 'ENTROPY-BALANCE' -export class BalanceCommand extends BaseCommand { +export class BalanceCommand extends EntropyBase { constructor (entropy: Entropy, endpoint: string) { - super(entropy, endpoint, FLOW_CONTEXT) + super({ entropy, endpoint, flowContext: FLOW_CONTEXT }) } public async getBalance (address: string) { diff --git a/src/cli.ts b/src/cli.ts index c2b25c4f..faca5508 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -3,15 +3,13 @@ /* NOTE: calling this file entropy.ts helps commander parse process.argv */ import { Command, Option } from 'commander' import launchTui from './tui' -import * as config from './config' import { EntropyTuiOptions } from './types' import { cliSign } from './flows/sign/cli' -import { getSelectedAccount, stringify, updateConfig } from './common/utils' +import { stringify } from './common/utils' import { endpointOption, currentAccountAddressOption, loadEntropy, passwordOption } from './common/utils-cli' import Entropy from '@entropyxyz/sdk' import { entropyAccountCommand } from './account/command' -import { EntropyAccount } from './account/main' import { BalanceCommand } from './balance/command' import { TransferCommand } from './transfer/command' @@ -63,41 +61,6 @@ program entropyAccountCommand(entropy, program) -/* register */ -program.command('register') - .description('Register an entropy account with a program') - .argument('address', 'Address of existing entropy account') - .addOption(passwordOption()) - .addOption(endpointOption()) - .addOption( - new Option( - '-pointer, --pointer', - 'Program pointer of program to be used for registering' - ) - ) - .addOption( - new Option( - '-data, --program-data', - 'Path to file containing program data in JSON format' - ) - ) - .action(async (address, opts) => { - const storedConfig = await config.get() - const { accounts } = storedConfig - const accountsCommand = new EntropyAccount(entropy, opts.endpoint) - writeOut('Attempting to register account with addtess: ' + address) - const accountToRegister = getSelectedAccount(accounts, address) - if (!accountToRegister) { - throw new Error('AccountError: Unable to register non-existent account') - } - const updatedAccount = await accountsCommand.registerAccount(accountToRegister) - const arrIdx = accounts.indexOf(accountToRegister) - accounts.splice(arrIdx, 1, updatedAccount) - await updateConfig(storedConfig, { accounts, selectedAccount: updatedAccount.address }) - writeOut("Your address" + updatedAccount.address + "has been successfully registered.") - process.exit(0) - }) - /* balance */ program.command('balance') .description('Get the balance of an Entropy account. Output is a number') diff --git a/src/common/entropy-base.ts b/src/common/entropy-base.ts index 1e14bf3b..d2fd98c1 100644 --- a/src/common/entropy-base.ts +++ b/src/common/entropy-base.ts @@ -5,7 +5,7 @@ export abstract class EntropyBase { protected logger: EntropyLogger protected entropy: Entropy - constructor (entropy: Entropy, endpoint: string, flowContext: string) { + constructor ({ entropy, endpoint, flowContext }: { entropy?: Entropy, endpoint: string, flowContext: string }) { this.logger = new EntropyLogger(flowContext, endpoint) this.entropy = entropy } diff --git a/src/transfer/command.ts b/src/transfer/command.ts index 0fe1277a..8747f0ed 100644 --- a/src/transfer/command.ts +++ b/src/transfer/command.ts @@ -1,5 +1,5 @@ import Entropy from "@entropyxyz/sdk"; -import { BaseCommand } from "../common/entropy-base"; +import { EntropyBase } from "../common/entropy-base"; import { setupProgress } from "../common/progress"; import * as TransferUtils from './utils' import inquirer from "inquirer"; @@ -25,9 +25,9 @@ const question = [ }, ] -export class TransferCommand extends BaseCommand { +export class TransferCommand extends EntropyBase { constructor (entropy: Entropy, endpoint: string) { - super(entropy, endpoint, FLOW_CONTEXT) + super({ entropy, endpoint, flowContext: FLOW_CONTEXT }) } public async askQuestions () { diff --git a/src/tui.ts b/src/tui.ts index 99b787fb..24c436c0 100644 --- a/src/tui.ts +++ b/src/tui.ts @@ -4,12 +4,12 @@ import * as config from './config' import * as flows from './flows' import { EntropyTuiOptions } from './types' import { logo } from './common/ascii' -import { getSelectedAccount, print, updateConfig } from './common/utils' +import { print, updateConfig } from './common/utils' import { loadEntropy } from './common/utils-cli' import { EntropyLogger } from './common/logger' import { BalanceCommand } from './balance/command' -import { EntropyAccount } from './account/main' import { TransferCommand } from './transfer/command' +import { entropyManageAccounts, entropyRegister } from './account/interaction' let shouldInit = true @@ -51,7 +51,6 @@ async function main (entropy: Entropy, choices, options, logger: EntropyLogger) shouldInit = false } const balanceCommand = new BalanceCommand(entropy, options.endpoint) - const accountsCommand = new EntropyAccount(entropy, options.endpoint) const transferCommand = new TransferCommand(entropy, options.endpoint) let storedConfig = await config.get() @@ -94,24 +93,14 @@ async function main (entropy: Entropy, choices, options, logger: EntropyLogger) break; } case 'Manage Accounts': { - const response = await accountsCommand.runInteraction(storedConfig) + const response = await entropyManageAccounts(options.endpoint, storedConfig) returnToMain = await updateConfig(storedConfig, response) storedConfig = await config.get() break; } case 'Register': { - const { accounts, selectedAccount } = storedConfig - const currentAccount = getSelectedAccount(accounts, selectedAccount) - if (!currentAccount) { - print("No account selected to register") - break; - } - print("Attempting to register the address:", currentAccount.address) - const updatedAccount = await accountsCommand.registerAccount(currentAccount) - const arrIdx = accounts.indexOf(currentAccount) - accounts.splice(arrIdx, 1, updatedAccount) - print("Your address", updatedAccount.address, "has been successfully registered.") - returnToMain = await updateConfig(storedConfig, { accounts, selectedAccount: updatedAccount.address }) + const { accounts, selectedAccount } = await entropyRegister(entropy, options.endpoint, storedConfig) + returnToMain = await updateConfig(storedConfig, { accounts, selectedAccount }) storedConfig = await config.get() break; } diff --git a/tests/manage-accounts.test.ts b/tests/manage-accounts.test.ts index 479c4cbe..9e8e060e 100644 --- a/tests/manage-accounts.test.ts +++ b/tests/manage-accounts.test.ts @@ -5,12 +5,15 @@ import { isValidSubstrateAddress } from '@entropyxyz/sdk/utils' // @ts-ignore import Keyring from '@entropyxyz/sdk/keys' import { randomAsHex } from '@polkadot/util-crypto' +import { EntropyAccount } from '../src/account/main' import { EntropyAccountConfig, EntropyConfig } from '../src/config/types' -import { createAccount, listAccounts } from '../src/accounts/utils' import * as config from '../src/config' import { promiseRunner, sleep } from './testing-utils' import { charlieStashAddress, charlieStashSeed } from './testing-utils/constants' +const endpoint = "ws://127.0.0.1:9944" +const AccountService = new EntropyAccount({ endpoint }); + test('List Accounts', async t => { const account: EntropyAccountConfig = { name: 'Test Config', @@ -31,10 +34,11 @@ test('List Accounts', async t => { dev: 'ws://127.0.0.1:9944', 'test-net': 'wss://testnet.entropy.xyz', }, + selectedAccount: account.address, 'migration-version': '0' } - const accountsArray = listAccounts(config) + const accountsArray = AccountService.list(config) t.deepEqual(accountsArray, [{ name: account.name, @@ -45,10 +49,10 @@ test('List Accounts', async t => { // Resetting accounts on config to test for empty list config.accounts = [] try { - listAccounts(config) + AccountService.list(config) } catch (error) { const msg = error.message - t.equal(msg, 'There are currently no accounts available, please create or import your new account using the Manage Accounts feature') + t.equal(msg, 'AccountsError: There are currently no accounts available, please create or import your new account using the Manage Accounts feature') } t.end() @@ -63,7 +67,7 @@ test('Create Account', async t => { await run('config.init', config.init(configPath)) const testAccountSeed = randomAsHex(32) const testAccountName = 'Test Account' - const newAccount = await createAccount({ name: testAccountName, seed: testAccountSeed }) + const newAccount = await AccountService.create({ name: testAccountName, seed: testAccountSeed }) const testKeyring = new Keyring({ seed: testAccountSeed, path: 'none', debug: true }) const { admin } = testKeyring.getAccount() From 6cd2e9f9d9527eb77077a0ed0327800c57d82c4a Mon Sep 17 00:00:00 2001 From: Nayyir Jutha Date: Wed, 28 Aug 2024 19:09:56 -0400 Subject: [PATCH 13/28] Update main.ts --- src/account/main.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/account/main.ts b/src/account/main.ts index 270b575f..606457a7 100644 --- a/src/account/main.ts +++ b/src/account/main.ts @@ -44,7 +44,7 @@ export class EntropyAccount extends EntropyBase { : [] if (!accountsArray.length) throw new Error( - 'Accounts Error: There are currently no accounts available, please create or import a new account' + 'AccountsError: There are currently no accounts available, please create or import a new account using the Manage Accounts feature' ) return formatAccountsList(accountsArray) } From b5af2310e1602077a0901580f92948c08896a8a8 Mon Sep 17 00:00:00 2001 From: Nayyir Jutha Date: Wed, 28 Aug 2024 19:13:22 -0400 Subject: [PATCH 14/28] Update manage-accounts.test.ts --- tests/manage-accounts.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/manage-accounts.test.ts b/tests/manage-accounts.test.ts index 9e8e060e..a8b44bf6 100644 --- a/tests/manage-accounts.test.ts +++ b/tests/manage-accounts.test.ts @@ -52,7 +52,7 @@ test('List Accounts', async t => { AccountService.list(config) } catch (error) { const msg = error.message - t.equal(msg, 'AccountsError: There are currently no accounts available, please create or import your new account using the Manage Accounts feature') + t.equal(msg, 'AccountsError: There are currently no accounts available, please create or import a new account using the Manage Accounts feature') } t.end() From 9cb1b818cc48e93e4586544c3a528fd8a038fe68 Mon Sep 17 00:00:00 2001 From: mixmix Date: Thu, 29 Aug 2024 14:27:16 +1200 Subject: [PATCH 15/28] fixups --- src/account/command.ts | 18 +++----- src/account/interaction.ts | 8 ++-- src/account/main.ts | 87 +++++++++++++++++++++----------------- src/account/types.ts | 7 ++- tests/register.test.ts | 9 ++-- 5 files changed, 70 insertions(+), 59 deletions(-) diff --git a/src/account/command.ts b/src/account/command.ts index 556d5da9..e3f351e9 100644 --- a/src/account/command.ts +++ b/src/account/command.ts @@ -3,7 +3,7 @@ import { Command, Option } from 'commander' import { EntropyAccount } from "./main"; import { ACCOUNTS_CONTENT } from './constants' import * as config from '../config' -import { cliWrite, endpointOption, passwordOption } from "../common/utils-cli"; +import { cliWrite, passwordOption } from "../common/utils-cli"; import { updateConfig } from "src/common/utils"; export async function entropyAccountCommand (entropy: Entropy, rootCommand: Command) { @@ -18,7 +18,6 @@ function entropyAccountNew (accountCommand: Command) { accountCommand.command('create') .alias('new') .description('Create a new entropy account from scratch. Output is JSON of form {name, address}') - .addOption(endpointOption()) .addOption(passwordOption()) .argument('', 'A user friendly name for your nem account.') .addOption( @@ -28,13 +27,8 @@ function entropyAccountNew (accountCommand: Command) { ).default(ACCOUNTS_CONTENT.path.default) ) .action(async (name, opts) => { - const { endpoint, path } = opts - - const service = new EntropyAccount({ endpoint }) - const newAccount = await service.create({ - name, - path - }) + const { path } = opts + const newAccount = EntropyAccount.create({ name, path }) const storedConfig = await config.get() const { accounts } = storedConfig @@ -58,12 +52,10 @@ function entropyAccountList (accountCommand: Command) { accountCommand.command('list') .alias('ls') .description('List all accounts. Output is JSON of form [{ name, address, verifyingKeys }]') - .addOption(endpointOption()) - .action(async (options) => { + .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 storedConfig = await config.get() - const service = new EntropyAccount({ endpoint: options.endpoint }) - const accounts = service.list(storedConfig.accounts) + const accounts = EntropyAccount.list(storedConfig.accounts) cliWrite(accounts) process.exit(0) }) diff --git a/src/account/interaction.ts b/src/account/interaction.ts index f1f27b60..512ebb8e 100644 --- a/src/account/interaction.ts +++ b/src/account/interaction.ts @@ -13,7 +13,6 @@ import { export async function entropyManageAccounts (endpoint: string, storedConfig: EntropyConfig) { - const AccountService = new EntropyAccount({ endpoint }) const { accounts } = storedConfig const { interactionChoice } = await inquirer.prompt(manageAccountsQuestions) switch (interactionChoice) { @@ -25,8 +24,11 @@ export async function entropyManageAccounts (endpoint: string, storedConfig: Ent // isDebugMode = true seed = seed.split('#debug')[0] } - const newAccount = await AccountService.create({ seed, name, path }) + const newAccount = seed + ? EntropyAccount.import({ seed, name, path }) + : EntropyAccount.create({ name, path }) accounts.push(newAccount) + return { accounts, selectedAccount: newAccount.address @@ -55,7 +57,7 @@ export async function entropyManageAccounts (endpoint: string, storedConfig: Ent } export async function entropyRegister (entropy: Entropy, endpoint: string, storedConfig: EntropyConfig): Promise> { - const AccountService = new EntropyAccount({ entropy, endpoint }) + const AccountService = new EntropyAccount(entropy, endpoint) const { accounts, selectedAccount } = storedConfig const currentAccount = getSelectedAccount(accounts, selectedAccount) diff --git a/src/account/main.ts b/src/account/main.ts index 606457a7..102cdbd1 100644 --- a/src/account/main.ts +++ b/src/account/main.ts @@ -3,24 +3,25 @@ import Entropy from "@entropyxyz/sdk"; import Keyring from '@entropyxyz/sdk/keys' import { randomAsHex } from '@polkadot/util-crypto' -import { EntropyBase } from "../common/entropy-base"; -import { EntropyAccountConfig } from "../config/types"; - import { FLOW_CONTEXT } from "./constants"; -import { AccountCreateParams, AccountRegisterParams } from "./types"; +import { AccountCreateParams, AccountImportParams, AccountRegisterParams } from "./types"; import { print } from "src/common/utils"; import { formatAccountsList } from "./utils"; +import { EntropyBase } from "../common/entropy-base"; +import { EntropyAccountConfig } from "../config/types"; + export class EntropyAccount extends EntropyBase { - // Entropy does not need to be required, as only register needs it - // Idea: We could use entropy as an argument for the register method, - // the base class has been updated to optionally require entropy in the - // constructor. - constructor ({ entropy, endpoint }: { entropy?: Entropy, endpoint: string }) { + constructor (entropy: Entropy, endpoint: string) { super({ entropy, endpoint, flowContext: FLOW_CONTEXT }) } - async create ({ seed = randomAsHex(32), name, path }: AccountCreateParams): Promise { + static create ({ name, path }: AccountCreateParams): EntropyAccountConfig { + const seed = randomAsHex(32) + return EntropyAccount.import({ name, seed, path }) + } + + static import ({ name, seed, path }: AccountImportParams ): EntropyAccountConfig { const keyring = new Keyring({ seed, path, debug: true }) const fullAccount = keyring.getAccount() // TODO: sdk should create account on constructor @@ -31,14 +32,14 @@ export class EntropyAccount extends EntropyBase { // const encryptedData = password ? passwordFlow.encrypt(data, password) : data return { - name: name, + name, address: admin.address, data // data: encryptedData // TODO: replace once password input is added back } } - list ({ accounts }: { accounts: EntropyAccountConfig[] }) { + static list (accounts: EntropyAccountConfig[]) { const accountsArray = Array.isArray(accounts) && accounts.length ? accounts : [] @@ -49,44 +50,36 @@ export class EntropyAccount extends EntropyBase { return formatAccountsList(accountsArray) } - private async register (params?: AccountRegisterParams): Promise { + async register (params?: AccountRegisterParams): Promise { const { programModAddress, programData } = params - let verifyingKey: string - try { - const registerParams = programModAddress && programData ? { programDeployer: programModAddress, programData } : undefined - - verifyingKey = await this.entropy.register(registerParams) - return verifyingKey - } catch (error) { - if (!verifyingKey) { - try { - const tx = this.entropy.substrate.tx.registry.pruneRegistration() - await tx.signAndSend(this.entropy.keyring.accounts.registration.pair, ({ status }) => { - if (status.isFinalized) { - print('Successfully pruned registration'); - } - }) - } catch (error) { - console.error('Unable to prune registration due to:', error.message); - throw error - } + const registerParams = programModAddress && programData + ? { + programDeployer: programModAddress, + programData } - throw error - } + : undefined + + return this.entropy.register(registerParams) + .catch(async error => { + await this.pruneRegistration() + throw error + }) } + // WATCH: should this be extracted to interaction.ts? async registerAccount (account: EntropyAccountConfig, registerParams?: AccountRegisterParams): Promise { this.logger.debug( - 'about to register selectedAccount.address' + - account.address + 'keyring:' + - // @ts-expect-error Type export of ChildKey still not available from SDK - this.entropy.keyring.getLazyLoadAccountProxy('registration').pair.address, + [ + `registering account: ${account.address}`, + // @ts-expect-error Type export of ChildKey still not available from SDK + `to keyring: ${this.entropy.keyring.getLazyLoadAccountProxy('registration').pair.address}` + ].join(', '), 'REGISTER' ) // Register params to be defined from user input (arguments/options or inquirer prompts) try { const verifyingKey = await this.register(registerParams) - + account?.data?.registration?.verifyingKeys?.push(verifyingKey) return account } catch (error) { @@ -94,4 +87,20 @@ export class EntropyAccount extends EntropyBase { throw error } } + + /* PRIVATE */ + + private async pruneRegistration () { + try { + const tx = this.entropy.substrate.tx.registry.pruneRegistration() + await tx.signAndSend(this.entropy.keyring.accounts.registration.pair, ({ status }) => { + if (status.isFinalized) { + print('Successfully pruned registration'); + } + }) + } catch (error) { + console.error('Unable to prune registration due to:', error.message); + throw error + } + } } diff --git a/src/account/types.ts b/src/account/types.ts index a9727bc2..f1d1cf30 100644 --- a/src/account/types.ts +++ b/src/account/types.ts @@ -1,6 +1,11 @@ export interface AccountCreateParams { name: string - seed?: string + path?: string +} + +export interface AccountImportParams { + seed: string + name: string path?: string } diff --git a/tests/register.test.ts b/tests/register.test.ts index 67167a0d..914e90f1 100644 --- a/tests/register.test.ts +++ b/tests/register.test.ts @@ -1,14 +1,16 @@ import test from 'tape' -import { registerAccount } from '../src/accounts/utils' +import { EntropyAccount } from '../src/account/main' import { charlieStashSeed, setupTest } from './testing-utils' import { readFileSync } from 'node:fs' const networkType = 'two-nodes' +const endpoint = 'ws://127.0.0.1:9944' test('Register - Default Program', async (t) => { const { run, entropy } = await setupTest(t, { networkType, seed: charlieStashSeed }) + const accountService = new EntropyAccount(entropy, endpoint) - const verifyingKey = await run('register account', registerAccount(entropy)) + const verifyingKey = await run('register account', accountService.register()) const fullAccount = entropy.keyring.getAccount() @@ -27,9 +29,10 @@ test('Register - Barebones Program', async t => { entropy.programs.dev.deploy(dummyProgram) ) + const accountService = new EntropyAccount(entropy, endpoint) const verifyingKey = await run( 'register - using custom params', - registerAccount(entropy, { + accountService.register({ programModAddress: entropy.keyring.accounts.registration.address, programData: [{ program_pointer: pointer, program_config: '0x' }], }) From dbbb777a3af6e06c7c639232b8008c561ab86b2c Mon Sep 17 00:00:00 2001 From: mixmix Date: Thu, 29 Aug 2024 16:03:37 +1200 Subject: [PATCH 16/28] WIP --- src/account/command.ts | 59 ++++++++++++++++++++++++++++++-------- src/account/interaction.ts | 1 + src/account/main.ts | 24 +++++++++------- src/account/utils.ts | 8 ------ src/cli.ts | 11 +++++-- src/config/encoding.ts | 1 + 6 files changed, 72 insertions(+), 32 deletions(-) diff --git a/src/account/command.ts b/src/account/command.ts index e3f351e9..85a5a84c 100644 --- a/src/account/command.ts +++ b/src/account/command.ts @@ -1,4 +1,4 @@ -import Entropy from "@entropyxyz/sdk"; +import Entropy from "@entropyxyz/sdk" import { Command, Option } from 'commander' import { EntropyAccount } from "./main"; import { ACCOUNTS_CONTENT } from './constants' @@ -10,16 +10,17 @@ export async function entropyAccountCommand (entropy: Entropy, rootCommand: Comm const accountCommand = rootCommand.command('account') .description('Commands to work with accounts on the Entropy Network') + entropyAccountCreate(accountCommand) + entropyAccountImport(accountCommand) entropyAccountList(accountCommand) - entropyAccountNew(accountCommand) } -function entropyAccountNew (accountCommand: Command) { +function entropyAccountCreate (accountCommand: Command) { accountCommand.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 nem account.') + .argument('', 'A user friendly name for your new account.') .addOption( new Option( '--path', @@ -28,16 +29,35 @@ function entropyAccountNew (accountCommand: Command) { ) .action(async (name, opts) => { const { path } = opts - const newAccount = EntropyAccount.create({ name, path }) + const newAccount = await EntropyAccount.create({ name, path }) - const storedConfig = await config.get() - const { accounts } = storedConfig - accounts.push(newAccount) - // WIP - sort out the updateConfig stuff - await updateConfig(storedConfig, { - accounts, - selectedAccount: newAccount.address + await persistAndSelectNewAccount(newAccount) + + cliWrite({ + name: newAccount.name, + address: newAccount.address }) + process.exit(0) + }) +} + +function entropyAccountImport (accountCommand: Command) { + accountCommand.command('import') + .description('Import an existing entropy account from seed. Output is JSON of form {name, address}') + .addOption(passwordOption()) + .argument('', 'A user friendly name for your new account.') + .argument('', 'The seed for the account you are importing') + .addOption( + new Option( + '--path', + 'Derivation path' + ).default(ACCOUNTS_CONTENT.path.default) + ) + .action(async (name, seed, opts) => { + const { path } = opts + const newAccount = await EntropyAccount.import({ name, seed, path }) + + await persistAndSelectNewAccount(newAccount) cliWrite({ name: newAccount.name, @@ -45,7 +65,22 @@ function entropyAccountNew (accountCommand: Command) { }) process.exit(0) }) +} + +async function persistAndSelectNewAccount (newAccount) { + const storedConfig = await config.get() + const { accounts } = storedConfig + + const isExistingName = accounts.find(account => account.name === newAccount.name) + if (isExistingName) { + throw Error(`An account with name "${newAccount.name}" already exists. Choose a different name`) + } + accounts.push(newAccount) + await updateConfig(storedConfig, { + accounts, + selectedAccount: newAccount.address + }) } function entropyAccountList (accountCommand: Command) { diff --git a/src/account/interaction.ts b/src/account/interaction.ts index 512ebb8e..2bfcb3f5 100644 --- a/src/account/interaction.ts +++ b/src/account/interaction.ts @@ -29,6 +29,7 @@ export async function entropyManageAccounts (endpoint: string, storedConfig: Ent : EntropyAccount.create({ name, path }) accounts.push(newAccount) + // WIP HERE return { accounts, selectedAccount: newAccount.address diff --git a/src/account/main.ts b/src/account/main.ts index 102cdbd1..f55aa6f1 100644 --- a/src/account/main.ts +++ b/src/account/main.ts @@ -1,4 +1,4 @@ -import Entropy from "@entropyxyz/sdk"; +import Entropy, { wasmGlobalsReady } from "@entropyxyz/sdk"; // @ts-expect-error import Keyring from '@entropyxyz/sdk/keys' import { randomAsHex } from '@polkadot/util-crypto' @@ -6,7 +6,6 @@ import { randomAsHex } from '@polkadot/util-crypto' import { FLOW_CONTEXT } from "./constants"; import { AccountCreateParams, AccountImportParams, AccountRegisterParams } from "./types"; import { print } from "src/common/utils"; -import { formatAccountsList } from "./utils"; import { EntropyBase } from "../common/entropy-base"; import { EntropyAccountConfig } from "../config/types"; @@ -16,12 +15,15 @@ export class EntropyAccount extends EntropyBase { super({ entropy, endpoint, flowContext: FLOW_CONTEXT }) } - static create ({ name, path }: AccountCreateParams): EntropyAccountConfig { + static async create ({ name, path }: AccountCreateParams): Promise { const seed = randomAsHex(32) return EntropyAccount.import({ name, seed, path }) } - static import ({ name, seed, path }: AccountImportParams ): EntropyAccountConfig { + static async import ({ name, seed, path }: AccountImportParams ): Promise { + // WARNING: #create currently depends on this => be careful modifying this function + + await wasmGlobalsReady() const keyring = new Keyring({ seed, path, debug: true }) const fullAccount = keyring.getAccount() // TODO: sdk should create account on constructor @@ -30,7 +32,7 @@ export class EntropyAccount extends EntropyBase { const data = fullAccount delete admin.pair // const encryptedData = password ? passwordFlow.encrypt(data, password) : data - + return { name, address: admin.address, @@ -40,14 +42,16 @@ export class EntropyAccount extends EntropyBase { } static list (accounts: EntropyAccountConfig[]) { - const accountsArray = Array.isArray(accounts) && accounts.length - ? accounts - : [] - if (!accountsArray.length) + if (!accounts.length) throw new Error( 'AccountsError: There are currently no accounts available, please create or import a new account using the Manage Accounts feature' ) - return formatAccountsList(accountsArray) + + return accounts.map((account: EntropyAccountConfig) => ({ + name: account.name, + address: account.address, + verifyingKeys: account?.data?.admin?.verifyingKeys + })) } async register (params?: AccountRegisterParams): Promise { diff --git a/src/account/utils.ts b/src/account/utils.ts index d9e737be..ba6cdc4a 100644 --- a/src/account/utils.ts +++ b/src/account/utils.ts @@ -57,11 +57,3 @@ export const manageAccountsQuestions = [ choices: ACCOUNTS_CONTENT.interactionChoice.choices } ] - -export function formatAccountsList (accounts: EntropyAccountConfig[]): AccountListResults[] { - return accounts.map((account: EntropyAccountConfig) => ({ - name: account.name, - address: account.address, - verifyingKeys: account?.data?.admin?.verifyingKeys - })) -} diff --git a/src/cli.ts b/src/cli.ts index b64d8082..526bd804 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -16,7 +16,7 @@ async function setEntropyGlobal (address: string, endpoint: string, password?: s if (entropy) { const currentAddress = entropy?.keyring?.accounts?.registration?.address if (address !== currentAddress) { - // Is it possible to hit this? + // QUESTION: Is it possible to hit this? // - programmatic usage kills process after function call // - tui usage manages mutation of entropy instance itself await entropy.close() @@ -29,6 +29,7 @@ async function setEntropyGlobal (address: string, endpoint: string, password?: s } const program = new Command() +let commandName: string // the top level command /* no command */ program @@ -44,9 +45,15 @@ program .env('DEV_MODE') .hideHelp() ) + .hook('preSubcommand', async (_thisCommand, subCommand) => { + commandName = subCommand.name() + }) .hook('preAction', async (_thisCommand, actionCommand) => { + if (commandName === 'account') return + // entropy not required for any account commands + const { account, endpoint, password } = actionCommand.opts() - const address = actionCommand.name() === 'balance' + const address = commandName === 'balance' ? actionCommand.args[0] : account diff --git a/src/config/encoding.ts b/src/config/encoding.ts index a41da0cc..1ce81fcc 100644 --- a/src/config/encoding.ts +++ b/src/config/encoding.ts @@ -10,6 +10,7 @@ export function deserialize (config) { } function replacer (key, value) { + console.log(key, typeof value, value instanceof Uint8Array) if (value instanceof Uint8Array) { return PREFIX + Buffer.from(value).toString('base64') } From 378fffa897ea923edbd41ea3f9eca58acf4f1f3b Mon Sep 17 00:00:00 2001 From: mixmix Date: Thu, 29 Aug 2024 16:14:38 +1200 Subject: [PATCH 17/28] fixups --- src/account/interaction.ts | 5 ++--- tests/transfer.test.ts | 1 - 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/account/interaction.ts b/src/account/interaction.ts index 2bfcb3f5..9ecffb5b 100644 --- a/src/account/interaction.ts +++ b/src/account/interaction.ts @@ -25,11 +25,10 @@ export async function entropyManageAccounts (endpoint: string, storedConfig: Ent seed = seed.split('#debug')[0] } const newAccount = seed - ? EntropyAccount.import({ seed, name, path }) - : EntropyAccount.create({ name, path }) + ? await EntropyAccount.import({ seed, name, path }) + : await EntropyAccount.create({ name, path }) accounts.push(newAccount) - // WIP HERE return { accounts, selectedAccount: newAccount.address diff --git a/tests/transfer.test.ts b/tests/transfer.test.ts index b744a684..a94567c4 100644 --- a/tests/transfer.test.ts +++ b/tests/transfer.test.ts @@ -1,6 +1,5 @@ import test from 'tape' import { wasmGlobalsReady } from '@entropyxyz/sdk' -// WIP: I'm seeing problems importing this? // @ts-ignore import Keyring from '@entropyxyz/sdk/keys' import { From d971d4df2b93dbe566d1c16fc652786fde48880a Mon Sep 17 00:00:00 2001 From: mixmix Date: Thu, 29 Aug 2024 16:49:25 +1200 Subject: [PATCH 18/28] compleeeete --- src/account/command.ts | 2 +- src/account/interaction.ts | 14 ++++++++++---- src/account/main.ts | 2 +- tests/{manage-accounts.test.ts => account.test.ts} | 14 ++++++-------- 4 files changed, 18 insertions(+), 14 deletions(-) rename tests/{manage-accounts.test.ts => account.test.ts} (87%) diff --git a/src/account/command.ts b/src/account/command.ts index 85a5a84c..701bb163 100644 --- a/src/account/command.ts +++ b/src/account/command.ts @@ -90,7 +90,7 @@ function entropyAccountList (accountCommand: Command) { .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 storedConfig = await config.get() - const accounts = EntropyAccount.list(storedConfig.accounts) + const accounts = EntropyAccount.list(storedConfig) cliWrite(accounts) process.exit(0) }) diff --git a/src/account/interaction.ts b/src/account/interaction.ts index 9ecffb5b..26e3cd44 100644 --- a/src/account/interaction.ts +++ b/src/account/interaction.ts @@ -2,7 +2,7 @@ import inquirer from "inquirer"; import Entropy from "@entropyxyz/sdk"; import { getSelectedAccount, print } from "../common/utils" -import { EntropyAccountConfig, EntropyConfig } from "../config/types"; +import { EntropyConfig } from "../config/types"; import { EntropyAccount } from './main' import { @@ -15,7 +15,9 @@ import { export async function entropyManageAccounts (endpoint: string, storedConfig: EntropyConfig) { const { accounts } = storedConfig const { interactionChoice } = await inquirer.prompt(manageAccountsQuestions) + switch (interactionChoice) { + case 'create-import': { const answers = await inquirer.prompt(newAccountQuestions) const { name, path, importKey } = answers @@ -34,23 +36,27 @@ export async function entropyManageAccounts (endpoint: string, storedConfig: Ent selectedAccount: newAccount.address } } + case 'select-account': { const { selectedAccount } = await inquirer.prompt(selectAccountQuestions(accounts)) - print('Current selected account is ' + selectedAccount) + return { accounts: storedConfig.accounts, selectedAccount: selectedAccount.address } } + case 'list-account': { - const list = this.list(accounts) - list?.forEach((account: EntropyAccountConfig)=> print(account)) + EntropyAccount.list({ accounts }) + .forEach((account) => print(account)) return } + case 'exit': { return 'exit' } + default: throw new Error('AccountsError: Unknown interaction action') } diff --git a/src/account/main.ts b/src/account/main.ts index f55aa6f1..b4ffacd0 100644 --- a/src/account/main.ts +++ b/src/account/main.ts @@ -41,7 +41,7 @@ export class EntropyAccount extends EntropyBase { } } - static list (accounts: EntropyAccountConfig[]) { + static list ({ accounts }: { accounts: EntropyAccountConfig[] }) { if (!accounts.length) throw new Error( 'AccountsError: There are currently no accounts available, please create or import a new account using the Manage Accounts feature' diff --git a/tests/manage-accounts.test.ts b/tests/account.test.ts similarity index 87% rename from tests/manage-accounts.test.ts rename to tests/account.test.ts index a8b44bf6..b78ff13c 100644 --- a/tests/manage-accounts.test.ts +++ b/tests/account.test.ts @@ -11,10 +11,7 @@ import * as config from '../src/config' import { promiseRunner, sleep } from './testing-utils' import { charlieStashAddress, charlieStashSeed } from './testing-utils/constants' -const endpoint = "ws://127.0.0.1:9944" -const AccountService = new EntropyAccount({ endpoint }); - -test('List Accounts', async t => { +test('Account - list', async t => { const account: EntropyAccountConfig = { name: 'Test Config', address: charlieStashAddress, @@ -38,7 +35,7 @@ test('List Accounts', async t => { 'migration-version': '0' } - const accountsArray = AccountService.list(config) + const accountsArray = EntropyAccount.list(config) t.deepEqual(accountsArray, [{ name: account.name, @@ -49,7 +46,7 @@ test('List Accounts', async t => { // Resetting accounts on config to test for empty list config.accounts = [] try { - AccountService.list(config) + EntropyAccount.list(config) } catch (error) { const msg = error.message t.equal(msg, 'AccountsError: There are currently no accounts available, please create or import a new account using the Manage Accounts feature') @@ -59,15 +56,16 @@ test('List Accounts', async t => { }) let counter = 0 -test('Create Account', async t => { + +test('Account - create', async t => { const configPath = `/tmp/entropy-cli-${Date.now()}_${counter++}.json` /* Setup */ const run = promiseRunner(t) await run('wasm', wasmGlobalsReady()) await run('config.init', config.init(configPath)) const testAccountSeed = randomAsHex(32) + const newAccount = await EntropyAccount.import({ name: testAccountName, seed: testAccountSeed }) const testAccountName = 'Test Account' - const newAccount = await AccountService.create({ name: testAccountName, seed: testAccountSeed }) const testKeyring = new Keyring({ seed: testAccountSeed, path: 'none', debug: true }) const { admin } = testKeyring.getAccount() From 50ab1e1cd3610bd68dfea34d64859e338c17432f Mon Sep 17 00:00:00 2001 From: mixmix Date: Thu, 29 Aug 2024 17:05:12 +1200 Subject: [PATCH 19/28] get working for fresh install. see WIP --- src/cli.ts | 9 +++++++-- src/common/utils-cli.ts | 2 ++ src/config/index.ts | 1 + src/tui.ts | 6 ------ 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index 526bd804..ea80c030 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -3,13 +3,16 @@ /* NOTE: calling this file entropy.ts helps commander parse process.argv */ import { Command, Option } from 'commander' import Entropy from '@entropyxyz/sdk' + +import * as config from './config' +import { EntropyTuiOptions } from './types' import { currentAccountAddressOption, endpointOption, loadEntropy } from './common/utils-cli' + +import launchTui from './tui' import { entropyAccountCommand } from './account/command' import { entropyTransferCommand } from './transfer/command' import { entropySignCommand } from './sign/command' import { entropyBalanceCommand } from './balance/command' -import { EntropyTuiOptions } from './types' -import launchTui from './tui' let entropy: Entropy async function setEntropyGlobal (address: string, endpoint: string, password?: string) { @@ -49,6 +52,8 @@ program commandName = subCommand.name() }) .hook('preAction', async (_thisCommand, actionCommand) => { + await config.init() + console.log({ commandName }) if (commandName === 'account') return // entropy not required for any account commands diff --git a/src/common/utils-cli.ts b/src/common/utils-cli.ts index e0536c84..65c49bbb 100644 --- a/src/common/utils-cli.ts +++ b/src/common/utils-cli.ts @@ -42,6 +42,8 @@ export function passwordOption (description?: string) { export function currentAccountAddressOption () { const storedConfig = config.getSync() + // WIP: this needs a try-catch which runs config.init if it's missing ... which may need a sync version + // TODO return new Option( '-a, --account
', 'Sets the current account for the session or defaults to the account stored in the config' diff --git a/src/config/index.ts b/src/config/index.ts index d5a7a145..9404af6e 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -35,6 +35,7 @@ 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) => { + console.log("error", err.code) if (err && err.code !== 'ENOENT') throw err const oldConfig = await get(oldConfigPath).catch(noop) // drop errors diff --git a/src/tui.ts b/src/tui.ts index a79525f4..5ec1ab4d 100644 --- a/src/tui.ts +++ b/src/tui.ts @@ -12,13 +12,7 @@ import { entropySign } from './sign/interaction' import { entropyBalance } from './balance/interaction' import { entropyTransfer } from './transfer/interaction' -let hasConfigInit = false async function setupConfig () { - if (!hasConfigInit) { - await config.init() - hasConfigInit = true - } - let storedConfig = await config.get() // set selectedAccount if we can From b013ce3e5625788e73ed433403a3a2464fff898a Mon Sep 17 00:00:00 2001 From: Nayyir Jutha Date: Thu, 29 Aug 2024 13:37:49 -0400 Subject: [PATCH 20/28] fixed fresh install and using tui, might have fixed cli not sure --- src/account/interaction.ts | 12 ++++++++++-- src/account/main.ts | 6 +++++- src/cli.ts | 5 +++-- src/config/encoding.ts | 3 +-- src/config/index.ts | 10 ++++++++-- src/tui.ts | 6 +++--- tests/account.test.ts | 2 +- 7 files changed, 31 insertions(+), 13 deletions(-) diff --git a/src/account/interaction.ts b/src/account/interaction.ts index 26e3cd44..a1686bd0 100644 --- a/src/account/interaction.ts +++ b/src/account/interaction.ts @@ -38,6 +38,10 @@ export async function entropyManageAccounts (endpoint: string, storedConfig: Ent } case 'select-account': { + if (!accounts.length) { + console.error('There are currently no accounts available, please create or import a new account using the Manage Accounts feature') + return + } const { selectedAccount } = await inquirer.prompt(selectAccountQuestions(accounts)) print('Current selected account is ' + selectedAccount) @@ -48,8 +52,12 @@ export async function entropyManageAccounts (endpoint: string, storedConfig: Ent } case 'list-account': { - EntropyAccount.list({ accounts }) - .forEach((account) => print(account)) + try { + EntropyAccount.list({ accounts }) + .forEach((account) => print(account)) + } catch (error) { + console.error(error.message.split('AccountsError: ')[1]) + } return } diff --git a/src/account/main.ts b/src/account/main.ts index b4ffacd0..3d7a4657 100644 --- a/src/account/main.ts +++ b/src/account/main.ts @@ -55,7 +55,11 @@ export class EntropyAccount extends EntropyBase { } async register (params?: AccountRegisterParams): Promise { - const { programModAddress, programData } = params + let programModAddress: string + let programData: any + if (params) { + ({ programModAddress, programData } = params) + } const registerParams = programModAddress && programData ? { programDeployer: programModAddress, diff --git a/src/cli.ts b/src/cli.ts index ea80c030..e7d7c9ff 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -26,8 +26,10 @@ async function setEntropyGlobal (address: string, endpoint: string, password?: s entropy = await loadEntropy(address, endpoint, password) } } - else { + else if (address && endpoint) { entropy = await loadEntropy(address, endpoint, password) + } else { + return } } @@ -53,7 +55,6 @@ program }) .hook('preAction', async (_thisCommand, actionCommand) => { await config.init() - console.log({ commandName }) if (commandName === 'account') return // entropy not required for any account commands diff --git a/src/config/encoding.ts b/src/config/encoding.ts index 1ce81fcc..cb580655 100644 --- a/src/config/encoding.ts +++ b/src/config/encoding.ts @@ -9,8 +9,7 @@ export function deserialize (config) { return JSON.parse(config, reviver) } -function replacer (key, value) { - console.log(key, typeof value, value instanceof Uint8Array) +function replacer (_key: string, value: any) { if (value instanceof Uint8Array) { return PREFIX + Buffer.from(value).toString('base64') } diff --git a/src/config/index.ts b/src/config/index.ts index 9404af6e..fa57e3bd 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 } from 'node:fs' +import { readFileSync, writeFileSync } from 'node:fs' import { mkdirp } from 'mkdirp' import { join, dirname } from 'path' import envPaths from 'env-paths' @@ -63,7 +63,13 @@ export async function get (configPath = CONFIG_PATH) { } export function getSync (configPath = CONFIG_PATH) { - const configBuffer = readFileSync(configPath, 'utf8') + let configBuffer + try { + configBuffer = readFileSync(configPath, 'utf8') + } catch (error) { + writeFileSync(configPath, '{}') + configBuffer = readFileSync(configPath, 'utf8') + } return deserialize(configBuffer) } diff --git a/src/tui.ts b/src/tui.ts index 5ec1ab4d..0fb24a77 100644 --- a/src/tui.ts +++ b/src/tui.ts @@ -61,10 +61,10 @@ export default function tui (entropy: Entropy, options: EntropyTuiOptions) { async function main (entropy: Entropy, choices, options, logger: EntropyLogger) { let storedConfig = await setupConfig() - + // If the selected account changes within the TUI we need to reset the entropy instance being used - const currentAccount = entropy.keyring.accounts.registration.address - if (currentAccount !== storedConfig.selectedAccount) { + const currentAccount = entropy?.keyring?.accounts?.registration?.address + if (currentAccount && currentAccount !== storedConfig.selectedAccount) { await entropy.close() entropy = await loadEntropy(storedConfig.selectedAccount, options.endpoint); } diff --git a/tests/account.test.ts b/tests/account.test.ts index b78ff13c..df397199 100644 --- a/tests/account.test.ts +++ b/tests/account.test.ts @@ -64,8 +64,8 @@ test('Account - create', async t => { await run('wasm', wasmGlobalsReady()) await run('config.init', config.init(configPath)) const testAccountSeed = randomAsHex(32) - const newAccount = await EntropyAccount.import({ name: testAccountName, seed: testAccountSeed }) const testAccountName = 'Test Account' + const newAccount = await EntropyAccount.import({ name: testAccountName, seed: testAccountSeed }) const testKeyring = new Keyring({ seed: testAccountSeed, path: 'none', debug: true }) const { admin } = testKeyring.getAccount() From 22726895f6a3c7fa0c9da78cb9d8ce25634097d4 Mon Sep 17 00:00:00 2001 From: Nayyir Jutha Date: Mon, 9 Sep 2024 15:33:00 -0400 Subject: [PATCH 21/28] updated initialization of entropy and formt of how we instantiate commands --- src/account/command.ts | 102 ++++++++++++++++++++++------------------ src/balance/command.ts | 10 ++-- src/cli.ts | 38 +++++++++------ src/sign/command.ts | 9 ++-- src/transfer/command.ts | 9 ++-- 5 files changed, 97 insertions(+), 71 deletions(-) diff --git a/src/account/command.ts b/src/account/command.ts index 701bb163..d8fc3512 100644 --- a/src/account/command.ts +++ b/src/account/command.ts @@ -3,20 +3,22 @@ import { Command, Option } from 'commander' import { EntropyAccount } from "./main"; import { ACCOUNTS_CONTENT } from './constants' import * as config from '../config' -import { cliWrite, passwordOption } from "../common/utils-cli"; -import { updateConfig } from "src/common/utils"; +import { cliWrite, currentAccountAddressOption, endpointOption, loadEntropy, passwordOption } from "../common/utils-cli"; +import { getSelectedAccount, updateConfig } from "src/common/utils"; -export async function entropyAccountCommand (entropy: Entropy, rootCommand: Command) { - const accountCommand = rootCommand.command('account') +export function entropyAccountCommand () { + const accountCommand = new Command('account') .description('Commands to work with accounts on the Entropy Network') + .addCommand(entropyAccountCreate()) + .addCommand(entropyAccountImport()) + .addCommand(entropyAccountList()) + .addCommand(entropyAccountRegister()) - entropyAccountCreate(accountCommand) - entropyAccountImport(accountCommand) - entropyAccountList(accountCommand) + return accountCommand } -function entropyAccountCreate (accountCommand: Command) { - accountCommand.command('create') +function entropyAccountCreate () { + const accountCreateCommand = new Command('create') .alias('new') .description('Create a new entropy account from scratch. Output is JSON of form {name, address}') .addOption(passwordOption()) @@ -39,10 +41,11 @@ function entropyAccountCreate (accountCommand: Command) { }) process.exit(0) }) + return accountCreateCommand } -function entropyAccountImport (accountCommand: Command) { - accountCommand.command('import') +function entropyAccountImport () { + const accountImportCommand = new Command('import') .description('Import an existing entropy account from seed. Output is JSON of form {name, address}') .addOption(passwordOption()) .argument('', 'A user friendly name for your new account.') @@ -65,6 +68,7 @@ function entropyAccountImport (accountCommand: Command) { }) process.exit(0) }) + return accountImportCommand } async function persistAndSelectNewAccount (newAccount) { @@ -83,8 +87,8 @@ async function persistAndSelectNewAccount (newAccount) { }) } -function entropyAccountList (accountCommand: Command) { - accountCommand.command('list') +function entropyAccountList () { + const accountListCommand = new Command('list') .alias('ls') .description('List all accounts. Output is JSON of form [{ name, address, verifyingKeys }]') .action(async () => { @@ -94,39 +98,45 @@ function entropyAccountList (accountCommand: Command) { cliWrite(accounts) process.exit(0) }) + return accountListCommand } /* register */ -// program.command('register') -// .description('Register an entropy account with a program') -// .argument('address', 'Address of existing entropy account') -// .addOption(passwordOption()) -// .addOption(endpointOption()) -// .addOption( -// new Option( -// '-pointer, --pointer', -// 'Program pointer of program to be used for registering' -// ) -// ) -// .addOption( -// new Option( -// '-data, --program-data', -// 'Path to file containing program data in JSON format' -// ) -// ) -// .action(async (address, opts) => { -// const storedConfig = await config.get() -// const { accounts } = storedConfig -// const accountsCommand = new EntropyAccount(entropy, opts.endpoint) -// writeOut('Attempting to register account with addtess: ' + address) -// const accountToRegister = getSelectedAccount(accounts, address) -// if (!accountToRegister) { -// throw new Error('AccountError: Unable to register non-existent account') -// } -// const updatedAccount = await accountsCommand.registerAccount(accountToRegister) -// const arrIdx = accounts.indexOf(accountToRegister) -// accounts.splice(arrIdx, 1, updatedAccount) -// await updateConfig(storedConfig, { accounts, selectedAccount: updatedAccount.address }) -// writeOut("Your address" + updatedAccount.address + "has been successfully registered.") -// process.exit(0) -// }) +function entropyAccountRegister () { + const accountRegisterCommand = new Command('register') + accountRegisterCommand + .description('Register an entropy account with a program') + .addOption(passwordOption()) + .addOption(endpointOption()) + .addOption(currentAccountAddressOption()) + .addOption( + new Option( + '-pointer, --pointer', + 'Program pointer of program to be used for registering' + ) + ) + .addOption( + new Option( + '-data, --program-data', + 'Path to file containing program data in JSON format' + ) + ) + .action(async (opts) => { + const storedConfig = await config.get() + const { accounts } = storedConfig + const entropy = await loadEntropy(opts.account, opts.endpoint) + const AccountsService = new EntropyAccount(entropy, opts.endpoint) + cliWrite('Attempting to register account with addtess: ' + opts.account) + const accountToRegister = getSelectedAccount(accounts, opts.account) + if (!accountToRegister) { + throw new Error('AccountError: Unable to register non-existent account') + } + const updatedAccount = await AccountsService.registerAccount(accountToRegister) + const arrIdx = accounts.indexOf(accountToRegister) + accounts.splice(arrIdx, 1, updatedAccount) + await updateConfig(storedConfig, { accounts, selectedAccount: updatedAccount.address }) + cliWrite("Your address" + updatedAccount.address + "has been successfully registered.") + process.exit(0) + }) + return accountRegisterCommand +} diff --git a/src/balance/command.ts b/src/balance/command.ts index bc05c34a..c307337f 100644 --- a/src/balance/command.ts +++ b/src/balance/command.ts @@ -1,18 +1,22 @@ import Entropy from "@entropyxyz/sdk"; import { Command } from "commander"; -import { cliWrite, endpointOption, passwordOption } from "src/common/utils-cli"; +import { cliWrite, endpointOption, loadEntropy, passwordOption } from "src/common/utils-cli"; import { EntropyBalance } from "./main"; -export async function entropyBalanceCommand (entropy: Entropy, rootCommand: Command) { - rootCommand.command('balance') +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(endpointOption()) .action(async (address, opts) => { + const entropy = await loadEntropy(address,opts.endpoint) const BalanceService = new EntropyBalance(entropy, opts.endpoint) const balance = await BalanceService.getBalance(address) cliWrite(`${balance.toLocaleString('en-US')} BITS`) process.exit(0) }) + + return balanceCommand } diff --git a/src/cli.ts b/src/cli.ts index e7d7c9ff..05f45b24 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -14,8 +14,11 @@ import { entropyTransferCommand } from './transfer/command' import { entropySignCommand } from './sign/command' import { entropyBalanceCommand } from './balance/command' -let entropy: Entropy +let entropyGlobal: Entropy async function setEntropyGlobal (address: string, endpoint: string, password?: string) { + console.log('args', address, endpoint, password); + + let entropy: Entropy = entropyGlobal if (entropy) { const currentAddress = entropy?.keyring?.accounts?.registration?.address if (address !== currentAddress) { @@ -28,13 +31,14 @@ async function setEntropyGlobal (address: string, endpoint: string, password?: s } else if (address && endpoint) { entropy = await loadEntropy(address, endpoint, password) - } else { - return } + + // console.log('entropy', entropy); + + return entropy } const program = new Command() -let commandName: string // the top level command /* no command */ program @@ -50,10 +54,10 @@ program .env('DEV_MODE') .hideHelp() ) - .hook('preSubcommand', async (_thisCommand, subCommand) => { - commandName = subCommand.name() - }) .hook('preAction', async (_thisCommand, actionCommand) => { + const commandName = actionCommand?.name() + console.log('command name', commandName); + await config.init() if (commandName === 'account') return // entropy not required for any account commands @@ -62,16 +66,20 @@ program const address = commandName === 'balance' ? actionCommand.args[0] : account - - await setEntropyGlobal(address, endpoint, password) + console.log('address from hook', address); + console.log('action command', actionCommand.args, actionCommand.opts()); + + entropyGlobal = await setEntropyGlobal(address, endpoint, password) + // console.log('entropy global in hook', entropyGlobal); + }) + .addCommand(entropyBalanceCommand()) + .addCommand(entropyAccountCommand()) + .addCommand(entropyTransferCommand()) + .addCommand(entropySignCommand()) .action((options: EntropyTuiOptions) => { - launchTui(entropy, options) + launchTui(entropyGlobal, options) }) - -entropyAccountCommand(entropy, program) -entropyBalanceCommand(entropy, program) -entropyTransferCommand(entropy, program) -entropySignCommand(entropy, program) +// entropySignCommand(entropyGlobal, program) program.parseAsync().then(() => {}) diff --git a/src/sign/command.ts b/src/sign/command.ts index 5caa6ce5..db116d74 100644 --- a/src/sign/command.ts +++ b/src/sign/command.ts @@ -1,10 +1,9 @@ import { Command, /* Option */ } from 'commander' -import { Entropy } from '@entropyxyz/sdk' -import { cliWrite, currentAccountAddressOption, endpointOption, passwordOption } from '../common/utils-cli' +import { cliWrite, currentAccountAddressOption, endpointOption, loadEntropy, passwordOption } from '../common/utils-cli' import { EntropySign } from './main' -export async function entropySignCommand (entropy: Entropy, rootCommand: Command) { - rootCommand.command('sign') +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)')) @@ -17,6 +16,7 @@ export async function entropySignCommand (entropy: Entropy, rootCommand: Command // ) // ) .action(async (msg, opts) => { + const entropy = await loadEntropy(opts.account, opts.endpoint) 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 /** @@ -28,4 +28,5 @@ export async function entropySignCommand (entropy: Entropy, rootCommand: Command cliWrite({ verifyingKey, signature }) process.exit(0) }) + return signCommand } diff --git a/src/transfer/command.ts b/src/transfer/command.ts index 34b5ac34..509bf515 100644 --- a/src/transfer/command.ts +++ b/src/transfer/command.ts @@ -1,10 +1,11 @@ import Entropy from "@entropyxyz/sdk" import { Command } from "commander" -import { currentAccountAddressOption, endpointOption, passwordOption } from "src/common/utils-cli" +import { currentAccountAddressOption, endpointOption, loadEntropy, passwordOption } from "src/common/utils-cli" import { EntropyTransfer } from "./main" -export async function entropyTransferCommand (entropy: Entropy, rootCommand: Command) { - rootCommand.command('transfer') +export function entropyTransferCommand () { + const transferCommand = new Command('tranfer') + transferCommand .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') @@ -12,9 +13,11 @@ export async function entropyTransferCommand (entropy: Entropy, rootCommand: Com .addOption(endpointOption()) .addOption(currentAccountAddressOption()) .action(async (destination, amount, opts) => { + const entropy = await loadEntropy(opts.account, opts.endpoint) const TransferService = new EntropyTransfer(entropy, opts.endpoint) await TransferService.transfer(destination, amount) // cliWrite(??) // TODO: write the output process.exit(0) }) + return transferCommand } From 69511679e8b12c9d47d8924faf04233925239fe6 Mon Sep 17 00:00:00 2001 From: Nayyir Jutha Date: Mon, 16 Sep 2024 15:26:29 -0400 Subject: [PATCH 22/28] skipping faucet test for now, shenanigans occurring --- tests/{faucet.test.ts => faucet.skip.ts} | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) rename tests/{faucet.test.ts => faucet.skip.ts} (78%) diff --git a/tests/faucet.test.ts b/tests/faucet.skip.ts similarity index 78% rename from tests/faucet.test.ts rename to tests/faucet.skip.ts index 517d0b72..ea1981f5 100644 --- a/tests/faucet.test.ts +++ b/tests/faucet.skip.ts @@ -1,18 +1,18 @@ import test from 'tape' -import * as util from "@polkadot/util" import { charlieStashSeed, setupTest } from './testing-utils' import { stripHexPrefix } from '../src/common/utils' import { readFileSync } from 'fs' import { EntropyBalance } from '../src/balance/main' import { EntropyTransfer } from '../src/transfer/main' import { getRandomFaucet, sendMoney } from '../src/flows/entropyFaucet/faucet' -import { register } from '../src/flows/register/register' import { LOCAL_PROGRAM_HASH } from '../src/flows/entropyFaucet/constants' +import { EntropyAccount } from '../src/account/main' -test('Faucet Tests', async t => { +test.skip('Faucet Tests', async t => { const { run, entropy, endpoint } = await setupTest(t, { seed: charlieStashSeed }) const { entropy: naynayEntropy } = await setupTest(t) + const AccountService = new EntropyAccount(entropy, endpoint) const BalanceService = new EntropyBalance(entropy, endpoint) const TransferService = new EntropyTransfer(entropy, endpoint) @@ -40,12 +40,13 @@ test('Faucet Tests', async t => { // Confirm faucetPointer matches deployed program pointer t.equal(faucetProgramPointer, LOCAL_PROGRAM_HASH, 'Program pointer matches') - + let entropyBalance = await BalanceService.getBalance(entropy.keyring.accounts.registration.address) + console.log('Balance Charlie::', entropyBalance); + let naynayBalance = await BalanceService.getBalance(naynayEntropy.keyring.accounts.registration.address) t.equal(naynayBalance, 0, 'Naynay is broke af') // register with faucet program - await run('Register Faucet Program for charlie stash', register( - entropy, + await run('Register Faucet Program for charlie stash', AccountService.register( { programModAddress: entropy.keyring.accounts.registration.address, programData: [{ program_pointer: faucetProgramPointer, program_config: userConfig }] @@ -54,8 +55,13 @@ test('Faucet Tests', async t => { const { chosenVerifyingKey, faucetAddress } = await getRandomFaucet(entropy, [], entropy.keyring.accounts.registration.address) // adding funds to faucet address - - await run('Transfer funds to faucet address', TransferService.transfer(faucetAddress, "100000000000000")) + entropyBalance = await BalanceService.getBalance(entropy.keyring.accounts.registration.address) + const faucetAddressBalance = await BalanceService.getBalance(faucetAddress) + console.log('Balance faucetAddress::', faucetAddressBalance); + console.log('Balance charlie 2::', entropyBalance); + + + await run('Transfer funds to faucet address', TransferService.transfer(faucetAddress, "10000000000000")) const transferStatus = await sendMoney( naynayEntropy, From 96d273cd95400a8e0ffdb5852387fe19130255e3 Mon Sep 17 00:00:00 2001 From: Nayyir Jutha Date: Mon, 16 Sep 2024 16:13:34 -0400 Subject: [PATCH 23/28] fixed faucet test --- tests/{faucet.skip.ts => faucet.test.ts} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename tests/{faucet.skip.ts => faucet.test.ts} (98%) diff --git a/tests/faucet.skip.ts b/tests/faucet.test.ts similarity index 98% rename from tests/faucet.skip.ts rename to tests/faucet.test.ts index ea1981f5..10ae1b70 100644 --- a/tests/faucet.skip.ts +++ b/tests/faucet.test.ts @@ -61,7 +61,7 @@ test.skip('Faucet Tests', async t => { console.log('Balance charlie 2::', entropyBalance); - await run('Transfer funds to faucet address', TransferService.transfer(faucetAddress, "10000000000000")) + await run('Transfer funds to faucet address', TransferService.transfer(faucetAddress, "1000")) const transferStatus = await sendMoney( naynayEntropy, From 930d149594543f6607de2233a5f7c94187d5f488 Mon Sep 17 00:00:00 2001 From: Nayyir Jutha Date: Mon, 16 Sep 2024 18:11:47 -0400 Subject: [PATCH 24/28] updated tests; --- tests/faucet.test.ts | 2 +- tests/transfer.test.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/faucet.test.ts b/tests/faucet.test.ts index 10ae1b70..255626cf 100644 --- a/tests/faucet.test.ts +++ b/tests/faucet.test.ts @@ -8,7 +8,7 @@ import { getRandomFaucet, sendMoney } from '../src/flows/entropyFaucet/faucet' import { LOCAL_PROGRAM_HASH } from '../src/flows/entropyFaucet/constants' import { EntropyAccount } from '../src/account/main' -test.skip('Faucet Tests', async t => { +test('Faucet Tests', async t => { const { run, entropy, endpoint } = await setupTest(t, { seed: charlieStashSeed }) const { entropy: naynayEntropy } = await setupTest(t) diff --git a/tests/transfer.test.ts b/tests/transfer.test.ts index a94567c4..c64a221a 100644 --- a/tests/transfer.test.ts +++ b/tests/transfer.test.ts @@ -1,6 +1,5 @@ import test from 'tape' import { wasmGlobalsReady } from '@entropyxyz/sdk' -// @ts-ignore import Keyring from '@entropyxyz/sdk/keys' import { makeSeed, @@ -13,6 +12,7 @@ import { initializeEntropy } from '../src/common/initializeEntropy' import { EntropyTransfer } from '../src/transfer/main' import { EntropyBalance } from '../src/balance/main' import { charlieStashAddress, charlieStashSeed } from './testing-utils/constants' +import { EntropyAccountData } from '../src/config/types' const networkType = 'two-nodes' const endpoint = 'ws://127.0.0.1:9944' @@ -32,12 +32,12 @@ test('Transfer', async (t) => { }) const charlieKeyring = new Keyring({ seed: charlieStashSeed, debug: true }) - const charlieEntropy = await initializeEntropy({ keyMaterial: charlieKeyring.getAccount(), endpoint, }) + const charlieEntropy = await initializeEntropy({ keyMaterial: charlieKeyring.getAccount() as EntropyAccountData, endpoint, }) await run('charlie ready', charlieEntropy.ready) const naynaySeed = makeSeed() const naynayKeyring = new Keyring({ seed: naynaySeed, debug: true }) - const naynayEntropy = await initializeEntropy({ keyMaterial: naynayKeyring.getAccount(), endpoint, }) + const naynayEntropy = await initializeEntropy({ keyMaterial: naynayKeyring.getAccount() as EntropyAccountData, endpoint, }) await run('naynay ready', naynayEntropy.ready) const naynayAddress = naynayEntropy.keyring.accounts.registration.address From 6d0ea12139cb816ff4e170bdee046ff0dc199d94 Mon Sep 17 00:00:00 2001 From: Nayyir Jutha Date: Mon, 16 Sep 2024 23:11:45 -0400 Subject: [PATCH 25/28] Update command.ts Co-authored-by: mix irving --- src/balance/command.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/balance/command.ts b/src/balance/command.ts index c307337f..7273b8d5 100644 --- a/src/balance/command.ts +++ b/src/balance/command.ts @@ -11,7 +11,7 @@ export function entropyBalanceCommand () { .addOption(passwordOption()) .addOption(endpointOption()) .action(async (address, opts) => { - const entropy = await loadEntropy(address,opts.endpoint) + const entropy = await loadEntropy(address, opts.endpoint) const BalanceService = new EntropyBalance(entropy, opts.endpoint) const balance = await BalanceService.getBalance(address) cliWrite(`${balance.toLocaleString('en-US')} BITS`) From c941e171c66e6894beb938c868ae0145b9fc777c Mon Sep 17 00:00:00 2001 From: Nayyir Jutha Date: Tue, 17 Sep 2024 12:48:06 -0400 Subject: [PATCH 26/28] pr review updates --- src/account/command.ts | 32 ++++++++++----------- src/account/interaction.ts | 4 +-- src/account/main.ts | 38 +++++++++++++++++-------- src/cli.ts | 16 ++--------- src/common/utils-cli.ts | 4 +-- src/common/utils.ts | 9 ++++-- src/config/index.ts | 8 ++++-- src/flows/entropyFaucet/index.ts | 4 +-- src/flows/programs/index.ts | 6 ++-- tests/account.test.ts | 48 ++++++++++++++++++++++++++++++-- tests/register.test.ts | 46 ------------------------------ 11 files changed, 112 insertions(+), 103 deletions(-) delete mode 100644 tests/register.test.ts diff --git a/src/account/command.ts b/src/account/command.ts index d8fc3512..620b5689 100644 --- a/src/account/command.ts +++ b/src/account/command.ts @@ -4,7 +4,7 @@ import { EntropyAccount } from "./main"; import { ACCOUNTS_CONTENT } from './constants' import * as config from '../config' import { cliWrite, currentAccountAddressOption, endpointOption, loadEntropy, passwordOption } from "../common/utils-cli"; -import { getSelectedAccount, updateConfig } from "src/common/utils"; +import { findAccountNameByAddress, updateConfig } from "src/common/utils"; export function entropyAccountCommand () { const accountCommand = new Command('account') @@ -109,25 +109,25 @@ function entropyAccountRegister () { .addOption(passwordOption()) .addOption(endpointOption()) .addOption(currentAccountAddressOption()) - .addOption( - new Option( - '-pointer, --pointer', - 'Program pointer of program to be used for registering' - ) - ) - .addOption( - new Option( - '-data, --program-data', - 'Path to file containing program data in JSON format' - ) - ) + // Removing these options for now until we update the design to accept program configs + // .addOption( + // new Option( + // '-pointer, --pointer', + // 'Program pointer of program to be used for registering' + // ) + // ) + // .addOption( + // new Option( + // '-data, --program-data', + // 'Path to file containing program data in JSON format' + // ) + // ) .action(async (opts) => { const storedConfig = await config.get() const { accounts } = storedConfig - const entropy = await loadEntropy(opts.account, opts.endpoint) + const entropy: Entropy = await loadEntropy(opts.account, opts.endpoint) const AccountsService = new EntropyAccount(entropy, opts.endpoint) - cliWrite('Attempting to register account with addtess: ' + opts.account) - const accountToRegister = getSelectedAccount(accounts, opts.account) + const accountToRegister = findAccountNameByAddress(accounts, opts.account) if (!accountToRegister) { throw new Error('AccountError: Unable to register non-existent account') } diff --git a/src/account/interaction.ts b/src/account/interaction.ts index a1686bd0..ea3f7ad1 100644 --- a/src/account/interaction.ts +++ b/src/account/interaction.ts @@ -1,7 +1,7 @@ import inquirer from "inquirer"; import Entropy from "@entropyxyz/sdk"; -import { getSelectedAccount, print } from "../common/utils" +import { findAccountNameByAddress, print } from "../common/utils" import { EntropyConfig } from "../config/types"; import { EntropyAccount } from './main' @@ -74,7 +74,7 @@ export async function entropyRegister (entropy: Entropy, endpoint: string, store const AccountService = new EntropyAccount(entropy, endpoint) const { accounts, selectedAccount } = storedConfig - const currentAccount = getSelectedAccount(accounts, selectedAccount) + const currentAccount = findAccountNameByAddress(accounts, selectedAccount) if (!currentAccount) { print("No account selected to register") return; diff --git a/src/account/main.ts b/src/account/main.ts index 3d7a4657..779b6f8c 100644 --- a/src/account/main.ts +++ b/src/account/main.ts @@ -5,7 +5,6 @@ import { randomAsHex } from '@polkadot/util-crypto' import { FLOW_CONTEXT } from "./constants"; import { AccountCreateParams, AccountImportParams, AccountRegisterParams } from "./types"; -import { print } from "src/common/utils"; import { EntropyBase } from "../common/entropy-base"; import { EntropyAccountConfig } from "../config/types"; @@ -99,16 +98,31 @@ export class EntropyAccount extends EntropyBase { /* PRIVATE */ private async pruneRegistration () { - try { - const tx = this.entropy.substrate.tx.registry.pruneRegistration() - await tx.signAndSend(this.entropy.keyring.accounts.registration.pair, ({ status }) => { - if (status.isFinalized) { - print('Successfully pruned registration'); - } - }) - } catch (error) { - console.error('Unable to prune registration due to:', error.message); - throw error - } + return new Promise((resolve, reject) => { + this.entropy.substrate.tx.registry.pruneRegistration() + .signAndSend(this.entropy.keyring.accounts.registration.pair, ({ status, dispatchError }) => { + if (dispatchError) { + let msg: string + if (dispatchError.isModule) { + // for module errors, we have the section indexed, lookup + const decoded = this.entropy.substrate.registry.findMetaError( + dispatchError.asModule + ) + const { docs, name, section } = decoded + + msg = `${section}.${name}: ${docs.join(' ')}` + } else { + // Other, CannotLookup, BadOrigin, no extra info + msg = dispatchError.toString() + } + const error = Error(msg) + this.logger.error('There was an issue pruning registration', error) + return reject(error) + } + if (status.isFinalized) { + resolve(status) + } + }) + }) } } diff --git a/src/cli.ts b/src/cli.ts index 05f45b24..aa3cd785 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -14,11 +14,10 @@ import { entropyTransferCommand } from './transfer/command' import { entropySignCommand } from './sign/command' import { entropyBalanceCommand } from './balance/command' -let entropyGlobal: Entropy +let entropy: Entropy async function setEntropyGlobal (address: string, endpoint: string, password?: string) { console.log('args', address, endpoint, password); - let entropy: Entropy = entropyGlobal if (entropy) { const currentAddress = entropy?.keyring?.accounts?.registration?.address if (address !== currentAddress) { @@ -33,8 +32,6 @@ async function setEntropyGlobal (address: string, endpoint: string, password?: s entropy = await loadEntropy(address, endpoint, password) } - // console.log('entropy', entropy); - return entropy } @@ -56,8 +53,6 @@ program ) .hook('preAction', async (_thisCommand, actionCommand) => { const commandName = actionCommand?.name() - console.log('command name', commandName); - await config.init() if (commandName === 'account') return // entropy not required for any account commands @@ -66,20 +61,15 @@ program const address = commandName === 'balance' ? actionCommand.args[0] : account - console.log('address from hook', address); - console.log('action command', actionCommand.args, actionCommand.opts()); - - entropyGlobal = await setEntropyGlobal(address, endpoint, password) - // console.log('entropy global in hook', entropyGlobal); + await setEntropyGlobal(address, endpoint, password) }) .addCommand(entropyBalanceCommand()) .addCommand(entropyAccountCommand()) .addCommand(entropyTransferCommand()) .addCommand(entropySignCommand()) .action((options: EntropyTuiOptions) => { - launchTui(entropyGlobal, options) + launchTui(entropy, options) }) -// entropySignCommand(entropyGlobal, program) program.parseAsync().then(() => {}) diff --git a/src/common/utils-cli.ts b/src/common/utils-cli.ts index 65c49bbb..d53ced00 100644 --- a/src/common/utils-cli.ts +++ b/src/common/utils-cli.ts @@ -1,5 +1,5 @@ import { Option } from 'commander' -import { getSelectedAccount, stringify } from './utils' +import { findAccountNameByAddress, stringify } from './utils' import * as config from '../config' import Entropy from '@entropyxyz/sdk' import { initializeEntropy } from './initializeEntropy' @@ -63,7 +63,7 @@ export function currentAccountAddressOption () { export async function loadEntropy (address: string, endpoint: string, password?: string): Promise { const storedConfig = config.getSync() - const selectedAccount = getSelectedAccount(storedConfig.accounts, address) + const selectedAccount = findAccountNameByAddress(storedConfig.accounts, address) if (!selectedAccount) throw new Error(`AddressError: No account with name or address "${address}"`) // check if data is encrypted + we have a password diff --git a/src/common/utils.ts b/src/common/utils.ts index 422dcf21..4a7d7c42 100644 --- a/src/common/utils.ts +++ b/src/common/utils.ts @@ -66,8 +66,7 @@ export function accountChoicesWithOther (accounts: EntropyAccountConfig[]) { .concat([{ name: "Other", value: null }]) } -// TODO: rename => findAccountByNameAddress -export function getSelectedAccount (accounts: EntropyAccountConfig[], aliasOrAddress: string) { +export function findAccountNameByAddress (accounts: EntropyAccountConfig[], aliasOrAddress: string) { if (!aliasOrAddress || !aliasOrAddress.length) throw Error('aliasOrAddress required') return ( @@ -76,7 +75,11 @@ export function getSelectedAccount (accounts: EntropyAccountConfig[], aliasOrAdd ) } -export async function updateConfig (storedConfig: EntropyConfig, newUpdates: any) { +// Used to update config with new updates, new updates can either be partial properties from the EntropyConfig type, or 'exit' +// if newUpdates is populated with 'exit' function will return true to handle returning to main menu for the TUI. +// if newUpdates is populated with an object containing partial entropy config properties, false is returned, forcing the TUI +// to ask the user if they want to return to main menu. +export async function updateConfig (storedConfig: EntropyConfig, newUpdates?: Partial | "exit"): Promise { if (typeof newUpdates === 'string' && newUpdates === 'exit') { return true } else if (newUpdates) { diff --git a/src/config/index.ts b/src/config/index.ts index fa57e3bd..da27ca72 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -67,8 +67,12 @@ export function getSync (configPath = CONFIG_PATH) { try { configBuffer = readFileSync(configPath, 'utf8') } catch (error) { - writeFileSync(configPath, '{}') - configBuffer = readFileSync(configPath, 'utf8') + if (error.message.includes('ENOENT: no such file or directory')) { + writeFileSync(configPath, '{}') + configBuffer = readFileSync(configPath, 'utf8') + } else { + throw error + } } return deserialize(configBuffer) } diff --git a/src/flows/entropyFaucet/index.ts b/src/flows/entropyFaucet/index.ts index e76b7f14..7edbd310 100644 --- a/src/flows/entropyFaucet/index.ts +++ b/src/flows/entropyFaucet/index.ts @@ -1,5 +1,5 @@ import Entropy from "@entropyxyz/sdk" -import { getSelectedAccount, print } from "../../common/utils" +import { findAccountNameByAddress, print } from "../../common/utils" import { initializeEntropy } from "../../common/initializeEntropy" import { EntropyLogger } from '../../common/logger' import { getRandomFaucet, sendMoney } from "./faucet" @@ -14,7 +14,7 @@ export async function entropyFaucet ({ accounts, selectedAccount: selectedAccoun let verifyingKeys: string[] = [] const amount = "10000000000" const { endpoint } = options - const selectedAccount = getSelectedAccount(accounts, selectedAccountAddress) + const selectedAccount = findAccountNameByAddress(accounts, selectedAccountAddress) logger.log(`selectedAccount::`, FLOW_CONTEXT) logger.log(selectedAccount, FLOW_CONTEXT) try { diff --git a/src/flows/programs/index.ts b/src/flows/programs/index.ts index 3e1deae4..d3b7b6af 100644 --- a/src/flows/programs/index.ts +++ b/src/flows/programs/index.ts @@ -9,7 +9,7 @@ import { removeProgram } from "./remove"; import { addQuestions, getProgramPointerInput, verifyingKeyQuestion } from "./helpers/questions"; import { displayPrograms } from "./helpers/utils"; import { initializeEntropy } from "../../common/initializeEntropy" -import { getSelectedAccount, print } from "../../common/utils" +import { findAccountNameByAddress, print } from "../../common/utils" import { EntropyLogger } from "../../common/logger"; import { EntropyTuiOptions } from "../../types" @@ -18,7 +18,7 @@ let verifyingKey: string; export async function userPrograms ({ accounts, selectedAccount: selectedAccountAddress }, options: EntropyTuiOptions, logger: EntropyLogger) { const FLOW_CONTEXT = 'PROGRAMS' const { endpoint } = options - const selectedAccount = getSelectedAccount(accounts, selectedAccountAddress) + const selectedAccount = findAccountNameByAddress(accounts, selectedAccountAddress) const actionChoice = await inquirer.prompt([ { @@ -120,7 +120,7 @@ export async function userPrograms ({ accounts, selectedAccount: selectedAccount export async function devPrograms ({ accounts, selectedAccount: selectedAccountAddress }, options: EntropyTuiOptions, logger: EntropyLogger) { // const FLOW_CONTEXT = 'PROGRAMS' const { endpoint } = options - const selectedAccount = getSelectedAccount(accounts, selectedAccountAddress) + const selectedAccount = findAccountNameByAddress(accounts, selectedAccountAddress) const choices = { "Deploy": deployProgramTUI, diff --git a/tests/account.test.ts b/tests/account.test.ts index df397199..9f0288f6 100644 --- a/tests/account.test.ts +++ b/tests/account.test.ts @@ -8,8 +8,9 @@ import { randomAsHex } from '@polkadot/util-crypto' import { EntropyAccount } from '../src/account/main' import { EntropyAccountConfig, EntropyConfig } from '../src/config/types' import * as config from '../src/config' -import { promiseRunner, sleep } from './testing-utils' +import { promiseRunner, setupTest } from './testing-utils' import { charlieStashAddress, charlieStashSeed } from './testing-utils/constants' +import { readFileSync } from 'fs' test('Account - list', async t => { const account: EntropyAccountConfig = { @@ -57,7 +58,7 @@ test('Account - list', async t => { let counter = 0 -test('Account - create', async t => { +test('Account - import', async t => { const configPath = `/tmp/entropy-cli-${Date.now()}_${counter++}.json` /* Setup */ const run = promiseRunner(t) @@ -76,3 +77,46 @@ test('Account - create', async t => { t.equal(newAccount.address, admin?.address, 'Generated Account matches Account created by Keyring') t.end() }) + +const networkType = 'two-nodes' +const endpoint = 'ws://127.0.0.1:9944' + +test('Account - Register: Default Program', async (t) => { + const { run, entropy } = await setupTest(t, { networkType, seed: charlieStashSeed }) + const accountService = new EntropyAccount(entropy, endpoint) + + const verifyingKey = await run('register account', accountService.register()) + + const fullAccount = entropy.keyring.getAccount() + + t.equal(verifyingKey, fullAccount?.registration?.verifyingKeys?.[0], 'verifying key matches key added to registration account') + + t.end() +}) + +test('Account - Register: Barebones Program', async t => { + const { run, entropy } = await setupTest(t, { networkType, seed: charlieStashSeed }) + const dummyProgram: any = readFileSync( + new URL('./programs/template_barebones.wasm', import.meta.url) + ) + const pointer = await run( + 'deploy program', + entropy.programs.dev.deploy(dummyProgram) + ) + + const accountService = new EntropyAccount(entropy, endpoint) + const verifyingKey = await run( + 'register - using custom params', + accountService.register({ + programModAddress: entropy.keyring.accounts.registration.address, + programData: [{ program_pointer: pointer, program_config: '0x' }], + }) + ) + + const fullAccount = entropy.keyring.getAccount() + + t.equal(verifyingKey, fullAccount?.registration?.verifyingKeys?.[1], 'verifying key matches key added to registration account') + + t.end() +}) + diff --git a/tests/register.test.ts b/tests/register.test.ts deleted file mode 100644 index 914e90f1..00000000 --- a/tests/register.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -import test from 'tape' -import { EntropyAccount } from '../src/account/main' -import { charlieStashSeed, setupTest } from './testing-utils' -import { readFileSync } from 'node:fs' - -const networkType = 'two-nodes' -const endpoint = 'ws://127.0.0.1:9944' - -test('Register - Default Program', async (t) => { - const { run, entropy } = await setupTest(t, { networkType, seed: charlieStashSeed }) - const accountService = new EntropyAccount(entropy, endpoint) - - const verifyingKey = await run('register account', accountService.register()) - - const fullAccount = entropy.keyring.getAccount() - - t.equal(verifyingKey, fullAccount?.registration?.verifyingKeys?.[0], 'verifying key matches key added to registration account') - - t.end() -}) - -test('Register - Barebones Program', async t => { - const { run, entropy } = await setupTest(t, { networkType, seed: charlieStashSeed }) - const dummyProgram: any = readFileSync( - new URL('./programs/template_barebones.wasm', import.meta.url) - ) - const pointer = await run( - 'deploy program', - entropy.programs.dev.deploy(dummyProgram) - ) - - const accountService = new EntropyAccount(entropy, endpoint) - const verifyingKey = await run( - 'register - using custom params', - accountService.register({ - programModAddress: entropy.keyring.accounts.registration.address, - programData: [{ program_pointer: pointer, program_config: '0x' }], - }) - ) - - const fullAccount = entropy.keyring.getAccount() - - t.equal(verifyingKey, fullAccount?.registration?.verifyingKeys?.[1], 'verifying key matches key added to registration account') - - t.end() -}) From 3e091f9389f5391e9cbea320f22e224b040f49a8 Mon Sep 17 00:00:00 2001 From: mix irving Date: Wed, 18 Sep 2024 09:37:34 +1200 Subject: [PATCH 27/28] Update src/cli.ts --- src/cli.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index aa3cd785..94d06a16 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -16,8 +16,6 @@ import { entropyBalanceCommand } from './balance/command' let entropy: Entropy async function setEntropyGlobal (address: string, endpoint: string, password?: string) { - console.log('args', address, endpoint, password); - if (entropy) { const currentAddress = entropy?.keyring?.accounts?.registration?.address if (address !== currentAddress) { From 183e200471f8a1065350a33b9feddda4a21f8a8c Mon Sep 17 00:00:00 2001 From: mix irving Date: Wed, 18 Sep 2024 17:13:31 +1200 Subject: [PATCH 28/28] account-restructure tweaks (#226) --- src/account/command.ts | 62 +++++++++++++------------------ src/account/interaction.ts | 37 +++++++++--------- src/account/main.ts | 6 ++- src/account/utils.ts | 25 ++++++++++++- src/balance/interaction.ts | 4 +- src/cli.ts | 41 +++----------------- src/common/entropy-base.ts | 4 +- src/common/utils-cli.ts | 14 +++---- src/common/utils.ts | 18 +-------- src/config/index.ts | 29 +++++++-------- src/config/types.ts | 5 ++- src/flows/entropyFaucet/faucet.ts | 6 +-- src/flows/entropyFaucet/index.ts | 6 +-- src/flows/programs/index.ts | 6 +-- src/sign/interaction.ts | 4 +- src/transfer/command.ts | 5 +-- src/transfer/interaction.ts | 4 +- src/tui.ts | 55 ++++++++++----------------- src/types/index.ts | 5 ++- tests/balance.test.ts | 10 ++--- tests/faucet.test.ts | 22 +++++------ tests/sign.test.ts | 4 +- tests/transfer.test.ts | 13 ++++--- 23 files changed, 169 insertions(+), 216 deletions(-) diff --git a/src/account/command.ts b/src/account/command.ts index 620b5689..4c9db8bf 100644 --- a/src/account/command.ts +++ b/src/account/command.ts @@ -1,24 +1,23 @@ import Entropy from "@entropyxyz/sdk" import { Command, Option } from 'commander' 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 { findAccountNameByAddress, updateConfig } from "src/common/utils"; +import { findAccountByAddressOrName } from "src/common/utils"; export function entropyAccountCommand () { - const accountCommand = new Command('account') + return new Command('account') .description('Commands to work with accounts on the Entropy Network') .addCommand(entropyAccountCreate()) .addCommand(entropyAccountImport()) .addCommand(entropyAccountList()) .addCommand(entropyAccountRegister()) - - return accountCommand } function entropyAccountCreate () { - const accountCreateCommand = new Command('create') + return new Command('create') .alias('new') .description('Create a new entropy account from scratch. Output is JSON of form {name, address}') .addOption(passwordOption()) @@ -33,7 +32,7 @@ function entropyAccountCreate () { const { path } = opts const newAccount = await EntropyAccount.create({ name, path }) - await persistAndSelectNewAccount(newAccount) + await selectAndPersistNewAccount(newAccount) cliWrite({ name: newAccount.name, @@ -41,11 +40,10 @@ function entropyAccountCreate () { }) process.exit(0) }) - return accountCreateCommand } function entropyAccountImport () { - const accountImportCommand = new Command('import') + return new Command('import') .description('Import an existing entropy account from seed. Output is JSON of form {name, address}') .addOption(passwordOption()) .argument('', 'A user friendly name for your new account.') @@ -60,7 +58,7 @@ function entropyAccountImport () { const { path } = opts const newAccount = await EntropyAccount.import({ name, seed, path }) - await persistAndSelectNewAccount(newAccount) + await selectAndPersistNewAccount(newAccount) cliWrite({ name: newAccount.name, @@ -68,27 +66,10 @@ function entropyAccountImport () { }) process.exit(0) }) - return accountImportCommand -} - -async function persistAndSelectNewAccount (newAccount) { - const storedConfig = await config.get() - const { accounts } = storedConfig - - const isExistingName = accounts.find(account => account.name === newAccount.name) - if (isExistingName) { - throw Error(`An account with name "${newAccount.name}" already exists. Choose a different name`) - } - - accounts.push(newAccount) - await updateConfig(storedConfig, { - accounts, - selectedAccount: newAccount.address - }) } function entropyAccountList () { - const accountListCommand = new Command('list') + return new Command('list') .alias('ls') .description('List all accounts. Output is JSON of form [{ name, address, verifyingKeys }]') .action(async () => { @@ -98,13 +79,11 @@ function entropyAccountList () { cliWrite(accounts) process.exit(0) }) - return accountListCommand } /* register */ function entropyAccountRegister () { - const accountRegisterCommand = new Command('register') - accountRegisterCommand + return new Command('register') .description('Register an entropy account with a program') .addOption(passwordOption()) .addOption(endpointOption()) @@ -123,20 +102,29 @@ function entropyAccountRegister () { // ) // ) .action(async (opts) => { + const { account, endpoint, /* password */ } = opts const storedConfig = await config.get() const { accounts } = storedConfig - const entropy: Entropy = await loadEntropy(opts.account, opts.endpoint) - const AccountsService = new EntropyAccount(entropy, opts.endpoint) - const accountToRegister = findAccountNameByAddress(accounts, opts.account) + const accountToRegister = findAccountByAddressOrName(accounts, account) if (!accountToRegister) { throw new Error('AccountError: Unable to register non-existent account') } - const updatedAccount = await AccountsService.registerAccount(accountToRegister) + + const entropy: Entropy = await loadEntropy(accountToRegister.address, endpoint) + const accountService = new EntropyAccount(entropy, endpoint) + const updatedAccount = await accountService.registerAccount(accountToRegister) + const arrIdx = accounts.indexOf(accountToRegister) accounts.splice(arrIdx, 1, updatedAccount) - await updateConfig(storedConfig, { accounts, selectedAccount: updatedAccount.address }) - cliWrite("Your address" + updatedAccount.address + "has been successfully registered.") + await config.set({ + ...storedConfig, + accounts, + selectedAccount: updatedAccount.address + }) + + const verifyingKeys = updatedAccount?.data?.registration?.verifyingKeys + const verifyingKey = verifyingKeys[verifyingKeys.length - 1] + cliWrite(verifyingKey) process.exit(0) }) - return accountRegisterCommand } diff --git a/src/account/interaction.ts b/src/account/interaction.ts index ea3f7ad1..46f6f24a 100644 --- a/src/account/interaction.ts +++ b/src/account/interaction.ts @@ -1,9 +1,11 @@ import inquirer from "inquirer"; import Entropy from "@entropyxyz/sdk"; -import { findAccountNameByAddress, print } from "../common/utils" -import { EntropyConfig } from "../config/types"; import { EntropyAccount } from './main' +import { selectAndPersistNewAccount } from "./utils"; +import { findAccountByAddressOrName, print } from "../common/utils" +import { EntropyConfig } from "../config/types"; +import * as config from "../config"; import { manageAccountsQuestions, @@ -11,8 +13,10 @@ import { selectAccountQuestions } from "./utils" - -export async function entropyManageAccounts (endpoint: string, storedConfig: EntropyConfig) { +/* + * @returns partialConfigUpdate | "exit" | undefined + */ +export async function entropyAccount (endpoint: string, storedConfig: EntropyConfig) { const { accounts } = storedConfig const { interactionChoice } = await inquirer.prompt(manageAccountsQuestions) @@ -26,15 +30,13 @@ export async function entropyManageAccounts (endpoint: string, storedConfig: Ent // isDebugMode = true seed = seed.split('#debug')[0] } + const newAccount = seed ? await EntropyAccount.import({ seed, name, path }) : await EntropyAccount.create({ name, path }) - accounts.push(newAccount) - return { - accounts, - selectedAccount: newAccount.address - } + await selectAndPersistNewAccount(newAccount) + return } case 'select-account': { @@ -43,12 +45,13 @@ export async function entropyManageAccounts (endpoint: string, storedConfig: Ent return } const { selectedAccount } = await inquirer.prompt(selectAccountQuestions(accounts)) - print('Current selected account is ' + selectedAccount) - - return { - accounts: storedConfig.accounts, + await config.set({ + ...storedConfig, selectedAccount: selectedAccount.address - } + }) + + print('Current selected account is ' + selectedAccount) + return } case 'list-account': { @@ -71,16 +74,16 @@ export async function entropyManageAccounts (endpoint: string, storedConfig: Ent } export async function entropyRegister (entropy: Entropy, endpoint: string, storedConfig: EntropyConfig): Promise> { - const AccountService = new EntropyAccount(entropy, endpoint) + const accountService = new EntropyAccount(entropy, endpoint) const { accounts, selectedAccount } = storedConfig - const currentAccount = findAccountNameByAddress(accounts, selectedAccount) + const currentAccount = findAccountByAddressOrName(accounts, selectedAccount) if (!currentAccount) { print("No account selected to register") return; } print("Attempting to register the address:", currentAccount.address) - const updatedAccount = await AccountService.registerAccount(currentAccount) + const updatedAccount = await accountService.registerAccount(currentAccount) const arrIdx = accounts.indexOf(currentAccount) accounts.splice(arrIdx, 1, updatedAccount) print("Your address", updatedAccount.address, "has been successfully registered.") diff --git a/src/account/main.ts b/src/account/main.ts index 779b6f8c..0c5d46d2 100644 --- a/src/account/main.ts +++ b/src/account/main.ts @@ -67,6 +67,8 @@ export class EntropyAccount extends EntropyBase { : undefined return this.entropy.register(registerParams) + // NOTE: if "register" fails for any reason, core currently leaves the chain in a "polluted" + // state. To fix this we manually "prune" the dirty registration transaction. .catch(async error => { await this.pruneRegistration() throw error @@ -86,8 +88,8 @@ export class EntropyAccount extends EntropyBase { // Register params to be defined from user input (arguments/options or inquirer prompts) try { const verifyingKey = await this.register(registerParams) - - account?.data?.registration?.verifyingKeys?.push(verifyingKey) + // NOTE: this mutation triggers events in Keyring + account.data.registration.verifyingKeys.push(verifyingKey) return account } catch (error) { this.logger.error('There was a problem registering', error) diff --git a/src/account/utils.ts b/src/account/utils.ts index ba6cdc4a..183c06dc 100644 --- a/src/account/utils.ts +++ b/src/account/utils.ts @@ -1,9 +1,30 @@ import { EntropyAccountConfig } from "../config/types"; -import { AccountListResults } from './types'; +import * as config from "../config"; import { ACCOUNTS_CONTENT } from './constants'; import { generateAccountChoices } from '../common/utils'; -const validateSeedInput = (seed) => { +export async function selectAndPersistNewAccount (newAccount) { + const storedConfig = await config.get() + const { accounts } = storedConfig + + const isExistingName = accounts.find(account => account.name === newAccount.name) + if (isExistingName) { + throw Error(`An account with name "${newAccount.name}" already exists. Choose a different name`) + } + const isExistingAddress = accounts.find(account => account.address === newAccount.address) + if (isExistingAddress) { + throw Error(`An account with address "${newAccount.address}" already exists.`) + } + + accounts.push(newAccount) + await config.set({ + ...storedConfig, + accounts, + selectedAccount: newAccount.address + }) +} + +function validateSeedInput (seed) { if (seed.includes('#debug')) return true if (seed.length === 66 && seed.startsWith('0x')) return true if (seed.length === 64) return true diff --git a/src/balance/interaction.ts b/src/balance/interaction.ts index aa9f7cda..1fcef279 100644 --- a/src/balance/interaction.ts +++ b/src/balance/interaction.ts @@ -3,8 +3,8 @@ import { EntropyBalance } from "./main" export async function entropyBalance (entropy, endpoint, storedConfig) { try { - const BalanceService = new EntropyBalance(entropy, endpoint) - const balance = await BalanceService.getBalance(storedConfig.selectedAccount) + const balanceService = new EntropyBalance(entropy, endpoint) + const balance = await balanceService.getBalance(storedConfig.selectedAccount) print(`Address ${storedConfig.selectedAccount} has a balance of: ${balance.toLocaleString('en-US')} BITS`) } catch (error) { console.error('There was an error retrieving balance', error) diff --git a/src/cli.ts b/src/cli.ts index 94d06a16..c307b9f9 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -2,9 +2,7 @@ /* NOTE: calling this file entropy.ts helps commander parse process.argv */ import { Command, Option } from 'commander' -import Entropy from '@entropyxyz/sdk' -import * as config from './config' import { EntropyTuiOptions } from './types' import { currentAccountAddressOption, endpointOption, loadEntropy } from './common/utils-cli' @@ -14,33 +12,15 @@ import { entropyTransferCommand } from './transfer/command' import { entropySignCommand } from './sign/command' import { entropyBalanceCommand } from './balance/command' -let entropy: Entropy -async function setEntropyGlobal (address: string, endpoint: string, password?: string) { - if (entropy) { - const currentAddress = entropy?.keyring?.accounts?.registration?.address - if (address !== currentAddress) { - // QUESTION: Is it possible to hit this? - // - programmatic usage kills process after function call - // - tui usage manages mutation of entropy instance itself - await entropy.close() - entropy = await loadEntropy(address, endpoint, password) - } - } - else if (address && endpoint) { - entropy = await loadEntropy(address, endpoint, password) - } - - return entropy -} - const program = new Command() /* no command */ program .name('entropy') .description('CLI interface for interacting with entropy.xyz. Running without commands starts an interactive ui') - .addOption(endpointOption()) .addOption(currentAccountAddressOption()) + .addOption(endpointOption()) + // NOTE: I think this is currently unused .addOption( new Option( '-d, --dev', @@ -49,24 +29,13 @@ program .env('DEV_MODE') .hideHelp() ) - .hook('preAction', async (_thisCommand, actionCommand) => { - const commandName = actionCommand?.name() - await config.init() - if (commandName === 'account') return - // entropy not required for any account commands - - const { account, endpoint, password } = actionCommand.opts() - const address = commandName === 'balance' - ? actionCommand.args[0] - : account - - await setEntropyGlobal(address, endpoint, password) - }) .addCommand(entropyBalanceCommand()) .addCommand(entropyAccountCommand()) .addCommand(entropyTransferCommand()) .addCommand(entropySignCommand()) - .action((options: EntropyTuiOptions) => { + .action(async (options: EntropyTuiOptions) => { + const { account, endpoint } = options + const entropy = await loadEntropy(account, endpoint) launchTui(entropy, options) }) diff --git a/src/common/entropy-base.ts b/src/common/entropy-base.ts index d2fd98c1..5f604ba5 100644 --- a/src/common/entropy-base.ts +++ b/src/common/entropy-base.ts @@ -5,8 +5,8 @@ export abstract class EntropyBase { protected logger: EntropyLogger protected entropy: Entropy - constructor ({ entropy, endpoint, flowContext }: { entropy?: Entropy, endpoint: string, flowContext: string }) { + constructor ({ entropy, endpoint, flowContext }: { entropy: Entropy, endpoint: string, flowContext: string }) { this.logger = new EntropyLogger(flowContext, endpoint) this.entropy = entropy } -} \ No newline at end of file +} diff --git a/src/common/utils-cli.ts b/src/common/utils-cli.ts index d53ced00..eae015cb 100644 --- a/src/common/utils-cli.ts +++ b/src/common/utils-cli.ts @@ -1,5 +1,5 @@ import { Option } from 'commander' -import { findAccountNameByAddress, stringify } from './utils' +import { findAccountByAddressOrName, stringify } from './utils' import * as config from '../config' import Entropy from '@entropyxyz/sdk' import { initializeEntropy } from './initializeEntropy' @@ -42,8 +42,7 @@ export function passwordOption (description?: string) { export function currentAccountAddressOption () { const storedConfig = config.getSync() - // WIP: this needs a try-catch which runs config.init if it's missing ... which may need a sync version - // TODO + return new Option( '-a, --account
', 'Sets the current account for the session or defaults to the account stored in the config' @@ -57,14 +56,15 @@ export function currentAccountAddressOption () { return account }) - .hideHelp() .default(storedConfig.selectedAccount) + // TODO: display the *name* not address + // TODO: standardise whether selectedAccount is name or address. } -export async function loadEntropy (address: string, endpoint: string, password?: string): Promise { +export async function loadEntropy (addressOrName: string, endpoint: string, password?: string): Promise { const storedConfig = config.getSync() - const selectedAccount = findAccountNameByAddress(storedConfig.accounts, address) - if (!selectedAccount) throw new Error(`AddressError: No account with name or address "${address}"`) + const selectedAccount = findAccountByAddressOrName(storedConfig.accounts, addressOrName) + if (!selectedAccount) throw new Error(`AddressError: 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/common/utils.ts b/src/common/utils.ts index 4a7d7c42..419340ef 100644 --- a/src/common/utils.ts +++ b/src/common/utils.ts @@ -1,6 +1,5 @@ -import * as config from '../config' import { Buffer } from 'buffer' -import { EntropyAccountConfig, EntropyConfig } from "../config/types" +import { EntropyAccountConfig } from "../config/types" export function stripHexPrefix (str: string): string { if (str.startsWith('0x')) return str.slice(2) @@ -66,7 +65,7 @@ export function accountChoicesWithOther (accounts: EntropyAccountConfig[]) { .concat([{ name: "Other", value: null }]) } -export function findAccountNameByAddress (accounts: EntropyAccountConfig[], aliasOrAddress: string) { +export function findAccountByAddressOrName (accounts: EntropyAccountConfig[], aliasOrAddress: string) { if (!aliasOrAddress || !aliasOrAddress.length) throw Error('aliasOrAddress required') return ( @@ -74,16 +73,3 @@ export function findAccountNameByAddress (accounts: EntropyAccountConfig[], alia accounts.find(account => account.name === aliasOrAddress) ) } - -// Used to update config with new updates, new updates can either be partial properties from the EntropyConfig type, or 'exit' -// if newUpdates is populated with 'exit' function will return true to handle returning to main menu for the TUI. -// if newUpdates is populated with an object containing partial entropy config properties, false is returned, forcing the TUI -// to ask the user if they want to return to main menu. -export async function updateConfig (storedConfig: EntropyConfig, newUpdates?: Partial | "exit"): Promise { - if (typeof newUpdates === 'string' && newUpdates === 'exit') { - return true - } else if (newUpdates) { - await config.set({ ...storedConfig, ...newUpdates }) - } - return false -} diff --git a/src/config/index.ts b/src/config/index.ts index da27ca72..2afa3287 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -6,6 +6,7 @@ import envPaths from 'env-paths' import allMigrations from './migrations' import { serialize, deserialize } from './encoding' +import { EntropyConfig } from './types' const paths = envPaths('entropy-cryptography', { suffix: '' }) const CONFIG_PATH = join(paths.config, 'entropy-cli.json') @@ -35,8 +36,7 @@ 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) => { - console.log("error", err.code) - if (err && err.code !== 'ENOENT') throw err + if (err.code !== 'ENOENT') throw err const oldConfig = await get(oldConfigPath).catch(noop) // drop errors if (oldConfig) { @@ -58,27 +58,24 @@ function noop () {} export async function get (configPath = CONFIG_PATH) { const configBuffer = await readFile(configPath) - + return deserialize(configBuffer.toString()) } -export function getSync (configPath = CONFIG_PATH) { - let configBuffer +export function getSync (configPath = CONFIG_PATH): EntropyConfig { try { - configBuffer = readFileSync(configPath, 'utf8') - } catch (error) { - if (error.message.includes('ENOENT: no such file or directory')) { - writeFileSync(configPath, '{}') - configBuffer = readFileSync(configPath, 'utf8') - } else { - throw error - } + const configBuffer = readFileSync(configPath, 'utf8') + return deserialize(configBuffer) + } catch (err) { + if (err.code !== 'ENOENT') throw err + + const newConfig = migrateData(allMigrations, {}) + writeFileSync(configPath, serialize(newConfig)) + return newConfig } - return deserialize(configBuffer) } -export async function set (config = {}, configPath = CONFIG_PATH) { +export async function set (config: EntropyConfig, configPath = CONFIG_PATH) { await mkdirp(dirname(configPath)) await writeFile(configPath, serialize(config)) } - diff --git a/src/config/types.ts b/src/config/types.ts index e176f689..7d4deb78 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -1,6 +1,9 @@ export interface EntropyConfig { accounts: EntropyAccountConfig[] - endpoints: { dev: string; 'test-net': string } + endpoints: { + dev: string; + 'test-net': string + } selectedAccount: string 'migration-version': string } diff --git a/src/flows/entropyFaucet/faucet.ts b/src/flows/entropyFaucet/faucet.ts index 0a9c3044..5e0c4e8c 100644 --- a/src/flows/entropyFaucet/faucet.ts +++ b/src/flows/entropyFaucet/faucet.ts @@ -75,9 +75,9 @@ export async function sendMoney ( faucetProgramPointer: string } ): Promise { - const BalanceService = new EntropyBalance(entropy, endpoint) + const balanceService = new EntropyBalance(entropy, endpoint) // check balance of faucet address - const balance = await BalanceService.getBalance(faucetAddress) + const balance = await balanceService.getBalance(faucetAddress) if (balance <= 0) throw new Error('FundsError: Faucet Account does not have funds') // check verifying key for only one program matching the program hash const programs = await viewPrograms(entropy, { verifyingKey: chosenVerifyingKey }) @@ -93,4 +93,4 @@ export async function sendMoney ( const transfer = entropy.substrate.tx.balances.transferAllowDeath(addressToSendTo, BigInt(amount)); const transferStatus = await faucetSignAndSend(transfer, entropy, parseInt(amount), faucetAddress, chosenVerifyingKey ) if (transferStatus.isFinalized) return transferStatus -} \ No newline at end of file +} diff --git a/src/flows/entropyFaucet/index.ts b/src/flows/entropyFaucet/index.ts index 7edbd310..9d5c4c95 100644 --- a/src/flows/entropyFaucet/index.ts +++ b/src/flows/entropyFaucet/index.ts @@ -1,5 +1,5 @@ import Entropy from "@entropyxyz/sdk" -import { findAccountNameByAddress, print } from "../../common/utils" +import { findAccountByAddressOrName, print } from "../../common/utils" import { initializeEntropy } from "../../common/initializeEntropy" import { EntropyLogger } from '../../common/logger' import { getRandomFaucet, sendMoney } from "./faucet" @@ -14,7 +14,7 @@ export async function entropyFaucet ({ accounts, selectedAccount: selectedAccoun let verifyingKeys: string[] = [] const amount = "10000000000" const { endpoint } = options - const selectedAccount = findAccountNameByAddress(accounts, selectedAccountAddress) + const selectedAccount = findAccountByAddressOrName(accounts, selectedAccountAddress) logger.log(`selectedAccount::`, FLOW_CONTEXT) logger.log(selectedAccount, FLOW_CONTEXT) try { @@ -42,4 +42,4 @@ export async function entropyFaucet ({ accounts, selectedAccount: selectedAccoun await entropyFaucet({ accounts, selectedAccount: selectedAccountAddress }, options, logger) } } -} \ No newline at end of file +} diff --git a/src/flows/programs/index.ts b/src/flows/programs/index.ts index d3b7b6af..d168ecd1 100644 --- a/src/flows/programs/index.ts +++ b/src/flows/programs/index.ts @@ -9,7 +9,7 @@ import { removeProgram } from "./remove"; import { addQuestions, getProgramPointerInput, verifyingKeyQuestion } from "./helpers/questions"; import { displayPrograms } from "./helpers/utils"; import { initializeEntropy } from "../../common/initializeEntropy" -import { findAccountNameByAddress, print } from "../../common/utils" +import { findAccountByAddressOrName, print } from "../../common/utils" import { EntropyLogger } from "../../common/logger"; import { EntropyTuiOptions } from "../../types" @@ -18,7 +18,7 @@ let verifyingKey: string; export async function userPrograms ({ accounts, selectedAccount: selectedAccountAddress }, options: EntropyTuiOptions, logger: EntropyLogger) { const FLOW_CONTEXT = 'PROGRAMS' const { endpoint } = options - const selectedAccount = findAccountNameByAddress(accounts, selectedAccountAddress) + const selectedAccount = findAccountByAddressOrName(accounts, selectedAccountAddress) const actionChoice = await inquirer.prompt([ { @@ -120,7 +120,7 @@ export async function userPrograms ({ accounts, selectedAccount: selectedAccount export async function devPrograms ({ accounts, selectedAccount: selectedAccountAddress }, options: EntropyTuiOptions, logger: EntropyLogger) { // const FLOW_CONTEXT = 'PROGRAMS' const { endpoint } = options - const selectedAccount = findAccountNameByAddress(accounts, selectedAccountAddress) + const selectedAccount = findAccountByAddressOrName(accounts, selectedAccountAddress) const choices = { "Deploy": deployProgramTUI, diff --git a/src/sign/interaction.ts b/src/sign/interaction.ts index 5a3c68ba..a68947a8 100644 --- a/src/sign/interaction.ts +++ b/src/sign/interaction.ts @@ -5,7 +5,7 @@ import Entropy from "@entropyxyz/sdk" import { EntropySign } from "./main" export async function entropySign (entropy: Entropy, endpoint: string) { - const SigningService = new EntropySign(entropy, endpoint) + const signingService = new EntropySign(entropy, endpoint) // const { interactionChoice } = await inquirer.prompt(interactionChoiceQuestions) // switch (interactionChoice) { // case 'Raw Sign': { @@ -25,7 +25,7 @@ export async function entropySign (entropy: Entropy, endpoint: string) { // } // case 'Sign With Adapter': { const { msg } = await getMsgFromUser(inquirer) - const { signature, verifyingKey } = await SigningService.signMessageWithAdapters({ msg }) + const { signature, verifyingKey } = await signingService.signMessageWithAdapters({ msg }) print('msg to be signed:', msg) print('verifying key:', verifyingKey) print('signature:', signature) diff --git a/src/transfer/command.ts b/src/transfer/command.ts index 509bf515..537f2149 100644 --- a/src/transfer/command.ts +++ b/src/transfer/command.ts @@ -1,4 +1,3 @@ -import Entropy from "@entropyxyz/sdk" import { Command } from "commander" import { currentAccountAddressOption, endpointOption, loadEntropy, passwordOption } from "src/common/utils-cli" import { EntropyTransfer } from "./main" @@ -14,8 +13,8 @@ export function entropyTransferCommand () { .addOption(currentAccountAddressOption()) .action(async (destination, amount, opts) => { const entropy = await loadEntropy(opts.account, opts.endpoint) - const TransferService = new EntropyTransfer(entropy, opts.endpoint) - await TransferService.transfer(destination, amount) + const transferService = new EntropyTransfer(entropy, opts.endpoint) + await transferService.transfer(destination, amount) // cliWrite(??) // TODO: write the output process.exit(0) }) diff --git a/src/transfer/interaction.ts b/src/transfer/interaction.ts index cd8e83b8..e0e43097 100644 --- a/src/transfer/interaction.ts +++ b/src/transfer/interaction.ts @@ -6,9 +6,9 @@ import { setupProgress } from "src/common/progress" export async function entropyTransfer (entropy, endpoint) { const progressTracker = setupProgress('Transferring Funds') - const TransferService = new EntropyTransfer(entropy, endpoint) + const transferService = new EntropyTransfer(entropy, endpoint) const { amount, recipientAddress } = await inquirer.prompt(transferInputQuestions) - await TransferService.transfer(recipientAddress, amount, progressTracker) + await transferService.transfer(recipientAddress, amount, progressTracker) print('') print(`Transaction successful: Sent ${amount} to ${recipientAddress}`) print('') diff --git a/src/tui.ts b/src/tui.ts index bc21dfdc..db18c149 100644 --- a/src/tui.ts +++ b/src/tui.ts @@ -4,10 +4,11 @@ import * as config from './config' import * as flows from './flows' import { EntropyTuiOptions } from './types' import { logo } from './common/ascii' -import { print, updateConfig } from './common/utils' +import { print } from './common/utils' import { loadEntropy } from './common/utils-cli' import { EntropyLogger } from './common/logger' -import { entropyManageAccounts, entropyRegister } from './account/interaction' + +import { entropyAccount, entropyRegister } from './account/interaction' import { entropySign } from './sign/interaction' import { entropyBalance } from './balance/interaction' import { entropyTransfer } from './transfer/interaction' @@ -17,9 +18,9 @@ async function setupConfig () { // set selectedAccount if we can if (!storedConfig.selectedAccount && storedConfig.accounts.length) { - await config.set({ - selectedAccount: storedConfig.accounts[0].address, - ...storedConfig + await config.set({ + ...storedConfig, + selectedAccount: storedConfig.accounts[0].address }) storedConfig = await config.get() } @@ -60,8 +61,8 @@ export default function tui (entropy: Entropy, options: EntropyTuiOptions) { } async function main (entropy: Entropy, choices, options, logger: EntropyLogger) { - let storedConfig = await setupConfig() - + const storedConfig = await setupConfig() + // If the selected account changes within the TUI we need to reset the entropy instance being used const currentAccount = entropy?.keyring?.accounts?.registration?.address if (currentAccount && currentAccount !== storedConfig.selectedAccount) { @@ -90,54 +91,36 @@ async function main (entropy: Entropy, choices, options, logger: EntropyLogger) logger.debug(answers) switch (answers.choice) { case 'Manage Accounts': { - const response = await entropyManageAccounts(options.endpoint, storedConfig) - returnToMain = await updateConfig(storedConfig, response) - storedConfig = await config.get() + const response = await entropyAccount(options.endpoint, storedConfig) + if (response === 'exit') { returnToMain = true } break } case 'Register': { - const { accounts, selectedAccount } = await entropyRegister(entropy, options.endpoint, storedConfig) - returnToMain = await updateConfig(storedConfig, { accounts, selectedAccount }) - storedConfig = await config.get() + await entropyRegister(entropy, options.endpoint, storedConfig) break } case 'Balance': { - try { - await entropyBalance(entropy, options.endpoint, storedConfig) - } catch (error) { - console.error('There was an error retrieving balance', error) - } + await entropyBalance(entropy, options.endpoint, storedConfig) + .catch(err => console.error('There was an error retrieving balance', err)) break } case 'Transfer': { - try { - await entropyTransfer(entropy, options.endpoint) - } catch (error) { - console.error('There was an error sending the transfer', error) - } + await entropyTransfer(entropy, options.endpoint) + .catch(err => console.error('There was an error sending the transfer', err)) break } case 'Sign': { - try { - await entropySign(entropy, options.endpoint) - } catch (error) { - console.error('There was an issue with signing', error) - } + await entropySign(entropy, options.endpoint) + .catch(err => console.error('There was an issue with signing', err)) break } default: { - const newConfigUpdates = await choices[answers.choice](storedConfig, options, logger) - if (typeof newConfigUpdates === 'string' && newConfigUpdates === 'exit') { - returnToMain = true - } else { - await config.set({ ...storedConfig, ...newConfigUpdates }) - } - storedConfig = await config.get() + throw Error(`unsupported choice: ${answers.choice}`) } } } - if (!returnToMain) { + if (returnToMain === undefined) { ({ returnToMain } = await inquirer.prompt([{ type: 'confirm', name: 'returnToMain', diff --git a/src/types/index.ts b/src/types/index.ts index af964cd7..d1411368 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,6 +1,7 @@ export interface EntropyTuiOptions { - dev: boolean + account: string endpoint: string + dev: boolean } type EntropyLoggerLogLevel = 'error' | 'warn' | 'info' | 'debug' @@ -9,4 +10,4 @@ export interface EntropyLoggerOptions { debug?: boolean level?: EntropyLoggerLogLevel isTesting?: boolean -} \ No newline at end of file +} diff --git a/tests/balance.test.ts b/tests/balance.test.ts index 3edf9aaa..269059e1 100644 --- a/tests/balance.test.ts +++ b/tests/balance.test.ts @@ -7,26 +7,26 @@ const networkType = 'two-nodes' test('getBalance + getBalances', async (t) => { const { run, entropy, endpoint } = await setupTest(t, { networkType }) - const BalanceService = new EntropyBalance(entropy, endpoint) + const balanceService = new EntropyBalance(entropy, endpoint) const newAddress = entropy.keyring.accounts.registration.address /* getBalance */ const newAddressBalance = await run( 'getBalance (newSeed)', - BalanceService.getBalance(newAddress) + balanceService.getBalance(newAddress) ) t.equal(newAddressBalance, 0, 'newSeed balance = 0') const richAddressBalance = await run( 'getBalance (richAddress)', - BalanceService.getBalance(richAddress) + balanceService.getBalance(richAddress) ) t.true(richAddressBalance > BigInt(10e10), 'richAddress balance >>> 0') /* getBalances */ const balances = await run( 'getBalances', - BalanceService.getBalances([newAddress, richAddress]) + balanceService.getBalances([newAddress, richAddress]) ) t.deepEqual( balances, @@ -40,7 +40,7 @@ test('getBalance + getBalances', async (t) => { const badAddresses = ['5Cz6BfUaxxXCA3jninzxdan4JdmC1NVpgkiRPYhXbhr', '5Cz6BfUaxxXCA3jninzxdan4JdmC1NVpgkiRPYhXbhrfnD'] const balancesWithNoGoodAddress = await run( 'getBalances::one good address', - BalanceService.getBalances(badAddresses) + balanceService.getBalances(badAddresses) ) badAddresses.forEach(addr => { diff --git a/tests/faucet.test.ts b/tests/faucet.test.ts index 255626cf..08c37eb9 100644 --- a/tests/faucet.test.ts +++ b/tests/faucet.test.ts @@ -12,9 +12,9 @@ test('Faucet Tests', async t => { const { run, entropy, endpoint } = await setupTest(t, { seed: charlieStashSeed }) const { entropy: naynayEntropy } = await setupTest(t) - const AccountService = new EntropyAccount(entropy, endpoint) - const BalanceService = new EntropyBalance(entropy, endpoint) - const TransferService = new EntropyTransfer(entropy, endpoint) + const accountService = new EntropyAccount(entropy, endpoint) + const balanceService = new EntropyBalance(entropy, endpoint) + const transferService = new EntropyTransfer(entropy, endpoint) const faucetProgram = readFileSync('tests/programs/faucet_program.wasm') @@ -40,13 +40,13 @@ test('Faucet Tests', async t => { // Confirm faucetPointer matches deployed program pointer t.equal(faucetProgramPointer, LOCAL_PROGRAM_HASH, 'Program pointer matches') - let entropyBalance = await BalanceService.getBalance(entropy.keyring.accounts.registration.address) + let entropyBalance = await balanceService.getBalance(entropy.keyring.accounts.registration.address) console.log('Balance Charlie::', entropyBalance); - let naynayBalance = await BalanceService.getBalance(naynayEntropy.keyring.accounts.registration.address) + let naynayBalance = await balanceService.getBalance(naynayEntropy.keyring.accounts.registration.address) t.equal(naynayBalance, 0, 'Naynay is broke af') // register with faucet program - await run('Register Faucet Program for charlie stash', AccountService.register( + await run('Register Faucet Program for charlie stash', accountService.register( { programModAddress: entropy.keyring.accounts.registration.address, programData: [{ program_pointer: faucetProgramPointer, program_config: userConfig }] @@ -55,13 +55,13 @@ test('Faucet Tests', async t => { const { chosenVerifyingKey, faucetAddress } = await getRandomFaucet(entropy, [], entropy.keyring.accounts.registration.address) // adding funds to faucet address - entropyBalance = await BalanceService.getBalance(entropy.keyring.accounts.registration.address) - const faucetAddressBalance = await BalanceService.getBalance(faucetAddress) + entropyBalance = await balanceService.getBalance(entropy.keyring.accounts.registration.address) + const faucetAddressBalance = await balanceService.getBalance(faucetAddress) console.log('Balance faucetAddress::', faucetAddressBalance); console.log('Balance charlie 2::', entropyBalance); - await run('Transfer funds to faucet address', TransferService.transfer(faucetAddress, "1000")) + await run('Transfer funds to faucet address', transferService.transfer(faucetAddress, "1000")) const transferStatus = await sendMoney( naynayEntropy, @@ -77,9 +77,9 @@ test('Faucet Tests', async t => { t.ok(transferStatus.isFinalized, 'Transfer is good') - naynayBalance = await BalanceService.getBalance(naynayEntropy.keyring.accounts.registration.address) + naynayBalance = await balanceService.getBalance(naynayEntropy.keyring.accounts.registration.address) t.ok(naynayBalance > 0, 'Naynay is drippin in faucet tokens') t.end() -}) \ No newline at end of file +}) diff --git a/tests/sign.test.ts b/tests/sign.test.ts index bdc567f7..e6f1fd5b 100644 --- a/tests/sign.test.ts +++ b/tests/sign.test.ts @@ -6,12 +6,12 @@ const endpoint = 'ws://127.0.0.1:9944' test('Sign - signMessageWithAdapters', async (t) => { const { run, entropy } = await setupTest(t, { seed: charlieStashSeed }) - const SigningService = new EntropySign(entropy, endpoint) + const signService = new EntropySign(entropy, endpoint) await run('register', entropy.register()) const result = await run( 'sign', - SigningService.signMessageWithAdapters({ msg: "heyo!" }) + signService.signMessageWithAdapters({ msg: "heyo!" }) ) t.true(result?.signature?.length > 32, 'signature has some body!') diff --git a/tests/transfer.test.ts b/tests/transfer.test.ts index c64a221a..4dda0800 100644 --- a/tests/transfer.test.ts +++ b/tests/transfer.test.ts @@ -1,5 +1,6 @@ import test from 'tape' import { wasmGlobalsReady } from '@entropyxyz/sdk' +// @ts-ignore import Keyring from '@entropyxyz/sdk/keys' import { makeSeed, @@ -43,31 +44,31 @@ test('Transfer', async (t) => { const naynayAddress = naynayEntropy.keyring.accounts.registration.address // Check initial balances - const BalanceService = new EntropyBalance(naynayEntropy, endpoint) + const balanceService = new EntropyBalance(naynayEntropy, endpoint) let naynayBalance = await run( 'getBalance (naynay)', - BalanceService.getBalance(naynayAddress) + balanceService.getBalance(naynayAddress) ) t.equal(naynayBalance, 0, 'naynay is broke') let charlieBalance = await run( 'getBalance (charlieStash)', - BalanceService.getBalance(charlieStashAddress) + balanceService.getBalance(charlieStashAddress) ) t.equal(charlieBalance, 1e17, 'charlie got bank') // Do transer - const TransferService = new EntropyTransfer(charlieEntropy, endpoint) + const transferService = new EntropyTransfer(charlieEntropy, endpoint) const inputAmount = "1.5" await run( 'transfer', - TransferService.transfer(naynayAddress, inputAmount) + transferService.transfer(naynayAddress, inputAmount) ) // Re-Check balance naynayBalance = await run( 'getBalance (naynay)', - BalanceService.getBalance(naynayAddress) + balanceService.getBalance(naynayAddress) ) const expected = Number(inputAmount) * 1e10 t.equal(naynayBalance, expected,'naynay is rolling in it!')