Skip to content

Commit

Permalink
ISSUE#405 dev programs rework (#409)
Browse files Browse the repository at this point in the history
<!-- Provide a brief description of the changes made in this PR -->

## Related Issue(s)
<!-- Link to the issue(s) that this PR addresses -->

closes #405 


## Testing
<!-- Describe how you tested the changes -->

- [x] Unit tests added/updated
- [x] Integration tests added/updated

## Checklist
<!-- Confirm that the following items are true and correct: -->

- [x] I have performed a self-review of my code.
- [ ] I have commented my code.
- [ ] I have updated documentation, where necessary.
  • Loading branch information
frankiebee authored Oct 2, 2024
2 parents d80f73a + 099dd3f commit e2cf2ac
Show file tree
Hide file tree
Showing 7 changed files with 231 additions and 50 deletions.
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
62 changes: 47 additions & 15 deletions src/programs/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,27 @@ 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.
* @property {number} refCounter - The reference count for the program.
*/

// 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
}
Expand Down Expand Up @@ -55,7 +58,7 @@ export default class ProgramDev extends ExtrinsicBaseClass {
* @returns {Promise<string[]>} A promise that resolves to the list of program pointers
*/

async get (address: string): Promise<any> {
async getByDeployer (address: string): Promise<any> {
const programs = await this.substrate.query.programs.ownedPrograms(address);
return programs.toHuman()
}
Expand All @@ -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<ProgramInfo>} A promise that resolves to the program information.
* @returns {Promise<ProgramInterface>} A promise that resolves to the program information.
*/

async getProgramInfo (pointer: string): Promise<ProgramInfo> {
async get (pointer: string): Promise<ProgramInterface> {
// 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)
}

/**
Expand All @@ -95,12 +99,13 @@ export default class ProgramDev extends ExtrinsicBaseClass {
): Promise<HexString> {
// 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, {
Expand All @@ -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<void>} A promise that resolves when the program is removed.
*/
Expand All @@ -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,
}
}
}
17 changes: 9 additions & 8 deletions src/registration/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
}
}
19 changes: 13 additions & 6 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
121 changes: 121 additions & 0 deletions tests/programs-dev.test.ts
Original file line number Diff line number Diff line change
@@ -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()
})
Loading

0 comments on commit e2cf2ac

Please sign in to comment.