From 488dc37fecfcf5b1b281251159f717094ee41b38 Mon Sep 17 00:00:00 2001 From: Michael Vines Date: Wed, 13 Oct 2021 16:52:52 -0700 Subject: [PATCH] Add wasm bindings for `Pubkey` and `Keypair` --- Cargo.lock | 24 +++++ ci/buildkite-pipeline.sh | 13 +++ ci/docker-rust/Dockerfile | 5 + ci/test-stable.sh | 13 +++ ci/test-wasm.sh | 1 + programs/bpf/Cargo.lock | 48 ++++++--- sdk/.gitignore | 4 +- sdk/Cargo.toml | 4 + sdk/macro/src/lib.rs | 28 +++++ sdk/package.json | 1 + sdk/program/.gitignore | 2 + sdk/program/Cargo.toml | 7 ++ sdk/program/package.json | 14 +++ sdk/program/src/lib.rs | 6 ++ sdk/program/src/pubkey.rs | 5 +- sdk/program/src/wasm/mod.rs | 16 +++ sdk/program/src/wasm/pubkey.rs | 121 +++++++++++++++++++++ sdk/program/tests/pubkey.mjs | 185 +++++++++++++++++++++++++++++++++ sdk/src/lib.rs | 1 + sdk/src/signer/keypair.rs | 2 + sdk/src/wasm/keypair.rs | 34 ++++++ sdk/src/wasm/mod.rs | 4 + sdk/tests/keypair.mjs | 14 +++ 23 files changed, 537 insertions(+), 15 deletions(-) create mode 120000 ci/test-wasm.sh create mode 120000 sdk/package.json create mode 100644 sdk/program/.gitignore create mode 100644 sdk/program/package.json create mode 100644 sdk/program/src/wasm/mod.rs create mode 100644 sdk/program/src/wasm/pubkey.rs create mode 100644 sdk/program/tests/pubkey.mjs create mode 100644 sdk/src/wasm/keypair.rs create mode 100644 sdk/src/wasm/mod.rs create mode 100644 sdk/tests/keypair.mjs diff --git a/Cargo.lock b/Cargo.lock index 5587ee6d2e..703e9f257d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -761,6 +761,26 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if 1.0.0", + "wasm-bindgen", +] + +[[package]] +name = "console_log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "501a375961cef1a0d44767200e66e4a559283097e91d0730b1d75dfb2f8a1494" +dependencies = [ + "log 0.4.14", + "web-sys", +] + [[package]] name = "const_fn" version = "0.4.8" @@ -5440,6 +5460,8 @@ dependencies = [ "bs58 0.4.0", "bv", "bytemuck", + "console_error_panic_hook", + "console_log", "curve25519-dalek 3.2.0", "itertools 0.10.3", "lazy_static", @@ -5463,6 +5485,7 @@ dependencies = [ "solana-sdk-macro 1.10.0", "static_assertions", "thiserror", + "wasm-bindgen", ] [[package]] @@ -5755,6 +5778,7 @@ dependencies = [ "thiserror", "tiny-bip39", "uriparse", + "wasm-bindgen", ] [[package]] diff --git a/ci/buildkite-pipeline.sh b/ci/buildkite-pipeline.sh index c289df3354..667a30302a 100755 --- a/ci/buildkite-pipeline.sh +++ b/ci/buildkite-pipeline.sh @@ -226,6 +226,19 @@ EOF annotate --style info \ "downstream-projects skipped as no relevant files were modified" fi + + # Wasm support + if affects \ + ^ci/test-wasm.sh \ + ^ci/test-stable.sh \ + ^sdk/ \ + ; then + command_step wasm ". ci/rust-version.sh; ci/docker-run.sh \$\$rust_stable_docker_image ci/test-wasm.sh" 20 + else + annotate --style info \ + "wasm skipped as no relevant files were modified" + fi + # Benches... if affects \ .rs$ \ diff --git a/ci/docker-rust/Dockerfile b/ci/docker-rust/Dockerfile index 5d768a81db..028e811988 100644 --- a/ci/docker-rust/Dockerfile +++ b/ci/docker-rust/Dockerfile @@ -11,6 +11,7 @@ RUN set -x \ && apt-get install apt-transport-https \ && echo deb https://apt.buildkite.com/buildkite-agent stable main > /etc/apt/sources.list.d/buildkite-agent.list \ && apt-key adv --no-tty --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 32A37959C2FA5C3C99EFBC32A79206696452D198 \ + && curl -fsSL https://deb.nodesource.com/setup_current.x | bash - \ && apt update \ && apt install -y \ buildkite-agent \ @@ -19,6 +20,7 @@ RUN set -x \ lcov \ libudev-dev \ mscgen \ + nodejs \ net-tools \ rsync \ sudo \ @@ -26,8 +28,11 @@ RUN set -x \ unzip \ \ && rm -rf /var/lib/apt/lists/* \ + && node --version \ + && npm --version \ && rustup component add rustfmt \ && rustup component add clippy \ + && rustup target add wasm32-unknown-unknown \ && cargo install cargo-audit \ && cargo install mdbook \ && cargo install mdbook-linkcheck \ diff --git a/ci/test-stable.sh b/ci/test-stable.sh index 8f36b68b88..177175e874 100755 --- a/ci/test-stable.sh +++ b/ci/test-stable.sh @@ -103,6 +103,19 @@ test-local-cluster) _ "$cargo" stable test --release --package solana-local-cluster ${V:+--verbose} -- --nocapture --test-threads=1 exit 0 ;; +test-wasm) + _ node --version + _ npm --version + for dir in sdk/{program,}; do + if [[ -r "$dir"/package.json ]]; then + pushd "$dir" + _ npm install + _ npm test + popd + fi + done + exit 0 + ;; *) echo "Error: Unknown test: $testName" ;; diff --git a/ci/test-wasm.sh b/ci/test-wasm.sh new file mode 120000 index 0000000000..0c92a5c7bd --- /dev/null +++ b/ci/test-wasm.sh @@ -0,0 +1 @@ +test-stable.sh \ No newline at end of file diff --git a/programs/bpf/Cargo.lock b/programs/bpf/Cargo.lock index 82afe1fab4..60c8c93171 100644 --- a/programs/bpf/Cargo.lock +++ b/programs/bpf/Cargo.lock @@ -458,6 +458,26 @@ dependencies = [ "winapi", ] +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if 1.0.0", + "wasm-bindgen", +] + +[[package]] +name = "console_log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "501a375961cef1a0d44767200e66e4a559283097e91d0730b1d75dfb2f8a1494" +dependencies = [ + "log", + "web-sys", +] + [[package]] name = "constant_time_eq" version = "0.1.5" @@ -1419,9 +1439,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.49" +version = "0.3.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc15e39392125075f60c95ba416f5381ff6c3a948ff02ab12464715adf56c821" +checksum = "7cc9ffccd38c451a86bf13657df244e9c3f37493cce8e5e21e940963777acc84" dependencies = [ "wasm-bindgen", ] @@ -3270,6 +3290,8 @@ dependencies = [ "bs58 0.4.0", "bv", "bytemuck", + "console_error_panic_hook", + "console_log", "curve25519-dalek 3.2.0", "itertools 0.10.3", "lazy_static", @@ -3291,6 +3313,7 @@ dependencies = [ "solana-logger 1.10.0", "solana-sdk-macro 1.10.0", "thiserror", + "wasm-bindgen", ] [[package]] @@ -3454,6 +3477,7 @@ dependencies = [ "solana-sdk-macro 1.10.0", "thiserror", "uriparse", + "wasm-bindgen", ] [[package]] @@ -4198,9 +4222,9 @@ checksum = "93c6c3420963c5c64bca373b25e77acb562081b9bb4dd5bb864187742186cea9" [[package]] name = "wasm-bindgen" -version = "0.2.72" +version = "0.2.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fe8f61dba8e5d645a4d8132dc7a0a66861ed5e1045d2c0ed940fab33bac0fbe" +checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce" dependencies = [ "cfg-if 1.0.0", "wasm-bindgen-macro", @@ -4208,9 +4232,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.72" +version = "0.2.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "046ceba58ff062da072c7cb4ba5b22a37f00a302483f7e2a6cdc18fedbdc1fd3" +checksum = "a317bf8f9fba2476b4b2c85ef4c4af8ff39c3c7f0cdfeed4f82c34a880aa837b" dependencies = [ "bumpalo", "lazy_static", @@ -4235,9 +4259,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.72" +version = "0.2.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ef9aa01d36cda046f797c57959ff5f3c615c9cc63997a8d545831ec7976819b" +checksum = "d56146e7c495528bf6587663bea13a8eb588d39b36b679d83972e1a2dbbdacf9" dependencies = [ "quote 1.0.6", "wasm-bindgen-macro-support", @@ -4245,9 +4269,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.72" +version = "0.2.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96eb45c1b2ee33545a813a92dbb53856418bf7eb54ab34f7f7ff1448a5b3735d" +checksum = "7803e0eea25835f8abdc585cd3021b3deb11543c6fe226dcd30b228857c5c5ab" dependencies = [ "proc-macro2 1.0.24", "quote 1.0.6", @@ -4258,9 +4282,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.72" +version = "0.2.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7148f4696fb4960a346eaa60bbfb42a1ac4ebba21f750f75fc1375b098d5ffa" +checksum = "0237232789cf037d5480773fe568aac745bfe2afbc11a863e97901780a6b47cc" [[package]] name = "web-sys" diff --git a/sdk/.gitignore b/sdk/.gitignore index 5404b132db..14bd5d1709 100644 --- a/sdk/.gitignore +++ b/sdk/.gitignore @@ -1,2 +1,4 @@ -/target/ /farf/ +/node_modules/ +/package-lock.json +/target/ diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml index e9a9b32331..082bcb31da 100644 --- a/sdk/Cargo.toml +++ b/sdk/Cargo.toml @@ -77,6 +77,10 @@ solana-program = { path = "program", version = "=1.10.0" } solana-sdk-macro = { path = "macro", version = "=1.10.0" } thiserror = "1.0" uriparse = "0.6.3" +wasm-bindgen = "0.2" + +[target.'cfg(target_arch = "wasm32")'.dependencies] +js-sys = "0.3.55" [dev-dependencies] curve25519-dalek = "3.2.0" diff --git a/sdk/macro/src/lib.rs b/sdk/macro/src/lib.rs index 8ffb6f5467..7c240f4c75 100644 --- a/sdk/macro/src/lib.rs +++ b/sdk/macro/src/lib.rs @@ -373,3 +373,31 @@ pub fn pubkeys(input: TokenStream) -> TokenStream { let pubkeys = parse_macro_input!(input as Pubkeys); TokenStream::from(quote! {#pubkeys}) } + +// The normal `wasm_bindgen` macro generates a .bss section which causes the resulting +// BPF program to fail to load, so for now this stub should be used when building for BPF +#[proc_macro_attribute] +pub fn wasm_bindgen_stub(_attr: TokenStream, item: TokenStream) -> TokenStream { + match parse_macro_input!(item as syn::Item) { + syn::Item::Struct(mut item_struct) => { + if let syn::Fields::Named(fields) = &mut item_struct.fields { + // Strip out any `#[wasm_bindgen]` added to struct fields. This is custom + // syntax supplied by the normal `wasm_bindgen` macro. + for field in fields.named.iter_mut() { + field.attrs.retain(|attr| { + !attr + .path + .segments + .iter() + .any(|segment| segment.ident == "wasm_bindgen") + }); + } + } + quote! { #item_struct } + } + item => { + quote!(#item) + } + } + .into() +} diff --git a/sdk/package.json b/sdk/package.json new file mode 120000 index 0000000000..aa87faef28 --- /dev/null +++ b/sdk/package.json @@ -0,0 +1 @@ +program/package.json \ No newline at end of file diff --git a/sdk/program/.gitignore b/sdk/program/.gitignore new file mode 100644 index 0000000000..936e5c57af --- /dev/null +++ b/sdk/program/.gitignore @@ -0,0 +1,2 @@ +/node_modules/ +/package-lock.json diff --git a/sdk/program/Cargo.toml b/sdk/program/Cargo.toml index e24b9aa4b7..7f4408eab5 100644 --- a/sdk/program/Cargo.toml +++ b/sdk/program/Cargo.toml @@ -42,6 +42,13 @@ libsecp256k1 = "0.6.0" rand = "0.7.0" solana-logger = { path = "../../logger", version = "=1.10.0" } itertools = "0.10.1" +wasm-bindgen = "0.2" + +[target.'cfg(target_arch = "wasm32")'.dependencies] +console_error_panic_hook = "0.1.7" +console_log = "0.2.0" +js-sys = "0.3.55" +getrandom = { version = "0.1", features = ["wasm-bindgen"] } [target.'cfg(not(target_pointer_width = "64"))'.dependencies] parking_lot = "0.11" diff --git a/sdk/program/package.json b/sdk/program/package.json new file mode 100644 index 0000000000..f1f074ff20 --- /dev/null +++ b/sdk/program/package.json @@ -0,0 +1,14 @@ +{ + "devDependencies": { + "chai": "^4.3.4", + "mocha": "^9.1.2", + "prettier": "^2.4.1" + }, + "scripts": { + "postinstall": "npm run build", + "build": "wasm-pack build --target nodejs --dev --out-dir node_modules/crate --out-name crate", + "pretty": "prettier --check 'tests/*.mjs'", + "pretty:fix": "prettier --write 'tests/*.mjs'", + "test": "mocha 'tests/*.mjs'" + } +} diff --git a/sdk/program/src/lib.rs b/sdk/program/src/lib.rs index 3d5768a6d9..8ac5140ad8 100644 --- a/sdk/program/src/lib.rs +++ b/sdk/program/src/lib.rs @@ -52,6 +52,12 @@ pub mod stake_history; pub mod system_instruction; pub mod system_program; pub mod sysvar; +pub mod wasm; + +#[cfg(target_arch = "bpf")] +pub use solana_sdk_macro::wasm_bindgen_stub as wasm_bindgen; +#[cfg(not(target_arch = "bpf"))] +pub use wasm_bindgen::prelude::wasm_bindgen; pub mod config { pub mod program { diff --git a/sdk/program/src/pubkey.rs b/sdk/program/src/pubkey.rs index 395d0c528d..35a6a25eb3 100644 --- a/sdk/program/src/pubkey.rs +++ b/sdk/program/src/pubkey.rs @@ -1,6 +1,6 @@ #![allow(clippy::integer_arithmetic)] use { - crate::{decode_error::DecodeError, hash::hashv}, + crate::{decode_error::DecodeError, hash::hashv, wasm_bindgen}, borsh::{BorshDeserialize, BorshSchema, BorshSerialize}, bytemuck::{Pod, Zeroable}, num_derive::{FromPrimitive, ToPrimitive}, @@ -48,6 +48,7 @@ impl From for PubkeyError { } } +#[wasm_bindgen] #[repr(transparent)] #[derive( AbiExample, @@ -67,7 +68,7 @@ impl From for PubkeyError { Serialize, Zeroable, )] -pub struct Pubkey([u8; 32]); +pub struct Pubkey(pub(crate) [u8; 32]); impl crate::sanitize::Sanitize for Pubkey {} diff --git a/sdk/program/src/wasm/mod.rs b/sdk/program/src/wasm/mod.rs new file mode 100644 index 0000000000..0e8d11e4c9 --- /dev/null +++ b/sdk/program/src/wasm/mod.rs @@ -0,0 +1,16 @@ +//! solana-program Javascript interface +#![cfg(target_arch = "wasm32")] +use wasm_bindgen::prelude::*; + +pub mod pubkey; + +/// Initialize Javascript logging and panic handler +#[wasm_bindgen] +pub fn init() { + std::panic::set_hook(Box::new(console_error_panic_hook::hook)); + console_log::init_with_level(log::Level::Info).unwrap(); +} + +pub fn display_to_jsvalue(display: T) -> JsValue { + display.to_string().into() +} diff --git a/sdk/program/src/wasm/pubkey.rs b/sdk/program/src/wasm/pubkey.rs new file mode 100644 index 0000000000..a3aa279419 --- /dev/null +++ b/sdk/program/src/wasm/pubkey.rs @@ -0,0 +1,121 @@ +//! `Pubkey` Javascript interface +#![cfg(target_arch = "wasm32")] +#![allow(non_snake_case)] +use { + crate::{pubkey::*, wasm::display_to_jsvalue}, + js_sys::{Array, Uint8Array}, + wasm_bindgen::{prelude::*, JsCast}, +}; + +fn js_value_to_seeds_vec(array_of_uint8_arrays: &[JsValue]) -> Result>, JsValue> { + let vec_vec_u8 = array_of_uint8_arrays + .iter() + .filter_map(|u8_array| { + u8_array + .dyn_ref::() + .map(|u8_array| u8_array.to_vec()) + }) + .collect::>(); + + if vec_vec_u8.len() != array_of_uint8_arrays.len() { + Err("Invalid Array of Uint8Arrays".into()) + } else { + Ok(vec_vec_u8) + } +} + +#[wasm_bindgen] +impl Pubkey { + /// Create a new Pubkey object + /// + /// * `value` - optional public key as a base58 encoded string, `Uint8Array`, `[number]` + #[wasm_bindgen(constructor)] + pub fn constructor(value: JsValue) -> Result { + if let Some(base58_str) = value.as_string() { + base58_str.parse::().map_err(display_to_jsvalue) + } else if let Some(uint8_array) = value.dyn_ref::() { + Ok(Pubkey::new(&uint8_array.to_vec())) + } else if let Some(array) = value.dyn_ref::() { + let mut bytes = vec![]; + let iterator = js_sys::try_iter(&array.values())?.expect("array to be iterable"); + for x in iterator { + let x = x?; + + if let Some(n) = x.as_f64() { + if n >= 0. && n <= 255. { + bytes.push(n as u8); + continue; + } + } + return Err(format!("Invalid array argument: {:?}", x).into()); + } + Ok(Pubkey::new(&bytes)) + } else if value.is_undefined() { + Ok(Pubkey::default()) + } else { + Err("Unsupported argument".into()) + } + } + + /// Return the base58 string representation of the public key + pub fn toString(&self) -> String { + self.to_string() + } + + /// Check if a `Pubkey` is on the ed25519 curve. + pub fn isOnCurve(&self) -> bool { + self.is_on_curve() + } + + /// Checks if two `Pubkey`s are equal + pub fn equals(&self, other: &Pubkey) -> bool { + self == other + } + + /// Return the `Uint8Array` representation of the public key + pub fn toBytes(&self) -> Box<[u8]> { + self.0.clone().into() + } + + /// Derive a Pubkey from another Pubkey, string seed, and a program id + pub fn createWithSeed(base: &Pubkey, seed: &str, owner: &Pubkey) -> Result { + Pubkey::create_with_seed(base, seed, owner).map_err(display_to_jsvalue) + } + + /// Derive a program address from seeds and a program id + pub fn createProgramAddress( + seeds: Box<[JsValue]>, + program_id: &Pubkey, + ) -> Result { + let seeds_vec = js_value_to_seeds_vec(&seeds)?; + let seeds_slice = seeds_vec + .iter() + .map(|seed| seed.as_slice()) + .collect::>(); + + Pubkey::create_program_address(seeds_slice.as_slice(), program_id) + .map_err(display_to_jsvalue) + } + + /// Find a valid program address + /// + /// Returns: + /// * `[PubKey, number]` - the program address and bump seed + pub fn findProgramAddress( + seeds: Box<[JsValue]>, + program_id: &Pubkey, + ) -> Result { + let seeds_vec = js_value_to_seeds_vec(&seeds)?; + let seeds_slice = seeds_vec + .iter() + .map(|seed| seed.as_slice()) + .collect::>(); + + let (address, bump_seed) = Pubkey::find_program_address(seeds_slice.as_slice(), program_id); + + let result = Array::new_with_length(2); + result.set(0, address.into()); + result.set(1, bump_seed.into()); + Ok(result.into()) + } +} diff --git a/sdk/program/tests/pubkey.mjs b/sdk/program/tests/pubkey.mjs new file mode 100644 index 0000000000..67ee73ba3d --- /dev/null +++ b/sdk/program/tests/pubkey.mjs @@ -0,0 +1,185 @@ +import { expect } from "chai"; +import { init, Pubkey } from "crate"; +init(); + +// TODO: wasm_bindgen doesn't currently support exporting constants +const MAX_SEED_LEN = 32; + +describe("Pubkey", function () { + it("invalid", () => { + expect(() => { + new Pubkey([ + 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]); + }).to.throw(); + + expect(() => { + new Pubkey([ + 'invalid', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, + ]); + }).to.throw(); + + expect(() => { + new Pubkey( + "0x300000000000000000000000000000000000000000000000000000000000000000000" + ); + }).to.throw(); + + expect(() => { + new Pubkey( + "0x300000000000000000000000000000000000000000000000000000000000000" + ); + }).to.throw(); + + expect(() => { + new Pubkey( + "135693854574979916511997248057056142015550763280047535983739356259273198796800000" + ); + }).to.throw(); + + expect(() => { + new Pubkey("12345"); + }).to.throw(); + }); + + it("toString", () => { + const key = new Pubkey("CiDwVBFgWV9E5MvXWoLgnEgn2hK7rJikbvfWavzAQz3"); + expect(key.toString()).to.eq("CiDwVBFgWV9E5MvXWoLgnEgn2hK7rJikbvfWavzAQz3"); + + const key2 = new Pubkey("1111111111111111111111111111BukQL"); + expect(key2.toString()).to.eq("1111111111111111111111111111BukQL"); + + const key3 = new Pubkey("11111111111111111111111111111111"); + expect(key3.toString()).to.eq("11111111111111111111111111111111"); + + const key4 = new Pubkey([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, + ]); + expect(key4.toString()).to.eq("11111111111111111111111111111111"); + }); + + it("toBytes", () => { + const key = new Pubkey("CiDwVBFgWV9E5MvXWoLgnEgn2hK7rJikbvfWavzAQz3"); + expect(key.toBytes()).to.deep.equal( + new Uint8Array([ + 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + ]) + ); + + const key2 = new Pubkey(); + expect(key2.toBytes()).to.deep.equal( + new Uint8Array([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + ]) + ); + }); + + it("isOnCurve", () => { + let onCurve = new Pubkey("J4NYrSRccTUGXP7wmFwiByakqWKZb5RwpiAoskpgAQRb"); + expect(onCurve.isOnCurve()).to.be.true; + + let offCurve = new Pubkey("12rqwuEgBYiGhBrDJStCiqEtzQpTTiZbh7teNVLuYcFA"); + expect(offCurve.isOnCurve()).to.be.false; + }); + + it("equals", () => { + const arrayKey = new Pubkey([ + 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, + ]); + const base58Key = new Pubkey("CiDwVBFgWV9E5MvXWoLgnEgn2hK7rJikbvfWavzAQz3"); + + expect(arrayKey.equals(base58Key)).to.be.true; + }); + + it("createWithSeed", async () => { + const defaultPublicKey = new Pubkey("11111111111111111111111111111111"); + const derivedKey = Pubkey.createWithSeed( + defaultPublicKey, + "limber chicken: 4/45", + defaultPublicKey + ); + + expect( + derivedKey.equals( + new Pubkey("9h1HyLCW5dZnBVap8C5egQ9Z6pHyjsh5MNy83iPqqRuq") + ) + ).to.be.true; + }); + + it("createProgramAddress", async () => { + const programId = new Pubkey("BPFLoader1111111111111111111111111111111111"); + const publicKey = new Pubkey("SeedPubey1111111111111111111111111111111111"); + + let programAddress = Pubkey.createProgramAddress( + [Buffer.from("", "utf8"), Buffer.from([1])], + programId + ); + expect( + programAddress.equals( + new Pubkey("3gF2KMe9KiC6FNVBmfg9i267aMPvK37FewCip4eGBFcT") + ) + ).to.be.true; + + programAddress = Pubkey.createProgramAddress( + [Buffer.from("☉", "utf8")], + programId + ); + expect( + programAddress.equals( + new Pubkey("7ytmC1nT1xY4RfxCV2ZgyA7UakC93do5ZdyhdF3EtPj7") + ) + ).to.be.true; + + programAddress = Pubkey.createProgramAddress( + [Buffer.from("Talking", "utf8"), Buffer.from("Squirrels", "utf8")], + programId + ); + expect( + programAddress.equals( + new Pubkey("HwRVBufQ4haG5XSgpspwKtNd3PC9GM9m1196uJW36vds") + ) + ).to.be.true; + + programAddress = Pubkey.createProgramAddress( + [publicKey.toBytes()], + programId + ); + expect( + programAddress.equals( + new Pubkey("GUs5qLUfsEHkcMB9T38vjr18ypEhRuNWiePW2LoK4E3K") + ) + ).to.be.true; + + const programAddress2 = Pubkey.createProgramAddress( + [Buffer.from("Talking", "utf8")], + programId + ); + expect(programAddress.equals(programAddress2)).to.eq(false); + + expect(() => { + Pubkey.createProgramAddress([Buffer.alloc(MAX_SEED_LEN + 1)], programId); + }).to.throw(); + }); + + it("findProgramAddress", async () => { + const programId = new Pubkey("BPFLoader1111111111111111111111111111111111"); + let [programAddress, nonce] = Pubkey.findProgramAddress( + [Buffer.from("", "utf8")], + programId + ); + expect( + programAddress.equals( + Pubkey.createProgramAddress( + [Buffer.from("", "utf8"), Buffer.from([nonce])], + programId + ) + ) + ).to.be.true; + }); +}); diff --git a/sdk/src/lib.rs b/sdk/src/lib.rs index af9aa18203..2df94726a1 100644 --- a/sdk/src/lib.rs +++ b/sdk/src/lib.rs @@ -47,6 +47,7 @@ pub mod system_transaction; pub mod timing; pub mod transaction; pub mod transport; +pub mod wasm; /// Same as `declare_id` except report that this id has been deprecated pub use solana_sdk_macro::declare_deprecated_id; diff --git a/sdk/src/signer/keypair.rs b/sdk/src/signer/keypair.rs index 3a80cf308e..63283d403e 100644 --- a/sdk/src/signer/keypair.rs +++ b/sdk/src/signer/keypair.rs @@ -17,9 +17,11 @@ use { io::{Read, Write}, path::Path, }, + wasm_bindgen::prelude::*, }; /// A vanilla Ed25519 key pair +#[wasm_bindgen] #[derive(Debug)] pub struct Keypair(ed25519_dalek::Keypair); diff --git a/sdk/src/wasm/keypair.rs b/sdk/src/wasm/keypair.rs new file mode 100644 index 0000000000..6f2ffebbb7 --- /dev/null +++ b/sdk/src/wasm/keypair.rs @@ -0,0 +1,34 @@ +//! `Keypair` Javascript interface +#![cfg(target_arch = "wasm32")] +#![allow(non_snake_case)] +use { + crate::signer::{keypair::Keypair, Signer}, + solana_program::{pubkey::Pubkey, wasm::display_to_jsvalue}, + wasm_bindgen::prelude::*, +}; + +#[wasm_bindgen] +impl Keypair { + /// Create a new `Keypair ` + #[wasm_bindgen(constructor)] + pub fn constructor() -> Keypair { + Keypair::new() + } + + /// Convert a `Keypair` to a `Uint8Array` + pub fn toBytes(&self) -> Box<[u8]> { + self.to_bytes().into() + } + + /// Recover a `Keypair` from a `Uint8Array` + pub fn fromBytes(bytes: &[u8]) -> Result { + Keypair::from_bytes(bytes).map_err(display_to_jsvalue) + } + + /// Return the `Pubkey` for this `Keypair` + #[wasm_bindgen(js_name = pubkey)] + pub fn js_pubkey(&self) -> Pubkey { + // `wasm_bindgen` does not support traits (`Signer) yet + self.pubkey() + } +} diff --git a/sdk/src/wasm/mod.rs b/sdk/src/wasm/mod.rs new file mode 100644 index 0000000000..021f9508fb --- /dev/null +++ b/sdk/src/wasm/mod.rs @@ -0,0 +1,4 @@ +//! solana-sdk Javascript interface +#![cfg(target_arch = "wasm32")] + +pub mod keypair; diff --git a/sdk/tests/keypair.mjs b/sdk/tests/keypair.mjs new file mode 100644 index 0000000000..092ba511ba --- /dev/null +++ b/sdk/tests/keypair.mjs @@ -0,0 +1,14 @@ +import { expect } from "chai"; +import { init, Keypair } from "crate"; +init(); + +describe("Keypair", function () { + it("works", () => { + const keypair = new Keypair(); + let bytes = keypair.toBytes(); + expect(bytes).to.have.length(64); + + const recoveredKeypair = Keypair.fromBytes(bytes); + expect(keypair.pubkey().equals(recoveredKeypair.pubkey())); + }); +});