From c0aaf60027c6a4f46665dfbd43cf7dc8c8fac685 Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Wed, 4 Feb 2026 13:34:19 +0100 Subject: [PATCH 1/3] feat(wasm-utxo): support private key handling in BIP32.from_bip32_properties Enhanced BIP32.from_bip32_properties to properly handle private keys when converting from JavaScript BIP32 interfaces. Added optional buffer field utility function to safely extract private key data when present. Tests confirm proper handling of both xpub and xprv conversions with complete feature parity between WASM and utxolib implementations. Issue: BTC-2650 Co-authored-by: llm-git --- packages/wasm-utxo/src/wasm/bip32.rs | 61 +++++++++++++------ .../wasm-utxo/src/wasm/try_from_js_value.rs | 27 ++++++++ packages/wasm-utxo/test/bip32.ts | 54 ++++++++++++++++ 3 files changed, 123 insertions(+), 19 deletions(-) diff --git a/packages/wasm-utxo/src/wasm/bip32.rs b/packages/wasm-utxo/src/wasm/bip32.rs index 316e2062..8812fc60 100644 --- a/packages/wasm-utxo/src/wasm/bip32.rs +++ b/packages/wasm-utxo/src/wasm/bip32.rs @@ -4,7 +4,9 @@ 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_buffer_field, get_field, get_nested_field, get_optional_buffer_field, +}; use wasm_bindgen::prelude::*; use wasm_bindgen::JsValue; @@ -156,30 +158,51 @@ 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 { - // 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))) + + // Check if private key exists + let private_key_bytes: Option<[u8; 32]> = + get_optional_buffer_field(bip32_key, "privateKey")?; + + if let Some(priv_key) = private_key_bytes { + // 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_bytes); // 32 bytes: chain code + data.push(0x00); // 1 byte: padding for private key + data.extend_from_slice(&priv_key); // 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: [u8; 33] = get_buffer_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_bytes); // 32 bytes: chain code + data.extend_from_slice(&public_key_bytes); // 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 diff --git a/packages/wasm-utxo/src/wasm/try_from_js_value.rs b/packages/wasm-utxo/src/wasm/try_from_js_value.rs index 1e201905..49288e39 100644 --- a/packages/wasm-utxo/src/wasm/try_from_js_value.rs +++ b/packages/wasm-utxo/src/wasm/try_from_js_value.rs @@ -130,6 +130,33 @@ pub(crate) fn get_buffer_field_vec(obj: &JsValue, key: &str) -> Result, Ok(bytes) } +// Helper function to get an optional buffer field as a fixed-size byte array +pub(crate) fn get_optional_buffer_field( + obj: &JsValue, + key: &str, +) -> Result, 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() { + return Ok(None); + } + + 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(Some(bytes)) +} + impl TryFromJsValue for UtxolibNetwork { fn try_from_js_value(value: &JsValue) -> Result { let pub_key_hash = get_field(value, "pubKeyHash")?; diff --git a/packages/wasm-utxo/test/bip32.ts b/packages/wasm-utxo/test/bip32.ts index d65e9d49..5f6167dd 100644 --- a/packages/wasm-utxo/test/bip32.ts +++ b/packages/wasm-utxo/test/bip32.ts @@ -378,4 +378,58 @@ describe("WasmBIP32 parity with utxolib", () => { assert.ok(bufferEqual(wasmKey.chainCode, utxolibKey.chainCode)); } }); + + it("should create from utxolib BIP32 instance with private key", () => { + const xprv = + "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi"; + + const utxolibKey = utxolibBip32.fromBase58(xprv); + const wasmKey = bip32.BIP32.from(utxolibKey); + + // Compare all properties + assert.strictEqual(wasmKey.toBase58(), utxolibKey.toBase58()); + assert.strictEqual(wasmKey.depth, utxolibKey.depth); + assert.strictEqual(wasmKey.index, utxolibKey.index); + assert.strictEqual(wasmKey.parentFingerprint, utxolibKey.parentFingerprint); + assert.strictEqual(wasmKey.isNeutered(), false); + assert.ok(bufferEqual(wasmKey.chainCode, utxolibKey.chainCode)); + assert.ok(bufferEqual(wasmKey.publicKey, utxolibKey.publicKey)); + assert.ok(bufferEqual(wasmKey.identifier, utxolibKey.identifier)); + assert.ok(bufferEqual(wasmKey.fingerprint, utxolibKey.fingerprint)); + assert.ok( + wasmKey.privateKey && + utxolibKey.privateKey && + bufferEqual(wasmKey.privateKey, utxolibKey.privateKey), + ); + + // Verify derivation still works after conversion + const wasmChild = wasmKey.derivePath("m/44'/0'/0'"); + const utxolibChild = utxolibKey.derivePath("m/44'/0'/0'"); + assert.strictEqual(wasmChild.toBase58(), utxolibChild.toBase58()); + }); + + it("should create from utxolib BIP32 instance without private key", () => { + const xpub = + "xpub6D4BDPcP2GT577Vvch3R8wDkScZWzQzMMUm3PWbmWvVJrZwQY4VUNgqFJPMM3No2dFDFGTsxxpG5uJh7n7epu4trkrX7x7DogT5Uv6fcLW5"; + + const utxolibKey = utxolibBip32.fromBase58(xpub); + const wasmKey = bip32.BIP32.from(utxolibKey); + + // Compare all properties + assert.strictEqual(wasmKey.toBase58(), utxolibKey.toBase58()); + assert.strictEqual(wasmKey.depth, utxolibKey.depth); + assert.strictEqual(wasmKey.index, utxolibKey.index); + assert.strictEqual(wasmKey.parentFingerprint, utxolibKey.parentFingerprint); + assert.strictEqual(wasmKey.isNeutered(), true); + assert.strictEqual(wasmKey.privateKey, undefined); + assert.ok(bufferEqual(wasmKey.chainCode, utxolibKey.chainCode)); + assert.ok(bufferEqual(wasmKey.publicKey, utxolibKey.publicKey)); + assert.ok(bufferEqual(wasmKey.identifier, utxolibKey.identifier)); + assert.ok(bufferEqual(wasmKey.fingerprint, utxolibKey.fingerprint)); + + // Verify derivation still works after conversion + const wasmChild = wasmKey.derive(0).derive(0); + const utxolibChild = utxolibKey.derive(0).derive(0); + assert.strictEqual(wasmChild.toBase58(), utxolibChild.toBase58()); + }); }); From 171eb9e80d517612686a35fe26f602da9255f779 Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Wed, 4 Feb 2026 13:41:06 +0100 Subject: [PATCH 2/3] fix(wasm-utxo): refactor byte array handling in bip32 module Introduce a type-safe `Bytes` wrapper for fixed-size byte arrays that implements TryFromJsValue. Replace direct buffer access with this cleaner approach in the BIP32 implementation. Issue: BTC-2650 Co-authored-by: llm-git --- packages/wasm-utxo/src/wasm/bip32.rs | 21 +- .../wasm-utxo/src/wasm/try_from_js_value.rs | 231 ++++++++---------- 2 files changed, 112 insertions(+), 140 deletions(-) diff --git a/packages/wasm-utxo/src/wasm/bip32.rs b/packages/wasm-utxo/src/wasm/bip32.rs index 8812fc60..9a05a943 100644 --- a/packages/wasm-utxo/src/wasm/bip32.rs +++ b/packages/wasm-utxo/src/wasm/bip32.rs @@ -4,9 +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, get_optional_buffer_field, -}; +use crate::wasm::try_from_js_value::{get_field, get_nested_field, Bytes}; use wasm_bindgen::prelude::*; use wasm_bindgen::JsValue; @@ -165,13 +163,12 @@ impl WasmBIP32 { 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 chain_code: Bytes<32> = get_field(bip32_key, "chainCode")?; // Check if private key exists - let private_key_bytes: Option<[u8; 32]> = - get_optional_buffer_field(bip32_key, "privateKey")?; + let private_key: Option> = get_field(bip32_key, "privateKey")?; - if let Some(priv_key) = private_key_bytes { + 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); @@ -179,9 +176,9 @@ impl WasmBIP32 { 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(chain_code.as_ref()); // 32 bytes: chain code data.push(0x00); // 1 byte: padding for private key - data.extend_from_slice(&priv_key); // 32 bytes: 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)))?; @@ -189,15 +186,15 @@ impl WasmBIP32 { } else { // Build xpub serialization (78 bytes total) let version: u32 = get_nested_field(bip32_key, "network.bip32.public")?; - let public_key_bytes: [u8; 33] = get_buffer_field(bip32_key, "publicKey")?; + 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_bytes); // 32 bytes: chain code - data.extend_from_slice(&public_key_bytes); // 33 bytes: public key + 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)))?; diff --git a/packages/wasm-utxo/src/wasm/try_from_js_value.rs b/packages/wasm-utxo/src/wasm/try_from_js_value.rs index 49288e39..0f2828a8 100644 --- a/packages/wasm-utxo/src/wasm/try_from_js_value.rs +++ b/packages/wasm-utxo/src/wasm/try_from_js_value.rs @@ -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 - 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; +} + +// ============================================================================= +// Bytes: Fixed-size byte array wrapper +// ============================================================================= + +/// Fixed-size byte array that implements TryFromJsValue +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct Bytes(pub [u8; N]); + +impl Deref for Bytes { + type Target = [u8; N]; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl AsRef<[u8]> for Bytes { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +impl From> for [u8; N] { + fn from(bytes: Bytes) -> Self { + bytes.0 + } +} + +impl TryFromJsValue for Bytes { + fn try_from_js_value(value: &JsValue) -> Result { + 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 { @@ -36,6 +86,15 @@ impl TryFromJsValue for u32 { } } +impl TryFromJsValue for Vec { + fn try_from_js_value(value: &JsValue) -> Result { + let buffer = js_sys::Uint8Array::new(value); + let mut bytes = vec![0u8; buffer.length() as usize]; + buffer.copy_to(&mut bytes); + Ok(bytes) + } +} + impl TryFromJsValue for Option { fn try_from_js_value(value: &JsValue) -> Result { if value.is_undefined() || value.is_null() { @@ -46,157 +105,72 @@ impl TryFromJsValue for Option { } } -// Helper function to get a field from an object and convert it using TryFromJsValue -pub(crate) fn get_field(obj: &JsValue, key: &str) -> Result { - 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 { + 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( - obj: &JsValue, - key: &str, -) -> Result, 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 { + path.split('.').try_fold(obj.clone(), |current, part| { + js_sys::Reflect::get(¤t, &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(obj: &JsValue, key: &str) -> Result { + 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( obj: &JsValue, path: &str, ) -> Result { - 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(¤t, part); - } else { - // Intermediate part - just get the object - current = js_sys::Reflect::get(¤t, &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( - 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) -} - -// 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, 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) + 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 an optional buffer field as a fixed-size byte array -pub(crate) fn get_optional_buffer_field( - obj: &JsValue, - key: &str, -) -> Result, 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() { - return Ok(None); - } - - 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(Some(bytes)) -} +// ============================================================================= +// TryFromJsValue implementations for domain types +// ============================================================================= impl TryFromJsValue for UtxolibNetwork { fn try_from_js_value(value: &JsValue) -> Result { - 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 { - 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 { - 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")?, }) } } @@ -211,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 )) }) From fa78f4bf8774e734d02d52ffbbce28ffe2ef526c Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Wed, 4 Feb 2026 14:00:07 +0100 Subject: [PATCH 3/3] feat(wasm-utxo): refactor bip32 tests for clarity and consistency Refactors BIP32 tests to improve readability and maintainability by: - Adding reusable test fixtures for common values - Creating helper functions for buffer comparison and validation - Consolidating redundant test assertions - Simplifying test structure with more consistent patterns - Improving organization of test cases Issue: BTC-2650 Co-authored-by: llm-git --- packages/wasm-utxo/test/bip32.ts | 420 ++++++++++--------------------- 1 file changed, 133 insertions(+), 287 deletions(-) diff --git a/packages/wasm-utxo/test/bip32.ts b/packages/wasm-utxo/test/bip32.ts index 5f6167dd..a9257c18 100644 --- a/packages/wasm-utxo/test/bip32.ts +++ b/packages/wasm-utxo/test/bip32.ts @@ -1,21 +1,53 @@ import * as assert from "assert"; import * as crypto from "crypto"; -import { bip32 as utxolibBip32 } from "@bitgo/utxo-lib"; +import { bip32 as utxolibBip32, BIP32Interface } from "@bitgo/utxo-lib"; import { BIP32 } from "../js/bip32.js"; -const bip32 = { BIP32 }; +// Test fixtures +const XPRV = + "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi"; +const XPUB = + "xpub6D4BDPcP2GT577Vvch3R8wDkScZWzQzMMUm3PWbmWvVJrZwQY4VUNgqFJPMM3No2dFDFGTsxxpG5uJh7n7epu4trkrX7x7DogT5Uv6fcLW5"; +const BIP39_SEED = Buffer.from( + "fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542", + "hex", +); + +// Helper to compare Uint8Array with Buffer +function assertBuffersEqual(a: Uint8Array, b: Buffer, msg?: string): void { + assert.strictEqual(Buffer.from(a).compare(b), 0, msg); +} + +// Helper to assert all common BIP32 properties match +function assertKeysMatch(wasm: BIP32, utxolib: BIP32Interface, msg?: string): void { + const prefix = msg ? `${msg}: ` : ""; + assert.strictEqual(wasm.toBase58(), utxolib.toBase58(), `${prefix}toBase58`); + assert.strictEqual(wasm.depth, utxolib.depth, `${prefix}depth`); + assert.strictEqual(wasm.index, utxolib.index, `${prefix}index`); + assert.strictEqual( + wasm.parentFingerprint, + utxolib.parentFingerprint, + `${prefix}parentFingerprint`, + ); + assert.strictEqual(wasm.isNeutered(), utxolib.isNeutered(), `${prefix}isNeutered`); + assertBuffersEqual(wasm.chainCode, utxolib.chainCode, `${prefix}chainCode`); + assertBuffersEqual(wasm.publicKey, utxolib.publicKey, `${prefix}publicKey`); + assertBuffersEqual(wasm.identifier, utxolib.identifier, `${prefix}identifier`); + assertBuffersEqual(wasm.fingerprint, utxolib.fingerprint, `${prefix}fingerprint`); + if (wasm.privateKey && utxolib.privateKey) { + assertBuffersEqual(wasm.privateKey, utxolib.privateKey, `${prefix}privateKey`); + } else { + assert.strictEqual(wasm.privateKey, utxolib.privateKey, `${prefix}privateKey undefined`); + } +} describe("WasmBIP32", () => { it("should create from base58 xpub", () => { - const xpub = - "xpub6D4BDPcP2GT577Vvch3R8wDkScZWzQzMMUm3PWbmWvVJrZwQY4VUNgqFJPMM3No2dFDFGTsxxpG5uJh7n7epu4trkrX7x7DogT5Uv6fcLW5"; - const key = bip32.BIP32.fromBase58(xpub); + const key = BIP32.fromBase58(XPUB); assert.strictEqual(key.isNeutered(), true); assert.strictEqual(key.depth, 3); - assert.strictEqual(key.toBase58(), xpub); - - // Verify properties exist + assert.strictEqual(key.toBase58(), XPUB); assert.ok(key.chainCode instanceof Uint8Array); assert.ok(key.publicKey instanceof Uint8Array); assert.ok(key.identifier instanceof Uint8Array); @@ -24,15 +56,11 @@ describe("WasmBIP32", () => { }); it("should create from base58 xprv", () => { - const xprv = - "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi"; - const key = bip32.BIP32.fromBase58(xprv); + const key = BIP32.fromBase58(XPRV); assert.strictEqual(key.isNeutered(), false); assert.strictEqual(key.depth, 0); - assert.strictEqual(key.toBase58(), xprv); - - // Verify properties exist + assert.strictEqual(key.toBase58(), XPRV); assert.ok(key.chainCode instanceof Uint8Array); assert.ok(key.publicKey instanceof Uint8Array); assert.ok(key.privateKey instanceof Uint8Array); @@ -41,119 +69,90 @@ describe("WasmBIP32", () => { }); it("should derive child keys", () => { - const xpub = - "xpub6D4BDPcP2GT577Vvch3R8wDkScZWzQzMMUm3PWbmWvVJrZwQY4VUNgqFJPMM3No2dFDFGTsxxpG5uJh7n7epu4trkrX7x7DogT5Uv6fcLW5"; - const key = bip32.BIP32.fromBase58(xpub); - + const key = BIP32.fromBase58(XPUB); const child = key.derive(0); + assert.strictEqual(child.depth, 4); assert.strictEqual(child.isNeutered(), true); }); it("should derive using path", () => { - const xprv = - "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi"; - const key = bip32.BIP32.fromBase58(xprv); + const key = BIP32.fromBase58(XPRV); const derived1 = key.derivePath("0/1/2"); - assert.strictEqual(derived1.depth, 3); - const derived2 = key.derivePath("m/0/1/2"); - assert.strictEqual(derived2.depth, 3); - // Both should produce the same result + assert.strictEqual(derived1.depth, 3); + assert.strictEqual(derived2.depth, 3); assert.strictEqual(derived1.toBase58(), derived2.toBase58()); }); - it("should neutered a private key", () => { - const xprv = - "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi"; - const key = bip32.BIP32.fromBase58(xprv); - const neuteredKey = key.neutered(); + it("should neuter a private key", () => { + const key = BIP32.fromBase58(XPRV); + const neutered = key.neutered(); - assert.strictEqual(neuteredKey.isNeutered(), true); - assert.strictEqual(neuteredKey.privateKey, undefined); - assert.ok(neuteredKey.publicKey instanceof Uint8Array); + assert.strictEqual(neutered.isNeutered(), true); + assert.strictEqual(neutered.privateKey, undefined); + assert.ok(neutered.publicKey instanceof Uint8Array); }); it("should derive hardened keys from private key", () => { - const xprv = - "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi"; - const key = bip32.BIP32.fromBase58(xprv); - + const key = BIP32.fromBase58(XPRV); const hardened = key.deriveHardened(0); + assert.strictEqual(hardened.depth, 1); assert.strictEqual(hardened.isNeutered(), false); }); it("should fail to derive hardened from public key", () => { - const xpub = - "xpub6D4BDPcP2GT577Vvch3R8wDkScZWzQzMMUm3PWbmWvVJrZwQY4VUNgqFJPMM3No2dFDFGTsxxpG5uJh7n7epu4trkrX7x7DogT5Uv6fcLW5"; - const key = bip32.BIP32.fromBase58(xpub); - - assert.throws(() => { - key.deriveHardened(0); - }); + const key = BIP32.fromBase58(XPUB); + assert.throws(() => key.deriveHardened(0)); }); it("should export to WIF", () => { - const xprv = - "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi"; - const key = bip32.BIP32.fromBase58(xprv); - + const key = BIP32.fromBase58(XPRV); const wif = key.toWIF(); + assert.ok(typeof wif === "string"); assert.ok(wif.length > 0); }); it("should fail to export WIF from public key", () => { - const xpub = - "xpub6D4BDPcP2GT577Vvch3R8wDkScZWzQzMMUm3PWbmWvVJrZwQY4VUNgqFJPMM3No2dFDFGTsxxpG5uJh7n7epu4trkrX7x7DogT5Uv6fcLW5"; - const key = bip32.BIP32.fromBase58(xpub); - - assert.throws(() => { - key.toWIF(); - }); + const key = BIP32.fromBase58(XPUB); + assert.throws(() => key.toWIF()); }); it("should create from seed", () => { - const seed = new Uint8Array(32); - for (let i = 0; i < 32; i++) { - seed[i] = i; - } + const seed = Uint8Array.from({ length: 32 }, (_, i) => i); + const key = BIP32.fromSeed(seed); - const key = bip32.BIP32.fromSeed(seed); assert.strictEqual(key.depth, 0); assert.strictEqual(key.isNeutered(), false); assert.ok(key.privateKey instanceof Uint8Array); }); it("should create from seed with network", () => { - const seed = new Uint8Array(32); - for (let i = 0; i < 32; i++) { - seed[i] = i; - } + const seed = Uint8Array.from({ length: 32 }, (_, i) => i); + const key = BIP32.fromSeed(seed, "BitcoinTestnet3"); - const key = bip32.BIP32.fromSeed(seed, "BitcoinTestnet3"); assert.strictEqual(key.depth, 0); assert.strictEqual(key.isNeutered(), false); assert.ok(key.toBase58().startsWith("tprv")); }); it("should create from seed string using SHA256", () => { - const seedString = "test"; - const key = bip32.BIP32.fromSeedSha256(seedString); - assert.strictEqual(key.depth, 0); - assert.strictEqual(key.isNeutered(), false); - assert.ok(key.privateKey instanceof Uint8Array); - // Should be deterministic - const key2 = bip32.BIP32.fromSeedSha256(seedString); - assert.strictEqual(key.toBase58(), key2.toBase58()); + const key1 = BIP32.fromSeedSha256("test"); + const key2 = BIP32.fromSeedSha256("test"); + + assert.strictEqual(key1.depth, 0); + assert.strictEqual(key1.isNeutered(), false); + assert.ok(key1.privateKey instanceof Uint8Array); + assert.strictEqual(key1.toBase58(), key2.toBase58()); // deterministic }); it("should create from seed string with network", () => { - const seedString = "test"; - const key = bip32.BIP32.fromSeedSha256(seedString, "BitcoinTestnet3"); + const key = BIP32.fromSeedSha256("test", "BitcoinTestnet3"); + assert.strictEqual(key.depth, 0); assert.strictEqual(key.isNeutered(), false); assert.ok(key.toBase58().startsWith("tprv")); @@ -161,275 +160,122 @@ describe("WasmBIP32", () => { }); describe("WasmBIP32 parity with utxolib", () => { - function bufferEqual(a: Uint8Array, b: Buffer): boolean { - if (a.length !== b.length) return false; - for (let i = 0; i < a.length; i++) { - if (a[i] !== b[i]) return false; - } - return true; - } - it("should match utxolib when creating from base58 xpub", () => { - const xpub = - "xpub6D4BDPcP2GT577Vvch3R8wDkScZWzQzMMUm3PWbmWvVJrZwQY4VUNgqFJPMM3No2dFDFGTsxxpG5uJh7n7epu4trkrX7x7DogT5Uv6fcLW5"; - - const wasmKey = bip32.BIP32.fromBase58(xpub); - const utxolibKey = utxolibBip32.fromBase58(xpub); - - // Compare all properties - assert.strictEqual(wasmKey.toBase58(), utxolibKey.toBase58()); - assert.strictEqual(wasmKey.depth, utxolibKey.depth); - assert.strictEqual(wasmKey.index, utxolibKey.index); - assert.strictEqual(wasmKey.parentFingerprint, utxolibKey.parentFingerprint); - assert.strictEqual(wasmKey.isNeutered(), utxolibKey.isNeutered()); - assert.ok(bufferEqual(wasmKey.chainCode, utxolibKey.chainCode)); - assert.ok(bufferEqual(wasmKey.publicKey, utxolibKey.publicKey)); - assert.ok(bufferEqual(wasmKey.identifier, utxolibKey.identifier)); - assert.ok(bufferEqual(wasmKey.fingerprint, utxolibKey.fingerprint)); + assertKeysMatch(BIP32.fromBase58(XPUB), utxolibBip32.fromBase58(XPUB)); }); it("should match utxolib when creating from base58 xprv", () => { - const xprv = - "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi"; - - const wasmKey = bip32.BIP32.fromBase58(xprv); - const utxolibKey = utxolibBip32.fromBase58(xprv); - - // Compare all properties - assert.strictEqual(wasmKey.toBase58(), utxolibKey.toBase58()); - assert.strictEqual(wasmKey.depth, utxolibKey.depth); - assert.strictEqual(wasmKey.index, utxolibKey.index); - assert.strictEqual(wasmKey.parentFingerprint, utxolibKey.parentFingerprint); - assert.strictEqual(wasmKey.isNeutered(), utxolibKey.isNeutered()); - assert.ok(bufferEqual(wasmKey.chainCode, utxolibKey.chainCode)); - assert.ok(bufferEqual(wasmKey.publicKey, utxolibKey.publicKey)); - assert.ok(bufferEqual(wasmKey.identifier, utxolibKey.identifier)); - assert.ok(bufferEqual(wasmKey.fingerprint, utxolibKey.fingerprint)); - assert.ok( - wasmKey.privateKey && - utxolibKey.privateKey && - bufferEqual(wasmKey.privateKey, utxolibKey.privateKey), - ); + assertKeysMatch(BIP32.fromBase58(XPRV), utxolibBip32.fromBase58(XPRV)); }); it("should match utxolib when deriving normal child keys", () => { - const xprv = - "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi"; - - const wasmKey = bip32.BIP32.fromBase58(xprv); - const utxolibKey = utxolibBip32.fromBase58(xprv); + const wasmKey = BIP32.fromBase58(XPRV); + const utxolibKey = utxolibBip32.fromBase58(XPRV); - // Derive several children and compare for (const index of [0, 1, 10, 100, 2147483647]) { - const wasmChild = wasmKey.derive(index); - const utxolibChild = utxolibKey.derive(index); - - assert.strictEqual(wasmChild.toBase58(), utxolibChild.toBase58(), `Failed at index ${index}`); - assert.ok(bufferEqual(wasmChild.publicKey, utxolibChild.publicKey)); - assert.ok(bufferEqual(wasmChild.chainCode, utxolibChild.chainCode)); + assertKeysMatch(wasmKey.derive(index), utxolibKey.derive(index), `index ${index}`); } }); it("should match utxolib when deriving hardened child keys", () => { - const xprv = - "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi"; - - const wasmKey = bip32.BIP32.fromBase58(xprv); - const utxolibKey = utxolibBip32.fromBase58(xprv); + const wasmKey = BIP32.fromBase58(XPRV); + const utxolibKey = utxolibBip32.fromBase58(XPRV); - // Derive several hardened children and compare for (const index of [0, 1, 10, 2147483647]) { - const wasmChild = wasmKey.deriveHardened(index); - const utxolibChild = utxolibKey.deriveHardened(index); - - assert.strictEqual( - wasmChild.toBase58(), - utxolibChild.toBase58(), - `Failed at hardened index ${index}`, + assertKeysMatch( + wasmKey.deriveHardened(index), + utxolibKey.deriveHardened(index), + `hardened ${index}`, ); - assert.ok(bufferEqual(wasmChild.publicKey, utxolibChild.publicKey)); - assert.ok(bufferEqual(wasmChild.chainCode, utxolibChild.chainCode)); } }); it("should match utxolib when deriving using paths", () => { - const xprv = - "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi"; - - const wasmKey = bip32.BIP32.fromBase58(xprv); - const utxolibKey = utxolibBip32.fromBase58(xprv); + const wasmKey = BIP32.fromBase58(XPRV); + const utxolibKey = utxolibBip32.fromBase58(XPRV); - const paths = ["0", "0/1", "0/1/2", "m/0/1/2", "0'/1", "m/44'/0'/0'", "m/44'/0'/0'/0/0"]; - - for (const path of paths) { - const wasmDerived = wasmKey.derivePath(path); - const utxolibDerived = utxolibKey.derivePath(path); - - assert.strictEqual( - wasmDerived.toBase58(), - utxolibDerived.toBase58(), - `Failed at path ${path}`, - ); - assert.ok(bufferEqual(wasmDerived.publicKey, utxolibDerived.publicKey)); - assert.ok(bufferEqual(wasmDerived.chainCode, utxolibDerived.chainCode)); + for (const path of ["0", "0/1", "0/1/2", "m/0/1/2", "0'/1", "m/44'/0'/0'", "m/44'/0'/0'/0/0"]) { + assertKeysMatch(wasmKey.derivePath(path), utxolibKey.derivePath(path), `path ${path}`); } }); it("should match utxolib when deriving from public keys", () => { - const xpub = - "xpub6D4BDPcP2GT577Vvch3R8wDkScZWzQzMMUm3PWbmWvVJrZwQY4VUNgqFJPMM3No2dFDFGTsxxpG5uJh7n7epu4trkrX7x7DogT5Uv6fcLW5"; + const wasmKey = BIP32.fromBase58(XPUB); + const utxolibKey = utxolibBip32.fromBase58(XPUB); - const wasmKey = bip32.BIP32.fromBase58(xpub); - const utxolibKey = utxolibBip32.fromBase58(xpub); - - // Derive several children from public key for (const index of [0, 1, 10, 100]) { - const wasmChild = wasmKey.derive(index); - const utxolibChild = utxolibKey.derive(index); - - assert.strictEqual(wasmChild.toBase58(), utxolibChild.toBase58()); - assert.ok(bufferEqual(wasmChild.publicKey, utxolibChild.publicKey)); + assertKeysMatch(wasmKey.derive(index), utxolibKey.derive(index), `index ${index}`); } }); it("should match utxolib when neutering", () => { - const xprv = - "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi"; - - const wasmKey = bip32.BIP32.fromBase58(xprv); - const utxolibKey = utxolibBip32.fromBase58(xprv); - - const wasmNeutered = wasmKey.neutered(); - const utxolibNeutered = utxolibKey.neutered(); - - assert.strictEqual(wasmNeutered.toBase58(), utxolibNeutered.toBase58()); - assert.ok(bufferEqual(wasmNeutered.publicKey, utxolibNeutered.publicKey)); - assert.ok(bufferEqual(wasmNeutered.chainCode, utxolibNeutered.chainCode)); - assert.strictEqual(wasmNeutered.privateKey, undefined); + const wasmKey = BIP32.fromBase58(XPRV).neutered(); + const utxolibKey = utxolibBip32.fromBase58(XPRV).neutered(); + assertKeysMatch(wasmKey, utxolibKey); }); it("should match utxolib when exporting to WIF", () => { - const xprv = - "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi"; - - const wasmKey = bip32.BIP32.fromBase58(xprv); - const utxolibKey = utxolibBip32.fromBase58(xprv); - - assert.strictEqual(wasmKey.toWIF(), utxolibKey.toWIF()); + assert.strictEqual(BIP32.fromBase58(XPRV).toWIF(), utxolibBip32.fromBase58(XPRV).toWIF()); }); - it("should match utxolib for BIP44 wallet derivation (m/44'/0'/0'/0/0)", () => { - const seed = Buffer.from( - "fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542", - "hex", - ); - - const wasmMaster = bip32.BIP32.fromSeed(seed); - const utxolibMaster = utxolibBip32.fromSeed(seed); - - // Standard BIP44 path for Bitcoin: m/44'/0'/0'/0/0 + it("should match utxolib for BIP44 wallet derivation", () => { const path = "m/44'/0'/0'/0/0"; - - const wasmDerived = wasmMaster.derivePath(path); - const utxolibDerived = utxolibMaster.derivePath(path); - - assert.strictEqual(wasmDerived.toBase58(), utxolibDerived.toBase58()); - assert.ok(bufferEqual(wasmDerived.publicKey, utxolibDerived.publicKey)); - assert.ok(bufferEqual(wasmDerived.chainCode, utxolibDerived.chainCode)); + assertKeysMatch( + BIP32.fromSeed(BIP39_SEED).derivePath(path), + utxolibBip32.fromSeed(BIP39_SEED).derivePath(path), + ); }); it("should produce same fingerprint for derived keys", () => { - const xprv = - "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi"; - - const wasmKey = bip32.BIP32.fromBase58(xprv); - const utxolibKey = utxolibBip32.fromBase58(xprv); + const wasmKey = BIP32.fromBase58(XPRV); + const utxolibKey = utxolibBip32.fromBase58(XPRV); - // Derive a child and check its parent fingerprint matches the parent's fingerprint const wasmChild = wasmKey.derive(0); const utxolibChild = utxolibKey.derive(0); - // Parent fingerprints should match assert.strictEqual(wasmChild.parentFingerprint, utxolibChild.parentFingerprint); - - // The parent fingerprint should match the parent's fingerprint - const wasmParentFp = new DataView(wasmKey.fingerprint.buffer).getUint32(0, false); - assert.strictEqual(wasmChild.parentFingerprint, wasmParentFp); + assert.strictEqual( + wasmChild.parentFingerprint, + new DataView(wasmKey.fingerprint.buffer).getUint32(0, false), + ); }); it("should match utxolib when using fromSeedSha256", () => { - // Test various seed strings to ensure parity with manual SHA256 + fromSeed - const seedStrings = ["test", "user", "backup", "bitgo", "default.0", "default.1", "default.2"]; - - for (const seedString of seedStrings) { - // Manual approach: hash with SHA256, then create from seed + for (const seedString of [ + "test", + "user", + "backup", + "bitgo", + "default.0", + "default.1", + "default.2", + ]) { const hash = crypto.createHash("sha256").update(seedString).digest(); - const utxolibKey = utxolibBip32.fromSeed(hash); - - // WASM approach: fromSeedSha256 does hashing internally - const wasmKey = bip32.BIP32.fromSeedSha256(seedString); - - assert.strictEqual( - wasmKey.toBase58(), - utxolibKey.toBase58(), - `Failed for seed string: ${seedString}`, + assertKeysMatch( + BIP32.fromSeedSha256(seedString), + utxolibBip32.fromSeed(hash), + `seed "${seedString}"`, ); - assert.ok(bufferEqual(wasmKey.publicKey, utxolibKey.publicKey)); - assert.ok(bufferEqual(wasmKey.chainCode, utxolibKey.chainCode)); } }); it("should create from utxolib BIP32 instance with private key", () => { - const xprv = - "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi"; - - const utxolibKey = utxolibBip32.fromBase58(xprv); - const wasmKey = bip32.BIP32.from(utxolibKey); - - // Compare all properties - assert.strictEqual(wasmKey.toBase58(), utxolibKey.toBase58()); - assert.strictEqual(wasmKey.depth, utxolibKey.depth); - assert.strictEqual(wasmKey.index, utxolibKey.index); - assert.strictEqual(wasmKey.parentFingerprint, utxolibKey.parentFingerprint); - assert.strictEqual(wasmKey.isNeutered(), false); - assert.ok(bufferEqual(wasmKey.chainCode, utxolibKey.chainCode)); - assert.ok(bufferEqual(wasmKey.publicKey, utxolibKey.publicKey)); - assert.ok(bufferEqual(wasmKey.identifier, utxolibKey.identifier)); - assert.ok(bufferEqual(wasmKey.fingerprint, utxolibKey.fingerprint)); - assert.ok( - wasmKey.privateKey && - utxolibKey.privateKey && - bufferEqual(wasmKey.privateKey, utxolibKey.privateKey), + const utxolibKey = utxolibBip32.fromBase58(XPRV); + const wasmKey = BIP32.from(utxolibKey); + + assertKeysMatch(wasmKey, utxolibKey); + assertKeysMatch( + wasmKey.derivePath("m/44'/0'/0'"), + utxolibKey.derivePath("m/44'/0'/0'"), + "derived", ); - - // Verify derivation still works after conversion - const wasmChild = wasmKey.derivePath("m/44'/0'/0'"); - const utxolibChild = utxolibKey.derivePath("m/44'/0'/0'"); - assert.strictEqual(wasmChild.toBase58(), utxolibChild.toBase58()); }); it("should create from utxolib BIP32 instance without private key", () => { - const xpub = - "xpub6D4BDPcP2GT577Vvch3R8wDkScZWzQzMMUm3PWbmWvVJrZwQY4VUNgqFJPMM3No2dFDFGTsxxpG5uJh7n7epu4trkrX7x7DogT5Uv6fcLW5"; - - const utxolibKey = utxolibBip32.fromBase58(xpub); - const wasmKey = bip32.BIP32.from(utxolibKey); - - // Compare all properties - assert.strictEqual(wasmKey.toBase58(), utxolibKey.toBase58()); - assert.strictEqual(wasmKey.depth, utxolibKey.depth); - assert.strictEqual(wasmKey.index, utxolibKey.index); - assert.strictEqual(wasmKey.parentFingerprint, utxolibKey.parentFingerprint); - assert.strictEqual(wasmKey.isNeutered(), true); - assert.strictEqual(wasmKey.privateKey, undefined); - assert.ok(bufferEqual(wasmKey.chainCode, utxolibKey.chainCode)); - assert.ok(bufferEqual(wasmKey.publicKey, utxolibKey.publicKey)); - assert.ok(bufferEqual(wasmKey.identifier, utxolibKey.identifier)); - assert.ok(bufferEqual(wasmKey.fingerprint, utxolibKey.fingerprint)); - - // Verify derivation still works after conversion - const wasmChild = wasmKey.derive(0).derive(0); - const utxolibChild = utxolibKey.derive(0).derive(0); - assert.strictEqual(wasmChild.toBase58(), utxolibChild.toBase58()); + const utxolibKey = utxolibBip32.fromBase58(XPUB); + const wasmKey = BIP32.from(utxolibKey); + + assertKeysMatch(wasmKey, utxolibKey); + assertKeysMatch(wasmKey.derive(0).derive(0), utxolibKey.derive(0).derive(0), "derived"); }); });