diff --git a/CHANGELOG.md b/CHANGELOG.md index 248b0cbd..a6e9a9bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,8 +29,19 @@ Version header format: `[version] Name - year-month-day (entropy-core compatibil - new: './src/flows/user-program-management/remove.ts' - service file for removing user program pure function ### Changed + - folder name for user programs to match the kebab-case style for folder namespace - updated SDK version to v0.2.3 +- merged user + dev program folders + tests + + +### Broke + +- deploying programs with TUI + - now requires a `*.wasm` file for `bytecode` + - now requires a `*.json` file path for `configurationSchema` + - now requires a `*.json` file path for `auxillaryDataSchema` + ## [0.0.3] Blade - 2024-07-17 (entropy-core compatibility: 0.2.0) diff --git a/package.json b/package.json index 85d34d31..ec790c82 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,6 @@ "homepage": "https://github.com/entropyxyz/cli#readme", "dependencies": { "@entropyxyz/sdk": "^0.2.3", - "@types/node": "^20.12.12", "ansi-colors": "^4.1.3", "cli-progress": "^3.12.0", "commander": "^12.0.0", @@ -61,6 +60,7 @@ "@types/cli-progress": "^3", "@types/inquirer": "^9.0.2", "@types/node": "^20.12.12", + "@types/tape": "^5.6.4", "@typescript-eslint/eslint-plugin": "^6.21.0", "@typescript-eslint/parser": "^6.21.0", "eslint": "^8.56.0", diff --git a/src/flows/programs/deploy.ts b/src/flows/programs/deploy.ts new file mode 100644 index 00000000..e71b8ff2 --- /dev/null +++ b/src/flows/programs/deploy.ts @@ -0,0 +1,49 @@ +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/index.ts b/src/flows/programs/index.ts index 02b1048d..db8eadee 100644 --- a/src/flows/programs/index.ts +++ b/src/flows/programs/index.ts @@ -1,8 +1,8 @@ import Entropy from "@entropyxyz/sdk" -import { readFileSync } from "fs" import inquirer from "inquirer" -import * as util from "@polkadot/util" +import { u8aToHex } from "@polkadot/util" +import { deployProgram } from "./deploy"; import { addProgram } from "./add"; import { viewPrograms } from "./view"; import { removeProgram } from "./remove"; @@ -35,11 +35,11 @@ export async function userPrograms ({ accounts, selectedAccount: selectedAccount }, ]) - const entropy = await initializeEntropy({ + const entropy = await initializeEntropy({ keyMaterial: selectedAccount.data, endpoint }) - + if (!entropy.registrationManager?.signer?.pair) { throw new Error("Keys are undefined") } @@ -85,13 +85,13 @@ export async function userPrograms ({ accounts, selectedAccount: selectedAccount case "Add a Program to My List": { try { const { programPointerToAdd, programConfigJson } = await inquirer.prompt(addQuestions) - + const encoder = new TextEncoder() const byteArray = encoder.encode(programConfigJson) - const programConfigHex = util.u8aToHex(byteArray) - + const programConfigHex = u8aToHex(byteArray) + await addProgram(entropy, { programPointer: programPointerToAdd, programConfig: programConfigHex }) - + print("Program added successfully.") } catch (error) { console.error(error.message) @@ -123,8 +123,8 @@ export async function devPrograms ({ accounts, selectedAccount: selectedAccountA const selectedAccount = getSelectedAccount(accounts, selectedAccountAddress) const choices = { - "Deploy": deployProgram, - "Get Owned Programs": getOwnedPrograms, + "Deploy": deployProgramTUI, + "Get Owned Programs": getOwnedProgramsTUI, "Exit to Main Menu": () => 'exit' } @@ -146,47 +146,43 @@ export async function devPrograms ({ accounts, selectedAccount: selectedAccountA await flow(entropy, selectedAccount) } -async function deployProgram (entropy: Entropy, account: any) { - const deployQuestions = [ +async function deployProgramTUI (entropy: Entropy, account: any) { + const answers = await inquirer.prompt([ { type: "input", - name: "programPath", - message: "Please provide the path to your program:", + name: "bytecodePath", + message: "Please provide the path to your program binary:", + validate (input: string) { + return input.endsWith('.wasm') + ? true + : 'program binary must be .wasm file' + } }, { - type: "confirm", - name: "hasConfig", - message: "Does your program have a configuration file?", - default: false, + type: "input", + name: "configurationSchemaPath", + message: "Please provide the path to your configuration schema:", + validate (input: string) { + return input.endsWith('.json') + ? true + : 'configuration schema must be a .json file' + } }, - ] - - const deployAnswers = await inquirer.prompt(deployQuestions) - const userProgram = readFileSync(deployAnswers.programPath) - - let programConfig = "" - - if (deployAnswers.hasConfig) { - const configAnswers = await inquirer.prompt([ - { - type: "input", - name: "config", - message: "Please provide your program configuration as a JSON string:", - }, - ]) - - // Convert JSON string to bytes and then to hex - const encoder = new TextEncoder() - const byteArray = encoder.encode(configAnswers.config) - programConfig = util.u8aToHex(new Uint8Array(byteArray)) - } + { + type: "input", + name: "auxillaryDataSchemaPath", + message: "Please provide the path to your auxillary data schema:", + validate (input: string) { + return input.endsWith('.json') + ? true + : 'configuration schema must be a .json file' + } + }, + ]) try { - // Deploy the program with config - const pointer = await entropy.programs.dev.deploy( - userProgram, - programConfig - ) + const pointer = await deployProgram(entropy, answers) + print("Program deployed successfully with pointer:", pointer) } catch (deployError) { console.error("Deployment failed:", deployError) @@ -195,7 +191,7 @@ async function deployProgram (entropy: Entropy, account: any) { print("Deploying from account:", account.address) } -async function getOwnedPrograms (entropy: Entropy, account: any) { +async function getOwnedProgramsTUI (entropy: Entropy, account: any) { const userAddress = account.address if (!userAddress) return diff --git a/src/flows/programs/types.ts b/src/flows/programs/types.ts index c4274bf9..0086414c 100644 --- a/src/flows/programs/types.ts +++ b/src/flows/programs/types.ts @@ -11,4 +11,11 @@ export interface ViewProgramsParams { export interface RemoveProgramParams { programPointer: string verifyingKey: string -} \ No newline at end of file +} + +export interface DeployProgramParams { + bytecodePath: string, + configurationSchemaPath?: string + auxillaryDataSchemaPath?: string + // TODO: confirm which of these are optional +} diff --git a/tests/programs.test.ts b/tests/programs.test.ts index e3700bb8..978c67b4 100644 --- a/tests/programs.test.ts +++ b/tests/programs.test.ts @@ -1,57 +1,77 @@ import test from 'tape' -import { readFileSync } from 'node:fs' 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 { AddProgramParams } from '../src/flows/programs/types' +import { deployProgram } from '../src/flows/programs/deploy' const networkType = 'two-nodes' test('programs', async t => { const { run, entropy } = await setupTest(t, { seed: charlieStashSeed, networkType }) - await run('charlie stash register', entropy.register()) - const noopProgram: any = readFileSync( - new URL('./programs/program_noop.wasm', import.meta.url) - ) - const newPointer = await run( - 'deploy', - entropy.programs.dev.deploy(noopProgram) - ) - - const noopProgramInstance: AddProgramParams = { - programPointer: newPointer, - programConfig: '', - } - - t.test('Add Program', async ap => { - const runAp = promiseRunner(ap) - - const programsBeforeAdd = await runAp('get programs initial', entropy.programs.get(entropy.programs.verifyingKey)) - ap.equal(programsBeforeAdd.length, 1, 'charlie has 1 program') - await runAp('adding program', addProgram(entropy, noopProgramInstance)) - const programsAfterAdd = await runAp('get programs after add', entropy.programs.get(entropy.programs.verifyingKey)) - ap.equal(programsAfterAdd.length, 2, 'charlie has 2 programs') - ap.end() + await run('register', entropy.register()) // TODO: consider removing this in favour of just testing add + + let programPointer1 + + t.test('programs - deploy', async t => { + const run = promiseRunner(t) + + programPointer1 = await run ( + 'deploy!', + deployProgram(entropy, { + bytecodePath: './tests/programs/program_noop.wasm' + }) + ) + + t.end() }) - t.test('Remove Program', async rp => { - const runRp = promiseRunner(rp) - const programsBeforeRemove = await runRp('get programs initial', entropy.programs.get(entropy.programs.verifyingKey)) - - rp.equal(programsBeforeRemove.length, 2, 'charlie has 2 programs') - await runRp('removing noop program', removeProgram(entropy, { programPointer: newPointer, verifyingKey: entropy.programs.verifyingKey })) - const programsAfterRemove = await runRp('get programs initial', entropy.programs.get(entropy.programs.verifyingKey)) - rp.equal(programsAfterRemove.length, 1, 'charlie has 1 less program') - rp.end() + const getPrograms = () => viewPrograms(entropy, { verifyingKey: entropy.programs.verifyingKey }) + const verifyingKey = entropy.programs.verifyingKey + + t.test('programs - add', async t => { + const run = promiseRunner(t) + + const programsBeforeAdd = await run('get programs initial', getPrograms()) + t.equal(programsBeforeAdd.length, 1, 'charlie has 1 program') + + await run( + 'adding program', + addProgram(entropy, { programPointer: programPointer1, programConfig: '' }) + ) + const programsAfterAdd = await run('get programs after add', getPrograms()) + t.equal(programsAfterAdd.length, 2, 'charlie has 2 programs') + + t.end() }) - t.test('View Program', async vp => { - const runVp = promiseRunner(vp) - const programs = await runVp('get charlie programs', viewPrograms(entropy, { verifyingKey: entropy.programs.verifyingKey })) + t.test('programs - remove', async t => { + const run = promiseRunner(t) + + const programsBeforeRemove = await run('get programs initial', getPrograms()) + t.equal(programsBeforeRemove.length, 2, 'charlie has 2 programs') + + await run( + 'removing noop program', + removeProgram(entropy, { programPointer: programPointer1, verifyingKey }) + ) + const programsAfterRemove = await run('get programs initial', getPrograms()) + t.equal(programsAfterRemove.length, 1, 'charlie has 1 less program') + + t.end() + }) + + t.test('programs - view', async t => { + const run = promiseRunner(t) + + const programs = await run( + 'get charlie programs', + viewPrograms(entropy, { verifyingKey }) + ) + + t.equal(programs.length, 1, 'charlie has 1 program') - vp.equal(programs.length, 1, 'charlie has 1 program') - vp.end() + t.end() }) }) diff --git a/tests/register.test.ts b/tests/register.test.ts index c1f4b800..f093df6f 100644 --- a/tests/register.test.ts +++ b/tests/register.test.ts @@ -13,7 +13,7 @@ test('Regsiter - Default Program', async (t) => { const fullAccount = entropy.keyring.getAccount() - t.equal(verifyingKey, fullAccount.registration.verifyingKeys[0], 'verifying key matches key added to registration account') + t.equal(verifyingKey, fullAccount?.registration?.verifyingKeys?.[0], 'verifying key matches key added to registration account') t.end() }) @@ -37,8 +37,8 @@ test('Register - Barebones Program', async t => { ) const fullAccount = entropy.keyring.getAccount() - - t.equal(verifyingKey, fullAccount.registration.verifyingKeys[1], 'verifying key matches key added to registration account') + + t.equal(verifyingKey, fullAccount?.registration?.verifyingKeys?.[1], 'verifying key matches key added to registration account') t.end() }) diff --git a/yarn.lock b/yarn.lock index 7d7d46bb..53eef8ff 100644 --- a/yarn.lock +++ b/yarn.lock @@ -973,6 +973,14 @@ resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.8.tgz#8268a8c57a3e4abd25c165ecd36237db7948a55e" integrity sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ== +"@types/tape@^5.6.4": + version "5.6.4" + resolved "https://registry.yarnpkg.com/@types/tape/-/tape-5.6.4.tgz#efae4202493043457b1900dceb4808c8f04c7d8f" + integrity sha512-EmL4fJpZyByNCkupLLcJhneqcnT+rQUG5fWKNCsZyBK1x7nUuDTwwEerc4biEMZgvSK2+FXr775aLeXhKXK4Yw== + dependencies: + "@types/node" "*" + "@types/through" "*" + "@types/through@*": version "0.0.33" resolved "https://registry.yarnpkg.com/@types/through/-/through-0.0.33.tgz#14ebf599320e1c7851e7d598149af183c6b9ea56"