Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 40 additions & 20 deletions packages/wasm-utxo/src/wasm/bip32.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use crate::bitcoin::bip32::{ChildNumber, DerivationPath, Xpriv, Xpub};
use crate::bitcoin::secp256k1::Secp256k1;
use crate::bitcoin::{PrivateKey, PublicKey};
use crate::error::WasmUtxoError;
use crate::wasm::try_from_js_value::{get_buffer_field, get_field, get_nested_field};
use crate::wasm::try_from_js_value::{get_field, get_nested_field, Bytes};
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsValue;

Expand Down Expand Up @@ -156,30 +156,50 @@ impl WasmBIP32 {
}

/// Create a BIP32 key from BIP32 properties
/// Extracts properties from a JavaScript object and constructs an xpub
/// Extracts properties from a JavaScript object and constructs an xpub or xprv
#[wasm_bindgen]
pub fn from_bip32_properties(bip32_key: &JsValue) -> Result<WasmBIP32, WasmUtxoError> {
// Extract properties using helper functions
let version: u32 = get_nested_field(bip32_key, "network.bip32.public")?;
// Extract common properties
let depth: u8 = get_field(bip32_key, "depth")?;
let parent_fingerprint: u32 = get_field(bip32_key, "parentFingerprint")?;
let index: u32 = get_field(bip32_key, "index")?;
let chain_code_bytes: [u8; 32] = get_buffer_field(bip32_key, "chainCode")?;
let public_key_bytes: [u8; 33] = get_buffer_field(bip32_key, "publicKey")?;

// Build BIP32 serialization (78 bytes total)
let mut data = Vec::with_capacity(78);
data.extend_from_slice(&version.to_be_bytes()); // 4 bytes: version
data.push(depth); // 1 byte: depth
data.extend_from_slice(&parent_fingerprint.to_be_bytes()); // 4 bytes: parent fingerprint
data.extend_from_slice(&index.to_be_bytes()); // 4 bytes: index
data.extend_from_slice(&chain_code_bytes); // 32 bytes: chain code
data.extend_from_slice(&public_key_bytes); // 33 bytes: public key

// Use the Xpub::decode method which properly handles network detection and constructs the Xpub
let xpub = Xpub::decode(&data)
.map_err(|e| WasmUtxoError::new(&format!("Failed to decode xpub: {}", e)))?;
Ok(WasmBIP32(BIP32Key::Public(xpub)))
let chain_code: Bytes<32> = get_field(bip32_key, "chainCode")?;

// Check if private key exists
let private_key: Option<Bytes<32>> = get_field(bip32_key, "privateKey")?;

if let Some(priv_key) = private_key {
// Build xprv serialization (78 bytes total)
let version: u32 = get_nested_field(bip32_key, "network.bip32.private")?;
let mut data = Vec::with_capacity(78);
data.extend_from_slice(&version.to_be_bytes()); // 4 bytes: version
data.push(depth); // 1 byte: depth
data.extend_from_slice(&parent_fingerprint.to_be_bytes()); // 4 bytes: parent fingerprint
data.extend_from_slice(&index.to_be_bytes()); // 4 bytes: index
data.extend_from_slice(chain_code.as_ref()); // 32 bytes: chain code
data.push(0x00); // 1 byte: padding for private key
data.extend_from_slice(priv_key.as_ref()); // 32 bytes: private key

let xpriv = Xpriv::decode(&data)
.map_err(|e| WasmUtxoError::new(&format!("Failed to decode xprv: {}", e)))?;
Ok(WasmBIP32(BIP32Key::Private(xpriv)))
} else {
// Build xpub serialization (78 bytes total)
let version: u32 = get_nested_field(bip32_key, "network.bip32.public")?;
let public_key: Bytes<33> = get_field(bip32_key, "publicKey")?;

let mut data = Vec::with_capacity(78);
data.extend_from_slice(&version.to_be_bytes()); // 4 bytes: version
data.push(depth); // 1 byte: depth
data.extend_from_slice(&parent_fingerprint.to_be_bytes()); // 4 bytes: parent fingerprint
data.extend_from_slice(&index.to_be_bytes()); // 4 bytes: index
data.extend_from_slice(chain_code.as_ref()); // 32 bytes: chain code
data.extend_from_slice(public_key.as_ref()); // 33 bytes: public key

let xpub = Xpub::decode(&data)
.map_err(|e| WasmUtxoError::new(&format!("Failed to decode xpub: {}", e)))?;
Ok(WasmBIP32(BIP32Key::Public(xpub)))
}
}

/// Create a BIP32 master key from a seed
Expand Down
204 changes: 103 additions & 101 deletions packages/wasm-utxo/src/wasm/try_from_js_value.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,64 @@
use std::ops::Deref;

use crate::address::utxolib_compat::{CashAddr, UtxolibNetwork};
use crate::error::WasmUtxoError;
use wasm_bindgen::JsValue;

