Skip to content

Commit 878d8f1

Browse files
OttoAllmendingerllm-git
andcommitted
feat(wasm-utxo): add RootWalletKeys class wrapper for wallet keys
Added a new TypeScript class wrapper for wallet keys that provides a more type-safe and consistent API over the raw WASM bindings. - Created RootWalletKeys class with proper static factory methods - Updated fixedScriptWallet to work with the new RootWalletKeys class - Extended BitGoPsbt.verifySignature to support both BIP32 and ECPair - Improved README.md with detailed architecture patterns documentation - Updated tests to use the new API This implements a clean class wrapper pattern similar to BIP32 and ECPair, maintaining a consistent API style across the library. Issue: BTC-2786 Co-authored-by: llm-git <[email protected]>
1 parent 93ce5df commit 878d8f1

File tree

14 files changed

+627
-184
lines changed

14 files changed

+627
-184
lines changed

packages/wasm-utxo/js/README.md

Lines changed: 139 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,42 +6,61 @@ generated by the `wasm-pack` command (which uses `wasm-bindgen`).
66
While the `wasm-bindgen` crate allows some customization of the emitted type signatures, it
77
is a bit painful to use and has certain limitations that cannot be easily worked around.
88

9-
## Architecture Pattern
9+
## Architecture Patterns
1010

11-
This directory implements a **namespace wrapper pattern** that provides a cleaner, more
12-
type-safe API over the raw WASM bindings.
11+
This directory implements two complementary patterns to provide cleaner, more type-safe APIs over the raw WASM bindings:
1312

14-
### Pattern Overview
13+
1. **Namespace Wrapper Pattern** - For static utility functions
14+
2. **Class Wrapper Pattern** - For stateful objects with methods
15+
16+
### Common Elements
1517

1618
1. **WASM Generation** (`wasm/wasm_utxo.d.ts`)
1719

1820
- Generated by `wasm-bindgen` from Rust code
19-
- Exports classes with static methods (e.g., `AddressNamespace`, `UtxolibCompatNamespace`)
20-
- Uses `snake_case` naming (Rust convention)
21+
- Uses `snake_case` naming (Rust convention) - **no `js_name` overrides in Rust**
2122
- Uses loose types (`any`, `string | null`) due to WASM-bindgen limitations
23+
- TypeScript wrapper layer handles conversion to `camelCase`
2224

23-
2. **Namespace Wrapper Files** (e.g., `address.ts`, `utxolibCompat.ts`, `fixedScriptWallet.ts`)
24-
25-
- Import the generated WASM namespace classes
26-
- Define precise TypeScript types to replace `any` types
27-
- Export individual functions that wrap the static WASM methods
28-
- Convert `snake_case` WASM methods to `camelCase` (JavaScript convention)
29-
- Re-export related types for convenience
30-
31-
3. **Shared Type Files** (e.g., `coinName.ts`, `triple.ts`)
25+
2. **Shared Type Files** (e.g., `coinName.ts`, `triple.ts`)
3226

3327
- Define common types used across multiple modules
3428
- Single source of truth to avoid duplication
3529
- Imported by wrapper files as needed
3630

37-
4. **Main Entry Point** (`index.ts`)
31+
3. **Main Entry Point** (`index.ts`)
3832
- Uses `export * as` to group related functionality into namespaces
39-
- Re-exports shared types for top-level access
33+
- Re-exports shared types and classes for top-level access
4034
- Augments WASM types with additional TypeScript declarations
4135

42-
### Example
36+
### Pattern 1: Namespace Wrapper Pattern
37+
38+
Used for static utility functions (e.g., `address.ts`, `utxolibCompat.ts`).
39+
40+
**Characteristics:**
41+
42+
- Import the generated WASM namespace classes
43+
- Define precise TypeScript types to replace `any` types
44+
- Export individual functions that wrap the static WASM methods
45+
- Convert `snake_case` WASM methods to `camelCase` (JavaScript convention)
46+
- Re-export related types for convenience
47+
48+
### Pattern 2: Class Wrapper Pattern
49+
50+
Used for stateful objects that maintain WASM instances (e.g., `BIP32`, `RootWalletKeys`, `BitGoPsbt`).
51+
52+
**Characteristics:**
4353

44-
Given a WASM-generated class:
54+
- Private `_wasm` property holds the underlying WASM instance
55+
- Private constructor prevents direct instantiation
56+
- Static factory methods (camelCase) for object creation
57+
- Instance methods (camelCase) wrap WASM methods and return wrapped instances when appropriate
58+
- Public `wasm` getter for internal access to WASM instance (marked `@internal`)
59+
- Implements interfaces to ensure compatibility with existing code
60+
61+
### Example 1: Namespace Wrapper Pattern
62+
63+
Given a WASM-generated namespace class:
4564

4665
```typescript
4766
// wasm/wasm_utxo.d.ts (generated by wasm-bindgen)
@@ -88,10 +107,110 @@ And expose it via the main entry point:
88107
export * as address from "./address";
89108
```
90109

