diff --git a/CHANGELOG.md b/CHANGELOG.md index dcb81a4d..fc89113f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,10 +13,19 @@ Version header format: `[version] Name - year-month-day (entropy-core compatibil ## [UNRELEASED] ### Added +- new: 'src/flows/register/register.ts' - service file for register pure function +- new: './src/flows/manage-accounts/helpers/create-account.ts' - new helper file to house the pure function used to create a new entropy account +- update: './tests/manage-accounts.test.ts' - added test for create account pure function +- update: './src/common/utils.ts' - removed isValidSubstrateAddress and imported the method in from the sdk - new: './tests/user-program-management.test.ts' - unit tests file for user program management flows - added test for adding a user program + - added test for viewing a user program - added test for removing a user program - new: './src/flows/user-program-management/add.ts' - service file for adding user program pure function +- new: 'src/flows/user-program-management/helpers/questions.ts' - utility helper file for all the different inquirer questions used +- new: 'src/flows/user-program-management/types.ts' - user program management method types +- 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 ### Fixed diff --git a/package.json b/package.json index 37e6dd40..c7ac98c4 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "commander": "^12.0.0", "env-paths": "^3.0.0", "inquirer": "8.0.0", + "is-base64": "^1.1.0", "lodash.clonedeep": "^4.5.0", "mkdirp": "^3.0.1", "typescript": "^4.8.4", diff --git a/src/common/logger.ts b/src/common/logger.ts index 436f3b4b..7f112143 100644 --- a/src/common/logger.ts +++ b/src/common/logger.ts @@ -1,6 +1,7 @@ import envPaths from 'env-paths' import { join } from 'path' import * as winston from 'winston' +import { replacer } from './utils' import { maskPayload } from './masking' import { EntropyLoggerOptions } from 'src/types' @@ -38,7 +39,7 @@ export class EntropyLogger { winston.format.splat(), // Uses safe-stable-stringify to finalize full object message as string // (prevents circular references from crashing) - winston.format.json(), + winston.format.json({ replacer }), ); if (isTesting) { @@ -122,5 +123,4 @@ export class EntropyLogger { stack, }); } - -} \ No newline at end of file +} diff --git a/src/common/utils.ts b/src/common/utils.ts index b7e03451..f8eb76ba 100644 --- a/src/common/utils.ts +++ b/src/common/utils.ts @@ -1,5 +1,3 @@ -import { decodeAddress, encodeAddress } from "@polkadot/keyring" -import { hexToU8a, isHex } from "@polkadot/util" import { Buffer } from 'buffer' import { EntropyAccountConfig } from "../config/types" @@ -9,7 +7,7 @@ export function stripHexPrefix (str: string): string { } export function replacer (key, value) { - if(value instanceof Uint8Array ){ + if (value instanceof Uint8Array) { return Buffer.from(value).toString('base64') } else return value @@ -48,16 +46,6 @@ export function buf2hex (buffer: ArrayBuffer): string { return Buffer.from(buffer).toString("hex") } -export function isValidSubstrateAddress (address: any) { - try { - encodeAddress(isHex(address) ? hexToU8a(address) : decodeAddress(address)) - - return true - } catch (error) { - return false - } -} - export function accountChoices (accounts: EntropyAccountConfig[]) { return accounts .map((account) => ({ diff --git a/src/config/index.ts b/src/config/index.ts index e502b780..4ea72eec 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -3,7 +3,7 @@ import { readFileSync } from 'node:fs' import { mkdirp } from 'mkdirp' import { join, dirname } from 'path' import envPaths from 'env-paths' - +import isBase64 from 'is-base64' import allMigrations from './migrations' import { replacer } from 'src/common/utils' @@ -58,15 +58,35 @@ function noop () {} export async function get (configPath = CONFIG_PATH) { const configBuffer = await readFile(configPath) - return JSON.parse(configBuffer.toString()) + return deserialize(configBuffer.toString()) } export function getSync (configPath = CONFIG_PATH) { const configBuffer = readFileSync(configPath, 'utf8') - return JSON.parse(configBuffer) + return deserialize(configBuffer) } export async function set (config = {}, configPath = CONFIG_PATH) { await mkdirp(dirname(configPath)) - await writeFile(configPath, JSON.stringify(config, replacer)) + await writeFile(configPath, serialize(config)) +} + +function serialize (config) { + return JSON.stringify(config, replacer, 2) +} + +function deserialize (config) { + function reviver (key, value) { + if ( + isBase64(value, { allowEmpty: false }) && + value.length >= 32 + // NOTE: we have to check length so we don't accidentally transform + // user simple string that are valid base64 like "registration" + ) { + return Uint8Array.from(Buffer.from(value, 'base64')) + } + else return value + } + + return JSON.parse(config, reviver) } diff --git a/src/config/migrations/02.ts b/src/config/migrations/02.ts new file mode 100644 index 00000000..c84b7c78 --- /dev/null +++ b/src/config/migrations/02.ts @@ -0,0 +1,43 @@ +export const version = 2 + +const targetKeys = new Set(['secretKey', 'publicKey', 'addressRaw']) + +export function migrate (data = {}) { + if (!isObject(data)) return data + if (isUI8A(data)) return data + + const initial = isArray(data) ? [] : {} + + return Object.entries(data).reduce((acc, [key, value]) => { + if (targetKeys.has(key) && !isUI8A(value)) { + acc[key] = objToUI8A(value) + } + else { + acc[key] = migrate(value) + } + + return acc + }, initial) +} + + +function isObject (thing) { + return typeof thing === 'object' +} + +function isArray (thing) { + return Array.isArray(thing) +} + +function isUI8A (thing) { + return thing instanceof Uint8Array +} + + +function objToUI8A (obj) { + const bytes = Object.keys(obj) + .sort((a, b) => Number(a) > Number(b) ? 1 : -1) + .map(arrayIndex => obj[arrayIndex]) + + return new Uint8Array(bytes) +} diff --git a/src/config/migrations/index.ts b/src/config/migrations/index.ts index 5d1f18b7..541608cd 100644 --- a/src/config/migrations/index.ts +++ b/src/config/migrations/index.ts @@ -1,9 +1,11 @@ import * as migration00 from './00' import * as migration01 from './01' +import * as migration02 from './02' const migrations = [ migration00, migration01, + migration02, ] export default migrations diff --git a/src/flows/index.ts b/src/flows/index.ts index d86504ca..2187a5eb 100644 --- a/src/flows/index.ts +++ b/src/flows/index.ts @@ -1,6 +1,6 @@ export { entropyFaucet } from './entropyFaucet' export { checkBalance } from './balance' -export { register } from './register' +export { entropyRegister } from './register' export { userPrograms } from './user-program-management' export { devPrograms } from './DeployPrograms' export { sign } from './sign' diff --git a/src/flows/manage-accounts/cli.ts b/src/flows/manage-accounts/cli.ts index 34c6ec57..390f6fe1 100644 --- a/src/flows/manage-accounts/cli.ts +++ b/src/flows/manage-accounts/cli.ts @@ -1,12 +1,8 @@ import * as config from '../../config' +import { listAccounts } from './list' export async function cliListAccounts () { const storedConfig = await config.get() - return storedConfig.accounts - .map(account => ({ - name: account.name, - address: account.address, - verifyingKeys: account?.data?.admin?.verifyingKeys - })) + return listAccounts(storedConfig) } diff --git a/src/flows/manage-accounts/helpers/create-account.ts b/src/flows/manage-accounts/helpers/create-account.ts new file mode 100644 index 00000000..0e9a66fa --- /dev/null +++ b/src/flows/manage-accounts/helpers/create-account.ts @@ -0,0 +1,25 @@ +// @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/helpers/import-key.ts b/src/flows/manage-accounts/helpers/import-account.ts similarity index 97% rename from src/flows/manage-accounts/helpers/import-key.ts rename to src/flows/manage-accounts/helpers/import-account.ts index 50667d01..4000c60c 100644 --- a/src/flows/manage-accounts/helpers/import-key.ts +++ b/src/flows/manage-accounts/helpers/import-account.ts @@ -33,7 +33,7 @@ export const importQuestions = [ { type: 'input', name: 'path', - meesage: 'derivation path:', + message: 'derivation path:', default: 'none', when: ({ importKey }) => importKey }, diff --git a/src/flows/manage-accounts/index.ts b/src/flows/manage-accounts/index.ts index 413f2d16..300ceb1b 100644 --- a/src/flows/manage-accounts/index.ts +++ b/src/flows/manage-accounts/index.ts @@ -1,13 +1,13 @@ import inquirer from 'inquirer' import { print } from '../../common/utils' -import { newKey } from './new-key' +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': newKey, + 'Create/Import Account': newAccount, 'Select Account': selectAccount, 'List Accounts': (config) => { try { diff --git a/src/flows/manage-accounts/list.ts b/src/flows/manage-accounts/list.ts index 63ae5995..74b43db8 100644 --- a/src/flows/manage-accounts/list.ts +++ b/src/flows/manage-accounts/list.ts @@ -1,10 +1,12 @@ +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) => ({ + return accountsArray.map((account: EntropyAccountConfig) => ({ name: account.name, address: account.address, verifyingKeys: account?.data?.admin?.verifyingKeys diff --git a/src/flows/manage-accounts/new-key.ts b/src/flows/manage-accounts/new-account.ts similarity index 71% rename from src/flows/manage-accounts/new-key.ts rename to src/flows/manage-accounts/new-account.ts index f0a781a6..86209d85 100644 --- a/src/flows/manage-accounts/new-key.ts +++ b/src/flows/manage-accounts/new-account.ts @@ -1,14 +1,12 @@ import inquirer from 'inquirer' import { randomAsHex } from '@polkadot/util-crypto' -// @ts-ignore -import Keyring from '@entropyxyz/sdk/keys' -import { importQuestions } from './helpers/import-key' +import { importQuestions } from './helpers/import-account' // import * as passwordFlow from '../password' import { print } from '../../common/utils' +import { createAccount } from './helpers/create-account' import { EntropyLogger } from 'src/common/logger' -export async function newKey ({ accounts }, logger: EntropyLogger) { - const FLOW_CONTEXT = 'MANAGE_ACCOUNTS::NEW_KEY' +export async function newAccount ({ accounts }, logger: EntropyLogger) { accounts = Array.isArray(accounts) ? accounts : [] const questions = [ @@ -64,23 +62,7 @@ export async function newKey ({ accounts }, logger: EntropyLogger) { seed = importKey ? secret : randomAsHex(32) } - 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 - - const newAccount = { - name: name, - address: admin.address, - // TODO: replace with data: encryptedData once pasword input is added back - data, - } + const newAccount = await createAccount({ name, seed, path }, logger) print(`New account:\n{\n\tname: ${newAccount.name}\n\taddress: ${newAccount.address}\n}`) diff --git a/src/flows/register/index.ts b/src/flows/register/index.ts index d15d6f88..05247578 100644 --- a/src/flows/register/index.ts +++ b/src/flows/register/index.ts @@ -2,8 +2,9 @@ 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 register (storedConfig, options, logger: EntropyLogger) { +export async function entropyRegister (storedConfig, options, logger: EntropyLogger) { const FLOW_CONTEXT = 'REGISTER' const { accounts, selectedAccount: selectedFromConfig } = storedConfig; const { endpoint } = options @@ -22,41 +23,19 @@ export async function register (storedConfig, options, logger: EntropyLogger) { // // Setting default to default key proxy program // default: '0x0000000000000000000000000000000000000000000000000000000000000000' // }]) - //@ts-ignore: + // @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, ) - let verifyingKey: string + try { - // For now we are forcing users to only register with the default info before having to format the config for them - // verifyingKey = await entropy.register({ - // programDeployer: entropy.keyring.accounts.registration.address, - // programData: [{ - // program_pointer: programPointer, - // program_config: '0x', - // }] - // }) - verifyingKey = await entropy.register() - if (verifyingKey) { - 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 } - } + 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) { - console.error('error', error); - if (!verifyingKey) { - logger.debug('Pruning Registration', FLOW_CONTEXT) - try { - const tx = await 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); - } - } + logger.error('There was a problem registering', error) } } diff --git a/src/flows/register/register.ts b/src/flows/register/register.ts new file mode 100644 index 00000000..8b2c3941 --- /dev/null +++ b/src/flows/register/register.ts @@ -0,0 +1,28 @@ +import Entropy from "@entropyxyz/sdk"; +import { RegsiterParams } from "./types"; +import { print } from "src/common/utils"; + +export async function register (entropy: Entropy, params?: RegsiterParams): 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 new file mode 100644 index 00000000..df77d563 --- /dev/null +++ b/src/flows/register/types.ts @@ -0,0 +1,5 @@ +export interface RegsiterParams { + programModAddress?: string + // TODO: Export ProgramInstance type from sdk + programData?: any +} \ No newline at end of file diff --git a/src/flows/user-program-management/helpers/questions.ts b/src/flows/user-program-management/helpers/questions.ts index 38c7590a..3a0a00d2 100644 --- a/src/flows/user-program-management/helpers/questions.ts +++ b/src/flows/user-program-management/helpers/questions.ts @@ -1,3 +1,5 @@ +import Entropy from "@entropyxyz/sdk"; + export const addQuestions = [ { type: "input", @@ -27,4 +29,12 @@ export const getProgramPointerInput = [ name: "programPointer", message: "Enter the program pointer you wish to remove:", }, -] \ No newline at end of file +] + +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/user-program-management/helpers/utils.ts b/src/flows/user-program-management/helpers/utils.ts new file mode 100644 index 00000000..ff0a3fe6 --- /dev/null +++ b/src/flows/user-program-management/helpers/utils.ts @@ -0,0 +1,11 @@ +import { print } from "src/common/utils" + +export function displayPrograms (programs): void { + programs.forEach((program, index) => { + print( + `${index + 1}. Pointer: ${ + program.program_pointer + }, Config: ${JSON.stringify(program.program_config)}` + ) + }) +} \ No newline at end of file diff --git a/src/flows/user-program-management/index.ts b/src/flows/user-program-management/index.ts index ac5983b6..aa52964e 100644 --- a/src/flows/user-program-management/index.ts +++ b/src/flows/user-program-management/index.ts @@ -4,7 +4,9 @@ import { initializeEntropy } from "../../common/initializeEntropy" import { getSelectedAccount, print } from "../../common/utils" import { EntropyLogger } from "src/common/logger"; import { addProgram } from "./add"; -import { addQuestions, getProgramPointerInput } from "./helpers/questions"; +import { viewPrograms } from "./view"; +import { addQuestions, getProgramPointerInput, verifyingKeyQuestion } from "./helpers/questions"; +import { displayPrograms } from "./helpers/utils"; import { removeProgram } from "./remove"; let verifyingKey: string; @@ -38,35 +40,21 @@ export async function userPrograms ({ accounts, selectedAccount: selectedAccount throw new Error("Keys are undefined") } - const verifyingKeyQuestion = [{ - type: 'list', - name: 'verifyingKey', - message: 'Select the key to proceeed', - choices: entropy.keyring.accounts.registration.verifyingKeys, - default: entropy.keyring.accounts.registration.verifyingKeys[0] - }] - switch (actionChoice.action) { case "View My Programs": { try { if (!verifyingKey && entropy.keyring.accounts.registration.verifyingKeys.length) { - ({ verifyingKey } = await inquirer.prompt(verifyingKeyQuestion)) + ({ verifyingKey } = await inquirer.prompt(verifyingKeyQuestion(entropy))) } else { print('You currently have no verifying keys, please register this account to generate the keys') break } - const programs = await entropy.programs.get(verifyingKey) + const programs = await viewPrograms(entropy, { verifyingKey }) if (programs.length === 0) { print("You currently have no programs set.") } else { print("Your Programs:") - programs.forEach((program, index) => { - print( - `${index + 1}. Pointer: ${ - program.program_pointer - }, Config: ${JSON.stringify(program.program_config)}` - ) - }) + displayPrograms(programs) } } catch (error) { console.error(error.message) @@ -109,7 +97,7 @@ export async function userPrograms ({ accounts, selectedAccount: selectedAccount case "Remove a Program from My List": { try { if (!verifyingKey) { - ({ verifyingKey } = await inquirer.prompt(verifyingKeyQuestion)) + ({ verifyingKey } = await inquirer.prompt(verifyingKeyQuestion(entropy))) } const { programPointer: programPointerToRemove } = await inquirer.prompt(getProgramPointerInput) await removeProgram(entropy, { programPointer: programPointerToRemove, verifyingKey }) diff --git a/src/flows/user-program-management/types.ts b/src/flows/user-program-management/types.ts index 849df9d5..c4274bf9 100644 --- a/src/flows/user-program-management/types.ts +++ b/src/flows/user-program-management/types.ts @@ -4,6 +4,10 @@ export interface AddProgramParams { verifyingKey?: string } +export interface ViewProgramsParams { + verifyingKey: string +} + export interface RemoveProgramParams { programPointer: string verifyingKey: string diff --git a/src/flows/user-program-management/view.ts b/src/flows/user-program-management/view.ts new file mode 100644 index 00000000..6cff1e9f --- /dev/null +++ b/src/flows/user-program-management/view.ts @@ -0,0 +1,6 @@ +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/tui.ts b/src/tui.ts index 872fd5ee..90553819 100644 --- a/src/tui.ts +++ b/src/tui.ts @@ -19,7 +19,7 @@ export default function tui (options: EntropyTuiOptions) { const choices = { 'Manage Accounts': flows.manageAccounts, 'Balance': flows.checkBalance, - 'Register': flows.register, + 'Register': flows.entropyRegister, 'Sign': flows.sign, 'Transfer': flows.entropyTransfer, 'Deploy Program': flows.devPrograms, diff --git a/tests/config.test.ts b/tests/config.test.ts index 767f214c..0c835a36 100644 --- a/tests/config.test.ts +++ b/tests/config.test.ts @@ -2,6 +2,7 @@ import test from 'tape' import { writeFile } from 'node:fs/promises' import migrations from '../src/config/migrations' import { migrateData, init, get, set } from '../src/config' +import { replacer } from '../src/common/utils' // used to ensure unique test ids let id = Date.now() @@ -65,10 +66,17 @@ test('config - migrateData', async t => { t.end() }) +const makeKey = () => new Uint8Array( + Array(32).fill(0).map((_, i) => i * 2 + 1) +) + test('config - get', async t => { const configPath = makeTmpPath() - const config = { boop: 'doop' } - await writeFile(configPath, JSON.stringify(config)) + const config = { + boop: 'doop', + secretKey: makeKey() + } + await writeFile(configPath, JSON.stringify(config, replacer)) const result = await get(configPath) t.deepEqual(result, config, 'get works') @@ -83,11 +91,14 @@ test('config - get', async t => { test('config - set', async t => { const configPath = makeTmpPath() - const config = { dog: true } + const config = { + dog: true, + secretKey: makeKey() + } await set(config, configPath) const actual = await get(configPath) - t.deepEqual(actual, config, 'set works') + t.deepEqual(config, actual, 'set works') t.end() }) @@ -146,3 +157,110 @@ test('config - init (migration)', async t => { t.end() }) + + +test('config/migrattions/02', t => { + const initial = JSON.parse( + '{"accounts":[{"name":"Mix","address":"5GCaN3fcL6vAQQKamHzVSorwv2XqtM3WcxosCLd9JqGVrtS8","data":{"debug":true,"seed":"0xc4c466182b86ff1f4a16548df79c5808ab9bcde87c22c27938ac9aabc4300840","admin":{"address":"5GCaN3fcL6vAQQKamHzVSorwv2XqtM3WcxosCLd9JqGVrtS8","type":"registration","verifyingKeys":["0x03b225d2032e1dbff26316cc8b7d695b3386400d30ce004c1b42e2c28bcd834039"],"userContext":"ADMIN_KEY","seed":"0xc4c466182b86ff1f4a16548df79c5808ab9bcde87c22c27938ac9aabc4300840","path":"","pair":{"address":"5GCaN3fcL6vAQQKamHzVSorwv2XqtM3WcxosCLd9JqGVrtS8","addressRaw":{"0":182,"1":241,"2":171,"3":246,"4":239,"5":100,"6":192,"7":41,"8":49,"9":32,"10":10,"11":84,"12":241,"13":225,"14":183,"15":152,"16":164,"17":182,"18":176,"19":244,"20":39,"21":237,"22":74,"23":225,"24":250,"25":244,"26":187,"27":129,"28":97,"29":222,"30":33,"31":116},"isLocked":false,"meta":{},"publicKey":{"0":182,"1":241,"2":171,"3":246,"4":239,"5":100,"6":192,"7":41,"8":49,"9":32,"10":10,"11":84,"12":241,"13":225,"14":183,"15":152,"16":164,"17":182,"18":176,"19":244,"20":39,"21":237,"22":74,"23":225,"24":250,"25":244,"26":187,"27":129,"28":97,"29":222,"30":33,"31":116},"type":"sr25519","secretKey":{"0":120,"1":247,"2":1,"3":38,"4":246,"5":195,"6":0,"7":49,"8":84,"9":240,"10":226,"11":144,"12":66,"13":172,"14":130,"15":168,"16":237,"17":74,"18":121,"19":243,"20":49,"21":217,"22":208,"23":70,"24":160,"25":220,"26":125,"27":114,"28":230,"29":17,"30":254,"31":71,"32":158,"33":68,"34":133,"35":24,"36":119,"37":34,"38":46,"39":154,"40":85,"41":62,"42":178,"43":69,"44":206,"45":217,"46":132,"47":184,"48":8,"49":219,"50":89,"51":165,"52":189,"53":106,"54":6,"55":51,"56":112,"57":76,"58":42,"59":157,"60":146,"61":130,"62":203,"63":241}},"used":true},"registration":{"address":"5GCaN3fcL6vAQQKamHzVSorwv2XqtM3WcxosCLd9JqGVrtS8","type":"registration","verifyingKeys":["0x03b225d2032e1dbff26316cc8b7d695b3386400d30ce004c1b42e2c28bcd834039"],"userContext":"ADMIN_KEY","seed":"0xc4c466182b86ff1f4a16548df79c5808ab9bcde87c22c27938ac9aabc4300840","path":"","pair":{"address":"5GCaN3fcL6vAQQKamHzVSorwv2XqtM3WcxosCLd9JqGVrtS8","addressRaw":{"0":182,"1":241,"2":171,"3":246,"4":239,"5":100,"6":192,"7":41,"8":49,"9":32,"10":10,"11":84,"12":241,"13":225,"14":183,"15":152,"16":164,"17":182,"18":176,"19":244,"20":39,"21":237,"22":74,"23":225,"24":250,"25":244,"26":187,"27":129,"28":97,"29":222,"30":33,"31":116},"isLocked":false,"meta":{},"publicKey":{"0":182,"1":241,"2":171,"3":246,"4":239,"5":100,"6":192,"7":41,"8":49,"9":32,"10":10,"11":84,"12":241,"13":225,"14":183,"15":152,"16":164,"17":182,"18":176,"19":244,"20":39,"21":237,"22":74,"23":225,"24":250,"25":244,"26":187,"27":129,"28":97,"29":222,"30":33,"31":116},"type":"sr25519","secretKey":{"0":120,"1":247,"2":1,"3":38,"4":246,"5":195,"6":0,"7":49,"8":84,"9":240,"10":226,"11":144,"12":66,"13":172,"14":130,"15":168,"16":237,"17":74,"18":121,"19":243,"20":49,"21":217,"22":208,"23":70,"24":160,"25":220,"26":125,"27":114,"28":230,"29":17,"30":254,"31":71,"32":158,"33":68,"34":133,"35":24,"36":119,"37":34,"38":46,"39":154,"40":85,"41":62,"42":178,"43":69,"44":206,"45":217,"46":132,"47":184,"48":8,"49":219,"50":89,"51":165,"52":189,"53":106,"54":6,"55":51,"56":112,"57":76,"58":42,"59":157,"60":146,"61":130,"62":203,"63":241}},"used":true},"deviceKey":{"address":"5GCaN3fcL6vAQQKamHzVSorwv2XqtM3WcxosCLd9JqGVrtS8","type":"deviceKey","verifyingKeys":["0x03b225d2032e1dbff26316cc8b7d695b3386400d30ce004c1b42e2c28bcd834039"],"userContext":"CONSUMER_KEY","seed":"0xc4c466182b86ff1f4a16548df79c5808ab9bcde87c22c27938ac9aabc4300840","path":"","pair":{"address":"5GCaN3fcL6vAQQKamHzVSorwv2XqtM3WcxosCLd9JqGVrtS8","addressRaw":{"0":182,"1":241,"2":171,"3":246,"4":239,"5":100,"6":192,"7":41,"8":49,"9":32,"10":10,"11":84,"12":241,"13":225,"14":183,"15":152,"16":164,"17":182,"18":176,"19":244,"20":39,"21":237,"22":74,"23":225,"24":250,"25":244,"26":187,"27":129,"28":97,"29":222,"30":33,"31":116},"isLocked":false,"meta":{},"publicKey":{"0":182,"1":241,"2":171,"3":246,"4":239,"5":100,"6":192,"7":41,"8":49,"9":32,"10":10,"11":84,"12":241,"13":225,"14":183,"15":152,"16":164,"17":182,"18":176,"19":244,"20":39,"21":237,"22":74,"23":225,"24":250,"25":244,"26":187,"27":129,"28":97,"29":222,"30":33,"31":116},"type":"sr25519","secretKey":{"0":120,"1":247,"2":1,"3":38,"4":246,"5":195,"6":0,"7":49,"8":84,"9":240,"10":226,"11":144,"12":66,"13":172,"14":130,"15":168,"16":237,"17":74,"18":121,"19":243,"20":49,"21":217,"22":208,"23":70,"24":160,"25":220,"26":125,"27":114,"28":230,"29":17,"30":254,"31":71,"32":158,"33":68,"34":133,"35":24,"36":119,"37":34,"38":46,"39":154,"40":85,"41":62,"42":178,"43":69,"44":206,"45":217,"46":132,"47":184,"48":8,"49":219,"50":89,"51":165,"52":189,"53":106,"54":6,"55":51,"56":112,"57":76,"58":42,"59":157,"60":146,"61":130,"62":203,"63":241}},"used":true}}}],"selectedAccount":"5GCaN3fcL6vAQQKamHzVSorwv2XqtM3WcxosCLd9JqGVrtS8","endpoints":{"dev":"ws://127.0.0.1:9944","test-net":"wss://testnet.entropy.xyz"},"migration-version":1}' + ) + + const migrated = migrations[2].migrate(initial) + + // console.log(JSON.stringify(migrated, replacer, 2)) + // => { + // "accounts": [ + // { + // "name": "Mix", + // "address": "5GCaN3fcL6vAQQKamHzVSorwv2XqtM3WcxosCLd9JqGVrtS8", + // "data": { + // "debug": true, + // "seed": "0xc4c466182b86ff1f4a16548df79c5808ab9bcde87c22c27938ac9aabc4300840", + // "admin": { + // "address": "5GCaN3fcL6vAQQKamHzVSorwv2XqtM3WcxosCLd9JqGVrtS8", + // "type": "registration", + // "verifyingKeys": [ + // "0x03b225d2032e1dbff26316cc8b7d695b3386400d30ce004c1b42e2c28bcd834039" + // ], + // "userContext": "ADMIN_KEY", + // "seed": "0xc4c466182b86ff1f4a16548df79c5808ab9bcde87c22c27938ac9aabc4300840", + // "path": "", + // "pair": { + // "address": "5GCaN3fcL6vAQQKamHzVSorwv2XqtM3WcxosCLd9JqGVrtS8", + // "addressRaw": "tvGr9u9kwCkxIApU8eG3mKS2sPQn7Urh+vS7gWHeIXQ=", + // "isLocked": false, + // "meta": {}, + // "publicKey": "tvGr9u9kwCkxIApU8eG3mKS2sPQn7Urh+vS7gWHeIXQ=", + // "type": "sr25519", + // "secretKey": "ePcBJvbDADFU8OKQQqyCqO1KefMx2dBGoNx9cuYR/keeRIUYdyIumlU+skXO2YS4CNtZpb1qBjNwTCqdkoLL8Q==" + // }, + // "used": true + // }, + // "registration": { + // "address": "5GCaN3fcL6vAQQKamHzVSorwv2XqtM3WcxosCLd9JqGVrtS8", + // "type": "registration", + // "verifyingKeys": [ + // "0x03b225d2032e1dbff26316cc8b7d695b3386400d30ce004c1b42e2c28bcd834039" + // ], + // "userContext": "ADMIN_KEY", + // "seed": "0xc4c466182b86ff1f4a16548df79c5808ab9bcde87c22c27938ac9aabc4300840", + // "path": "", + // "pair": { + // "address": "5GCaN3fcL6vAQQKamHzVSorwv2XqtM3WcxosCLd9JqGVrtS8", + // "addressRaw": "tvGr9u9kwCkxIApU8eG3mKS2sPQn7Urh+vS7gWHeIXQ=", + // "isLocked": false, + // "meta": {}, + // "publicKey": "tvGr9u9kwCkxIApU8eG3mKS2sPQn7Urh+vS7gWHeIXQ=", + // "type": "sr25519", + // "secretKey": "ePcBJvbDADFU8OKQQqyCqO1KefMx2dBGoNx9cuYR/keeRIUYdyIumlU+skXO2YS4CNtZpb1qBjNwTCqdkoLL8Q==" + // }, + // "used": true + // }, + // "deviceKey": { + // "address": "5GCaN3fcL6vAQQKamHzVSorwv2XqtM3WcxosCLd9JqGVrtS8", + // "type": "deviceKey", + // "verifyingKeys": [ + // "0x03b225d2032e1dbff26316cc8b7d695b3386400d30ce004c1b42e2c28bcd834039" + // ], + // "userContext": "CONSUMER_KEY", + // "seed": "0xc4c466182b86ff1f4a16548df79c5808ab9bcde87c22c27938ac9aabc4300840", + // "path": "", + // "pair": { + // "address": "5GCaN3fcL6vAQQKamHzVSorwv2XqtM3WcxosCLd9JqGVrtS8", + // "addressRaw": "tvGr9u9kwCkxIApU8eG3mKS2sPQn7Urh+vS7gWHeIXQ=", + // "isLocked": false, + // "meta": {}, + // "publicKey": "tvGr9u9kwCkxIApU8eG3mKS2sPQn7Urh+vS7gWHeIXQ=", + // "type": "sr25519", + // "secretKey": "ePcBJvbDADFU8OKQQqyCqO1KefMx2dBGoNx9cuYR/keeRIUYdyIumlU+skXO2YS4CNtZpb1qBjNwTCqdkoLL8Q==" + // }, + // "used": true + // } + // } + // } + // ], + // "selectedAccount": "5GCaN3fcL6vAQQKamHzVSorwv2XqtM3WcxosCLd9JqGVrtS8", + // "endpoints": { + // "dev": "ws://127.0.0.1:9944", + // "test-net": "wss://testnet.entropy.xyz" + // }, + // "migration-version": 1 + // } + + const targetKeys = ['addressRaw', 'publicKey', 'secretKey'] + + // @ts-ignore + migrated.accounts.forEach(account => { + return Object.keys(account.data).forEach(subAccount => { + if (typeof account.data[subAccount] !== 'object') return + + t.true( + targetKeys.every(targetKey => { + return account.data[subAccount].pair[targetKey] instanceof Uint8Array + }), + `migrated: ${subAccount}` + ) + }) + }) + + t.end() +}) diff --git a/tests/manage-accounts.test.ts b/tests/manage-accounts.test.ts index b2931e8d..76a8db2e 100644 --- a/tests/manage-accounts.test.ts +++ b/tests/manage-accounts.test.ts @@ -1,7 +1,18 @@ -import { EntropyAccountConfig, EntropyConfig } from 'src/config/types' +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 test from 'tape' -import { charlieStashAddress, charlieStashSeed } from './testing-utils/constants' +// @ts-ignore +import { spinNetworkUp, spinNetworkDown, } from "@entropyxyz/sdk/testing" +import { EntropyAccountConfig, EntropyConfig } from 'src/config/types' import { listAccounts } from 'src/flows/manage-accounts/list' +import { createAccount } from 'src/flows/manage-accounts/helpers/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 = { @@ -43,5 +54,28 @@ test('List Accounts', async t => { 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() }) \ No newline at end of file diff --git a/tests/register.test.ts b/tests/register.test.ts new file mode 100644 index 00000000..bbd21618 --- /dev/null +++ b/tests/register.test.ts @@ -0,0 +1,44 @@ +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 regsitration account') + + t.end() +}) + +test('Register - Barebones Program', async t => { + const { run, entropy } = await setupTest(t, { networkType, seed: charlieStashSeed }) + const dummyProgram: any = readFileSync( + 'src/programs/template_barebones.wasm' + ) + const pointer = await run( + 'deploy program', + entropy.programs.dev.deploy(dummyProgram) + ) + + const verifyingKey = await run( + 'register', + entropy.register({ + programDeployer: 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 regsitration account') + + t.end() +}) diff --git a/tests/testing-utils/constants.ts b/tests/testing-utils/constants.ts index 0593c40a..14f7374d 100644 --- a/tests/testing-utils/constants.ts +++ b/tests/testing-utils/constants.ts @@ -1,5 +1,7 @@ -export const charlieStashAddress = - '5Ck5SLSHYac6WFt5UZRSsdJjwmpSZq85fd5TRNAdZQVzEAPT' - +export const charlieStashAddress = '5Ck5SLSHYac6WFt5UZRSsdJjwmpSZq85fd5TRNAdZQVzEAPT' export const charlieStashSeed = '0x66256c4e2f90e273bf387923a9a7860f2e9f47a1848d6263de512f7fb110fc08' + +export const charlieSeed = + '0xbc1ede780f784bb6991a585e4f6e61522c14e1cae6ad0895fb57b9a205a8f938' +export const charlieAddress = '5FLSigC9HGRKVhB9FiEo4Y3koPsNmBmLJbpXg2mp1hXcS59Y' \ No newline at end of file diff --git a/tests/user-program-management.test.ts b/tests/user-program-management.test.ts index 782d6ff9..1fe1733e 100644 --- a/tests/user-program-management.test.ts +++ b/tests/user-program-management.test.ts @@ -1,8 +1,9 @@ import test from 'tape' import { readFileSync } from 'node:fs' -import { charlieStashSeed, promiseRunner, setupTest } from './testing-utils' +import { promiseRunner, charlieStashSeed, setupTest } from './testing-utils' import { AddProgramParams } from 'src/flows/user-program-management/types' import { addProgram } from 'src/flows/user-program-management/add' +import { viewPrograms } from 'src/flows/user-program-management/view' import { removeProgram } from 'src/flows/user-program-management/remove' const networkType = 'two-nodes' @@ -44,4 +45,12 @@ test('User Program Management', async t => { rp.equal(programsAfterRemove.length, 1, 'charlie has 1 less program') rp.end() }) + + t.test('View Program', async vp => { + const runVp = promiseRunner(vp) + const programs = await runVp('get charlie programs', viewPrograms(entropy, { verifyingKey: entropy.programs.verifyingKey })) + + vp.equal(programs.length, 1, 'charlie has 1 program') + vp.end() + }) }) \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 7a9863ff..d18f41bc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2378,6 +2378,11 @@ is-arrayish@^0.3.1: resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03" integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ== +is-base64@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-base64/-/is-base64-1.1.0.tgz#8ce1d719895030a457c59a7dcaf39b66d99d56b4" + integrity sha512-Nlhg7Z2dVC4/PTvIFkgVVNvPHSO2eR/Yd0XzhGiXCXEvWnptXlXa/clQ8aePPiMuxEGcWfzWbGw2Fe3d+Y3v1g== + is-bigint@^1.0.1: version "1.0.4" resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3"