Skip to content

Commit 9ddab67

Browse files
OttoAllmendingerllm-git
andcommitted
docs(wasm-utxo): add comprehensive new coin integration guide
Add detailed guide for integrating new UTXO coins into wasm-utxo. Include step-by-step instructions for network enum updates, address codec configuration, PSBT handling, and fixture generation. Document match arm requirements, version byte sources, script type support flags, and sighash configuration. Provide worked example using foocoin throughout with code snippets, architecture diagram, and complete testing checklist. BTC-3047 Co-authored-by: llm-git <llm-git@ttll.de>
1 parent 9c8b1bf commit 9ddab67

1 file changed

Lines changed: 335 additions & 0 deletions

File tree

Lines changed: 335 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,335 @@
1+
# Adding a New Coin to wasm-utxo
2+
3+
This guide covers adding support for a new UTXO coin to the wasm-utxo library.
4+
wasm-utxo handles low-level PSBT construction, transaction signing, and address
5+
encoding/decoding, compiled from Rust to WASM. It uses **foocoin**
6+
(`foo`/`tfoo`) as a worked example.
7+
8+
## Overview of changes
9+
10+
```mermaid
11+
graph TD
12+
N[src/networks.rs<br/>Network enum] --> A[src/address/mod.rs<br/>Codec constants]
13+
N --> C[js/coinName.ts<br/>CoinName type + helpers]
14+
A --> AN[src/address/networks.rs<br/>Codec wiring + script support]
15+
N --> P[src/fixed_script_wallet/bitgo_psbt/mod.rs<br/>PSBT deserialization + sighash]
16+
AN --> T[test/fixtures/<br/>Address + PSBT fixtures]
17+
C --> T
18+
P --> T
19+
```
20+
21+
## 1. Network enum
22+
23+
**File:** `src/networks.rs`
24+
25+
Add two variants to the `Network` enum (mainnet + testnet) and update every
26+
match arm. The Rust compiler will enforce exhaustive matching, so any missed arm
27+
will be a compile error.
28+
29+
### Enum definition
30+
31+
```rust
32+
pub enum Network {
33+
// ...existing variants...
34+
Foocoin,
35+
FoocoinTestnet,
36+
}
37+
```
38+
39+
### Match arms to update
40+
41+
There are 5 match-based functions/arrays that need a new arm. Use the existing
42+
Dogecoin entries as a template for a simple coin.
43+
44+
| Location | What to add |
45+
|----------|-------------|
46+
| `ALL` array | `Network::Foocoin, Network::FoocoinTestnet` |
47+
| `as_str()` | `"Foocoin"`, `"FoocoinTestnet"` |
48+
| `from_name_exact()` | `"Foocoin" => Some(Network::Foocoin)`, etc. |
49+
| `from_coin_name()` | `"foo" => Some(Network::Foocoin)`, `"tfoo" => ...` |
50+
| `to_coin_name()` | `Network::Foocoin => "foo"`, etc. |
51+
| `mainnet()` | `Network::Foocoin => Network::Foocoin`, `Network::FoocoinTestnet => Network::Foocoin` |
52+
53+
> **Skip `from_utxolib_name()` / `to_utxolib_name()`** — these exist for
54+
> backwards compatibility with existing coins routed through the deprecated
55+
> utxo-lib. New coins must not be added to these functions.
56+
57+
Also update the test `test_all_networks` assertion count.
58+
59+
## 2. TypeScript coin name
60+
61+
**File:** `js/coinName.ts`
62+
63+
Register the new coin's short names so that the TypeScript layer can reference
64+
them. The `CoinName` type is derived automatically from the `coinNames` tuple.
65+
66+
1. Add `"foo"` and `"tfoo"` to the `coinNames` array.
67+
2. Add a `case "tfoo": return "foo"` arm to `getMainnet()`.
68+
69+
No changes are needed to `isMainnet()` / `isTestnet()` — they delegate to
70+
`getMainnet()`.
71+
72+
## 3. Address codec constants
73+
74+
**File:** `src/address/mod.rs`
75+
76+
Define the Base58Check version bytes for the coin. Find these in the coin's
77+
`chainparams.cpp` under `base58Prefixes[PUBKEY_ADDRESS]` and
78+
`base58Prefixes[SCRIPT_ADDRESS]`.
79+
80+
```rust
81+
// Foocoin
82+
// https://github.com/example/foocoin/blob/master/src/chainparams.cpp
83+
pub const FOOCOIN: Base58CheckCodec = Base58CheckCodec::new(0x3f, 0x41);
84+
pub const FOOCOIN_TEST: Base58CheckCodec = Base58CheckCodec::new(0x6f, 0xc4);
85+
```
86+
87+
If the coin supports SegWit (bech32 addresses), also add:
88+
89+
```rust
90+
pub const FOOCOIN_BECH32: Bech32Codec = Bech32Codec::new("foo");
91+
pub const FOOCOIN_TEST_BECH32: Bech32Codec = Bech32Codec::new("tfoo");
92+
```
93+
94+
If the coin uses CashAddr (like Bitcoin Cash), use `CashAddrCodec` instead.
95+
96+
### Where to find version bytes
97+
98+
| Coin | Source |
99+
|------|--------|
100+
| Bitcoin | `base58Prefixes[PUBKEY_ADDRESS] = {0}` → 0x00 |
101+
| Dogecoin | `base58Prefixes[PUBKEY_ADDRESS] = {30}` → 0x1e |
102+
| Zcash | Uses 2-byte versions: `{0x1C,0xB8}` → 0x1cb8 |
103+
104+
## 4. Address codec wiring
105+
106+
**File:** `src/address/networks.rs`
107+
108+
Update three functions and one method.
109+
110+
### get_decode_codecs()
111+
112+
Returns the codecs to try when decoding an address string.
113+
114+
```rust
115+
fn get_decode_codecs(network: Network) -> Vec<&'static dyn AddressCodec> {
116+
match network {
117+
// ...existing cases...
118+
Network::Foocoin => vec![&FOOCOIN, &FOOCOIN_BECH32],
119+
Network::FoocoinTestnet => vec![&FOOCOIN_TEST, &FOOCOIN_TEST_BECH32],
120+
}
121+
}
122+
```
123+
124+
If the coin does not support SegWit, omit the bech32 codec:
125+
```rust
126+
Network::Foocoin => vec![&FOOCOIN],
127+
```
128+
129+
### get_encode_codec()
130+
131+
Returns the single codec to use when encoding an output script to an address.
132+
133+
```rust
134+
fn get_encode_codec(network: Network, script: &Script, format: AddressFormat)
135+
-> Result<&'static dyn AddressCodec>
136+
{
137+
match network {
138+
// ...existing cases...
139+
Network::Foocoin => {
140+
if is_witness { Ok(&FOOCOIN_BECH32) } else { Ok(&FOOCOIN) }
141+
}
142+
Network::FoocoinTestnet => {
143+
if is_witness { Ok(&FOOCOIN_TEST_BECH32) } else { Ok(&FOOCOIN_TEST) }
144+
}
145+
}
146+
}
147+
```
148+
149+
### output_script_support()
150+
151+
Declares which script types the coin supports.
152+
153+
```rust
154+
impl Network {
155+
pub fn output_script_support(&self) -> OutputScriptSupport {
156+
let segwit = matches!(
157+
self.mainnet(),
158+
Network::Bitcoin | Network::Litecoin | Network::BitcoinGold
159+
| Network::Foocoin // <-- add if coin supports segwit
160+
);
161+
162+
let taproot = segwit && matches!(
163+
self.mainnet(),
164+
Network::Bitcoin
165+
// Foocoin intentionally omitted — no taproot
166+
);
167+
168+
OutputScriptSupport { segwit, taproot }
169+
}
170+
}
171+
```
172+
173+
## 5. PSBT deserialization
174+
175+
**File:** `src/fixed_script_wallet/bitgo_psbt/mod.rs`
176+
177+
### BitGoPsbt::deserialize()
178+
179+
The `BitGoPsbt` enum has three variants:
180+
181+
| Variant | When to use |
182+
|---------|-------------|
183+
| `BitcoinLike(Psbt, Network)` | Standard Bitcoin transaction format (most coins) |
184+
| `Dash(DashBitGoPsbt, Network)` | Dash special transaction format |
185+
| `Zcash(ZcashBitGoPsbt, Network)` | Zcash overwintered transaction format |
186+
187+
For most Bitcoin forks, use `BitcoinLike`:
188+
189+
```rust
190+
pub fn deserialize(psbt_bytes: &[u8], network: Network) -> Result<BitGoPsbt, DeserializeError> {
191+
match network {
192+
// ...existing cases...
193+
194+
// Add foocoin to the BitcoinLike arm:
195+
Network::Bitcoin
196+
| Network::BitcoinTestnet3
197+
// ...
198+
| Network::Foocoin // <-- add
199+
| Network::FoocoinTestnet // <-- add
200+
=> Ok(BitGoPsbt::BitcoinLike(
201+
Psbt::deserialize(psbt_bytes)?,
202+
network,
203+
)),
204+
}
205+
}
206+
```
207+
208+
If the coin has a non-standard transaction format (like Zcash's overwintered
209+
format or Dash's special transactions), you'll need to create a dedicated PSBT
210+
type. See `zcash_psbt.rs` or `dash_psbt.rs` as examples.
211+
212+
### BitGoPsbt::new() / new_internal()
213+
214+
Similarly, add foocoin to the arm that creates empty PSBTs. If the coin is
215+
BitcoinLike, it will be handled by the existing fallthrough.
216+
217+
### get_default_sighash_type()
218+
219+
**Location:** Same file, `get_default_sighash_type()` function.
220+
221+
If foocoin uses `SIGHASH_ALL|FORKID` (like BCH, BTG, BSV), add it to the
222+
`uses_forkid` match:
223+
224+
```rust
225+
let uses_forkid = matches!(
226+
network.mainnet(),
227+
Network::BitcoinCash | Network::BitcoinGold | Network::BitcoinSV | Network::Ecash
228+
// | Network::Foocoin // <-- only if coin uses FORKID
229+
);
230+
```
231+
232+
If foocoin uses standard `SIGHASH_ALL`, no change is needed — it falls through
233+
to the default.
234+
235+
## 6. Test fixtures
236+
237+
### Address fixtures
238+
239+
**Directory:** `test/fixtures/address/`
240+
241+
Create `foocoin.json` with test vectors: `[scriptType, scriptHex, expectedAddress]`.
242+
243+
The easiest way to generate these is to use the coin's reference implementation
244+
or a known address from a block explorer. You need vectors for each supported
245+
script type (P2PKH, P2SH, and P2WPKH/P2WSH if segwit-capable).
246+
247+
```json
248+
[
249+
["p2pkh", "76a914...88ac", "F..."],
250+
["p2sh", "a914...87", "3..."],
251+
["p2wpkh","0014...", "foo1..."]
252+
]
253+
```
254+
255+
Also update `get_codecs_for_fixture()` in `src/address/mod.rs` (test section):
256+
```rust
257+
"foocoin.json" => vec![&FOOCOIN, &FOOCOIN_BECH32],
258+
```
259+
260+
### PSBT fixtures
261+
262+
**Directory:** `test/fixtures/fixed-script/`
263+
264+
PSBT fixtures are **auto-generated** when the JSON files don't exist on disk.
265+
The generator lives in `test/fixedScript/generateFixture.ts` and creates PSBTs
266+
with one input per supported script type plus a replay protection input, then
267+
signs progressively to produce all three signature states.
268+
269+
Fixtures are generated for two transaction formats (`psbt` and `psbt-lite`),
270+
giving six files per coin:
271+
272+
- `psbt.foo.unsigned.json` / `psbt-lite.foo.unsigned.json`
273+
- `psbt.foo.halfsigned.json` / `psbt-lite.foo.halfsigned.json`
274+
- `psbt.foo.fullsigned.json` / `psbt-lite.foo.fullsigned.json`
275+
276+
The `psbt` format includes `non_witness_utxo` on every input; `psbt-lite`
277+
omits it. Zcash skips the `psbt` format because it does not support
278+
`non_witness_utxo`.
279+
280+
**To generate fixtures for a new coin:**
281+
282+
No manual registration step is needed — `mainnetCoinNames` in
283+
`test/fixedScript/networkSupport.util.ts` is derived automatically from
284+
`coinNames` in `js/coinName.ts` (step 2). On the first test run,
285+
`loadPsbtFixture()` detects missing fixture files, generates them, writes
286+
them to disk, and then throws an error prompting you to commit the new files.
287+
Re-run the tests after committing.
288+
289+
The generator selects script types based on `output_script_support()`:
290+
291+
| Network capability | Chains included |
292+
|--------------------|----------------|
293+
| Legacy only | 0 (p2sh) |
294+
| Segwit | 0, 10 (p2shP2wsh), 20 (p2wsh) |
295+
| Taproot | + 30 (p2trLegacy), 40 (p2trMusig2 script path + key path) |
296+
297+
If the generated fixtures need updating (e.g. after changing signing logic),
298+
delete the JSON files and re-run the tests to regenerate them.
299+
300+
## 7. TypeScript bindings
301+
302+
The TypeScript layer wraps the WASM module. The `NetworkName` type should
303+
automatically include new networks if it's derived from the Rust enum's string
304+
representation. Verify that:
305+
306+
- `fixedScriptWallet.BitGoPsbt.fromBytes(buf, "foo")` works
307+
- `fixedScriptWallet.address(rootWalletKeys, chainCode, index, network)` works
308+
309+
If `NetworkName` is a manually maintained union type, add `'foo' | 'tfoo'` to it.
310+
311+
## 8. Run tests
312+
313+
```bash
314+
# Rust tests (address encoding, PSBT parsing, signing)
315+
cargo test
316+
317+
# TypeScript integration tests
318+
npm test
319+
```
320+
321+
## 9. Checklist
322+
323+
- [ ] `src/networks.rs`: `Foocoin` + `FoocoinTestnet` added to enum + all 7 match arms + `ALL`
324+
- [ ] `js/coinName.ts`: `"foo"` + `"tfoo"` added to `coinNames`, `getMainnet()` updated
325+
- [ ] `src/address/mod.rs`: Codec constants defined (Base58Check, optionally Bech32/CashAddr)
326+
- [ ] `src/address/networks.rs`: `get_decode_codecs()` updated
327+
- [ ] `src/address/networks.rs`: `get_encode_codec()` updated
328+
- [ ] `src/address/networks.rs`: `output_script_support()` updated (segwit/taproot flags)
329+
- [ ] `src/fixed_script_wallet/bitgo_psbt/mod.rs`: `deserialize()` case added
330+
- [ ] `src/fixed_script_wallet/bitgo_psbt/mod.rs`: `get_default_sighash_type()` updated (if FORKID)
331+
- [ ] `test/fixtures/address/foocoin.json` created
332+
- [ ] `test/fixtures/fixed-script/psbt.foo.*.json` + `psbt-lite.foo.*.json` auto-generated by `npm test`
333+
- [ ] TypeScript `NetworkName` includes new network
334+
- [ ] `cargo test` passes
335+
- [ ] `npm test` passes

0 commit comments

Comments
 (0)