110+
### Example 2: Class Wrapper Pattern
111+
112+
Given a WASM-generated class with instance methods:
113+
114+
```typescript
115+
// wasm/wasm_utxo.d.ts (generated by wasm-bindgen)
116+
export class WasmBIP32 {
117+
private constructor();
118+
// Note: snake_case naming from Rust (no js_name overrides)
119+
static from_base58(base58_str: string): WasmBIP32;
120+
derive(index: number): WasmBIP32;
121+
derive_path(path: string): WasmBIP32;
122+
to_base58(): string;
123+
readonly public_key: Uint8Array;
124+
}
125+
```
126+
127+
We create a wrapper class that encapsulates the WASM instance:
128+
129+
```typescript
130+
// bip32.ts
131+
import { WasmBIP32 } from "./wasm/wasm_utxo";
132+
133+
export class BIP32 {
134+
// Private property with underscore prefix
135+
private constructor(private _wasm: WasmBIP32) {}
136+
137+
// Static factory method (camelCase) calls snake_case WASM method
138+
static fromBase58(base58Str: string): BIP32 {
139+
const wasm = WasmBIP32.from_base58(base58Str);
140+
return new BIP32(wasm);
141+
}
142+
143+
// Property getter (camelCase) accesses snake_case WASM property
144+
get publicKey(): Uint8Array {
145+
return this._wasm.public_key;
146+
}
147+
148+
// Instance method (camelCase) returns wrapped instance
149+
derive(index: number): BIP32 {
150+
const wasm = this._wasm.derive(index);
151+
return new BIP32(wasm);
152+
}
153+
154+
// Convert snake_case to camelCase
155+
derivePath(path: string): BIP32 {
156+
const wasm = this._wasm.derive_path(path);
157+
return new BIP32(wasm);
158+
}
159+
160+
// Convert snake_case to camelCase
161+
toBase58(): string {
162+
return this._wasm.to_base58();
163+
}
164+
165+
// Public getter for internal use (marked @internal)
166+
/**
167+
* @internal
168+
*/
169+
get wasm(): WasmBIP32 {
170+
return this._wasm;
171+
}
172+
}
173+
```
174+
175+
And expose it directly:
176+
177+
```typescript
178+
// index.ts
179+
export { BIP32 } from "./bip32";
180+
```
181+
91182
### Benefits
92183

