diff --git a/packages/wasm-utxo/js/bip32.ts b/packages/wasm-utxo/js/bip32.ts index 42a3e6822ed..dd5329ef23d 100644 --- a/packages/wasm-utxo/js/bip32.ts +++ b/packages/wasm-utxo/js/bip32.ts @@ -97,6 +97,18 @@ export class BIP32 implements BIP32Interface { return new BIP32(wasm); } + /** + * Create a BIP32 master key from a string by hashing it with SHA256. + * Useful for deterministic test key generation. + * @param seedString - The seed string to hash + * @param network - Optional network string + * @returns A BIP32 instance + */ + static fromSeedSha256(seedString: string, network?: string | null): BIP32 { + const wasm = WasmBIP32.from_seed_sha256(seedString, network); + return new BIP32(wasm); + } + /** * Get the chain code as a Uint8Array */ diff --git a/packages/wasm-utxo/js/index.ts b/packages/wasm-utxo/js/index.ts index c318c31a28a..c238f86d1a0 100644 --- a/packages/wasm-utxo/js/index.ts +++ b/packages/wasm-utxo/js/index.ts @@ -14,6 +14,7 @@ export * as utxolibCompat from "./utxolibCompat.js"; export * as fixedScriptWallet from "./fixedScriptWallet/index.js"; export * as bip32 from "./bip32.js"; export * as ecpair from "./ecpair.js"; +export * as testutils from "./testutils/index.js"; // Only the most commonly used classes and types are exported at the top level for convenience export { ECPair } from "./ecpair.js"; diff --git a/packages/wasm-utxo/js/testutils/keys.ts b/packages/wasm-utxo/js/testutils/keys.ts index cb1881a1e1f..c71edcae179 100644 --- a/packages/wasm-utxo/js/testutils/keys.ts +++ b/packages/wasm-utxo/js/testutils/keys.ts @@ -1,4 +1,3 @@ -import * as crypto from "crypto"; import { BIP32 } from "../bip32.js"; import { RootWalletKeys } from "../fixedScriptWallet/RootWalletKeys.js"; import type { Triple } from "../triple.js"; @@ -17,7 +16,7 @@ import type { Triple } from "../triple.js"; * ``` */ export function getKey(seed: string): BIP32 { - return BIP32.fromSeed(crypto.createHash("sha256").update(seed).digest()); + return BIP32.fromSeedSha256(seed); } /** diff --git a/packages/wasm-utxo/src/wasm/bip32.rs b/packages/wasm-utxo/src/wasm/bip32.rs index c03934b82ff..316e20623a7 100644 --- a/packages/wasm-utxo/src/wasm/bip32.rs +++ b/packages/wasm-utxo/src/wasm/bip32.rs @@ -210,6 +210,18 @@ impl WasmBIP32 { Ok(WasmBIP32(BIP32Key::Private(xpriv))) } + /// Create a BIP32 master key from a string by hashing it with SHA256. + /// This is useful for deterministic test key generation. + #[wasm_bindgen] + pub fn from_seed_sha256( + seed_string: &str, + network: Option, + ) -> Result { + use crate::bitcoin::hashes::{sha256, Hash}; + let hash = sha256::Hash::hash(seed_string.as_bytes()); + Self::from_seed(&hash[..], network) + } + /// Get the chain code as a Uint8Array #[wasm_bindgen(getter)] pub fn chain_code(&self) -> js_sys::Uint8Array { diff --git a/packages/wasm-utxo/test/bip32.ts b/packages/wasm-utxo/test/bip32.ts index 4ddd8f762d2..d65e9d493b6 100644 --- a/packages/wasm-utxo/test/bip32.ts +++ b/packages/wasm-utxo/test/bip32.ts @@ -1,4 +1,5 @@ import * as assert from "assert"; +import * as crypto from "crypto"; import { bip32 as utxolibBip32 } from "@bitgo/utxo-lib"; import { BIP32 } from "../js/bip32.js"; @@ -138,6 +139,25 @@ describe("WasmBIP32", () => { 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()); + }); + + it("should create from seed string with network", () => { + const seedString = "test"; + const key = bip32.BIP32.fromSeedSha256(seedString, "BitcoinTestnet3"); + assert.strictEqual(key.depth, 0); + assert.strictEqual(key.isNeutered(), false); + assert.ok(key.toBase58().startsWith("tprv")); + }); }); describe("WasmBIP32 parity with utxolib", () => { @@ -336,4 +356,26 @@ describe("WasmBIP32 parity with utxolib", () => { const wasmParentFp = new DataView(wasmKey.fingerprint.buffer).getUint32(0, false); assert.strictEqual(wasmChild.parentFingerprint, wasmParentFp); }); + + 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 + 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}`, + ); + assert.ok(bufferEqual(wasmKey.publicKey, utxolibKey.publicKey)); + assert.ok(bufferEqual(wasmKey.chainCode, utxolibKey.chainCode)); + } + }); });