diff --git a/justfile b/justfile new file mode 100644 index 0000000..8972194 --- /dev/null +++ b/justfile @@ -0,0 +1,8 @@ +build-web: + @mkdir -p dist + cargo build --release --target wasm32-unknown-unknown + @cp ./target/wasm32-unknown-unknown/release/pjs.wasm dist/ + wasm-bindgen --out-dir dist --target web --no-typescript --remove-name-section dist/pjs.wasm + +test: + cargo test diff --git a/libwallet/Cargo.toml b/libwallet/Cargo.toml index 30c11da..e9fa705 100644 --- a/libwallet/Cargo.toml +++ b/libwallet/Cargo.toml @@ -15,7 +15,7 @@ serde = {version = "1.0", default-features = false, features = ["derive"], optio hmac = {version = "0.12.1", default-features = false, optional = true} pbkdf2 = {version = "0.11.0", default-features = false, optional = true} sha2 = {version = "0.10.2", default-features = false, optional = true} -pjs = { git= "https://github.com/S0c5/pjs-rs.git", branch = "feat/get-current-account", optional = true } +pjs = { path = "../pjs-rs", optional = true } mnemonic = { package = "bip0039", version = "0.10.1", default-features = false, optional = true} rand_core = {version = "0.6.3", optional = true} diff --git a/libwallet/examples/account_generation.rs b/libwallet/examples/account_generation.rs index 001db03..c34b8c4 100644 --- a/libwallet/examples/account_generation.rs +++ b/libwallet/examples/account_generation.rs @@ -1,4 +1,4 @@ -use libwallet::{self, vault}; +use libwallet::{self, vault, Account}; use std::env; type Wallet = libwallet::Wallet>; @@ -16,9 +16,9 @@ async fn main() -> Result<(), Box> { let mut wallet = Wallet::new(vault); wallet.unlock(None, None).await?; - let account = wallet.default_signer(); + let account = wallet.default_account(); println!("Secret phrase: \"{phrase}\""); - // println!("Default Account: 0x{account}"); + println!("Default Account: 0x{:?}", account.unwrap()); Ok(()) } diff --git a/libwallet/examples/persisted_in_keyring.rs b/libwallet/examples/persisted_in_keyring.rs index aea8c94..d3a1840 100644 --- a/libwallet/examples/persisted_in_keyring.rs +++ b/libwallet/examples/persisted_in_keyring.rs @@ -18,8 +18,8 @@ async fn main() -> Result<(), Box> { wallet.unlock(None, pin).await?; - let account = wallet.default_signer(); - // println!("Default account: {}", account); + let account = wallet.default_account(); + println!("Default account: {}", account.unwrap()); Ok(()) } diff --git a/libwallet/examples/persisted_in_pass.rs b/libwallet/examples/persisted_in_pass.rs index 6fa8676..9c1b173 100644 --- a/libwallet/examples/persisted_in_pass.rs +++ b/libwallet/examples/persisted_in_pass.rs @@ -1,7 +1,8 @@ use dirs::home_dir; use libwallet::{self, vault::Pass, Language}; use std::error::Error; -type Wallet = libwallet::Wallet; +type PassVault = Pass; +type Wallet = libwallet::Wallet; #[async_std::main] async fn main() -> Result<(), Box> { @@ -15,8 +16,8 @@ async fn main() -> Result<(), Box> { wallet.unlock(None, account).await?; - let account = wallet.default_signer(); - // println!("Default account: {}", account); + let account = wallet.default_account(); + println!("Default account: {}", account.unwrap()); Ok(()) } diff --git a/libwallet/src/account.rs b/libwallet/src/account.rs index f48228a..f6d3d9a 100644 --- a/libwallet/src/account.rs +++ b/libwallet/src/account.rs @@ -1,10 +1,9 @@ +use core::fmt::{Debug, Display}; + use crate::{ - any::{self, AnySignature}, - Derive, Network, Pair, Public, - Signer, + Public, Signer, }; - -pub trait Account: Signer { +pub trait Account: Signer + Display { fn public(&self) -> impl Public; } diff --git a/libwallet/src/key_pair.rs b/libwallet/src/key_pair.rs index b72cec4..9004e82 100644 --- a/libwallet/src/key_pair.rs +++ b/libwallet/src/key_pair.rs @@ -26,8 +26,8 @@ impl Signature for Bytes {} /// Something that can sign messages pub trait Signer { type Signature: Signature; - async fn sign_msg>(&self, data: M) -> Result; - async fn verify>(&self, msg: M, sig: &[u8]) -> bool; + async fn sign_msg(&self, data: impl AsRef<[u8]>) -> Result; + async fn verify(&self, msg: impl AsRef<[u8]>, sig: impl AsRef<[u8]>) -> bool; } /// Wrappers to represent any supported key pair. pub mod any { @@ -94,14 +94,14 @@ pub mod any { impl super::Signer for Pair { type Signature = AnySignature; - async fn sign_msg>(&self, msg: M) -> Result { + async fn sign_msg(&self, msg: impl AsRef<[u8]>) -> Result { match self { #[cfg(feature = "sr25519")] Pair::Sr25519(p) => Ok(p.sign_msg(msg).await?.into()), } } - async fn verify>(&self, msg: M, sig: &[u8]) -> bool { + async fn verify(&self, msg: impl AsRef<[u8]>, sig: impl AsRef<[u8]>) -> bool { match self { #[cfg(feature = "sr25519")] Pair::Sr25519(p) => super::Signer::verify(p, msg, sig).await, @@ -200,13 +200,13 @@ pub mod sr25519 { impl Signer for Pair { type Signature = Signature; - async fn sign_msg>(&self, msg: M) -> Result { + async fn sign_msg(&self, msg: impl AsRef<[u8]>) -> Result { let context = signing_context(SIGNING_CTX); Ok(self.sign(context.bytes(msg.as_ref())).to_bytes()) } - async fn verify>(&self, msg: M, sig: &[u8]) -> bool { - let sig = match schnorrkel::Signature::from_bytes(sig) { + async fn verify(&self, msg: impl AsRef<[u8]>, sig: impl AsRef<[u8]>) -> bool { + let sig = match schnorrkel::Signature::from_bytes(sig.as_ref()) { Ok(s) => s, Err(_) => return false, }; diff --git a/libwallet/src/lib.rs b/libwallet/src/lib.rs index 7b7ded4..54a38a1 100644 --- a/libwallet/src/lib.rs +++ b/libwallet/src/lib.rs @@ -14,7 +14,7 @@ pub mod util; #[cfg(feature = "substrate")] mod substrate_ext; -use account::Account; +pub use account::Account; use arrayvec::ArrayVec; use core::{cell::RefCell, convert::TryInto, fmt}; use key_pair::any::AnySignature; @@ -66,7 +66,7 @@ where } /// Get the account currently set as default - pub fn default_signer(&self) -> Option<&V::Account> { + pub fn default_account(&self) -> Option<&V::Account> { self.default_account.map(|x| &self.accounts[x as usize]) } @@ -132,13 +132,13 @@ where /// let msg = &[0x12, 0x34, 0x56]; /// let signature = wallet.sign(msg).await.expect("it must sign"); /// - /// assert!(wallet.default_signer().expect("it must have a default signer").verify(msg, signature.as_ref()).await); + /// assert!(wallet.default_account().expect("it must have a default signer").verify(msg, signature.as_ref()).await); /// # Ok(()) } /// ``` pub async fn sign(&self, message: &[u8]) -> Result { assert!(!self.is_locked()); - let Some(signer) = self.default_signer() else { + let Some(signer) = self.default_account() else { return Err(()); }; @@ -194,7 +194,7 @@ where for (msg, a) in self.pending_sign.take() { let signer = a .map(|idx| self.account(idx)) - .unwrap_or_else(|| self.default_signer().expect("Signer not set")); + .unwrap_or_else(|| self.default_account().expect("Signer not set")); let message = signer.sign_msg(&msg).await?; signatures.push(message); diff --git a/libwallet/src/vault.rs b/libwallet/src/vault.rs index f5b20a8..69fbd1b 100644 --- a/libwallet/src/vault.rs +++ b/libwallet/src/vault.rs @@ -2,21 +2,22 @@ #[cfg(feature = "vault_os")] mod os; -#[cfg(feature = "vault_os")] -pub use os::*; #[cfg(feature = "vault_pjs")] pub mod pjs; + +#[cfg(feature = "vault_pjs")] +pub use pjs::*; + #[cfg(feature = "vault_pass")] mod pass; -mod simple; #[cfg(feature = "vault_pass")] pub use pass::*; + +mod simple; pub use simple::*; -#[cfg(feature = "vault_pjs")] -pub use pjs::*; use crate::account::Account; @@ -140,7 +141,7 @@ mod utils { impl crate::Signer for AccountSigner { type Signature = AnySignature; - async fn sign_msg>(&self, msg: M) -> Result { + async fn sign_msg(&self, msg: impl AsRef<[u8]>) -> Result { Ok(self .pair .as_ref() @@ -149,7 +150,7 @@ mod utils { .await?) } - async fn verify>(&self, msg: M, sig: &[u8]) -> bool { + async fn verify(&self, msg: impl AsRef<[u8]>, sig: impl AsRef<[u8]>) -> bool { self.pair .as_ref() .expect("account unlocked") diff --git a/libwallet/src/vault/pass.rs b/libwallet/src/vault/pass.rs index e4749bb..a62ae56 100644 --- a/libwallet/src/vault/pass.rs +++ b/libwallet/src/vault/pass.rs @@ -1,3 +1,5 @@ +use core::marker::PhantomData; + use arrayvec::ArrayVec; use mnemonic::Language; use prs_lib::{ @@ -14,15 +16,16 @@ use crate::{ }; /// A vault that stores secrets in a `pass` compatible repository -pub struct Pass { +pub struct Pass { store: Store, root: Option, auto_generate: Option, + _phantom_data: PhantomData, } const DEFAULT_DIR: &str = "libwallet_accounts/"; -impl Pass { +impl Pass { /// Create a new `Pass` vault in the given location. /// The optional `lang` instructs the vault to generarte a backup phrase /// in the given language in case one does not exist. @@ -33,6 +36,7 @@ impl Pass { store, root: None, auto_generate: lang.into(), + _phantom_data: Default::default(), } } @@ -135,8 +139,8 @@ impl From for PassCreds { } } -impl Vault for Pass { - type Id = Option>; +impl> Vault for Pass { + type Id = Option; type Credentials = PassCreds; type Account = AccountSigner; type Error = Error; @@ -155,10 +159,7 @@ impl Vault for Pass { .and_then(|l| self.generate(&credentials, l)) }) .map(|r| { - let acc = AccountSigner::new(path.as_ref().map(|x| { - core::str::from_utf8(x.as_slice()).expect("it must be a valid utf8 string") - })) - .unlock(&r); + let acc = AccountSigner::new(path.as_ref().map(|x| x.as_ref())).unlock(&r); self.root = Some(r); acc }) diff --git a/libwallet/src/vault/simple.rs b/libwallet/src/vault/simple.rs index 7ff4078..ae003e5 100644 --- a/libwallet/src/vault/simple.rs +++ b/libwallet/src/vault/simple.rs @@ -105,10 +105,6 @@ impl> Vault for Simple { self.unlocked = self.locked.take(); let pin = creds.into(); let root_account = self.get_key(pin.unwrap_or_default())?; - // let path = path.as_ref().map(|r| { - // core::str::from_utf8(r.as_slice()) - // .expect("it must be a valid utf8 string") - // }); let path = path.as_ref().map(|x| x.as_ref()); Ok(AccountSigner::new(path).unlock(&root_account)) } diff --git a/pjs-rs/Cargo.toml b/pjs-rs/Cargo.toml new file mode 100644 index 0000000..bed4478 --- /dev/null +++ b/pjs-rs/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "pjs" +version = "0.1.0" +edition = "2021" + +[dependencies] +wasm-bindgen = { version = "0.2.92", default-features = false } +wasm-bindgen-futures = "0.4.42" + +[dependencies.web-sys] +version = "0.3.69" +features = [ + "Window", +] + +[lib] +crate-type = ["cdylib"] diff --git a/pjs-rs/dist/pjs.js b/pjs-rs/dist/pjs.js new file mode 100644 index 0000000..4d21846 --- /dev/null +++ b/pjs-rs/dist/pjs.js @@ -0,0 +1,682 @@ +let wasm; + +const heap = new Array(128).fill(undefined); + +heap.push(undefined, null, true, false); + +function getObject(idx) { return heap[idx]; } + +let heap_next = heap.length; + +function dropObject(idx) { + if (idx < 132) return; + heap[idx] = heap_next; + heap_next = idx; +} + +function takeObject(idx) { + const ret = getObject(idx); + dropObject(idx); + return ret; +} + +function addHeapObject(obj) { + if (heap_next === heap.length) heap.push(heap.length + 1); + const idx = heap_next; + heap_next = heap[idx]; + + heap[idx] = obj; + return idx; +} + +const cachedTextDecoder = (typeof TextDecoder !== 'undefined' ? new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }) : { decode: () => { throw Error('TextDecoder not available') } } ); + +if (typeof TextDecoder !== 'undefined') { cachedTextDecoder.decode(); }; + +let cachedUint8Memory0 = null; + +function getUint8Memory0() { + if (cachedUint8Memory0 === null || cachedUint8Memory0.byteLength === 0) { + cachedUint8Memory0 = new Uint8Array(wasm.memory.buffer); + } + return cachedUint8Memory0; +} + +function getStringFromWasm0(ptr, len) { + ptr = ptr >>> 0; + return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len)); +} + +let WASM_VECTOR_LEN = 0; + +const cachedTextEncoder = (typeof TextEncoder !== 'undefined' ? new TextEncoder('utf-8') : { encode: () => { throw Error('TextEncoder not available') } } ); + +const encodeString = (typeof cachedTextEncoder.encodeInto === 'function' + ? function (arg, view) { + return cachedTextEncoder.encodeInto(arg, view); +} + : function (arg, view) { + const buf = cachedTextEncoder.encode(arg); + view.set(buf); + return { + read: arg.length, + written: buf.length + }; +}); + +function passStringToWasm0(arg, malloc, realloc) { + + if (realloc === undefined) { + const buf = cachedTextEncoder.encode(arg); + const ptr = malloc(buf.length, 1) >>> 0; + getUint8Memory0().subarray(ptr, ptr + buf.length).set(buf); + WASM_VECTOR_LEN = buf.length; + return ptr; + } + + let len = arg.length; + let ptr = malloc(len, 1) >>> 0; + + const mem = getUint8Memory0(); + + let offset = 0; + + for (; offset < len; offset++) { + const code = arg.charCodeAt(offset); + if (code > 0x7F) break; + mem[ptr + offset] = code; + } + + if (offset !== len) { + if (offset !== 0) { + arg = arg.slice(offset); + } + ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0; + const view = getUint8Memory0().subarray(ptr + offset, ptr + len); + const ret = encodeString(arg, view); + + offset += ret.written; + ptr = realloc(ptr, len, offset, 1) >>> 0; + } + + WASM_VECTOR_LEN = offset; + return ptr; +} + +function isLikeNone(x) { + return x === undefined || x === null; +} + +let cachedInt32Memory0 = null; + +function getInt32Memory0() { + if (cachedInt32Memory0 === null || cachedInt32Memory0.byteLength === 0) { + cachedInt32Memory0 = new Int32Array(wasm.memory.buffer); + } + return cachedInt32Memory0; +} + +function debugString(val) { + // primitive types + const type = typeof val; + if (type == 'number' || type == 'boolean' || val == null) { + return `${val}`; + } + if (type == 'string') { + return `"${val}"`; + } + if (type == 'symbol') { + const description = val.description; + if (description == null) { + return 'Symbol'; + } else { + return `Symbol(${description})`; + } + } + if (type == 'function') { + const name = val.name; + if (typeof name == 'string' && name.length > 0) { + return `Function(${name})`; + } else { + return 'Function'; + } + } + // objects + if (Array.isArray(val)) { + const length = val.length; + let debug = '['; + if (length > 0) { + debug += debugString(val[0]); + } + for(let i = 1; i < length; i++) { + debug += ', ' + debugString(val[i]); + } + debug += ']'; + return debug; + } + // Test for built-in + const builtInMatches = /\[object ([^\]]+)\]/.exec(toString.call(val)); + let className; + if (builtInMatches.length > 1) { + className = builtInMatches[1]; + } else { + // Failed to match the standard '[object ClassName]' + return toString.call(val); + } + if (className == 'Object') { + // we're a user defined class or Object + // JSON.stringify avoids problems with cycles, and is generally much + // easier than looping through ownProperties of `val`. + try { + return 'Object(' + JSON.stringify(val) + ')'; + } catch (_) { + return 'Object'; + } + } + // errors + if (val instanceof Error) { + return `${val.name}: ${val.message}\n${val.stack}`; + } + // TODO we could test for more things here, like `Set`s and `Map`s. + return className; +} + +const CLOSURE_DTORS = (typeof FinalizationRegistry === 'undefined') + ? { register: () => {}, unregister: () => {} } + : new FinalizationRegistry(state => { + wasm.__wbindgen_export_2.get(state.dtor)(state.a, state.b) +}); + +function makeMutClosure(arg0, arg1, dtor, f) { + const state = { a: arg0, b: arg1, cnt: 1, dtor }; + const real = (...args) => { + // First up with a closure we increment the internal reference + // count. This ensures that the Rust closure environment won't + // be deallocated while we're invoking it. + state.cnt++; + const a = state.a; + state.a = 0; + try { + return f(a, state.b, ...args); + } finally { + if (--state.cnt === 0) { + wasm.__wbindgen_export_2.get(state.dtor)(a, state.b); + CLOSURE_DTORS.unregister(state); + } else { + state.a = a; + } + } + }; + real.original = state; + CLOSURE_DTORS.register(real, state, state); + return real; +} +function __wbg_adapter_22(arg0, arg1, arg2) { + wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h0b05bfd8b9210c14(arg0, arg1, addHeapObject(arg2)); +} + +let cachedUint32Memory0 = null; + +function getUint32Memory0() { + if (cachedUint32Memory0 === null || cachedUint32Memory0.byteLength === 0) { + cachedUint32Memory0 = new Uint32Array(wasm.memory.buffer); + } + return cachedUint32Memory0; +} + +function getArrayJsValueFromWasm0(ptr, len) { + ptr = ptr >>> 0; + const mem = getUint32Memory0(); + const slice = mem.subarray(ptr / 4, ptr / 4 + len); + const result = []; + for (let i = 0; i < slice.length; i++) { + result.push(takeObject(slice[i])); + } + return result; +} + +function handleError(f, args) { + try { + return f.apply(this, args); + } catch (e) { + wasm.__wbindgen_exn_store(addHeapObject(e)); + } +} +function __wbg_adapter_59(arg0, arg1, arg2, arg3) { + wasm.wasm_bindgen__convert__closures__invoke2_mut__h5a1d3288f9952199(arg0, arg1, addHeapObject(arg2), addHeapObject(arg3)); +} + +/** +*/ +export const Error = Object.freeze({ ExtensionUnavailable:0,"0":"ExtensionUnavailable",NoPermission:1,"1":"NoPermission",FailedFetchingAccounts:2,"2":"FailedFetchingAccounts",NoAccountSelected:3,"3":"NoAccountSelected",NoAccounts:4,"4":"NoAccounts",Sign:5,"5":"Sign", }); +/** +*/ +export const Network = Object.freeze({ Generic:0,"0":"Generic",Kusama:1,"1":"Kusama",Polkadot:2,"2":"Polkadot",Kreivo:3,"3":"Kreivo", }); + +const AccountFinalization = (typeof FinalizationRegistry === 'undefined') + ? { register: () => {}, unregister: () => {} } + : new FinalizationRegistry(ptr => wasm.__wbg_account_free(ptr >>> 0)); +/** +*/ +export class Account { + + static __wrap(ptr) { + ptr = ptr >>> 0; + const obj = Object.create(Account.prototype); + obj.__wbg_ptr = ptr; + AccountFinalization.register(obj, obj.__wbg_ptr, obj); + return obj; + } + + __destroy_into_raw() { + const ptr = this.__wbg_ptr; + this.__wbg_ptr = 0; + AccountFinalization.unregister(this); + return ptr; + } + + free() { + const ptr = this.__destroy_into_raw(); + wasm.__wbg_account_free(ptr); + } + /** + * @param {string} name + * @param {string} address + * @param {Network} net + */ + constructor(name, address, net) { + const ptr0 = passStringToWasm0(name, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + const ptr1 = passStringToWasm0(address, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len1 = WASM_VECTOR_LEN; + const ret = wasm.account_new(ptr0, len0, ptr1, len1, net); + this.__wbg_ptr = ret >>> 0; + return this; + } + /** + * @returns {string} + */ + get name() { + let deferred1_0; + let deferred1_1; + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + wasm.account_name(retptr, this.__wbg_ptr); + var r0 = getInt32Memory0()[retptr / 4 + 0]; + var r1 = getInt32Memory0()[retptr / 4 + 1]; + deferred1_0 = r0; + deferred1_1 = r1; + return getStringFromWasm0(r0, r1); + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + wasm.__wbindgen_free(deferred1_0, deferred1_1, 1); + } + } + /** + * @returns {string} + */ + get address() { + let deferred1_0; + let deferred1_1; + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + wasm.account_address(retptr, this.__wbg_ptr); + var r0 = getInt32Memory0()[retptr / 4 + 0]; + var r1 = getInt32Memory0()[retptr / 4 + 1]; + deferred1_0 = r0; + deferred1_1 = r1; + return getStringFromWasm0(r0, r1); + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + wasm.__wbindgen_free(deferred1_0, deferred1_1, 1); + } + } + /** + * @returns {Network} + */ + get network() { + const ret = wasm.account_network(this.__wbg_ptr); + return ret; + } +} + +const PjsExtensionFinalization = (typeof FinalizationRegistry === 'undefined') + ? { register: () => {}, unregister: () => {} } + : new FinalizationRegistry(ptr => wasm.__wbg_pjsextension_free(ptr >>> 0)); +/** +*/ +export class PjsExtension { + + static __wrap(ptr) { + ptr = ptr >>> 0; + const obj = Object.create(PjsExtension.prototype); + obj.__wbg_ptr = ptr; + PjsExtensionFinalization.register(obj, obj.__wbg_ptr, obj); + return obj; + } + + __destroy_into_raw() { + const ptr = this.__wbg_ptr; + this.__wbg_ptr = 0; + PjsExtensionFinalization.unregister(this); + return ptr; + } + + free() { + const ptr = this.__destroy_into_raw(); + wasm.__wbg_pjsextension_free(ptr); + } + /** + * @param {string} app_name + * @returns {Promise} + */ + static connect(app_name) { + const ptr0 = passStringToWasm0(app_name, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + const ret = wasm.pjsextension_connect(ptr0, len0); + return takeObject(ret); + } + /** + * @param {number} idx + */ + selectAccount(idx) { + wasm.pjsextension_selectAccount(this.__wbg_ptr, idx); + } + /** + * @param {string} payload + * @param {Function} cb + * @returns {Promise} + */ + sign(payload, cb) { + const ptr0 = passStringToWasm0(payload, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + const ret = wasm.pjsextension_sign(this.__wbg_ptr, ptr0, len0, addHeapObject(cb)); + return takeObject(ret); + } + /** + * @returns {Promise} + */ + fetchAccounts() { + const ret = wasm.pjsextension_fetchAccounts(this.__wbg_ptr); + return takeObject(ret); + } + /** + * @returns {(Account)[]} + */ + get accounts() { + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + wasm.pjsextension_accounts(retptr, this.__wbg_ptr); + var r0 = getInt32Memory0()[retptr / 4 + 0]; + var r1 = getInt32Memory0()[retptr / 4 + 1]; + var v1 = getArrayJsValueFromWasm0(r0, r1).slice(); + wasm.__wbindgen_free(r0, r1 * 4, 4); + return v1; + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + } + } + /** + * @returns {Account | undefined} + */ + get selectedAccount() { + const ret = wasm.pjsextension_selectedAccount(this.__wbg_ptr); + return ret === 0 ? undefined : Account.__wrap(ret); + } +} + +async function __wbg_load(module, imports) { + if (typeof Response === 'function' && module instanceof Response) { + if (typeof WebAssembly.instantiateStreaming === 'function') { + try { + return await WebAssembly.instantiateStreaming(module, imports); + + } catch (e) { + if (module.headers.get('Content-Type') != 'application/wasm') { + console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e); + + } else { + throw e; + } + } + } + + const bytes = await module.arrayBuffer(); + return await WebAssembly.instantiate(bytes, imports); + + } else { + const instance = await WebAssembly.instantiate(module, imports); + + if (instance instanceof WebAssembly.Instance) { + return { instance, module }; + + } else { + return instance; + } + } +} + +function __wbg_get_imports() { + const imports = {}; + imports.wbg = {}; + imports.wbg.__wbindgen_object_drop_ref = function(arg0) { + takeObject(arg0); + }; + imports.wbg.__wbindgen_object_clone_ref = function(arg0) { + const ret = getObject(arg0); + return addHeapObject(ret); + }; + imports.wbg.__wbindgen_string_new = function(arg0, arg1) { + const ret = getStringFromWasm0(arg0, arg1); + return addHeapObject(ret); + }; + imports.wbg.__wbindgen_number_new = function(arg0) { + const ret = arg0; + return addHeapObject(ret); + }; + imports.wbg.__wbg_pjsextension_new = function(arg0) { + const ret = PjsExtension.__wrap(arg0); + return addHeapObject(ret); + }; + imports.wbg.__wbg_account_new = function(arg0) { + const ret = Account.__wrap(arg0); + return addHeapObject(ret); + }; + imports.wbg.__wbindgen_string_get = function(arg0, arg1) { + const obj = getObject(arg1); + const ret = typeof(obj) === 'string' ? obj : undefined; + var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + var len1 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len1; + getInt32Memory0()[arg0 / 4 + 0] = ptr1; + }; + imports.wbg.__wbindgen_cb_drop = function(arg0) { + const obj = takeObject(arg0).original; + if (obj.cnt-- == 1) { + obj.a = 0; + return true; + } + const ret = false; + return ret; + }; + imports.wbg.__wbg_instanceof_Window_f401953a2cf86220 = function(arg0) { + let result; + try { + result = getObject(arg0) instanceof Window; + } catch (_) { + result = false; + } + const ret = result; + return ret; + }; + imports.wbg.__wbg_get_d36d61640bbf4503 = function(arg0, arg1, arg2) { + const ret = getObject(arg0)[getStringFromWasm0(arg1, arg2)]; + return isLikeNone(ret) ? 0 : addHeapObject(ret); + }; + imports.wbg.__wbg_queueMicrotask_481971b0d87f3dd4 = function(arg0) { + queueMicrotask(getObject(arg0)); + }; + imports.wbg.__wbg_queueMicrotask_3cbae2ec6b6cd3d6 = function(arg0) { + const ret = getObject(arg0).queueMicrotask; + return addHeapObject(ret); + }; + imports.wbg.__wbindgen_is_function = function(arg0) { + const ret = typeof(getObject(arg0)) === 'function'; + return ret; + }; + imports.wbg.__wbg_get_bd8e338fbd5f5cc8 = function(arg0, arg1) { + const ret = getObject(arg0)[arg1 >>> 0]; + return addHeapObject(ret); + }; + imports.wbg.__wbg_length_cd7af8117672b8b8 = function(arg0) { + const ret = getObject(arg0).length; + return ret; + }; + imports.wbg.__wbg_newnoargs_e258087cd0daa0ea = function(arg0, arg1) { + const ret = new Function(getStringFromWasm0(arg0, arg1)); + return addHeapObject(ret); + }; + imports.wbg.__wbg_get_e3c254076557e348 = function() { return handleError(function (arg0, arg1) { + const ret = Reflect.get(getObject(arg0), getObject(arg1)); + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_call_27c0f87801dedf93 = function() { return handleError(function (arg0, arg1) { + const ret = getObject(arg0).call(getObject(arg1)); + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_new_72fb9a18b5ae2624 = function() { + const ret = new Object(); + return addHeapObject(ret); + }; + imports.wbg.__wbg_self_ce0dbfc45cf2f5be = function() { return handleError(function () { + const ret = self.self; + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_window_c6fb939a7f436783 = function() { return handleError(function () { + const ret = window.window; + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_globalThis_d1e6af4856ba331b = function() { return handleError(function () { + const ret = globalThis.globalThis; + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_global_207b558942527489 = function() { return handleError(function () { + const ret = global.global; + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbindgen_is_undefined = function(arg0) { + const ret = getObject(arg0) === undefined; + return ret; + }; + imports.wbg.__wbg_from_89e3fc3ba5e6fb48 = function(arg0) { + const ret = Array.from(getObject(arg0)); + return addHeapObject(ret); + }; + imports.wbg.__wbg_call_b3ca7c6051f9bec1 = function() { return handleError(function (arg0, arg1, arg2) { + const ret = getObject(arg0).call(getObject(arg1), getObject(arg2)); + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_new_81740750da40724f = function(arg0, arg1) { + try { + var state0 = {a: arg0, b: arg1}; + var cb0 = (arg0, arg1) => { + const a = state0.a; + state0.a = 0; + try { + return __wbg_adapter_59(a, state0.b, arg0, arg1); + } finally { + state0.a = a; + } + }; + const ret = new Promise(cb0); + return addHeapObject(ret); + } finally { + state0.a = state0.b = 0; + } + }; + imports.wbg.__wbg_resolve_b0083a7967828ec8 = function(arg0) { + const ret = Promise.resolve(getObject(arg0)); + return addHeapObject(ret); + }; + imports.wbg.__wbg_then_0c86a60e8fcfe9f6 = function(arg0, arg1) { + const ret = getObject(arg0).then(getObject(arg1)); + return addHeapObject(ret); + }; + imports.wbg.__wbg_then_a73caa9a87991566 = function(arg0, arg1, arg2) { + const ret = getObject(arg0).then(getObject(arg1), getObject(arg2)); + return addHeapObject(ret); + }; + imports.wbg.__wbg_set_1f9b04f170055d33 = function() { return handleError(function (arg0, arg1, arg2) { + const ret = Reflect.set(getObject(arg0), getObject(arg1), getObject(arg2)); + return ret; + }, arguments) }; + imports.wbg.__wbindgen_debug_string = function(arg0, arg1) { + const ret = debugString(getObject(arg1)); + const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len1 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len1; + getInt32Memory0()[arg0 / 4 + 0] = ptr1; + }; + imports.wbg.__wbindgen_throw = function(arg0, arg1) { + throw new Error(getStringFromWasm0(arg0, arg1)); + }; + imports.wbg.__wbindgen_closure_wrapper148 = function(arg0, arg1, arg2) { + const ret = makeMutClosure(arg0, arg1, 18, __wbg_adapter_22); + return addHeapObject(ret); + }; + + return imports; +} + +function __wbg_init_memory(imports, maybe_memory) { + +} + +function __wbg_finalize_init(instance, module) { + wasm = instance.exports; + __wbg_init.__wbindgen_wasm_module = module; + cachedInt32Memory0 = null; + cachedUint32Memory0 = null; + cachedUint8Memory0 = null; + + + return wasm; +} + +function initSync(module) { + if (wasm !== undefined) return wasm; + + const imports = __wbg_get_imports(); + + __wbg_init_memory(imports); + + if (!(module instanceof WebAssembly.Module)) { + module = new WebAssembly.Module(module); + } + + const instance = new WebAssembly.Instance(module, imports); + + return __wbg_finalize_init(instance, module); +} + +async function __wbg_init(input) { + if (wasm !== undefined) return wasm; + + if (typeof input === 'undefined') { + input = new URL('pjs_bg.wasm', import.meta.url); + } + const imports = __wbg_get_imports(); + + if (typeof input === 'string' || (typeof Request === 'function' && input instanceof Request) || (typeof URL === 'function' && input instanceof URL)) { + input = fetch(input); + } + + __wbg_init_memory(imports); + + const { instance, module } = await __wbg_load(await input, imports); + + return __wbg_finalize_init(instance, module); +} + +export { initSync } +export default __wbg_init; diff --git a/pjs-rs/dist/pjs.wasm b/pjs-rs/dist/pjs.wasm new file mode 100755 index 0000000..e4e5eb7 Binary files /dev/null and b/pjs-rs/dist/pjs.wasm differ diff --git a/pjs-rs/dist/pjs_bg.wasm b/pjs-rs/dist/pjs_bg.wasm new file mode 100644 index 0000000..2e8c39d Binary files /dev/null and b/pjs-rs/dist/pjs_bg.wasm differ diff --git a/pjs-rs/index.html b/pjs-rs/index.html new file mode 100644 index 0000000..9c24578 --- /dev/null +++ b/pjs-rs/index.html @@ -0,0 +1,121 @@ + + + + + + + PJS extension from Rust + + + + +
+ + + + +
+ ~ + + + + + \ No newline at end of file diff --git a/pjs-rs/src/lib.rs b/pjs-rs/src/lib.rs new file mode 100644 index 0000000..d82596e --- /dev/null +++ b/pjs-rs/src/lib.rs @@ -0,0 +1,219 @@ +use wasm_bindgen::prelude::*; +use wasm_bindgen_futures::JsFuture; +use web_sys::js_sys::{Array, Function, Object, Promise, Reflect}; + +macro_rules! get { + (^ $obj:expr, $($prop:expr),+ $(,)?) => {{ + let val = get!($obj, $($prop),+); + val.unchecked_into() + }}; + ($obj:expr, $($prop:expr),+ $(,)?) => {{ + let mut current_val = JsValue::from($obj); + $( + current_val = Reflect::get(¤t_val, &JsValue::from_str($prop)) + .unwrap_or_else(|_| panic!("Property '{}' does not exist in {:?}", $prop, current_val)); + )+ + current_val + }}; +} + +const NULL: JsValue = JsValue::null(); + +#[wasm_bindgen] +pub struct PjsExtension { + pjs: JsValue, + accounts: Vec, + selected: Option, +} + +#[wasm_bindgen] +impl PjsExtension { + pub async fn connect(app_name: &str) -> Result { + let Some(web3) = web_sys::window().expect("browser").get("injectedWeb3") else { + return Err(Error::ExtensionUnavailable); + }; + let pjs = get!(web3, "polkadot-js"); + let enable: Function = get!(^ &pjs, "enable"); + let p = enable + .call1(&pjs, &app_name.into()) + .expect("promise") + .unchecked_into::(); + let Ok(pjs) = JsFuture::from(p).await else { + return Err(Error::NoPermission); + }; + + Ok(Self { + pjs, + accounts: vec![], + selected: None, + }) + } + + #[wasm_bindgen(js_name = selectAccount)] + pub fn select_account(&mut self, idx: u8) { + self.selected = self + .accounts + .len() + .checked_sub(1) + .map(|i| idx.min(i.min(u8::MAX as usize) as u8)); + } + + /// + #[wasm_bindgen(js_name = sign)] + pub async fn js_sign(&self, payload: &str, cb: &Function) -> Result { + let sign: Function = get!(^ &self.pjs, "signer", "signRaw"); + let account = self + .accounts + .get(self.selected.ok_or(Error::NoAccountSelected)? as usize) + .ok_or(Error::NoAccounts)?; + let data = { + let o = Object::new(); + Reflect::set(&o, &"address".into(), &account.address.as_str().into()).unwrap(); + Reflect::set(&o, &"data".into(), &payload.into()).unwrap(); + Reflect::set(&o, &"type".into(), &"bytes".into()).unwrap(); + o + }; + + let p = sign + .call1(&NULL, &data.into()) + .expect("promise") + .unchecked_into::(); + let signature = JsFuture::from(p).await.map_err(|_| Error::Sign)?; + let res = cb.call1(&NULL, &signature).map_err(|_| Error::Sign)?; + Ok(get!(&res, "signature")) + } + + /// + #[wasm_bindgen(js_name = fetchAccounts)] + pub async fn fetch_accounts(&mut self) -> Result<(), Error> { + let accounts: Function = get!(^ &self.pjs, "accounts", "get"); + let p = accounts.call0(&NULL).unwrap().unchecked_into::(); + let Ok(accounts) = JsFuture::from(p).await else { + return Err(Error::FailedFetchingAccounts); + }; + self.accounts = Array::from(&accounts) + .iter() + .map(|a| { + let name = get!(&a, "name").as_string().unwrap(); + let address = get!(&a, "address").as_string().unwrap(); + let net: Network = get!(&a, "genesisHash").into(); + Account { name, address, net } + }) + .collect(); + if !self.accounts.is_empty() { + self.selected = Some(0); + } + Ok(()) + } + + #[wasm_bindgen(getter)] + pub fn accounts(&self) -> Vec { + self.accounts.clone() + } + + #[wasm_bindgen(getter, js_name = selectedAccount)] + pub fn get_selected(&self) -> Option { + self.selected + .and_then(|a| self.accounts.get(a as usize)) + .cloned() + } +} + +impl PjsExtension { + pub async fn sign(&self, payload: &[u8]) -> Result<[u8; 64], Error> { + let payload = Self::to_hex(payload); + let mut signature = [0u8; 64]; + let cb = Closure::wrap(Box::new(move |s: JsValue| { + Self::from_hex(s.as_string().unwrap_or_default().as_str(), &mut signature) + }) as Box); + self.js_sign(payload.as_str(), cb.as_ref().unchecked_ref()) + .await?; + Ok(signature) + } + + fn to_hex(bytes: &[u8]) -> String { + use std::fmt::Write; + let mut s = String::with_capacity(2 + bytes.len()); + let _ = write!(s, "0x"); + for b in bytes { + let _ = write!(s, "{b:x}"); + } + s + } + fn from_hex(input: &str, buf: &mut [u8]) { + for (i, b) in buf.iter_mut().enumerate() { + let Some(s) = input.get(i * 2..i * 2 + 2) else { + return; + }; + *b = u8::from_str_radix(s, 16).unwrap_or_default(); + } + } +} + +#[wasm_bindgen] +#[derive(Debug)] +pub enum Error { + ExtensionUnavailable, + NoPermission, + FailedFetchingAccounts, + NoAccountSelected, + NoAccounts, + Sign, +} + +#[wasm_bindgen] +#[derive(Debug, Clone)] +pub struct Account { + name: String, + address: String, + net: Network, +} + +#[wasm_bindgen] +impl Account { + #[wasm_bindgen(constructor)] + pub fn new(name: &str, address: &str, net: Network) -> Self { + Account { + name: name.to_string(), + address: address.to_string(), + net, + } + } + #[wasm_bindgen(getter)] + pub fn name(&self) -> String { + self.name.clone() + } + #[wasm_bindgen(getter)] + pub fn address(&self) -> String { + self.address.clone() + } + #[wasm_bindgen(getter)] + pub fn network(&self) -> Network { + self.net + } +} + +#[wasm_bindgen] +#[derive(Debug, Clone, Copy)] +pub enum Network { + Generic, + Kusama, + Polkadot, + Kreivo, +} + +const KSM: &str = "0xb0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafe"; +const DOT: &str = "0x91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3"; +const KREIVO: &str = "0xc710a5f16adc17bcd212cff0aedcbf1c1212a043cdc0fb2dcba861efe5305b01"; + +impl From for Network { + fn from(value: JsValue) -> Self { + let value = value.as_string(); + match value.as_deref() { + Some(KSM) => Network::Kusama, + Some(DOT) => Network::Polkadot, + Some(KREIVO) => Network::Kreivo, + _ => Network::Generic, + } + } +}