From 03a956e8d96e8d88fd9df2fd8465ed0af09d82f4 Mon Sep 17 00:00:00 2001 From: Michael Vines Date: Mon, 18 Oct 2021 22:01:39 -0700 Subject: [PATCH] Add wasm bindings for `Hash` --- sdk/program/src/hash.rs | 4 +- sdk/program/src/wasm/hash.rs | 57 +++++++++++++++++++++++++ sdk/program/src/wasm/mod.rs | 10 ++++- sdk/program/tests/hash.mjs | 81 ++++++++++++++++++++++++++++++++++++ 4 files changed, 149 insertions(+), 3 deletions(-) create mode 100644 sdk/program/src/wasm/hash.rs create mode 100644 sdk/program/tests/hash.mjs diff --git a/sdk/program/src/hash.rs b/sdk/program/src/hash.rs index c150268cd3..66f5ecaa98 100644 --- a/sdk/program/src/hash.rs +++ b/sdk/program/src/hash.rs @@ -1,7 +1,7 @@ //! The `hash` module provides functions for creating SHA-256 hashes. use { - crate::sanitize::Sanitize, + crate::{sanitize::Sanitize, wasm_bindgen}, borsh::{BorshDeserialize, BorshSchema, BorshSerialize}, sha2::{Digest, Sha256}, std::{convert::TryFrom, fmt, mem, str::FromStr}, @@ -11,6 +11,8 @@ use { pub const HASH_BYTES: usize = 32; /// Maximum string length of a base58 encoded hash const MAX_BASE58_LEN: usize = 44; + +#[wasm_bindgen] #[derive( Serialize, Deserialize, diff --git a/sdk/program/src/wasm/hash.rs b/sdk/program/src/wasm/hash.rs new file mode 100644 index 0000000000..add1e6bbe8 --- /dev/null +++ b/sdk/program/src/wasm/hash.rs @@ -0,0 +1,57 @@ +//! `Hash` Javascript interface +#![cfg(target_arch = "wasm32")] +#![allow(non_snake_case)] +use { + crate::{hash::*, wasm::display_to_jsvalue}, + js_sys::{Array, Uint8Array}, + wasm_bindgen::{prelude::*, JsCast}, +}; + +#[wasm_bindgen] +impl Hash { + /// Create a new Hash object + /// + /// * `value` - optional hash 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(Hash::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(Hash::new(&bytes)) + } else if value.is_undefined() { + Ok(Hash::default()) + } else { + Err("Unsupported argument".into()) + } + } + + /// Return the base58 string representation of the hash + pub fn toString(&self) -> String { + self.to_string() + } + + /// Checks if two `Hash`s are equal + pub fn equals(&self, other: &Hash) -> bool { + self == other + } + + /// Return the `Uint8Array` representation of the hash + pub fn toBytes(&self) -> Box<[u8]> { + self.0.clone().into() + } +} diff --git a/sdk/program/src/wasm/mod.rs b/sdk/program/src/wasm/mod.rs index 0e8d11e4c9..63d06f9ce0 100644 --- a/sdk/program/src/wasm/mod.rs +++ b/sdk/program/src/wasm/mod.rs @@ -2,13 +2,19 @@ #![cfg(target_arch = "wasm32")] use wasm_bindgen::prelude::*; +pub mod hash; 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(); + use std::sync::Once; + static INIT: Once = Once::new(); + + INIT.call_once(|| { + 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 { diff --git a/sdk/program/tests/hash.mjs b/sdk/program/tests/hash.mjs new file mode 100644 index 0000000000..4b25857a49 --- /dev/null +++ b/sdk/program/tests/hash.mjs @@ -0,0 +1,81 @@ +import { expect } from "chai"; +import { init, Hash } from "crate"; +init(); + +// TODO: wasm_bindgen doesn't currently support exporting constants +const HASH_BYTES = 32; + +describe("Hash", function () { + it("invalid", () => { + expect(() => { + new Hash([ + 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 Hash([ + '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 Hash( + "0x300000000000000000000000000000000000000000000000000000000000000000000" + ); + }).to.throw(); + + expect(() => { + new Hash( + "0x300000000000000000000000000000000000000000000000000000000000000" + ); + }).to.throw(); + + expect(() => { + new Hash( + "135693854574979916511997248057056142015550763280047535983739356259273198796800000" + ); + }).to.throw(); + + expect(() => { + new Hash("12345"); + }).to.throw(); + }); + + it("toString", () => { + const key = new Hash("CiDwVBFgWV9E5MvXWoLgnEgn2hK7rJikbvfWavzAQz3"); + expect(key.toString()).to.eq("CiDwVBFgWV9E5MvXWoLgnEgn2hK7rJikbvfWavzAQz3"); + + const key2 = new Hash("1111111111111111111111111111BukQL"); + expect(key2.toString()).to.eq("1111111111111111111111111111BukQL"); + + const key3 = new Hash("11111111111111111111111111111111"); + expect(key3.toString()).to.eq("11111111111111111111111111111111"); + + const key4 = new Hash([ + 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 Hash("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 Hash(); + 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, + ]) + ); + }); +});