Skip to content

Commit 01cd75a

Browse files
authored
Merge pull request #168 from BitGo/BTC-3047.add-onboarding-docs
docs(wasm-utxo): add comprehensive new coin integration guide
2 parents b0b9933 + 1514d0e commit 01cd75a

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)