bitcoin-tx-lib
A TypeScript library for building and signing Bitcoin transactions (P2PKH and P2WPKH), focused on compatibility across any TypeScript environment, with minimal dependencies and built in pure TypeScript. Fully compatible with React, React Native, and any TypeScript project, with no reliance on native modules.
Index
- Manage Pair Key
- How to set up a transaction
- How to use Hierarchical Deterministic Keys
- How to use HD Wallet functions
- Building an HD Bitcoin Wallet with
bitcoin-tx-lib
Install
npm install bitcoin-tx-libManage Pair Key
How to create and import key pairs from different sources
import { ECPairKey } from 'bitcoin-tx-lib'
// Generate a random key pair (mainnet)
const pairKey = new ECPairKey()
// Generate a random key pair (testnet)
const pairKey = new ECPairKey({ network: "testnet" })
// Import from raw private key (hex)
const pairKey = ECPairKey.fromHex(
"0c28fca386c7a227600b2fe50b7cae11ec86d3bf1fbe471be89827e19d72aa1d",
"testnet"
)
// Import from WIF (Wallet Import Format)
// Both compressed (52-char) and uncompressed (51-char) WIFs are supported
const pairKey = ECPairKey.fromWif("cNk5Vf4VwDPSUFqn4JzwGJHpNMm5mWnHSTRFDWJZm7jdHrr5Uef")How to extract key pair information
const pairKey = ECPairKey.fromWif("cNk5Vf4VwDPSUFqn4JzwGJHpNMm5mWnHSTRFDWJZm7jdHrr5Uef")
// Get private key as Uint8Array (use getPrivateKeyHex() for hex string)
const privateKey: Uint8Array = pairKey.getPrivateKey()
// Get compressed public key as Uint8Array (use getPublicKeyHex() for hex string)
const publicKey: Uint8Array = pairKey.getPublicKey()
// Get address (P2WPKH native SegWit by default)
const addressSegwit = pairKey.getAddress() // "p2wpkh" is the default
const addressLegacy = pairKey.getAddress("p2pkh")
// Export WIF (compressed format, compatible with standard wallets)
const wif = pairKey.getWif()How to set up a transaction
Supported script types: P2PKH (legacy) and P2WPKH (native SegWit).
The library automatically detects the type from the scriptPubKey of each input.
Transaction (single key)
Use Transaction when all inputs are controlled by the same key pair.
import { ECPairKey, Transaction } from 'bitcoin-tx-lib'
const pairKey = ECPairKey.fromWif("cNk5Vf4VwDPSUFqn4JzwGJHpNMm5mWnHSTRFDWJZm7jdHrr5Uef")
const tx = new Transaction(pairKey)
tx.addInput({
txid: "157da15b3cdb2561602bd889d578227aa089915e3945c6d26569d27aecb9a4f7",
vout: 1,
value: 15197,
// scriptPubKey is optional — auto-derived from the key pair if omitted
scriptPubKey: "0014a8439c50793b033df810de257b313144a8f7edc9"
})
tx.addOutput({
address: "tb1q4mqy9h6km8wzltgtxra0vt4efuruhg7vh8hlvf",
amount: 14197 // value minus fee
})
tx.sign()
const rawHex = tx.getRawHex() // hex-encoded signed transaction
const txid = tx.getTxid() // correct txid (non-witness hash for SegWit, BIP 141)Note: Multiple UTXOs from the same parent transaction (same txid, different vout) are fully supported. Only spending the exact same UTXO (txid + vout) twice is rejected.
HDTransaction (per-input key)
Use HDTransaction when inputs are signed by different HD-derived key pairs.
import { HDTransaction, HDWallet } from 'bitcoin-tx-lib'
const { wallet } = HDWallet.create() // or HDWallet.import(mnemonic)
const tx = new HDTransaction()
tx.addInput({
txid: "157da15b3cdb2561602bd889d578227aa089915e3945c6d26569d27aecb9a4f7",
vout: 0,
value: 10000
}, wallet.getPairKey(0)) // key for this specific input
tx.addInput({
txid: "157da15b3cdb2561602bd889d578227aa089915e3945c6d26569d27aecb9a4f7",
vout: 1,
value: 20000
}, wallet.getPairKey(1)) // different key for this input
tx.addOutput({
address: "tb1q4mqy9h6km8wzltgtxra0vt4efuruhg7vh8hlvf",
amount: 29500
})
tx.sign()
const rawHex = tx.getRawHex()
const txid = tx.getTxid()Network fee
Pass whoPayTheFee and fee (sat/vbyte) when creating the transaction.
Call resolveFee() to deduct the fee from the designated output(s),
then call sign() to rebuild the transaction with the updated amounts.
resolveFee() is idempotent — calling it multiple times has no additional effect.
const tx = new Transaction(pairKey, {
whoPayTheFee: "tb1q4mqy9h6km8wzltgtxra0vt4efuruhg7vh8hlvf",
fee: 2, // 2 sat/vbyte
})
tx.addInput({ txid: "...", vout: 1, value: 30000 })
tx.addOutput({ address: "tb1q4mqy9h6km8wzltgtxra0vt4efuruhg7vh8hlvf", amount: 15000 })
tx.addOutput({ address: "tb1q4ppec5re8vpnm7qsmcjhkvf3gj500mwfw0yxaj", amount: 15000 })
tx.resolveFee() // deducts fee from the first address only
tx.sign() // rebuild with fee-adjusted amounts
const raw = tx.getRawHex()Set whoPayTheFee to "everyone" to split the fee evenly among all outputs:
const tx = new Transaction(pairKey, {
whoPayTheFee: "everyone",
fee: 2,
})
// ...
tx.resolveFee()
tx.sign()Use getFeeSats() to inspect the fee without deducting it:
tx.sign()
const fee = tx.getFeeSats() // total fee in satoshisTransaction size helpers
tx.sign()
const weight = tx.weight() // BIP 141 weight units
const vbytes = tx.vBytes() // virtual bytes (ceil(weight / 4))Replace-By-Fee (RBF): By default every input uses
sequence = 0xfffffffd, enabling RBF (BIP 125). To disable RBF for an input, setsequence: "ffffffff".
Changelog
0.6.0
- Fix:
HDWallet.listAddresses()in watch-only mode now passes the wallet'snetworktoAddress.fromPubkey(). Previously, watch-only wallets always generated mainnet addresses regardless of the configured network. - Fix:
HDWallet.import()andHDWallet.create()now propagatenetworktoHDKManager.fromMnemonic(). Previously,network: "testnet"in options was silently ignored, causinggetXPriv()/getXPub()to always return mainnet-prefixed keys (xprv/xpub) even for testnet wallets. - Feature:
HDKManager.fromXPriv()andfromXPub()now support all four extended key formats:xprv/xpub(BIP44 mainnet),tprv/tpub(BIP44 testnet),zprv/zpub(BIP84 mainnet),vprv/vpub(BIP84 testnet). Previously, onlyxprv/xpubwere accepted; other formats caused a "Version mismatch" error from the underlying BIP32 library. - Feature:
HDKManager.fromXPriv()andfromXPub()now automatically inferpurpose(44 or 84) andnetwork(mainnet or testnet) from the key prefix. These inferred values can be overridden viapathParams. - Feature:
HDWallet.import()now acceptszprv/zpubandvprv/vpubin addition toxprv/tprvandxpub/tpub. Network and purpose are inferred from the key prefix automatically. - Feature:
HDKManagernow stores anetworkproperty. When creating from mnemonic or seed withnetwork: "testnet", derived pair keys inherit testnet by default without requiring an explicit network argument on eachderivatePairKey()call. - Feature:
getXPriv()/getXPub()now return the correct prefix for the configured network and purpose. For example, a BIP84 testnet wallet exportsvprv/vpubinstead of always returningzprv/zpub. - Fix: Flaky test in
txbuilder.test.ts("legacy transaction: raw equals txid build") corrected to allow ±1 byte variation caused by non-deterministic DER signature length (extraEntropy: true). The test was re-signing the transaction on eachbuild()call, producing occasional mismatches.
0.5.0
- Fix:
HDTransaction.sign()now correctly uses the stripped (non-witness) serialization for txid computation, satisfying BIP 141. Previously, SegWit txids were computed over the witness-inclusive bytes. - Fix:
HDKManager.fromMnemonic()andfromMasterSeed()now forwardpurpose,coinType,account, andchangeoptions to the constructor. Previously,purpose: 44was silently ignored, causing BIP84 paths to be used instead of BIP44. - Fix:
ECPairKey.getWif()now appends the0x01compressed-key flag byte before the checksum, producing standard compressed WIF (52-char). Previously, exported WIFs were interpreted by external wallets as uncompressed keys, deriving incorrect addresses. - Fix:
generateScriptSig()now uses each input's ownsequencevalue in the signing preimage. Previously, all inputs in the preimage inherited the sequence of the input being signed, producing invalid signatures for multi-input legacy transactions. - Fix:
resolveFee()is now idempotent (calling twice no longer double-deducts the fee) and invalidates the cached raw transaction after adjusting output amounts, sogetRawHex()always returns the fee-adjusted transaction. - Fix:
validateInput()now checkstxid + voutfor duplicates instead oftxidalone, allowing multiple UTXOs from the same parent transaction and correctly rejecting only double-spends of the exact same output. - Fix:
HDTransactionBase.build()now encodes the legacy scriptSig length as a proper Bitcoin varint instead of a fixed 1-byte value, fixing malformed transactions when scriptSig exceeds 252 bytes. - Fix:
scriptPubkeyToScriptCode()now correctly identifies P2WSH scripts (OP_0 <32-byte-hash>, prefix0x00 0x20) enabling witness generation for P2WSH inputs. The previous check used opcode0x79(OP_SWAP) which never matched any real script. - Fix:
ECPairKey.signDER()no longer loops until exactly 70 bytes. All valid DER signatures (70–72 bytes) are now accepted, eliminating an unnecessary restriction and potential long loops for certain key/message combinations. - Fix:
hexToBytes()loop bound corrected from<=to<, eliminating a silent out-of-bounds write on typed arrays. - Fix:
OP_PUSHBYTES_20andOP_PUSHBYTES_32corrected to0x14(20) and0x20(32) respectively. - Improvement:
weight()now derives witness size from the cached serialized transaction (txraw vs txidraw diff) instead of re-signing all inputs on each call, making it consistent and eliminating redundant signing. - Test: Added idempotency tests for
resolveFee(). - Test: Added test covering multiple inputs from the same parent transaction.