Skip to content

Commit 1514d0e

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 1f2b9ba commit 1514d0e

1 file changed

Lines changed: 337 additions & 0 deletions

File tree

Lines changed: 337 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,337 @@
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+
126+
```rust
127+
Network::Foocoin => vec![&FOOCOIN],
128+
```
129+
130+
### get_encode_codec()
131+
132+
Returns the single codec to use when encoding an output script to an address.
133+
134+
```rust
135+
fn get_encode_codec(network: Network, script: &Script, format: AddressFormat)
136+
-> Result<&'static dyn AddressCodec>
137+
{
138+
match network {
139+
// ...existing cases...
140+
Network::Foocoin => {
141+
if is_witness { Ok(&FOOCOIN_BECH32) } else { Ok(&FOOCOIN) }
142+
}
143+
Network::FoocoinTestnet => {
144+
if is_witness { Ok(&FOOCOIN_TEST_BECH32) } else { Ok(&FOOCOIN_TEST) }
145+
}
146+
}
147+
}
148+
```
149+
150+
### output_script_support()
151+
152+
Declares which script types the coin supports.
153+
154+
```rust
155+
impl Network {
156+
pub fn output_script_support(&self) -> OutputScriptSupport {
157+
let segwit = matches!(
158+
self.mainnet(),
159+
Network::Bitcoin | Network::Litecoin | Network::BitcoinGold
160+
| Network::Foocoin // <-- add if coin supports segwit
161+
);
162+
163+
let taproot = segwit && matches!(
164+
self.mainnet(),
165+
Network::Bitcoin
166+
// Foocoin intentionally omitted — no taproot
167+
);
168+
169+
OutputScriptSupport { segwit, taproot }
170+
}
171+
}
172+
```
173+
174+
## 5. PSBT deserialization
175+
176+
**File:** `src/fixed_script_wallet/bitgo_psbt/mod.rs`
177+
178+
### BitGoPsbt::deserialize()
179+
180+
The `BitGoPsbt` enum has three variants:
181+
182+
| Variant | When to use |
183+
| -------------------------------- | ------------------------------------------------ |
184+
| `BitcoinLike(Psbt, Network)` | Standard Bitcoin transaction format (most coins) |
185+
| `Dash(DashBitGoPsbt, Network)` | Dash special transaction format |
186+
| `Zcash(ZcashBitGoPsbt, Network)` | Zcash overwintered transaction format |
187+
188+
For most Bitcoin forks, use `BitcoinLike`:
189+
190+
```rust
191+
pub fn deserialize(psbt_bytes: &[u8], network: Network) -> Result<BitGoPsbt, DeserializeError> {
192+
match network {
193+
// ...existing cases...
194+
195+
// Add foocoin to the BitcoinLike arm:
196+
Network::Bitcoin
197+
| Network::BitcoinTestnet3
198+
// ...
199+
| Network::Foocoin // <-- add
200+
| Network::FoocoinTestnet // <-- add
201+
=> Ok(BitGoPsbt::BitcoinLike(
202+
Psbt::deserialize(psbt_bytes)?,
203+
network,
204+
)),
205+
}
206+
}
207+
```
208+
209+
If the coin has a non-standard transaction format (like Zcash's overwintered
210+
format or Dash's special transactions), you'll need to create a dedicated PSBT
211+
type. See `zcash_psbt.rs` or `dash_psbt.rs` as examples.
212+
213+
### BitGoPsbt::new() / new_internal()
214+
215+
Similarly, add foocoin to the arm that creates empty PSBTs. If the coin is
216+
BitcoinLike, it will be handled by the existing fallthrough.
217+
218+
### get_default_sighash_type()
219+
220+
**Location:** Same file, `get_default_sighash_type()` function.
221+
222+
If foocoin uses `SIGHASH_ALL|FORKID` (like BCH, BTG, BSV), add it to the
223+
`uses_forkid` match:
224+
225+
```rust
226+
let uses_forkid = matches!(
227+
network.mainnet(),
228+
Network::BitcoinCash | Network::BitcoinGold | Network::BitcoinSV | Network::Ecash
229+
// | Network::Foocoin // <-- only if coin uses FORKID
230+
);
231+
```
232+
233+
If foocoin uses standard `SIGHASH_ALL`, no change is needed — it falls through
234+
to the default.
235+
236+
## 6. Test fixtures
237+
238+
### Address fixtures
239+
240+
**Directory:** `test/fixtures/address/`
241+
242+
Create `foocoin.json` with test vectors: `[scriptType, scriptHex, expectedAddress]`.
243+
244+
The easiest way to generate these is to use the coin's reference implementation
245+
or a known address from a block explorer. You need vectors for each supported
246+
script type (P2PKH, P2SH, and P2WPKH/P2WSH if segwit-capable).
247+
248+
```json
249+
[
250+
["p2pkh", "76a914...88ac", "F..."],
251+
["p2sh", "a914...87", "3..."],
252+
["p2wpkh", "0014...", "foo1..."]
253+
]
254+
```
255+
256+
Also update `get_codecs_for_fixture()` in `src/address/mod.rs` (test section):
257+
258+
```rust
259+
"foocoin.json" => vec![&FOOCOIN, &FOOCOIN_BECH32],
260+
```
261+
262+
### PSBT fixtures
263+
264+
**Directory:** `test/fixtures/fixed-script/`
265+
266+
PSBT fixtures are **auto-generated** when the JSON files don't exist on disk.
267+
The generator lives in `test/fixedScript/generateFixture.ts` and creates PSBTs
268+
with one input per supported script type plus a replay protection input, then
269+
signs progressively to produce all three signature states.
270+
271+
Fixtures are generated for two transaction formats (`psbt` and `psbt-lite`),
272+
giving six files per coin:
273+
274+
- `psbt.foo.unsigned.json` / `psbt-lite.foo.unsigned.json`
275+
- `psbt.foo.halfsigned.json` / `psbt-lite.foo.halfsigned.json`
276+
- `psbt.foo.fullsigned.json` / `psbt-lite.foo.fullsigned.json`
277+
278+
The `psbt` format includes `non_witness_utxo` on every input; `psbt-lite`
279+
omits it. Zcash skips the `psbt` format because it does not support
280+
`non_witness_utxo`.
281+
282+
**To generate fixtures for a new coin:**
283+
284+
No manual registration step is needed — `mainnetCoinNames` in
285+
`test/fixedScript/networkSupport.util.ts` is derived automatically from
286+
`coinNames` in `js/coinName.ts` (step 2). On the first test run,
287+
`loadPsbtFixture()` detects missing fixture files, generates them, writes
288+
them to disk, and then throws an error prompting you to commit the new files.
289+
Re-run the tests after committing.
290+
291+
The generator selects script types based on `output_script_support()`:
292+
293+
| Network capability | Chains included |
294+
| ------------------ | --------------------------------------------------------- |
295+
| Legacy only | 0 (p2sh) |
296+
| Segwit | 0, 10 (p2shP2wsh), 20 (p2wsh) |
297+
| Taproot | + 30 (p2trLegacy), 40 (p2trMusig2 script path + key path) |
298+
299+
If the generated fixtures need updating (e.g. after changing signing logic),
300+
delete the JSON files and re-run the tests to regenerate them.
301+
302+
## 7. TypeScript bindings
303+
304+
The TypeScript layer wraps the WASM module. The `NetworkName` type should
305+
automatically include new networks if it's derived from the Rust enum's string
306+
representation. Verify that:
307+
308+
- `fixedScriptWallet.BitGoPsbt.fromBytes(buf, "foo")` works
309+
- `fixedScriptWallet.address(rootWalletKeys, chainCode, index, network)` works
310+
311+
If `NetworkName` is a manually maintained union type, add `'foo' | 'tfoo'` to it.
312+
313+
## 8. Run tests
314+
315+
```bash
316+
# Rust tests (address encoding, PSBT parsing, signing)
317+
cargo test
318+
319+
# TypeScript integration tests
320+
npm test
321+
```
322+
323+
## 9. Checklist
324+
325+
- [ ] `src/networks.rs`: `Foocoin` + `FoocoinTestnet` added to enum + all 7 match arms + `ALL`
326+
- [ ] `js/coinName.ts`: `"foo"` + `"tfoo"` added to `coinNames`, `getMainnet()` updated
327+
- [ ] `src/address/mod.rs`: Codec constants defined (Base58Check, optionally Bech32/CashAddr)
328+
- [ ] `src/address/networks.rs`: `get_decode_codecs()` updated
329+
- [ ] `src/address/networks.rs`: `get_encode_codec()` updated
330+
- [ ] `src/address/networks.rs`: `output_script_support()` updated (segwit/taproot flags)
331+
- [ ] `src/fixed_script_wallet/bitgo_psbt/mod.rs`: `deserialize()` case added
332+
- [ ] `src/fixed_script_wallet/bitgo_psbt/mod.rs`: `get_default_sighash_type()` updated (if FORKID)
333+
- [ ] `test/fixtures/address/foocoin.json` created
334+
- [ ] `test/fixtures/fixed-script/psbt.foo.*.json` + `psbt-lite.foo.*.json` auto-generated by `npm test`
335+
- [ ] TypeScript `NetworkName` includes new network
336+
- [ ] `cargo test` passes
337+
- [ ] `npm test` passes

0 commit comments

Comments
 (0)