-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #231 from entropyxyz/naynay/file-restructure
[NayNay] CLI/TUI File Restructure
- Loading branch information
Showing
85 changed files
with
1,917 additions
and
1,369 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
import Entropy from "@entropyxyz/sdk" | ||
import { Command, Option } from 'commander' | ||
import { EntropyAccount } from "./main"; | ||
import { selectAndPersistNewAccount, addVerifyingKeyToAccountAndSelect } from "./utils"; | ||
import { ACCOUNTS_CONTENT } from './constants' | ||
import * as config from '../config' | ||
import { cliWrite, accountOption, endpointOption, loadEntropy, passwordOption } from "../common/utils-cli"; | ||
|
||
export function entropyAccountCommand () { | ||
return new Command('account') | ||
.description('Commands to work with accounts on the Entropy Network') | ||
.addCommand(entropyAccountCreate()) | ||
.addCommand(entropyAccountImport()) | ||
.addCommand(entropyAccountList()) | ||
.addCommand(entropyAccountRegister()) | ||
} | ||
|
||
function entropyAccountCreate () { | ||
return new Command('create') | ||
.alias('new') | ||
.description('Create a new entropy account from scratch. Output is JSON of form {name, address}') | ||
.addOption(passwordOption()) | ||
.argument('<name>', 'A user friendly name for your new account.') | ||
.addOption( | ||
new Option( | ||
'--path', | ||
'Derivation path' | ||
).default(ACCOUNTS_CONTENT.path.default) | ||
) | ||
.action(async (name, opts) => { | ||
const { path } = opts | ||
const newAccount = await EntropyAccount.create({ name, path }) | ||
|
||
await selectAndPersistNewAccount(newAccount) | ||
|
||
cliWrite({ | ||
name: newAccount.name, | ||
address: newAccount.address | ||
}) | ||
process.exit(0) | ||
}) | ||
} | ||
|
||
function entropyAccountImport () { | ||
return new Command('import') | ||
.description('Import an existing entropy account from seed. Output is JSON of form {name, address}') | ||
.addOption(passwordOption()) | ||
.argument('<name>', 'A user friendly name for your new account.') | ||
.argument('<seed>', '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 selectAndPersistNewAccount(newAccount) | ||
|
||
cliWrite({ | ||
name: newAccount.name, | ||
address: newAccount.address | ||
}) | ||
process.exit(0) | ||
}) | ||
} | ||
|
||
function entropyAccountList () { | ||
return new Command('list') | ||
.alias('ls') | ||
.description('List all accounts. Output is JSON of form [{ name, address, verifyingKeys }]') | ||
.action(async () => { | ||
// TODO: test if it's an encrypted account, if no password provided, throw because later on there's no protection from a prompt coming up | ||
const storedConfig = await config.get() | ||
const accounts = EntropyAccount.list(storedConfig) | ||
cliWrite(accounts) | ||
process.exit(0) | ||
}) | ||
} | ||
|
||
/* register */ | ||
function entropyAccountRegister () { | ||
return new Command('register') | ||
.description('Register an entropy account with a program') | ||
.addOption(passwordOption()) | ||
.addOption(endpointOption()) | ||
.addOption(accountOption()) | ||
// 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) => { | ||
// NOTE: loadEntropy throws if it can't find opts.account | ||
const entropy: Entropy = await loadEntropy(opts.account, opts.endpoint) | ||
const accountService = new EntropyAccount(entropy, opts.endpoint) | ||
|
||
const verifyingKey = await accountService.register() | ||
await addVerifyingKeyToAccountAndSelect(verifyingKey, opts.account) | ||
|
||
cliWrite(verifyingKey) | ||
process.exit(0) | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
export const FLOW_CONTEXT = 'ENTROPY_ACCOUNT' | ||
|
||
export const ACCOUNTS_CONTENT = { | ||
seed: { | ||
name: 'seed', | ||
message: 'Enter seed:', | ||
invalidSeed: 'Seed provided is not valid' | ||
}, | ||
path: { | ||
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: 'interactionChoice', | ||
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' } | ||
] | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
import inquirer from "inquirer"; | ||
import Entropy from "@entropyxyz/sdk"; | ||
|
||
import { EntropyAccount } from './main' | ||
import { selectAndPersistNewAccount, addVerifyingKeyToAccountAndSelect } from "./utils"; | ||
import { findAccountByAddressOrName, print } from "../common/utils" | ||
import { EntropyConfig } from "../config/types"; | ||
import * as config from "../config"; | ||
|
||
import { | ||
accountManageQuestions, | ||
accountNewQuestions, | ||
accountSelectQuestions | ||
} from "./utils" | ||
|
||
/* | ||
* @returns partialConfigUpdate | "exit" | undefined | ||
*/ | ||
export async function entropyAccount (endpoint: string, storedConfig: EntropyConfig) { | ||
const { accounts } = storedConfig | ||
const { interactionChoice } = await inquirer.prompt(accountManageQuestions) | ||
|
||
switch (interactionChoice) { | ||
|
||
case 'create-import': { | ||
const answers = await inquirer.prompt(accountNewQuestions) | ||
const { name, path, importKey } = answers | ||
let { seed } = answers | ||
if (importKey && seed.includes('#debug')) { | ||
// isDebugMode = true | ||
seed = seed.split('#debug')[0] | ||
} | ||
|
||
const newAccount = seed | ||
? await EntropyAccount.import({ seed, name, path }) | ||
: await EntropyAccount.create({ name, path }) | ||
|
||
await selectAndPersistNewAccount(newAccount) | ||
return | ||
} | ||
|
||
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(accountSelectQuestions(accounts)) | ||
await config.set({ | ||
...storedConfig, | ||
selectedAccount: selectedAccount.address | ||
}) | ||
|
||
print('Current selected account is ' + selectedAccount) | ||
return | ||
} | ||
|
||
case 'list-account': { | ||
try { | ||
EntropyAccount.list({ accounts }) | ||
.forEach((account) => print(account)) | ||
} catch (error) { | ||
console.error(error.message.split('AccountsError: ')[1]) | ||
} | ||
return | ||
} | ||
|
||
case 'exit': { | ||
return 'exit' | ||
} | ||
|
||
default: | ||
throw new Error('AccountsError: Unknown interaction action') | ||
} | ||
} | ||
|
||
export async function entropyRegister (entropy: Entropy, endpoint: string, storedConfig: EntropyConfig): Promise<Partial<EntropyConfig>> { | ||
const accountService = new EntropyAccount(entropy, endpoint) | ||
|
||
const { accounts, selectedAccount } = storedConfig | ||
const account = findAccountByAddressOrName(accounts, selectedAccount) | ||
if (!account) { | ||
print("No account selected to register") | ||
return | ||
} | ||
|
||
print("Attempting to register the address:", account.address) | ||
const verifyingKey = await accountService.register() | ||
await addVerifyingKeyToAccountAndSelect(verifyingKey, account.address) | ||
|
||
print("Your address", account.address, "has been successfully registered.") | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
import Entropy, { wasmGlobalsReady } from "@entropyxyz/sdk"; | ||
// @ts-expect-error | ||
import Keyring from '@entropyxyz/sdk/keys' | ||
import { randomAsHex } from '@polkadot/util-crypto' | ||
|
||
import { FLOW_CONTEXT } from "./constants"; | ||
import { AccountCreateParams, AccountImportParams, AccountRegisterParams } from "./types"; | ||
|
||
import { EntropyBase } from "../common/entropy-base"; | ||
import { EntropyAccountConfig } from "../config/types"; | ||
|
||
export class EntropyAccount extends EntropyBase { | ||
constructor (entropy: Entropy, endpoint: string) { | ||
super({ entropy, endpoint, flowContext: FLOW_CONTEXT }) | ||
} | ||
|
||
static async create ({ name, path }: AccountCreateParams): Promise<EntropyAccountConfig> { | ||
const seed = randomAsHex(32) | ||
return EntropyAccount.import({ name, seed, path }) | ||
} | ||
|
||
static async import ({ name, seed, path }: AccountImportParams ): Promise<EntropyAccountConfig> { | ||
// 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 | ||
const { admin } = keyring.getAccount() | ||
|
||
const data = fullAccount | ||
delete admin.pair | ||
// const encryptedData = password ? passwordFlow.encrypt(data, password) : data | ||
|
||
return { | ||
name, | ||
address: admin.address, | ||
data | ||
// data: encryptedData // TODO: replace once password input is added back | ||
} | ||
} | ||
|
||
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' | ||
) | ||
|
||
return accounts.map((account: EntropyAccountConfig) => ({ | ||
name: account.name, | ||
address: account.address, | ||
verifyingKeys: account?.data?.admin?.verifyingKeys | ||
})) | ||
} | ||
|
||
async register (params?: AccountRegisterParams): Promise<string> { | ||
let programModAddress: string | ||
let programData: any | ||
if (params) { | ||
({ programModAddress, programData } = params) | ||
} | ||
const registerParams = programModAddress && programData | ||
? { | ||
programDeployer: programModAddress, | ||
programData | ||
} | ||
: undefined | ||
|
||
this.logger.debug(`registering with params: ${registerParams}`, 'REGISTER') | ||
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 | ||
}) | ||
} | ||
|
||
/* PRIVATE */ | ||
|
||
private async pruneRegistration () { | ||
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) | ||
} | ||
}) | ||
}) | ||
} | ||
} |
Oops, something went wrong.