Skip to content

Commit

Permalink
Mixmix/programs deploy (#206)
Browse files Browse the repository at this point in the history
  • Loading branch information
mixmix authored Aug 1, 2024
1 parent 621e0fb commit df36f05
Show file tree
Hide file tree
Showing 8 changed files with 180 additions and 89 deletions.
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
49 changes: 49 additions & 0 deletions src/flows/programs/deploy.ts
Original file line number Diff line number Diff line change
@@ -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))
}
86 changes: 41 additions & 45 deletions src/flows/programs/index.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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")
}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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'
}

Expand All @@ -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)
Expand All @@ -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

Expand Down
9 changes: 8 additions & 1 deletion src/flows/programs/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,11 @@ export interface ViewProgramsParams {
export interface RemoveProgramParams {
programPointer: string
verifyingKey: string
}
}

export interface DeployProgramParams {
bytecodePath: string,
configurationSchemaPath?: string
auxillaryDataSchemaPath?: string
// TODO: confirm which of these are optional
}
98 changes: 59 additions & 39 deletions tests/programs.test.ts
Original file line number Diff line number Diff line change
@@ -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()
})
})
6 changes: 3 additions & 3 deletions tests/register.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
})
Expand All @@ -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()
})
8 changes: 8 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down

0 comments on commit df36f05

Please sign in to comment.