Skip to content

Commit 8ecd880

Browse files
OttoAllmendingerllm-git
andcommitted
feat(wasm-utxo): implement namespace wrapper pattern for better TypeScript APIs
This PR introduces a cleaner, more type-safe architecture for WASM bindings: 1. Organizes related Rust functions into namespace classes 2. Wraps generated WASM bindings with precise TypeScript types 3. Creates dedicated wrapper modules for each feature area 4. Adds comprehensive documentation for the pattern The pattern replaces loose types (`any`, `string | null`) with precise TypeScript types, improving IDE autocompletion and compile-time safety. Issue: BTC-2652 Co-authored-by: llm-git <[email protected]>
1 parent d157957 commit 8ecd880

File tree

13 files changed

+378
-120
lines changed

13 files changed

+378
-120
lines changed

packages/wasm-utxo/js/README.md

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# Purpose
2+
3+
The primary purpose of this directory is to expose better TypeScript signatures than those
4+
generated by the `wasm-pack` command (which uses `wasm-bindgen`).
5+
6+
While the `wasm-bindgen` crate allows some customization of the emitted type signatures, it
7+
is a bit painful to use and has certain limitations that cannot be easily worked around.
8+
9+
## Architecture Pattern
10+
11+
This directory implements a **namespace wrapper pattern** that provides a cleaner, more
12+
type-safe API over the raw WASM bindings.
13+
14+
### Pattern Overview
15+
16+
1. **WASM Generation** (`wasm/wasm_utxo.d.ts`)
17+
18+
- Generated by `wasm-bindgen` from Rust code
19+
- Exports classes with static methods (e.g., `AddressNamespace`, `UtxolibCompatNamespace`)
20+
- Uses loose types (`any`, `string | null`) due to WASM-bindgen limitations
21+
22+
2. **Namespace Wrapper Files** (e.g., `address.ts`, `utxolibCompat.ts`, `fixedScriptWallet.ts`)
23+
24+
- Import the generated WASM namespace classes
25+
- Define precise TypeScript types to replace `any` types
26+
- Export individual functions that wrap the static WASM methods
27+
- Re-export related types for convenience
28+
29+
3. **Shared Type Files** (e.g., `coinName.ts`, `triple.ts`)
30+
31+
- Define common types used across multiple modules
32+
- Single source of truth to avoid duplication
33+
- Imported by wrapper files as needed
34+
35+
4. **Main Entry Point** (`index.ts`)
36+
- Uses `export * as` to group related functionality into namespaces
37+
- Re-exports shared types for top-level access
38+
- Augments WASM types with additional TypeScript declarations
39+
40+
### Example
41+
42+
Given a WASM-generated class:
43+
44+
```typescript
45+
// wasm/wasm_utxo.d.ts (generated)
46+
export class AddressNamespace {
47+
static to_output_script_with_coin(address: string, coin: string): Uint8Array;
48+
static from_output_script_with_coin(
49+
script: Uint8Array,
50+
coin: string,
51+
format?: string | null,
52+
): string;
53+
}
54+
```
55+
56+
We create a wrapper module:
57+
58+
```typescript
59+
// address.ts
60+
import { AddressNamespace } from "./wasm/wasm_utxo";
61+
import type { CoinName } from "./coinName";
62+
63+
export type AddressFormat = "default" | "cashaddr";
64+
65+
export function toOutputScriptWithCoin(address: string, coin: CoinName): Uint8Array {
66+
return AddressNamespace.to_output_script_with_coin(address, coin);
67+
}
68+
69+
export function fromOutputScriptWithCoin(
70+
script: Uint8Array,
71+
coin: CoinName,
72+
format?: AddressFormat,
73+
): string {
74+
return AddressNamespace.from_output_script_with_coin(script, coin, format);
75+
}
76+
```
77+
78+
And expose it via the main entry point:
79+
80+
```typescript
81+
// index.ts
82+
export * as address from "./address";
83+
```
84+
85+
### Benefits
86+
87+
- **Type Safety**: Replace loose `any` and `string` types with precise union types
88+
- **Better DX**: IDE autocomplete works better with concrete types
89+
- **Maintainability**: Centralized type definitions prevent duplication

packages/wasm-utxo/js/address.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { AddressNamespace } from "./wasm/wasm_utxo";
2+
import type { CoinName } from "./coinName";
3+
4+
export type AddressFormat = "default" | "cashaddr";
5+
6+
export function toOutputScriptWithCoin(address: string, coin: CoinName): Uint8Array {
7+
return AddressNamespace.to_output_script_with_coin(address, coin);
8+
}
9+
10+
export function fromOutputScriptWithCoin(
11+
script: Uint8Array,
12+
coin: CoinName,
13+
format?: AddressFormat,
14+
): string {
15+
return AddressNamespace.from_output_script_with_coin(script, coin, format);
16+
}

