Skip to content

Commit

Permalink
Mixmix/cli (#46)
Browse files Browse the repository at this point in the history
* wip: addresses are the bane of my existence

* fixed up transfers, added progress bar

* converting hex to bigint to be able use the right value when transferring

* wip: started changes to cli for new sdk, finally got the sdk to link to this project

* wip: update balance check to work with new sdk

* wip: updated transfer with new sdk methods; updated new key creation with correct methods from new sdk

* Improves README readability. (#42)

* Improves README readability.

* Update README.md

Co-authored-by: Nayyir Jutha <nayyir.jutha@gmail.com>

---------

Co-authored-by: Nayyir Jutha <nayyir.jutha@gmail.com>

* remove deps, move files, add license, fix package.json

* linting hooks, tidy

* mostly fix types?

* replace console.log > debug

* improve debug by stringifying output

* Mixmix/types+lint (#44)

* Improves README readability. (#42)

* Improves README readability.

* Update README.md

Co-authored-by: Nayyir Jutha <nayyir.jutha@gmail.com>

---------

Co-authored-by: Nayyir Jutha <nayyir.jutha@gmail.com>

* remove deps, move files, add license, fix package.json

* linting hooks, tidy

* mostly fix types?

---------

Co-authored-by: Johnny <9611008+johnnymatthews@users.noreply.github.com>
Co-authored-by: Nayyir Jutha <nayyir.jutha@gmail.com>

* start setting up bin scrupts

* removing zaps; changing language around wallet

* wip: getting register ready

* ignor built files

* wip: "singletonish" entropy and keyring

* wip: pulling in frankies changes with a bit of cleanup

* install commander

* fixups

* more tidy

* add transfer sketch

* first pass on balance

* support encrypted accounts

* wip: minor fixes, removing mnemonic from key generation

* wip: setting up keyrings

* wip: updated main to initialize with selecting or create new account

* wip: initializing entropy in main, passing new entropy to flows

* wip: can get acct on cli initialization; entropy intitialized; entropy passed to balance flow for now

* wip: remvoed use of wallet namespace (switched to manage accounts); main is now updated to what is expected when talking about having a selected acct or force user to create a new one

* wip: balance is now working with the new flow

* wip: cleanup of manage accounts folder

* wip: updating register to use account selected from config

* undo some changes

* Mixmix/debug (#45)

* Improves README readability. (#42)

* Improves README readability.

* Update README.md

Co-authored-by: Nayyir Jutha <nayyir.jutha@gmail.com>

---------

Co-authored-by: Nayyir Jutha <nayyir.jutha@gmail.com>

* remove deps, move files, add license, fix package.json

* linting hooks, tidy

* mostly fix types?

* replace console.log > debug

* improve debug by stringifying output

* fixups

* more tidy

* undo some changes

* Update src/common/initializeEntropy.ts

Co-authored-by: Nayyir Jutha <nayyir.jutha@gmail.com>

---------

Co-authored-by: Johnny <9611008+johnnymatthews@users.noreply.github.com>
Co-authored-by: Nayyir Jutha <nayyir.jutha@gmail.com>

* add cli list

* first pass transfer

* wip: registration looks to be working with properly created acct

* wip: user programs added fucntion to check if program exists on endpoint

* wip: registration and removing a program

* wip

* install pre-release sdk

* get transfer working

* wip: trying to debug accounts their were some bugs i was finding when creating a new account i also added the abbilty to create a debug account when importing by adding `#debug` to the end of the seed

* wip: cleanup from frankies changes

* wip: more cleanup and added no op program to filesystem

* wip: registering is working with default key proxy and adding verifying keys to list

* wip: silently handling error, logging an error message

* wip: debugging undefined error in subscriber

* wip: removing hardcoded debug from initialize entropy, already available on account data

* wip: new release for sdk

* wip: fixing account flows

* replace console.log with "print"

* wip

* wip

* got balances woorking

* fix register

* sdk upgrade: has key fix potentioally, also seems like their was some types that were not updated

* fix accounts?

* fix accounts

* temp: jhonny plz coppy console.log

* remove temp commit

* fix refference

* Update src/entropy.ts

Co-authored-by: Nayyir Jutha <nayyir.jutha@gmail.com>

* tidy up

* tidy

* fix endpoints logic

* yarn.lock

* add bundling to ts

* add commander

* tidy tui

* tidyups

* Update tsconfig.json

Co-authored-by: Frankie <frankie.diamond@gmail.com>

* Update tsconfig.json

Co-authored-by: Frankie <frankie.diamond@gmail.com>

* Update tsconfig.json

Co-authored-by: Frankie <frankie.diamond@gmail.com>

* fixups

* fixup start command

* lock fix

---------

Co-authored-by: Nayyir Jutha <nayyir@entropy.xyz>
Co-authored-by: Johnny <9611008+johnnymatthews@users.noreply.github.com>
Co-authored-by: Nayyir Jutha <nayyir.jutha@gmail.com>
Co-authored-by: frankie <frankie.diamond@gmail.com>
  • Loading branch information
5 people authored Jun 12, 2024
1 parent b27a9e5 commit de39fd9
Show file tree
Hide file tree
Showing 19 changed files with 494 additions and 72 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,15 @@ Start an interactive interface:
entropy
```

Output current balances:
```bash
entropy balance
```

See help on programmatic usage:
```bash
entropy --help # all commands
entropy balance --help # a specific command
```

## Build and run
Expand Down
73 changes: 72 additions & 1 deletion src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@

/* NOTE: calling this file entropy.ts helps commander parse process.argv */
import { Command, Option } from 'commander'
import launchTui from './index'
import launchTui from './tui'
import * as config from './config'
import { EntropyTuiOptions } from './types'

import { cliGetBalance } from './flows/balance/cli'
import { cliListAccounts } from './flows/manage-accounts/cli'
import { cliEntropyTransfer } from './flows/entropyTransfer/cli'
import { cliSign } from './flows/sign/cli'
// import { debug } from './common/utils'

const program = new Command()
Expand Down Expand Up @@ -34,6 +38,13 @@ function endpointOption (){
// 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 <password>',
description || 'Password for the account'
)
}

/* no command */
program
.name('entropy')
Expand All @@ -51,4 +62,64 @@ program
launchTui(options)
})

/* list */
program.command('list')
.alias('ls')
.description('List all accounts. Output is JSON of form [{ name, address, data }]')
.action(async () => {
// TODO: test if it's an encrypted account, if no password provided, throw because later on there's no protection from a prompt coming up

const accounts = await cliListAccounts()
writeOut(accounts)
process.exit(0)
})

/* balance */
program.command('balance')
.description('Get the balance of an Entropy account. Output is a number')
.argument('address', 'Account address whose balance you want to query')
.addOption(passwordOption())
.addOption(endpointOption())
.action(async (address, opts) => {
const balance = await cliGetBalance({ address, ...opts })
writeOut(balance)
process.exit(0)
})

/* Transfer */
program.command('transfer')
.description('Transfer funds between two Entropy accounts.') // TODO: name the output
.argument('source', 'Account address funds will be drawn from')
.argument('destination', 'Account address funds will be sent to')
.argument('amount', 'Amount of funds to be moved')
.addOption(passwordOption('Password for the source account (if required)'))
.addOption(endpointOption())
.action(async (source, destination, amount, opts) => {
await cliEntropyTransfer({ source, destination, amount, ...opts })
// writeOut(??) // TODO: write the output
process.exit(0)
})

/* Sign */
program.command('sign')
.description('Sign a message using the Entropy network. Output is a signature (string)')
.argument('address', 'Account address to use to sign')
.argument('message', 'Message you would like to sign')
.addOption(passwordOption('Password for the source account (if required)'))
.addOption(endpointOption())
.action(async (address, message, opts) => {
const signature = await cliSign({ address, message, ...opts })
writeOut(signature)
process.exit(0)
})



function writeOut (result) {
const prettyResult = typeof result === 'object'
? JSON.stringify(result, null, 2)
: result
process.stdout.write(prettyResult)
}

program.parse()
138 changes: 89 additions & 49 deletions src/common/initializeEntropy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import inquirer from "inquirer"
import { decrypt, encrypt } from "../flows/password"
import { debug } from "../common/utils"
import * as config from "../config"
import { EntropyAccountData } from "../types"

// TODO: unused
// let defaultAccount // have a main account to use
Expand All @@ -19,69 +20,37 @@ const keyrings = {
export function getKeyring (address) {
if (!address && keyrings.default) return keyrings.default
if (address && keyrings[address]) return keyrings[address]
// If there is no default keyring and no keyring matching the address
// provided, return undefined instead of keyring.default
return undefined
return keyrings.default
}

export const initializeEntropy = async ({ keyMaterial }, endpoint: string): Promise<Entropy> => {
interface InitializeEntropyOpts {
keyMaterial: MaybeKeyMaterial,
password?: string,
endpoint: string
}
type MaybeKeyMaterial = EntropyAccountData | string

// WARNING: in programatic cli mode this function should NEVER prompt users, but it will if no password was provided
// This is currently caught earlier in the code
export const initializeEntropy = async ({ keyMaterial, password, endpoint }: InitializeEntropyOpts): Promise<Entropy> => {
try {
// if (defaultAccount && defaultAccount.seed === keyMaterial.seed) return entropys[defaultAccount.registering.address]
await wasmGlobalsReady()
let password

let accountData
if (keyMaterial && typeof keyMaterial === 'object' && 'seed' in keyMaterial) {
accountData = keyMaterial
} else if (typeof keyMaterial === 'string') {

let decryptedData
let attempts = 0
// TO-DO: this should be a generator function not a while loop
while (attempts < 3) {
const answers = await inquirer.prompt([
{
type: 'password',
name: 'password',
message: 'Enter password to decrypt keyMaterial:',
mask: '*',
}
])

try {
decryptedData = decrypt(keyMaterial, answers.password)
//@ts-ignore
if (!decryptedData || typeof decryptedData !== 'object' || !('seed' in decryptedData)) {
throw new Error("Failed to decrypt keyMaterial or decrypted keyMaterial is invalid")
}
password = answers.password
break
} catch (error) {
console.error("Incorrect password. Try again")
attempts++
if (attempts >= 3) {
throw new Error("Failed to decrypt keyMaterial after 3 attempts.")
}
}
}

accountData = decryptedData
} else {
throw new Error("Data format is not recognized as either encrypted or unencrypted")
}

const { accountData, password: successfulPassword } = await getAccountDataAndPassword(keyMaterial, password)
if (!accountData.seed || !accountData.admin) {
throw new Error("Data format is not recognized as either encrypted or unencrypted")
}

if (accountData && accountData.admin && !accountData.registration) {
accountData.registration = accountData.admin
accountData.registration.used = true
accountData.registration.used = true // TODO: is this even used?
const store = await config.get()
store.accounts = store.accounts.map((account) => {
if (account.address === accountData.admin.address) {
let data = accountData
if (typeof account.data === 'string' ) data = encrypt(accountData, password)
// @ts-ignore
if (typeof account.data === 'string' ) data = encrypt(accountData, successfulPassword)
account = {
...account,
data,
Expand All @@ -103,7 +72,7 @@ export const initializeEntropy = async ({ keyMaterial }, endpoint: string): Prom
store.accounts = store.accounts.map((account) => {
if (account.address === store.selectedAccount) {
let data = newAccountData
if (typeof account.data === 'string' ) data = encrypt(newAccountData, password)
if (typeof account.data === 'string') data = encrypt(newAccountData, successfulPassword)
const newAccount = {
...account,
data,
Expand All @@ -112,7 +81,7 @@ export const initializeEntropy = async ({ keyMaterial }, endpoint: string): Prom
}
return account
})

// re save the entire config
await config.set(store)

Expand Down Expand Up @@ -145,3 +114,74 @@ export const initializeEntropy = async ({ keyMaterial }, endpoint: string): Prom
}
}
}


// NOTE: frankie this was prettier before I had to refactor it for merge conflicts, promise
async function getAccountDataAndPassword (keyMaterial: MaybeKeyMaterial, password?: string): Promise<{ password: string | null, accountData: EntropyAccountData }> {
if (isEntropyAccountData(keyMaterial)) {
return {
password: null,
accountData: keyMaterial as EntropyAccountData
}
}

if (typeof keyMaterial !== 'string') {
throw new Error("Data format is not recognized as either encrypted or unencrypted")
}

/* Programmatic Mode */
if (password) {
const decryptedData = decrypt(keyMaterial, password)
if (!isEntropyAccountData(decryptedData)) {
throw new Error("Failed to decrypt keyMaterial or decrypted keyMaterial is invalid")
}
// @ts-ignore TODO: some type work here
return { password, accountData: decryptedData }
}

/* Interactive Mode */
let sucessfulPassword: string
let decryptedData
let attempts = 0

while (attempts < 3) {
const answers = await inquirer.prompt([
{
type: 'password',
name: 'password',
message: 'Enter password to decrypt keyMaterial:',
mask: '*',
}
])

try {
decryptedData = decrypt(keyMaterial, answers.password)
//@ts-ignore
if (!isEntropyAccountData(decryptedData)) {
throw new Error("Failed to decrypt keyMaterial or decrypted keyMaterial is invalid")
}

sucessfulPassword = answers.password
break
} catch (error) {
console.error("Incorrect password. Try again")
attempts++
if (attempts >= 3) {
throw new Error("Failed to decrypt keyMaterial after 3 attempts.")
}
}
}

return {
password: sucessfulPassword,
accountData: decryptedData as EntropyAccountData
}
}

function isEntropyAccountData (maybeAccountData: any) {
return (
maybeAccountData &&
typeof maybeAccountData === 'object' &&
'seed' in maybeAccountData
)
}
2 changes: 1 addition & 1 deletion src/common/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export function pubToAddress (publicKey: string): string {
}

export const formatAmountAsHex = (amount: number) => {
return `${PREFIX}${(amount * (1 * (10 ** DECIMALS))).toString(16)}`
return `${PREFIX}${(amount * (10 ** DECIMALS)).toString(16)}`
}

export function getActiveOptions (options) {
Expand Down
8 changes: 4 additions & 4 deletions src/flows/DeployPrograms/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import Entropy from "@entropyxyz/sdk"
import * as util from "@polkadot/util"
import inquirer from "inquirer"
import { readFileSync } from "fs"
import { initializeEntropy } from "../../common/initializeEntropy"
import { debug, print, getSelectedAccount } from "../../common/utils"
import { readFileSync } from "fs"

export async function devPrograms ({ accounts, selectedAccount: selectedAccountAddress }, options) {
const { endpoint } = options
Expand All @@ -24,10 +24,10 @@ export async function devPrograms ({ accounts, selectedAccount: selectedAccountA
},
])

const entropy = await initializeEntropy(
{ keyMaterial: selectedAccount.data },
const entropy = await initializeEntropy({
keyMaterial: selectedAccount.data,
endpoint
)
})

const flow = choices[actionChoice.action]
await flow(entropy, selectedAccount)
Expand Down
10 changes: 4 additions & 6 deletions src/flows/UserPrograms/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ export async function userPrograms ({ accounts, selectedAccount: selectedAccount
},
])

const entropy = await initializeEntropy(
{ keyMaterial: selectedAccount.data },
const entropy = await initializeEntropy({
keyMaterial: selectedAccount.data,
endpoint
)
})

if (!entropy.registrationManager?.signer?.pair) {
throw new Error("Keys are undefined")
Expand Down Expand Up @@ -77,9 +77,7 @@ export async function userPrograms ({ accounts, selectedAccount: selectedAccount
validate: (input) => (input ? true : "Program pointer is required!"),
}])
debug('program pointer', programPointer);

const program = await entropy.programs.dev.getProgramInfo(programPointer);
debug('Program from:', programPointer);
const program = await entropy.programs.dev.get(programPointer);
print(program);
} catch (error) {
console.error(error.message);
Expand Down
26 changes: 26 additions & 0 deletions src/flows/balance/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { initializeEntropy } from '../../common/initializeEntropy'
import * as config from '../../config'
import { debug } from '../../common/utils'

const hexToBigInt = (hexString: string) => BigInt(hexString)

export async function cliGetBalance ({ address, password, endpoint }) {
const storedConfig = await config.get()
const account = storedConfig.accounts.find(account => account.address === address)
if (!account) throw Error(`No account with address ${address}`)
// QUESTION: is throwing the right response?
debug('account', account)

// check if data is encrypted + we have a password
if (typeof account.data === 'string' && !password) {
throw Error('This account requires a password, add --password <password>')
}

const entropy = await initializeEntropy({ keyMaterial: account.data, password, endpoint })

const accountInfo = (await entropy.substrate.query.system.account(address)) as any
debug('accountInfo', accountInfo)

return hexToBigInt(accountInfo.data.free).toString()
}

2 changes: 1 addition & 1 deletion src/flows/balance/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export async function checkBalance ({ accounts, selectedAccount: selectedAccount
debug('endpoint', endpoint);

const selectedAccount = getSelectedAccount(accounts, selectedAccountAddress)
const entropy = await initializeEntropy({ keyMaterial: selectedAccount.data }, endpoint);
const entropy = await initializeEntropy({ keyMaterial: selectedAccount.data, endpoint });
const accountAddress = selectedAccountAddress
// @ts-ignore
const accountInfo = (await entropy.substrate.query.system.account(accountAddress)) as any
Expand Down
Loading

0 comments on commit de39fd9

Please sign in to comment.