Skip to content

Commit 4b1832a

Browse files
Merge pull request #67 from BitGo/BTC-2786.walletKeys
feat(wasm-utxo): implement key management and signature verification
2 parents 9e5ab15 + 9aae5ff commit 4b1832a

30 files changed

+2546
-688
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

0 commit comments

Comments
 (0)