From 385df60c30cc032e578f74f07fc20f490fdce7d9 Mon Sep 17 00:00:00 2001 From: peg Date: Tue, 18 Jun 2024 11:46:09 +0200 Subject: [PATCH 01/25] Dependencies for wasm bindgen --- Cargo.lock | 3 +++ crates/client/Cargo.toml | 17 +++++++++++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fb994804f..7b0a30256 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2459,6 +2459,9 @@ dependencies = [ "thiserror", "tokio", "tracing", + "wasm-bindgen", + "wasm-bindgen-derive", + "wasm-bindgen-futures", "x25519-dalek 2.0.1", ] diff --git a/crates/client/Cargo.toml b/crates/client/Cargo.toml index e141a308a..9271ba0b5 100644 --- a/crates/client/Cargo.toml +++ b/crates/client/Cargo.toml @@ -32,7 +32,10 @@ hex ={ version="0.4.3", optional=true } anyhow ="1.0.86" # Only for the browser -js-sys={ version="0.3.68", optional=true } +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 } [dev-dependencies] tokio ="1.38" @@ -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"] From f07b10c20cbaa653e17fc878a2ecf03e50f47d40 Mon Sep 17 00:00:00 2001 From: peg Date: Tue, 18 Jun 2024 11:46:39 +0200 Subject: [PATCH 02/25] Fix for errors when building on wasm --- crates/client/src/errors.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/crates/client/src/errors.rs b/crates/client/src/errors.rs index 7c16d28e2..7e8f961d7 100644 --- a/crates/client/src/errors.rs +++ b/crates/client/src/errors.rs @@ -83,7 +83,7 @@ pub enum ClientError { #[error("Base64 decode: {0}")] Base64(#[from] base64::DecodeError), #[error("ECDSA: {0}")] - Ecdsa(#[from] synedrion::ecdsa::Error), + Ecdsa(synedrion::ecdsa::Error), #[error("Cannot get recovery ID from signature")] NoRecoveryId, #[error("Cannot parse recovery ID from signature")] @@ -103,3 +103,9 @@ pub enum ClientError { #[error("Verifying key has incorrect length")] BadVerifyingKeyLength, } + +impl From for ClientError { + fn from(ecdsa_error: synedrion::ecdsa::Error) -> ClientError { + ClientError::Ecdsa(ecdsa_error) + } +} From a49f246ae45c556508cec01802918f0537eeda2d Mon Sep 17 00:00:00 2001 From: peg Date: Tue, 18 Jun 2024 11:46:55 +0200 Subject: [PATCH 03/25] Add wasm bindings to sign and register --- crates/client/src/lib.rs | 3 + crates/client/src/wasm.rs | 145 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 148 insertions(+) create mode 100644 crates/client/src/wasm.rs diff --git a/crates/client/src/lib.rs b/crates/client/src/lib.rs index 1dcdc8fa2..c0cbd9615 100644 --- a/crates/client/src/lib.rs +++ b/crates/client/src/lib.rs @@ -28,3 +28,6 @@ mod tests; pub mod client; #[cfg(feature = "full-client")] pub use client::*; + +#[cfg(feature = "full-client-wasm")] +pub mod wasm; diff --git a/crates/client/src/wasm.rs b/crates/client/src/wasm.rs new file mode 100644 index 000000000..7f13a041f --- /dev/null +++ b/crates/client/src/wasm.rs @@ -0,0 +1,145 @@ +use crate::{ + chain_api::{ + entropy::runtime_types::{bounded_collections::bounded_vec::BoundedVec, pallet_registry}, + get_api, get_rpc, EntropyConfig, + }, + client, VERIFYING_KEY_LENGTH, +}; +use entropy_shared::KeyVisibility; +use js_sys::Error; +use sp_core::{sr25519, Pair}; +use subxt::{backend::legacy::LegacyRpcMethods, utils::AccountId32, OnlineClient}; +use wasm_bindgen::prelude::*; + +/// A connection to an Entropy chain endpoint +#[wasm_bindgen] +pub struct EntropyApi { + api: OnlineClient, + rpc: LegacyRpcMethods, +} + +#[wasm_bindgen] +impl EntropyApi { + #[wasm_bindgen(constructor)] + pub async fn new(url: String) -> Result { + Ok(Self { + api: get_api(&url).await.map_err(|err| Error::new(&format!("{:?}", err)))?, + rpc: get_rpc(&url).await.unwrap(), + }) + } +} + +/// An sr25519 signing keypair +#[wasm_bindgen] +pub struct Sr25519Pair(sr25519::Pair); + +#[wasm_bindgen] +impl Sr25519Pair { + #[wasm_bindgen(constructor)] + pub fn new(mnemonic: String) -> Result { + let (pair, _) = sr25519::Pair::from_phrase(&mnemonic, None) + .map_err(|err| Error::new(&format!("{:?}", err)))?; + Ok(Self(pair)) + } + + /// Get the public key + pub fn public(&self) -> Vec { + self.0.public().0.to_vec() + } +} + +/// An instance of a program, with configuration (which may be empty) +#[wasm_bindgen] +pub struct ProgramInstance(pallet_registry::pallet::ProgramInstance); + +#[wasm_bindgen] +impl ProgramInstance { + #[wasm_bindgen(constructor)] + pub fn new(hash: Vec, program_config: Vec) -> Result { + let program_pointer: [u8; 32] = + hash.try_into().map_err(|_| Error::new("Program hash must be 32 bytes"))?; + Ok(ProgramInstance(pallet_registry::pallet::ProgramInstance { + program_pointer: program_pointer.into(), + program_config, + })) + } +} + +/// The public key of a distributed Entropy keypair +#[wasm_bindgen] +pub struct VerifyingKey([u8; VERIFYING_KEY_LENGTH]); + +#[wasm_bindgen] +impl VerifyingKey { + #[wasm_bindgen(js_name=fromHexString)] + pub fn from_hex_string(input: String) -> Result { + let vec = hex::decode(input).map_err(|_| Error::new("Program hash must be 32 bytes"))?; + VerifyingKey::from_bytes(vec) + } + + #[wasm_bindgen(js_name=fromBytes)] + pub fn from_bytes(input: Vec) -> Result { + Ok(VerifyingKey(input.try_into().map_err(|_| Error::new("Program hash must be 32 bytes"))?)) + } + + #[wasm_bindgen(js_name=toBytes)] + pub fn to_bytes(&self) -> Vec { + self.0.to_vec() + } + + #[wasm_bindgen(js_name=toHexString)] + pub fn to_hex_string(&self) -> String { + hex::encode(self.to_bytes()) + } +} + +/// Register an Entropy account +#[wasm_bindgen] +pub async fn register( + entropy_api: EntropyApi, + user_keypair: Sr25519Pair, + program_account: Vec, + // TODO this should be a js array of programs - for now allow just one program + programs: ProgramInstance, +) -> Result { + let program_account: [u8; 32] = + program_account.try_into().map_err(|_| Error::new("Program account must be 32 bytes"))?; + let (verifying_key, _, _) = client::register( + &entropy_api.api, + &entropy_api.rpc, + user_keypair.0, + AccountId32(program_account), + KeyVisibility::Public, + BoundedVec(vec![programs.0]), + None, + ) + .await + .map_err(|err| Error::new(&format!("{:?}", err)))?; + + Ok(VerifyingKey(verifying_key)) +} + +/// Request to sign a message +#[wasm_bindgen] +pub async fn sign( + entropy_api: EntropyApi, + user_keypair: Sr25519Pair, + verifying_key: VerifyingKey, + message: Vec, + auxilary_data: Option>, +) -> Result { + let recoverable_signature = client::sign( + &entropy_api.api, + &entropy_api.rpc, + user_keypair.0, + verifying_key.0, + message, + None, + auxilary_data, + ) + .await + .map_err(|err| Error::new(&format!("{:?}", err)))?; + + // TODO type for signature + Ok(format!("{:?}", recoverable_signature)) +} From de8a16762e201095daa6af63dd51a85ae2432918 Mon Sep 17 00:00:00 2001 From: peg Date: Tue, 18 Jun 2024 15:10:45 +0200 Subject: [PATCH 04/25] Beginning of test --- crates/client/nodejs-test/index.js | 19 +++++++++++++++++++ crates/client/nodejs-test/package.json | 10 ++++++++++ crates/client/src/wasm.rs | 2 +- 3 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 crates/client/nodejs-test/index.js create mode 100644 crates/client/nodejs-test/package.json diff --git a/crates/client/nodejs-test/index.js b/crates/client/nodejs-test/index.js new file mode 100644 index 000000000..db351f465 --- /dev/null +++ b/crates/client/nodejs-test/index.js @@ -0,0 +1,19 @@ +const client = require('entropy-client') + +// 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') }) + +// console.log(client) +async function getApi () { + const api = await new client.EntropyApi('ws://testnet.entropy.xyz:9944') + console.log(api) + // create a keypair from mnemonic + // create a program instance from a known program hash + // register + // sign with registered account +} + +getApi().then(() => { + console.log('done') +}) diff --git a/crates/client/nodejs-test/package.json b/crates/client/nodejs-test/package.json new file mode 100644 index 000000000..2ce0db8f8 --- /dev/null +++ b/crates/client/nodejs-test/package.json @@ -0,0 +1,10 @@ +{ + "name": "nodejs-test", + "version": "1.0.0", + "main": "index.js", + "license": "AGPL-3.0-only", + "dependencies": { + "entropy-client": "file:../pkg", + "ws": "^8.14.2" + } +} diff --git a/crates/client/src/wasm.rs b/crates/client/src/wasm.rs index 7f13a041f..7e87b0715 100644 --- a/crates/client/src/wasm.rs +++ b/crates/client/src/wasm.rs @@ -24,7 +24,7 @@ impl EntropyApi { pub async fn new(url: String) -> Result { Ok(Self { api: get_api(&url).await.map_err(|err| Error::new(&format!("{:?}", err)))?, - rpc: get_rpc(&url).await.unwrap(), + rpc: get_rpc(&url).await.map_err(|err| Error::new(&format!("{:?}", err)))?, }) } } From a0c52c19797c34a7f2962e34dbd757b4d9bd0eb1 Mon Sep 17 00:00:00 2001 From: peg Date: Tue, 18 Jun 2024 15:13:36 +0200 Subject: [PATCH 05/25] Makefile for building js package --- crates/client/Makefile | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 crates/client/Makefile diff --git a/crates/client/Makefile b/crates/client/Makefile new file mode 100644 index 000000000..f3c5005da --- /dev/null +++ b/crates/client/Makefile @@ -0,0 +1,22 @@ +# 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/ + +# Cleans out build artifacts. +clean :: + rm -rf pkg/ nodejs-test/node_modules/ From 56cc99de851c265c7aed1dfd7ab279d04c7ce08f Mon Sep 17 00:00:00 2001 From: peg Date: Wed, 19 Jun 2024 10:38:17 +0200 Subject: [PATCH 06/25] Add dev mode to makefile --- crates/client/Makefile | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/client/Makefile b/crates/client/Makefile index f3c5005da..794754a4c 100644 --- a/crates/client/Makefile +++ b/crates/client/Makefile @@ -17,6 +17,10 @@ build-bundler :: # 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/ From db7c9c078eee339c45dce425a4e26f91d6f2192c Mon Sep 17 00:00:00 2001 From: peg Date: Wed, 19 Jun 2024 10:41:22 +0200 Subject: [PATCH 07/25] HashingAlgorithm enum needs extra serde stuff when on wasm --- crates/client/nodejs-test/index.js | 60 +++++++++++++++++++++----- crates/client/nodejs-test/package.json | 1 + crates/client/nodejs-test/yarn.lock | 16 +++++++ crates/shared/src/types.rs | 10 ++--- 4 files changed, 72 insertions(+), 15 deletions(-) create mode 100644 crates/client/nodejs-test/yarn.lock diff --git a/crates/client/nodejs-test/index.js b/crates/client/nodejs-test/index.js index db351f465..9da7cac72 100644 --- a/crates/client/nodejs-test/index.js +++ b/crates/client/nodejs-test/index.js @@ -1,19 +1,59 @@ const client = require('entropy-client') +const minimist = require('minimist') +const fs = require('node:fs'); // 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') }) -// console.log(client) -async function getApi () { - const api = await new client.EntropyApi('ws://testnet.entropy.xyz:9944') - console.log(api) - // create a keypair from mnemonic - // create a program instance from a known program hash - // register - // sign with registered account +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 + + // Use the device key proxy program for now + const program = new client.ProgramInstance(new Uint8Array(32), new Uint8Array()) + const programAccount = userKeypair.public() + + let verifyingKey = await client.register(api, userKeypair, programAccount, program) + console.log(`Registered succesfully. Verifying key: ${verifyingKey}`) + break + case 'sign': + // Sign a message + const signature = await client.sign( + api, + userKeypair, + client.VerifyingKey.fromHexString(args._[1]), + new Uint8Array(Buffer.from('my message to sign')), + undefined, // Aux data goes here + ) + console.log(`Signed message ${signature}`) + break + case 'accounts': + // Display information about all registered accounts + const accounts = await client.get_accounts(api) + console.log(accounts) + break + } } -getApi().then(() => { - console.log('done') +main().then(() => { + console.log('Done') }) diff --git a/crates/client/nodejs-test/package.json b/crates/client/nodejs-test/package.json index 2ce0db8f8..65ad69702 100644 --- a/crates/client/nodejs-test/package.json +++ b/crates/client/nodejs-test/package.json @@ -5,6 +5,7 @@ "license": "AGPL-3.0-only", "dependencies": { "entropy-client": "file:../pkg", + "minimist": "^1.2.8", "ws": "^8.14.2" } } diff --git a/crates/client/nodejs-test/yarn.lock b/crates/client/nodejs-test/yarn.lock new file mode 100644 index 000000000..d635533b9 --- /dev/null +++ b/crates/client/nodejs-test/yarn.lock @@ -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== diff --git a/crates/shared/src/types.rs b/crates/shared/src/types.rs index b1ae9840a..d198bff32 100644 --- a/crates/shared/src/types.rs +++ b/crates/shared/src/types.rs @@ -22,7 +22,7 @@ use scale_info::TypeInfo; use serde::{Deserialize, Serialize}; #[cfg(feature = "wasm-no-std")] use sp_runtime::RuntimeDebug; -#[cfg(feature = "std")] +#[cfg(any(feature = "std", feature = "wasm"))] use strum_macros::EnumIter; /// X25519 public key used by the client in non-interactive ECDH to authenticate/encrypt @@ -86,11 +86,11 @@ pub struct OcwMessageProactiveRefresh { } /// 256-bit hashing algorithms for deriving the point to be signed. -#[cfg_attr(any(feature = "wasm", feature = "std"), derive(Serialize, Deserialize))] -#[cfg_attr(feature = "std", derive(EnumIter))] +#[cfg_attr(any(feature = "std", feature = "wasm"), derive(Serialize, Deserialize))] +#[cfg_attr(any(feature = "std", feature = "wasm"), derive(EnumIter))] #[derive(Clone, Debug, Eq, PartialEq)] -#[cfg_attr(feature = "std", serde(rename = "hash"))] -#[cfg_attr(feature = "std", serde(rename_all = "lowercase"))] +#[cfg_attr(any(feature = "std", feature = "wasm"), serde(rename = "hash"))] +#[cfg_attr(any(feature = "std", feature = "wasm"), serde(rename_all = "lowercase"))] #[non_exhaustive] pub enum HashingAlgorithm { Sha1, From e707be65e4e739a4a43b7cc4eb063b104b5bbd6b Mon Sep 17 00:00:00 2001 From: peg Date: Wed, 19 Jun 2024 10:41:43 +0200 Subject: [PATCH 08/25] More features exposed on wasm --- crates/client/src/errors.rs | 1 + crates/client/src/wasm.rs | 37 ++++++++++++++++++++++++++++++++++++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/crates/client/src/errors.rs b/crates/client/src/errors.rs index 7e8f961d7..6eb75e591 100644 --- a/crates/client/src/errors.rs +++ b/crates/client/src/errors.rs @@ -104,6 +104,7 @@ pub enum ClientError { BadVerifyingKeyLength, } +#[cfg(feature = "full-client")] impl From for ClientError { fn from(ecdsa_error: synedrion::ecdsa::Error) -> ClientError { ClientError::Ecdsa(ecdsa_error) diff --git a/crates/client/src/wasm.rs b/crates/client/src/wasm.rs index 7e87b0715..e96e7d967 100644 --- a/crates/client/src/wasm.rs +++ b/crates/client/src/wasm.rs @@ -37,7 +37,7 @@ pub struct Sr25519Pair(sr25519::Pair); impl Sr25519Pair { #[wasm_bindgen(constructor)] pub fn new(mnemonic: String) -> Result { - let (pair, _) = sr25519::Pair::from_phrase(&mnemonic, None) + let pair = sr25519::Pair::from_string(&mnemonic, None) .map_err(|err| Error::new(&format!("{:?}", err)))?; Ok(Self(pair)) } @@ -143,3 +143,38 @@ pub async fn sign( // TODO type for signature Ok(format!("{:?}", recoverable_signature)) } + +/// Store a given program binary and return its hash +#[wasm_bindgen(js_name=storeProgram)] +pub async fn store_program( + entropy_api: EntropyApi, + deployer_pair: Sr25519Pair, + program: Vec, + configuration_interface: Vec, + auxiliary_data_interface: Vec, + oracle_data_pointer: Vec, +) -> Result { + let program_hash = client::store_program( + &entropy_api.api, + &entropy_api.rpc, + &deployer_pair.0, + program, + configuration_interface, + auxiliary_data_interface, + oracle_data_pointer, + ) + .await + .map_err(|err| Error::new(&format!("{:?}", err)))?; + + Ok(program_hash.to_string()) +} + +/// Get a list of all registered Entropy accounts +#[wasm_bindgen(js_name=getAccounts)] +pub async fn get_accounts(entropy_api: EntropyApi) -> Result { + let accounts = client::get_accounts(&entropy_api.api, &entropy_api.rpc) + .await + .map_err(|err| Error::new(&format!("{:?}", err)))?; + + Ok(format!("{:?}", accounts)) +} From 4404efbd8663855dcc5f3b0db6ca538bd43ddfbc Mon Sep 17 00:00:00 2001 From: peg Date: Wed, 19 Jun 2024 11:05:23 +0200 Subject: [PATCH 09/25] Small fix --- crates/client/nodejs-test/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/client/nodejs-test/index.js b/crates/client/nodejs-test/index.js index 9da7cac72..df7f26016 100644 --- a/crates/client/nodejs-test/index.js +++ b/crates/client/nodejs-test/index.js @@ -48,7 +48,7 @@ async function main () { break case 'accounts': // Display information about all registered accounts - const accounts = await client.get_accounts(api) + const accounts = await client.getAccounts(api) console.log(accounts) break } From b436f3f482a015c70ca61fa4bd0df784c945a052 Mon Sep 17 00:00:00 2001 From: peg Date: Wed, 19 Jun 2024 13:23:43 +0200 Subject: [PATCH 10/25] Fix registration --- crates/client/nodejs-test/index.js | 90 ++++++++++++++++++------------ crates/client/src/client.rs | 44 ++++++++++----- crates/client/src/wasm.rs | 45 ++++++++++----- 3 files changed, 114 insertions(+), 65 deletions(-) diff --git a/crates/client/nodejs-test/index.js b/crates/client/nodejs-test/index.js index df7f26016..4320e0ca4 100644 --- a/crates/client/nodejs-test/index.js +++ b/crates/client/nodejs-test/index.js @@ -1,6 +1,6 @@ const client = require('entropy-client') const minimist = require('minimist') -const fs = require('node:fs'); +const fs = require('node:fs') // This is needed on Nodejs as we use bindings to the browser websocket API which is a property // of the global object @@ -16,44 +16,62 @@ async function main () { 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 - - // Use the device key proxy program for now - const program = new client.ProgramInstance(new Uint8Array(32), new Uint8Array()) - const programAccount = userKeypair.public() - - let verifyingKey = await client.register(api, userKeypair, programAccount, program) - console.log(`Registered succesfully. Verifying key: ${verifyingKey}`) - break - case 'sign': - // Sign a message - const signature = await client.sign( - api, - userKeypair, - client.VerifyingKey.fromHexString(args._[1]), - new Uint8Array(Buffer.from('my message to sign')), - undefined, // Aux data goes here - ) - console.log(`Signed message ${signature}`) - break - case 'accounts': - // Display information about all registered accounts - const accounts = await client.getAccounts(api) - console.log(accounts) - break + 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 + + // Use the device key proxy program for now + const program = new client.ProgramInstance(new Uint8Array(32), new Uint8Array()) + 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.toHexString()}`) + break + case 'sign': + // Sign a message + const signature = await client.sign( + api, + userKeypair, + client.VerifyingKey.fromHexString(args._[1]), + new Uint8Array(Buffer.from('my message to sign')), + undefined // Aux data goes here + ) + console.log(`Signed message ${signature}`) + break + case 'accounts': + // Display information about all registered accounts + const accounts = await client.getAccounts(api) + console.log(accounts) + break } } main().then(() => { console.log('Done') + 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)) +} diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index bd0a0372c..4595ebdc2 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -128,27 +128,41 @@ pub async fn register( SubxtAccountId32(signature_request_keypair.public().0); for _ in 0..50 { - let block_hash = rpc.chain_get_block_hash(None).await?; - let events = - EventsClient::new(api.clone()).at(block_hash.ok_or(ClientError::BlockHash)?).await?; - let registered_event = events.find::(); - for event in registered_event.flatten() { - // check if the event belongs to this user - if event.0 == account_id { - let registered_query = entropy::storage().registry().registered(&event.1); - let registered_status = query_chain(api, rpc, registered_query, block_hash).await?; - if let Some(status) = registered_status { - let verifying_key = - event.1 .0.try_into().map_err(|_| ClientError::BadVerifyingKeyLength)?; - return Ok((verifying_key, status, keyshare_option)); - } - } + if let Ok((verifying_key, registration_status)) = + poll_for_registration(&api, &rpc, &account_id).await + { + return Ok((verifying_key, registration_status, keyshare_option)); } std::thread::sleep(std::time::Duration::from_millis(1000)); } Err(ClientError::RegistrationTimeout) } +/// Check if a registration made by a given account was successful, and if so return registration details +pub async fn poll_for_registration( + api: &OnlineClient, + rpc: &LegacyRpcMethods, + account_id: &SubxtAccountId32, +) -> Result<([u8; VERIFYING_KEY_LENGTH], RegisteredInfo), ClientError> { + let block_hash = rpc.chain_get_block_hash(None).await?; + let events = + EventsClient::new(api.clone()).at(block_hash.ok_or(ClientError::BlockHash)?).await?; + let registered_event = events.find::(); + for event in registered_event.flatten() { + // check if the event belongs to this user + if &event.0 == account_id { + let registered_query = entropy::storage().registry().registered(&event.1); + let registered_status = query_chain(api, rpc, registered_query, block_hash).await?; + if let Some(status) = registered_status { + let verifying_key = + event.1 .0.try_into().map_err(|_| ClientError::BadVerifyingKeyLength)?; + return Ok((verifying_key, status)); + } + } + } + Err(ClientError::RegistrationTimeout) +} + /// Request to sign a message #[tracing::instrument( skip_all, diff --git a/crates/client/src/wasm.rs b/crates/client/src/wasm.rs index e96e7d967..2e2bd9732 100644 --- a/crates/client/src/wasm.rs +++ b/crates/client/src/wasm.rs @@ -96,42 +96,59 @@ impl VerifyingKey { /// Register an Entropy account #[wasm_bindgen] pub async fn register( - entropy_api: EntropyApi, - user_keypair: Sr25519Pair, + entropy_api: &EntropyApi, + user_keypair: &Sr25519Pair, program_account: Vec, // TODO this should be a js array of programs - for now allow just one program programs: ProgramInstance, -) -> Result { +) -> Result<(), Error> { let program_account: [u8; 32] = program_account.try_into().map_err(|_| Error::new("Program account must be 32 bytes"))?; - let (verifying_key, _, _) = client::register( + + client::put_register_request_on_chain( &entropy_api.api, &entropy_api.rpc, - user_keypair.0, + user_keypair.0.clone(), AccountId32(program_account), KeyVisibility::Public, BoundedVec(vec![programs.0]), - None, ) .await .map_err(|err| Error::new(&format!("{:?}", err)))?; - Ok(VerifyingKey(verifying_key)) + Ok(()) +} + +#[wasm_bindgen(js_name=pollForRegistration)] +pub async fn poll_for_registration( + entropy_api: &EntropyApi, + account_id: Vec, +) -> Result, Error> { + let account_id: [u8; 32] = + account_id.try_into().map_err(|_| Error::new("Account ID must be 32 bytes"))?; + if let Ok((verifying_key, _)) = + client::poll_for_registration(&entropy_api.api, &entropy_api.rpc, &AccountId32(account_id)) + .await + { + Ok(Some(VerifyingKey(verifying_key))) + } else { + Ok(None) + } } /// Request to sign a message #[wasm_bindgen] pub async fn sign( - entropy_api: EntropyApi, - user_keypair: Sr25519Pair, - verifying_key: VerifyingKey, + entropy_api: &EntropyApi, + user_keypair: &Sr25519Pair, + verifying_key: &VerifyingKey, message: Vec, auxilary_data: Option>, ) -> Result { let recoverable_signature = client::sign( &entropy_api.api, &entropy_api.rpc, - user_keypair.0, + user_keypair.0.clone(), verifying_key.0, message, None, @@ -147,8 +164,8 @@ pub async fn sign( /// Store a given program binary and return its hash #[wasm_bindgen(js_name=storeProgram)] pub async fn store_program( - entropy_api: EntropyApi, - deployer_pair: Sr25519Pair, + entropy_api: &EntropyApi, + deployer_pair: &Sr25519Pair, program: Vec, configuration_interface: Vec, auxiliary_data_interface: Vec, @@ -171,7 +188,7 @@ pub async fn store_program( /// Get a list of all registered Entropy accounts #[wasm_bindgen(js_name=getAccounts)] -pub async fn get_accounts(entropy_api: EntropyApi) -> Result { +pub async fn get_accounts(entropy_api: &EntropyApi) -> Result { let accounts = client::get_accounts(&entropy_api.api, &entropy_api.rpc) .await .map_err(|err| Error::new(&format!("{:?}", err)))?; From 818cf6f47b042c2c4fd38b7b4197e715c1d200ad Mon Sep 17 00:00:00 2001 From: peg Date: Wed, 19 Jun 2024 13:39:30 +0200 Subject: [PATCH 11/25] Add test script readme --- crates/client/nodejs-test/README.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 crates/client/nodejs-test/README.md diff --git a/crates/client/nodejs-test/README.md b/crates/client/nodejs-test/README.md new file mode 100644 index 000000000..4f0484e8c --- /dev/null +++ b/crates/client/nodejs-test/README.md @@ -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` From 0bf5626bcd41560d612c52e10d479cdcc5cb48dd Mon Sep 17 00:00:00 2001 From: peg Date: Wed, 19 Jun 2024 14:04:44 +0200 Subject: [PATCH 12/25] Clippy --- crates/client/src/client.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 4595ebdc2..346b31bc3 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -129,7 +129,7 @@ pub async fn register( for _ in 0..50 { if let Ok((verifying_key, registration_status)) = - poll_for_registration(&api, &rpc, &account_id).await + poll_for_registration(api, rpc, &account_id).await { return Ok((verifying_key, registration_status, keyshare_option)); } From 861fe0383c2fe04fcddc85a462de9359d9fa587b Mon Sep 17 00:00:00 2001 From: peg Date: Wed, 19 Jun 2024 17:32:24 +0200 Subject: [PATCH 13/25] Write readme to stdout if no command recognised --- crates/client/nodejs-test/index.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/client/nodejs-test/index.js b/crates/client/nodejs-test/index.js index 4320e0ca4..080a66913 100644 --- a/crates/client/nodejs-test/index.js +++ b/crates/client/nodejs-test/index.js @@ -1,6 +1,7 @@ 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 @@ -53,6 +54,8 @@ async function main () { const accounts = await client.getAccounts(api) console.log(accounts) break + default: + console.log(fs.readFileSync(path.join(__dirname, 'README.md'), 'utf8')) } } From 3ab2d6f1ce727088a83a4c0bea3be027587f858b Mon Sep 17 00:00:00 2001 From: peg Date: Thu, 20 Jun 2024 11:22:52 +0200 Subject: [PATCH 14/25] Allow an array of programs to be passed in --- crates/client/nodejs-test/index.js | 2 +- crates/client/src/lib.rs | 2 ++ crates/client/src/wasm.rs | 40 +++++++++++++++++++++++++++--- 3 files changed, 40 insertions(+), 4 deletions(-) diff --git a/crates/client/nodejs-test/index.js b/crates/client/nodejs-test/index.js index 080a66913..d8c83c628 100644 --- a/crates/client/nodejs-test/index.js +++ b/crates/client/nodejs-test/index.js @@ -33,7 +33,7 @@ async function main () { const program = new client.ProgramInstance(new Uint8Array(32), new Uint8Array()) const programAccount = userKeypair.public() - await client.register(api, userKeypair, programAccount, program) + 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.toHexString()}`) diff --git a/crates/client/src/lib.rs b/crates/client/src/lib.rs index c0cbd9615..d350e552a 100644 --- a/crates/client/src/lib.rs +++ b/crates/client/src/lib.rs @@ -21,6 +21,8 @@ pub mod user; pub mod util; pub use util::Hasher; +extern crate alloc; + #[cfg(test)] mod tests; diff --git a/crates/client/src/wasm.rs b/crates/client/src/wasm.rs index 2e2bd9732..645d74593 100644 --- a/crates/client/src/wasm.rs +++ b/crates/client/src/wasm.rs @@ -10,6 +10,7 @@ use js_sys::Error; use sp_core::{sr25519, Pair}; use subxt::{backend::legacy::LegacyRpcMethods, utils::AccountId32, OnlineClient}; use wasm_bindgen::prelude::*; +use wasm_bindgen_derive::TryFromJsValue; /// A connection to an Entropy chain endpoint #[wasm_bindgen] @@ -49,6 +50,7 @@ impl Sr25519Pair { } /// An instance of a program, with configuration (which may be empty) +#[derive(TryFromJsValue)] #[wasm_bindgen] pub struct ProgramInstance(pallet_registry::pallet::ProgramInstance); @@ -65,6 +67,21 @@ impl ProgramInstance { } } +impl Clone for ProgramInstance { + fn clone(&self) -> Self { + ProgramInstance(pallet_registry::pallet::ProgramInstance { + program_pointer: self.0.program_pointer.clone(), + program_config: self.0.program_config.clone(), + }) + } +} + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "ProgramInstance[]")] + pub type ProgramInstanceArray; +} + /// The public key of a distributed Entropy keypair #[wasm_bindgen] pub struct VerifyingKey([u8; VERIFYING_KEY_LENGTH]); @@ -99,19 +116,20 @@ pub async fn register( entropy_api: &EntropyApi, user_keypair: &Sr25519Pair, program_account: Vec, - // TODO this should be a js array of programs - for now allow just one program - programs: ProgramInstance, + programs: ProgramInstanceArray, ) -> Result<(), Error> { let program_account: [u8; 32] = program_account.try_into().map_err(|_| Error::new("Program account must be 32 bytes"))?; + let programs = parse_program_instances(programs)?; + client::put_register_request_on_chain( &entropy_api.api, &entropy_api.rpc, user_keypair.0.clone(), AccountId32(program_account), KeyVisibility::Public, - BoundedVec(vec![programs.0]), + BoundedVec(programs), ) .await .map_err(|err| Error::new(&format!("{:?}", err)))?; @@ -195,3 +213,19 @@ pub async fn get_accounts(entropy_api: &EntropyApi) -> Result { Ok(format!("{:?}", accounts)) } + +/// Parse a JS array of ProgramInstance to a vector of pallet_registry ProgramsInstance +fn parse_program_instances( + program_instances_js: ProgramInstanceArray, +) -> Result, Error> { + let js_val: &JsValue = program_instances_js.as_ref(); + let array: &js_sys::Array = + js_val.dyn_ref().ok_or_else(|| Error::new("The argument must be an array"))?; + let length: usize = array.length().try_into().map_err(|err| Error::new(&format!("{}", err)))?; + let mut programs = Vec::::with_capacity(length); + for js in array.iter() { + let typed_elem = ProgramInstance::try_from(&js).map_err(|err| Error::new(&err))?; + programs.push(typed_elem.0); + } + Ok(programs) +} From a39407df5e7193143294ee57816f99d69c091181 Mon Sep 17 00:00:00 2001 From: peg Date: Thu, 20 Jun 2024 12:34:56 +0200 Subject: [PATCH 15/25] Display accounts correctly --- crates/client/nodejs-test/index.js | 10 ++++--- crates/client/src/wasm.rs | 48 +++++++++++++++++++++++------- 2 files changed, 44 insertions(+), 14 deletions(-) diff --git a/crates/client/nodejs-test/index.js b/crates/client/nodejs-test/index.js index d8c83c628..8a8df4744 100644 --- a/crates/client/nodejs-test/index.js +++ b/crates/client/nodejs-test/index.js @@ -36,14 +36,14 @@ async function main () { 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.toHexString()}`) + console.log(`Registered succesfully. Verifying key: ${verifyingKey.toString()}`) break case 'sign': // Sign a message const signature = await client.sign( api, userKeypair, - client.VerifyingKey.fromHexString(args._[1]), + client.VerifyingKey.fromString(args._[1]), new Uint8Array(Buffer.from('my message to sign')), undefined // Aux data goes here ) @@ -52,7 +52,10 @@ async function main () { case 'accounts': // Display information about all registered accounts const accounts = await client.getAccounts(api) - console.log(accounts) + 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')) @@ -60,7 +63,6 @@ async function main () { } main().then(() => { - console.log('Done') process.exit(0) }) diff --git a/crates/client/src/wasm.rs b/crates/client/src/wasm.rs index 645d74593..10a360504 100644 --- a/crates/client/src/wasm.rs +++ b/crates/client/src/wasm.rs @@ -9,8 +9,8 @@ use entropy_shared::KeyVisibility; use js_sys::Error; use sp_core::{sr25519, Pair}; use subxt::{backend::legacy::LegacyRpcMethods, utils::AccountId32, OnlineClient}; -use wasm_bindgen::prelude::*; -use wasm_bindgen_derive::TryFromJsValue; +use wasm_bindgen::{prelude::*, JsCast, JsValue}; +use wasm_bindgen_derive::{into_js_array, TryFromJsValue}; /// A connection to an Entropy chain endpoint #[wasm_bindgen] @@ -80,16 +80,18 @@ impl Clone for ProgramInstance { extern "C" { #[wasm_bindgen(typescript_type = "ProgramInstance[]")] pub type ProgramInstanceArray; + #[wasm_bindgen(typescript_type = "VerifyingKey[]")] + pub type VerifyingKeyArray; } /// The public key of a distributed Entropy keypair -#[wasm_bindgen] +#[wasm_bindgen(inspectable)] pub struct VerifyingKey([u8; VERIFYING_KEY_LENGTH]); #[wasm_bindgen] impl VerifyingKey { - #[wasm_bindgen(js_name=fromHexString)] - pub fn from_hex_string(input: String) -> Result { + #[wasm_bindgen(js_name=fromString)] + pub fn from_string(input: String) -> Result { let vec = hex::decode(input).map_err(|_| Error::new("Program hash must be 32 bytes"))?; VerifyingKey::from_bytes(vec) } @@ -104,8 +106,8 @@ impl VerifyingKey { self.0.to_vec() } - #[wasm_bindgen(js_name=toHexString)] - pub fn to_hex_string(&self) -> String { + #[wasm_bindgen(js_name=toString)] + pub fn to_string(&self) -> String { hex::encode(self.to_bytes()) } } @@ -204,14 +206,40 @@ pub async fn store_program( Ok(program_hash.to_string()) } -/// Get a list of all registered Entropy accounts +/// Update the programs associated with an Entropy account +#[wasm_bindgen(js_name=updatePrograms)] +pub async fn update_programs( + entropy_api: &EntropyApi, + verifying_key: &VerifyingKey, + deployer_pair: &Sr25519Pair, + programs: ProgramInstanceArray, +) -> Result<(), Error> { + let programs = parse_program_instances(programs)?; + + client::update_programs( + &entropy_api.api, + &entropy_api.rpc, + verifying_key.0, + &deployer_pair.0, + BoundedVec(programs), + ) + .await + .map_err(|err| Error::new(&format!("{:?}", err)))?; + + Ok(()) +} + +/// Get a string list of all registered Entropy account's #[wasm_bindgen(js_name=getAccounts)] -pub async fn get_accounts(entropy_api: &EntropyApi) -> Result { +pub async fn get_accounts(entropy_api: &EntropyApi) -> Result { let accounts = client::get_accounts(&entropy_api.api, &entropy_api.rpc) .await .map_err(|err| Error::new(&format!("{:?}", err)))?; - Ok(format!("{:?}", accounts)) + let verifying_keys: Vec<_> = + accounts.into_iter().map(|(verifying_key, _info)| VerifyingKey(verifying_key)).collect(); + + Ok(into_js_array(verifying_keys)) } /// Parse a JS array of ProgramInstance to a vector of pallet_registry ProgramsInstance From 61f21cee359b78c5a7b3a5c6ea8a00fbcebcdcf9 Mon Sep 17 00:00:00 2001 From: peg Date: Thu, 20 Jun 2024 12:38:23 +0200 Subject: [PATCH 16/25] Standard formatting for JS tests --- crates/client/nodejs-test/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/client/nodejs-test/index.js b/crates/client/nodejs-test/index.js index 8a8df4744..6b261583c 100644 --- a/crates/client/nodejs-test/index.js +++ b/crates/client/nodejs-test/index.js @@ -54,7 +54,7 @@ async function main () { 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()) + console.log(account.toString()) } break default: From a8e7d440471b132c3ff9512608cebebeebdd7f7e Mon Sep 17 00:00:00 2001 From: peg Date: Thu, 20 Jun 2024 17:54:05 +0200 Subject: [PATCH 17/25] Allow program hash and aux data to be passed in --- crates/client/nodejs-test/index.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/client/nodejs-test/index.js b/crates/client/nodejs-test/index.js index 6b261583c..e0649ae5f 100644 --- a/crates/client/nodejs-test/index.js +++ b/crates/client/nodejs-test/index.js @@ -29,8 +29,10 @@ async function main () { case 'register': // Register an account - // Use the device key proxy program for now - const program = new client.ProgramInstance(new Uint8Array(32), new Uint8Array()) + // 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]) From afd7f9780819d419cf107b6ee89f2be9d0d15765 Mon Sep 17 00:00:00 2001 From: peg Date: Thu, 20 Jun 2024 22:53:34 +0200 Subject: [PATCH 18/25] Changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 106931eb3..dc3472176 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ At the moment this project **does not** adhere to case that an unknown variant is added in the future. ### 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)) From 18586fdf91f37e9496eec331b0a1bec03dc45416 Mon Sep 17 00:00:00 2001 From: peg Date: Thu, 18 Jul 2024 12:43:26 +0200 Subject: [PATCH 19/25] Clippy --- crates/shared/src/types.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/crates/shared/src/types.rs b/crates/shared/src/types.rs index b8df16c71..391ec0bbc 100644 --- a/crates/shared/src/types.rs +++ b/crates/shared/src/types.rs @@ -20,8 +20,6 @@ use codec::{Decode, Encode}; use scale_info::TypeInfo; #[cfg(any(feature = "std", feature = "wasm"))] use serde::{Deserialize, Serialize}; -#[cfg(feature = "wasm-no-std")] -use sp_runtime::RuntimeDebug; #[cfg(any(feature = "std", feature = "wasm"))] use strum_macros::EnumIter; From 34aaf6e385af77fc3ac43850134cce4688912175 Mon Sep 17 00:00:00 2001 From: peg Date: Mon, 29 Jul 2024 08:36:00 +0200 Subject: [PATCH 20/25] Add js readme --- crates/client/Makefile | 6 +++--- crates/client/js-README.md | 17 +++++++++++++++++ 2 files changed, 20 insertions(+), 3 deletions(-) create mode 100644 crates/client/js-README.md diff --git a/crates/client/Makefile b/crates/client/Makefile index 794754a4c..b32d06087 100644 --- a/crates/client/Makefile +++ b/crates/client/Makefile @@ -1,20 +1,20 @@ # 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 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 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 js-README.md ./pkg/README.md cp ../../LICENSE ./pkg/ # Builds a JS Module for nodejs in dev mode (no optimisations) diff --git a/crates/client/js-README.md b/crates/client/js-README.md new file mode 100644 index 000000000..f7b6dad07 --- /dev/null +++ b/crates/client/js-README.md @@ -0,0 +1,17 @@ +# `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`. From d0615ef81196179cd4535555beb295d5f7651209 Mon Sep 17 00:00:00 2001 From: peg Date: Mon, 29 Jul 2024 09:59:08 +0200 Subject: [PATCH 21/25] Add to js readme --- crates/client/js-README.md | 152 +++++++++++++++++++++++++++++++++++++ 1 file changed, 152 insertions(+) diff --git a/crates/client/js-README.md b/crates/client/js-README.md index f7b6dad07..d5d25ebcd 100644 --- a/crates/client/js-README.md +++ b/crates/client/js-README.md @@ -15,3 +15,155 @@ 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` 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 a specify the program hash as well as the program +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 will resolve to a `VerifyingKey`, +otherwise it will resolve 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 the signature encoded as a string. A `Signature` type will be implemented +soon. + +```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. + +### `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. From 0299b383e9d09a5a73afa53169ad4e0e026b4903 Mon Sep 17 00:00:00 2001 From: Johnny <9611008+johnnymatthews@users.noreply.github.com> Date: Tue, 30 Jul 2024 03:40:01 -0300 Subject: [PATCH 22/25] Minor grammatical and spelling updates. (#967) * Minor grammatical and spelling updates. * Fixed SDK NPM link. --- crates/client/js-README.md | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/crates/client/js-README.md b/crates/client/js-README.md index d5d25ebcd..fa77d95a0 100644 --- a/crates/client/js-README.md +++ b/crates/client/js-README.md @@ -2,12 +2,12 @@ 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). +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 +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 @@ -24,7 +24,7 @@ const client = require('entropy-client') ### `EntropyApi` -To interact with an Entropy chain node you need to instantiate the `EntropyApi` object, giving the +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 @@ -57,7 +57,7 @@ The `StoreProgram` async function takes the following arguments: - `oracleDataPointer: Uint8Array` this should be a `Uint8Array` of length zero since oracle data is not yet fully implemented. -If successful it returns a `Promise` containing the hex encoded hash of the stored program. +If successful, it returns a `Promise` containing the hex-encoded hash of the stored program. ```js const programBinary = new Uint8Array(fs.readFileSync('my-program.wasm')) @@ -69,11 +69,10 @@ const programHash = await client.storeProgram(api, userKeypair, programBinary, c ### `programInstance` -When registering or updating a program, we have a specify the program hash as well as the program +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`. +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 @@ -90,15 +89,14 @@ function, and attempt to get the verifying key if registration was successful wi 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, +- `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 will resolve to a `VerifyingKey`, -otherwise it will resolve to `undefined`. +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]) @@ -128,9 +126,9 @@ The `sign` function takes the following arguments: 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. +- `auxData: Uint8Array | undefined` auxiliary data to be passed to the program if present. -If successful it returns the signature encoded as a string. A `Signature` type will be implemented +If successful, it returns the signature encoded as a string. A `Signature` type will be implemented soon. ```js From 36d5422867fe301fac6ee104113a2625c811d62d Mon Sep 17 00:00:00 2001 From: peg Date: Tue, 30 Jul 2024 11:19:27 +0200 Subject: [PATCH 23/25] Fixes following update --- crates/client/src/client.rs | 2 +- crates/client/src/wasm.rs | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 6ba5f4aff..f77387ed8 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -87,7 +87,7 @@ pub async fn register( ) .await?; - let account_id: SubxtAccountId32 = signature_request_keypair.public().into(); + let account_id = SubxtAccountId32(signature_request_keypair.public().0); for _ in 0..50 { if let Ok((verifying_key, registration_status)) = diff --git a/crates/client/src/wasm.rs b/crates/client/src/wasm.rs index 10a360504..cc2330f76 100644 --- a/crates/client/src/wasm.rs +++ b/crates/client/src/wasm.rs @@ -5,7 +5,6 @@ use crate::{ }, client, VERIFYING_KEY_LENGTH, }; -use entropy_shared::KeyVisibility; use js_sys::Error; use sp_core::{sr25519, Pair}; use subxt::{backend::legacy::LegacyRpcMethods, utils::AccountId32, OnlineClient}; @@ -130,7 +129,6 @@ pub async fn register( &entropy_api.rpc, user_keypair.0.clone(), AccountId32(program_account), - KeyVisibility::Public, BoundedVec(programs), ) .await @@ -171,7 +169,6 @@ pub async fn sign( user_keypair.0.clone(), verifying_key.0, message, - None, auxilary_data, ) .await From d3199afbdd864d5b7a828e3a691a5756bc8e171b Mon Sep 17 00:00:00 2001 From: peg Date: Tue, 30 Jul 2024 11:47:38 +0200 Subject: [PATCH 24/25] Add signature type --- crates/client/nodejs-test/index.js | 2 +- crates/client/src/wasm.rs | 45 +++++++++++++++++++++++++++--- 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/crates/client/nodejs-test/index.js b/crates/client/nodejs-test/index.js index e0649ae5f..a25dd8408 100644 --- a/crates/client/nodejs-test/index.js +++ b/crates/client/nodejs-test/index.js @@ -49,7 +49,7 @@ async function main () { new Uint8Array(Buffer.from('my message to sign')), undefined // Aux data goes here ) - console.log(`Signed message ${signature}`) + console.log(`Signature: ${signature.toString()}`) break case 'accounts': // Display information about all registered accounts diff --git a/crates/client/src/wasm.rs b/crates/client/src/wasm.rs index cc2330f76..4a5f6af5f 100644 --- a/crates/client/src/wasm.rs +++ b/crates/client/src/wasm.rs @@ -3,8 +3,9 @@ use crate::{ entropy::runtime_types::{bounded_collections::bounded_vec::BoundedVec, pallet_registry}, get_api, get_rpc, EntropyConfig, }, - client, VERIFYING_KEY_LENGTH, + client, Hasher, VERIFYING_KEY_LENGTH, }; +use entropy_protocol::RecoverableSignature; use js_sys::Error; use sp_core::{sr25519, Pair}; use subxt::{backend::legacy::LegacyRpcMethods, utils::AccountId32, OnlineClient}; @@ -111,6 +112,43 @@ impl VerifyingKey { } } +/// An ECDSA recoverable signature +#[wasm_bindgen(inspectable)] +pub struct Signature(RecoverableSignature); + +#[wasm_bindgen] +impl Signature { + /// Given the associated message, recover the public key for this signature + #[wasm_bindgen(js_name=recoverVerifyingKey)] + pub fn recover_verifying_key(&self, message: Vec) -> Result { + let message_hash = Hasher::keccak(&message); + let verifying_key = synedrion::k256::ecdsa::VerifyingKey::recover_from_prehash( + &message_hash, + &self.0.signature, + self.0.recovery_id, + ) + .map_err(|err| Error::new(&format!("{:?}", err)))?; + + Ok(VerifyingKey( + verifying_key + .to_encoded_point(true) + .as_bytes() + .try_into() + .map_err(|_| Error::new("Bad verifying key length"))?, + )) + } + + #[wasm_bindgen(js_name=toBytes)] + pub fn to_bytes(&self) -> Vec { + self.0.to_rsv_bytes().to_vec() + } + + #[wasm_bindgen(js_name=toString)] + pub fn to_string(&self) -> String { + hex::encode(self.to_bytes()) + } +} + /// Register an Entropy account #[wasm_bindgen] pub async fn register( @@ -162,7 +200,7 @@ pub async fn sign( verifying_key: &VerifyingKey, message: Vec, auxilary_data: Option>, -) -> Result { +) -> Result { let recoverable_signature = client::sign( &entropy_api.api, &entropy_api.rpc, @@ -174,8 +212,7 @@ pub async fn sign( .await .map_err(|err| Error::new(&format!("{:?}", err)))?; - // TODO type for signature - Ok(format!("{:?}", recoverable_signature)) + Ok(Signature(recoverable_signature)) } /// Store a given program binary and return its hash From b83ea22d83fe47c3204f26c1a12507026b97f289 Mon Sep 17 00:00:00 2001 From: peg Date: Tue, 30 Jul 2024 11:54:20 +0200 Subject: [PATCH 25/25] Update readme --- crates/client/js-README.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/crates/client/js-README.md b/crates/client/js-README.md index d5d25ebcd..6f8f11c72 100644 --- a/crates/client/js-README.md +++ b/crates/client/js-README.md @@ -130,8 +130,7 @@ The `sign` function takes the following arguments: - `message: Uint8Array` the message to sign. - `auxData: Uint8Array | undefined` auxiliary data to be passed to the program, if present. -If successful it returns the signature encoded as a string. A `Signature` type will be implemented -soon. +If successful it returns a `Signature`. ```js const signature = await client.sign( @@ -153,6 +152,15 @@ Represents the public key of a registered Entropy account. - `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.