Skip to content

Commit 93ce5df

Browse files
OttoAllmendingerllm-git
andcommitted
feat(wasm-utxo): implement BIP32 extended key functionality
Add complete BIP32 implementation with both Rust and TypeScript interfaces. The implementation handles key derivation for both private and public keys, including hardened derivation, path-based derivation, and WIF export. This provides a full replacement for the BIP32 functionality from utxo-lib, with test coverage to ensure API parity. Issue: BTC-2786 Co-authored-by: llm-git <[email protected]>
1 parent 4d7fb3d commit 93ce5df

File tree

8 files changed

+1023
-58
lines changed

8 files changed

+1023
-58
lines changed

packages/wasm-utxo/js/bip32.ts

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
import { WasmBIP32 } from "./wasm/wasm_utxo.js";
2+
3+
/**
4+
* BIP32Arg represents the various forms that BIP32 keys can take
5+
* before being converted to a WasmBIP32 instance
6+
*/
7+
export type BIP32Arg =
8+
/** base58-encoded extended key string (xpub/xprv/tpub/tprv) */
9+
| string
10+
/** BIP32 instance */
11+
| BIP32
12+
/** WasmBIP32 instance */
13+
| WasmBIP32
14+
/** BIP32Interface compatible object */
15+
| BIP32Interface;
16+
17+
/**
18+
* BIP32 interface for extended key operations
19+
*/
20+
export interface BIP32Interface {
21+
chainCode: Uint8Array;
22+
depth: number;
23+
index: number;
24+
parentFingerprint: number;
25+
privateKey?: Uint8Array;
26+
publicKey: Uint8Array;
27+
identifier: Uint8Array;
28+
fingerprint: Uint8Array;
29+
isNeutered(): boolean;
30+
neutered(): BIP32Interface;
31+
toBase58(): string;
32+
toWIF(): string;
33+
derive(index: number): BIP32Interface;
34+
deriveHardened(index: number): BIP32Interface;
35+
derivePath(path: string): BIP32Interface;
36+
}
37+
38+
/**
39+
* BIP32 wrapper class for extended key operations
40+
*/
41+
export class BIP32 implements BIP32Interface {
42+
private constructor(private _wasm: WasmBIP32) {}
43+
44+
/**
45+
* Create a BIP32 instance from a WasmBIP32 instance (internal use)
46+
* @internal
47+
*/
48+
static fromWasm(wasm: WasmBIP32): BIP32 {
49+
return new BIP32(wasm);
50+
}
51+
52+
/**
53+
* Convert BIP32Arg to BIP32 instance
54+
* @param key - The BIP32 key in various formats
55+
* @returns BIP32 instance
56+
*/
57+
static from(key: BIP32Arg): BIP32 {
58+
// Short-circuit if already a BIP32 instance
59+
if (key instanceof BIP32) {
60+
return key;
61+
}
62+
// If it's a WasmBIP32 instance, wrap it
63+
if (key instanceof WasmBIP32) {
64+
return new BIP32(key);
65+
}
66+
// If it's a string, parse from base58
67+
if (typeof key === "string") {
68+
const wasm = WasmBIP32.from_base58(key);
69+
return new BIP32(wasm);
70+
}
71+
// If it's an object (BIP32Interface), use from_bip32_interface
72+
if (typeof key === "object" && key !== null) {
73+
const wasm = WasmBIP32.from_bip32_interface(key);
74+
return new BIP32(wasm);
75+
}
76+
throw new Error("Invalid BIP32Arg type");
77+
}
78+
79+
/**
80+
* Create a BIP32 key from a base58 string (xpub/xprv/tpub/tprv)
81+
* @param base58Str - The base58-encoded extended key string
82+
* @returns A BIP32 instance
83+
*/
84+
static fromBase58(base58Str: string): BIP32 {
85+
const wasm = WasmBIP32.from_base58(base58Str);
86+
return new BIP32(wasm);
87+
}
88+
89+
/**
90+
* Create a BIP32 master key from a seed
91+
* @param seed - The seed bytes
92+
* @param network - Optional network string
93+
* @returns A BIP32 instance
94+
*/
95+
static fromSeed(seed: Uint8Array, network?: string | null): BIP32 {
96+
const wasm = WasmBIP32.from_seed(seed, network);
97+
return new BIP32(wasm);
98+
}
99+
100+
/**
101+
* Get the chain code as a Uint8Array
102+
*/
103+
get chainCode(): Uint8Array {
104+
return this._wasm.chain_code;
105+
}
106+
107+
/**
108+
* Get the depth
109+
*/
110+
get depth(): number {
111+
return this._wasm.depth;
112+
}
113+
114+
/**
115+
* Get the child index
116+
*/
117+
get index(): number {
118+
return this._wasm.index;
119+
}
120+
121+
/**
122+
* Get the parent fingerprint
123+
*/
124+
get parentFingerprint(): number {
125+
return this._wasm.parent_fingerprint;
126+
}
127+
128+
/**
129+
* Get the private key as a Uint8Array (if available)
130+
*/
131+
get privateKey(): Uint8Array | undefined {
132+
return this._wasm.private_key;
133+
}
134+
135+
/**
136+
* Get the public key as a Uint8Array
137+
*/
138+
get publicKey(): Uint8Array {
139+
return this._wasm.public_key;
140+
}
141+
142+
/**
143+
* Get the identifier as a Uint8Array
144+
*/
145+
get identifier(): Uint8Array {
146+
return this._wasm.identifier;
147+
}
148+
149+
/**
150+
* Get the fingerprint as a Uint8Array
151+
*/
152+
get fingerprint(): Uint8Array {
153+
return this._wasm.fingerprint;
154+
}
155+
156+
/**
157+
* Check if this is a neutered (public) key
158+
* @returns True if the key is public-only (neutered)
159+
*/
160+
isNeutered(): boolean {
161+
return this._wasm.is_neutered();
162+
}
163+
164+
/**
165+
* Get the neutered (public) version of this key
166+
* @returns A new BIP32 instance containing only the public key
167+
*/
168+
neutered(): BIP32 {
169+
const wasm = this._wasm.neutered();
170+
return new BIP32(wasm);
171+
}
172+
173+
/**
174+
* Serialize to base58 string
175+
* @returns The base58-encoded extended key string
176+
*/
177+
toBase58(): string {
178+
return this._wasm.to_base58();
179+
}
180+
181+
/**
182+
* Get the WIF encoding of the private key
183+
* @returns The WIF-encoded private key
184+
*/
185+
toWIF(): string {
186+
return this._wasm.to_wif();
187+
}
188+
189+
/**
190+
* Derive a normal (non-hardened) child key
191+
* @param index - The child index
192+
* @returns A new BIP32 instance for the derived key
193+
*/
194+
derive(index: number): BIP32 {
195+
const wasm = this._wasm.derive(index);
196+
return new BIP32(wasm);
197+
}
198+
199+
/**
200+
* Derive a hardened child key (only works for private keys)
201+
* @param index - The child index
202+
* @returns A new BIP32 instance for the derived key
203+
*/
204+
deriveHardened(index: number): BIP32 {
205+
const wasm = this._wasm.derive_hardened(index);
206+
return new BIP32(wasm);
207+
}
208+
209+
/**
210+
* Derive a key using a derivation path (e.g., "0/1/2" or "m/0/1/2")
211+
* @param path - The derivation path string
212+
* @returns A new BIP32 instance for the derived key
213+
*/
214+
derivePath(path: string): BIP32 {
215+
const wasm = this._wasm.derive_path(path);
216+
return new BIP32(wasm);
217+
}
218+
219+
/**
220+
* Get the underlying WASM instance (internal use only)
221+
* @internal
222+
*/
223+
get wasm(): WasmBIP32 {
224+
return this._wasm;
225+
}
226+
}

packages/wasm-utxo/js/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export * as utxolibCompat from "./utxolibCompat.js";
1010
export * as fixedScriptWallet from "./fixedScriptWallet.js";
1111

1212
export { ECPair } from "./ecpair.js";
13+
export { BIP32 } from "./bip32.js";
1314

1415
export type { CoinName } from "./coinName.js";
1516
export type { Triple } from "./triple.js";

0 commit comments

Comments
 (0)