diff --git a/src/common/initializeEntropy.ts b/src/common/initializeEntropy.ts index f9953b6b..7eec720a 100644 --- a/src/common/initializeEntropy.ts +++ b/src/common/initializeEntropy.ts @@ -6,7 +6,7 @@ import inquirer from "inquirer" import { decrypt, encrypt } from "../flows/password" import { debug } from "../common/utils" import * as config from "../config" -import { EntropyAccountData } from "../types" +import { EntropyAccountData } from "../config/types" // TODO: unused // let defaultAccount // have a main account to use @@ -17,7 +17,8 @@ const keyrings = { default: undefined // this is the "selected account" keyring } -export function getKeyring (address) { +export function getKeyring (address?: string) { + if (!address && keyrings.default) return keyrings.default if (address && keyrings[address]) return keyrings[address] // explicitly return undefined so there is no confusion around what is selected @@ -65,7 +66,7 @@ export const initializeEntropy = async ({ keyMaterial, password, endpoint, confi } let selectedAccount - const storedKeyring = getKeyring(accountData?.admin?.address) + const storedKeyring = getKeyring(accountData.admin.address) if(!storedKeyring) { const keyring = new Keyring({ ...accountData, debug: true }) diff --git a/src/common/progress.ts b/src/common/progress.ts new file mode 100644 index 00000000..a2410501 --- /dev/null +++ b/src/common/progress.ts @@ -0,0 +1,32 @@ +import cliProgress from 'cli-progress' +import colors from 'ansi-colors' + +export function setupProgress (label: string): { start: () => void; stop: () => void } { + let interval: NodeJS.Timeout + const b1 = new cliProgress.SingleBar({ + format: `${label} |` + colors.cyan('{bar}') + '| {percentage}%', + barCompleteChar: '\u2588', + barIncompleteChar: '\u2591', + hideCursor: true + }) + + const start = () => { + // 160 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, { + speed: "N/A" + }) + // update values + interval = setInterval(() => { + b1.increment() + }, 100) + } + + const stop = () => { + b1.stop() + clearInterval(interval) + } + + return { start, stop } +} \ No newline at end of file diff --git a/src/common/utils.ts b/src/common/utils.ts index dca31346..ece9bdcb 100644 --- a/src/common/utils.ts +++ b/src/common/utils.ts @@ -2,7 +2,7 @@ import { decodeAddress, encodeAddress } from "@polkadot/keyring" import { hexToU8a, isHex } from "@polkadot/util" import { Buffer } from 'buffer' import Debug from 'debug' -import { EntropyAccountConfig } from "../types" +import { EntropyAccountConfig } from "../config/types" const _debug = Debug('@entropyxyz/cli') diff --git a/src/config/index.ts b/src/config/index.ts index c37114fe..3ece9e38 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -7,7 +7,7 @@ import envPaths from 'env-paths' import allMigrations from './migrations' -const paths = envPaths('entropyxyz', { suffix: '' }) +const paths = envPaths('entropy-cryptography', { suffix: '' }) const CONFIG_PATH = join(paths.config, 'entropy-cli.json') const OLD_CONFIG_PATH = join(process.env.HOME, '.entropy-cli.config') diff --git a/src/config/types.ts b/src/config/types.ts new file mode 100644 index 00000000..74d52ffc --- /dev/null +++ b/src/config/types.ts @@ -0,0 +1,36 @@ +export interface EntropyConfig { + accounts: EntropyAccountConfig[] + endpoints: { dev: string; 'test-net': string } + 'migration-version': string +} + +export interface EntropyAccountConfig { + name: string + address: string + data: EntropyAccountData +} + +export interface EntropyAccountData { + debug?: boolean + seed: string + admin?: EntropyAccount + registration?: EntropyAccount + deviceKey?: EntropyAccount + programDev?: EntropyAccount +} + +export interface EntropyAccount { + seed: string + path: string + address: string + verifyingKeys?: string[] + userContext?: EntropyAccountContextType + used?: boolean +} + +export enum EntropyAccountContextType { + programDev = 'PROGRAM_DEV_KEY', + registration = 'ADMIN_KEY', + deviceKey = 'CONSUMER_KEY', + undefined = 'MIXED_KEY', +} \ No newline at end of file diff --git a/src/flows/entropyTransfer/index.ts b/src/flows/entropyTransfer/index.ts index b7216c18..d376703f 100644 --- a/src/flows/entropyTransfer/index.ts +++ b/src/flows/entropyTransfer/index.ts @@ -1,9 +1,8 @@ import inquirer from "inquirer" -import cliProgress from 'cli-progress' -import colors from 'ansi-colors' - -import { print, getSelectedAccount } from "../../common/utils" +import { getSelectedAccount, print } from "../../common/utils" import { initializeEntropy } from "../../common/initializeEntropy" +import { transfer } from "./transfer" +import { setupProgress } from "src/common/progress" const question = [ { @@ -29,53 +28,37 @@ export async function entropyTransfer ({ accounts, selectedAccount: selectedAcco 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 b1 = new cliProgress.SingleBar({ - format: 'Transferring Funds |' + colors.cyan('{bar}') + '| {percentage}%', - barCompleteChar: '\u2588', - barIncompleteChar: '\u2591', - hideCursor: true - }) - 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) - const tx = await entropy.substrate.tx.balances.transferAllowDeath( - recipientAddress, - BigInt(formattedAmount), + startProgress() + const transferStatus = await transfer( + entropy, + { + from: entropy.keyring.accounts.registration.pair, + to: recipientAddress, + amount: formattedAmount + } ) + if (transferStatus.isFinalized) stopProgress() - await tx.signAndSend (entropy.keyring.accounts.registration.pair, ({ status }) => { - // initialize the bar - defining payload token "speed" with the default value "N/A" - b1.start(300, 0, { - speed: "N/A" - }); - // update values - const interval = setInterval(() => { - b1.increment() - }, 100) - if (status.isFinalized) { - b1.stop() - clearInterval(interval) - print( - `\nTransaction successful: Sent ${amount} to ${recipientAddress}` - ) - print('\nPress enter to return to main menu'); - } - }) - return; - + print( + `\nTransaction successful: Sent ${amount} to ${recipientAddress}` + ) + print('\nPress enter to return to main menu') } catch (error) { + stopProgress() console.error('ERR:::', error); - - } -} +} \ No newline at end of file diff --git a/src/flows/entropyTransfer/transfer.ts b/src/flows/entropyTransfer/transfer.ts new file mode 100644 index 00000000..d8663eee --- /dev/null +++ b/src/flows/entropyTransfer/transfer.ts @@ -0,0 +1,34 @@ +import Entropy from "@entropyxyz/sdk"; +import { TransferOptions } from "./types"; + +export async function transfer (entropy: Entropy, 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: + 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 + + msg = `${section}.${name}: ${docs.join(' ')}` + } else { + // Other, CannotLookup, BadOrigin, no extra info + msg = dispatchError.toString() + } + return reject(Error(msg)) + } + + if (status.isFinalized) resolve(status) + }) + }) +} \ No newline at end of file diff --git a/src/flows/entropyTransfer/types.ts b/src/flows/entropyTransfer/types.ts new file mode 100644 index 00000000..c23b6b77 --- /dev/null +++ b/src/flows/entropyTransfer/types.ts @@ -0,0 +1,7 @@ +// @ts-ignore +import { Pair } from '@entropyxyz/sdk/keys' +export interface TransferOptions { + from: Pair + to: string + amount: bigint +} \ No newline at end of file diff --git a/src/types/index.ts b/src/types/index.ts index 791cdb5c..26295245 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,39 +1,3 @@ -export interface EntropyConfig { - accounts: EntropyAccountConfig[] - endpoints: { dev: string; 'test-net': string } - 'migration-version': string -} -export interface EntropyAccountConfig { - name: string - address: string - data: EntropyAccountData -} - -export interface EntropyAccountData { - debug?: boolean - seed: string - admin?: EntropyAccount - registration?: EntropyAccount - deviceKey?: EntropyAccount - programDev?: EntropyAccount -} - -export interface EntropyAccount { - seed: string - path: string - address: string - verifyingKeys?: string[] - userContext?: EntropyAccountContextType - used?: boolean -} - -export enum EntropyAccountContextType { - programDev = 'PROGRAM_DEV_KEY', - registration = 'ADMIN_KEY', - deviceKey = 'CONSUMER_KEY', - undefined = 'MIXED_KEY', -} - export interface EntropyTuiOptions { dev: boolean endpoint: string diff --git a/tests/manage-accounts.test.ts b/tests/manage-accounts.test.ts index a4540652..b2931e8d 100644 --- a/tests/manage-accounts.test.ts +++ b/tests/manage-accounts.test.ts @@ -1,4 +1,4 @@ -import { EntropyAccountConfig, EntropyConfig } from 'src/types' +import { EntropyAccountConfig, EntropyConfig } from 'src/config/types' import test from 'tape' import { charlieStashAddress, charlieStashSeed } from './testing-utils/constants' import { listAccounts } from 'src/flows/manage-accounts/list' diff --git a/tests/testing-utils/setup-test.ts b/tests/testing-utils/setup-test.ts index ff70fa3b..b2e3206f 100644 --- a/tests/testing-utils/setup-test.ts +++ b/tests/testing-utils/setup-test.ts @@ -2,6 +2,8 @@ import { Test } from 'tape' import { Entropy, wasmGlobalsReady } from '@entropyxyz/sdk' // @ts-ignore import { spinNetworkUp, spinNetworkDown, } from "@entropyxyz/sdk/testing" +// @ts-ignore +import Keyring from '@entropyxyz/sdk/keys' import { initializeEntropy } from '../../src/common/initializeEntropy' import * as config from '../../src/config' @@ -38,9 +40,11 @@ export async function setupTest (t: Test, opts?: SetupTestOpts): Promise<{ entro // TODO: remove this after new SDK is published await sleep(process.env.GITHUB_WORKSPACE ? 30_000 : 5_000) - + // To follow the same way we initiate entropy within the cli we must go through the same process of creating an initial keyring + // as done in src/flows/manage-accounts/new-key.ts + const keyring = new Keyring({ seed, debug: true }) const entropy = await initializeEntropy({ - keyMaterial: { seed, debug: true }, + keyMaterial: keyring.getAccount(), endpoint: 'ws://127.0.0.1:9944', configPath }) diff --git a/tests/transfer.test.ts b/tests/transfer.test.ts new file mode 100644 index 00000000..309862f7 --- /dev/null +++ b/tests/transfer.test.ts @@ -0,0 +1,82 @@ +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 { + makeSeed, + promiseRunner, + sleep, + spinNetworkUp, + 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' + +const networkType = 'two-nodes' + +test('Transfer', async (t) => { + /* Setup */ + const run = promiseRunner(t) + await run('wasm', wasmGlobalsReady()) + await run('network up', spinNetworkUp(networkType)) + // this gets called after all tests are run + t.teardown(async () => { + await entropy.close() + await charlieEntropy.close() + await spinNetworkDown(networkType).catch((error) => + console.error('Error while spinning network down', error.message) + ) + }) + await sleep(process.env.GITHUB_WORKSPACE ? 30_000 : 5_000) + + const naynaySeed = makeSeed() + 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', }) + await run('entropy ready', entropy.ready) + await run('charlie ready', charlieEntropy.ready) + + const recipientAddress = entropy.keyring.accounts.registration.address + + // Check Balance of new account + let naynayBalance = await run( + 'getBalance (naynay)', + getBalance(entropy, recipientAddress) + ) + + t.equal(naynayBalance, 0, 'naynay is broke') + + let charlieBalance = await run( + 'getBalance (charlieStash)', + getBalance(entropy, charlieStashAddress) + ) + + t.equal(charlieBalance, 1e21, 'charlie got bank') + + const transferStatus = await run( + 'transfer', + transfer(entropy, { + from: charlieEntropy.keyring.accounts.registration.pair, + to: recipientAddress, + amount: BigInt(1000 * 10e10) + }) + ) + // @ts-ignore + t.true(transferStatus?.isFinalized, 'Funds transferred successfully') + + // Re-Check Balance of new account + naynayBalance = await run( + 'getBalance (naynay)', + getBalance(entropy, recipientAddress) + ) + + t.equal(naynayBalance, 1000 * 10e10, 'naynay is rolling in it!') + + t.end() +}) \ No newline at end of file