184+
**Common to Both Patterns:**
185+
93186
- **Type Safety**: Replace loose `any` and `string` types with precise union types
94-
- **Idiomatic Naming**: Each layer uses its native convention (`snake_case` in Rust, `camelCase` in JavaScript)
187+
- **Idiomatic Naming**: Each layer uses its native convention (`snake_case` in Rust/WASM, `camelCase` in TypeScript/JavaScript)
188+
- Rust exports use `snake_case` (no `js_name` overrides)
189+
- TypeScript wrappers provide `camelCase` API
95190
- **Better DX**: IDE autocomplete works better with concrete types and familiar naming
96191
- **Maintainability**: Centralized type definitions prevent duplication
97192
- **Clear Separation**: WASM bindings stay pure to Rust conventions, TypeScript handles JS conventions
193+
194+
**Class Wrapper Pattern Specific:**
195+
196+
- **Encapsulation**: Private `_wasm` property hides implementation details
197+
- **Controlled Access**: Private constructor forces use of factory methods
198+
- **Consistent Returns**: Methods that return new instances automatically wrap them
199+
- **Internal Access**: Public `wasm` getter allows internal code to access WASM instance when needed
200+
- **Type Compatibility**: Can implement interfaces to maintain backward compatibility
201+
202+
### When to Use Which Pattern
203+
204+
**Use Namespace Wrapper Pattern when:**
205+
206+
- Functions are stateless utilities
207+
- No need to maintain WASM instance state
208+
- Simple input → output transformations
209+
- Examples: address encoding/decoding, network conversions
210+
211+
**Use Class Wrapper Pattern when:**
212+
213+
- Object represents stateful data (keys, PSBTs, etc.)
214+
- Methods need to return new instances of the same type
215+
- Need to encapsulate underlying WASM instance
216+
- Examples: BIP32 keys, RootWalletKeys, BitGoPsbt
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import type { BIP32Interface } from "./bip32.js";
2+
import { BIP32 } from "./bip32.js";
3+
import { Triple } from "./triple.js";
4+
import { WasmRootWalletKeys, WasmBIP32 } from "./wasm/wasm_utxo.js";
5+
6+
/**
7+
* IWalletKeys represents the various forms that wallet keys can take
8+
* before being converted to a RootWalletKeys instance
9+
*/
10+
export type IWalletKeys = {
11+
triple: Triple<BIP32Interface>;
12+
derivationPrefixes: Triple<string>;
13+
};
14+
15+
export type WalletKeysArg =
16+
/** Just an xpub triple, will assume default derivation prefixes */
17+
| Triple<string>
18+
/** Compatible with utxolib RootWalletKeys */
19+
| IWalletKeys
20+
/** RootWalletKeys instance */
21+
| RootWalletKeys;
22+
23+
/**
24+
* Convert WalletKeysArg to a triple of WasmBIP32 instances
25+
*/
26+
function toBIP32Triple(keys: WalletKeysArg): Triple<WasmBIP32> {
27+
if (keys instanceof RootWalletKeys) {
28+
return [keys.userKey().wasm, keys.backupKey().wasm, keys.bitgoKey().wasm];
29+
}
30+
31+
// Check if it's an IWalletKeys object
32+
if (typeof keys === "object" && "triple" in keys) {
33+
// Extract BIP32 keys from the triple
34+
return keys.triple.map((key) => BIP32.from(key).wasm) as Triple<WasmBIP32>;
35+
}
36+
37+
// Otherwise it's a triple of strings (xpubs)
38+
return keys.map((xpub) => WasmBIP32.from_xpub(xpub)) as Triple<WasmBIP32>;
39+
}
40+
41+
/**
42+
* Extract derivation prefixes from WalletKeysArg, if present
43+
*/
44+
function extractDerivationPrefixes(keys: WalletKeysArg): Triple<string> | null {
45+
if (typeof keys === "object" && "derivationPrefixes" in keys) {
46+
return keys.derivationPrefixes;
47+
}
48+
return null;
49+
}
50+
51+
/**
52+
* RootWalletKeys represents a set of three extended public keys with their derivation prefixes
53+
*/
54+
export class RootWalletKeys {
55+
private constructor(private _wasm: WasmRootWalletKeys) {}
56+
57+
/**
58+
* Create a RootWalletKeys from various input formats
59+
* @param keys - Can be a triple of xpub strings, an IWalletKeys object, or another RootWalletKeys instance
60+
* @returns A RootWalletKeys instance
61+
*/
62+
static from(keys: WalletKeysArg): RootWalletKeys {
63+
if (keys instanceof RootWalletKeys) {
64+
return keys;
65+
}
66+
67+
const [user, backup, bitgo] = toBIP32Triple(keys);
68+
const derivationPrefixes = extractDerivationPrefixes(keys);
69+
70+
const wasm = derivationPrefixes
71+
? WasmRootWalletKeys.with_derivation_prefixes(
72+
user,
73+
backup,
74+
bitgo,
75+
derivationPrefixes[0],
76+
derivationPrefixes[1],
77+
derivationPrefixes[2],
78+
)
79+
: new WasmRootWalletKeys(user, backup, bitgo);
80+
81+
return new RootWalletKeys(wasm);
82+
}
83+
84+
/**
85+
* Create a RootWalletKeys from three xpub strings
86+
* Uses default derivation prefix of m/0/0 for all three keys
87+
* @param xpubs - Triple of xpub strings
88+
* @returns A RootWalletKeys instance
89+
*/
90+
static fromXpubs(xpubs: Triple<string>): RootWalletKeys {
91+
const [user, backup, bitgo] = xpubs.map((xpub) =>
92+
WasmBIP32.from_xpub(xpub),
93+
) as Triple<WasmBIP32>;
94+
const wasm = new WasmRootWalletKeys(user, backup, bitgo);
95+
return new RootWalletKeys(wasm);
96+
}
97+
98+
/**
99+
* Create a RootWalletKeys from three xpub strings with custom derivation prefixes
100+
* @param xpubs - Triple of xpub strings
101+
* @param derivationPrefixes - Triple of derivation path strings (e.g., ["0/0", "0/0", "0/0"])
102+
* @returns A RootWalletKeys instance
103+
*/
104+
static withDerivationPrefixes(
105+
xpubs: Triple<string>,
106+
derivationPrefixes: Triple<string>,
107+
): RootWalletKeys {
108+
const [user, backup, bitgo] = xpubs.map((xpub) =>
109+
WasmBIP32.from_xpub(xpub),
110+
) as Triple<WasmBIP32>;
111+
const wasm = WasmRootWalletKeys.with_derivation_prefixes(
112+
user,
113+
backup,
114+
bitgo,
115+
derivationPrefixes[0],
116+
derivationPrefixes[1],
117+
derivationPrefixes[2],
118+
);
119+
return new RootWalletKeys(wasm);
120+
}
121+
122+
/**
123+
* Get the user key (first xpub)
124+
* @returns The user key as a BIP32 instance
125+
*/
126+
userKey(): BIP32 {
127+
const wasm = this._wasm.user_key();
128+
return BIP32.fromWasm(wasm);
129+
}
130+
131+
/**
132+
* Get the backup key (second xpub)
133+
* @returns The backup key as a BIP32 instance
134+
*/
135+
backupKey(): BIP32 {
136+
const wasm = this._wasm.backup_key();
137+
return BIP32.fromWasm(wasm);
138+
}
139+
140+
/**
141+
* Get the BitGo key (third xpub)
142+
* @returns The BitGo key as a BIP32 instance
143+
*/
144+
bitgoKey(): BIP32 {
145+
const wasm = this._wasm.bitgo_key();
146+
return BIP32.fromWasm(wasm);
147+
}
148+
149+
/**
150+
* Get the underlying WASM instance (internal use only)
151+
* @internal
152+
*/
153+
get wasm(): WasmRootWalletKeys {
154+
return this._wasm;
155+
}
156+
}

0 commit comments

Comments
 (0)