diff --git a/packages/wasm-utxo/src/wasm/bip32.rs b/packages/wasm-utxo/src/wasm/bip32.rs index 316e206..9a05a94 100644 --- a/packages/wasm-utxo/src/wasm/bip32.rs +++ b/packages/wasm-utxo/src/wasm/bip32.rs @@ -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; @@ -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 { - // 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> = 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 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 1e20190..0f2828a 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,130 +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) + 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, 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 { - 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")?, }) } } @@ -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 )) }) diff --git a/packages/wasm-utxo/test/bip32.ts b/packages/wasm-utxo/test/bip32.ts index d65e9d4..a9257c1 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,221 +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.fromBase58(XPRV); + const utxolibKey = utxolibBip32.fromBase58(XPRV); - const wasmKey = bip32.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.fromBase58(XPRV); + const utxolibKey = utxolibBip32.fromBase58(XPRV); - const wasmKey = bip32.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 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", + ); + }); + + it("should create from utxolib BIP32 instance without private key", () => { + 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"); + }); });