Skip to content

saniddhyaDubey/PSBTForge

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

⚒️ Coin Smith

A safe, BIP-174 compliant PSBT transaction builder

Greedy coin selection · 6 script types · Full-stack React visualizer

TypeScript Bitcoin React Node.js Tailwind CSS

Live Demo · PSBT Spec (BIP-174)


What is Coin Smith?

Coin Smith takes a UTXO set, a set of payment outputs, a change template, and a fee rate — and produces a fully valid, unsigned PSBT ready for signing. It handles coin selection, fee estimation, dust filtering, change construction, RBF signaling, and locktime semantics in a single deterministic pipeline. Everything is exposed through a structured JSON report and an interactive web UI.

The name fits: it forges raw UTXOs into a safe, spendable transaction.


Numbers that matter

Metric Value
Script types supported 6 (p2wpkh, p2wsh, p2pkh, p2sh, p2tr, p2sh-p2wpkh)
Public fixtures 26+
Coin selection strategy Greedy, largest-first
Dust threshold 546 sats
Fee model vbyte-accurate per script type
External runtime deps bitcoinjs-lib, express

Architecture

Three layers — validation, selection, construction — with a REST API and React UI on top.

┌──────────────────────────────────────────────────────────────────┐
│                        USER / BROWSER                            │
│             React + TypeScript + Tailwind + TanStack Query       │
│   ┌──────────────────┐        ┌────────────────────────────────┐  │
│   │   JSON Input     │        │   Result Viewer                │  │
│   │   (fixture)      │   →    │   inputs · outputs · PSBT b64  │  │
│   └──────────────────┘        └────────────────────────────────┘  │
└──────────────────────────────┬───────────────────────────────────┘
                               │  HTTP/JSON  (REST API)
┌──────────────────────────────▼───────────────────────────────────┐
│                     Node.js  +  Express                          │
│                                                                  │
│   GET  /api/health   →  liveness check                           │
│   POST /api/build    →  validate → select → build PSBT → respond │
│                                                                  │
└──────────────────────────────┬───────────────────────────────────┘
                               │  in-process function calls
┌──────────────────────────────▼───────────────────────────────────┐
│                     TypeScript Core                              │
│                                                                  │
│  ① validate.ts      Defensive fixture validation                 │
│  ② helperFunctions  Coin selection (greedy) + fee/vbyte calc     │
│  ③ psbtCode.ts      PSBT construction via bitcoinjs-lib          │
│     · RBF + locktime semantics (BIP-125, anti-fee-sniping)       │
│     · witness_utxo / non_witness_utxo per input                  │
│     · change inclusion / dust filtering                          │
│     · warnings: HIGH_FEE · DUST_CHANGE · SEND_ALL · RBF_SIGNALING│
│  ④ main.ts          CLI entry point → writes out/<name>.json     │
└──────────────────────────────────────────────────────────────────┘

Coin selection

The selector uses a greedy, largest-first strategy:

  1. Sort UTXOs descending by value
  2. Accumulate inputs until Σ inputs ≥ Σ payments + estimated_fee
  3. Compute change; if change ≥ 546 sats → include it, else fold into fee and recalculate
  4. Enforce policy.max_inputs if provided — raise POLICY_VIOLATION otherwise

Fee is calculated using a per-script-type vbyte model:

weight = 40 + 2 + (nonwitness_bytes × 4) + witness_bytes + (output_count × 4)
vbytes = ceil(weight / 4)

Nonwitness/witness sizes are looked up per script type (p2wpkh, p2tr, p2pkh, etc.).


RBF & Locktime semantics

rbf locktime present current_height nSequence nLockTime
false/absent no 0xFFFFFFFF 0
false/absent yes 0xFFFFFFFE locktime
true no yes 0xFFFFFFFD current_height
true yes 0xFFFFFFFD locktime
true no no 0xFFFFFFFD 0

Locktime is classified in the JSON report:

  • "none"nLockTime == 0
  • "block_height"0 < nLockTime < 500,000,000
  • "unix_timestamp"nLockTime ≥ 500,000,000

Warnings

Coin Smith flags safety conditions in the output report:

Code Condition
HIGH_FEE fee_sats > 1,000,000 or fee_rate_sat_vb > 200
DUST_CHANGE Change output exists but value_sats < 546
SEND_ALL No change output — all leftover consumed as fee
RBF_SIGNALING Any input has nSequence ≤ 0xFFFFFFFD

Tech stack

┌── TypeScript / Node.js ──────────────────────────────────────────┐
│   main.ts           CLI entry point, fixture → JSON report        │
│   psbtCode.ts       PSBT construction (bitcoinjs-lib)             │
│   helperFunctions   Coin selection, fee calc, vbyte model         │
│   validate.ts       Defensive fixture validation                  │
│   server.ts         Express REST API + static file serving        │
└──────────────────────────────────────────────────────────────────┘
┌── React + TypeScript ────────────────────────────────────────────┐
│   Vite              Build tooling                                 │
│   Tailwind CSS      Utility-first styling                         │
│   TanStack Query    Async state management                        │
│   Lucide React      Icons                                         │
│   Components        JsonInput · ResultDisplay · Output badges     │
└──────────────────────────────────────────────────────────────────┘
┌── Testing ───────────────────────────────────────────────────────┐
│   Vitest            25+ unit tests                                │
│   Fixtures          26+ JSON test cases                           │
└──────────────────────────────────────────────────────────────────┘

