From 4b802e0bfcc03ca93cdeba8a6b58714093235a67 Mon Sep 17 00:00:00 2001 From: Nayyir Jutha Date: Wed, 31 Jul 2024 15:46:59 -0400 Subject: [PATCH 01/25] [NayNay] File Restructure: Base Class + Balance Restructure - created new base entropy class to handle the shared nature of the intended use of the class - restructured balance to be in its own parent directory in the root of src - created command and util file to contain pure functions and controller functions --- src/balance/command.ts | 21 +++++++++++++++ src/balance/constants.ts | 1 + src/{flows => }/balance/types.ts | 0 src/balance/utils.ts | 44 +++++++++++++++++++++++++++++++ src/common/entropy-base.ts | 12 +++++++++ src/common/logger.ts | 2 +- src/flows/balance/balance.ts | 36 ------------------------- src/flows/balance/cli.ts | 9 ++++--- src/flows/balance/index.ts | 11 +++++--- tests/balance.test.ts | 14 +++++----- tests/testing-utils/setup-test.ts | 12 +++++---- tests/transfer.test.ts | 14 +++++----- 12 files changed, 114 insertions(+), 62 deletions(-) create mode 100644 src/balance/command.ts create mode 100644 src/balance/constants.ts rename src/{flows => }/balance/types.ts (100%) create mode 100644 src/balance/utils.ts create mode 100644 src/common/entropy-base.ts delete mode 100644 src/flows/balance/balance.ts diff --git a/src/balance/command.ts b/src/balance/command.ts new file mode 100644 index 00000000..09f96d4f --- /dev/null +++ b/src/balance/command.ts @@ -0,0 +1,21 @@ +import Entropy from "@entropyxyz/sdk" +import { EntropyBase } from "../common/entropy-base" +import { BalanceService } from "./utils" +import { FLOW_CONTEXT } from "./constants" + +export class BalanceController extends EntropyBase { + private readonly balanceService: BalanceService + + constructor (entropy: Entropy, endpoint: string) { + super(entropy, endpoint, FLOW_CONTEXT) + this.balanceService = new BalanceService(this.entropy, endpoint) + } + + public async getBalance (address: string) { + const balance = await this.balanceService.getBalance(address) + + this.logger.log(`Current balance of ${address}: ${balance}`, `${BalanceController.name}`) + + return `${balance.toLocaleString('en-US')} BITS` + } +} \ No newline at end of file diff --git a/src/balance/constants.ts b/src/balance/constants.ts new file mode 100644 index 00000000..204e8cd2 --- /dev/null +++ b/src/balance/constants.ts @@ -0,0 +1 @@ +export const FLOW_CONTEXT = 'ENTROPY-BALANCE' \ No newline at end of file diff --git a/src/flows/balance/types.ts b/src/balance/types.ts similarity index 100% rename from src/flows/balance/types.ts rename to src/balance/types.ts diff --git a/src/balance/utils.ts b/src/balance/utils.ts new file mode 100644 index 00000000..4b809dcd --- /dev/null +++ b/src/balance/utils.ts @@ -0,0 +1,44 @@ +import Entropy from "@entropyxyz/sdk"; +import { EntropyBase } from "../common/entropy-base"; +import { BalanceInfo } from "./types"; +import { FLOW_CONTEXT } from "./constants"; + +const hexToBigInt = (hexString: string) => BigInt(hexString) + +export class BalanceService extends EntropyBase { + constructor (entropy: Entropy, endpoint: string) { + super(entropy, endpoint, FLOW_CONTEXT) + } + + public async getBalance (address: string): Promise { + try { + const accountInfo = (await this.entropy.substrate.query.system.account(address)) as any + + return parseInt(hexToBigInt(accountInfo.data.free).toString()) + } catch (error) { + this.logger.error(`There was an error getting balance for [acct = ${address}]`, error); + throw new Error(error.message) + } + } + + public async getBalances (addresses: string[]): Promise { + const balanceInfo: BalanceInfo = {} + try { + await Promise.all(addresses.map(async address => { + try { + const balance = await this.getBalance(address) + + balanceInfo[address] = { balance } + } catch (error) { + this.logger.error(`Error retrieving balance for ${address}`, error); + balanceInfo[address] = { error: error.message } + } + })) + + return balanceInfo + } catch (error) { + this.logger.error(`There was an error getting balances for [${addresses}]`, error); + throw new Error(error.message) + } + } +} \ No newline at end of file diff --git a/src/common/entropy-base.ts b/src/common/entropy-base.ts new file mode 100644 index 00000000..1e14bf3b --- /dev/null +++ b/src/common/entropy-base.ts @@ -0,0 +1,12 @@ +import Entropy from "@entropyxyz/sdk"; +import { EntropyLogger } from "./logger"; + +export abstract class EntropyBase { + protected logger: EntropyLogger + protected entropy: Entropy + + constructor (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/logger.ts b/src/common/logger.ts index 7f112143..14a428e6 100644 --- a/src/common/logger.ts +++ b/src/common/logger.ts @@ -117,7 +117,7 @@ export class EntropyLogger { this.winstonLogger.log({ level, message: maskPayload(message), - context: context || this.context, + context: context ? `${this.context}:${context}` : this.context, endpoint: this.endpoint, description, stack, diff --git a/src/flows/balance/balance.ts b/src/flows/balance/balance.ts deleted file mode 100644 index d2a5ce3b..00000000 --- a/src/flows/balance/balance.ts +++ /dev/null @@ -1,36 +0,0 @@ -import Entropy from "@entropyxyz/sdk"; -import { BalanceInfo } from "./types"; - -const hexToBigInt = (hexString: string) => BigInt(hexString) - -export async function getBalance (entropy: Entropy, address: string): Promise { - try { - const accountInfo = (await entropy.substrate.query.system.account(address)) as any - - return parseInt(hexToBigInt(accountInfo.data.free).toString()) - } catch (error) { - // console.error(`There was an error getting balance for [acct = ${address}]`, error); - throw new Error(error.message) - } -} - -export async function getBalances (entropy: Entropy, addresses: string[]): Promise { - const balanceInfo: BalanceInfo = {} - try { - await Promise.all(addresses.map(async address => { - try { - const balance = await getBalance(entropy, address) - - balanceInfo[address] = { balance } - } catch (error) { - // console.error(`Error retrieving balance for ${address}`, error); - balanceInfo[address] = { error: error.message } - } - })) - - return balanceInfo - } catch (error) { - // console.error(`There was an error getting balances for [${addresses}]`, error); - throw new Error(error.message) - } -} \ No newline at end of file diff --git a/src/flows/balance/cli.ts b/src/flows/balance/cli.ts index 4bcb208d..78fa71a2 100644 --- a/src/flows/balance/cli.ts +++ b/src/flows/balance/cli.ts @@ -1,8 +1,10 @@ +import { BalanceController } from 'src/balance/command' import { initializeEntropy } from '../../common/initializeEntropy' import * as config from '../../config' -import { getBalance } from './balance' import { EntropyLogger } from 'src/common/logger' +// TO-DO: move entropy initialization and account retrieval to a shared container +// should remove the need to initialize entropy every time export async function cliGetBalance ({ address, password, endpoint }) { const logger = new EntropyLogger('CLI::CHECK_BALANCE', endpoint) const storedConfig = await config.get() @@ -17,8 +19,9 @@ export async function cliGetBalance ({ address, password, endpoint }) { } const entropy = await initializeEntropy({ keyMaterial: account.data, password, endpoint }) - const balance = await getBalance(entropy, address) + const balanceController = new BalanceController(entropy, endpoint) + const balance = await balanceController.getBalance(address) - return `${balance.toLocaleString('en-US')} BITS` + return balance } diff --git a/src/flows/balance/index.ts b/src/flows/balance/index.ts index 0481c86e..b13557ac 100644 --- a/src/flows/balance/index.ts +++ b/src/flows/balance/index.ts @@ -1,10 +1,12 @@ import { EntropyLogger } from "src/common/logger"; import { initializeEntropy } from "../../common/initializeEntropy" import { print, getSelectedAccount } from "../../common/utils" -import { getBalance } from "./balance"; +import { BalanceController } from "src/balance/command"; -// TO-DO setup flow method to provide options to allow users to select account, +// TO-DO: setup flow method to provide options to allow users to select account, // use external address, or get balances for all accounts in config +// TO-DO: move entropy initialization and account retrieval to a shared container +// should remove the need to initialize entropy every time export async function checkBalance ({ accounts, selectedAccount: selectedAccountAddress }, options, logger: EntropyLogger) { const FLOW_CONTEXT = 'CHECK_BALANCE' const { endpoint } = options @@ -13,7 +15,8 @@ export async function checkBalance ({ accounts, selectedAccount: selectedAccount const selectedAccount = getSelectedAccount(accounts, selectedAccountAddress) logger.log(selectedAccount, FLOW_CONTEXT) const entropy = await initializeEntropy({ keyMaterial: selectedAccount.data, endpoint }); + const balanceController = new BalanceController(entropy, endpoint) const accountAddress = selectedAccountAddress - const freeBalance = await getBalance(entropy, accountAddress) - print(`Address ${accountAddress} has a balance of: ${freeBalance.toLocaleString('en-US')} BITS`) + const freeBalanceString = await balanceController.getBalance(accountAddress) + print(`Address ${accountAddress} has a balance of: ${freeBalanceString}`) } diff --git a/tests/balance.test.ts b/tests/balance.test.ts index 2d05b25e..3b77d141 100644 --- a/tests/balance.test.ts +++ b/tests/balance.test.ts @@ -1,32 +1,32 @@ import test from 'tape' -import { getBalance, getBalances } from '../src/flows/balance/balance' import { setupTest, charlieStashAddress as richAddress } from './testing-utils' +import { BalanceService } from 'src/balance/utils' const networkType = 'two-nodes' test('getBalance + getBalances', async (t) => { - const { run, entropy } = await setupTest(t, { networkType }) - + const { run, entropy, endpoint } = await setupTest(t, { networkType }) + const balanceService = new BalanceService(entropy, endpoint) const newAddress = entropy.keyring.accounts.registration.address /* getBalance */ const newAddressBalance = await run( 'getBalance (newSeed)', - getBalance(entropy, newAddress) + balanceService.getBalance(newAddress) ) t.equal(newAddressBalance, 0, 'newSeed balance = 0') const richAddressBalance = await run( 'getBalance (richAddress)', - getBalance(entropy, richAddress) + balanceService.getBalance(richAddress) ) t.true(richAddressBalance > BigInt(10e10), 'richAddress balance >>> 0') /* getBalances */ const balances = await run( 'getBalances', - getBalances(entropy, [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', - getBalances(entropy, badAddresses) + balanceService.getBalances(badAddresses) ) badAddresses.forEach(addr => { diff --git a/tests/testing-utils/setup-test.ts b/tests/testing-utils/setup-test.ts index b2e3206f..ec2e59ce 100644 --- a/tests/testing-utils/setup-test.ts +++ b/tests/testing-utils/setup-test.ts @@ -12,16 +12,18 @@ import { makeSeed, promiseRunner, sleep } from './' interface SetupTestOpts { configPath?: string networkType?: string - seed?: string, + seed?: string + endpoint?: string } const NETWORK_TYPE_DEFAULT = 'two-nodes' let counter = 0 -export async function setupTest (t: Test, opts?: SetupTestOpts): Promise<{ entropy: Entropy; run: any }> { +export async function setupTest (t: Test, opts?: SetupTestOpts): Promise<{ entropy: Entropy; run: any; endpoint: string }> { const { configPath = `/tmp/entropy-cli-${Date.now()}_${counter++}.json`, networkType = NETWORK_TYPE_DEFAULT, - seed = makeSeed() + seed = makeSeed(), + endpoint = 'ws://127.0.0.1:9944', } = opts || {} const run = promiseRunner(t) @@ -45,11 +47,11 @@ export async function setupTest (t: Test, opts?: SetupTestOpts): Promise<{ entro const keyring = new Keyring({ seed, debug: true }) const entropy = await initializeEntropy({ keyMaterial: keyring.getAccount(), - endpoint: 'ws://127.0.0.1:9944', + endpoint, configPath }) await run('entropy ready', entropy.ready) - return { entropy, run } + return { entropy, run, endpoint } } diff --git a/tests/transfer.test.ts b/tests/transfer.test.ts index 83379bc2..1cb666b7 100644 --- a/tests/transfer.test.ts +++ b/tests/transfer.test.ts @@ -11,12 +11,13 @@ import { spinNetworkDown } from './testing-utils' -import { getBalance } from '../src/flows/balance/balance' import { initializeEntropy } from 'src/common/initializeEntropy' import { charlieStashAddress, charlieStashSeed } from './testing-utils/constants' import { transfer } from 'src/flows/entropyTransfer/transfer' +import { BalanceService } from 'src/balance/utils' const networkType = 'two-nodes' +const endpoint = 'ws://127.0.0.1:9944' test('Transfer', async (t) => { /* Setup */ @@ -37,8 +38,9 @@ test('Transfer', async (t) => { const naynayKeyring = new Keyring({ seed: naynaySeed, debug: true }) const charlieKeyring = new Keyring({ seed: charlieStashSeed, debug: true }) - const entropy = await initializeEntropy({ keyMaterial: naynayKeyring.getAccount(), endpoint: 'ws://127.0.0.1:9944', }) - const charlieEntropy = await initializeEntropy({ keyMaterial: charlieKeyring.getAccount(), endpoint: 'ws://127.0.0.1:9944', }) + const entropy = await initializeEntropy({ keyMaterial: naynayKeyring.getAccount(), endpoint, }) + const charlieEntropy = await initializeEntropy({ keyMaterial: charlieKeyring.getAccount(), endpoint, }) + const balanceService = new BalanceService(entropy, endpoint) await run('entropy ready', entropy.ready) await run('charlie ready', charlieEntropy.ready) @@ -47,14 +49,14 @@ test('Transfer', async (t) => { // Check Balance of new account let naynayBalance = await run( 'getBalance (naynay)', - getBalance(entropy, recipientAddress) + balanceService.getBalance(recipientAddress) ) t.equal(naynayBalance, 0, 'naynay is broke') let charlieBalance = await run( 'getBalance (charlieStash)', - getBalance(entropy, charlieStashAddress) + balanceService.getBalance(charlieStashAddress) ) t.equal(charlieBalance, 1e17, 'charlie got bank') @@ -73,7 +75,7 @@ test('Transfer', async (t) => { // Re-Check Balance of new account naynayBalance = await run( 'getBalance (naynay)', - getBalance(entropy, recipientAddress) + balanceService.getBalance(recipientAddress) ) t.equal(naynayBalance, 1000 * 10e10, 'naynay is rolling in it!') From 5d647293cdf1cc66fd0d02375a75cbec5f7598ef Mon Sep 17 00:00:00 2001 From: Nayyir Jutha Date: Wed, 31 Jul 2024 16:23:14 -0400 Subject: [PATCH 02/25] updated naming scheme for command, utils and base class --- src/balance/command.ts | 12 ++++++------ src/balance/utils.ts | 4 ++-- src/common/{entropy-base.ts => base.ts} | 2 +- src/flows/balance/cli.ts | 4 ++-- src/flows/balance/index.ts | 4 ++-- tests/balance.test.ts | 4 ++-- tests/transfer.test.ts | 4 ++-- 7 files changed, 17 insertions(+), 17 deletions(-) rename src/common/{entropy-base.ts => base.ts} (89%) diff --git a/src/balance/command.ts b/src/balance/command.ts index 09f96d4f..4d0f3aea 100644 --- a/src/balance/command.ts +++ b/src/balance/command.ts @@ -1,20 +1,20 @@ import Entropy from "@entropyxyz/sdk" -import { EntropyBase } from "../common/entropy-base" -import { BalanceService } from "./utils" +import { Base } from "../common/base" +import { BalanceUtils } from "./utils" import { FLOW_CONTEXT } from "./constants" -export class BalanceController extends EntropyBase { - private readonly balanceService: BalanceService +export class BalanceCommand extends Base { + private readonly balanceService: BalanceUtils constructor (entropy: Entropy, endpoint: string) { super(entropy, endpoint, FLOW_CONTEXT) - this.balanceService = new BalanceService(this.entropy, endpoint) + this.balanceService = new BalanceUtils(this.entropy, endpoint) } public async getBalance (address: string) { const balance = await this.balanceService.getBalance(address) - this.logger.log(`Current balance of ${address}: ${balance}`, `${BalanceController.name}`) + this.logger.log(`Current balance of ${address}: ${balance}`, `${BalanceCommand.name}`) return `${balance.toLocaleString('en-US')} BITS` } diff --git a/src/balance/utils.ts b/src/balance/utils.ts index 4b809dcd..56c721bf 100644 --- a/src/balance/utils.ts +++ b/src/balance/utils.ts @@ -1,11 +1,11 @@ import Entropy from "@entropyxyz/sdk"; -import { EntropyBase } from "../common/entropy-base"; +import { Base } from "../common/base"; import { BalanceInfo } from "./types"; import { FLOW_CONTEXT } from "./constants"; const hexToBigInt = (hexString: string) => BigInt(hexString) -export class BalanceService extends EntropyBase { +export class BalanceUtils extends Base { constructor (entropy: Entropy, endpoint: string) { super(entropy, endpoint, FLOW_CONTEXT) } diff --git a/src/common/entropy-base.ts b/src/common/base.ts similarity index 89% rename from src/common/entropy-base.ts rename to src/common/base.ts index 1e14bf3b..3b264cb9 100644 --- a/src/common/entropy-base.ts +++ b/src/common/base.ts @@ -1,7 +1,7 @@ import Entropy from "@entropyxyz/sdk"; import { EntropyLogger } from "./logger"; -export abstract class EntropyBase { +export abstract class Base { protected logger: EntropyLogger protected entropy: Entropy diff --git a/src/flows/balance/cli.ts b/src/flows/balance/cli.ts index 78fa71a2..d677a2bc 100644 --- a/src/flows/balance/cli.ts +++ b/src/flows/balance/cli.ts @@ -1,4 +1,4 @@ -import { BalanceController } from 'src/balance/command' +import { BalanceCommand } from 'src/balance/command' import { initializeEntropy } from '../../common/initializeEntropy' import * as config from '../../config' import { EntropyLogger } from 'src/common/logger' @@ -19,7 +19,7 @@ export async function cliGetBalance ({ address, password, endpoint }) { } const entropy = await initializeEntropy({ keyMaterial: account.data, password, endpoint }) - const balanceController = new BalanceController(entropy, endpoint) + const balanceController = new BalanceCommand(entropy, endpoint) const balance = await balanceController.getBalance(address) return balance diff --git a/src/flows/balance/index.ts b/src/flows/balance/index.ts index b13557ac..210b34be 100644 --- a/src/flows/balance/index.ts +++ b/src/flows/balance/index.ts @@ -1,7 +1,7 @@ import { EntropyLogger } from "src/common/logger"; import { initializeEntropy } from "../../common/initializeEntropy" import { print, getSelectedAccount } from "../../common/utils" -import { BalanceController } from "src/balance/command"; +import { BalanceCommand } from "src/balance/command"; // TO-DO: setup flow method to provide options to allow users to select account, // use external address, or get balances for all accounts in config @@ -15,7 +15,7 @@ export async function checkBalance ({ accounts, selectedAccount: selectedAccount const selectedAccount = getSelectedAccount(accounts, selectedAccountAddress) logger.log(selectedAccount, FLOW_CONTEXT) const entropy = await initializeEntropy({ keyMaterial: selectedAccount.data, endpoint }); - const balanceController = new BalanceController(entropy, endpoint) + const balanceController = new BalanceCommand(entropy, endpoint) const accountAddress = selectedAccountAddress const freeBalanceString = await balanceController.getBalance(accountAddress) print(`Address ${accountAddress} has a balance of: ${freeBalanceString}`) diff --git a/tests/balance.test.ts b/tests/balance.test.ts index 3b77d141..4c2e67e5 100644 --- a/tests/balance.test.ts +++ b/tests/balance.test.ts @@ -1,13 +1,13 @@ import test from 'tape' import { setupTest, charlieStashAddress as richAddress } from './testing-utils' -import { BalanceService } from 'src/balance/utils' +import { BalanceUtils } from 'src/balance/utils' const networkType = 'two-nodes' test('getBalance + getBalances', async (t) => { const { run, entropy, endpoint } = await setupTest(t, { networkType }) - const balanceService = new BalanceService(entropy, endpoint) + const balanceService = new BalanceUtils(entropy, endpoint) const newAddress = entropy.keyring.accounts.registration.address /* getBalance */ diff --git a/tests/transfer.test.ts b/tests/transfer.test.ts index 1cb666b7..4d1bc798 100644 --- a/tests/transfer.test.ts +++ b/tests/transfer.test.ts @@ -14,7 +14,7 @@ import { import { initializeEntropy } from 'src/common/initializeEntropy' import { charlieStashAddress, charlieStashSeed } from './testing-utils/constants' import { transfer } from 'src/flows/entropyTransfer/transfer' -import { BalanceService } from 'src/balance/utils' +import { BalanceUtils } from 'src/balance/utils' const networkType = 'two-nodes' const endpoint = 'ws://127.0.0.1:9944' @@ -40,7 +40,7 @@ test('Transfer', async (t) => { const entropy = await initializeEntropy({ keyMaterial: naynayKeyring.getAccount(), endpoint, }) const charlieEntropy = await initializeEntropy({ keyMaterial: charlieKeyring.getAccount(), endpoint, }) - const balanceService = new BalanceService(entropy, endpoint) + const balanceService = new BalanceUtils(entropy, endpoint) await run('entropy ready', entropy.ready) await run('charlie ready', charlieEntropy.ready) From 366072b0e728fd6baa8d36331208f6a5be76efe2 Mon Sep 17 00:00:00 2001 From: Nayyir Jutha Date: Wed, 31 Jul 2024 16:25:32 -0400 Subject: [PATCH 03/25] more naming changes --- tests/balance.test.ts | 10 +++++----- tests/transfer.test.ts | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/balance.test.ts b/tests/balance.test.ts index 4c2e67e5..f609ddba 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 BalanceUtils(entropy, endpoint) + const balanceUtils = new BalanceUtils(entropy, endpoint) const newAddress = entropy.keyring.accounts.registration.address /* getBalance */ const newAddressBalance = await run( 'getBalance (newSeed)', - balanceService.getBalance(newAddress) + balanceUtils.getBalance(newAddress) ) t.equal(newAddressBalance, 0, 'newSeed balance = 0') const richAddressBalance = await run( 'getBalance (richAddress)', - balanceService.getBalance(richAddress) + balanceUtils.getBalance(richAddress) ) t.true(richAddressBalance > BigInt(10e10), 'richAddress balance >>> 0') /* getBalances */ const balances = await run( 'getBalances', - balanceService.getBalances([newAddress, richAddress]) + balanceUtils.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) + balanceUtils.getBalances(badAddresses) ) badAddresses.forEach(addr => { diff --git a/tests/transfer.test.ts b/tests/transfer.test.ts index 4d1bc798..d0c12101 100644 --- a/tests/transfer.test.ts +++ b/tests/transfer.test.ts @@ -40,7 +40,7 @@ test('Transfer', async (t) => { const entropy = await initializeEntropy({ keyMaterial: naynayKeyring.getAccount(), endpoint, }) const charlieEntropy = await initializeEntropy({ keyMaterial: charlieKeyring.getAccount(), endpoint, }) - const balanceService = new BalanceUtils(entropy, endpoint) + const balanceUtils = new BalanceUtils(entropy, endpoint) await run('entropy ready', entropy.ready) await run('charlie ready', charlieEntropy.ready) @@ -49,14 +49,14 @@ test('Transfer', async (t) => { // Check Balance of new account let naynayBalance = await run( 'getBalance (naynay)', - balanceService.getBalance(recipientAddress) + balanceUtils.getBalance(recipientAddress) ) t.equal(naynayBalance, 0, 'naynay is broke') let charlieBalance = await run( 'getBalance (charlieStash)', - balanceService.getBalance(charlieStashAddress) + balanceUtils.getBalance(charlieStashAddress) ) t.equal(charlieBalance, 1e17, 'charlie got bank') @@ -75,7 +75,7 @@ test('Transfer', async (t) => { // Re-Check Balance of new account naynayBalance = await run( 'getBalance (naynay)', - balanceService.getBalance(recipientAddress) + balanceUtils.getBalance(recipientAddress) ) t.equal(naynayBalance, 1000 * 10e10, 'naynay is rolling in it!') From cd1b81743445f2e4d611a7e7fd102e98ab2cf0a4 Mon Sep 17 00:00:00 2001 From: Nayyir Jutha Date: Wed, 31 Jul 2024 16:30:51 -0400 Subject: [PATCH 04/25] changelog updated --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 248b0cbd..50d596c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,10 +27,13 @@ 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/base.ts' - base abstract class for new command and utils classes +- new: './src/balance' - new file structure for our CLI/TUI 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 ## [0.0.3] Blade - 2024-07-17 (entropy-core compatibility: 0.2.0) From c0fbd87efd7ba78b5195e56132136ea158aa9ab7 Mon Sep 17 00:00:00 2001 From: Nayyir Jutha Date: Wed, 31 Jul 2024 16:33:18 -0400 Subject: [PATCH 05/25] naming is fun --- src/flows/balance/cli.ts | 4 ++-- src/flows/balance/index.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/flows/balance/cli.ts b/src/flows/balance/cli.ts index d677a2bc..99e7879e 100644 --- a/src/flows/balance/cli.ts +++ b/src/flows/balance/cli.ts @@ -19,8 +19,8 @@ export async function cliGetBalance ({ address, password, endpoint }) { } const entropy = await initializeEntropy({ keyMaterial: account.data, password, endpoint }) - const balanceController = new BalanceCommand(entropy, endpoint) - const balance = await balanceController.getBalance(address) + const balanceCommand = new BalanceCommand(entropy, endpoint) + const balance = await balanceCommand.getBalance(address) return balance } diff --git a/src/flows/balance/index.ts b/src/flows/balance/index.ts index 210b34be..fda87176 100644 --- a/src/flows/balance/index.ts +++ b/src/flows/balance/index.ts @@ -15,8 +15,8 @@ export async function checkBalance ({ accounts, selectedAccount: selectedAccount const selectedAccount = getSelectedAccount(accounts, selectedAccountAddress) logger.log(selectedAccount, FLOW_CONTEXT) const entropy = await initializeEntropy({ keyMaterial: selectedAccount.data, endpoint }); - const balanceController = new BalanceCommand(entropy, endpoint) + const balanceCommand = new BalanceCommand(entropy, endpoint) const accountAddress = selectedAccountAddress - const freeBalanceString = await balanceController.getBalance(accountAddress) + const freeBalanceString = await balanceCommand.getBalance(accountAddress) print(`Address ${accountAddress} has a balance of: ${freeBalanceString}`) } From 05c82ed0e840e7d0eb5c211e043e7c08c3f8ab90 Mon Sep 17 00:00:00 2001 From: Nayyir Jutha Date: Wed, 31 Jul 2024 21:21:07 -0400 Subject: [PATCH 06/25] updated utils to be pure functions rather than class; updated tests; updated changelog --- CHANGELOG.md | 4 ++- src/balance/command.ts | 11 +++--- src/balance/utils.ts | 46 +++++++------------------ src/common/{base.ts => base-command.ts} | 2 +- tests/balance.test.ts | 11 +++--- tests/transfer.test.ts | 9 +++-- 6 files changed, 30 insertions(+), 53 deletions(-) rename src/common/{base.ts => base-command.ts} (89%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 50d596c7..f87a58f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,8 +27,10 @@ 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/base.ts' - base abstract class for new command and utils classes +- new: './src/common/base.ts' - base abstract class for new command classes - 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 ### Changed - folder name for user programs to match the kebab-case style for folder namespace diff --git a/src/balance/command.ts b/src/balance/command.ts index 4d0f3aea..cab1adf7 100644 --- a/src/balance/command.ts +++ b/src/balance/command.ts @@ -1,18 +1,15 @@ import Entropy from "@entropyxyz/sdk" -import { Base } from "../common/base" -import { BalanceUtils } from "./utils" +import { BaseCommand } from "../common/base-command" +import * as BalanceUtils from "./utils" import { FLOW_CONTEXT } from "./constants" -export class BalanceCommand extends Base { - private readonly balanceService: BalanceUtils - +export class BalanceCommand extends BaseCommand { constructor (entropy: Entropy, endpoint: string) { super(entropy, endpoint, FLOW_CONTEXT) - this.balanceService = new BalanceUtils(this.entropy, endpoint) } public async getBalance (address: string) { - const balance = await this.balanceService.getBalance(address) + const balance = await BalanceUtils.getBalance(this.entropy, address) this.logger.log(`Current balance of ${address}: ${balance}`, `${BalanceCommand.name}`) diff --git a/src/balance/utils.ts b/src/balance/utils.ts index 56c721bf..917d0840 100644 --- a/src/balance/utils.ts +++ b/src/balance/utils.ts @@ -1,44 +1,24 @@ import Entropy from "@entropyxyz/sdk"; -import { Base } from "../common/base"; import { BalanceInfo } from "./types"; -import { FLOW_CONTEXT } from "./constants"; const hexToBigInt = (hexString: string) => BigInt(hexString) -export class BalanceUtils extends Base { - constructor (entropy: Entropy, endpoint: string) { - super(entropy, endpoint, FLOW_CONTEXT) - } +export async function getBalance (entropy: Entropy, address: string): Promise { + const accountInfo = (await entropy.substrate.query.system.account(address)) as any + return parseInt(hexToBigInt(accountInfo.data.free).toString()) +} - public async getBalance (address: string): Promise { +export async function getBalances (entropy: Entropy, addresses: string[]): Promise { + const balanceInfo: BalanceInfo = {} + await Promise.all(addresses.map(async address => { try { - const accountInfo = (await this.entropy.substrate.query.system.account(address)) as any + const balance = await getBalance(entropy, address) - return parseInt(hexToBigInt(accountInfo.data.free).toString()) + balanceInfo[address] = { balance } } catch (error) { - this.logger.error(`There was an error getting balance for [acct = ${address}]`, error); - throw new Error(error.message) + balanceInfo[address] = { error: error.message } } - } - - public async getBalances (addresses: string[]): Promise { - const balanceInfo: BalanceInfo = {} - try { - await Promise.all(addresses.map(async address => { - try { - const balance = await this.getBalance(address) - - balanceInfo[address] = { balance } - } catch (error) { - this.logger.error(`Error retrieving balance for ${address}`, error); - balanceInfo[address] = { error: error.message } - } - })) - - return balanceInfo - } catch (error) { - this.logger.error(`There was an error getting balances for [${addresses}]`, error); - throw new Error(error.message) - } - } + })) + + return balanceInfo } \ No newline at end of file diff --git a/src/common/base.ts b/src/common/base-command.ts similarity index 89% rename from src/common/base.ts rename to src/common/base-command.ts index 3b264cb9..ded342e4 100644 --- a/src/common/base.ts +++ b/src/common/base-command.ts @@ -1,7 +1,7 @@ import Entropy from "@entropyxyz/sdk"; import { EntropyLogger } from "./logger"; -export abstract class Base { +export abstract class BaseCommand { protected logger: EntropyLogger protected entropy: Entropy diff --git a/tests/balance.test.ts b/tests/balance.test.ts index f609ddba..2fe9e39b 100644 --- a/tests/balance.test.ts +++ b/tests/balance.test.ts @@ -1,32 +1,31 @@ import test from 'tape' import { setupTest, charlieStashAddress as richAddress } from './testing-utils' -import { BalanceUtils } from 'src/balance/utils' +import * as BalanceUtils from 'src/balance/utils' const networkType = 'two-nodes' test('getBalance + getBalances', async (t) => { const { run, entropy, endpoint } = await setupTest(t, { networkType }) - const balanceUtils = new BalanceUtils(entropy, endpoint) const newAddress = entropy.keyring.accounts.registration.address /* getBalance */ const newAddressBalance = await run( 'getBalance (newSeed)', - balanceUtils.getBalance(newAddress) + BalanceUtils.getBalance(entropy, newAddress) ) t.equal(newAddressBalance, 0, 'newSeed balance = 0') const richAddressBalance = await run( 'getBalance (richAddress)', - balanceUtils.getBalance(richAddress) + BalanceUtils.getBalance(entropy, richAddress) ) t.true(richAddressBalance > BigInt(10e10), 'richAddress balance >>> 0') /* getBalances */ const balances = await run( 'getBalances', - balanceUtils.getBalances([newAddress, richAddress]) + BalanceUtils.getBalances(entropy, [newAddress, richAddress]) ) t.deepEqual( balances, @@ -40,7 +39,7 @@ test('getBalance + getBalances', async (t) => { const badAddresses = ['5Cz6BfUaxxXCA3jninzxdan4JdmC1NVpgkiRPYhXbhr', '5Cz6BfUaxxXCA3jninzxdan4JdmC1NVpgkiRPYhXbhrfnD'] const balancesWithNoGoodAddress = await run( 'getBalances::one good address', - balanceUtils.getBalances(badAddresses) + BalanceUtils.getBalances(entropy, badAddresses) ) badAddresses.forEach(addr => { diff --git a/tests/transfer.test.ts b/tests/transfer.test.ts index d0c12101..22a0fe86 100644 --- a/tests/transfer.test.ts +++ b/tests/transfer.test.ts @@ -14,7 +14,7 @@ import { import { initializeEntropy } from 'src/common/initializeEntropy' import { charlieStashAddress, charlieStashSeed } from './testing-utils/constants' import { transfer } from 'src/flows/entropyTransfer/transfer' -import { BalanceUtils } from 'src/balance/utils' +import * as BalanceUtils from 'src/balance/utils' const networkType = 'two-nodes' const endpoint = 'ws://127.0.0.1:9944' @@ -40,7 +40,6 @@ test('Transfer', async (t) => { const entropy = await initializeEntropy({ keyMaterial: naynayKeyring.getAccount(), endpoint, }) const charlieEntropy = await initializeEntropy({ keyMaterial: charlieKeyring.getAccount(), endpoint, }) - const balanceUtils = new BalanceUtils(entropy, endpoint) await run('entropy ready', entropy.ready) await run('charlie ready', charlieEntropy.ready) @@ -49,14 +48,14 @@ test('Transfer', async (t) => { // Check Balance of new account let naynayBalance = await run( 'getBalance (naynay)', - balanceUtils.getBalance(recipientAddress) + BalanceUtils.getBalance(entropy, recipientAddress) ) t.equal(naynayBalance, 0, 'naynay is broke') let charlieBalance = await run( 'getBalance (charlieStash)', - balanceUtils.getBalance(charlieStashAddress) + BalanceUtils.getBalance(entropy, charlieStashAddress) ) t.equal(charlieBalance, 1e17, 'charlie got bank') @@ -75,7 +74,7 @@ test('Transfer', async (t) => { // Re-Check Balance of new account naynayBalance = await run( 'getBalance (naynay)', - balanceUtils.getBalance(recipientAddress) + BalanceUtils.getBalance(entropy, recipientAddress) ) t.equal(naynayBalance, 1000 * 10e10, 'naynay is rolling in it!') From e8aef10214132aa14eef2dbba52a618016e06fcc Mon Sep 17 00:00:00 2001 From: Nayyir Jutha Date: Wed, 31 Jul 2024 22:20:45 -0400 Subject: [PATCH 07/25] Update CHANGELOG.md Co-authored-by: mix irving --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f87a58f1..f3a286a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,7 +27,7 @@ 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/base.ts' - base abstract class for new command classes +- new: './src/common/base-commands.ts' - base abstract class for new command classes - 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 From f1b87f4abbb0cd7e3ae92922fbe3b1bbecf9117b Mon Sep 17 00:00:00 2001 From: Nayyir Jutha Date: Wed, 31 Jul 2024 22:43:16 -0400 Subject: [PATCH 08/25] lint errors --- tests/balance.test.ts | 2 +- tests/testing-utils/setup-test.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/balance.test.ts b/tests/balance.test.ts index 2fe9e39b..f95d7e16 100644 --- a/tests/balance.test.ts +++ b/tests/balance.test.ts @@ -1,7 +1,7 @@ import test from 'tape' import { setupTest, charlieStashAddress as richAddress } from './testing-utils' -import * as BalanceUtils from 'src/balance/utils' +import * as BalanceUtils from '../src/balance/utils' const networkType = 'two-nodes' diff --git a/tests/testing-utils/setup-test.ts b/tests/testing-utils/setup-test.ts index dffc28b0..36391127 100644 --- a/tests/testing-utils/setup-test.ts +++ b/tests/testing-utils/setup-test.ts @@ -44,6 +44,7 @@ export async function setupTest (t: Test, opts?: SetupTestOpts): Promise<{ entro // as done in src/flows/manage-accounts/new-key.ts const keyring = new Keyring({ seed, debug: true }) const entropy = await initializeEntropy({ + // @ts-expect-error keyMaterial: keyring.getAccount(), endpoint, configPath From 67c33cb922ca33712f2d55f033354d0f51a96341 Mon Sep 17 00:00:00 2001 From: Nayyir Jutha Date: Thu, 1 Aug 2024 15:30:48 -0400 Subject: [PATCH 09/25] added lifecycle hook to main entropy entry point to load entropy instance on every command; used to be passed down to each of the commands; added new option to define new address as selected account --- src/cli.ts | 50 +++++++++++++++++++++++++++++++++++++++++++++++--- src/tui.ts | 11 ++++++----- 2 files changed, 53 insertions(+), 8 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index f8dab60c..8d9ce7a4 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -10,7 +10,9 @@ 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 { stringify } from './common/utils' +import { getSelectedAccount, stringify } from './common/utils' +import Entropy from '@entropyxyz/sdk' +import { initializeEntropy } from './common/initializeEntropy' const program = new Command() @@ -45,11 +47,40 @@ function passwordOption (description?: string) { ) } +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 + +async function loadEntropy (address: string, endpoint: string, password: string) { + const storedConfig = config.getSync() + const selectedAccount = getSelectedAccount(storedConfig.accounts, address) + + entropy = await initializeEntropy({ keyMaterial: selectedAccount.data, endpoint, password }) +} + /* 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( new Option( '-d, --dev', @@ -58,8 +89,19 @@ program .env('DEV_MODE') .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 + if (actionCommand.name() === 'balance') { + 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) + } + } + }) .action((options: EntropyTuiOptions) => { - launchTui(options) + launchTui(entropy, options) }) /* list */ @@ -93,6 +135,7 @@ program.command('transfer') .argument('amount', 'Amount of funds to be moved') .addOption(passwordOption('Password for the source account (if required)')) .addOption(endpointOption()) + .addOption(currentAccountAddressOption()) .action(async (source, destination, amount, opts) => { await cliEntropyTransfer({ source, destination, amount, ...opts }) // writeOut(??) // TODO: write the output @@ -106,6 +149,7 @@ program.command('sign') .argument('message', 'Message you would like to sign') .addOption(passwordOption('Password for the source account (if required)')) .addOption(endpointOption()) + .addOption(currentAccountAddressOption()) .action(async (address, message, opts) => { const signature = await cliSign({ address, message, ...opts }) writeOut(signature) @@ -117,4 +161,4 @@ function writeOut (result) { process.stdout.write(prettyResult) } -program.parse() +program.parseAsync().then(() => {}) diff --git a/src/tui.ts b/src/tui.ts index 746640fa..339c010f 100644 --- a/src/tui.ts +++ b/src/tui.ts @@ -1,4 +1,5 @@ import inquirer from 'inquirer' +import Entropy from '@entropyxyz/sdk' import * as config from './config' import * as flows from './flows' import { EntropyTuiOptions } from './types' @@ -9,9 +10,9 @@ import { EntropyLogger } from './common/logger' let shouldInit = true // tui = text user interface -export default function tui (options: EntropyTuiOptions) { +export default function tui (entropy: Entropy, options: EntropyTuiOptions) { const logger = new EntropyLogger('TUI', options.endpoint) - console.clear() + // console.clear() console.log(logo) // the Entropy logo logger.debug(options) @@ -36,10 +37,10 @@ export default function tui (options: EntropyTuiOptions) { // assign exit so its last Object.assign(choices, { 'Exit': async () => {} }) - main(choices, options, logger) + main(entropy, choices, options, logger) } -async function main (choices, options, logger: EntropyLogger) { +async function main (entropy: Entropy, choices, options, logger: EntropyLogger) { if (shouldInit) { await config.init() shouldInit = false @@ -90,7 +91,7 @@ async function main (choices, options, logger: EntropyLogger) { }])) } - if (returnToMain) main(choices, options, logger) + if (returnToMain) main(entropy, choices, options, logger) else { print('Have a nice day') process.exit() From d6d8d76c8d6696aaca9965afa9241918f49c915c Mon Sep 17 00:00:00 2001 From: Nayyir Jutha Date: Thu, 1 Aug 2024 15:53:01 -0400 Subject: [PATCH 10/25] updated tui and cli to be the main source of entry for everything and removed flows/balance directory --- src/cli.ts | 14 +++++++++++--- src/flows/balance/cli.ts | 27 --------------------------- src/flows/balance/index.ts | 22 ---------------------- src/flows/index.ts | 1 - src/tui.ts | 27 ++++++++++++++++++++------- 5 files changed, 31 insertions(+), 60 deletions(-) delete mode 100644 src/flows/balance/cli.ts delete mode 100644 src/flows/balance/index.ts diff --git a/src/cli.ts b/src/cli.ts index 8d9ce7a4..f0f56424 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -6,13 +6,13 @@ 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 { getSelectedAccount, stringify } from './common/utils' import Entropy from '@entropyxyz/sdk' import { initializeEntropy } from './common/initializeEntropy' +import { BalanceCommand } from './balance/command' const program = new Command() @@ -72,6 +72,13 @@ async function loadEntropy (address: string, endpoint: string, password: string) 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 ') + } + entropy = await initializeEntropy({ keyMaterial: selectedAccount.data, endpoint, password }) } @@ -92,7 +99,7 @@ program .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 - if (actionCommand.name() === 'balance') { + if (actionCommand.name() === 'balance' && 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 @@ -122,7 +129,8 @@ program.command('balance') .addOption(passwordOption()) .addOption(endpointOption()) .action(async (address, opts) => { - const balance = await cliGetBalance({ address, ...opts }) + const balanceCommand = new BalanceCommand(entropy, opts.endpoint) + const balance = await balanceCommand.getBalance(address) writeOut(balance) process.exit(0) }) diff --git a/src/flows/balance/cli.ts b/src/flows/balance/cli.ts deleted file mode 100644 index 99e7879e..00000000 --- a/src/flows/balance/cli.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { BalanceCommand } from 'src/balance/command' -import { initializeEntropy } from '../../common/initializeEntropy' -import * as config from '../../config' -import { EntropyLogger } from 'src/common/logger' - -// TO-DO: move entropy initialization and account retrieval to a shared container -// should remove the need to initialize entropy every time -export async function cliGetBalance ({ address, password, endpoint }) { - const logger = new EntropyLogger('CLI::CHECK_BALANCE', 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? - logger.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 ') - } - - const entropy = await initializeEntropy({ keyMaterial: account.data, password, endpoint }) - const balanceCommand = new BalanceCommand(entropy, endpoint) - const balance = await balanceCommand.getBalance(address) - - return balance -} - diff --git a/src/flows/balance/index.ts b/src/flows/balance/index.ts deleted file mode 100644 index fda87176..00000000 --- a/src/flows/balance/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { EntropyLogger } from "src/common/logger"; -import { initializeEntropy } from "../../common/initializeEntropy" -import { print, getSelectedAccount } from "../../common/utils" -import { BalanceCommand } from "src/balance/command"; - -// TO-DO: setup flow method to provide options to allow users to select account, -// use external address, or get balances for all accounts in config -// TO-DO: move entropy initialization and account retrieval to a shared container -// should remove the need to initialize entropy every time -export async function checkBalance ({ accounts, selectedAccount: selectedAccountAddress }, options, logger: EntropyLogger) { - const FLOW_CONTEXT = 'CHECK_BALANCE' - const { endpoint } = options - logger.debug(`endpoint: ${endpoint}`, FLOW_CONTEXT) - - const selectedAccount = getSelectedAccount(accounts, selectedAccountAddress) - logger.log(selectedAccount, FLOW_CONTEXT) - const entropy = await initializeEntropy({ keyMaterial: selectedAccount.data, endpoint }); - const balanceCommand = new BalanceCommand(entropy, endpoint) - const accountAddress = selectedAccountAddress - const freeBalanceString = await balanceCommand.getBalance(accountAddress) - print(`Address ${accountAddress} has a balance of: ${freeBalanceString}`) -} diff --git a/src/flows/index.ts b/src/flows/index.ts index 82c5a11e..1aac99a8 100644 --- a/src/flows/index.ts +++ b/src/flows/index.ts @@ -1,5 +1,4 @@ export { entropyFaucet } from './entropyFaucet' -export { checkBalance } from './balance' export { entropyRegister } from './register' export { userPrograms, devPrograms } from './programs' export { sign } from './sign' diff --git a/src/tui.ts b/src/tui.ts index 339c010f..ebb24acb 100644 --- a/src/tui.ts +++ b/src/tui.ts @@ -6,6 +6,7 @@ import { EntropyTuiOptions } from './types' import { logo } from './common/ascii' import { print } from './common/utils' import { EntropyLogger } from './common/logger' +import { BalanceCommand } from './balance/command' let shouldInit = true @@ -18,7 +19,8 @@ export default function tui (entropy: Entropy, options: EntropyTuiOptions) { const choices = { 'Manage Accounts': flows.manageAccounts, - 'Balance': flows.checkBalance, + // leaving as a noop function until all flows are restructured + 'Balance': () => {}, 'Register': flows.entropyRegister, 'Sign': flows.sign, 'Transfer': flows.entropyTransfer, @@ -74,13 +76,24 @@ async function main (entropy: Entropy, choices, options, logger: EntropyLogger) console.error('There are currently no accounts available, please create or import your new account using the Manage Accounts feature') } else { logger.debug(answers) - const newConfigUpdates = await choices[answers.choice](storedConfig, options, logger) - if (typeof newConfigUpdates === 'string' && newConfigUpdates === 'exit') { - returnToMain = true - } else { - await config.set({ ...storedConfig, ...newConfigUpdates }) + 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; + } + 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() + break; + } } - storedConfig = await config.get() } if (!returnToMain) { From aefe05e64ee548d44f6642be34fd8e91754dabe6 Mon Sep 17 00:00:00 2001 From: Nayyir Jutha Date: Thu, 1 Aug 2024 16:03:06 -0400 Subject: [PATCH 11/25] updated changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f93ac80..1a554011 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,7 @@ Version header format: `[version] Name - year-month-day (entropy-core compatibil - 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 ### Broke From 197a8151ea7437a92b6f29d18215378ac2f2265e Mon Sep 17 00:00:00 2001 From: Nayyir Jutha Date: Thu, 1 Aug 2024 18:04:49 -0400 Subject: [PATCH 12/25] undid commented code --- src/tui.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tui.ts b/src/tui.ts index ebb24acb..e22f71ac 100644 --- a/src/tui.ts +++ b/src/tui.ts @@ -13,7 +13,7 @@ let shouldInit = true // tui = text user interface export default function tui (entropy: Entropy, options: EntropyTuiOptions) { const logger = new EntropyLogger('TUI', options.endpoint) - // console.clear() + console.clear() console.log(logo) // the Entropy logo logger.debug(options) From 423631c279c87da91c0a856acf1402bf352ecd88 Mon Sep 17 00:00:00 2001 From: Nayyir Jutha Date: Thu, 1 Aug 2024 18:17:03 -0400 Subject: [PATCH 13/25] little cleanup --- src/cli.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/cli.ts b/src/cli.ts index f0f56424..5af9da0a 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -15,6 +15,8 @@ import { initializeEntropy } from './common/initializeEntropy' import { BalanceCommand } from './balance/command' const program = new Command() +// Array of restructured commands to make it easier to migrate them to the new "flow" +const RESTRUCTURED_COMMANDS = ['balance'] function endpointOption (){ return new Option( @@ -99,7 +101,8 @@ program .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 - if (actionCommand.name() === 'balance' && actionCommand.args.length) { + // 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 From ddbd6e32e2735d1fabe64714f840208e38daa594 Mon Sep 17 00:00:00 2001 From: Nayyir Jutha Date: Thu, 1 Aug 2024 22:15:19 -0400 Subject: [PATCH 14/25] removed unnecessary constants file --- src/balance/command.ts | 2 +- src/balance/constants.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) delete mode 100644 src/balance/constants.ts diff --git a/src/balance/command.ts b/src/balance/command.ts index cab1adf7..d3c1a70f 100644 --- a/src/balance/command.ts +++ b/src/balance/command.ts @@ -1,8 +1,8 @@ import Entropy from "@entropyxyz/sdk" import { BaseCommand } from "../common/base-command" import * as BalanceUtils from "./utils" -import { FLOW_CONTEXT } from "./constants" +const FLOW_CONTEXT = 'ENTROPY-BALANCE' export class BalanceCommand extends BaseCommand { constructor (entropy: Entropy, endpoint: string) { super(entropy, endpoint, FLOW_CONTEXT) diff --git a/src/balance/constants.ts b/src/balance/constants.ts deleted file mode 100644 index 204e8cd2..00000000 --- a/src/balance/constants.ts +++ /dev/null @@ -1 +0,0 @@ -export const FLOW_CONTEXT = 'ENTROPY-BALANCE' \ No newline at end of file From 6dbc7571e9dd9ab5a119d7f485eab6012e9d5df0 Mon Sep 17 00:00:00 2001 From: Nayyir Jutha Date: Fri, 2 Aug 2024 12:38:52 -0400 Subject: [PATCH 15/25] [NayNay] File Restructure: Transfer - removed flows/entropyTransfer - created new root directory for transfer to house utils, new command file, and types - updated tui and cli with new transfer method --- src/cli.ts | 15 +++-- src/common/progress.ts | 4 +- src/flows/entropyTransfer/cli.ts | 32 ---------- src/flows/entropyTransfer/index.ts | 64 ------------------- src/flows/index.ts | 1 - src/transfer/command.ts | 51 +++++++++++++++ .../entropyTransfer => transfer}/types.ts | 1 + .../transfer.ts => transfer/utils.ts} | 0 src/tui.ts | 33 ++++++++-- 9 files changed, 94 insertions(+), 107 deletions(-) delete mode 100644 src/flows/entropyTransfer/cli.ts delete mode 100644 src/flows/entropyTransfer/index.ts create mode 100644 src/transfer/command.ts rename src/{flows/entropyTransfer => transfer}/types.ts (99%) rename src/{flows/entropyTransfer/transfer.ts => transfer/utils.ts} (100%) diff --git a/src/cli.ts b/src/cli.ts index 5af9da0a..6e1e9bc2 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -7,12 +7,12 @@ 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 { TransferCommand } from './transfer/command' const program = new Command() // Array of restructured commands to make it easier to migrate them to the new "flow" @@ -70,7 +70,7 @@ function currentAccountAddressOption () { let entropy: Entropy -async function loadEntropy (address: string, endpoint: string, password: string) { +export async function loadEntropy (address: string, endpoint: string, password?: string): Promise { const storedConfig = config.getSync() const selectedAccount = getSelectedAccount(storedConfig.accounts, address) @@ -82,6 +82,12 @@ async function loadEntropy (address: string, endpoint: string, password: string) } 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 } /* no command */ @@ -147,8 +153,9 @@ program.command('transfer') .addOption(passwordOption('Password for the source account (if required)')) .addOption(endpointOption()) .addOption(currentAccountAddressOption()) - .action(async (source, destination, amount, opts) => { - await cliEntropyTransfer({ source, destination, amount, ...opts }) + .action(async (_source, destination, amount, opts) => { + const transferCommand = new TransferCommand(entropy, opts.endpoint) + await transferCommand.sendTransfer(destination, amount) // writeOut(??) // TODO: write the output process.exit(0) }) diff --git a/src/common/progress.ts b/src/common/progress.ts index a2410501..bbdd6564 100644 --- a/src/common/progress.ts +++ b/src/common/progress.ts @@ -11,10 +11,10 @@ export function setupProgress (label: string): { start: () => void; stop: () => }) const start = () => { - // 160 was found through trial and error, don't believe there is a formula to + // 150 was found through trial and error, don't believe there is a formula to // determine the exact time it takes for the transaction to be processed and finalized // TO-DO: Change progress bar to loading animation? - b1.start(160, 0, { + b1.start(150, 0, { speed: "N/A" }) // update values diff --git a/src/flows/entropyTransfer/cli.ts b/src/flows/entropyTransfer/cli.ts deleted file mode 100644 index 1a0abe51..00000000 --- a/src/flows/entropyTransfer/cli.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { initializeEntropy } from '../../common/initializeEntropy' -import * as config from '../../config' -import { formatAmountAsHex } from '../../common/utils' -import { EntropyLogger } from 'src/common/logger' - -export async function cliEntropyTransfer ({ source, password, destination, amount, endpoint }) { - const logger = new EntropyLogger('CLI::TRANSFER', endpoint) - // NOTE: password is optional, is only for source account (if that is encrypted) - - const storedConfig = await config.get() - const account = storedConfig.accounts.find(account => account.address === source) - if (!account) throw Error(`No account with address ${source}`) - // QUESTION: is throwing the right response? - logger.debug('account', account) - - const entropy = await initializeEntropy({ keyMaterial: account.data, password, endpoint }) - - if (!entropy?.registrationManager?.signer?.pair) { - throw new Error("Signer keypair is undefined or not properly initialized.") - } - const formattedAmount = formatAmountAsHex(amount) - const tx = await entropy.substrate.tx.balances.transferAllowDeath( - destination, - BigInt(formattedAmount), - // WARNING: this is moving ... a lot? What? - ) - - await tx.signAndSend (entropy.registrationManager.signer.pair, ({ status }) => { - logger.debug('signAndSend status:') - logger.debug(status) - }) -} diff --git a/src/flows/entropyTransfer/index.ts b/src/flows/entropyTransfer/index.ts deleted file mode 100644 index 43f5532e..00000000 --- a/src/flows/entropyTransfer/index.ts +++ /dev/null @@ -1,64 +0,0 @@ -import inquirer from "inquirer" -import { getSelectedAccount, print } from "../../common/utils" -import { initializeEntropy } from "../../common/initializeEntropy" -import { transfer } from "./transfer" -import { setupProgress } from "src/common/progress" - -const question = [ - { - type: "input", - name: "amount", - message: "Input amount to transfer:", - default: "1", - validate: (amount) => { - if (isNaN(amount) || parseInt(amount) <= 0) { - return 'Please enter a value greater than 0' - } - return true - } - }, - { - type: "input", - name: "recipientAddress", - message: "Input recipient's address:", - }, -] - -export async function entropyTransfer ({ accounts, selectedAccount: selectedAccountAddress }, options) { - const { endpoint } = options - const selectedAccount = getSelectedAccount(accounts, selectedAccountAddress) - - const { start: startProgress, stop: stopProgress } = setupProgress('Transferring Funds') - - try { - const entropy = await initializeEntropy({ - keyMaterial: selectedAccount.data, - endpoint - }) - - const { amount, recipientAddress } = await inquirer.prompt(question) - - if (!entropy?.keyring?.accounts?.registration?.pair) { - throw new Error("Signer keypair is undefined or not properly initialized.") - } - const formattedAmount = BigInt(parseInt(amount) * 1e10) - startProgress() - const transferStatus = await transfer( - entropy, - { - from: entropy.keyring.accounts.registration.pair, - to: recipientAddress, - amount: formattedAmount - } - ) - if (transferStatus.isFinalized) stopProgress() - - print('') - print(`Transaction successful: Sent ${amount} to ${recipientAddress}`) - print('') - print('Press enter to return to main menu') - } catch (error) { - stopProgress() - console.error('ERR:::', error); - } -} diff --git a/src/flows/index.ts b/src/flows/index.ts index 1aac99a8..474d6823 100644 --- a/src/flows/index.ts +++ b/src/flows/index.ts @@ -2,5 +2,4 @@ export { entropyFaucet } from './entropyFaucet' 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/transfer/command.ts b/src/transfer/command.ts new file mode 100644 index 00000000..7fee0ff1 --- /dev/null +++ b/src/transfer/command.ts @@ -0,0 +1,51 @@ +import Entropy from "@entropyxyz/sdk"; +import { BaseCommand } from "../common/base-command"; +import { setupProgress } from "../common/progress"; +import * as TransferUtils from './utils' +import inquirer from "inquirer"; + +const FLOW_CONTEXT = 'ENTROPY_TRANSFER' +const question = [ + { + type: "input", + name: "amount", + message: "Input amount to transfer:", + default: "1", + validate: (amount) => { + if (isNaN(amount) || parseInt(amount) <= 0) { + return 'Please enter a value greater than 0' + } + return true + } + }, + { + type: "input", + name: "recipientAddress", + message: "Input recipient's address:", + }, +] + +export class TransferCommand extends BaseCommand { + constructor (entropy: Entropy, endpoint: string) { + super(entropy, endpoint, FLOW_CONTEXT) + } + + public async askQuestions () { + return inquirer.prompt(question) + } + + public async sendTransfer (toAddress: string, amount: string) { + const { start: startProgress, stop: stopProgress } = setupProgress('Transferring Funds') + + const formattedAmount = BigInt(parseInt(amount) * 1e10) + startProgress() + try { + const transferStatus = await TransferUtils.transfer(this.entropy, { from: this.entropy.keyring.accounts.registration.pair, to: toAddress, amount: formattedAmount }) + if (transferStatus.isFinalized) return stopProgress() + } catch (error) { + this.logger.error('There was an issue sending this transfer', error) + stopProgress() + throw error + } + } +} \ No newline at end of file diff --git a/src/flows/entropyTransfer/types.ts b/src/transfer/types.ts similarity index 99% rename from src/flows/entropyTransfer/types.ts rename to src/transfer/types.ts index c23b6b77..1ceaf8b2 100644 --- a/src/flows/entropyTransfer/types.ts +++ b/src/transfer/types.ts @@ -1,5 +1,6 @@ // @ts-ignore import { Pair } from '@entropyxyz/sdk/keys' + export interface TransferOptions { from: Pair to: string diff --git a/src/flows/entropyTransfer/transfer.ts b/src/transfer/utils.ts similarity index 100% rename from src/flows/entropyTransfer/transfer.ts rename to src/transfer/utils.ts diff --git a/src/tui.ts b/src/tui.ts index e22f71ac..4db275f4 100644 --- a/src/tui.ts +++ b/src/tui.ts @@ -7,6 +7,8 @@ import { logo } from './common/ascii' import { print } from './common/utils' import { EntropyLogger } from './common/logger' import { BalanceCommand } from './balance/command' +import { TransferCommand } from './transfer/command' +import { loadEntropy } from './cli' let shouldInit = true @@ -23,7 +25,7 @@ export default function tui (entropy: Entropy, options: EntropyTuiOptions) { 'Balance': () => {}, 'Register': flows.entropyRegister, 'Sign': flows.sign, - 'Transfer': flows.entropyTransfer, + 'Transfer': () => {}, // TODO: design programs in TUI (merge deploy+user programs) 'Deploy Program': flows.devPrograms, 'User Programs': flows.userPrograms, @@ -57,6 +59,11 @@ async function main (entropy: Entropy, choices, options, logger: EntropyLogger) storedConfig = await config.get() } + // If the selected account changes within the TUI we need to reset the entropy instance being used + if (storedConfig.selectedAccount !== entropy.keyring.accounts.registration.address) { + entropy = await loadEntropy(storedConfig.selectedAccount, options.endpoint) + } + const answers = await inquirer.prompt([{ type: 'list', name: 'choice', @@ -78,11 +85,29 @@ 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}`) + try { + const balanceCommand = new BalanceCommand(entropy, options.endpoint) + const balanceString = await balanceCommand.getBalance(storedConfig.selectedAccount) + print(`Address ${storedConfig.selectedAccount} has a balance of: ${balanceString}`) + } catch (error) { + console.error('There was an error retrieving balance', error) + } break; } + case "Transfer": { + try { + const transferCommand = new TransferCommand(entropy, options.endpoint) + const { amount, recipientAddress } = await transferCommand.askQuestions() + await transferCommand.sendTransfer(recipientAddress, amount) + print('') + print(`Transaction successful: Sent ${amount} to ${recipientAddress}`) + print('') + print('Press enter to return to main menu') + } catch (error) { + console.error('There was an error sending the transfer', error) + } + break + } default: { const newConfigUpdates = await choices[answers.choice](storedConfig, options, logger) if (typeof newConfigUpdates === 'string' && newConfigUpdates === 'exit') { From 073f910ba943cd8483fe962b5b4bad171095a2d3 Mon Sep 17 00:00:00 2001 From: Nayyir Jutha Date: Fri, 2 Aug 2024 12:44:14 -0400 Subject: [PATCH 16/25] updated tests + changelog --- CHANGELOG.md | 4 ++++ tests/transfer.test.ts | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a554011..9c5538e3 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/transfer' - new file structure for our CLI/TUI flows + - new: './src/transfer/command.ts' - main entry file for transfer command for tui/cli + - new: './src/transfer/utils.ts' - utilities and helper methods for all things transfer ### Changed @@ -39,6 +42,7 @@ 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/entropyTransfer/*.ts directory with file restructure ### Broke diff --git a/tests/transfer.test.ts b/tests/transfer.test.ts index d806403b..12c9736c 100644 --- a/tests/transfer.test.ts +++ b/tests/transfer.test.ts @@ -11,8 +11,8 @@ import { } from './testing-utils' import { initializeEntropy } from '../src/common/initializeEntropy' -import { transfer } from '../src/flows/entropyTransfer/transfer' import * as BalanceUtils from '../src/balance/utils' +import * as TransferUtils from '../src/transfer/utils' import { charlieStashAddress, charlieStashSeed } from './testing-utils/constants' const networkType = 'two-nodes' @@ -62,7 +62,7 @@ test('Transfer', async (t) => { const transferStatus = await run( 'transfer', - transfer(entropy, { + TransferUtils.transfer(entropy, { from: charlieEntropy.keyring.accounts.registration.pair, to: recipientAddress, amount: BigInt(1000 * 10e10) From 67045d4c50c130973b86ae57dba94995d107a274 Mon Sep 17 00:00:00 2001 From: Nayyir Jutha Date: Wed, 28 Aug 2024 20:44:57 -0400 Subject: [PATCH 17/25] [NayNay] File Restructure: Signing Restructure (#218) * [NayNay] File Restructure: Signing Restructure - created new file structure for signing flow - updated tui/cli with new changes * some cleanup; fixed sign tests * removed inquirer input from commands file, added file inoput back to signing * added raw sign back in * wip: raw sign stuff * wip: finished off last changes for raw sign but need to test * wip: porting to newly defined structure * updated signing to new structure, and added utils-cli from mix pr * updated file structure for balance * updated file structure for transfer * pr review updates * updated to sign not signing * signing-restructure tweeeeks (#220) * tweeeeks * more tweeks * fix tests * rename test * fix maskPayload * test fix * change sign command to just return sig * stdout cleanup for balance and sign * tweeks * fix transfer to allow decimal transfers --------- Co-authored-by: mix irving --- src/balance/command.ts | 33 ++-- src/balance/interaction.ts | 12 ++ src/balance/main.ts | 34 ++++ src/balance/utils.ts | 25 +-- src/cli.ts | 160 ++++-------------- .../{base-command.ts => entropy-base.ts} | 2 +- src/common/masking.ts | 2 + src/common/utils-cli.ts | 79 +++++++++ src/common/utils.ts | 9 +- src/config/index.ts | 1 + src/config/types.ts | 1 + src/flows/index.ts | 1 - src/flows/sign/cli.ts | 21 --- src/flows/sign/index.ts | 114 ------------- src/flows/sign/sign.ts | 23 --- src/sign/command.ts | 31 ++++ src/sign/constants.ts | 38 +++++ src/sign/interaction.ts | 39 +++++ src/sign/main.ts | 64 +++++++ src/sign/types.ts | 11 ++ src/sign/utils.ts | 96 +++++++++++ src/transfer/command.ts | 69 +++----- src/transfer/constants.ts | 12 ++ src/transfer/interaction.ts | 16 ++ src/transfer/main.ts | 69 ++++++++ src/transfer/types.ts | 4 +- src/transfer/utils.ts | 53 +++--- src/tui.ts | 70 ++++---- tests/balance.test.ts | 11 +- tests/sign.test.ts | 17 +- tests/transfer.test.ts | 52 +++--- 31 files changed, 682 insertions(+), 487 deletions(-) create mode 100644 src/balance/interaction.ts create mode 100644 src/balance/main.ts rename src/common/{base-command.ts => entropy-base.ts} (89%) create mode 100644 src/common/utils-cli.ts delete mode 100644 src/flows/sign/cli.ts delete mode 100644 src/flows/sign/index.ts delete mode 100644 src/flows/sign/sign.ts create mode 100644 src/sign/command.ts create mode 100644 src/sign/constants.ts create mode 100644 src/sign/interaction.ts create mode 100644 src/sign/main.ts create mode 100644 src/sign/types.ts create mode 100644 src/sign/utils.ts create mode 100644 src/transfer/constants.ts create mode 100644 src/transfer/interaction.ts create mode 100644 src/transfer/main.ts diff --git a/src/balance/command.ts b/src/balance/command.ts index d3c1a70f..37879e5f 100644 --- a/src/balance/command.ts +++ b/src/balance/command.ts @@ -1,18 +1,19 @@ -import Entropy from "@entropyxyz/sdk" -import { BaseCommand } from "../common/base-command" -import * as BalanceUtils from "./utils" +import Entropy from "@entropyxyz/sdk"; +import { Command } from "commander"; +import { cliWrite, endpointOption, passwordOption } from "src/common/utils-cli"; +import { EntropyBalance } from "./main"; -const FLOW_CONTEXT = 'ENTROPY-BALANCE' -export class BalanceCommand extends BaseCommand { - constructor (entropy: Entropy, endpoint: string) { - super(entropy, endpoint, FLOW_CONTEXT) - } +export async function entropyBalanceCommand (entropy: Entropy, rootCommand: Command) { + rootCommand.command('balance') + .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 BalanceService = new EntropyBalance(entropy, opts.endpoint) + const balance = await BalanceService.getBalance(address) + cliWrite(`${balance.toLocaleString('en-US')} BITS`) + process.exit(0) + }) - public async getBalance (address: string) { - const balance = await BalanceUtils.getBalance(this.entropy, address) - - this.logger.log(`Current balance of ${address}: ${balance}`, `${BalanceCommand.name}`) - - return `${balance.toLocaleString('en-US')} BITS` - } -} \ No newline at end of file +} diff --git a/src/balance/interaction.ts b/src/balance/interaction.ts new file mode 100644 index 00000000..aa9f7cda --- /dev/null +++ b/src/balance/interaction.ts @@ -0,0 +1,12 @@ +import { print } from "src/common/utils" +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) + 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/balance/main.ts b/src/balance/main.ts new file mode 100644 index 00000000..2c865413 --- /dev/null +++ b/src/balance/main.ts @@ -0,0 +1,34 @@ +import Entropy from "@entropyxyz/sdk" +import { EntropyBase } from "../common/entropy-base" +import * as BalanceUtils from "./utils" +import { BalanceInfo } from "./types" + +const FLOW_CONTEXT = 'ENTROPY-BALANCE' +export class EntropyBalance extends EntropyBase { + constructor (entropy: Entropy, endpoint: string) { + super(entropy, endpoint, FLOW_CONTEXT) + } + + async getBalance (address: string): Promise { + const accountInfo = (await this.entropy.substrate.query.system.account(address)) as any + const balance = parseInt(BalanceUtils.hexToBigInt(accountInfo.data.free).toString()) + + this.logger.log(`Current balance of ${address}: ${balance}`, EntropyBalance.name) + return balance + } + + async getBalances (addresses: string[]): Promise { + const balanceInfo: BalanceInfo = {} + await Promise.all(addresses.map(async address => { + try { + const balance = await this.getBalance(address) + + balanceInfo[address] = { balance } + } catch (error) { + balanceInfo[address] = { error: error.message } + } + })) + + return balanceInfo + } +} diff --git a/src/balance/utils.ts b/src/balance/utils.ts index 917d0840..2fe9b038 100644 --- a/src/balance/utils.ts +++ b/src/balance/utils.ts @@ -1,24 +1 @@ -import Entropy from "@entropyxyz/sdk"; -import { BalanceInfo } from "./types"; - -const hexToBigInt = (hexString: string) => BigInt(hexString) - -export async function getBalance (entropy: Entropy, address: string): Promise { - const accountInfo = (await entropy.substrate.query.system.account(address)) as any - return parseInt(hexToBigInt(accountInfo.data.free).toString()) -} - -export async function getBalances (entropy: Entropy, addresses: string[]): Promise { - const balanceInfo: BalanceInfo = {} - await Promise.all(addresses.map(async address => { - try { - const balance = await getBalance(entropy, address) - - balanceInfo[address] = { balance } - } catch (error) { - balanceInfo[address] = { error: error.message } - } - })) - - return balanceInfo -} \ No newline at end of file +export const hexToBigInt = (hexString: string) => BigInt(hexString) \ No newline at end of file diff --git a/src/cli.ts b/src/cli.ts index 6e1e9bc2..2e2838e2 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -2,92 +2,33 @@ /* 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 { cliListAccounts } from './flows/manage-accounts/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 { TransferCommand } from './transfer/command' +import { cliListAccounts } from './flows/manage-accounts/cli' +import { currentAccountAddressOption, endpointOption, loadEntropy, cliWrite } from './common/utils-cli' +import { entropyTransferCommand } from './transfer/command' +import { entropySignCommand } from './sign/command' +import { entropyBalanceCommand } from './balance/command' +import { EntropyTuiOptions } from './types' +import launchTui from './tui' const program = new Command() -// Array of restructured commands to make it easier to migrate them to the new "flow" -const RESTRUCTURED_COMMANDS = ['balance'] - -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 { - 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 } /* no command */ @@ -105,16 +46,12 @@ 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 + + await setEntropyGlobal(address, endpoint, password) }) .action((options: EntropyTuiOptions) => { launchTui(entropy, options) @@ -127,56 +64,17 @@ program.command('list') .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) + cliWrite(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 balanceCommand = new BalanceCommand(entropy, opts.endpoint) - const balance = await balanceCommand.getBalance(address) - writeOut(balance) - process.exit(0) - }) +entropyBalanceCommand(entropy, program) /* 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()) - .addOption(currentAccountAddressOption()) - .action(async (_source, destination, amount, opts) => { - const transferCommand = new TransferCommand(entropy, opts.endpoint) - await transferCommand.sendTransfer(destination, amount) - // writeOut(??) // TODO: write the output - process.exit(0) - }) +entropyTransferCommand(entropy, program) /* 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()) - .addOption(currentAccountAddressOption()) - .action(async (address, message, opts) => { - const signature = await cliSign({ address, message, ...opts }) - writeOut(signature) - process.exit(0) - }) - -function writeOut (result) { - const prettyResult = stringify(result) - process.stdout.write(prettyResult) -} +entropySignCommand(entropy, program) program.parseAsync().then(() => {}) 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/common/masking.ts b/src/common/masking.ts index 4118ddb8..ae9aa07f 100644 --- a/src/common/masking.ts +++ b/src/common/masking.ts @@ -7,6 +7,8 @@ const DEFAULT_MASKED_FIELDS = [ ]; export function maskPayload (payload: any): any { + if (typeof payload === 'string') return payload + const clonedPayload = cloneDeep(payload); const maskedPayload = {} diff --git a/src/common/utils-cli.ts b/src/common/utils-cli.ts new file mode 100644 index 00000000..a5f423ad --- /dev/null +++ b/src/common/utils-cli.ts @@ -0,0 +1,79 @@ +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, 0) + 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 (account) => { + if (account === storedConfig.selectedAccount) return account + // Updated selected account in config with new address from this option + const newConfigUpdates = { selectedAccount: account } + await config.set({ ...storedConfig, ...newConfigUpdates }) + + return account + }) + .hideHelp() + .default(storedConfig.selectedAccount) +} + +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}`) + + // 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 ') + } + + 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 +} diff --git a/src/common/utils.ts b/src/common/utils.ts index ace2af19..d51e3dd2 100644 --- a/src/common/utils.ts +++ b/src/common/utils.ts @@ -65,6 +65,11 @@ export function accountChoicesWithOther (accounts: EntropyAccountConfig[]) { .concat([{ name: "Other", value: null }]) } -export function getSelectedAccount (accounts: EntropyAccountConfig[], address: string) { - return accounts.find(account => account.address === address) +export function getSelectedAccount (accounts: EntropyAccountConfig[], aliasOrAddress: string) { + if (!aliasOrAddress || !aliasOrAddress.length) throw Error('aliasOrAddress required') + + return ( + accounts.find(account => account.address === aliasOrAddress) || + accounts.find(account => account.name === aliasOrAddress) + ) } diff --git a/src/config/index.ts b/src/config/index.ts index 679314e2..d5a7a145 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -57,6 +57,7 @@ function noop () {} export async function get (configPath = CONFIG_PATH) { const configBuffer = await readFile(configPath) + return deserialize(configBuffer.toString()) } diff --git a/src/config/types.ts b/src/config/types.ts index 74d52ffc..7910c58d 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -2,6 +2,7 @@ export interface EntropyConfig { accounts: EntropyAccountConfig[] endpoints: { dev: string; 'test-net': string } 'migration-version': string + selectedAccount: string } export interface EntropyAccountConfig { diff --git a/src/flows/index.ts b/src/flows/index.ts index 474d6823..389e5483 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 { manageAccounts } from './manage-accounts' diff --git a/src/flows/sign/cli.ts b/src/flows/sign/cli.ts deleted file mode 100644 index 5fb4c6aa..00000000 --- a/src/flows/sign/cli.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { EntropyLogger } from "src/common/logger" -import { initializeEntropy } from "../../common/initializeEntropy" -import * as config from '../../config' -import { signWithAdapters } from './sign' - -// TODO: revisit this file, rename as signEthTransaction? -export async function cliSign ({ address, message, endpoint }) { - const logger = new EntropyLogger('CLI::SIGN', 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? - logger.debug('account:') - logger.debug(account) - - const entropy = await initializeEntropy({ keyMaterial: account.data, endpoint }) - - return signWithAdapters(entropy, { - msg: message - }) -} diff --git a/src/flows/sign/index.ts b/src/flows/sign/index.ts deleted file mode 100644 index 0c3f4074..00000000 --- a/src/flows/sign/index.ts +++ /dev/null @@ -1,114 +0,0 @@ -import inquirer from "inquirer" -import { u8aToHex } from '@polkadot/util' -import { initializeEntropy } from "../../common/initializeEntropy" -import { getSelectedAccount, print } from "../../common/utils" -import { signWithAdapters } from './sign' -import { EntropyLogger } from "src/common/logger" - -async function signWithAdaptersInOrder (entropy) { - let msg - const { messageAction } = await inquirer.prompt([{ - type: 'list', - name: 'messageAction', - message: 'Please choose how you would like to input your message to sign:', - choices: [ - 'Text Input', - /* DO NOT DELETE THIS */ - // 'From a File', - ], - }]) - switch (messageAction) { - case 'Text Input': { - const { userInput } = await inquirer.prompt([{ - type: "editor", - name: "userInput", - message: "Enter the message you wish to sign (this will open your default editor):", - }]) - msg = userInput - break - } - /* DO NOT DELETE THIS */ - // case 'From a File': { - // const { pathToFile } = await inquirer.prompt([{ - // type: 'input', - // name: 'pathToFile', - // message: 'Enter the path to the file you wish to sign:', - // }]) - // // TODO: relative/absolute path? encoding? - // msg = readFileSync(pathToFile, 'utf-8') - // break - // } - default: { - console.error('Unsupported Action') - return - } - } - - print('msg to be signed:', msg) - print('verifying key:', entropy.signingManager.verifyingKey) - const signature = await signWithAdapters(entropy, { msg }) - const signatureHexString = u8aToHex(signature) - print('signature:', signatureHexString) -} - -export async function sign ({ accounts, selectedAccount: selectedAccountAddress }, options, logger: EntropyLogger) { - const FLOW_CONTEXT = 'SIGN' - const { endpoint } = options - const actionChoice = await inquirer.prompt([ - { - type: "list", - name: "action", - message: "What would you like to do?", - choices: [ - // Removing the option to select Raw Sign until we fully release signing. - // We will need to update the flow to ask the user to input the auxilary data for the signature request - // "Raw Sign", - "Sign With Adapter", - "Exit to Main Menu", - ], - }, - ]) - - const selectedAccount = getSelectedAccount(accounts, selectedAccountAddress) - logger.debug("selectedAccount:", FLOW_CONTEXT) - logger.debug(selectedAccount, FLOW_CONTEXT) - const keyMaterial = selectedAccount?.data; - - const entropy = await initializeEntropy({ keyMaterial, endpoint }) - const { address } = entropy.keyring.accounts.registration - logger.debug("address:", FLOW_CONTEXT) - logger.debug(address, FLOW_CONTEXT) - if (address == undefined) { - throw new Error("address issue") - } - switch (actionChoice.action) { - // case 'Raw Sign': { - // const msg = Buffer.from('Hello world: new signature from entropy!').toString('hex') - // debug('msg', msg); - // const signature = await entropy.sign({ - // sigRequestHash: msg, - // hash: 'sha3', - // naynay does not think he is doing this properly - // auxiliaryData: [ - // { - // public_key_type: 'sr25519', - // public_key: Buffer.from(entropy.keyring.accounts.registration.pair.publicKey).toString('base64'), - // signature: entropy.keyring.accounts.registration.pair.sign(msg), - // context: 'substrate', - // }, - // ], - // }) - - // print('signature:', signature) - // return - // } - case 'Sign With Adapter': { - await signWithAdaptersInOrder(entropy) - return - } - case 'Exit to Main Menu': - return 'exit' - default: - throw new Error('Unrecognizable action') - } -} diff --git a/src/flows/sign/sign.ts b/src/flows/sign/sign.ts deleted file mode 100644 index e51e6924..00000000 --- a/src/flows/sign/sign.ts +++ /dev/null @@ -1,23 +0,0 @@ -interface SignWithAdapterInput { - /** the message as a utf-8 encoded string */ - msg: string, - verifyingKey?: string, - // LATER: - // auxillaryData: any -} - -function stringToHex (str: string): string { - return Buffer.from(str).toString('hex') -} - -export async function signWithAdapters (entropy, input: SignWithAdapterInput) { - return entropy.signWithAdaptersInOrder({ - msg: { - msg: stringToHex(input.msg) - }, - // type - order: ['deviceKeyProxy', 'noop'], - signatureVerifyingKey: input.verifyingKey - // auxillaryData - }) -} diff --git a/src/sign/command.ts b/src/sign/command.ts new file mode 100644 index 00000000..5caa6ce5 --- /dev/null +++ b/src/sign/command.ts @@ -0,0 +1,31 @@ +import { Command, /* Option */ } from 'commander' +import { Entropy } from '@entropyxyz/sdk' +import { cliWrite, currentAccountAddressOption, endpointOption, passwordOption } from '../common/utils-cli' +import { EntropySign } from './main' + +export async function entropySignCommand (entropy: Entropy, rootCommand: Command) { + rootCommand.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)')) + .addOption(endpointOption()) + .addOption(currentAccountAddressOption()) + // .addOption( + // new Option( + // '-r, --raw', + // 'Signs the provided message using the Raw Signing method. Output is a signature (string)' + // ) + // ) + .action(async (msg, opts) => { + 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 + /** + * if (opts.raw) { + * implement raw sign here + * } + */ + const { verifyingKey, signature } = await SigningService.signMessageWithAdapters({ msg }) + cliWrite({ verifyingKey, signature }) + process.exit(0) + }) +} diff --git a/src/sign/constants.ts b/src/sign/constants.ts new file mode 100644 index 00000000..4d497f0e --- /dev/null +++ b/src/sign/constants.ts @@ -0,0 +1,38 @@ +export const FLOW_CONTEXT = 'ENTROPY_SIGN' + +export const SIGNING_CONTENT = { + messageAction: { + name: 'messageAction', + message: 'Please choose how you would like to input your message to sign:', + choices: [ + 'Text Input', + // Input from file requires more design + // 'From a File', + ], + }, + textInput: { + name: "userInput", + message: "Enter the message you wish to sign (this will open your default editor):", + }, + pathToFile: { + name: 'pathToFile', + message: 'Enter the path to the file you wish to sign:', + }, + interactionChoice: { + name: "interactionChoice", + message: "What would you like to do?", + choices: [ + // "Raw Sign", + "Sign With Adapter", + "Exit to Main Menu", + ], + }, + hashingAlgorithmInput: { + name: 'hashingAlgorithm', + message: 'Enter the hashing algorigthm to be used:', + }, + auxiliaryDataInput: { + name: 'auxiliaryDataFile', + message: 'Enter path to file containing auxiliary data for signing:' + }, +} \ No newline at end of file diff --git a/src/sign/interaction.ts b/src/sign/interaction.ts new file mode 100644 index 00000000..5a3c68ba --- /dev/null +++ b/src/sign/interaction.ts @@ -0,0 +1,39 @@ +import { print } from "src/common/utils" +import { getMsgFromUser, /* interactionChoiceQuestions */ } from "./utils" +import inquirer from "inquirer" +import Entropy from "@entropyxyz/sdk" +import { EntropySign } from "./main" + +export async function entropySign (entropy: Entropy, endpoint: string) { + const SigningService = new EntropySign(entropy, endpoint) + // const { interactionChoice } = await inquirer.prompt(interactionChoiceQuestions) + // switch (interactionChoice) { + // case 'Raw Sign': { + // const { msg, msgPath } = await getMsgFromUser(inquirer) + // const { hashingAlgorithm, auxiliaryDataFile } = await inquirer.prompt(rawSignParamsQuestions) + // let hash = hashingAlgorithm + // const auxiliaryData = JSON.parse(readFileSync(auxiliaryDataFile).toString()) + // if (JSON.parse(hashingAlgorithm)) { + // hash = JSON.parse(hashingAlgorithm) + // } + + // const { signature, verifyingKey } = await Sign.rawSignMessage({ msg, msgPath, hashingAlgorithm: hash, auxiliaryData }) + // print('msg to be signed:', msg) + // print('verifying key:', verifyingKey) + // print('signature:', signature) + // return + // } + // case 'Sign With Adapter': { + const { msg } = await getMsgFromUser(inquirer) + const { signature, verifyingKey } = await SigningService.signMessageWithAdapters({ msg }) + print('msg to be signed:', msg) + print('verifying key:', verifyingKey) + print('signature:', signature) + // return + // } + // case 'Exit to Main Menu': + // return 'exit' + // default: + // throw new Error('Unrecognizable action') + // } +} diff --git a/src/sign/main.ts b/src/sign/main.ts new file mode 100644 index 00000000..b47d6531 --- /dev/null +++ b/src/sign/main.ts @@ -0,0 +1,64 @@ +import Entropy from "@entropyxyz/sdk" +import { u8aToHex } from '@polkadot/util' +import { EntropyBase } from "../common/entropy-base"; +import { SignResult } from "./types"; +import { FLOW_CONTEXT } from "./constants"; +import { stringToHex } from "./utils"; + +export class EntropySign extends EntropyBase { + constructor (entropy: Entropy, endpoint: string) { + super(entropy, endpoint, FLOW_CONTEXT) + } + + // async rawSign (entropy: Entropy, payload: RawSignPayload) { + // return entropy.sign(payload) + // } + + // async rawSignMessage ({ msg, msgPath, auxiliaryData, hashingAlgorithm }): Promise { + // const message = getMsgFromInputOrFile(msg, msgPath) + + // try { + // this.logger.log(`Msg to be signed: ${msg}`, 'SIGN_MSG') + // this.logger.log( `Verifying Key used: ${this.entropy.signingManager.verifyingKey}`) + // const signature = await rawSign( + // this.entropy, + // { + // sigRequestHash: stringAsHex(message), + // hash: hashingAlgorithm, + // auxiliaryData + // } + // ) + // const signatureHexString = u8aToHex(signature) + // this.logger.log(`Signature: ${signatureHexString}`) + + // return { signature: signatureHexString, verifyingKey: this.entropy.signingManager.verifyingKey } + // } catch (error) { + // this.logger.error('Error signing message', error) + // throw error + // } + // } + + async signMessageWithAdapters ({ msg }: { msg: string }): Promise { + try { + this.logger.log(`Msg to be signed: ${msg}`, 'SIGN_MSG') + this.logger.log( `Verifying Key used: ${this.entropy.signingManager.verifyingKey}`) + const signature: any = await this.entropy.signWithAdaptersInOrder({ + msg: { + msg: stringToHex(msg) + }, + // type + order: ['deviceKeyProxy', 'noop'], + // signatureVerifyingKey: verifyingKey + // auxillaryData + }) + + const signatureHexString = u8aToHex(signature) + this.logger.log(`Signature: ${signatureHexString}`) + + return { signature: signatureHexString, verifyingKey: this.entropy.signingManager.verifyingKey } + } catch (error) { + this.logger.error('Error signing message', error) + throw error + } + } +} diff --git a/src/sign/types.ts b/src/sign/types.ts new file mode 100644 index 00000000..4e222b82 --- /dev/null +++ b/src/sign/types.ts @@ -0,0 +1,11 @@ +export interface SignResult { + signature: string + verifyingKey: string +} + +export interface RawSignPayload { + sigRequestHash: string + hash: any + auxiliaryData: any + signatureVerifyingKey?: string +} diff --git a/src/sign/utils.ts b/src/sign/utils.ts new file mode 100644 index 00000000..0b63020d --- /dev/null +++ b/src/sign/utils.ts @@ -0,0 +1,96 @@ +import { readFileSync } from "fs" +import { SIGNING_CONTENT } from "./constants" +import { isHex } from '@polkadot/util' + +export function stringToHex (str: string): string { + if (isHex(str)) return str; + return Buffer.from(str).toString('hex') +} + +export const interactionChoiceQuestions = [{ + type: "list", + name: SIGNING_CONTENT.interactionChoice.name, + message: SIGNING_CONTENT.interactionChoice.message, + choices: SIGNING_CONTENT.interactionChoice.choices +}] + +export const messageActionQuestions = [{ + type: 'list', + name: SIGNING_CONTENT.messageAction.name, + message: SIGNING_CONTENT.messageAction.message, + choices: SIGNING_CONTENT.messageAction.choices, +}] + +export const userInputQuestions = [{ + type: "editor", + name: SIGNING_CONTENT.textInput.name, + message: SIGNING_CONTENT.textInput.message, +}] + +export const filePathInputQuestions = [{ + type: 'input', + name: SIGNING_CONTENT.pathToFile.name, + message: SIGNING_CONTENT.pathToFile.message, +}] + +export const hashingAlgorithmQuestions = [{ + type: 'input', + name: SIGNING_CONTENT.hashingAlgorithmInput.name, + message: SIGNING_CONTENT.hashingAlgorithmInput.message, +}] + +export const auxiliaryDataQuestions = [{ + type: 'input', + name: SIGNING_CONTENT.auxiliaryDataInput.name, + message: SIGNING_CONTENT.auxiliaryDataInput.message, +}] + +export const rawSignParamsQuestions = [ + ...hashingAlgorithmQuestions, + ...auxiliaryDataQuestions +] + +export async function getMsgFromUser (inquirer) { + // let msg: string + // let msgPath: string + // const { messageAction } = await inquirer.prompt(messageActionQuestions) + // switch (messageAction) { + // case 'Text Input': { + const { userInput } = await inquirer.prompt(userInputQuestions) + const msg = userInput + // break + // } + // Msg input from a file requires more design + // case 'From a File': { + // const { pathToFile } = await inquirer.prompt(filePathInputQuestions) + // // TODO: relative/absolute path? encoding? + // msgPath = pathToFile + // break + // } + // default: { + // const error = new Error('SigningError: Unsupported User Input Action') + // this.logger.error('Error signing with adapter', error) + // return + // } + // } + return { + msg, + // msgPath + }; +} + +export function getMsgFromInputOrFile (msg?: string, msgPath?: string) { + let result: string = msg + if (!msg && !msgPath) { + throw new Error('SigningError: You must provide a message or path to a file') + } + if (!msg && msgPath) { + try { + result = readFileSync(msgPath, 'utf-8') + } catch (error) { + // noop + } + } + + return result +} diff --git a/src/transfer/command.ts b/src/transfer/command.ts index 7fee0ff1..34b5ac34 100644 --- a/src/transfer/command.ts +++ b/src/transfer/command.ts @@ -1,51 +1,20 @@ -import Entropy from "@entropyxyz/sdk"; -import { BaseCommand } from "../common/base-command"; -import { setupProgress } from "../common/progress"; -import * as TransferUtils from './utils' -import inquirer from "inquirer"; +import Entropy from "@entropyxyz/sdk" +import { Command } from "commander" +import { currentAccountAddressOption, endpointOption, passwordOption } from "src/common/utils-cli" +import { EntropyTransfer } from "./main" -const FLOW_CONTEXT = 'ENTROPY_TRANSFER' -const question = [ - { - type: "input", - name: "amount", - message: "Input amount to transfer:", - default: "1", - validate: (amount) => { - if (isNaN(amount) || parseInt(amount) <= 0) { - return 'Please enter a value greater than 0' - } - return true - } - }, - { - type: "input", - name: "recipientAddress", - message: "Input recipient's address:", - }, -] - -export class TransferCommand extends BaseCommand { - constructor (entropy: Entropy, endpoint: string) { - super(entropy, endpoint, FLOW_CONTEXT) - } - - public async askQuestions () { - return inquirer.prompt(question) - } - - public async sendTransfer (toAddress: string, amount: string) { - const { start: startProgress, stop: stopProgress } = setupProgress('Transferring Funds') - - const formattedAmount = BigInt(parseInt(amount) * 1e10) - startProgress() - try { - const transferStatus = await TransferUtils.transfer(this.entropy, { from: this.entropy.keyring.accounts.registration.pair, to: toAddress, amount: formattedAmount }) - if (transferStatus.isFinalized) return stopProgress() - } catch (error) { - this.logger.error('There was an issue sending this transfer', error) - stopProgress() - throw error - } - } -} \ No newline at end of file +export async function entropyTransferCommand (entropy: Entropy, rootCommand: Command) { + rootCommand.command('transfer') + .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') + .addOption(passwordOption('Password for the source account (if required)')) + .addOption(endpointOption()) + .addOption(currentAccountAddressOption()) + .action(async (destination, amount, opts) => { + 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/constants.ts b/src/transfer/constants.ts new file mode 100644 index 00000000..5529b5be --- /dev/null +++ b/src/transfer/constants.ts @@ -0,0 +1,12 @@ +export const TRANSFER_CONTENT = { + amount: { + name: 'amount', + message: 'Input amount to transfer:', + default: '1', + invalidError: 'Please enter a value greater than 0', + }, + recipientAddress: { + name: 'recipientAddress', + message: `Input recipient's address:`, + }, +} \ No newline at end of file diff --git a/src/transfer/interaction.ts b/src/transfer/interaction.ts new file mode 100644 index 00000000..cd8e83b8 --- /dev/null +++ b/src/transfer/interaction.ts @@ -0,0 +1,16 @@ +import inquirer from "inquirer" +import { print } from "../common/utils" +import { EntropyTransfer } from "./main" +import { transferInputQuestions } from "./utils" +import { setupProgress } from "src/common/progress" + +export async function entropyTransfer (entropy, endpoint) { + const progressTracker = setupProgress('Transferring Funds') + const TransferService = new EntropyTransfer(entropy, endpoint) + const { amount, recipientAddress } = await inquirer.prompt(transferInputQuestions) + await TransferService.transfer(recipientAddress, amount, progressTracker) + print('') + print(`Transaction successful: Sent ${amount} to ${recipientAddress}`) + print('') + print('Press enter to return to main menu') +} diff --git a/src/transfer/main.ts b/src/transfer/main.ts new file mode 100644 index 00000000..45ea504b --- /dev/null +++ b/src/transfer/main.ts @@ -0,0 +1,69 @@ +import Entropy from "@entropyxyz/sdk"; +import { EntropyBase } from "../common/entropy-base"; +import { TransferOptions } from "./types"; + +const FLOW_CONTEXT = 'ENTROPY_TRANSFER' + +export class EntropyTransfer extends EntropyBase { + constructor (entropy: Entropy, endpoint: string) { + super(entropy, endpoint, FLOW_CONTEXT) + } + + // NOTE: a more accessible function which handles + // - setting `from` + // - converting `amount` (string => BigInt) + // - progress callbacks (optional) + + async transfer (toAddress: string, amount: string, progress?: { start: ()=>void, stop: ()=>void }) { + const formattedAmount = BigInt(Number(amount) * 1e10) + // TODO: name this multiplier 1e10 somewhere + + if (progress) progress.start() + try { + await this.rawTransfer({ + from: this.entropy.keyring.accounts.registration.pair, + to: toAddress, + amount: formattedAmount + }) + if (progress) return progress.stop() + } catch (error) { + if (progress) return progress.stop() + throw error + } + } + + private async rawTransfer (payload: TransferOptions): Promise { + const { from, to, amount } = payload + + return new Promise((resolve, reject) => { + // WARN: await signAndSend is dangerous as it does not resolve + // after transaction is complete :melt: + this.entropy.substrate.tx.balances + .transferAllowDeath(to, amount) + // @ts-ignore + .signAndSend(from, ({ 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 sending this transfer', error) + return reject(error) + } + + if (status.isFinalized) resolve(status) + }) + }) + } + +} diff --git a/src/transfer/types.ts b/src/transfer/types.ts index 1ceaf8b2..365cdaac 100644 --- a/src/transfer/types.ts +++ b/src/transfer/types.ts @@ -4,5 +4,5 @@ import { Pair } from '@entropyxyz/sdk/keys' export interface TransferOptions { from: Pair to: string - amount: bigint -} \ No newline at end of file + amount: bigint +} diff --git a/src/transfer/utils.ts b/src/transfer/utils.ts index d8663eee..d14ed76a 100644 --- a/src/transfer/utils.ts +++ b/src/transfer/utils.ts @@ -1,34 +1,27 @@ -import Entropy from "@entropyxyz/sdk"; -import { TransferOptions } from "./types"; +import { TRANSFER_CONTENT } from "./constants"; -export async function transfer (entropy: Entropy, payload: TransferOptions): Promise { - const { from, to, amount } = payload +function validateAmount (amount: string | number) { + if (isNaN(amount as number) || parseInt(amount as string) <= 0) { + return TRANSFER_CONTENT.amount.invalidError + } + return true +} - return new Promise((resolve, reject) => { - // WARN: await signAndSend is dangerous as it does not resolve - // after transaction is complete :melt: - entropy.substrate.tx.balances - .transferAllowDeath(to, amount) - // @ts-ignore - .signAndSend(from, ({ status, dispatchError }) => { - if (dispatchError) { - let msg: string - if (dispatchError.isModule) { - // for module errors, we have the section indexed, lookup - const decoded = entropy.substrate.registry.findMetaError( - dispatchError.asModule - ) - const { docs, name, section } = decoded +const amountQuestion = { + type: 'input', + name: TRANSFER_CONTENT.amount.name, + message: TRANSFER_CONTENT.amount.message, + default: TRANSFER_CONTENT.amount.default, + validate: validateAmount +} - msg = `${section}.${name}: ${docs.join(' ')}` - } else { - // Other, CannotLookup, BadOrigin, no extra info - msg = dispatchError.toString() - } - return reject(Error(msg)) - } +const recipientAddressQuestion = { + type: 'input', + name: TRANSFER_CONTENT.recipientAddress.name, + message: TRANSFER_CONTENT.recipientAddress.message, +} - if (status.isFinalized) resolve(status) - }) - }) -} \ No newline at end of file +export const transferInputQuestions = [ + amountQuestion, + recipientAddressQuestion +] \ No newline at end of file diff --git a/src/tui.ts b/src/tui.ts index 4db275f4..a438db65 100644 --- a/src/tui.ts +++ b/src/tui.ts @@ -6,11 +6,31 @@ import { EntropyTuiOptions } from './types' import { logo } from './common/ascii' import { print } from './common/utils' import { EntropyLogger } from './common/logger' -import { BalanceCommand } from './balance/command' -import { TransferCommand } from './transfer/command' -import { loadEntropy } from './cli' +import { loadEntropy } from './common/utils-cli' +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 shouldInit = true + let storedConfig = await config.get() + + // set selectedAccount if we can + if (!storedConfig.selectedAccount && storedConfig.accounts.length) { + await config.set({ + selectedAccount: storedConfig.accounts[0].address, + ...storedConfig + }) + storedConfig = await config.get() + } + + return storedConfig +} // tui = text user interface export default function tui (entropy: Entropy, options: EntropyTuiOptions) { @@ -24,7 +44,7 @@ export default function tui (entropy: Entropy, options: EntropyTuiOptions) { // leaving as a noop function until all flows are restructured 'Balance': () => {}, 'Register': flows.entropyRegister, - 'Sign': flows.sign, + 'Sign': () => {}, 'Transfer': () => {}, // TODO: design programs in TUI (merge deploy+user programs) 'Deploy Program': flows.devPrograms, @@ -45,23 +65,13 @@ export default function tui (entropy: Entropy, options: EntropyTuiOptions) { } async function main (entropy: Entropy, choices, options, logger: EntropyLogger) { - if (shouldInit) { - await config.init() - shouldInit = false - } - - let storedConfig = await config.get() - - // if there are accounts available and selected account is not set, - // first account in list is set as the selected account - if (!storedConfig.selectedAccount && storedConfig.accounts.length) { - await config.set({ ...storedConfig, ...{ selectedAccount: storedConfig.accounts[0].address } }) - storedConfig = await config.get() - } + let storedConfig = await setupConfig() // If the selected account changes within the TUI we need to reset the entropy instance being used - if (storedConfig.selectedAccount !== entropy.keyring.accounts.registration.address) { - entropy = await loadEntropy(storedConfig.selectedAccount, options.endpoint) + const currentAccount = entropy.keyring.accounts.registration.address + if (currentAccount !== storedConfig.selectedAccount) { + await entropy.close() + entropy = await loadEntropy(storedConfig.selectedAccount, options.endpoint); } const answers = await inquirer.prompt([{ @@ -86,9 +96,7 @@ async function main (entropy: Entropy, choices, options, logger: EntropyLogger) switch (answers.choice) { case "Balance": { try { - const balanceCommand = new BalanceCommand(entropy, options.endpoint) - const balanceString = await balanceCommand.getBalance(storedConfig.selectedAccount) - print(`Address ${storedConfig.selectedAccount} has a balance of: ${balanceString}`) + await entropyBalance(entropy, options.endpoint, storedConfig) } catch (error) { console.error('There was an error retrieving balance', error) } @@ -96,18 +104,20 @@ async function main (entropy: Entropy, choices, options, logger: EntropyLogger) } case "Transfer": { try { - const transferCommand = new TransferCommand(entropy, options.endpoint) - const { amount, recipientAddress } = await transferCommand.askQuestions() - await transferCommand.sendTransfer(recipientAddress, amount) - print('') - print(`Transaction successful: Sent ${amount} to ${recipientAddress}`) - print('') - print('Press enter to return to main menu') + await entropyTransfer(entropy, options.endpoint) } catch (error) { console.error('There was an error sending the transfer', error) } break } + case "Sign": { + try { + await entropySign(entropy, options.endpoint) + } catch (error) { + console.error('There was an issue with signing', error) + } + break + } default: { const newConfigUpdates = await choices[answers.choice](storedConfig, options, logger) if (typeof newConfigUpdates === 'string' && newConfigUpdates === 'exit') { diff --git a/tests/balance.test.ts b/tests/balance.test.ts index f95d7e16..3edf9aaa 100644 --- a/tests/balance.test.ts +++ b/tests/balance.test.ts @@ -1,31 +1,32 @@ import test from 'tape' import { setupTest, charlieStashAddress as richAddress } from './testing-utils' -import * as BalanceUtils from '../src/balance/utils' +import { EntropyBalance } from '../src/balance/main' const networkType = 'two-nodes' test('getBalance + getBalances', async (t) => { const { run, entropy, endpoint } = await setupTest(t, { networkType }) + const BalanceService = new EntropyBalance(entropy, endpoint) const newAddress = entropy.keyring.accounts.registration.address /* getBalance */ const newAddressBalance = await run( 'getBalance (newSeed)', - BalanceUtils.getBalance(entropy, newAddress) + BalanceService.getBalance(newAddress) ) t.equal(newAddressBalance, 0, 'newSeed balance = 0') const richAddressBalance = await run( 'getBalance (richAddress)', - BalanceUtils.getBalance(entropy, richAddress) + BalanceService.getBalance(richAddress) ) t.true(richAddressBalance > BigInt(10e10), 'richAddress balance >>> 0') /* getBalances */ const balances = await run( 'getBalances', - BalanceUtils.getBalances(entropy, [newAddress, richAddress]) + BalanceService.getBalances([newAddress, richAddress]) ) t.deepEqual( balances, @@ -39,7 +40,7 @@ test('getBalance + getBalances', async (t) => { const badAddresses = ['5Cz6BfUaxxXCA3jninzxdan4JdmC1NVpgkiRPYhXbhr', '5Cz6BfUaxxXCA3jninzxdan4JdmC1NVpgkiRPYhXbhrfnD'] const balancesWithNoGoodAddress = await run( 'getBalances::one good address', - BalanceUtils.getBalances(entropy, badAddresses) + BalanceService.getBalances(badAddresses) ) badAddresses.forEach(addr => { diff --git a/tests/sign.test.ts b/tests/sign.test.ts index aef6795d..bdc567f7 100644 --- a/tests/sign.test.ts +++ b/tests/sign.test.ts @@ -1,20 +1,21 @@ -import test from 'tape' +import test from 'tape' +import { EntropySign } from '../src/sign/main' -import { signWithAdapters } from '../src/flows/sign/sign' import { setupTest, charlieStashSeed } from './testing-utils' +const endpoint = 'ws://127.0.0.1:9944' -test('Sign - signWithAdapter', async (t) => { +test('Sign - signMessageWithAdapters', async (t) => { const { run, entropy } = await setupTest(t, { seed: charlieStashSeed }) + const SigningService = new EntropySign(entropy, endpoint) await run('register', entropy.register()) - - const signature = await run( + const result = await run( 'sign', - signWithAdapters(entropy, { msg: "heyo!" }) + SigningService.signMessageWithAdapters({ msg: "heyo!" }) ) - t.true(signature && signature.length > 32, 'signature has some body!') - signature && console.log(signature) + t.true(result?.signature?.length > 32, 'signature has some body!') + console.log(result) t.end() }) diff --git a/tests/transfer.test.ts b/tests/transfer.test.ts index 12c9736c..b744a684 100644 --- a/tests/transfer.test.ts +++ b/tests/transfer.test.ts @@ -11,8 +11,8 @@ import { } from './testing-utils' import { initializeEntropy } from '../src/common/initializeEntropy' -import * as BalanceUtils from '../src/balance/utils' -import * as TransferUtils from '../src/transfer/utils' +import { EntropyTransfer } from '../src/transfer/main' +import { EntropyBalance } from '../src/balance/main' import { charlieStashAddress, charlieStashSeed } from './testing-utils/constants' const networkType = 'two-nodes' @@ -25,59 +25,53 @@ test('Transfer', async (t) => { await run('network up', spinNetworkUp(networkType)) // this gets called after all tests are run t.teardown(async () => { - await entropy.close() + await naynayEntropy.close() await charlieEntropy.close() await spinNetworkDown(networkType).catch((error) => console.error('Error while spinning network down', error.message) ) }) - const naynaySeed = makeSeed() - const naynayKeyring = new Keyring({ seed: naynaySeed, debug: true }) const charlieKeyring = new Keyring({ seed: charlieStashSeed, debug: true }) - // Below expect errors are in place until we fix types export from sdk - // @ts-expect-error - const entropy = await initializeEntropy({ keyMaterial: naynayKeyring.getAccount(), endpoint, }) - // @ts-expect-error const charlieEntropy = await initializeEntropy({ keyMaterial: charlieKeyring.getAccount(), endpoint, }) - await run('entropy ready', entropy.ready) await run('charlie ready', charlieEntropy.ready) - - const recipientAddress = entropy.keyring.accounts.registration.address - // Check Balance of new account + const naynaySeed = makeSeed() + const naynayKeyring = new Keyring({ seed: naynaySeed, debug: true }) + const naynayEntropy = await initializeEntropy({ keyMaterial: naynayKeyring.getAccount(), endpoint, }) + await run('naynay ready', naynayEntropy.ready) + + const naynayAddress = naynayEntropy.keyring.accounts.registration.address + + // Check initial balances + const BalanceService = new EntropyBalance(naynayEntropy, endpoint) let naynayBalance = await run( 'getBalance (naynay)', - BalanceUtils.getBalance(entropy, recipientAddress) + BalanceService.getBalance(naynayAddress) ) - t.equal(naynayBalance, 0, 'naynay is broke') let charlieBalance = await run( 'getBalance (charlieStash)', - BalanceUtils.getBalance(entropy, charlieStashAddress) + BalanceService.getBalance(charlieStashAddress) ) - t.equal(charlieBalance, 1e17, 'charlie got bank') - const transferStatus = await run( + // Do transer + const TransferService = new EntropyTransfer(charlieEntropy, endpoint) + const inputAmount = "1.5" + await run( 'transfer', - TransferUtils.transfer(entropy, { - from: charlieEntropy.keyring.accounts.registration.pair, - to: recipientAddress, - amount: BigInt(1000 * 10e10) - }) + TransferService.transfer(naynayAddress, inputAmount) ) - // @ts-ignore - t.true(transferStatus?.isFinalized, 'Funds transferred successfully') - // Re-Check Balance of new account + // Re-Check balance naynayBalance = await run( 'getBalance (naynay)', - BalanceUtils.getBalance(entropy, recipientAddress) + BalanceService.getBalance(naynayAddress) ) + const expected = Number(inputAmount) * 1e10 + t.equal(naynayBalance, expected,'naynay is rolling in it!') - t.equal(naynayBalance, 1000 * 10e10, 'naynay is rolling in it!') - t.end() }) From 0cfb8f8f3e83ffac4ed93d8b6d36251ba59ef67f Mon Sep 17 00:00:00 2001 From: Nayyir Jutha Date: Wed, 18 Sep 2024 20:34:26 -0400 Subject: [PATCH 18/25] [NayNay] File Restructure: Accounts Restructure (#215) * [NayNay] File Restructure: Accounts Restructure - refactoring file structure + flow of accounts in cli/tui * updated new account, list account and selected account with new file structure; still need to update tests * finished updated manage accoutns stuff, updated tests too * updated register flow and moved methods to accounts namespace * updated register tests * cleanup from smoke test * updated changelog * start refactor * WIP: part way refactored account stuff * cleaning up the cleanup for accounts restructure; * forgot something * updated accounts restructure, continuing from mixs changes * Update main.ts * Update manage-accounts.test.ts * fixups * WIP * fixups * compleeeete * get working for fresh install. see WIP * fixed fresh install and using tui, might have fixed cli not sure * updated initialization of entropy and formt of how we instantiate commands * skipping faucet test for now, shenanigans occurring * fixed faucet test * updated tests; * Update command.ts Co-authored-by: mix irving * pr review updates * Update src/cli.ts * account-restructure tweaks (#226) --------- Co-authored-by: mixmix --- CHANGELOG.md | 5 + src/account/command.ts | 130 ++++++++++++++++++ src/account/constants.ts | 36 +++++ src/account/interaction.ts | 92 +++++++++++++ src/account/main.ts | 130 ++++++++++++++++++ src/account/types.ts | 22 +++ src/account/utils.ts | 80 +++++++++++ src/balance/command.ts | 11 +- src/balance/interaction.ts | 4 +- src/balance/main.ts | 2 +- src/cli.ts | 68 ++------- src/common/entropy-base.ts | 4 +- src/common/utils-cli.ts | 13 +- src/common/utils.ts | 6 +- src/config/encoding.ts | 2 +- src/config/index.ts | 24 ++-- src/config/types.ts | 9 +- src/flows/entropyFaucet/faucet.ts | 6 +- src/flows/entropyFaucet/index.ts | 6 +- src/flows/index.ts | 2 - 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 -- .../manage-accounts/utils/create-account.ts | 25 ---- .../manage-accounts/utils/import-account.ts | 40 ------ src/flows/programs/index.ts | 6 +- src/flows/register/index.ts | 41 ------ src/flows/register/register.ts | 28 ---- src/flows/register/types.ts | 5 - src/sign/command.ts | 9 +- src/sign/interaction.ts | 4 +- src/sign/main.ts | 2 +- src/transfer/command.ts | 14 +- src/transfer/interaction.ts | 4 +- src/transfer/main.ts | 2 +- src/tui.ts | 75 +++++----- src/types/index.ts | 5 +- tests/account.test.ts | 122 ++++++++++++++++ tests/balance.test.ts | 10 +- tests/faucet.test.ts | 30 ++-- tests/manage-accounts.test.ts | 79 ----------- tests/register.test.ts | 44 ------ tests/sign.test.ts | 4 +- tests/transfer.test.ts | 18 +-- 46 files changed, 774 insertions(+), 593 deletions(-) create mode 100644 src/account/command.ts create mode 100644 src/account/constants.ts create mode 100644 src/account/interaction.ts create mode 100644 src/account/main.ts create mode 100644 src/account/types.ts create mode 100644 src/account/utils.ts 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 delete mode 100644 src/flows/manage-accounts/utils/create-account.ts delete mode 100644 src/flows/manage-accounts/utils/import-account.ts delete mode 100644 src/flows/register/index.ts delete mode 100644 src/flows/register/register.ts delete mode 100644 src/flows/register/types.ts create mode 100644 tests/account.test.ts delete mode 100644 tests/manage-accounts.test.ts delete mode 100644 tests/register.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c5538e3..f982d02b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,9 @@ Version header format: `[version] Name - year-month-day (entropy-core compatibil - new: './src/transfer' - new file structure for our CLI/TUI flows - new: './src/transfer/command.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/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 @@ -43,6 +46,8 @@ Version header format: `[version] Name - year-month-day (entropy-core compatibil - 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 ### Broke diff --git a/src/account/command.ts b/src/account/command.ts new file mode 100644 index 00000000..4c9db8bf --- /dev/null +++ b/src/account/command.ts @@ -0,0 +1,130 @@ +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 { findAccountByAddressOrName } from "src/common/utils"; + +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('', '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('', '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 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(currentAccountAddressOption()) + // 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 { account, endpoint, /* password */ } = opts + const storedConfig = await config.get() + const { accounts } = storedConfig + const accountToRegister = findAccountByAddressOrName(accounts, account) + if (!accountToRegister) { + throw new Error('AccountError: Unable to register non-existent account') + } + + 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 config.set({ + ...storedConfig, + accounts, + selectedAccount: updatedAccount.address + }) + + const verifyingKeys = updatedAccount?.data?.registration?.verifyingKeys + const verifyingKey = verifyingKeys[verifyingKeys.length - 1] + cliWrite(verifyingKey) + process.exit(0) + }) +} diff --git a/src/account/constants.ts b/src/account/constants.ts new file mode 100644 index 00000000..4c815477 --- /dev/null +++ b/src/account/constants.ts @@ -0,0 +1,36 @@ +export const FLOW_CONTEXT = 'ENTROPY_ACCOUNTS' + +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' } + ] + } +} \ No newline at end of file diff --git a/src/account/interaction.ts b/src/account/interaction.ts new file mode 100644 index 00000000..46f6f24a --- /dev/null +++ b/src/account/interaction.ts @@ -0,0 +1,92 @@ +import inquirer from "inquirer"; +import Entropy from "@entropyxyz/sdk"; + +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, + newAccountQuestions, + selectAccountQuestions +} from "./utils" + +/* + * @returns partialConfigUpdate | "exit" | undefined + */ +export async function entropyAccount (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 + 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(selectAccountQuestions(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> { + const accountService = new EntropyAccount(entropy, endpoint) + + const { accounts, selectedAccount } = storedConfig + 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 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 new file mode 100644 index 00000000..0c5d46d2 --- /dev/null +++ b/src/account/main.ts @@ -0,0 +1,130 @@ +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 { + const seed = randomAsHex(32) + return EntropyAccount.import({ name, seed, path }) + } + + 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 + 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 { + let programModAddress: string + let programData: any + if (params) { + ({ programModAddress, programData } = params) + } + const registerParams = programModAddress && programData + ? { + programDeployer: programModAddress, + programData + } + : 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 + }) + } + + // WATCH: should this be extracted to interaction.ts? + async registerAccount (account: EntropyAccountConfig, registerParams?: AccountRegisterParams): Promise { + this.logger.debug( + [ + `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) + // 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) + 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) + } + }) + }) + } +} diff --git a/src/account/types.ts b/src/account/types.ts new file mode 100644 index 00000000..f1d1cf30 --- /dev/null +++ b/src/account/types.ts @@ -0,0 +1,22 @@ +export interface AccountCreateParams { + name: string + path?: string +} + +export interface AccountImportParams { + seed: string + name: string + path?: string +} + +export type AccountListResults = { + name: string + address: string + verifyingKeys: string[] +} + +export interface AccountRegisterParams { + programModAddress?: string + // TODO: Export ProgramInstance type from sdk + programData?: any +} diff --git a/src/account/utils.ts b/src/account/utils.ts new file mode 100644 index 00000000..183c06dc --- /dev/null +++ b/src/account/utils.ts @@ -0,0 +1,80 @@ +import { EntropyAccountConfig } from "../config/types"; +import * as config from "../config"; +import { ACCOUNTS_CONTENT } from './constants'; +import { generateAccountChoices } from '../common/utils'; + +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 + return ACCOUNTS_CONTENT.seed.invalidSeed +} + +export const importQuestions = [ + { + type: 'input', + name: ACCOUNTS_CONTENT.seed.name, + message: ACCOUNTS_CONTENT.seed.message, + validate: validateSeedInput, + when: ({ importKey }) => importKey + }, + { + type: 'input', + 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 + } +] diff --git a/src/balance/command.ts b/src/balance/command.ts index 37879e5f..7273b8d5 100644 --- a/src/balance/command.ts +++ b/src/balance/command.ts @@ -1,19 +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/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/balance/main.ts b/src/balance/main.ts index 2c865413..7f7345f4 100644 --- a/src/balance/main.ts +++ b/src/balance/main.ts @@ -6,7 +6,7 @@ import { BalanceInfo } from "./types" const FLOW_CONTEXT = 'ENTROPY-BALANCE' export class EntropyBalance extends EntropyBase { constructor (entropy: Entropy, endpoint: string) { - super(entropy, endpoint, FLOW_CONTEXT) + super({ entropy, endpoint, flowContext: FLOW_CONTEXT }) } async getBalance (address: string): Promise { diff --git a/src/cli.ts b/src/cli.ts index 2e2838e2..c307b9f9 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -2,41 +2,25 @@ /* NOTE: calling this file entropy.ts helps commander parse process.argv */ import { Command, Option } from 'commander' -import Entropy from '@entropyxyz/sdk' -import { cliListAccounts } from './flows/manage-accounts/cli' -import { currentAccountAddressOption, endpointOption, loadEntropy, cliWrite } from './common/utils-cli' + +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' const program = new 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) { - // 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 { - entropy = await loadEntropy(address, endpoint, password) - } - -} - /* 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', @@ -45,36 +29,14 @@ program .env('DEV_MODE') .hideHelp() ) - .hook('preAction', async (_thisCommand, actionCommand) => { - const { account, endpoint, password } = actionCommand.opts() - const address = actionCommand.name() === 'balance' - ? actionCommand.args[0] - : account - - await setEntropyGlobal(address, endpoint, password) - }) - .action((options: EntropyTuiOptions) => { + .addCommand(entropyBalanceCommand()) + .addCommand(entropyAccountCommand()) + .addCommand(entropyTransferCommand()) + .addCommand(entropySignCommand()) + .action(async (options: EntropyTuiOptions) => { + const { account, endpoint } = options + const entropy = await loadEntropy(account, endpoint) launchTui(entropy, 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() - cliWrite(accounts) - process.exit(0) - }) - -/* balance */ -entropyBalanceCommand(entropy, program) - -/* Transfer */ -entropyTransferCommand(entropy, program) - -/* Sign */ -entropySignCommand(entropy, program) - program.parseAsync().then(() => {}) diff --git a/src/common/entropy-base.ts b/src/common/entropy-base.ts index 1e14bf3b..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: 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 a5f423ad..eae015cb 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 { findAccountByAddressOrName, stringify } from './utils' import * as config from '../config' import Entropy from '@entropyxyz/sdk' import { initializeEntropy } from './initializeEntropy' @@ -42,6 +42,7 @@ export function passwordOption (description?: string) { 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' @@ -55,15 +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 = getSelectedAccount(storedConfig.accounts, address) - - if (!selectedAccount) throw new Error(`AddressError: No account with 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 d51e3dd2..419340ef 100644 --- a/src/common/utils.ts +++ b/src/common/utils.ts @@ -52,7 +52,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,11 +61,11 @@ 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[], aliasOrAddress: string) { +export function findAccountByAddressOrName (accounts: EntropyAccountConfig[], aliasOrAddress: string) { if (!aliasOrAddress || !aliasOrAddress.length) throw Error('aliasOrAddress required') return ( diff --git a/src/config/encoding.ts b/src/config/encoding.ts index a41da0cc..cb580655 100644 --- a/src/config/encoding.ts +++ b/src/config/encoding.ts @@ -9,7 +9,7 @@ export function deserialize (config) { return JSON.parse(config, reviver) } -function replacer (key, value) { +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 d5a7a145..2afa3287 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -1,11 +1,12 @@ 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' 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,7 +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) => { - if (err && err.code !== 'ENOENT') throw err + if (err.code !== 'ENOENT') throw err const oldConfig = await get(oldConfigPath).catch(noop) // drop errors if (oldConfig) { @@ -57,17 +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) { - const configBuffer = readFileSync(configPath, 'utf8') - return deserialize(configBuffer) +export function getSync (configPath = CONFIG_PATH): EntropyConfig { + try { + 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 + } } -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 7910c58d..7d4deb78 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -1,8 +1,11 @@ export interface EntropyConfig { accounts: EntropyAccountConfig[] - endpoints: { dev: string; 'test-net': string } - 'migration-version': string + endpoints: { + dev: string; + 'test-net': string + } selectedAccount: string + 'migration-version': string } export interface EntropyAccountConfig { @@ -34,4 +37,4 @@ export enum EntropyAccountContextType { registration = 'ADMIN_KEY', deviceKey = 'CONSUMER_KEY', undefined = 'MIXED_KEY', -} \ No newline at end of file +} 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 e76b7f14..9d5c4c95 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 { 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 = getSelectedAccount(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/index.ts b/src/flows/index.ts index 389e5483..fd1eb9c6 100644 --- a/src/flows/index.ts +++ b/src/flows/index.ts @@ -1,4 +1,2 @@ export { entropyFaucet } from './entropyFaucet' -export { entropyRegister } from './register' export { userPrograms, devPrograms } from './programs' -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 c8648131..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 './utils/import-account' -// import * as passwordFlow from '../password' -import { print } from '../../common/utils' -import { createAccount } from './utils/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/manage-accounts/utils/create-account.ts b/src/flows/manage-accounts/utils/create-account.ts deleted file mode 100644 index 0e9a66fa..00000000 --- a/src/flows/manage-accounts/utils/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/utils/import-account.ts b/src/flows/manage-accounts/utils/import-account.ts deleted file mode 100644 index 4000c60c..00000000 --- a/src/flows/manage-accounts/utils/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 - }, -] diff --git a/src/flows/programs/index.ts b/src/flows/programs/index.ts index 3e1deae4..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 { getSelectedAccount, 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 = getSelectedAccount(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 = getSelectedAccount(accounts, selectedAccountAddress) + const selectedAccount = findAccountByAddressOrName(accounts, selectedAccountAddress) const choices = { "Deploy": deployProgramTUI, diff --git a/src/flows/register/index.ts b/src/flows/register/index.ts deleted file mode 100644 index 05247578..00000000 --- a/src/flows/register/index.ts +++ /dev/null @@ -1,41 +0,0 @@ -// import inquirer from "inquirer" -import { getSelectedAccount, print, /*accountChoices*/ } 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 = accountChoices(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/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/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/sign/main.ts b/src/sign/main.ts index b47d6531..63ffa4df 100644 --- a/src/sign/main.ts +++ b/src/sign/main.ts @@ -7,7 +7,7 @@ import { stringToHex } from "./utils"; export class EntropySign extends EntropyBase { constructor (entropy: Entropy, endpoint: string) { - super(entropy, endpoint, FLOW_CONTEXT) + super({ entropy, endpoint, flowContext: FLOW_CONTEXT }) } // async rawSign (entropy: Entropy, payload: RawSignPayload) { diff --git a/src/transfer/command.ts b/src/transfer/command.ts index 34b5ac34..537f2149 100644 --- a/src/transfer/command.ts +++ b/src/transfer/command.ts @@ -1,10 +1,10 @@ -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 +12,11 @@ export async function entropyTransferCommand (entropy: Entropy, rootCommand: Com .addOption(endpointOption()) .addOption(currentAccountAddressOption()) .action(async (destination, amount, opts) => { - const TransferService = new EntropyTransfer(entropy, opts.endpoint) - await TransferService.transfer(destination, amount) + 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 } 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/transfer/main.ts b/src/transfer/main.ts index 45ea504b..650119e2 100644 --- a/src/transfer/main.ts +++ b/src/transfer/main.ts @@ -6,7 +6,7 @@ const FLOW_CONTEXT = 'ENTROPY_TRANSFER' export class EntropyTransfer extends EntropyBase { constructor (entropy: Entropy, endpoint: string) { - super(entropy, endpoint, FLOW_CONTEXT) + super({ entropy, endpoint, flowContext: FLOW_CONTEXT }) } // NOTE: a more accessible function which handles diff --git a/src/tui.ts b/src/tui.ts index 9dd100a0..db18c149 100644 --- a/src/tui.ts +++ b/src/tui.ts @@ -5,26 +5,22 @@ import * as flows from './flows' import { EntropyTuiOptions } from './types' import { logo } from './common/ascii' import { print } from './common/utils' -import { EntropyLogger } from './common/logger' import { loadEntropy } from './common/utils-cli' +import { EntropyLogger } from './common/logger' + +import { entropyAccount, entropyRegister } from './account/interaction' 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 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() } @@ -40,10 +36,10 @@ 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, + 'Register': () => {}, 'Sign': () => {}, 'Transfer': () => {}, // TODO: design programs in TUI (merge deploy+user programs) @@ -65,11 +61,11 @@ 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 !== storedConfig.selectedAccount) { + const currentAccount = entropy?.keyring?.accounts?.registration?.address + if (currentAccount && currentAccount !== storedConfig.selectedAccount) { await entropy.close() entropy = await loadEntropy(storedConfig.selectedAccount, options.endpoint); } @@ -94,44 +90,37 @@ async function main (entropy: Entropy, choices, options, logger: EntropyLogger) } else { logger.debug(answers) switch (answers.choice) { - case "Balance": { - try { - await entropyBalance(entropy, options.endpoint, storedConfig) - } catch (error) { - console.error('There was an error retrieving balance', error) - } - break; + case 'Manage Accounts': { + const response = await entropyAccount(options.endpoint, storedConfig) + if (response === 'exit') { returnToMain = true } + break + } + case 'Register': { + await entropyRegister(entropy, options.endpoint, storedConfig) + break + } + case 'Balance': { + 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) - } + case 'Transfer': { + 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) - } + case 'Sign': { + 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() - break; + 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/account.test.ts b/tests/account.test.ts new file mode 100644 index 00000000..9f0288f6 --- /dev/null +++ b/tests/account.test.ts @@ -0,0 +1,122 @@ +import test from 'tape' +import { wasmGlobalsReady } from '@entropyxyz/sdk' +// @ts-ignore +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 * as config from '../src/config' +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 = { + name: 'Test Config', + address: charlieStashAddress, + data: { + seed: charlieStashSeed, + admin: { + verifyingKeys: ['this-is-a-verifying-key'], + seed: charlieStashSeed, + address: charlieStashAddress, + path: '//Charlie' + } + } + } + const config: EntropyConfig = { + accounts: [account], + endpoints: { + dev: 'ws://127.0.0.1:9944', + 'test-net': 'wss://testnet.entropy.xyz', + }, + selectedAccount: account.address, + 'migration-version': '0' + } + + const accountsArray = EntropyAccount.list(config) + + t.deepEqual(accountsArray, [{ + name: account.name, + address: account.address, + verifyingKeys: account?.data?.admin?.verifyingKeys + }]) + + // Resetting accounts on config to test for empty list + config.accounts = [] + try { + 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') + } + + t.end() +}) + +let counter = 0 + +test('Account - import', 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 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() + + 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.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/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 517d0b72..08c37eb9 100644 --- a/tests/faucet.test.ts +++ b/tests/faucet.test.ts @@ -1,20 +1,20 @@ 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 => { const { run, entropy, endpoint } = await setupTest(t, { seed: charlieStashSeed }) const { entropy: naynayEntropy } = await setupTest(t) - 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,12 +40,13 @@ test('Faucet Tests', async t => { // Confirm faucetPointer matches deployed program pointer t.equal(faucetProgramPointer, LOCAL_PROGRAM_HASH, 'Program pointer matches') - - let naynayBalance = await BalanceService.getBalance(naynayEntropy.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) 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, "1000")) const transferStatus = await sendMoney( naynayEntropy, @@ -71,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/manage-accounts.test.ts b/tests/manage-accounts.test.ts deleted file mode 100644 index 01a4cdc1..00000000 --- a/tests/manage-accounts.test.ts +++ /dev/null @@ -1,79 +0,0 @@ -import test from 'tape' -import { wasmGlobalsReady } from '@entropyxyz/sdk' -// @ts-ignore -import { isValidSubstrateAddress } from '@entropyxyz/sdk/utils' -// @ts-ignore -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/utils/create-account' -import * as config from '../src/config' -import { promiseRunner, sleep } from './testing-utils' -import { charlieStashAddress, charlieStashSeed } from './testing-utils/constants' - -test('List Accounts', async t => { - const account: EntropyAccountConfig = { - name: 'Test Config', - address: charlieStashAddress, - data: { - seed: charlieStashSeed, - admin: { - verifyingKeys: ['this-is-a-verifying-key'], - seed: charlieStashSeed, - address: charlieStashAddress, - path: '//Charlie' - } - } - } - const config: EntropyConfig = { - accounts: [account], - endpoints: { - dev: 'ws://127.0.0.1:9944', - 'test-net': 'wss://testnet.entropy.xyz', - }, - 'migration-version': '0' - } - - const accountsArray = listAccounts(config) - - t.deepEqual(accountsArray, [{ - name: account.name, - address: account.address, - verifyingKeys: account?.data?.admin?.verifyingKeys - }]) - - // Resetting accounts on config to test for empty list - config.accounts = [] - try { - listAccounts(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.end() -}) - -const networkType = 'two-nodes' - -let counter = 0 -test('Create Account', 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 testAccountName = 'Test Account' - const newAccount = await createAccount({ name: testAccountName, seed: testAccountSeed }) - - const testKeyring = new Keyring({ seed: testAccountSeed, path: 'none', debug: true }) - const { admin } = testKeyring.getAccount() - - 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.end() -}) diff --git a/tests/register.test.ts b/tests/register.test.ts deleted file mode 100644 index f093df6f..00000000 --- a/tests/register.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -import test from 'tape' - -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) => { - const { run, entropy } = await setupTest(t, { networkType, seed: charlieStashSeed }) - - const verifyingKey = await run('register account', register(entropy)) - - 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 verifyingKey = await run( - 'register - using custom params', - register(entropy, { - 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/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 b744a684..4dda0800 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 { @@ -14,6 +13,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' @@ -33,42 +33,42 @@ 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 // 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!') From 96ec283ef02d8984221a2454b697327d92ee5aaa Mon Sep 17 00:00:00 2001 From: mix irving Date: Mon, 23 Sep 2024 10:46:03 +1200 Subject: [PATCH 19/25] get all config.get* methods fail safely (#227) * get all config.get* methods fail safely * fix getSync so that it mkdirps --- src/config/index.ts | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/src/config/index.ts b/src/config/index.ts index 2afa3287..73c91e27 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -54,12 +54,11 @@ export async function init (configPath = CONFIG_PATH, oldConfigPath = OLD_CONFIG await set(newConfig, configPath) } } -function noop () {} export async function get (configPath = CONFIG_PATH) { - const configBuffer = await readFile(configPath) - - return deserialize(configBuffer.toString()) + return readFile(configPath, 'utf-8') + .then(deserialize) + .catch(makeGetErrorHandler(configPath)) } export function getSync (configPath = CONFIG_PATH): EntropyConfig { @@ -67,11 +66,7 @@ export function getSync (configPath = CONFIG_PATH): EntropyConfig { 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 makeGetErrorHandler(configPath)(err) } } @@ -79,3 +74,17 @@ export async function set (config: EntropyConfig, configPath = CONFIG_PATH) { await mkdirp(dirname(configPath)) await writeFile(configPath, serialize(config)) } + +/* util */ +function noop () {} + +function makeGetErrorHandler (configPath) { + return function getErrorHandler (err) { + if (err.code !== 'ENOENT') throw err + + const newConfig = migrateData(allMigrations, {}) + mkdirp.sync(dirname(configPath)) + writeFileSync(configPath, serialize(newConfig)) + return newConfig + } +} From e9bd7867664e89a6a8c7743626abc6bd02999f2c Mon Sep 17 00:00:00 2001 From: mix irving Date: Tue, 24 Sep 2024 16:33:45 +1200 Subject: [PATCH 20/25] fix config (again) (#229) * fix config (again), rename > accountOption * Update src/cli.ts --- src/account/command.ts | 4 ++-- src/cli.ts | 23 ++++++++++++------- src/common/utils-cli.ts | 49 +++++++++++++++++++++++++++-------------- src/config/index.ts | 38 +++++++++++++++----------------- src/sign/command.ts | 4 ++-- src/transfer/command.ts | 4 ++-- tests/config.test.ts | 6 +++-- 7 files changed, 75 insertions(+), 53 deletions(-) diff --git a/src/account/command.ts b/src/account/command.ts index 4c9db8bf..06cf3a58 100644 --- a/src/account/command.ts +++ b/src/account/command.ts @@ -4,7 +4,7 @@ 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 { cliWrite, accountOption, endpointOption, loadEntropy, passwordOption } from "../common/utils-cli"; import { findAccountByAddressOrName } from "src/common/utils"; export function entropyAccountCommand () { @@ -87,7 +87,7 @@ function entropyAccountRegister () { .description('Register an entropy account with a program') .addOption(passwordOption()) .addOption(endpointOption()) - .addOption(currentAccountAddressOption()) + .addOption(accountOption()) // Removing these options for now until we update the design to accept program configs // .addOption( // new Option( diff --git a/src/cli.ts b/src/cli.ts index 0a169888..84d36b31 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -4,7 +4,8 @@ import { Command, Option } from 'commander' import { EntropyTuiOptions } from './types' -import { currentAccountAddressOption, endpointOption, loadEntropy } from './common/utils-cli' +import { accountOption, endpointOption, loadEntropy } from './common/utils-cli' +import * as config from './config' import launchTui from './tui' import { entropyAccountCommand } from './account/command' @@ -18,9 +19,8 @@ const program = new Command() program .name('entropy') .description('CLI interface for interacting with entropy.xyz. Running this binary without any commands or arguments starts a text-based interface.') - .addOption(currentAccountAddressOption()) + .addOption(accountOption()) .addOption(endpointOption()) - // NOTE: I think this is currently unused .addOption( new Option( '-d, --dev', @@ -33,10 +33,17 @@ program .addCommand(entropyAccountCommand()) .addCommand(entropyTransferCommand()) .addCommand(entropySignCommand()) - .action(async (options: EntropyTuiOptions) => { - const { account, endpoint } = options - const entropy = await loadEntropy(account, endpoint) - launchTui(entropy, options) + .action(async (opts: EntropyTuiOptions) => { + const { account, endpoint } = opts + const entropy = account + ? await loadEntropy(account, endpoint) + : undefined + // NOTE: on initial startup you have no account + launchTui(entropy, opts) + }) + .hook('preAction', async () => { + // set up config file, run migrations + return config.init() }) -program.parseAsync().then(() => {}) +program.parseAsync() diff --git a/src/common/utils-cli.ts b/src/common/utils-cli.ts index eae015cb..3aec350b 100644 --- a/src/common/utils-cli.ts +++ b/src/common/utils-cli.ts @@ -1,7 +1,7 @@ +import Entropy from '@entropyxyz/sdk' import { Option } from 'commander' import { findAccountByAddressOrName, stringify } from './utils' import * as config from '../config' -import Entropy from '@entropyxyz/sdk' import { initializeEntropy } from './initializeEntropy' export function cliWrite (result) { @@ -9,6 +9,15 @@ export function cliWrite (result) { process.stdout.write(prettyResult) } +function getConfigOrNull () { + try { + return config.getSync() + } catch (err) { + if (config.isDangerousReadError(err)) throw err + return null + } +} + export function endpointOption () { return new Option( '-e, --endpoint ', @@ -17,14 +26,14 @@ export function endpointOption () { 'Can also be given a stored endpoint name from config eg: `entropy --endpoint test-net`.' ].join(' ') ) - .env('ENDPOINT') + .env('ENTROPY_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] + const storedConfig = getConfigOrNull() + const endpoint = storedConfig?.endpoints?.[aliasOrEndpoint] if (!endpoint) throw Error('unknown endpoint alias: ' + aliasOrEndpoint) return endpoint @@ -40,31 +49,37 @@ export function passwordOption (description?: string) { ) } -export function currentAccountAddressOption () { - const storedConfig = config.getSync() +export function accountOption () { + const storedConfig = getConfigOrNull() return new Option( - '-a, --account
', - 'Sets the current account for the session or defaults to the account stored in the config' + '-a, --account ', + [ + 'Sets the account for the session.', + 'Defaults to the last set account (or the first account if one has not been set before).' + ].join(' ') ) - .env('ACCOUNT_ADDRESS') + .env('ENTROPY_ACCOUNT') .argParser(async (account) => { - if (account === storedConfig.selectedAccount) return account - // Updated selected account in config with new address from this option - const newConfigUpdates = { selectedAccount: account } - await config.set({ ...storedConfig, ...newConfigUpdates }) + if (storedConfig && storedConfig.selectedAccount !== account) { + // Updated selected account in config with new address from this option + await config.set({ + ...storedConfig, + selectedAccount: account + }) + } return account }) - .default(storedConfig.selectedAccount) + .default(storedConfig?.selectedAccount) // TODO: display the *name* not address // TODO: standardise whether selectedAccount is name or address. } export async function loadEntropy (addressOrName: string, endpoint: string, password?: string): Promise { - const storedConfig = config.getSync() - const selectedAccount = findAccountByAddressOrName(storedConfig.accounts, addressOrName) - if (!selectedAccount) throw new Error(`AddressError: No account with name or address "${addressOrName}"`) + const accounts = getConfigOrNull()?.accounts || [] + const selectedAccount = findAccountByAddressOrName(accounts, addressOrName) + if (!selectedAccount) throw new Error(`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/config/index.ts b/src/config/index.ts index 73c91e27..049b6775 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, writeFileSync } from 'node:fs' +import { readFileSync } from 'node:fs' import { mkdirp } from 'mkdirp' import { join, dirname } from 'path' import envPaths from 'env-paths' @@ -35,9 +35,10 @@ 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) => { - if (err.code !== 'ENOENT') throw err + .catch(async (err ) => { + if (isDangerousReadError(err)) throw err + // If there is no current config, try loading the old one const oldConfig = await get(oldConfigPath).catch(noop) // drop errors if (oldConfig) { // move the config @@ -58,33 +59,30 @@ export async function init (configPath = CONFIG_PATH, oldConfigPath = OLD_CONFIG export async function get (configPath = CONFIG_PATH) { return readFile(configPath, 'utf-8') .then(deserialize) - .catch(makeGetErrorHandler(configPath)) } -export function getSync (configPath = CONFIG_PATH): EntropyConfig { - try { - const configBuffer = readFileSync(configPath, 'utf8') - return deserialize(configBuffer) - } catch (err) { - return makeGetErrorHandler(configPath)(err) - } +export function getSync (configPath = CONFIG_PATH) { + const configStr = readFileSync(configPath, 'utf8') + return deserialize(configStr) } export async function set (config: EntropyConfig, configPath = CONFIG_PATH) { + assertConfigPath(configPath) + await mkdirp(dirname(configPath)) await writeFile(configPath, serialize(config)) } /* util */ function noop () {} - -function makeGetErrorHandler (configPath) { - return function getErrorHandler (err) { - if (err.code !== 'ENOENT') throw err - - const newConfig = migrateData(allMigrations, {}) - mkdirp.sync(dirname(configPath)) - writeFileSync(configPath, serialize(newConfig)) - return newConfig +function assertConfigPath (configPath) { + if (!configPath.endsWith('.json')) { + throw Error(`configPath must be of form *.json, got ${configPath}`) } } +export function isDangerousReadError (err) { + // file not found: + if (err.code === 'ENOENT') return false + + return true +} diff --git a/src/sign/command.ts b/src/sign/command.ts index db116d74..fc574228 100644 --- a/src/sign/command.ts +++ b/src/sign/command.ts @@ -1,5 +1,5 @@ import { Command, /* Option */ } from 'commander' -import { cliWrite, currentAccountAddressOption, endpointOption, loadEntropy, passwordOption } from '../common/utils-cli' +import { cliWrite, accountOption, endpointOption, loadEntropy, passwordOption } from '../common/utils-cli' import { EntropySign } from './main' export function entropySignCommand () { @@ -8,7 +8,7 @@ export function entropySignCommand () { .argument('msg', 'Message you would like to sign (string)') .addOption(passwordOption('Password for the source account (if required)')) .addOption(endpointOption()) - .addOption(currentAccountAddressOption()) + .addOption(accountOption()) // .addOption( // new Option( // '-r, --raw', diff --git a/src/transfer/command.ts b/src/transfer/command.ts index 537f2149..b814e2f9 100644 --- a/src/transfer/command.ts +++ b/src/transfer/command.ts @@ -1,5 +1,5 @@ import { Command } from "commander" -import { currentAccountAddressOption, endpointOption, loadEntropy, passwordOption } from "src/common/utils-cli" +import { accountOption, endpointOption, loadEntropy, passwordOption } from "src/common/utils-cli" import { EntropyTransfer } from "./main" export function entropyTransferCommand () { @@ -10,7 +10,7 @@ export function entropyTransferCommand () { .argument('amount', 'Amount of funds to be moved') .addOption(passwordOption('Password for the source account (if required)')) .addOption(endpointOption()) - .addOption(currentAccountAddressOption()) + .addOption(accountOption()) .action(async (destination, amount, opts) => { const entropy = await loadEntropy(opts.account, opts.endpoint) const transferService = new EntropyTransfer(entropy, opts.endpoint) diff --git a/tests/config.test.ts b/tests/config.test.ts index 8f28396b..5e2d0215 100644 --- a/tests/config.test.ts +++ b/tests/config.test.ts @@ -81,10 +81,11 @@ test('config - get', async t => { const result = await get(configPath) t.deepEqual(result, config, 'get works') + const MSG = 'path that does not exist fails' await get('/tmp/junk') - .then(() => t.fail('bad path should fail')) + .then(() => t.fail(MSG)) .catch(err => { - t.match(err.message, /no such file/, 'bad path should fail') + t.match(err.message, /ENOENT/, MSG) }) }) @@ -95,6 +96,7 @@ test('config - set', async t => { dog: true, secretKey: makeKey() } + // @ts-expect-error : this is a breaking test await set(config, configPath) const actual = await get(configPath) From 34e4c698b2016fdbf7382cb5d54929626859fef2 Mon Sep 17 00:00:00 2001 From: mix irving Date: Tue, 24 Sep 2024 16:34:40 +1200 Subject: [PATCH 21/25] add deploy to CLI, including start of CLI refactor (#213) * add deploy to CLI, including start of CLI refactor * refactor * tweaks * some renames * migrate program => new structure * fix bugs: no initial account, missing ProgramInfo interface * update config to be more safe * fix config (again), rename > accountOption --- src/account/constants.ts | 4 +- src/account/interaction.ts | 12 +- src/account/utils.ts | 10 +- src/balance/command.ts | 1 - src/cli.ts | 2 + src/common/utils.ts | 2 +- src/flows/entropyFaucet/faucet.ts | 8 +- src/flows/index.ts | 1 - src/flows/programs/add.ts | 12 -- src/flows/programs/deploy.ts | 49 -------- src/flows/programs/helpers/questions.ts | 40 ------- src/flows/programs/helpers/utils.ts | 30 ----- src/flows/programs/remove.ts | 9 -- src/flows/programs/view.ts | 6 - src/program/command.ts | 62 ++++++++++ src/program/constants.ts | 1 + .../index.ts => program/interaction.ts} | 65 ++++------- src/program/main.ts | 67 +++++++++++ src/{flows/programs => program}/types.ts | 23 ++-- src/program/utils.ts | 107 ++++++++++++++++++ src/tui.ts | 16 ++- tests/programs.test.ts | 34 +++--- 22 files changed, 324 insertions(+), 237 deletions(-) delete mode 100644 src/flows/programs/add.ts delete mode 100644 src/flows/programs/deploy.ts delete mode 100644 src/flows/programs/helpers/questions.ts delete mode 100644 src/flows/programs/helpers/utils.ts delete mode 100644 src/flows/programs/remove.ts delete mode 100644 src/flows/programs/view.ts create mode 100644 src/program/command.ts create mode 100644 src/program/constants.ts rename src/{flows/programs/index.ts => program/interaction.ts} (65%) create mode 100644 src/program/main.ts rename src/{flows/programs => program}/types.ts (61%) create mode 100644 src/program/utils.ts diff --git a/src/account/constants.ts b/src/account/constants.ts index 4c815477..432c58db 100644 --- a/src/account/constants.ts +++ b/src/account/constants.ts @@ -1,4 +1,4 @@ -export const FLOW_CONTEXT = 'ENTROPY_ACCOUNTS' +export const FLOW_CONTEXT = 'ENTROPY_ACCOUNT' export const ACCOUNTS_CONTENT = { seed: { @@ -33,4 +33,4 @@ export const ACCOUNTS_CONTENT = { { name: 'Exit to Main Menu', value: 'exit' } ] } -} \ No newline at end of file +} diff --git a/src/account/interaction.ts b/src/account/interaction.ts index 46f6f24a..d3277c0e 100644 --- a/src/account/interaction.ts +++ b/src/account/interaction.ts @@ -8,9 +8,9 @@ import { EntropyConfig } from "../config/types"; import * as config from "../config"; import { - manageAccountsQuestions, - newAccountQuestions, - selectAccountQuestions + accountManageQuestions, + accountNewQuestions, + accountSelectQuestions } from "./utils" /* @@ -18,12 +18,12 @@ import { */ export async function entropyAccount (endpoint: string, storedConfig: EntropyConfig) { const { accounts } = storedConfig - const { interactionChoice } = await inquirer.prompt(manageAccountsQuestions) + const { interactionChoice } = await inquirer.prompt(accountManageQuestions) switch (interactionChoice) { case 'create-import': { - const answers = await inquirer.prompt(newAccountQuestions) + const answers = await inquirer.prompt(accountNewQuestions) const { name, path, importKey } = answers let { seed } = answers if (importKey && seed.includes('#debug')) { @@ -44,7 +44,7 @@ export async function entropyAccount (endpoint: string, storedConfig: EntropyCon 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)) + const { selectedAccount } = await inquirer.prompt(accountSelectQuestions(accounts)) await config.set({ ...storedConfig, selectedAccount: selectedAccount.address diff --git a/src/account/utils.ts b/src/account/utils.ts index 183c06dc..86a595f1 100644 --- a/src/account/utils.ts +++ b/src/account/utils.ts @@ -31,7 +31,7 @@ function validateSeedInput (seed) { return ACCOUNTS_CONTENT.seed.invalidSeed } -export const importQuestions = [ +export const accountImportQuestions = [ { type: 'input', name: ACCOUNTS_CONTENT.seed.name, @@ -48,14 +48,14 @@ export const importQuestions = [ }, ] -export const newAccountQuestions = [ +export const accountNewQuestions = [ { type: 'confirm', name: ACCOUNTS_CONTENT.importKey.name, message: ACCOUNTS_CONTENT.importKey.message, default: ACCOUNTS_CONTENT.importKey.default, }, - ...importQuestions, + ...accountImportQuestions, { type: 'input', name: ACCOUNTS_CONTENT.name.name, @@ -63,14 +63,14 @@ export const newAccountQuestions = [ }, ] -export const selectAccountQuestions = (accounts: EntropyAccountConfig[]) => [{ +export const accountSelectQuestions = (accounts: EntropyAccountConfig[]) => [{ type: 'list', name: ACCOUNTS_CONTENT.selectAccount.name, message: ACCOUNTS_CONTENT.selectAccount.message, choices: generateAccountChoices(accounts) }] -export const manageAccountsQuestions = [ +export const accountManageQuestions = [ { type: 'list', name: ACCOUNTS_CONTENT.interactionChoice.name, diff --git a/src/balance/command.ts b/src/balance/command.ts index 7273b8d5..2ff7daa5 100644 --- a/src/balance/command.ts +++ b/src/balance/command.ts @@ -1,4 +1,3 @@ -import Entropy from "@entropyxyz/sdk"; import { Command } from "commander"; import { cliWrite, endpointOption, loadEntropy, passwordOption } from "src/common/utils-cli"; import { EntropyBalance } from "./main"; diff --git a/src/cli.ts b/src/cli.ts index 84d36b31..b531efa8 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -12,6 +12,7 @@ import { entropyAccountCommand } from './account/command' import { entropyTransferCommand } from './transfer/command' import { entropySignCommand } from './sign/command' import { entropyBalanceCommand } from './balance/command' +import { entropyProgramCommand } from './program/command' const program = new Command() @@ -33,6 +34,7 @@ program .addCommand(entropyAccountCommand()) .addCommand(entropyTransferCommand()) .addCommand(entropySignCommand()) + .addCommand(entropyProgramCommand()) .action(async (opts: EntropyTuiOptions) => { const { account, endpoint } = opts const entropy = account diff --git a/src/common/utils.ts b/src/common/utils.ts index 419340ef..b4dbc7ea 100644 --- a/src/common/utils.ts +++ b/src/common/utils.ts @@ -66,7 +66,7 @@ export function accountChoicesWithOther (accounts: EntropyAccountConfig[]) { } export function findAccountByAddressOrName (accounts: EntropyAccountConfig[], aliasOrAddress: string) { - if (!aliasOrAddress || !aliasOrAddress.length) throw Error('aliasOrAddress required') + if (!aliasOrAddress || !aliasOrAddress.length) throw Error('account name or address required') return ( accounts.find(account => account.address === aliasOrAddress) || diff --git a/src/flows/entropyFaucet/faucet.ts b/src/flows/entropyFaucet/faucet.ts index 5e0c4e8c..3e0d0c17 100644 --- a/src/flows/entropyFaucet/faucet.ts +++ b/src/flows/entropyFaucet/faucet.ts @@ -2,7 +2,7 @@ import Entropy from "@entropyxyz/sdk"; import { blake2AsHex, encodeAddress } from "@polkadot/util-crypto"; -import { viewPrograms } from "../programs/view"; +import { EntropyProgram } from '../../program/main' import FaucetSigner from "./signer"; import { FAUCET_PROGRAM_MOD_KEY, TESTNET_PROGRAM_HASH } from "./constants"; import { EntropyBalance } from "src/balance/main"; @@ -67,7 +67,7 @@ export async function sendMoney ( faucetAddress, chosenVerifyingKey, faucetProgramPointer = TESTNET_PROGRAM_HASH - }: { + }: { amount: string, addressToSendTo: string, faucetAddress: string, @@ -76,11 +76,13 @@ export async function sendMoney ( } ): Promise { const balanceService = new EntropyBalance(entropy, endpoint) + const programService = new EntropyProgram(entropy, endpoint) + // check balance of faucet address 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 }) + const programs = await programService.list({ verifyingKey: chosenVerifyingKey }) if (programs.length) { if (programs.length > 1) throw new Error('ProgramsError: Faucet Account has too many programs attached, expected less') if (programs.length === 1 && programs[0].program_pointer !== faucetProgramPointer) { diff --git a/src/flows/index.ts b/src/flows/index.ts index fd1eb9c6..1c2277d1 100644 --- a/src/flows/index.ts +++ b/src/flows/index.ts @@ -1,2 +1 @@ export { entropyFaucet } from './entropyFaucet' -export { userPrograms, devPrograms } from './programs' diff --git a/src/flows/programs/add.ts b/src/flows/programs/add.ts deleted file mode 100644 index c7780f13..00000000 --- a/src/flows/programs/add.ts +++ /dev/null @@ -1,12 +0,0 @@ -import Entropy from "@entropyxyz/sdk"; -import { AddProgramParams } from "./types"; - -export async function addProgram (entropy: Entropy, { programPointer, programConfig, verifyingKey }: AddProgramParams): Promise { - return entropy.programs.add( - { - program_pointer: programPointer, - program_config: programConfig, - }, - verifyingKey - ) -} \ No newline at end of file diff --git a/src/flows/programs/deploy.ts b/src/flows/programs/deploy.ts deleted file mode 100644 index e71b8ff2..00000000 --- a/src/flows/programs/deploy.ts +++ /dev/null @@ -1,49 +0,0 @@ -import Entropy from "@entropyxyz/sdk"; -import fs from "node:fs/promises" -import { isAbsolute, join } from "node:path" -import { u8aToHex } from "@polkadot/util" - -import { DeployProgramParams } from "./types" - -export async function deployProgram (entropy: Entropy, params: DeployProgramParams) { - const bytecode = await loadFile(params.bytecodePath) - const configurationSchema = await loadFile(params.configurationSchemaPath, 'json') - const auxillaryDataSchema = await loadFile(params.auxillaryDataSchemaPath, 'json') - // QUESTION: where / how are schema validated? - - return entropy.programs.dev.deploy( - bytecode, - jsonToHex(configurationSchema), - jsonToHex(auxillaryDataSchema) - ) -} - -function loadFile (path?: string, encoding?: string) { - if (path === undefined) return - - const absolutePath = isAbsolute(path) - ? path - : join(process.cwd(), path) - - switch (encoding) { - case undefined: - return fs.readFile(absolutePath) - - case 'json': - return fs.readFile(absolutePath, 'utf-8') - .then(string => JSON.parse(string)) - - default: - throw Error('unknown encoding: ' + encoding) - // return fs.readFile(absolutePath, encoding) - } -} - -function jsonToHex (obj?: object) { - if (obj === undefined) return - - const encoder = new TextEncoder() - const byteArray = encoder.encode(JSON.stringify(obj)) - - return u8aToHex(new Uint8Array(byteArray)) -} diff --git a/src/flows/programs/helpers/questions.ts b/src/flows/programs/helpers/questions.ts deleted file mode 100644 index 3a0a00d2..00000000 --- a/src/flows/programs/helpers/questions.ts +++ /dev/null @@ -1,40 +0,0 @@ -import Entropy from "@entropyxyz/sdk"; - -export const addQuestions = [ - { - type: "input", - name: "programPointerToAdd", - message: "Enter the program pointer you wish to add:", - validate: (input) => (input ? true : "Program pointer is required!"), - }, - { - type: "editor", - name: "programConfigJson", - message: - "Enter the program configuration as a JSON string (this will open your default editor):", - validate: (input) => { - try { - JSON.parse(input) - return true - } catch (e) { - return "Please enter a valid JSON string for the configuration." - } - }, - }, -] - -export const getProgramPointerInput = [ - { - type: "input", - name: "programPointer", - message: "Enter the program pointer you wish to remove:", - }, -] - -export const verifyingKeyQuestion = (entropy: Entropy) => [{ - type: 'list', - name: 'verifyingKey', - message: 'Select the key to proceeed', - choices: entropy.keyring.accounts.registration.verifyingKeys, - default: entropy.keyring.accounts.registration.verifyingKeys[0] -}] diff --git a/src/flows/programs/helpers/utils.ts b/src/flows/programs/helpers/utils.ts deleted file mode 100644 index df10be51..00000000 --- a/src/flows/programs/helpers/utils.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { print } from "src/common/utils" - -export function displayPrograms (programs): void { - programs.forEach((program, index) => { - print(`${index + 1}.`) - print({ - pointer: program.program_pointer, - config: parseProgramConfig(program.program_config) - }) - print('') - }) -} - -function parseProgramConfig (rawConfig: unknown) { - if (typeof rawConfig !== 'string') return rawConfig - if (!rawConfig.startsWith('0x')) return rawConfig - - const hex = rawConfig.slice(2) - const utf8 = Buffer.from(hex, 'hex').toString() - const output = JSON.parse(utf8) - Object.keys(output).forEach(key => { - output[key] = output[key].map(base64toHex) - }) - - return output -} - -function base64toHex (base64: string): string { - return Buffer.from(base64, 'base64').toString('hex') -} diff --git a/src/flows/programs/remove.ts b/src/flows/programs/remove.ts deleted file mode 100644 index 8c094d61..00000000 --- a/src/flows/programs/remove.ts +++ /dev/null @@ -1,9 +0,0 @@ -import Entropy from "@entropyxyz/sdk"; -import { RemoveProgramParams } from "./types"; - -export async function removeProgram (entropy: Entropy, { programPointer, verifyingKey }: RemoveProgramParams): Promise { - return entropy.programs.remove( - programPointer, - verifyingKey - ) -} \ No newline at end of file diff --git a/src/flows/programs/view.ts b/src/flows/programs/view.ts deleted file mode 100644 index 6cff1e9f..00000000 --- a/src/flows/programs/view.ts +++ /dev/null @@ -1,6 +0,0 @@ -import Entropy from "@entropyxyz/sdk"; -import { ViewProgramsParams } from "./types"; - -export async function viewPrograms (entropy: Entropy, { verifyingKey }: ViewProgramsParams): Promise { - return entropy.programs.get(verifyingKey) -} \ No newline at end of file diff --git a/src/program/command.ts b/src/program/command.ts new file mode 100644 index 00000000..0f02ffb5 --- /dev/null +++ b/src/program/command.ts @@ -0,0 +1,62 @@ +import { Command } from 'commander' + +import { EntropyProgram } from './main' +import { accountOption, endpointOption, cliWrite, loadEntropy } from '../common/utils-cli' + +export function entropyProgramCommand () { + return new Command('program') + .description('Commands for working with programs deployed to the Entropy Network') + .addCommand(entropyProgramDeploy()) + // TODO: + // .addCommand(entropyProgramGet()) + // .addCommand(entropyProgramListDeployed()) + // .addCommand(entropyProgramAdd()) + // .addCommand(entropyProgramRemove()) + // .addCommand(entropyProgramList()) +} + +function entropyProgramDeploy () { + return new Command('deploy') + .description([ + 'Deploys a program to the Entropy network, returning a program pointer.', + 'Requires funds.' + ].join(' ')) + .argument( + 'bytecode', + [ + 'The path to your program bytecode.', + 'Must be a .wasm file.' + ].join(' ') + ) + .argument( + 'configurationSchema', + [ + 'The path to the JSON Schema for validating configurations passed in by users installing this program.', + 'Must be a .json file.' + ].join(' ') + ) + .argument( + 'auxillaryDataSchema', + [ + 'The path to the JSON Schema for validating auxillary data passed to the program on calls to "sign".', + 'Must be a .json file.' + ].join(' ') + ) + .addOption(accountOption()) + .addOption(endpointOption()) + + .action(async (bytecodePath, configurationSchemaPath, auxillaryDataSchemaPath, opts) => { // eslint-disable-line + const entropy = await loadEntropy(opts.account, opts.endpoint) + + const program = new EntropyProgram(entropy, opts.endpoint) + + const pointer = await program.deploy({ + bytecodePath, + configurationSchemaPath, + auxillaryDataSchemaPath + }) + cliWrite(pointer) + + process.exit(0) + }) +} diff --git a/src/program/constants.ts b/src/program/constants.ts new file mode 100644 index 00000000..6b34c72e --- /dev/null +++ b/src/program/constants.ts @@ -0,0 +1 @@ +export const FLOW_CONTEXT = 'ENTROPY_PROGRAM' diff --git a/src/flows/programs/index.ts b/src/program/interaction.ts similarity index 65% rename from src/flows/programs/index.ts rename to src/program/interaction.ts index d168ecd1..ccb3fdc3 100644 --- a/src/flows/programs/index.ts +++ b/src/program/interaction.ts @@ -2,24 +2,13 @@ import Entropy from "@entropyxyz/sdk" import inquirer from "inquirer" import { u8aToHex } from "@polkadot/util" -import { deployProgram } from "./deploy"; -import { addProgram } from "./add"; -import { viewPrograms } from "./view"; -import { removeProgram } from "./remove"; -import { addQuestions, getProgramPointerInput, verifyingKeyQuestion } from "./helpers/questions"; -import { displayPrograms } from "./helpers/utils"; -import { initializeEntropy } from "../../common/initializeEntropy" -import { findAccountByAddressOrName, print } from "../../common/utils" -import { EntropyLogger } from "../../common/logger"; -import { EntropyTuiOptions } from "../../types" +import { displayPrograms, addQuestions, getProgramPointerInput, verifyingKeyQuestion } from "./utils"; +import { EntropyProgram } from "./main"; +import { print } from "../common/utils" let verifyingKey: string; -export async function userPrograms ({ accounts, selectedAccount: selectedAccountAddress }, options: EntropyTuiOptions, logger: EntropyLogger) { - const FLOW_CONTEXT = 'PROGRAMS' - const { endpoint } = options - const selectedAccount = findAccountByAddressOrName(accounts, selectedAccountAddress) - +export async function entropyProgram (entropy: Entropy, endpoint: string) { const actionChoice = await inquirer.prompt([ { type: "list", @@ -35,15 +24,12 @@ export async function userPrograms ({ accounts, selectedAccount: selectedAccount }, ]) - const entropy = await initializeEntropy({ - keyMaterial: selectedAccount.data, - endpoint - }) - if (!entropy.registrationManager?.signer?.pair) { throw new Error("Keys are undefined") } + const program = new EntropyProgram(entropy, endpoint) + switch (actionChoice.action) { case "View My Programs": { try { @@ -53,7 +39,7 @@ export async function userPrograms ({ accounts, selectedAccount: selectedAccount print('You currently have no verifying keys, please register this account to generate the keys') break } - const programs = await viewPrograms(entropy, { verifyingKey }) + const programs = await program.list({ verifyingKey }) if (programs.length === 0) { print("You currently have no programs set.") } else { @@ -73,9 +59,8 @@ export async function userPrograms ({ accounts, selectedAccount: selectedAccount message: "Enter the program pointer you wish to check:", validate: (input) => (input ? true : "Program pointer is required!"), }]) - logger.debug(`program pointer: ${programPointer}`, `${FLOW_CONTEXT}::PROGRAM_PRESENCE_CHECK`); - const program = await entropy.programs.dev.getProgramInfo(programPointer); - print(program); + const info = await program.get(programPointer); + print(info); } catch (error) { console.error(error.message); } @@ -90,7 +75,7 @@ export async function userPrograms ({ accounts, selectedAccount: selectedAccount const byteArray = encoder.encode(programConfigJson) const programConfigHex = u8aToHex(byteArray) - await addProgram(entropy, { programPointer: programPointerToAdd, programConfig: programConfigHex }) + await program.add({ programPointer: programPointerToAdd, programConfig: programConfigHex }) print("Program added successfully.") } catch (error) { @@ -104,7 +89,7 @@ export async function userPrograms ({ accounts, selectedAccount: selectedAccount ({ verifyingKey } = await inquirer.prompt(verifyingKeyQuestion(entropy))) } const { programPointer: programPointerToRemove } = await inquirer.prompt(getProgramPointerInput) - await removeProgram(entropy, { programPointer: programPointerToRemove, verifyingKey }) + await program.remove({ programPointer: programPointerToRemove, verifyingKey }) print("Program removed successfully.") } catch (error) { console.error(error.message) @@ -117,11 +102,7 @@ export async function userPrograms ({ accounts, selectedAccount: selectedAccount } // eslint-disable-next-line -export async function devPrograms ({ accounts, selectedAccount: selectedAccountAddress }, options: EntropyTuiOptions, logger: EntropyLogger) { - // const FLOW_CONTEXT = 'PROGRAMS' - const { endpoint } = options - const selectedAccount = findAccountByAddressOrName(accounts, selectedAccountAddress) - +export async function entropyProgramDev (entropy, endpoint) { const choices = { "Deploy": deployProgramTUI, "Get Owned Programs": getOwnedProgramsTUI, @@ -137,16 +118,13 @@ export async function devPrograms ({ accounts, selectedAccount: selectedAccountA }, ]) - const entropy = await initializeEntropy({ - keyMaterial: selectedAccount.data, - endpoint - }) - const flow = choices[actionChoice.action] - await flow(entropy, selectedAccount) + await flow(entropy, endpoint) } -async function deployProgramTUI (entropy: Entropy, account: any) { +async function deployProgramTUI (entropy: Entropy, endpoint: string) { + const program = new EntropyProgram(entropy, endpoint) + const answers = await inquirer.prompt([ { type: "input", @@ -181,22 +159,19 @@ async function deployProgramTUI (entropy: Entropy, account: any) { ]) try { - const pointer = await deployProgram(entropy, answers) + const pointer = await program.deploy(answers) print("Program deployed successfully with pointer:", pointer) } catch (deployError) { console.error("Deployment failed:", deployError) } - - print("Deploying from account:", account.address) } -async function getOwnedProgramsTUI (entropy: Entropy, account: any) { - const userAddress = account.address - if (!userAddress) return +async function getOwnedProgramsTUI (entropy: Entropy, endpoint: string) { + const program = new EntropyProgram(entropy, endpoint) try { - const fetchedPrograms = await entropy.programs.dev.get(userAddress) + const fetchedPrograms = await program.listDeployed() if (fetchedPrograms.length) { print("Retrieved program pointers:") print(fetchedPrograms) diff --git a/src/program/main.ts b/src/program/main.ts new file mode 100644 index 00000000..95e8a903 --- /dev/null +++ b/src/program/main.ts @@ -0,0 +1,67 @@ +import Entropy from "@entropyxyz/sdk" + +import { EntropyBase } from "../common/entropy-base" +import { FLOW_CONTEXT } from "./constants" +import { loadFile, jsonToHex } from "./utils" +import { + EntropyProgramDeployParams, + EntropyProgramAddParams, + EntropyProgramRemoveParams, + EntropyProgramViewProgramsParams +} from "./types" + +export class EntropyProgram extends EntropyBase { + constructor (entropy: Entropy, endpoint: string) { + super({ entropy, endpoint, flowContext: FLOW_CONTEXT }) + } + + // User Methods: + + async add ({ programPointer, programConfig, verifyingKey }: EntropyProgramAddParams): Promise { + return this.entropy.programs.add( + { + program_pointer: programPointer, + program_config: programConfig, + }, + verifyingKey + ) + } + + async remove ({ programPointer, verifyingKey }: EntropyProgramRemoveParams): Promise { + return this.entropy.programs.remove( + programPointer, + verifyingKey + ) + } + + async list ({ verifyingKey }: EntropyProgramViewProgramsParams): Promise { + return this.entropy.programs.get(verifyingKey) + } + + // Dev Methods: + + async deploy (params: EntropyProgramDeployParams) { + const bytecode = await loadFile(params.bytecodePath) + const configurationSchema = await loadFile(params.configurationSchemaPath, 'json') + const auxillaryDataSchema = await loadFile(params.auxillaryDataSchemaPath, 'json') + // QUESTION: where / how are schema validated? + + return this.entropy.programs.dev.deploy( + bytecode, + jsonToHex(configurationSchema), + jsonToHex(auxillaryDataSchema) + ) + } + + async get (programPointer: string): Promise { + this.logger.debug(`program pointer: ${programPointer}`, `${FLOW_CONTEXT}::PROGRAM_PRESENCE_CHECK`); + return this.entropy.programs.dev.getProgramInfo(programPointer) + } + + async listDeployed () { + const address = this.entropy.keyring.accounts.registration.address + // QUESTION: will we always be wanting this address? + return this.entropy.programs.dev.get(address) + } +} + diff --git a/src/flows/programs/types.ts b/src/program/types.ts similarity index 61% rename from src/flows/programs/types.ts rename to src/program/types.ts index 0086414c..16573e8d 100644 --- a/src/flows/programs/types.ts +++ b/src/program/types.ts @@ -1,21 +1,22 @@ -export interface AddProgramParams { + +export interface EntropyProgramDeployParams { + bytecodePath: string, + configurationSchemaPath?: string + auxillaryDataSchemaPath?: string + // TODO: confirm which of these are optional +} + +export interface EntropyProgramAddParams { programPointer: string programConfig: string verifyingKey?: string } -export interface ViewProgramsParams { - verifyingKey: string -} - -export interface RemoveProgramParams { +export interface EntropyProgramRemoveParams { programPointer: string verifyingKey: string } -export interface DeployProgramParams { - bytecodePath: string, - configurationSchemaPath?: string - auxillaryDataSchemaPath?: string - // TODO: confirm which of these are optional +export interface EntropyProgramViewProgramsParams { + verifyingKey: string } diff --git a/src/program/utils.ts b/src/program/utils.ts new file mode 100644 index 00000000..3cc60746 --- /dev/null +++ b/src/program/utils.ts @@ -0,0 +1,107 @@ +import Entropy from "@entropyxyz/sdk" +import fs from "node:fs/promises" +import { isAbsolute, join } from "node:path" +import { u8aToHex } from "@polkadot/util" + +import { print } from "../common/utils" + +export async function loadFile (path?: string, encoding?: string) { + if (path === undefined) return + + const absolutePath = isAbsolute(path) + ? path + : join(process.cwd(), path) + + switch (encoding) { + case undefined: + return fs.readFile(absolutePath) + + case 'json': + return fs.readFile(absolutePath, 'utf-8') + .then(string => JSON.parse(string)) + + default: + throw Error('unknown encoding: ' + encoding) + // return fs.readFile(absolutePath, encoding) + } +} + +export function jsonToHex (obj?: object) { + if (obj === undefined) return + + const encoder = new TextEncoder() + const byteArray = encoder.encode(JSON.stringify(obj)) + + return u8aToHex(new Uint8Array(byteArray)) +} + + +export function displayPrograms (programs): void { + programs.forEach((program, index) => { + print(`${index + 1}.`) + print({ + pointer: program.program_pointer, + config: parseProgramConfig(program.program_config) + }) + print('') + }) + + // private + + function parseProgramConfig (rawConfig: unknown) { + if (typeof rawConfig !== 'string') return rawConfig + if (!rawConfig.startsWith('0x')) return rawConfig + + const hex = rawConfig.slice(2) + const utf8 = Buffer.from(hex, 'hex').toString() + const output = JSON.parse(utf8) + Object.keys(output).forEach(key => { + output[key] = output[key].map(base64toHex) + }) + + return output + } + function base64toHex (base64: string): string { + return Buffer.from(base64, 'base64').toString('hex') + } +} + + +export const addQuestions = [ + { + type: "input", + name: "programPointerToAdd", + message: "Enter the program pointer you wish to add:", + validate: (input) => (input ? true : "Program pointer is required!"), + }, + { + type: "editor", + name: "programConfigJson", + message: + "Enter the program configuration as a JSON string (this will open your default editor):", + validate: (input) => { + try { + JSON.parse(input) + return true + } catch (e) { + return "Please enter a valid JSON string for the configuration." + } + }, + }, +] + +export const getProgramPointerInput = [ + { + type: "input", + name: "programPointer", + message: "Enter the program pointer you wish to remove:", + }, +] + +export const verifyingKeyQuestion = (entropy: Entropy) => [{ + type: 'list', + name: 'verifyingKey', + message: 'Select the key to proceeed', + choices: entropy.keyring.accounts.registration.verifyingKeys, + default: entropy.keyring.accounts.registration.verifyingKeys[0] +}] diff --git a/src/tui.ts b/src/tui.ts index db18c149..85e3fc1a 100644 --- a/src/tui.ts +++ b/src/tui.ts @@ -12,6 +12,7 @@ import { entropyAccount, entropyRegister } from './account/interaction' import { entropySign } from './sign/interaction' import { entropyBalance } from './balance/interaction' import { entropyTransfer } from './transfer/interaction' +import { entropyProgram, entropyProgramDev } from './program/interaction' async function setupConfig () { let storedConfig = await config.get() @@ -43,8 +44,8 @@ export default function tui (entropy: Entropy, options: EntropyTuiOptions) { 'Sign': () => {}, 'Transfer': () => {}, // TODO: design programs in TUI (merge deploy+user programs) - 'Deploy Program': flows.devPrograms, - 'User Programs': flows.userPrograms, + 'Deploy Program': () => {}, + 'User Programs': () => {}, 'Entropy Faucet': flows.entropyFaucet, } @@ -89,6 +90,7 @@ async function main (entropy: Entropy, choices, options, logger: EntropyLogger) console.error('There are currently no accounts available, please create or import your new account using the Manage Accounts feature') } else { logger.debug(answers) + switch (answers.choice) { case 'Manage Accounts': { const response = await entropyAccount(options.endpoint, storedConfig) @@ -114,6 +116,16 @@ async function main (entropy: Entropy, choices, options, logger: EntropyLogger) .catch(err => console.error('There was an issue with signing', err)) break } + case 'User Programs': { + await entropyProgram(entropy, options.endpoint) + .catch(err => console.error('There was an error with programs', err)) + break + } + case 'Deploy Program': { + await entropyProgramDev(entropy, options.endpoint) + .catch(err => console.error('There was an error with program dev', err)) + break + } default: { throw Error(`unsupported choice: ${answers.choice}`) } diff --git a/tests/programs.test.ts b/tests/programs.test.ts index 978c67b4..422629f9 100644 --- a/tests/programs.test.ts +++ b/tests/programs.test.ts @@ -1,25 +1,25 @@ import test from 'tape' import { promiseRunner, charlieStashSeed, setupTest } from './testing-utils' -import { addProgram } from '../src/flows/programs/add' -import { viewPrograms } from '../src/flows/programs/view' -import { removeProgram } from '../src/flows/programs/remove' -import { deployProgram } from '../src/flows/programs/deploy' +import { EntropyProgram } from '../src/program/main' const networkType = 'two-nodes' +const endpoint = 'ws://127.0.0.1:9944' -test('programs', async t => { +test('program', async t => { const { run, entropy } = await setupTest(t, { seed: charlieStashSeed, networkType }) await run('register', entropy.register()) // TODO: consider removing this in favour of just testing add + const program = new EntropyProgram(entropy, endpoint) + let programPointer1 - t.test('programs - deploy', async t => { + t.test('program - deploy', async t => { const run = promiseRunner(t) programPointer1 = await run ( 'deploy!', - deployProgram(entropy, { + program.deploy({ bytecodePath: './tests/programs/program_noop.wasm' }) ) @@ -27,10 +27,10 @@ test('programs', async t => { t.end() }) - const getPrograms = () => viewPrograms(entropy, { verifyingKey: entropy.programs.verifyingKey }) + const getPrograms = () => program.list({ verifyingKey: entropy.programs.verifyingKey }) const verifyingKey = entropy.programs.verifyingKey - t.test('programs - add', async t => { + t.test('program - add', async t => { const run = promiseRunner(t) const programsBeforeAdd = await run('get programs initial', getPrograms()) @@ -38,7 +38,10 @@ test('programs', async t => { await run( 'adding program', - addProgram(entropy, { programPointer: programPointer1, programConfig: '' }) + program.add({ + programPointer: programPointer1, + programConfig: '' + }) ) const programsAfterAdd = await run('get programs after add', getPrograms()) t.equal(programsAfterAdd.length, 2, 'charlie has 2 programs') @@ -46,7 +49,7 @@ test('programs', async t => { t.end() }) - t.test('programs - remove', async t => { + t.test('program - remove', async t => { const run = promiseRunner(t) const programsBeforeRemove = await run('get programs initial', getPrograms()) @@ -54,7 +57,10 @@ test('programs', async t => { await run( 'removing noop program', - removeProgram(entropy, { programPointer: programPointer1, verifyingKey }) + program.remove({ + programPointer: programPointer1, + verifyingKey + }) ) const programsAfterRemove = await run('get programs initial', getPrograms()) t.equal(programsAfterRemove.length, 1, 'charlie has 1 less program') @@ -62,12 +68,12 @@ test('programs', async t => { t.end() }) - t.test('programs - view', async t => { + t.test('program - view', async t => { const run = promiseRunner(t) const programs = await run( 'get charlie programs', - viewPrograms(entropy, { verifyingKey }) + program.list({ verifyingKey }) ) t.equal(programs.length, 1, 'charlie has 1 program') From 54de68d8e935d0e565ebef0283c69a879e5ef13c Mon Sep 17 00:00:00 2001 From: Nayyir Jutha Date: Tue, 24 Sep 2024 15:21:24 -0400 Subject: [PATCH 22/25] [NayNay] File Restructure: Faucet (#225) * [NayNay] File Restructure: Faucet - restructured faucet to match the new structure * completed restructure, working on tests * updated faucet tests * updated recursion for faucet retry mechanism, reduce calls to the chain * documented some of the methods used for the faucet * added changelog --- CHANGELOG.md | 18 ++-- src/balance/command.ts | 3 +- src/common/entropy-base.ts | 2 + .../helpers}/signer.ts | 2 +- src/faucet/interaction.ts | 44 ++++++++ src/faucet/main.ts | 101 ++++++++++++++++++ src/faucet/types.ts | 7 ++ .../constants.ts => faucet/utils.ts} | 0 src/flows/entropyFaucet/faucet.ts | 98 ----------------- src/flows/entropyFaucet/index.ts | 45 -------- src/flows/index.ts | 1 - src/tui.ts | 19 ++-- tests/faucet.test.ts | 16 +-- 13 files changed, 190 insertions(+), 166 deletions(-) rename src/{flows/entropyFaucet => faucet/helpers}/signer.ts (100%) create mode 100644 src/faucet/interaction.ts create mode 100644 src/faucet/main.ts create mode 100644 src/faucet/types.ts rename src/{flows/entropyFaucet/constants.ts => faucet/utils.ts} (100%) delete mode 100644 src/flows/entropyFaucet/faucet.ts delete mode 100644 src/flows/entropyFaucet/index.ts delete mode 100644 src/flows/index.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index f982d02b..f00817ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,16 +27,21 @@ 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/base-commands.ts' - base abstract class for new command classes +- 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/command.ts' - main entry file for balance command for tui/cli + - 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/command.ts' - main entry file for transfer command for tui/cli + - 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/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 +- 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 @@ -48,6 +53,7 @@ Version header format: `[version] Name - year-month-day (entropy-core compatibil - 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 diff --git a/src/balance/command.ts b/src/balance/command.ts index 2ff7daa5..4d0b023a 100644 --- a/src/balance/command.ts +++ b/src/balance/command.ts @@ -1,4 +1,5 @@ import { Command } from "commander"; +import Entropy from "@entropyxyz/sdk"; import { cliWrite, endpointOption, loadEntropy, passwordOption } from "src/common/utils-cli"; import { EntropyBalance } from "./main"; @@ -10,7 +11,7 @@ export function entropyBalanceCommand () { .addOption(passwordOption()) .addOption(endpointOption()) .action(async (address, opts) => { - const entropy = await loadEntropy(address, opts.endpoint) + const entropy: 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`) diff --git a/src/common/entropy-base.ts b/src/common/entropy-base.ts index 5f604ba5..e24c9479 100644 --- a/src/common/entropy-base.ts +++ b/src/common/entropy-base.ts @@ -4,9 +4,11 @@ import { EntropyLogger } from "./logger"; export abstract class EntropyBase { protected logger: EntropyLogger protected entropy: Entropy + protected endpoint: string constructor ({ entropy, endpoint, flowContext }: { entropy: Entropy, endpoint: string, flowContext: string }) { this.logger = new EntropyLogger(flowContext, endpoint) this.entropy = entropy + this.endpoint = endpoint } } diff --git a/src/flows/entropyFaucet/signer.ts b/src/faucet/helpers/signer.ts similarity index 100% rename from src/flows/entropyFaucet/signer.ts rename to src/faucet/helpers/signer.ts index bb492291..d3a9140a 100644 --- a/src/flows/entropyFaucet/signer.ts +++ b/src/faucet/helpers/signer.ts @@ -2,8 +2,8 @@ import Entropy from "@entropyxyz/sdk"; import type { Signer, SignerResult } from "@polkadot/api/types"; import { Registry, SignerPayloadJSON } from "@polkadot/types/types"; import { u8aToHex } from "@polkadot/util"; -import { stripHexPrefix } from "../../common/utils"; import { blake2AsHex, decodeAddress, encodeAddress, signatureVerify } from "@polkadot/util-crypto"; +import { stripHexPrefix } from "../../common/utils"; let id = 0 export default class FaucetSigner implements Signer { diff --git a/src/faucet/interaction.ts b/src/faucet/interaction.ts new file mode 100644 index 00000000..54cc647b --- /dev/null +++ b/src/faucet/interaction.ts @@ -0,0 +1,44 @@ +import Entropy from "@entropyxyz/sdk" +import { EntropyLogger } from '../common/logger' +import { TESTNET_PROGRAM_HASH } from "./utils" +import { EntropyFaucet } from "./main" +import { print } from "src/common/utils" + +let chosenVerifyingKeys = [] +const amount = "10000000000" +// context for logging file +const FLOW_CONTEXT = 'ENTROPY_FAUCET_INTERACTION' +export async function entropyFaucet (entropy: Entropy, options, logger: EntropyLogger) { + const { endpoint } = options + if (!entropy.registrationManager.signer.pair) { + throw new Error("Keys are undefined") + } + const faucetService = new EntropyFaucet(entropy, endpoint) + const verifyingKeys = await faucetService.getAllFaucetVerifyingKeys() + // @ts-expect-error + return sendMoneyFromRandomFaucet(entropy, options.endpoint, verifyingKeys, logger) +} + +// Method that takes in the initial list of verifying keys (to avoid multiple calls to the rpc) and recursively retries each faucet until +// a successful transfer is made +async function sendMoneyFromRandomFaucet (entropy: Entropy, endpoint: string, verifyingKeys: string[], logger: EntropyLogger) { + const faucetService = new EntropyFaucet(entropy, endpoint) + const selectedAccountAddress = entropy.keyring.accounts.registration.address + const { chosenVerifyingKey, faucetAddress } = faucetService.getRandomFaucet(chosenVerifyingKeys, verifyingKeys) + try { + await faucetService.sendMoney({ amount, addressToSendTo: selectedAccountAddress, faucetAddress, chosenVerifyingKey, faucetProgramPointer: TESTNET_PROGRAM_HASH }) + // reset chosen keys after successful transfer + chosenVerifyingKeys = [] + print(`Account: ${selectedAccountAddress} has been successfully funded with ${parseInt(amount).toLocaleString('en-US')} BITS`) + } catch (error) { + logger.error('Error issuing funds through faucet', error, FLOW_CONTEXT) + chosenVerifyingKeys.push(chosenVerifyingKey) + if (error.message.includes('FaucetError') || chosenVerifyingKeys.length === verifyingKeys.length) { + console.error('ERR::', error.message) + return + } else { + // Check for non faucet errors (FaucetError) and retry faucet + await sendMoneyFromRandomFaucet(entropy, endpoint, verifyingKeys, logger) + } + } +} \ No newline at end of file diff --git a/src/faucet/main.ts b/src/faucet/main.ts new file mode 100644 index 00000000..2933ce35 --- /dev/null +++ b/src/faucet/main.ts @@ -0,0 +1,101 @@ +import Entropy from "@entropyxyz/sdk"; +import { EntropyBase } from "../common/entropy-base"; +import { blake2AsHex, encodeAddress } from "@polkadot/util-crypto"; +import { FAUCET_PROGRAM_MOD_KEY, TESTNET_PROGRAM_HASH } from "./utils"; +import { EntropyBalance } from "src/balance/main"; +import { EntropyProgram } from "src/program/main"; +import FaucetSigner from "./helpers/signer"; +import { SendMoneyParams } from "./types"; + +const FLOW_CONTEXT = 'ENTROPY-FAUCET' + +export class EntropyFaucet extends EntropyBase { + constructor (entropy: Entropy, endpoint: string) { + super({ entropy, endpoint, flowContext: FLOW_CONTEXT }) + } + + // Method used to sign and send the transfer request (transfer request = call argument) using the custom signer + // created to overwrite how we sign the payload that is sent up chain + async faucetSignAndSend (call: any, amount: number, senderAddress: string, chosenVerifyingKey: any): Promise { + const api = this.entropy.substrate + const faucetSigner = new FaucetSigner(api.registry, this.entropy, amount, chosenVerifyingKey) + + const sig = await call.signAsync(senderAddress, { + signer: faucetSigner, + }); + return new Promise((resolve, reject) => { + sig.send(({ status, dispatchError }: any) => { + // status would still be set, but in the case of error we can shortcut + // to just check it (so an error would indicate InBlock or Finalized) + if (dispatchError) { + let msg: string + if (dispatchError.isModule) { + // for module errors, we have the section indexed, lookup + const decoded = api.registry.findMetaError(dispatchError.asModule); + // @ts-ignore + const { documentation, method, section } = decoded; + + msg = `${section}.${method}: ${documentation.join(' ')}` + } else { + // Other, CannotLookup, BadOrigin, no extra info + msg = dispatchError.toString() + } + return reject(Error(msg)) + } + if (status.isFinalized) resolve(status) + }) + }) + } + + async getAllFaucetVerifyingKeys (programModKey = FAUCET_PROGRAM_MOD_KEY) { + const modifiableKeys = await this.entropy.substrate.query.registry.modifiableKeys(programModKey) + return modifiableKeys.toJSON() + } + + // To handle overloading the individual faucet, multiple faucet accounts have been generated, and here is + // where we choose one of those faucet's at random + getRandomFaucet (previousVerifyingKeys: string[] = [], allVerifyingKeys: string[] = []) { + if (allVerifyingKeys.length === previousVerifyingKeys.length) { + throw new Error('FaucetError: There are no more faucets to choose from') + } + let chosenVerifyingKey = allVerifyingKeys[Math.floor(Math.random() * allVerifyingKeys.length)] + if (previousVerifyingKeys.length && previousVerifyingKeys.includes(chosenVerifyingKey)) { + const filteredVerifyingKeys = allVerifyingKeys.filter((key: string) => !previousVerifyingKeys.includes(key)) + chosenVerifyingKey = filteredVerifyingKeys[Math.floor(Math.random() * filteredVerifyingKeys.length)] + } + const hashedKey = blake2AsHex(chosenVerifyingKey) + const faucetAddress = encodeAddress(hashedKey, 42).toString() + + return { chosenVerifyingKey, faucetAddress } + } + + async sendMoney ( + { + amount, + addressToSendTo, + faucetAddress, + chosenVerifyingKey, + faucetProgramPointer = TESTNET_PROGRAM_HASH + }: SendMoneyParams + ): Promise { + const balanceService = new EntropyBalance(this.entropy, this.endpoint) + const programService = new EntropyProgram(this.entropy, this.endpoint) + // check balance of faucet address + 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 programService.list({ verifyingKey: chosenVerifyingKey }) + if (programs.length) { + if (programs.length > 1) throw new Error('ProgramsError: Faucet Account has too many programs attached, expected less') + if (programs.length === 1 && programs[0].program_pointer !== faucetProgramPointer) { + throw new Error('ProgramsError: Faucet Account does not possess Faucet program') + } + } else { + throw new Error('ProgramsError: Faucet Account has no programs attached') + } + + const transfer = this.entropy.substrate.tx.balances.transferAllowDeath(addressToSendTo, BigInt(amount)); + const transferStatus = await this.faucetSignAndSend(transfer, parseInt(amount), faucetAddress, chosenVerifyingKey) + if (transferStatus.isFinalized) return transferStatus + } +} \ No newline at end of file diff --git a/src/faucet/types.ts b/src/faucet/types.ts new file mode 100644 index 00000000..9e874085 --- /dev/null +++ b/src/faucet/types.ts @@ -0,0 +1,7 @@ +export interface SendMoneyParams { + amount: string + addressToSendTo: string + faucetAddress: string + chosenVerifyingKey: string + faucetProgramPointer: string +} \ No newline at end of file diff --git a/src/flows/entropyFaucet/constants.ts b/src/faucet/utils.ts similarity index 100% rename from src/flows/entropyFaucet/constants.ts rename to src/faucet/utils.ts diff --git a/src/flows/entropyFaucet/faucet.ts b/src/flows/entropyFaucet/faucet.ts deleted file mode 100644 index 3e0d0c17..00000000 --- a/src/flows/entropyFaucet/faucet.ts +++ /dev/null @@ -1,98 +0,0 @@ -// check verifying key has the balance and proper program hash - -import Entropy from "@entropyxyz/sdk"; -import { blake2AsHex, encodeAddress } from "@polkadot/util-crypto"; -import { EntropyProgram } from '../../program/main' -import FaucetSigner from "./signer"; -import { FAUCET_PROGRAM_MOD_KEY, TESTNET_PROGRAM_HASH } from "./constants"; -import { EntropyBalance } from "src/balance/main"; - -// only the faucet program should be on the key -async function faucetSignAndSend (call: any, entropy: Entropy, amount: number, senderAddress: string, chosenVerifyingKey: any): Promise { - const api = entropy.substrate - const faucetSigner = new FaucetSigner(api.registry, entropy, amount, chosenVerifyingKey) - - const sig = await call.signAsync(senderAddress, { - signer: faucetSigner, - }); - return new Promise((resolve, reject) => { - sig.send(({ status, dispatchError }: any) => { - // status would still be set, but in the case of error we can shortcut - // to just check it (so an error would indicate InBlock or Finalized) - if (dispatchError) { - let msg: string - if (dispatchError.isModule) { - // for module errors, we have the section indexed, lookup - const decoded = api.registry.findMetaError(dispatchError.asModule); - // @ts-ignore - const { documentation, method, section } = decoded; - - msg = `${section}.${method}: ${documentation.join(' ')}` - } else { - // Other, CannotLookup, BadOrigin, no extra info - msg = dispatchError.toString() - } - return reject(Error(msg)) - } - if (status.isFinalized) resolve(status) - }) - }) -} - -export async function getRandomFaucet (entropy: Entropy, previousVerifyingKeys: string[] = [], programModKey = FAUCET_PROGRAM_MOD_KEY) { - const modifiableKeys = await entropy.substrate.query.registry.modifiableKeys(programModKey) - const verifyingKeys = JSON.parse(JSON.stringify(modifiableKeys.toJSON())) - - // Choosing one of the 5 verifiying keys at random to be used as the faucet sender - if (verifyingKeys.length === previousVerifyingKeys.length) { - throw new Error('FaucetError: There are no more faucets to choose from') - } - let chosenVerifyingKey = verifyingKeys[Math.floor(Math.random() * verifyingKeys.length)] - if (previousVerifyingKeys.length && previousVerifyingKeys.includes(chosenVerifyingKey)) { - const filteredVerifyingKeys = verifyingKeys.filter((key: string) => !previousVerifyingKeys.includes(key)) - chosenVerifyingKey = filteredVerifyingKeys[Math.floor(Math.random() * filteredVerifyingKeys.length)] - } - const hashedKey = blake2AsHex(chosenVerifyingKey) - const faucetAddress = encodeAddress(hashedKey, 42).toString() - - return { chosenVerifyingKey, faucetAddress, verifyingKeys } -} - -export async function sendMoney ( - entropy: Entropy, - endpoint: string, - { - amount, - addressToSendTo, - faucetAddress, - chosenVerifyingKey, - faucetProgramPointer = TESTNET_PROGRAM_HASH - }: { - amount: string, - addressToSendTo: string, - faucetAddress: string, - chosenVerifyingKey: string, - faucetProgramPointer: string - } -): Promise { - const balanceService = new EntropyBalance(entropy, endpoint) - const programService = new EntropyProgram(entropy, endpoint) - - // check balance of faucet address - 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 programService.list({ verifyingKey: chosenVerifyingKey }) - if (programs.length) { - if (programs.length > 1) throw new Error('ProgramsError: Faucet Account has too many programs attached, expected less') - if (programs.length === 1 && programs[0].program_pointer !== faucetProgramPointer) { - throw new Error('ProgramsError: Faucet Account does not possess Faucet program') - } - } else { - throw new Error('ProgramsError: Faucet Account has no programs attached') - } - - 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 -} diff --git a/src/flows/entropyFaucet/index.ts b/src/flows/entropyFaucet/index.ts deleted file mode 100644 index 9d5c4c95..00000000 --- a/src/flows/entropyFaucet/index.ts +++ /dev/null @@ -1,45 +0,0 @@ -import Entropy from "@entropyxyz/sdk" -import { findAccountByAddressOrName, print } from "../../common/utils" -import { initializeEntropy } from "../../common/initializeEntropy" -import { EntropyLogger } from '../../common/logger' -import { getRandomFaucet, sendMoney } from "./faucet" -import { TESTNET_PROGRAM_HASH } from "./constants" - -let chosenVerifyingKeys = [] -export async function entropyFaucet ({ accounts, selectedAccount: selectedAccountAddress }, options, logger: EntropyLogger) { - const FLOW_CONTEXT = 'ENTROPY_FAUCET' - let faucetAddress - let chosenVerifyingKey - let entropy: Entropy - let verifyingKeys: string[] = [] - const amount = "10000000000" - const { endpoint } = options - const selectedAccount = findAccountByAddressOrName(accounts, selectedAccountAddress) - logger.log(`selectedAccount::`, FLOW_CONTEXT) - logger.log(selectedAccount, FLOW_CONTEXT) - try { - // @ts-ignore (see TODO on aliceAccount) - entropy = await initializeEntropy({ keyMaterial: selectedAccount.data, endpoint }) - - if (!entropy.registrationManager.signer.pair) { - throw new Error("Keys are undefined") - } - - ({ chosenVerifyingKey, faucetAddress, verifyingKeys } = await getRandomFaucet(entropy, chosenVerifyingKeys)) - - await sendMoney(entropy, options.endpoint, { amount, addressToSendTo: selectedAccountAddress, faucetAddress, chosenVerifyingKey, faucetProgramPointer: TESTNET_PROGRAM_HASH }) - // reset chosen keys after successful transfer - chosenVerifyingKeys = [] - print(`Account: ${selectedAccountAddress} has been successfully funded with ${parseInt(amount).toLocaleString('en-US')} BITS`) - } catch (error) { - logger.error('Error issuing funds through faucet', error, FLOW_CONTEXT) - chosenVerifyingKeys.push(chosenVerifyingKey) - if (error.message.includes('FaucetError') || chosenVerifyingKeys.length === verifyingKeys.length) { - console.error('ERR::', error.message) - return - } else { - // Check for non faucet errors (FaucetError) and retry faucet - await entropyFaucet({ accounts, selectedAccount: selectedAccountAddress }, options, logger) - } - } -} diff --git a/src/flows/index.ts b/src/flows/index.ts deleted file mode 100644 index 1c2277d1..00000000 --- a/src/flows/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { entropyFaucet } from './entropyFaucet' diff --git a/src/tui.ts b/src/tui.ts index 85e3fc1a..ef6c2279 100644 --- a/src/tui.ts +++ b/src/tui.ts @@ -1,7 +1,6 @@ import inquirer from 'inquirer' import Entropy from '@entropyxyz/sdk' import * as config from './config' -import * as flows from './flows' import { EntropyTuiOptions } from './types' import { logo } from './common/ascii' import { print } from './common/utils' @@ -12,6 +11,7 @@ import { entropyAccount, entropyRegister } from './account/interaction' import { entropySign } from './sign/interaction' import { entropyBalance } from './balance/interaction' import { entropyTransfer } from './transfer/interaction' +import { entropyFaucet } from './faucet/interaction' import { entropyProgram, entropyProgramDev } from './program/interaction' async function setupConfig () { @@ -46,14 +46,13 @@ export default function tui (entropy: Entropy, options: EntropyTuiOptions) { // TODO: design programs in TUI (merge deploy+user programs) 'Deploy Program': () => {}, 'User Programs': () => {}, - 'Entropy Faucet': flows.entropyFaucet, } - // const devChoices = { - // // 'Entropy Faucet': flows.entropyFaucet, - // } + const devChoices = { + 'Entropy Faucet': () => {}, + } - // if (options.dev) Object.assign(choices, devChoices) + if (options.dev) Object.assign(choices, devChoices) // assign exit so its last Object.assign(choices, { 'Exit': async () => {} }) @@ -116,6 +115,14 @@ async function main (entropy: Entropy, choices, options, logger: EntropyLogger) .catch(err => console.error('There was an issue with signing', err)) break } + case 'Entropy Faucet': { + try { + await entropyFaucet(entropy, options, logger) + } catch (error) { + console.error('There was an issue with running the faucet', error); + } + break + } case 'User Programs': { await entropyProgram(entropy, options.endpoint) .catch(err => console.error('There was an error with programs', err)) diff --git a/tests/faucet.test.ts b/tests/faucet.test.ts index 08c37eb9..3a151fe8 100644 --- a/tests/faucet.test.ts +++ b/tests/faucet.test.ts @@ -4,17 +4,18 @@ 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 { LOCAL_PROGRAM_HASH } from '../src/flows/entropyFaucet/constants' +import { EntropyFaucet } from '../src/faucet/main' +import { LOCAL_PROGRAM_HASH } from '../src/faucet/utils' import { EntropyAccount } from '../src/account/main' 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 faucetService = new EntropyFaucet(naynayEntropy, endpoint) + const accountService = new EntropyAccount(entropy, endpoint) const faucetProgram = readFileSync('tests/programs/faucet_program.wasm') @@ -52,8 +53,9 @@ test('Faucet Tests', async t => { programData: [{ program_pointer: faucetProgramPointer, program_config: userConfig }] } )) - - const { chosenVerifyingKey, faucetAddress } = await getRandomFaucet(entropy, [], entropy.keyring.accounts.registration.address) + const verifyingKeys = await faucetService.getAllFaucetVerifyingKeys(entropy.keyring.accounts.registration.address) + // @ts-expect-error + const { chosenVerifyingKey, faucetAddress } = faucetService.getRandomFaucet([], verifyingKeys) // adding funds to faucet address entropyBalance = await balanceService.getBalance(entropy.keyring.accounts.registration.address) const faucetAddressBalance = await balanceService.getBalance(faucetAddress) @@ -63,9 +65,7 @@ test('Faucet Tests', async t => { await run('Transfer funds to faucet address', transferService.transfer(faucetAddress, "1000")) - const transferStatus = await sendMoney( - naynayEntropy, - endpoint, + const transferStatus = await faucetService.sendMoney( { amount: "10000000000", addressToSendTo: naynayEntropy.keyring.accounts.registration.address, From 7cd50723abcf0b0d6550f4558c14aa4a2b11c353 Mon Sep 17 00:00:00 2001 From: Nayyir Jutha Date: Tue, 24 Sep 2024 18:20:33 -0400 Subject: [PATCH 23/25] updated tui to handle loading entropy on initial start of cli and create first account, updated choices to be array and removed noop fns --- src/tui.ts | 39 ++++++++++++++++++++++----------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/src/tui.ts b/src/tui.ts index ef6c2279..bd7780cf 100644 --- a/src/tui.ts +++ b/src/tui.ts @@ -36,26 +36,25 @@ export default function tui (entropy: Entropy, options: EntropyTuiOptions) { console.log(logo) // the Entropy logo logger.debug(options) - const choices = { - 'Manage Accounts': () => {}, - // leaving as a noop function until all flows are restructured - 'Balance': () => {}, - 'Register': () => {}, - 'Sign': () => {}, - 'Transfer': () => {}, + let choices = [ + 'Manage Accounts', + 'Balance', + 'Register', + 'Sign', + 'Transfer', // TODO: design programs in TUI (merge deploy+user programs) - 'Deploy Program': () => {}, - 'User Programs': () => {}, - } + 'Deploy Program', + 'User Programs', + ] - const devChoices = { - 'Entropy Faucet': () => {}, - } + const devChoices = [ + 'Entropy Faucet', + ] - if (options.dev) Object.assign(choices, devChoices) + if (options.dev) choices = [...choices, ...devChoices] // assign exit so its last - Object.assign(choices, { 'Exit': async () => {} }) + choices = [...choices, 'Exit'] main(entropy, choices, options, logger) } @@ -63,19 +62,25 @@ export default function tui (entropy: Entropy, options: EntropyTuiOptions) { async function main (entropy: Entropy, choices, options, logger: EntropyLogger) { const storedConfig = await setupConfig() + // Entropy is undefined on initial install, after user creates their first account, + // entropy should be loaded + if (storedConfig.selectedAccount && !entropy) { + entropy = await loadEntropy(storedConfig.selectedAccount, options.endpoint) + } // 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) { await entropy.close() entropy = await loadEntropy(storedConfig.selectedAccount, options.endpoint); } + const answers = await inquirer.prompt([{ type: 'list', name: 'choice', message: 'Select Action', - pageSize: Object.keys(choices).length, - choices: Object.keys(choices), + pageSize: choices.length, + choices, }]) if (answers.choice === 'Exit') { From cc63c728703495ce1ed1ba32adaf6a964e2937b5 Mon Sep 17 00:00:00 2001 From: mixmix Date: Thu, 26 Sep 2024 13:55:47 +1200 Subject: [PATCH 24/25] refactor entropy register so CLI/TUI are similar --- src/account/command.ts | 28 ++++++---------------------- src/account/interaction.ts | 19 +++++++++---------- src/account/main.ts | 23 +---------------------- src/account/utils.ts | 19 +++++++++++++++---- 4 files changed, 31 insertions(+), 58 deletions(-) diff --git a/src/account/command.ts b/src/account/command.ts index 06cf3a58..a502a850 100644 --- a/src/account/command.ts +++ b/src/account/command.ts @@ -1,11 +1,10 @@ import Entropy from "@entropyxyz/sdk" import { Command, Option } from 'commander' import { EntropyAccount } from "./main"; -import { selectAndPersistNewAccount } from "./utils"; +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"; -import { findAccountByAddressOrName } from "src/common/utils"; export function entropyAccountCommand () { return new Command('account') @@ -102,28 +101,13 @@ function entropyAccountRegister () { // ) // ) .action(async (opts) => { - const { account, endpoint, /* password */ } = opts - const storedConfig = await config.get() - const { accounts } = storedConfig - const accountToRegister = findAccountByAddressOrName(accounts, account) - if (!accountToRegister) { - throw new Error('AccountError: Unable to register non-existent account') - } - - const entropy: Entropy = await loadEntropy(accountToRegister.address, endpoint) - const accountService = new EntropyAccount(entropy, endpoint) - const updatedAccount = await accountService.registerAccount(accountToRegister) + // 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 arrIdx = accounts.indexOf(accountToRegister) - accounts.splice(arrIdx, 1, updatedAccount) - await config.set({ - ...storedConfig, - accounts, - selectedAccount: updatedAccount.address - }) + const verifyingKey = await accountService.register() + await addVerifyingKeyToAccountAndSelect(verifyingKey, opts.account) - const verifyingKeys = updatedAccount?.data?.registration?.verifyingKeys - const verifyingKey = verifyingKeys[verifyingKeys.length - 1] cliWrite(verifyingKey) process.exit(0) }) diff --git a/src/account/interaction.ts b/src/account/interaction.ts index d3277c0e..bf1c599f 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 { EntropyAccount } from './main' -import { selectAndPersistNewAccount } from "./utils"; +import { selectAndPersistNewAccount, addVerifyingKeyToAccountAndSelect } from "./utils"; import { findAccountByAddressOrName, print } from "../common/utils" import { EntropyConfig } from "../config/types"; import * as config from "../config"; @@ -77,16 +77,15 @@ export async function entropyRegister (entropy: Entropy, endpoint: string, store const accountService = new EntropyAccount(entropy, endpoint) const { accounts, selectedAccount } = storedConfig - const currentAccount = findAccountByAddressOrName(accounts, selectedAccount) - if (!currentAccount) { + const account = findAccountByAddressOrName(accounts, selectedAccount) + if (!account) { print("No account selected to register") - return; + return } - 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 } + 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.") } diff --git a/src/account/main.ts b/src/account/main.ts index 0c5d46d2..c1395dcb 100644 --- a/src/account/main.ts +++ b/src/account/main.ts @@ -66,6 +66,7 @@ export class EntropyAccount extends EntropyBase { } : 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. @@ -75,28 +76,6 @@ export class EntropyAccount extends EntropyBase { }) } - // WATCH: should this be extracted to interaction.ts? - async registerAccount (account: EntropyAccountConfig, registerParams?: AccountRegisterParams): Promise { - this.logger.debug( - [ - `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) - // 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) - throw error - } - } - /* PRIVATE */ private async pruneRegistration () { diff --git a/src/account/utils.ts b/src/account/utils.ts index 86a595f1..111f132b 100644 --- a/src/account/utils.ts +++ b/src/account/utils.ts @@ -1,9 +1,9 @@ +import { ACCOUNTS_CONTENT } from './constants'; import { EntropyAccountConfig } from "../config/types"; import * as config from "../config"; -import { ACCOUNTS_CONTENT } from './constants'; -import { generateAccountChoices } from '../common/utils'; +import { generateAccountChoices, findAccountByAddressOrName } from '../common/utils'; -export async function selectAndPersistNewAccount (newAccount) { +export async function selectAndPersistNewAccount (newAccount: EntropyAccountConfig) { const storedConfig = await config.get() const { accounts } = storedConfig @@ -19,11 +19,22 @@ export async function selectAndPersistNewAccount (newAccount) { accounts.push(newAccount) await config.set({ ...storedConfig, - accounts, selectedAccount: newAccount.address }) } +export async function addVerifyingKeyToAccountAndSelect (verifyingKey: string, accountNameOrAddress: string) { + const storedConfig = await config.get() + const account = findAccountByAddressOrName(storedConfig.accounts, accountNameOrAddress) + account.data.registration.verifyingKeys.push(verifyingKey) + + // persist to config, set selectedAccount + await config.set({ + ...storedConfig, + selectedAccount: account.address + }) +} + function validateSeedInput (seed) { if (seed.includes('#debug')) return true if (seed.length === 66 && seed.startsWith('0x')) return true From 4c86915d6e35f76200addad67f3c1889cf8ddb4b Mon Sep 17 00:00:00 2001 From: mixmix Date: Thu, 26 Sep 2024 14:05:15 +1200 Subject: [PATCH 25/25] add detailed error message on failure to persist verifyingKey --- src/account/utils.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/account/utils.ts b/src/account/utils.ts index 111f132b..cf51603a 100644 --- a/src/account/utils.ts +++ b/src/account/utils.ts @@ -26,6 +26,8 @@ export async function selectAndPersistNewAccount (newAccount: EntropyAccountConf export async function addVerifyingKeyToAccountAndSelect (verifyingKey: string, accountNameOrAddress: string) { const storedConfig = await config.get() const account = findAccountByAddressOrName(storedConfig.accounts, accountNameOrAddress) + if (!account) throw Error(`Unable to persist verifyingKey "${verifyingKey}" to unknown account "${accountNameOrAddress}"`) + account.data.registration.verifyingKeys.push(verifyingKey) // persist to config, set selectedAccount