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()