Project structure

coin-smith/
├── psbt/
│   ├── main.ts              CLI entry point
│   ├── server.ts            Express API + static serving
│   ├── psbtCode.ts          PSBT construction logic
│   ├── helperFunctions.ts   Coin selection + fee calculation
│   ├── validate.ts          Input validation
│   ├── types.ts             TypeScript interfaces
│   ├── coinsmith.test.ts    Vitest unit tests
│   ├── package.json
│   ├── tsconfig.json
│   └── client/              React frontend (Vite + Tailwind)
│       ├── src/
│       │   ├── App.tsx
│       │   ├── pages/
│       │   │   └── Index.tsx
│       │   ├── components/
│       │   └── lib/api.ts
│       ├── vite.config.ts
│       └── package.json
├── fixtures/                26+ JSON test fixtures
├── out/                     Generated PSBT reports
├── cli.sh                   CLI entry point script
├── web.sh                   Web server entry point script
└── setup.sh                 Install deps + build

Getting started

1. Setup (once)

./setup.sh

Installs Node.js dependencies and builds the TypeScript backend and React frontend.

2. CLI

./cli.sh fixtures/basic_change_p2wpkh.json
# Output written to out/basic_change_p2wpkh.json

3. Web visualizer

./web.sh
# → http://127.0.0.1:3000

Paste any fixture JSON into the UI, hit Build, and inspect the selected inputs, constructed outputs, fee breakdown, RBF/locktime status, and PSBT base64.


Fixture format

{
  "network": "mainnet",
  "utxos": [
    {
      "txid": "11...",
      "vout": 0,
      "value_sats": 100000,
      "script_pubkey_hex": "0014...",
      "script_type": "p2wpkh",
      "address": "bc1..."
    }
  ],
  "payments": [
    {
      "address": "bc1...",
      "script_pubkey_hex": "0014...",
      "script_type": "p2wpkh",
      "value_sats": 70000
    }
  ],
  "change": {
    "address": "bc1...",
    "script_pubkey_hex": "0014...",
    "script_type": "p2wpkh"
  },
  "fee_rate_sat_vb": 5,
  "rbf": true,
  "locktime": 850000,
  "current_height": 850000,
  "policy": { "max_inputs": 5 }
}

script_pubkey_hex is authoritative — addresses are display-only.


JSON output shape

{
  "ok": true,
  "network": "mainnet",
  "strategy": "greedy",
  "selected_inputs": [
    {
      "txid": "...",
      "vout": 0,
      "value_sats": 100000,
      "script_pubkey_hex": "...",
      "script_type": "p2wpkh",
      "address": "bc1..."
    }
  ],
  "outputs": [
    { "n": 0, "value_sats": 70000, "script_type": "p2wpkh", "is_change": false },
    { "n": 1, "value_sats": 29300, "script_type": "p2wpkh", "is_change": true }
  ],
  "change_index": 1,
  "fee_sats": 700,
  "fee_rate_sat_vb": 5.0,
  "vbytes": 140,
  "rbf_signaling": true,
  "locktime": 850000,
  "locktime_type": "block_height",
  "psbt_base64": "cHNidP8BAFICAAAA...",
  "warnings": [{ "code": "RBF_SIGNALING" }]
}

On error:

{ "ok": false, "error": { "code": "INSUFFICIENT_FUNDS", "message": "..." } }

Design decisions

  • Greedy selection, not branch-and-bound. For wallet use the marginal improvement from optimal selection is small; greedy is fast, predictable, and auditable.
  • Two-pass change logic. Adding a change output changes vbytes, which changes the required fee. Coin Smith recomputes after the first pass to handle boundary conditions correctly.
  • script_pubkey_hex over address. Addresses are ambiguous across networks and formats. The hex script is the authoritative spend condition used in PSBT construction.
  • witness_utxo always set. For segwit inputs, witness_utxo is required for hardware wallet signing. For legacy inputs, non_witness_utxo (full previous tx) is included.
  • Structured errors everywhere. Both CLI and API return { ok, error: { code, message } } — never raw stack traces.

References

  • BIP-174 — Partially Signed Bitcoin Transaction Format
  • BIP-125 — Opt-in Full Replace-by-Fee Signaling
  • BIP-141 — Segregated Witness
  • BIP-341 — Taproot
  • Bitcoin Wiki — Transaction, Script, UTXO model

Built by Saniddhya Dubey

About

Protocol-first Bitcoin PSBT builder — coin selection, fee estimation, RBF/locktime construction, and a web UI that teaches non-technical users how Bitcoin transactions actually work.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors