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

JS bindings for entropy client #897

Closed
wants to merge 29 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
385df60
Dependencies for wasm bindgen
ameba23 Jun 18, 2024
f07b10c
Fix for errors when building on wasm
ameba23 Jun 18, 2024
a49f246
Add wasm bindings to sign and register
ameba23 Jun 18, 2024
de8a167
Beginning of test
ameba23 Jun 18, 2024
a0c52c1
Makefile for building js package
ameba23 Jun 18, 2024
56cc99d
Add dev mode to makefile
ameba23 Jun 19, 2024
db7c9c0
HashingAlgorithm enum needs extra serde stuff when on wasm
ameba23 Jun 19, 2024
e707be6
More features exposed on wasm
ameba23 Jun 19, 2024
4404efb
Small fix
ameba23 Jun 19, 2024
b436f3f
Fix registration
ameba23 Jun 19, 2024
818cf6f
Add test script readme
ameba23 Jun 19, 2024
0bf5626
Clippy
ameba23 Jun 19, 2024
861fe03
Write readme to stdout if no command recognised
ameba23 Jun 19, 2024
3ab2d6f
Allow an array of programs to be passed in
ameba23 Jun 20, 2024
a39407d
Display accounts correctly
ameba23 Jun 20, 2024
61f21ce
Standard formatting for JS tests
ameba23 Jun 20, 2024
a8e7d44
Allow program hash and aux data to be passed in
ameba23 Jun 20, 2024
afd7f97
Changelog
ameba23 Jun 20, 2024
d92fa61
Merge master
ameba23 Jul 9, 2024
464e9d0
Merge master
ameba23 Jul 18, 2024
18586fd
Clippy
ameba23 Jul 18, 2024
34aaf6e
Add js readme
ameba23 Jul 29, 2024
d0615ef
Add to js readme
ameba23 Jul 29, 2024
eb1be80
Merge master
ameba23 Jul 29, 2024
0299b38
Minor grammatical and spelling updates. (#967)
johnnymatthews Jul 30, 2024
36d5422
Fixes following update
ameba23 Jul 30, 2024
d3199af
Add signature type
ameba23 Jul 30, 2024
b83ea22
Update readme
ameba23 Jul 30, 2024
1b971b1
Update readme
ameba23 Jul 30, 2024
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ At the moment this project **does not** adhere to
accounts (e.g development accounts like `//Alice`).

### Added
- JS bindings for entropy client ([#897](https://github.com/entropyxyz/entropy-core/pull/897))
- Add a way to change program modification account ([#843](https://github.com/entropyxyz/entropy-core/pull/843))
- Add support for `--mnemonic-file` and `THRESHOLD_SERVER_MNEMONIC` ([#864](https://github.com/entropyxyz/entropy-core/pull/864))
- Add validator helpers to cli ([#870](https://github.com/entropyxyz/entropy-core/pull/870))
Expand Down
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 16 additions & 3 deletions crates/client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,11 @@ hex ={ version="0.4.3", optional=true }
anyhow ="1.0.86"

# Only for the browser
js-sys={ version="0.3.68", optional=true }
tokio ="1.39"
js-sys ={ version="0.3.68", optional=true }
wasm-bindgen-futures={ version="0.4.42", optional=true }
wasm-bindgen ={ version="0.2.92", optional=true }
wasm-bindgen-derive ={ version="0.3", optional=true }
tokio ="1.39"

[dev-dependencies]
serial_test ="3.1.1"
Expand Down Expand Up @@ -64,4 +67,14 @@ full-client=[
"dep:hex",
]
full-client-native=["full-client", "entropy-protocol/server"]
full-client-wasm=["full-client", "entropy-protocol/wasm", "dep:js-sys"]
full-client-wasm=[
"full-client",
"entropy-protocol/wasm",
"dep:js-sys",
"wasm-bindgen",
"wasm-bindgen-futures",
"wasm-bindgen-derive",
]

[lib]
crate-type=["cdylib", "rlib"]
26 changes: 26 additions & 0 deletions crates/client/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Builds a JS Module for nodejs with glue for the compiled WASM.
build-nodejs ::
wasm-pack build --target nodejs --scope "entropyxyz" . --no-default-features -F full-client-wasm
cp js-README.md ./pkg/README.md
cp ../../LICENSE ./pkg/

# Builds a JS Module for web with glue for the compiled WASM.
build-web ::
wasm-pack build --target web --scope "entropyxyz" . --no-default-features -F full-client-wasm
cp js-README.md ./pkg/README.md
cp ../../LICENSE ./pkg/

# Another build option for compiling to webpack, builds a typescript library around the WASM for use
# with npm.
build-bundler ::
wasm-pack build --target bundler --scope "entropyxyz" . --no-default-features -F full-client-wasm
cp js-README.md ./pkg/README.md
cp ../../LICENSE ./pkg/

# Builds a JS Module for nodejs in dev mode (no optimisations)
build-nodejs-dev ::
wasm-pack build --dev --target nodejs --scope "entropyxyz" . --no-default-features -F full-client-wasm

# Cleans out build artifacts.
clean ::
rm -rf pkg/ nodejs-test/node_modules/
175 changes: 175 additions & 0 deletions crates/client/js-README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
# `entropy-client`

This is JS bindings for a basic client library for [Entropy](https://entropy.xyz).

For a full-featured client library, you probably want the [SDK](https://www.npmjs.com/package/@entropyxyz/sdk).

## A note on using this on NodeJS

This expects to have access to the browser WebSockets API, which is not present on NodeJS. To use
this on NodeJS, you must have the dependency [`ws`](https://www.npmjs.com/package/ws) as a property
of the `global` object like so:

```js
Object.assign(global, { WebSocket: require('ws') })
```

This is tested with `ws` version `^8.14.2`.

## Usage

```js
const client = require('entropy-client')
```

### `EntropyApi`

To interact with an Entropy chain node, you need to instantiate the `EntropyApi` object, giving the
chain endpoint URL as a string to the constructor:

```js
const api = await new client.EntropyApi('wss://testnet.entropy.xyz')
```

### `Sr25519Pair`

An account on the Entropy chain is represented by an sr25519 keypair. To instantiate the
`Sr25519Pair` object, you give the constructor a string. This may be either a BIP39 mnemonic
or a name from which to derive a keypair prefixed with `'//'`.

The `public()` method returns a public key as a Uint8Array.

```js
const userKeypair = new client.Sr25519Pair('//Alice')
```

### `StoreProgram`

The `StoreProgram` async function takes the following arguments:

- `api: EntropyApi` an instance of the API to interact with a chain node,
- `deployerPair: Sr25519Pair` a funded Entropy account from which to submit the program,
- `program: Uint8Array` the program binary data,
- `configurationInterface: Uint8Array` the program configuration interface. In the case that there
is no configuration interface, this may be a `Uint8Array` of length zero.
- `auxiliaryDataInterface: Uint8Array` the auxiliary data interface. In the case that there is no
auxiliary data interface, this may be a `Uint8Array` of length zero.
- `oracleDataPointer: Uint8Array` this should be a `Uint8Array` of length zero since oracle data is
not yet fully implemented.

If successful, it returns a `Promise<string>` containing the hex-encoded hash of the stored program.

```js
const programBinary = new Uint8Array(fs.readFileSync('my-program.wasm'))
const configurationInterface = new Uint8Array()
const auxDataInterface = new Uint8Array()
const oraclePointer = new Uint8Array()
const programHash = await client.storeProgram(api, userKeypair, programBinary, configurationInterface, auxDataInterface, oraclePointer)
```

### `programInstance`

When registering or updating a program, we have to specify the program hash and
configuration (if present). The `programInstance` object bundles these two things together.

The constructor takes a program hash and configuration interface, both given as `Uint8Array`s. If you have a hex-encoded hash from the output from `StroreProgram`, you need to convert it to a `Uint8Array`.
If no configuration interface is needed, it should be an empty `Uint8Array`:

```js
const hash = new Uint8Array(Buffer.from(hashAsHexString, 'hex'))
const auxData = new Uint8Array()
const program = new client.ProgramInstance(hash, auxData)
```

### `register` and `pollForRegistration`

The registration process has two steps. We submit a registration extrinsic using the `register`
function, and attempt to get the verifying key if registration was successful with `pollForRegistration`.

The `register` function takes the following arguments:
- `api: entropyapi` an instance of the api to interact with a chain node,
- `userkeypair: sr25519pair` a funded entropy account from which to submit the register extrinsic,
- `programaccount: uint8array` - the 32-byte account ID (public key) of the program modification account,
- `programs: programinstance[]` - an array of programs to be associated with the account.

The `pollForRegistration` function takes the following arguments:
- `api: EntropyApi` an instance of the API to interact with a chain node,
- `userAccountId: Uint8Array` the public key of the account which submitted the registration.

If a successful registration was made, the returned promise resolves to a `VerifyingKey`; otherwise, it resolves to `undefined`.

```js
await client.register(api, userKeypair, programAccount, [program])
const verifyingKey = await waitForRegistration(api, userKeypair.public())

async function waitForRegistration (api, accountId) {
let verifyingKey
for (let i = 0; i < 50; i++) {
verifyingKey = await client.pollForRegistration(api, accountId)
if (verifyingKey) { return verifyingKey } else {
await sleep(1000)
}
}
throw new Error('Timeout waiting for register confirmation')
}

function sleep (ms) {
return new Promise(resolve => setTimeout(resolve, ms))
}
```

### `sign`

The `sign` function takes the following arguments:
- `api: EntropyApi` an instance of the API to interact with a chain node.
- `userkeypair: sr25519pair` an account associated with the signature request. Does not need
to be funded.
- `verifyingKey: VerifyingKey` the public verifying key of the Entropy account.
- `message: Uint8Array` the message to sign.
- `auxData: Uint8Array | undefined` auxiliary data to be passed to the program if present.

If successful it returns a `Signature`.

```js
const signature = await client.sign(
api,
userKeypair,
verifyingKey,
new Uint8Array(Buffer.from('my message to sign')),
undefined // Aux data goes here
)
```

### `VerifyingKey`

Represents the public key of a registered Entropy account.

- `static fromString(input: string): VerifyingKey` - Create a `VerifyingKey` from a hex-encoded
string.
- `static fromBytes(input: Uint8Array): VerifyingKey` - Create a `VerifyingKey` from a bytes.
- `toBytes(): Uint8Array` - return a byte array.
- `toString(): string` - return a hex-encoded string.

### `Signature`

Represents a recoverable ECDSA signature.

- `recoverVerifyingKey(message: Uint8Array): VerifyingKey` - Given the associated message, recover
the public verifying key for this signature.
- `toBytes(): Uint8Array` - return the signature as a byte array.
- `toString(): string` - return the signature as a hex-encoded string.

### `updateProgram`

Updates the programs associated with a given Entropy account.

Takes the following arguments:
- `api: EntropyApi` an instance of the API to interact with a chain node.
- `verifyingKey: VerifyingKey` the public verifying key of the Entropy account.
- `deployerPair: Sr25519Pair` a funded Entropy account from which to submit the update extrinsic.
- `programs: programinstance[]` - an array of programs to be associated with the account.

### `getAccounts`

This async function takes an `EntropyApi` instance and returns an array of `VerifyingKey`s of all
registered Entropy accounts.
19 changes: 19 additions & 0 deletions crates/client/nodejs-test/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
NodeJS test CLI for JS bindings to `entropy-client`

Options:
`--keypair` - a keypair given as a mnemonic or `//name` - defaults to `//Alice`
`--endpoint` - chain endpoint URL - defaults to `ws://testnet.entropy.xyz:9944`

Command examples:

`register`:
`node index.js --endpoint ws://127.0.0.1:9944 --keypair '//Bob' register`

`sign`:
`node index.js --keypair '//Charlie' sign 039ddedf4528612760a71e681642e4a83330220ebc5b45c724dc312f3b326ca176`

`store`: (store a program)
`node index.js --keypair '//Charlie' store my-program.wasm`

`accounts`: (display all registered accounts)
`node index.js accounts`
84 changes: 84 additions & 0 deletions crates/client/nodejs-test/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
const client = require('entropy-client')
const minimist = require('minimist')
const fs = require('node:fs')
const path = require('node:path')

// This is needed on Nodejs as we use bindings to the browser websocket API which is a property
// of the global object
Object.assign(global, { WebSocket: require('ws') })

async function main () {
const args = minimist(process.argv.slice(2))

const endpointUrl = args.endpoint ? args.endpoint : 'ws://testnet.entropy.xyz:9944'
console.log(`Chain endpoint ${endpointUrl}`)

const api = await new client.EntropyApi(endpointUrl)
const userKeypair = new client.Sr25519Pair(args.keypair ? args.keypair : '//Alice')

switch (args._[0]) {
case 'store':
// Store a program
const programBinary = new Uint8Array(fs.readFileSync(args._[1]))
const configurationInterface = new Uint8Array()
const auxDataInterface = new Uint8Array()
const oraclePointer = new Uint8Array()
const programHash = await client.storeProgram(api, userKeypair, programBinary, configurationInterface, auxDataInterface, oraclePointer)
console.log(`Stored program with hash ${programHash}`)
break
case 'register':
// Register an account

// Program hash defaults to device key proxy program
const hash = args.program ? new Uint8Array(Buffer.from(arg.program, 'hex')) : new Uint8Array(32)
const auxData = args.programAuxData ? new Uint8Array(Buffer.from(arg.programAuxData, 'hex')) : new Uint8Array()
const program = new client.ProgramInstance(hash, auxData)
const programAccount = userKeypair.public()

await client.register(api, userKeypair, programAccount, [program])
console.log('Submitted registration extrinsic, waiting for confirmation...')
const verifyingKey = await pollForRegistration(api, userKeypair.public())
console.log(`Registered succesfully. Verifying key: ${verifyingKey.toString()}`)
break
case 'sign':
// Sign a message
const signature = await client.sign(
api,
userKeypair,
client.VerifyingKey.fromString(args._[1]),
new Uint8Array(Buffer.from('my message to sign')),
undefined // Aux data goes here
)
console.log(`Signature: ${signature.toString()}`)
break
case 'accounts':
// Display information about all registered accounts
const accounts = await client.getAccounts(api)
console.log(`There are ${accounts.length} Entropy accounts - with verifying keys:\n`)
for (const account of accounts) {
console.log(account.toString())
}
break
default:
console.log(fs.readFileSync(path.join(__dirname, 'README.md'), 'utf8'))
}
}

main().then(() => {
process.exit(0)
})

async function pollForRegistration (api, accountId) {
let verifyingKey
for (let i = 0; i < 50; i++) {
verifyingKey = await client.pollForRegistration(api, accountId)
if (verifyingKey) { return verifyingKey } else {
await sleep(1000)
}
}
throw new Error('Timeout waiting for register confirmation')
}

function sleep (ms) {
return new Promise(resolve => setTimeout(resolve, ms))
}
11 changes: 11 additions & 0 deletions crates/client/nodejs-test/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"name": "nodejs-test",
"version": "1.0.0",
"main": "index.js",
"license": "AGPL-3.0-only",
"dependencies": {
"entropy-client": "file:../pkg",
"minimist": "^1.2.8",
"ws": "^8.14.2"
}
}
16 changes: 16 additions & 0 deletions crates/client/nodejs-test/yarn.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1


"entropy-client@file:../pkg":
version "0.1.0"

minimist@^1.2.8:
version "1.2.8"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==

ws@^8.14.2:
version "8.17.1"
resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.1.tgz#9293da530bb548febc95371d90f9c878727d919b"
integrity sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==
Loading
Loading