Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mixmix/programs deploy #206

Merged
merged 8 commits into from
Aug 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -63,6 +62,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()
})
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apologies I moved lines around. Essentially

  • dropped the (IMO) unnecessary use of other variables for t and run ("know your scope" is sufficient?)
  • broke up lines of tests into "phases" which are like [action, query?, assertionOfChange, '\n']
    • this is totally a personal quirk of mine that I do to make tests easy to read for me 😬


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 @@ -1007,6 +1007,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
Loading