pub(crate) trait TryFromJsValue {
fn try_from_js_value(value: &JsValue) -> Result<Self, WasmUtxoError>
where
Self: Sized;
// =============================================================================
// TryFromJsValue trait
// =============================================================================

/// Trait for converting JsValue to Rust types
pub(crate) trait TryFromJsValue: Sized {
fn try_from_js_value(value: &JsValue) -> Result<Self, WasmUtxoError>;
}

// =============================================================================
// Bytes<N>: Fixed-size byte array wrapper
// =============================================================================

/// Fixed-size byte array that implements TryFromJsValue
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct Bytes<const N: usize>(pub [u8; N]);

impl<const N: usize> Deref for Bytes<N> {
type Target = [u8; N];
fn deref(&self) -> &Self::Target {
&self.0
}
}

impl<const N: usize> AsRef<[u8]> for Bytes<N> {
fn as_ref(&self) -> &[u8] {
&self.0
}
}

impl<const N: usize> From<Bytes<N>> for [u8; N] {
fn from(bytes: Bytes<N>) -> Self {
bytes.0
}
}

impl<const N: usize> TryFromJsValue for Bytes<N> {
fn try_from_js_value(value: &JsValue) -> Result<Self, WasmUtxoError> {
let buffer = js_sys::Uint8Array::new(value);
if buffer.length() as usize != N {
return Err(WasmUtxoError::new(&format!(
"Expected {} bytes, got {}",
N,
buffer.length()
)));
}
let mut bytes = [0u8; N];
buffer.copy_to(&mut bytes);
Ok(Bytes(bytes))
}
}

// Implement TryFromJsValue for primitive types
// =============================================================================
// TryFromJsValue implementations for primitive types
// =============================================================================

impl TryFromJsValue for String {
fn try_from_js_value(value: &JsValue) -> Result<Self, WasmUtxoError> {
Expand Down Expand Up @@ -36,6 +86,15 @@ impl TryFromJsValue for u32 {
}
}

impl TryFromJsValue for Vec<u8> {
fn try_from_js_value(value: &JsValue) -> Result<Self, WasmUtxoError> {
let buffer = js_sys::Uint8Array::new(value);
let mut bytes = vec![0u8; buffer.length() as usize];
buffer.copy_to(&mut bytes);
Ok(bytes)
}
}

impl<T: TryFromJsValue> TryFromJsValue for Option<T> {
fn try_from_js_value(value: &JsValue) -> Result<Self, WasmUtxoError> {
if value.is_undefined() || value.is_null() {
Expand All @@ -46,130 +105,72 @@ impl<T: TryFromJsValue> TryFromJsValue for Option<T> {
}
}

// Helper function to get a field from an object and convert it using TryFromJsValue
pub(crate) fn get_field<T: TryFromJsValue>(obj: &JsValue, key: &str) -> Result<T, WasmUtxoError> {
let field_value = js_sys::Reflect::get(obj, &JsValue::from_str(key))
.map_err(|_| WasmUtxoError::new(&format!("Failed to read {} from object", key)))?;
// =============================================================================
// Field access functions
// =============================================================================

T::try_from_js_value(&field_value)
.map_err(|e| WasmUtxoError::new(&format!("{} (field: {})", e, key)))
/// Get a raw JsValue field from an object without conversion
fn get_raw_field(obj: &JsValue, key: &str) -> Result<JsValue, WasmUtxoError> {
js_sys::Reflect::get(obj, &JsValue::from_str(key))
.map_err(|_| WasmUtxoError::new(&format!("Failed to read {} from object", key)))
}

// Helper function to get an optional field (returns None if undefined/null)
#[allow(dead_code)]
pub(crate) fn get_optional_field<T: TryFromJsValue>(
obj: &JsValue,
key: &str,
) -> Result<Option<T>, WasmUtxoError> {
let field_value = js_sys::Reflect::get(obj, &JsValue::from_str(key))
.map_err(|_| WasmUtxoError::new(&format!("Failed to read {} from object", key)))?;

if field_value.is_undefined() || field_value.is_null() {
Ok(None)
} else {
T::try_from_js_value(&field_value)
.map(Some)
.map_err(|e| WasmUtxoError::new(&format!("{} (field: {})", e, key)))
}
/// Navigate to a nested object using dot notation (e.g., "network.bip32")
fn get_nested_raw(obj: &JsValue, path: &str) -> Result<JsValue, WasmUtxoError> {
path.split('.').try_fold(obj.clone(), |current, part| {
js_sys::Reflect::get(&current, &JsValue::from_str(part))
.map_err(|_| WasmUtxoError::new(&format!("Failed to read {} from object", part)))
})
}

/// Get a field and convert it using TryFromJsValue
pub(crate) fn get_field<T: TryFromJsValue>(obj: &JsValue, key: &str) -> Result<T, WasmUtxoError> {
let field_value = get_raw_field(obj, key)?;
T::try_from_js_value(&field_value)
.map_err(|e| WasmUtxoError::new(&format!("{} (field: {})", e, key)))
}

// Helper function to get a nested field using dot notation (e.g., "network.bip32.public")
/// Get a nested field using dot notation (e.g., "network.bip32.public")
pub(crate) fn get_nested_field<T: TryFromJsValue>(
obj: &JsValue,
path: &str,
) -> Result<T, WasmUtxoError> {
let parts: Vec<&str> = path.split('.').collect();
let mut current = obj.clone();

for (i, part) in parts.iter().enumerate() {
if i == parts.len() - 1 {
// Last part - extract and convert
return get_field(&current, part);
} else {
// Intermediate part - just get the object
current = js_sys::Reflect::get(&current, &JsValue::from_str(part))
.map_err(|_| WasmUtxoError::new(&format!("Failed to read {} from object", part)))?;
}
}

Err(WasmUtxoError::new("Empty path"))
}

// Helper function to get a buffer field as a fixed-size byte array
pub(crate) fn get_buffer_field<const N: usize>(
obj: &JsValue,
key: &str,
) -> Result<[u8; N], WasmUtxoError> {
let field_value = js_sys::Reflect::get(obj, &JsValue::from_str(key))
.map_err(|_| WasmUtxoError::new(&format!("Failed to read {} from object", key)))?;

let buffer = js_sys::Uint8Array::new(&field_value);
if buffer.length() as usize != N {
return Err(WasmUtxoError::new(&format!(
"{} must be {} bytes, got {}",
key,
N,
buffer.length()
)));
}

let mut bytes = [0u8; N];
buffer.copy_to(&mut bytes);
Ok(bytes)
let field_value = get_nested_raw(obj, path)?;
T::try_from_js_value(&field_value)
.map_err(|e| WasmUtxoError::new(&format!("{} (path: {})", e, path)))
}

// Helper function to get a buffer field as a Vec
#[allow(dead_code)]
pub(crate) fn get_buffer_field_vec(obj: &JsValue, key: &str) -> Result<Vec<u8>, WasmUtxoError> {
let field_value = js_sys::Reflect::get(obj, &JsValue::from_str(key))
.map_err(|_| WasmUtxoError::new(&format!("Failed to read {} from object", key)))?;

let buffer = js_sys::Uint8Array::new(&field_value);
let mut bytes = vec![0u8; buffer.length() as usize];
buffer.copy_to(&mut bytes);
Ok(bytes)
}
// =============================================================================
// TryFromJsValue implementations for domain types
// =============================================================================

impl TryFromJsValue for UtxolibNetwork {
fn try_from_js_value(value: &JsValue) -> Result<Self, WasmUtxoError> {
let pub_key_hash = get_field(value, "pubKeyHash")?;
let script_hash = get_field(value, "scriptHash")?;
let bech32 = get_field(value, "bech32")?;
let cash_addr = get_field(value, "cashAddr")?;

Ok(UtxolibNetwork {
pub_key_hash,
script_hash,
cash_addr,
bech32,
pub_key_hash: get_field(value, "pubKeyHash")?,
script_hash: get_field(value, "scriptHash")?,
bech32: get_field(value, "bech32")?,
cash_addr: get_field(value, "cashAddr")?,
})
}
}

impl TryFromJsValue for CashAddr {
fn try_from_js_value(value: &JsValue) -> Result<Self, WasmUtxoError> {
let prefix = get_field(value, "prefix")?;
let pub_key_hash = get_field(value, "pubKeyHash")?;
let script_hash = get_field(value, "scriptHash")?;

Ok(CashAddr {
prefix,
pub_key_hash,
script_hash,
prefix: get_field(value, "prefix")?,
pub_key_hash: get_field(value, "pubKeyHash")?,
script_hash: get_field(value, "scriptHash")?,
})
}
}

impl TryFromJsValue for crate::inscriptions::TapLeafScript {
fn try_from_js_value(value: &JsValue) -> Result<Self, WasmUtxoError> {
let leaf_version: u8 = get_field(value, "leafVersion")?;
let script = get_buffer_field_vec(value, "script")?;
let control_block = get_buffer_field_vec(value, "controlBlock")?;

Ok(crate::inscriptions::TapLeafScript {
leaf_version,
script,
control_block,
leaf_version: get_field(value, "leafVersion")?,
script: get_field(value, "script")?,
control_block: get_field(value, "controlBlock")?,
})
}
}
Expand All @@ -184,7 +185,8 @@ impl TryFromJsValue for crate::networks::Network {
.or_else(|| crate::networks::Network::from_coin_name(&network_str))
.ok_or_else(|| {
WasmUtxoError::new(&format!(
"Unknown network '{}'. Expected a utxolib name (e.g., 'bitcoin', 'testnet') or coin name (e.g., 'btc', 'tbtc')",
"Unknown network '{}'. Expected a utxolib name (e.g., 'bitcoin', 'testnet') \
or coin name (e.g., 'btc', 'tbtc')",
network_str
))
})
Expand Down
Loading