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')