packages/wasm-utxo/js/coinName.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// BitGo coin names (from Network::from_coin_name in src/networks.rs)
2+
export type CoinName =
3+
| "btc"
4+
| "tbtc"
5+
| "tbtc4"
6+
| "tbtcsig"
7+
| "tbtcbgsig"
8+
| "bch"
9+
| "tbch"
10+
| "bcha"
11+
| "tbcha"
12+
| "btg"
13+
| "tbtg"
14+
| "bsv"
15+
| "tbsv"
16+
| "dash"
17+
| "tdash"
18+
| "doge"
19+
| "tdoge"
20+
| "ltc"
21+
| "tltc"
22+
| "zec"
23+
| "tzec";
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { FixedScriptWalletNamespace } from "./wasm/wasm_utxo";
2+
import type { UtxolibNetwork, UtxolibRootWalletKeys } from "./utxolibCompat";
3+
import { Triple } from "./triple";
4+
5+
export type WalletKeys =
6+
/** Just an xpub triple, will assume default derivation prefixes */
7+
| Triple<string>
8+
/** Compatible with utxolib RootWalletKeys */
9+
| UtxolibRootWalletKeys;
10+
11+
/**
12+
* Create the output script for a given wallet keys and chain and index
13+
*/
14+
export function outputScript(keys: WalletKeys, chain: number, index: number): Uint8Array {
15+
return FixedScriptWalletNamespace.output_script(keys, chain, index);
16+
}
17+
18+
/**
19+
* Create the address for a given wallet keys and chain and index and network.
20+
* Wrapper for outputScript that also encodes the script to an address.
21+
*/
22+
export function address(
23+
keys: WalletKeys,
24+
chain: number,
25+
index: number,
26+
network: UtxolibNetwork,
27+
): string {
28+
return FixedScriptWalletNamespace.address(keys, chain, index, network);
29+
}

packages/wasm-utxo/js/index.ts

Lines changed: 9 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -4,40 +4,23 @@ import * as wasm from "./wasm/wasm_utxo";
44
// and forgets to include it in the bundle
55
void wasm;
66

7+
export * as address from "./address";
8+
export * as ast from "./ast";
9+
export * as utxolibCompat from "./utxolibCompat";
10+
export * as fixedScriptWallet from "./fixedScriptWallet";
11+
12+
export type { CoinName } from "./coinName";
13+
export type { Triple } from "./triple";
14+
export type { AddressFormat } from "./address";
15+
716
export type DescriptorPkType = "derivable" | "definite" | "string";
817

918
export type ScriptContext = "tap" | "segwitv0" | "legacy";
1019

11-
export type AddressFormat = "default" | "cashaddr";
12-
1320
export type SignPsbtResult = {
1421
[inputIndex: number]: [pubkey: string][];
1522
};
1623

