Skip to content

Commit e585cfe

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 c8dde9b commit e585cfe

1 file changed

Lines changed: 311 additions & 0 deletions

File tree

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

0 commit comments

Comments
 (0)