diff --git a/crates/iris-crypto/src/cheetah.rs b/crates/iris-crypto/src/cheetah.rs index 08e5c7f..538a674 100644 --- a/crates/iris-crypto/src/cheetah.rs +++ b/crates/iris-crypto/src/cheetah.rs @@ -1,11 +1,10 @@ +#[cfg(feature = "wasm")] +use alloc::{boxed::Box, format, string::ToString}; #[cfg(feature = "alloc")] -use alloc::{ - boxed::Box, - format, - string::{String, ToString}, - vec::Vec, -}; +use alloc::{string::String, vec::Vec}; use arrayvec::ArrayVec; +use core::convert::Infallible; +use core::future::{ready, Future}; use iris_ztd::{ crypto::cheetah::{ ch_add, ch_neg, ch_scal_big, trunc_g_order, CheetahPoint, F6lt, A_GEN, G_ORDER, @@ -220,6 +219,28 @@ pub struct Signature { pub s: U256, } +impl Signature { + pub const BYTE_LEN: usize = 64; + + pub fn to_bytes_le(&self) -> [u8; Self::BYTE_LEN] { + let mut out = [0u8; Self::BYTE_LEN]; + out[..32].copy_from_slice(&self.c.to_le_bytes()); + out[32..].copy_from_slice(&self.s.to_le_bytes()); + out + } + + pub fn from_bytes_le(bytes: &[u8]) -> Option { + if bytes.len() != Self::BYTE_LEN { + return None; + } + + Some(Self { + c: U256::from_le_slice(&bytes[..32]), + s: U256::from_le_slice(&bytes[32..]), + }) + } +} + // Aggregate signature of the same challenge impl core::iter::Sum for Option { fn sum>(mut iter: I) -> Self { @@ -277,9 +298,40 @@ impl Hashable for Signature { } } +/// ECDSA signing for transaction witnesses. Implemented by [`PrivateKey`] and +/// wasm-side key handles that delegate to local or external signers. +pub trait SigningKey { + type Error; + + fn public_key(&self) -> impl Future> + '_; + + fn sign_digests<'a>( + &'a self, + digests: &'a [Digest], + ) -> impl Future, Self::Error>> + 'a; +} + #[derive(Debug, Clone)] pub struct PrivateKey(pub U256); +impl SigningKey for PrivateKey { + type Error = Infallible; + + fn public_key(&self) -> impl Future> + '_ { + ready(Ok(PrivateKey::public_key(self))) + } + + fn sign_digests<'a>( + &'a self, + digests: &'a [Digest], + ) -> impl Future, Self::Error>> + 'a { + ready(Ok(digests + .iter() + .map(|d| PrivateKey::sign(self, d)) + .collect())) + } +} + impl Drop for PrivateKey { fn drop(&mut self) { unsafe { diff --git a/crates/iris-crypto/src/lib.rs b/crates/iris-crypto/src/lib.rs index fc4839a..40b8c37 100644 --- a/crates/iris-crypto/src/lib.rs +++ b/crates/iris-crypto/src/lib.rs @@ -8,7 +8,7 @@ use alloc::string::{String, ToString}; pub mod cheetah; pub mod slip10; -pub use cheetah::{PrivateKey, PublicKey, Signature}; +pub use cheetah::{PrivateKey, PublicKey, Signature, SigningKey}; pub use slip10::{derive_master_key, ExtendedKey}; #[cfg(feature = "mnemonic")] diff --git a/crates/iris-nockchain-types/src/tx_engine/builder.rs b/crates/iris-nockchain-types/src/tx_engine/builder.rs index 44874ed..1a1fc4f 100644 --- a/crates/iris-nockchain-types/src/tx_engine/builder.rs +++ b/crates/iris-nockchain-types/src/tx_engine/builder.rs @@ -2,7 +2,7 @@ use alloc::collections::btree_map::BTreeMap; use alloc::collections::btree_set::BTreeSet; use alloc::vec; use alloc::vec::Vec; -use iris_crypto::{PrivateKey, PublicKey}; +use iris_crypto::{PublicKey, Signature, SigningKey}; use iris_ztd::{noun_deserialize, noun_serialize, Digest, Hashable as HashableTrait, Noun, ZMap}; use serde::{Deserialize, Serialize}; @@ -343,20 +343,41 @@ impl SpendBuilder { None } - pub fn sign(&mut self, signing_key: &PrivateKey) -> bool { - match &mut self.spend { + pub fn sig_hash(&self) -> Digest { + self.spend.sig_hash() + } + + pub fn accepts_signing_pubkey(&self, public_key: &PublicKey) -> bool { + match &self.spend { Spend::S1(spend) => { - let pkpkh = signing_key.public_key().hash(); + let pkpkh = public_key.hash(); + spend + .witness + .lock_merkle_proof + .spend_condition() + .pkh() + .any(|p| p.hashes.contains(&pkpkh)) + } + Spend::S0(_) => { + let Some(sig) = &self.note_info.sig else { + panic!("Note is not V0"); + }; + sig.pubkeys.contains(public_key) + } + } + } + pub fn try_apply_signature(&mut self, public_key: PublicKey, signature: Signature) -> bool { + match &mut self.spend { + Spend::S1(spend) => { + let pkpkh = public_key.hash(); for p in spend.witness.lock_merkle_proof.spend_condition().pkh() { if p.hashes.contains(&pkpkh) { - spend.witness.pkh_signature.0.insert( - signing_key.public_key().hash(), - ( - signing_key.public_key(), - signing_key.sign(&spend.sig_hash()), - ), - ); + spend + .witness + .pkh_signature + .0 + .insert(public_key.hash(), (public_key, signature)); return true; } } @@ -365,19 +386,43 @@ impl SpendBuilder { let Some(sig) = &self.note_info.sig else { panic!("Note is not V0"); }; - if sig.pubkeys.contains(&signing_key.public_key()) { - spend.signature.0.insert( - signing_key.public_key(), - signing_key.sign(&spend.sig_hash()), - ); + if sig.pubkeys.contains(&public_key) { + spend.signature.0.insert(public_key, signature); return true; } } } - false } + pub async fn sign(&mut self, signing_key: &K) -> Result> + where + K: SigningKey, + { + let pk = signing_key + .public_key() + .await + .map_err(SigningError::Signer)?; + if !self.accepts_signing_pubkey(&pk) { + return Ok(false); + } + let digest = self.sig_hash(); + let sig = signing_key + .sign_digests(&[digest]) + .await + .map_err(SigningError::Signer)? + .pop() + .ok_or(SigningError::SignatureRejected)?; + if !pk.verify(&digest, &sig) { + return Err(SigningError::InvalidSignature); + } + if !self.try_apply_signature(pk, sig) { + return Err(SigningError::SignatureRejected); + } + + Ok(true) + } + fn missing_unlocks_fee(&self, settings: &TxEngineSettings) -> Nicks { let mut fee = Nicks(0); @@ -544,11 +589,48 @@ impl TxBuilder { ret } - pub fn sign(&mut self, signing_key: &PrivateKey) -> &mut Self { - for spend in self.spends.values_mut() { - spend.sign(signing_key); + pub async fn sign(&mut self, signing_key: &K) -> Result<&mut Self, SigningError> + where + K: SigningKey, + { + let pk = signing_key + .public_key() + .await + .map_err(SigningError::Signer)?; + let mut name_and_digest = Vec::new(); + let mut digests = Vec::new(); + + for (name, spend) in &self.spends { + if spend.accepts_signing_pubkey(&pk) { + let digest = spend.sig_hash(); + name_and_digest.push((*name, digest)); + digests.push(digest); + } } - self + + let signatures = signing_key + .sign_digests(&digests) + .await + .map_err(SigningError::Signer)?; + + if signatures.len() != name_and_digest.len() { + return Err(SigningError::SignatureRejected); + } + + for ((name, digest), sig) in name_and_digest.into_iter().zip(signatures.into_iter()) { + if !pk.verify(&digest, &sig) { + return Err(SigningError::InvalidSignature); + } + let Some(spend) = self.spends.get_mut(&name) else { + debug_assert!(false, "signature staging used names from self.spends"); + return Err(SigningError::SignatureRejected); + }; + if !spend.try_apply_signature(pk, sig) { + return Err(SigningError::SignatureRejected); + } + } + + Ok(self) } pub fn validate(&mut self) -> Result<&mut Self, BuildError> { @@ -853,6 +935,25 @@ pub enum BuildError { MissingUnlocks(Vec), } +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SigningError { + Signer(E), + InvalidSignature, + SignatureRejected, +} + +impl core::fmt::Display for SigningError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + SigningError::Signer(error) => write!(f, "signer failed: {error:?}"), + SigningError::InvalidSignature => { + write!(f, "signer returned a signature that does not verify") + } + SigningError::SignatureRejected => write!(f, "signature was rejected by spend builder"), + } + } +} + impl core::fmt::Display for BuildError { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { @@ -897,11 +998,13 @@ impl core::fmt::Display for BuildError { #[cfg(test)] mod tests { + extern crate std; + use super::*; use crate::v1::{self, LockPrimitive, LockTim}; use alloc::{string::ToString, vec}; use bip39::Mnemonic; - use iris_crypto::{derive_master_key, PublicKey}; + use iris_crypto::{derive_master_key, PrivateKey, PublicKey}; use iris_ztd::{jam, NounEncode}; fn keys() -> (PrivateKey, PublicKey) { @@ -910,6 +1013,33 @@ mod tests { (ek.private_key.unwrap(), ek.public_key) } + fn block_on(future: F) -> F::Output { + use core::pin::pin; + use core::task::{Context, Poll}; + use std::sync::Arc; + use std::task::{Wake, Waker}; + + struct NoopWaker; + + impl Wake for NoopWaker { + fn wake(self: Arc) {} + } + + let waker = Waker::from(Arc::new(NoopWaker)); + let mut context = Context::from_waker(&waker); + let mut future = pin!(future); + loop { + match future.as_mut().poll(&mut context) { + Poll::Ready(output) => return output, + Poll::Pending => std::thread::yield_now(), + } + } + } + + fn sign_builder(builder: &mut TxBuilder, private_key: &PrivateKey) { + block_on(builder.sign(private_key)).unwrap(); + } + #[test] fn test_builder() { let (private_key, _) = keys(); @@ -959,7 +1089,8 @@ mod tests { "BzEPGBGvTfqo4saYmXLem7NgzmGzZdKNk2EUwWBfhgd1yzkdTeA8yQN", ), ] { - let tx = TxBuilder::new(settings) + let mut builder = TxBuilder::new(settings); + builder .simple_spend_base( vec![(note.clone(), Some(spend_condition.clone()))], recipient, @@ -969,11 +1100,9 @@ mod tests { ) .unwrap() .set_fee_and_balance_refund(fee, false, true) - .unwrap() - .sign(&private_key) - .validate() - .unwrap() - .build(); + .unwrap(); + sign_builder(&mut builder, &private_key); + let tx = builder.validate().unwrap().build(); let InputDisplay::V1 { .. } = tx.display.inputs else { panic!("Expected V1 inputs"); @@ -997,8 +1126,8 @@ mod tests { ) .unwrap() .set_fee_and_balance_refund(fee, false, true) - .unwrap() - .sign(&private_key); + .unwrap(); + sign_builder(&mut tx, &private_key); assert!(tx.validate().is_err()); @@ -1017,7 +1146,8 @@ mod tests { let fee1 = builder.calc_fee(); - let tx = builder.sign(&private_key).build(); + sign_builder(&mut builder, &private_key); + let tx = builder.build(); assert_eq!(tx.to_raw_tx().spends.fee(&settings), Nicks(2520000)); assert_eq!(fee1, Nicks(2520000)); @@ -1127,7 +1257,7 @@ mod tests { assert_eq!(builder.cur_fee(), Nicks(992)); // After signing, the fee shouldn't change. - builder.sign(&private_key); + sign_builder(&mut builder, &private_key); assert_eq!(builder.calc_fee(), Nicks(992)); assert_eq!(builder.cur_fee(), Nicks(992)); @@ -1207,7 +1337,8 @@ mod tests { let gift = Nicks(4294967296 * 3 - 65536 * 100); let refund_pkh = public_key.hash(); - let tx = TxBuilder::new(TxEngineSettings::v1_default()) + let mut builder = TxBuilder::new(TxEngineSettings::v1_default()); + builder .simple_spend_base( notes .into_iter() @@ -1235,11 +1366,9 @@ mod tests { ) .unwrap() .recalc_and_set_fee(false) - .unwrap() - .sign(&private_key) - .validate() - .unwrap() - .build(); + .unwrap(); + sign_builder(&mut builder, &private_key); + let tx = builder.validate().unwrap().build(); assert_eq!( tx.id.to_string(), @@ -1376,19 +1505,16 @@ mod tests { .try_into() .unwrap(), )), - LockPrimitive::Hax( - Hax { - preimages: [Digest([ - Belt(1730770831742798981), - Belt(2676322185709933211), - Belt(8329210750824781744), - Belt(16756092452590401876), - Belt(3547445316740171466), - ])] - .into(), - } + LockPrimitive::Hax(Hax { + preimages: [Digest([ + Belt(1730770831742798981), + Belt(2676322185709933211), + Belt(8329210750824781744), + Belt(16756092452590401876), + Belt(3547445316740171466), + ])] .into(), - ), + }), ] .into(), ) @@ -1464,7 +1590,8 @@ mod tests { .into(), 0, ); - let tx = TxBuilder::new(TxEngineSettings::v1_with_word_cost(Nicks(1))) + let mut builder = TxBuilder::new(TxEngineSettings::v1_with_word_cost(Nicks(1))); + builder .simple_spend_base( vec![(note.clone(), Some(spend_condition.clone()))], recipient, @@ -1474,11 +1601,9 @@ mod tests { ) .unwrap() .set_fee_and_balance_refund(fee, false, true) - .unwrap() - .sign(&private_key) - .validate() - .unwrap() - .build(); + .unwrap(); + sign_builder(&mut builder, &private_key); + let tx = builder.validate().unwrap().build(); assert_eq!( tx.id.to_string(), "3pmkA1knKhJzmd28t5TULP9DADK7GhWsHaNSTpPcGcN4nxzrWsDK2xe", diff --git a/crates/iris-wasm/README.md b/crates/iris-wasm/README.md index a78795a..08dde03 100644 --- a/crates/iris-wasm/README.md +++ b/crates/iris-wasm/README.md @@ -102,6 +102,7 @@ import init, { GrpcClient, PrivateKey, deriveMasterKeyFromMnemonic, + hashPublicKey, txEngineSettingsV1Default, TxBuilder, Note, @@ -137,12 +138,14 @@ console.log('Balance by name:', balanceByName); // Derive keys from mnemonic const mnemonic = "dice domain inspire horse time..."; const masterKey = deriveMasterKeyFromMnemonic(mnemonic, ""); +const privateKey = PrivateKey.fromBytes(masterKey.private_key); +const publicKey = await privateKey.publicKey(); // Use one available note from the balance query const note = Note.fromProtobuf(balance.notes[0].note); // Create spend condition -const pubkeyHash = new Digest("your_pubkey_hash_here"); +const pubkeyHash = hashPublicKey(publicKey); const spendCondition = new SpendCondition([ LockPrimitive.newPkh(Pkh.single(pubkeyHash)), LockPrimitive.newTim(LockTim.coinbase()) @@ -164,7 +167,6 @@ await builder.simpleSpend( ); // Sign and submit -const privateKey = PrivateKey.fromBytes(masterKey.private_key); await builder.sign(privateKey); const signedTx = builder.build(); const txProtobuf = signedTx.toProtobuf(); @@ -175,6 +177,27 @@ const accepted = await client.transactionAccepted(signedTx.id.value); console.log('Transaction accepted:', accepted); ``` +### Callback Signing + +Use callback-backed keys for Ledger, browser transport, or remote KMS integrations. The callbacks may return values directly or Promises. + +```javascript +const signingKey = PrivateKey.fromCallbacks({ + async getPublicKey() { + return ledgerPublicKeyBytes; // Uint8Array(97) + }, + + async signDigests(digests) { + // digests is Uint8Array[]; each digest is Uint8Array(40). + // Return one signature per digest: Uint8Array(64) = c || s, each 32-byte little-endian. + return ledgerSignatureBytesArray; // Uint8Array(64)[] + } +}); + +const publicKey = await signingKey.publicKey(); +await builder.sign(signingKey); +``` + ## API Reference ### `GrpcClient` diff --git a/crates/iris-wasm/examples/grpc-web-demo.html b/crates/iris-wasm/examples/grpc-web-demo.html index 03b2959..37d3b84 100644 --- a/crates/iris-wasm/examples/grpc-web-demo.html +++ b/crates/iris-wasm/examples/grpc-web-demo.html @@ -251,7 +251,9 @@

