Skip to content

Commit

Permalink
Merge pull request #231 from entropyxyz/naynay/file-restructure
Browse files Browse the repository at this point in the history
[NayNay] CLI/TUI File Restructure
  • Loading branch information
frankiebee authored Oct 2, 2024
2 parents c855201 + f3412d2 commit dd751e4
Show file tree
Hide file tree
Showing 85 changed files with 1,917 additions and 1,369 deletions.
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,33 @@ Version header format: `[version] Name - year-month-day (entropy-core compatibil
- new: 'src/flows/user-program-management/view.ts' - service file for pure functions of viewing user programs
- new: 'src/flows/user-program-management/helpers/utils.ts' - utility helper file for user program management specific methods
- new: './src/flows/user-program-management/remove.ts' - service file for removing user program pure function
- new: './src/common/entropy-base.ts' - base abstract class for new command classes
- new: './src/balance' - new file structure for our CLI/TUI flows
- new: './src/balance/main.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/transfer' - new file structure for our CLI/TUI flows
- new: './src/transfer/main.ts' - main entry file for transfer command for tui/cli
- new: './src/transfer/utils.ts' - utilities and helper methods for all things transfer
- new: './src/account' - new file structure for our CLI/TUI flows
- new: './src/account/main.ts' - main entry file for accounts command for tui/cli
- new: './src/account/utils.ts' - utilities and helper methods for all things accounts
- new: './src/faucet' - new file structure for our CLI/TUI flows
- new: './src/faucet/main.ts' - main entry file for faucet methods used for command and interaction files
- new: './src/faucet/utils.ts' - utilities and helper methods for all things faucet
- new: './src/faucet/interaction.ts' - main entrypoint for TUI flows
- new: './src/faucet/command.ts' - main entrypoint for CLI flows

### Changed

- folder name for user programs to match the kebab-case style for folder namespace
- updated SDK version to v0.2.3
- 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/entropyTransfer/*.ts directory with file restructure
- removed flows/manage-accounts/*/*.ts directory with file restructure
- removed flows/register/*.ts directory with file restructure
- removed flow/entropyFaucet/*.ts directory with file restructure


### Broke
Expand Down
114 changes: 114 additions & 0 deletions src/account/command.ts
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)
})
}
36 changes: 36 additions & 0 deletions src/account/constants.ts
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' }
]
}
}
91 changes: 91 additions & 0 deletions src/account/interaction.ts
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.")
}
109 changes: 109 additions & 0 deletions src/account/main.ts
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)
}
})
})
}
}
Loading

0 comments on commit dd751e4

Please sign in to comment.