Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
159 changes: 139 additions & 20 deletions packages/wasm-utxo/js/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,42 +6,61 @@ generated by the `wasm-pack` command (which uses `wasm-bindgen`).
While the `wasm-bindgen` crate allows some customization of the emitted type signatures, it
is a bit painful to use and has certain limitations that cannot be easily worked around.

## Architecture Pattern
## Architecture Patterns

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

### Pattern Overview
1. **Namespace Wrapper Pattern** - For static utility functions
2. **Class Wrapper Pattern** - For stateful objects with methods

### Common Elements

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

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

2. **Namespace Wrapper Files** (e.g., `address.ts`, `utxolibCompat.ts`, `fixedScriptWallet.ts`)

- Import the generated WASM namespace classes
- Define precise TypeScript types to replace `any` types
- Export individual functions that wrap the static WASM methods
- Convert `snake_case` WASM methods to `camelCase` (JavaScript convention)
- Re-export related types for convenience

3. **Shared Type Files** (e.g., `coinName.ts`, `triple.ts`)
2. **Shared Type Files** (e.g., `coinName.ts`, `triple.ts`)

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

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

### Example
### Pattern 1: Namespace Wrapper Pattern

Used for static utility functions (e.g., `address.ts`, `utxolibCompat.ts`).

**Characteristics:**

- Import the generated WASM namespace classes
- Define precise TypeScript types to replace `any` types
- Export individual functions that wrap the static WASM methods
- Convert `snake_case` WASM methods to `camelCase` (JavaScript convention)
- Re-export related types for convenience

### Pattern 2: Class Wrapper Pattern

Used for stateful objects that maintain WASM instances (e.g., `BIP32`, `RootWalletKeys`, `BitGoPsbt`).

**Characteristics:**

Given a WASM-generated class:
- Private `_wasm` property holds the underlying WASM instance
- Private constructor prevents direct instantiation
- Static factory methods (camelCase) for object creation
- Instance methods (camelCase) wrap WASM methods and return wrapped instances when appropriate
- Public `wasm` getter for internal access to WASM instance (marked `@internal`)
- Implements interfaces to ensure compatibility with existing code

### Example 1: Namespace Wrapper Pattern

Given a WASM-generated namespace class:

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

### Example 2: Class Wrapper Pattern

Given a WASM-generated class with instance methods:

```typescript
// wasm/wasm_utxo.d.ts (generated by wasm-bindgen)
export class WasmBIP32 {
private constructor();
// Note: snake_case naming from Rust (no js_name overrides)
static from_base58(base58_str: string): WasmBIP32;
derive(index: number): WasmBIP32;
derive_path(path: string): WasmBIP32;
to_base58(): string;
readonly public_key: Uint8Array;
}
```

We create a wrapper class that encapsulates the WASM instance:

```typescript
// bip32.ts
import { WasmBIP32 } from "./wasm/wasm_utxo";

export class BIP32 {
// Private property with underscore prefix
private constructor(private _wasm: WasmBIP32) {}

// Static factory method (camelCase) calls snake_case WASM method
static fromBase58(base58Str: string): BIP32 {
const wasm = WasmBIP32.from_base58(base58Str);
return new BIP32(wasm);
}

// Property getter (camelCase) accesses snake_case WASM property
get publicKey(): Uint8Array {
return this._wasm.public_key;
}

// Instance method (camelCase) returns wrapped instance
derive(index: number): BIP32 {
const wasm = this._wasm.derive(index);
return new BIP32(wasm);
}

// Convert snake_case to camelCase
derivePath(path: string): BIP32 {
const wasm = this._wasm.derive_path(path);
return new BIP32(wasm);
}

// Convert snake_case to camelCase
toBase58(): string {
return this._wasm.to_base58();
}

// Public getter for internal use (marked @internal)
/**
* @internal
*/
get wasm(): WasmBIP32 {
return this._wasm;
}
}
```

And expose it directly:

```typescript
// index.ts
export { BIP32 } from "./bip32";
```

### Benefits

**Common to Both Patterns:**

- **Type Safety**: Replace loose `any` and `string` types with precise union types
- **Idiomatic Naming**: Each layer uses its native convention (`snake_case` in Rust, `camelCase` in JavaScript)
- **Idiomatic Naming**: Each layer uses its native convention (`snake_case` in Rust/WASM, `camelCase` in TypeScript/JavaScript)
- Rust exports use `snake_case` (no `js_name` overrides)
- TypeScript wrappers provide `camelCase` API
- **Better DX**: IDE autocomplete works better with concrete types and familiar naming
- **Maintainability**: Centralized type definitions prevent duplication
- **Clear Separation**: WASM bindings stay pure to Rust conventions, TypeScript handles JS conventions

**Class Wrapper Pattern Specific:**

- **Encapsulation**: Private `_wasm` property hides implementation details
- **Controlled Access**: Private constructor forces use of factory methods
- **Consistent Returns**: Methods that return new instances automatically wrap them
- **Internal Access**: Public `wasm` getter allows internal code to access WASM instance when needed
- **Type Compatibility**: Can implement interfaces to maintain backward compatibility

### When to Use Which Pattern

**Use Namespace Wrapper Pattern when:**

- Functions are stateless utilities
- No need to maintain WASM instance state
- Simple input → output transformations
- Examples: address encoding/decoding, network conversions

**Use Class Wrapper Pattern when:**

- Object represents stateful data (keys, PSBTs, etc.)
- Methods need to return new instances of the same type
- Need to encapsulate underlying WASM instance
- Examples: BIP32 keys, RootWalletKeys, BitGoPsbt
Loading