Output

log('Deriving private key from mnemonic...', 'loading'); const masterKey = deriveMasterKeyFromMnemonic(mnemonicInput, ""); const privateKeyBytes = masterKey.private_key; - const pubkeyHashString = hashPublicKey(masterKey.public_key); + const privateKey = PrivateKey.fromBytes(privateKeyBytes); + const publicKey = await privateKey.publicKey(); + const pubkeyHashString = hashPublicKey(publicKey); log('Fetching balance to get available notes...', 'loading'); client = new GrpcClient(endpoint); @@ -287,7 +289,6 @@

Output

feeAmount, refundDigest ); - const privateKey = PrivateKey.fromBytes(privateKeyBytes); await builder.sign(privateKey); const rawTx = builder.build(); log('Transaction built successfully!', 'success'); diff --git a/crates/iris-wasm/src/tx.rs b/crates/iris-wasm/src/tx.rs index ab7a812..fb40af6 100644 --- a/crates/iris-wasm/src/tx.rs +++ b/crates/iris-wasm/src/tx.rs @@ -2,7 +2,7 @@ use alloc::format; use alloc::string::{String, ToString}; use alloc::vec::Vec; -use iris_crypto::PrivateKey as CryptoPrivateKey; +use iris_crypto::{PrivateKey as CryptoPrivateKey, PublicKey, Signature, SigningKey}; use iris_grpc_proto::pb::common::v1 as pb_v1; use iris_grpc_proto::pb::common::v2 as pb; use iris_nockchain_types::{ @@ -10,11 +10,14 @@ use iris_nockchain_types::{ note::Note, tx::RawTx, v1::{Lock, LockRoot, NockchainTx, RawTxV1, SeedV1 as Seed, SpendCondition}, - Nicks, SpendBuilder, TxEngineSettings, + Nicks, SigningError, SpendBuilder, TxEngineSettings, }; use iris_ztd::{cue, Digest, U256}; +use js_sys::{Array, Function, Reflect, Uint8Array}; use serde::{Deserialize, Serialize}; use wasm_bindgen::prelude::*; +use wasm_bindgen::JsCast; +use wasm_bindgen_futures::JsFuture; // ============================================================================ // Wasm Types - Adapters and Helpers @@ -127,18 +130,163 @@ impl TxLock { enum PrivateKeyBackend { Bytes(BytesPrivateKeyBackend), + Callback(CallbackPrivateKeyBackend), } struct BytesPrivateKeyBackend { signing_key: CryptoPrivateKey, - public_key_bytes: [u8; 97], } +struct CallbackPrivateKeyBackend { + // Keep the original callbacks object so method-style callbacks receive it as `this`. + callbacks: js_sys::Object, + get_public_key: Function, + sign_digests: Function, +} + +fn get_callback_fn(obj: &js_sys::Object, key: &str) -> Result { + let v = Reflect::get(obj, &JsValue::from_str(key)).map_err(|_| { + JsValue::from_str("fromCallbacks: failed to read property from callbacks object") + })?; + if let Ok(f) = v.dyn_into::() { + return Ok(f); + } + + Err(JsValue::from_str(&format!( + "fromCallbacks: expected a function at `{key}`" + ))) +} + +async fn await_resolved_promise(v: JsValue) -> Result { + JsFuture::from(js_sys::Promise::resolve(&v)).await +} + +async fn callback_fetch_public_key(cb: &CallbackPrivateKeyBackend) -> Result { + let v = await_resolved_promise(cb.get_public_key.call0(&cb.callbacks)?).await?; + let arr = v + .dyn_into::() + .map_err(|_| JsValue::from_str("getPublicKey must return Uint8Array of length 97"))?; + if arr.length() as usize != 97 { + return Err(JsValue::from_str("getPublicKey: expected 97 bytes")); + } + let mut b = [0u8; 97]; + arr.copy_to(&mut b); + Ok(PublicKey::from_be_bytes(&b)) +} + +async fn callback_sign_digests( + cb: &CallbackPrivateKeyBackend, + digests: &[Digest], +) -> Result, JsValue> { + let args = Array::new(); + for digest in digests { + let digest_bytes = digest.to_bytes(); + args.push(&Uint8Array::from(digest_bytes.as_slice())); + } + + let v = await_resolved_promise(cb.sign_digests.call1(&cb.callbacks, &args)?).await?; + let arr = v + .dyn_into::() + .map_err(|_| JsValue::from_str("signDigests must return an Array"))?; + + if arr.length() as usize != digests.len() { + return Err(JsValue::from_str( + "signDigests: expected one signature per digest", + )); + } + + let mut out = Vec::with_capacity(arr.length() as usize); + for (i, item) in arr.iter().enumerate() { + let sig_bytes = item + .dyn_into::() + .map_err(|_| JsValue::from_str("signDigests must return an Array"))?; + + if sig_bytes.length() as usize != Signature::BYTE_LEN { + return Err(JsValue::from_str( + "signDigests: expected Uint8Array(64) for each signature", + )); + } + + let mut buf = [0u8; Signature::BYTE_LEN]; + sig_bytes.copy_to(&mut buf); + let sig = Signature::from_bytes_le(&buf) + .ok_or_else(|| JsValue::from_str("signDigests: expected 64 bytes (32 c + 32 s LE)"))?; + out.push(sig); + + debug_assert!(i < digests.len()); + } + + Ok(out) +} + +fn signing_error_to_js(error: SigningError) -> JsValue { + match error { + SigningError::Signer(error) => error, + SigningError::InvalidSignature => JsValue::from_str("signer returned an invalid signature"), + SigningError::SignatureRejected => JsValue::from_str("signature was rejected by spend"), + } +} + +/// Holds raw key bytes in WASM; [`Drop`] zeroizes the scalar so the secret does not linger +/// after the JS handle is released. #[wasm_bindgen(js_name = PrivateKey)] pub struct WasmPrivateKey { backend: PrivateKeyBackend, } +impl SigningKey for WasmPrivateKey { + type Error = JsValue; + + fn public_key( + &self, + ) -> impl core::future::Future> + '_ { + async move { + match &self.backend { + PrivateKeyBackend::Bytes(bytes_backend) => { + Ok(bytes_backend.signing_key.public_key()) + } + PrivateKeyBackend::Callback(callback_backend) => { + callback_fetch_public_key(callback_backend).await + } + } + } + } + + fn sign_digests<'a>( + &'a self, + digests: &'a [Digest], + ) -> impl core::future::Future, Self::Error>> + 'a { + async move { + match &self.backend { + PrivateKeyBackend::Bytes(bytes_backend) => { + let mut out = Vec::with_capacity(digests.len()); + for digest in digests { + out.push(bytes_backend.signing_key.sign(digest)); + } + Ok(out) + } + PrivateKeyBackend::Callback(callback_backend) => { + callback_sign_digests(callback_backend, digests).await + } + } + } + } +} + +impl Drop for WasmPrivateKey { + fn drop(&mut self) { + match &mut self.backend { + PrivateKeyBackend::Bytes(b) => { + unsafe { + core::ptr::write_volatile(&mut b.signing_key.0, U256::ZERO); + } + core::sync::atomic::compiler_fence(core::sync::atomic::Ordering::SeqCst); + } + PrivateKeyBackend::Callback(_) => {} + } + } +} + #[wasm_bindgen(js_class = PrivateKey)] impl WasmPrivateKey { /// Construct a wasm `PrivateKey` from 32-byte private key material. @@ -175,22 +323,37 @@ impl WasmPrivateKey { } let signing_key = CryptoPrivateKey(U256::from_be_slice(signing_key_bytes)); - let public_key_bytes = signing_key.public_key().to_be_bytes(); + Ok(Self { + backend: PrivateKeyBackend::Bytes(BytesPrivateKeyBackend { signing_key }), + }) + } + /// External signer: JavaScript object with async (or sync) functions: + /// - `getPublicKey` -> `Uint8Array(97)` uncompressed pubkey + /// - `signDigests` -> `(digests: Uint8Array[])` -> `Uint8Array(64)[]` (`c`||`s` LE) + /// + /// Return values may be Promises; they are always awaited. + #[wasm_bindgen(js_name = fromCallbacks)] + pub fn from_callbacks(callbacks: &JsValue) -> Result { + let obj = callbacks + .dyn_ref::() + .ok_or_else(|| JsValue::from_str("fromCallbacks: expected a plain object"))?; + let get_public_key = get_callback_fn(obj, "getPublicKey")?; + let sign_digests = get_callback_fn(obj, "signDigests")?; Ok(Self { - backend: PrivateKeyBackend::Bytes(BytesPrivateKeyBackend { - signing_key, - public_key_bytes, + backend: PrivateKeyBackend::Callback(CallbackPrivateKeyBackend { + callbacks: obj.clone(), + get_public_key, + sign_digests, }), }) } /// Return this key's public key as 97-byte uncompressed bytes. - #[wasm_bindgen(getter, js_name = publicKey)] - pub fn public_key(&self) -> Vec { - match &self.backend { - PrivateKeyBackend::Bytes(bytes_backend) => bytes_backend.public_key_bytes.to_vec(), - } + #[wasm_bindgen(js_name = publicKey)] + pub async fn public_key(&self) -> Result, JsValue> { + let public_key = ::public_key(self).await?; + Ok(public_key.to_be_bytes().to_vec()) } /// Return the derivation path for this key backend, if available. @@ -200,6 +363,7 @@ impl WasmPrivateKey { pub fn derivation_path(&self) -> Option { match &self.backend { PrivateKeyBackend::Bytes(_) => None, + PrivateKeyBackend::Callback(_) => None, } } @@ -208,14 +372,7 @@ impl WasmPrivateKey { pub fn backend_kind(&self) -> String { match &self.backend { PrivateKeyBackend::Bytes(_) => "bytes".to_string(), - } - } -} - -impl WasmPrivateKey { - fn signing_key(&self) -> &CryptoPrivateKey { - match &self.backend { - PrivateKeyBackend::Bytes(bytes_backend) => &bytes_backend.signing_key, + PrivateKeyBackend::Callback(_) => "callback".to_string(), } } } @@ -328,8 +485,10 @@ impl WasmTxBuilder { #[wasm_bindgen] pub async fn sign(&mut self, signing_key: &WasmPrivateKey) -> Result<(), JsValue> { - self.builder.sign(signing_key.signing_key()); - + self.builder + .sign(signing_key) + .await + .map_err(signing_error_to_js)?; Ok(()) } @@ -434,7 +593,10 @@ impl WasmSpendBuilder { } pub async fn sign(&mut self, signing_key: &WasmPrivateKey) -> Result { - Ok(self.builder.sign(signing_key.signing_key())) + self.builder + .sign(signing_key) + .await + .map_err(signing_error_to_js) } fn from_internal(internal: &SpendBuilder) -> Self {