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