From e585cfe66399e6584ff0d8ecebb5786c70884eb3 Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Wed, 18 Feb 2026 15:16:43 +0100 Subject: [PATCH] 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 --- packages/wasm-utxo/docs/adding-new-coin.md | 311 +++++++++++++++++++++ 1 file changed, 311 insertions(+) create mode 100644 packages/wasm-utxo/docs/adding-new-coin.md diff --git a/packages/wasm-utxo/docs/adding-new-coin.md b/packages/wasm-utxo/docs/adding-new-coin.md new file mode 100644 index 0000000..ad7bd76 --- /dev/null +++ b/packages/wasm-utxo/docs/adding-new-coin.md @@ -0,0 +1,311 @@ +# Adding a New Coin to wasm-utxo + +This guide covers adding support for a new UTXO coin to the wasm-utxo library. +wasm-utxo handles low-level PSBT construction, transaction signing, and address +encoding/decoding, compiled from Rust to WASM. It uses **foocoin** +(`foo`/`tfoo`) as a worked example. + +## Overview of changes + +```mermaid +graph TD + N[src/networks.rs
Network enum] --> A[src/address/mod.rs
Codec constants] + A --> AN[src/address/networks.rs
Codec wiring + script support] + N --> P[src/fixed_script_wallet/bitgo_psbt/mod.rs
PSBT deserialization + sighash] + AN --> T[test/fixtures/
Address + PSBT fixtures] + P --> T +``` + +## 1. Network enum + +**File:** `src/networks.rs` + +Add two variants to the `Network` enum (mainnet + testnet) and update every +match arm. The Rust compiler will enforce exhaustive matching, so any missed arm +will be a compile error. + +### Enum definition + +```rust +pub enum Network { + // ...existing variants... + Foocoin, + FoocoinTestnet, +} +``` + +### Match arms to update + +There are 5 match-based functions/arrays that need a new arm. Use the existing +Dogecoin entries as a template for a simple coin. + +| Location | What to add | +|----------|-------------| +| `ALL` array | `Network::Foocoin, Network::FoocoinTestnet` | +| `as_str()` | `"Foocoin"`, `"FoocoinTestnet"` | +| `from_name_exact()` | `"Foocoin" => Some(Network::Foocoin)`, etc. | +| `from_coin_name()` | `"foo" => Some(Network::Foocoin)`, `"tfoo" => ...` | +| `to_coin_name()` | `Network::Foocoin => "foo"`, etc. | +| `mainnet()` | `Network::Foocoin => Network::Foocoin`, `Network::FoocoinTestnet => Network::Foocoin` | + +> **Skip `from_utxolib_name()` / `to_utxolib_name()`** — these exist for +> backwards compatibility with existing coins routed through the deprecated +> utxo-lib. New coins must not be added to these functions. + +Also update the test `test_all_networks` assertion count. + +## 2. Address codec constants + +**File:** `src/address/mod.rs` + +Define the Base58Check version bytes for the coin. Find these in the coin's +`chainparams.cpp` under `base58Prefixes[PUBKEY_ADDRESS]` and +`base58Prefixes[SCRIPT_ADDRESS]`. + +```rust +// Foocoin +// https://github.com/example/foocoin/blob/master/src/chainparams.cpp +pub const FOOCOIN: Base58CheckCodec = Base58CheckCodec::new(0x3f, 0x41); +pub const FOOCOIN_TEST: Base58CheckCodec = Base58CheckCodec::new(0x6f, 0xc4); +``` + +If the coin supports SegWit (bech32 addresses), also add: + +```rust +pub const FOOCOIN_BECH32: Bech32Codec = Bech32Codec::new("foo"); +pub const FOOCOIN_TEST_BECH32: Bech32Codec = Bech32Codec::new("tfoo"); +``` + +If the coin uses CashAddr (like Bitcoin Cash), use `CashAddrCodec` instead. + +### Where to find version bytes + +| Coin | Source | +|------|--------| +| Bitcoin | `base58Prefixes[PUBKEY_ADDRESS] = {0}` → 0x00 | +| Dogecoin | `base58Prefixes[PUBKEY_ADDRESS] = {30}` → 0x1e | +| Zcash | Uses 2-byte versions: `{0x1C,0xB8}` → 0x1cb8 | + +## 3. Address codec wiring + +**File:** `src/address/networks.rs` + +Update three functions and one method. + +### get_decode_codecs() + +Returns the codecs to try when decoding an address string. + +```rust +fn get_decode_codecs(network: Network) -> Vec<&'static dyn AddressCodec> { + match network { + // ...existing cases... + Network::Foocoin => vec![&FOOCOIN, &FOOCOIN_BECH32], + Network::FoocoinTestnet => vec![&FOOCOIN_TEST, &FOOCOIN_TEST_BECH32], + } +} +``` + +If the coin does not support SegWit, omit the bech32 codec: +```rust +Network::Foocoin => vec![&FOOCOIN], +``` + +### get_encode_codec() + +Returns the single codec to use when encoding an output script to an address. + +```rust +fn get_encode_codec(network: Network, script: &Script, format: AddressFormat) + -> Result<&'static dyn AddressCodec> +{ + match network { + // ...existing cases... + Network::Foocoin => { + if is_witness { Ok(&FOOCOIN_BECH32) } else { Ok(&FOOCOIN) } + } + Network::FoocoinTestnet => { + if is_witness { Ok(&FOOCOIN_TEST_BECH32) } else { Ok(&FOOCOIN_TEST) } + } + } +} +``` + +### output_script_support() + +Declares which script types the coin supports. + +```rust +impl Network { + pub fn output_script_support(&self) -> OutputScriptSupport { + let segwit = matches!( + self.mainnet(), + Network::Bitcoin | Network::Litecoin | Network::BitcoinGold + | Network::Foocoin // <-- add if coin supports segwit + ); + + let taproot = segwit && matches!( + self.mainnet(), + Network::Bitcoin + // Foocoin intentionally omitted — no taproot + ); + + OutputScriptSupport { segwit, taproot } + } +} +``` + +## 4. PSBT deserialization + +**File:** `src/fixed_script_wallet/bitgo_psbt/mod.rs` + +### BitGoPsbt::deserialize() + +The `BitGoPsbt` enum has three variants: + +| Variant | When to use | +|---------|-------------| +| `BitcoinLike(Psbt, Network)` | Standard Bitcoin transaction format (most coins) | +| `Dash(DashBitGoPsbt, Network)` | Dash special transaction format | +| `Zcash(ZcashBitGoPsbt, Network)` | Zcash overwintered transaction format | + +For most Bitcoin forks, use `BitcoinLike`: + +```rust +pub fn deserialize(psbt_bytes: &[u8], network: Network) -> Result { + match network { + // ...existing cases... + + // Add foocoin to the BitcoinLike arm: + Network::Bitcoin + | Network::BitcoinTestnet3 + // ... + | Network::Foocoin // <-- add + | Network::FoocoinTestnet // <-- add + => Ok(BitGoPsbt::BitcoinLike( + Psbt::deserialize(psbt_bytes)?, + network, + )), + } +} +``` + +If the coin has a non-standard transaction format (like Zcash's overwintered +format or Dash's special transactions), you'll need to create a dedicated PSBT +type. See `zcash_psbt.rs` or `dash_psbt.rs` as examples. + +### BitGoPsbt::new() / new_internal() + +Similarly, add foocoin to the arm that creates empty PSBTs. If the coin is +BitcoinLike, it will be handled by the existing fallthrough. + +### get_default_sighash_type() + +**Location:** Same file, `get_default_sighash_type()` function. + +If foocoin uses `SIGHASH_ALL|FORKID` (like BCH, BTG, BSV), add it to the +`uses_forkid` match: + +```rust +let uses_forkid = matches!( + network.mainnet(), + Network::BitcoinCash | Network::BitcoinGold | Network::BitcoinSV | Network::Ecash + // | Network::Foocoin // <-- only if coin uses FORKID +); +``` + +If foocoin uses standard `SIGHASH_ALL`, no change is needed — it falls through +to the default. + +## 5. Test fixtures + +### Address fixtures + +**Directory:** `test/fixtures/address/` + +Create `foocoin.json` with test vectors: `[scriptType, scriptHex, expectedAddress]`. + +The easiest way to generate these is to use the coin's reference implementation +or a known address from a block explorer. You need vectors for each supported +script type (P2PKH, P2SH, and P2WPKH/P2WSH if segwit-capable). + +```json +[ + ["p2pkh", "76a914...88ac", "F..."], + ["p2sh", "a914...87", "3..."], + ["p2wpkh","0014...", "foo1..."] +] +``` + +Also update `get_codecs_for_fixture()` in `src/address/mod.rs` (test section): +```rust +"foocoin.json" => vec![&FOOCOIN, &FOOCOIN_BECH32], +``` + +### PSBT fixtures + +**Directory:** `test/fixtures/fixed-script/` + +Create PSBT fixtures for each signature state: +- `psbt-lite.foo.unsigned.json` +- `psbt-lite.foo.halfsigned.json` +- `psbt-lite.foo.fullsigned.json` + +#### Generating PSBT fixtures from a fullnode + +1. Generate three BIP32 key triples (user, backup, bitgo) +2. Derive a 2-of-3 multisig address for the coin +3. Fund the address on testnet (faucet or `sendtoaddress`) +4. Construct a PSBT spending from that address +5. Sign progressively (unsigned → halfsigned → fullsigned) +6. Export each state as a JSON fixture + +The fixture format matches the `Fixture` type in `test/fixedScript/fixtureUtil.ts`: +```typescript +{ + walletKeys: [xprv1, xprv2, xprv3], + psbtBase64: "...", + psbtBase64Finalized: "..." | null, + inputs: [...], + psbtInputs: [...], + outputs: [...], + psbtOutputs: [...], + extractedTransaction: "..." | null +} +``` + +## 6. TypeScript bindings + +The TypeScript layer wraps the WASM module. The `NetworkName` type should +automatically include new networks if it's derived from the Rust enum's string +representation. Verify that: + +- `fixedScriptWallet.BitGoPsbt.fromBytes(buf, "foo")` works +- `fixedScriptWallet.address(rootWalletKeys, chainCode, index, network)` works + +If `NetworkName` is a manually maintained union type, add `'foo' | 'tfoo'` to it. + +## 7. Run tests + +```bash +# Rust tests (address encoding, PSBT parsing, signing) +cargo test + +# TypeScript integration tests +npm test +``` + +## 8. Checklist + +- [ ] `src/networks.rs`: `Foocoin` + `FoocoinTestnet` added to enum + all 7 match arms + `ALL` +- [ ] `src/address/mod.rs`: Codec constants defined (Base58Check, optionally Bech32/CashAddr) +- [ ] `src/address/networks.rs`: `get_decode_codecs()` updated +- [ ] `src/address/networks.rs`: `get_encode_codec()` updated +- [ ] `src/address/networks.rs`: `output_script_support()` updated (segwit/taproot flags) +- [ ] `src/fixed_script_wallet/bitgo_psbt/mod.rs`: `deserialize()` case added +- [ ] `src/fixed_script_wallet/bitgo_psbt/mod.rs`: `get_default_sighash_type()` updated (if FORKID) +- [ ] `test/fixtures/address/foocoin.json` created +- [ ] `test/fixtures/fixed-script/psbt-lite.foo.*.json` created +- [ ] TypeScript `NetworkName` includes new network +- [ ] `cargo test` passes +- [ ] `npm test` passes