17-
// BitGo coin names (from Network::from_coin_name in src/networks.rs)
18-
export type CoinName =
19-
| "btc"
20-
| "tbtc"
21-
| "tbtc4"
22-
| "tbtcsig"
23-
| "tbtcbgsig"
24-
| "bch"
25-
| "tbch"
26-
| "bcha"
27-
| "tbcha"
28-
| "btg"
29-
| "tbtg"
30-
| "bsv"
31-
| "tbsv"
32-
| "dash"
33-
| "tdash"
34-
| "doge"
35-
| "tdoge"
36-
| "ltc"
37-
| "tltc"
38-
| "zec"
39-
| "tzec";
40-
4124
declare module "./wasm/wasm_utxo" {
4225
interface WrapDescriptor {
4326
/** These are not the same types of nodes as in the ast module */
@@ -63,46 +46,8 @@ declare module "./wasm/wasm_utxo" {
6346
signWithXprv(this: WrapPsbt, xprv: string): SignPsbtResult;
6447
signWithPrv(this: WrapPsbt, prv: Uint8Array): SignPsbtResult;
6548
}
66-
67-
interface Address {
68-
/**
69-
* Convert output script to address string
70-
* @param script - The output script as a byte array
71-
* @param network - The utxolib Network object from JavaScript
72-
* @param format - Optional address format: "default" or "cashaddr" (only applicable for Bitcoin Cash and eCash)
73-
*/
74-
fromOutputScript(script: Uint8Array, network: any, format?: AddressFormat): string;
75-
/**
76-
* Convert address string to output script
77-
* @param address - The address string
78-
* @param network - The utxolib Network object from JavaScript
79-
* @param format - Optional address format (currently unused for decoding as all formats are accepted)
80-
*/
81-
toOutputScript(address: string, network: any, format?: AddressFormat): Uint8Array;
82-
}
8349
}
8450

85-
import { Address as WasmAddress } from "./wasm/wasm_utxo";
86-
8751
export { WrapDescriptor as Descriptor } from "./wasm/wasm_utxo";
8852
export { WrapMiniscript as Miniscript } from "./wasm/wasm_utxo";
8953
export { WrapPsbt as Psbt } from "./wasm/wasm_utxo";
90-
export { FixedScriptWallet } from "./wasm/wasm_utxo";
91-
92-
export namespace utxolibCompat {
93-
export const Address = WasmAddress;
94-
}
95-
96-
export function toOutputScriptWithCoin(address: string, coin: CoinName): Uint8Array {
97-
return wasm.toOutputScriptWithCoin(address, coin);
98-
}
99-
100-
export function fromOutputScriptWithCoin(
101-
script: Uint8Array,
102-
coin: CoinName,
103-
format?: AddressFormat,
104-
): string {
105-
return wasm.fromOutputScriptWithCoin(script, coin, format);
106-
}
107-
108-
export * as ast from "./ast";

packages/wasm-utxo/js/triple.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export type Triple<T> = [T, T, T];
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import type { AddressFormat } from "./address";
2+
import { Triple } from "./triple";
3+
import { UtxolibCompatNamespace } from "./wasm/wasm_utxo";
4+
5+
export type BIP32Interface = {
6+
network: {
7+
bip32: {
8+
public: number;
9+
};
10+
};
11+
depth: number;
12+
parentFingerprint: number;
13+
index: number;
14+
chainCode: Uint8Array;
15+
publicKey: Uint8Array;
16+
17+
toBase58?(): string;
18+
};
19+
20+
export type UtxolibRootWalletKeys = {
21+
triple: Triple<BIP32Interface>;
22+
derivationPrefixes: Triple<string>;
23+
};
24+
25+
export type UtxolibNetwork = {
26+
pubKeyHash: number;
27+
scriptHash: number;
28+
cashAddr?: {
29+
prefix: string;
30+
pubKeyHash: number;
31+
scriptHash: number;
32+
};
33+
bech32?: string;
34+
};
35+
36+
export function fromOutputScript(
37+
script: Uint8Array,
38+
network: UtxolibNetwork,
39+
format?: AddressFormat,
40+
): string {
41+
return UtxolibCompatNamespace.from_output_script(script, network, format);
42+
}
43+
44+
export function toOutputScript(
45+
address: string,
46+
network: UtxolibNetwork,
47+
format?: AddressFormat,
48+
): Uint8Array {
49+
return UtxolibCompatNamespace.to_output_script(address, network, format);
50+
}

packages/wasm-utxo/src/address/networks.rs

Lines changed: 28 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -211,36 +211,35 @@ pub fn from_output_script_with_coin_and_format(
211211
// WASM bindings
212212
use wasm_bindgen::prelude::*;
213213

214-
/// WASM binding: Convert an address string to an output script using a BitGo coin name.
215-
#[wasm_bindgen(js_name = toOutputScriptWithCoin)]
216-
pub fn to_output_script_with_coin_js(
217-
address: &str,
218-
coin: &str,
219-
) -> std::result::Result<Vec<u8>, JsValue> {
220-
to_output_script_with_coin(address, coin)
221-
.map(|script| script.to_bytes())
222-
.map_err(|e| JsValue::from_str(&e.to_string()))
223-
}
214+
#[wasm_bindgen]
215+
pub struct AddressNamespace;
216+
217+
#[wasm_bindgen]
218+
impl AddressNamespace {
219+
#[wasm_bindgen]
220+
pub fn to_output_script_with_coin(
221+
address: &str,
222+
coin: &str,
223+
) -> std::result::Result<Vec<u8>, JsValue> {
224+
to_output_script_with_coin(address, coin)
225+
.map(|script| script.to_bytes())
226+
.map_err(|e| JsValue::from_str(&e.to_string()))
227+
}
224228

225-
/// WASM binding: Convert an output script to an address string using a BitGo coin name.
226-
///
227-
/// # Arguments
228-
/// * `script` - The output script bytes
229-
/// * `coin` - The BitGo coin name (e.g., "btc", "bch", "ecash")
230-
/// * `format` - Optional address format: "default" or "cashaddr" (only applicable for Bitcoin Cash and eCash)
231-
#[wasm_bindgen(js_name = fromOutputScriptWithCoin)]
232-
pub fn from_output_script_with_coin_js(
233-
script: &[u8],
234-
coin: &str,
235-
format: Option<String>,
236-
) -> std::result::Result<String, JsValue> {
237-
let script_obj = Script::from_bytes(script);
238-
let format_str = format.as_deref();
239-
let address_format = AddressFormat::from_optional_str(format_str)
240-
.map_err(|e| JsValue::from_str(&e.to_string()))?;
241-
242-
from_output_script_with_coin_and_format(script_obj, coin, address_format)
243-
.map_err(|e| JsValue::from_str(&e.to_string()))
229+
#[wasm_bindgen]
230+
pub fn from_output_script_with_coin(
231+
script: &[u8],
232+
coin: &str,
233+
format: Option<String>,
234+
) -> std::result::Result<String, JsValue> {
235+
let script_obj = Script::from_bytes(script);
236+
let format_str = format.as_deref();
237+
let address_format = AddressFormat::from_optional_str(format_str)
238+
.map_err(|e| JsValue::from_str(&e.to_string()))?;
239+
240+
from_output_script_with_coin_and_format(script_obj, coin, address_format)
241+
.map_err(|e| JsValue::from_str(&e.to_string()))
242+
}
244243
}
245244

246245
#[cfg(test)]

0 commit comments

Comments
 (0)