diff --git a/CHANGELOG.md b/CHANGELOG.md index 8364877d..eb34658d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,11 +13,17 @@ Version header format: `[version] Name - year-month-day (entropy-core compatibil ## [UNRELEASED] ### Added + - `configurationSchema` & `auxiliaryDataSchema` to `ProgramInterface` + ### Fixed ### Changed - + - util function rename: `hex2buf` -> `hexStringToBuffer` + - massive changes to `entropy.programs.dev`: (Look at documentation for more details) + - `getProgramInfo` -> `get` and bytecode is returned as a buffer. not a Uint8buffer to match what was deployed + - you now get owned programs for an address using `getByDeployer`. + - interface name change: `ProgramInfo` -> `ProgramInterface` ### Broke ### Dev diff --git a/src/programs/dev.ts b/src/programs/dev.ts index 974a6515..7ae3181b 100644 --- a/src/programs/dev.ts +++ b/src/programs/dev.ts @@ -2,14 +2,14 @@ import ExtrinsicBaseClass from '../extrinsic' import { ApiPromise } from '@polkadot/api' import { Signer } from '../keys/types/internal' import { SubmittableExtrinsic } from '@polkadot/api/types' -import { hex2buf, stripHexPrefix } from '../utils' +import { hexStringToBuffer, stripHexPrefix, hexStringToJSON } from '../utils' import * as util from '@polkadot/util' import { HexString } from '../keys/types/json' /** * Represents program information. * - * @interface ProgramInfo + * @interface ProgramInterface * @property {ArrayBuffer} bytecode - The bytecode of the program. * @property {unknown} [interfaceDescription] - Optional. The configuration interface of the program. * @property {string} deployer - The address of the deployer of the program. @@ -17,9 +17,12 @@ import { HexString } from '../keys/types/json' */ // interfaceDescription needs better design and another type other than 'unknown' -export interface ProgramInfo { +export interface ProgramInterface { bytecode: ArrayBuffer - interfaceDescription?: unknown + configurationSchema: unknown + auxiliaryDataSchema: unknown + // not quite supported yet + // oracleDataPointer?: [] deployer: string refCounter: number } @@ -55,7 +58,7 @@ export default class ProgramDev extends ExtrinsicBaseClass { * @returns {Promise} A promise that resolves to the list of program pointers */ - async get (address: string): Promise { + async getByDeployer (address: string): Promise { const programs = await this.substrate.query.programs.ownedPrograms(address); return programs.toHuman() } @@ -64,16 +67,17 @@ export default class ProgramDev extends ExtrinsicBaseClass { * Retrieves program information using a program pointer. * * @param {string} pointer - The program pointer to fetch the program bytecode. - * @returns {Promise} A promise that resolves to the program information. + * @returns {Promise} A promise that resolves to the program information. */ - async getProgramInfo (pointer: string): Promise { + async get (pointer: string): Promise { // fetch program bytecode using the program pointer at the specific block hash + if (pointer.length <= 48) throw new Error('pointer length is less then or equal to 48. are you using an address?') const responseOption = await this.substrate.query.programs.programs(pointer) const programInfo = responseOption.toJSON() - return this.#formatProgramInfo(programInfo) + return this.#formatProgramInterface(programInfo) } /** @@ -95,12 +99,13 @@ export default class ProgramDev extends ExtrinsicBaseClass { ): Promise { // converts program and configurationInterface into a palatable format const formatedConfig = JSON.stringify(configurationSchema) + const formatedAuxData = JSON.stringify(auxiliaryDataSchema) // programModKey is the caller of the extrinsic const tx: SubmittableExtrinsic<'promise'> = this.substrate.tx.programs.setProgram( util.u8aToHex(new Uint8Array(program)), // new program formatedConfig, // config schema - auxiliaryDataSchema, // auxilary config schema + formatedAuxData, // auxilary config schema [] // oracleDataPointer // oracle data pointer ) const record = await this.sendAndWaitFor(tx, { @@ -115,6 +120,8 @@ export default class ProgramDev extends ExtrinsicBaseClass { /** * Removes an existing program. * + * (removing a program is currently unstable and may not remove the program from chain as intended.) + * * @param {string | Uint8Array} programHash - The hash of the program to remove. * @returns {Promise} A promise that resolves when the program is removed. */ @@ -129,18 +136,43 @@ export default class ProgramDev extends ExtrinsicBaseClass { }) } + /** + * @internal + * + * trys to parse schema as a json. If fails because it's not a json returns original schema. throws for any other reason + * + * @param {any} programInfo - The program information in JSON format. + * @returns {unknown} - The formatted program information. + */ + #tryParseSchema (schema: any): unknown { + try { + return hexStringToJSON(schema) + } catch (e) { + if (e.message.includes('is not valid JSON')) return schema + throw e + } + } + /** * @internal * * Formats program information. * - * @param {ProgramInfoJSON} programInfo - The program information in JSON format. - * @returns {ProgramInfo} - The formatted program information. + * @param {ProgramInterfaceJSON} programInfo - The program information in JSON format. + * @returns {ProgramInterface} - The formatted program information. */ - #formatProgramInfo (programInfo): ProgramInfo { - const { interfaceDescription, deployer, refCounter } = programInfo - const bytecode = hex2buf(stripHexPrefix(programInfo.bytecode)) // Convert hex string to ArrayBuffer - return { interfaceDescription, deployer, refCounter, bytecode } + #formatProgramInterface (programInfo): ProgramInterface { + const { deployer, refCounter } = programInfo + const bytecode = hexStringToBuffer(stripHexPrefix(programInfo.bytecode)) // Convert hex string to ArrayBuffer + const configurationSchema = this.#tryParseSchema(programInfo.configurationSchema) + const auxiliaryDataSchema = this.#tryParseSchema(programInfo.auxiliaryDataSchema) + return { + configurationSchema, + auxiliaryDataSchema, + deployer, + refCounter, + bytecode, + } } } diff --git a/src/registration/index.ts b/src/registration/index.ts index b7d5f810..e6080a6c 100644 --- a/src/registration/index.ts +++ b/src/registration/index.ts @@ -92,14 +92,7 @@ export default class RegistrationManager extends ExtrinsicBaseClass { const registerTx = this.substrate.tx.registry.register( programDeployer, keyVisibility, - programData.map((programInfo) => { - return { - program_pointer: programInfo.program_pointer, - program_config: Array.from( - Buffer.from(JSON.stringify(programInfo.program_config)) - ), - } - }) + programData.map(this.#formatProgramInfo) ) // @ts-ignore: next line // Send the registration transaction and wait for the result. @@ -160,4 +153,12 @@ export default class RegistrationManager extends ExtrinsicBaseClass { }) }) } + + #formatProgramInfo (programInfo): ProgramInstance { + const program: ProgramInstance = { program_pointer: programInfo.program_pointer } + if (programInfo.program_config) program.program_config = Array.from( + Buffer.from(JSON.stringify(programInfo.program_config)) + ) + return program + } } diff --git a/src/utils/index.ts b/src/utils/index.ts index b094b5fc..d2eee35c 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -140,12 +140,19 @@ export function toHex (str: any) { * @returns {ArrayBuffer} The ArrayBuffer representation of the hexadecimal string. */ -export function hex2buf (hex: string): ArrayBuffer { - const bytes = new Uint8Array(Math.ceil(hex.length / 2)) - for (let i = 0; i < bytes.length; i++) { - bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16) - } - return bytes.buffer +export function hexStringToBuffer (hex: string): ArrayBuffer { + return Buffer.from(stripHexPrefix(hex), 'hex') +} + +/** + * Converts a hexadecimal string to a JSON object. + * + * @param {string} hex - The hexadecimal string to convert. + * @returns {unknown} The ArrayBuffer representation of the hexadecimal string. + */ + +export function hexStringToJSON (hex: string): ArrayBuffer { + return JSON.parse(hexStringToBuffer(hex).toString()) } export function hexStringToUint8Array (hex: string): Uint8Array { diff --git a/tests/programs-dev.test.ts b/tests/programs-dev.test.ts new file mode 100644 index 00000000..2324db78 --- /dev/null +++ b/tests/programs-dev.test.ts @@ -0,0 +1,121 @@ +import test from 'tape' +import { readFileSync } from 'fs' +import Entropy, { wasmGlobalsReady } from '../src' +import Keyring from '../src/keys' + +import { + promiseRunner, + spinNetworkUp, + charlieStashAddress, + spinNetworkDown, + createTestAccount, +} from './testing-utils' + +const networkType = 'two-nodes' + +test('Programs#dev: all methods', async (t) => { + const run = promiseRunner(t) + await run('network up', spinNetworkUp(networkType)) + + await run('wasm', wasmGlobalsReady()) + + const entropy = await createTestAccount() + + t.teardown(async () => { + await entropy.close() + await spinNetworkDown(networkType) + }) + + + // wait for entropy to be ready + await run( + 'entropy ready', + entropy.ready + ) + + + // deploy + const noopProgram: any = readFileSync( + './tests/testing-utils/program_noop.wasm', + + ) + + const configSchema = { + type: 'object', + properties: { + noop_param: { type: 'string' } + } + } + const auxDataSchema = { + type: 'object', + properties: { + noop_param: { type: 'number' } + } + } + const newPointer = await run( + 'deploy', + entropy.programs.dev.deploy(noopProgram, configSchema, auxDataSchema) + ) + console.log('newPointer:', newPointer) + const programsDeployed = await run( + 'get deployed programs', + entropy.programs.dev.getByDeployer(entropy.keyring.accounts.programDev.address) + ) + t.deepEqual( + programsDeployed, + [newPointer], + 'charlie has 1 program deployed' + ) + + // Helpful error for old usage + try { + await entropy.programs.dev.get(entropy.keyring.accounts.programDev.address) + t.fail('entropy.programs.dev.get(entropy.keyring.accounts.programDev.address) should have failed') + } catch (e) { + t.ok(e.message.includes('pointer length is less then or equal to 48. are you using an address?'), 'should error when using an address') + } + + const noopProgramOnChain = await run( + 'get a specific program', + entropy.programs.dev.get(newPointer) + ) + + t.deepEqual( + noopProgramOnChain.bytecode, + noopProgram, + 'bytecode on chain should match what was deployed' + ) + t.deepEqual( + noopProgramOnChain.configurationSchema, + configSchema, + 'configurationSchema on chain should match what was deployed' + ) + t.deepEqual( + noopProgramOnChain.auxiliaryDataSchema, + auxDataSchema, + 'auxiliaryDataSchema on chain should match what was deployed' + ) + + run( + 'remove noopProgram', + entropy.programs.dev.remove(newPointer) + ) + + const programsDeployedAfterRemove = await run( + 'get deployed programs', + entropy.programs.dev.getByDeployer(entropy.keyring.accounts.programDev.address) + ) + // the removal of a program has failed + // the removing of a program is questionable + // functionality to begin with so ive commented this out + // for now but this needs digging + // see issue https://github.com/entropyxyz/sdk/issues/414 + // t.equal( + // programsDeployedAfterRemove.length, + // 0, + // 'charlie has no deployed programs' + // ) + + + t.end() +}) diff --git a/tests/programs.test.ts b/tests/programs.test.ts index e80dadee..7ffd24b1 100644 --- a/tests/programs.test.ts +++ b/tests/programs.test.ts @@ -7,13 +7,12 @@ import { promiseRunner, spinNetworkUp, charlieStashSeed, - charlieStashAddress, spinNetworkDown, } from './testing-utils' const networkType = 'two-nodes' -test('Programs: GET', async (t) => { +test('Programs: account programs get', async (t) => { const run = promiseRunner(t) await run('network up', spinNetworkUp(networkType)) t.teardown(async () => { @@ -34,16 +33,13 @@ test('Programs: GET', async (t) => { keyring, endpoint: 'ws://127.0.0.1:9944', }) - - // register - const verifyingKeyFromRegistration = await run('register', entropy.register()) - - t.equal( - verifyingKeyFromRegistration, - entropy.keyring.accounts.registration.verifyingKeys[0], - 'verifyingKeys match after registration' + // wait for entropy to be ready + await run( + 'entropy ready', + entropy.ready ) + // deploy const noopProgram: any = readFileSync( './tests/testing-utils/program_noop.wasm' @@ -54,21 +50,36 @@ test('Programs: GET', async (t) => { entropy.programs.dev.deploy(noopProgram) ) - const programsDeployed = await run( - 'get deployed programs', - entropy.programs.dev.get(charlieStashAddress) + // register + const registerOpts = { + programData: [{ + program_pointer: newPointer + }], + } + + const verifyingKeyFromRegistration = await run('register', entropy.register(registerOpts)) + + t.equal( + verifyingKeyFromRegistration, + entropy.keyring.accounts.registration.verifyingKeys[0], + 'verifyingKeys match after registration' + ) + + const programsForAccount = await run( + 'get programs for verifyingKey', + entropy.programs.get(verifyingKeyFromRegistration) ) t.equal( - programsDeployed.length, + programsForAccount.length, 1, - 'charlie has deployed 1 program' + programsDeployed + 'charlie entropy account has 1 program' + programsForAccount ) t.equal( - programsDeployed[0], + programsForAccount[0].program_pointer, newPointer, - 'program in list matches new pointer: ' + newPointer + ' = ' + programsDeployed[0] + 'program in list matches new pointer: ' + newPointer + ' = ' + programsForAccount[0].program_pointer ) t.end() diff --git a/tests/testing-utils/index.ts b/tests/testing-utils/index.ts index a4cfc7b8..f631bb13 100644 --- a/tests/testing-utils/index.ts +++ b/tests/testing-utils/index.ts @@ -15,14 +15,17 @@ export * from './constants' export * from './readKey' export async function createTestAccount( - entropy: Entropy, - seed = charlieStashSeed + seed = charlieStashSeed, + endpoint = 'ws://127.0.0.1:9944' ) { await wasmGlobalsReady() const keyring = new Keyring({ seed } as KeyMaterial) + const entropy = new Entropy({ + keyring, + endpoint, + }) - entropy = new Entropy({ keyring }) await entropy.ready.catch((err) => { console.log('createTestAccount failed: ', err) throw err