npm.io
3.1.2 • Published 2d ago

bitcoin-tx-lib

Licence
MIT
Version
3.1.2
Deps
6
Size
312 kB
Vulns
0
Weekly
0
Stars
1

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

Install

npm install bitcoin-tx-lib

Manage 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 satoshis
Transaction 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, set sequence: "ffffffff".

Changelog

0.6.0
  • Fix: HDWallet.listAddresses() in watch-only mode now passes the wallet's network to Address.fromPubkey(). Previously, watch-only wallets always generated mainnet addresses regardless of the configured network.
  • Fix: HDWallet.import() and HDWallet.create() now propagate network to HDKManager.fromMnemonic(). Previously, network: "testnet" in options was silently ignored, causing getXPriv()/getXPub() to always return mainnet-prefixed keys (xprv/xpub) even for testnet wallets.
  • Feature: HDKManager.fromXPriv() and fromXPub() now support all four extended key formats: xprv/xpub (BIP44 mainnet), tprv/tpub (BIP44 testnet), zprv/zpub (BIP84 mainnet), vprv/vpub (BIP84 testnet). Previously, only xprv/xpub were accepted; other formats caused a "Version mismatch" error from the underlying BIP32 library.
  • Feature: HDKManager.fromXPriv() and fromXPub() now automatically infer purpose (44 or 84) and network (mainnet or testnet) from the key prefix. These inferred values can be overridden via pathParams.
  • Feature: HDWallet.import() now accepts zprv/zpub and vprv/vpub in addition to xprv/tprv and xpub/tpub. Network and purpose are inferred from the key prefix automatically.
  • Feature: HDKManager now stores a network property. When creating from mnemonic or seed with network: "testnet", derived pair keys inherit testnet by default without requiring an explicit network argument on each derivatePairKey() call.
  • Feature: getXPriv()/getXPub() now return the correct prefix for the configured network and purpose. For example, a BIP84 testnet wallet exports vprv/vpub instead of always returning zprv/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 each build() 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() and fromMasterSeed() now forward purpose, coinType, account, and change options to the constructor. Previously, purpose: 44 was silently ignored, causing BIP84 paths to be used instead of BIP44.
  • Fix: ECPairKey.getWif() now appends the 0x01 compressed-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 own sequence value 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, so getRawHex() always returns the fee-adjusted transaction.
  • Fix: validateInput() now checks txid + vout for duplicates instead of txid alone, 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>, prefix 0x00 0x20) enabling witness generation for P2WSH inputs. The previous check used opcode 0x79 (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_20 and OP_PUSHBYTES_32 corrected to 0x14 (20) and 0x20 (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.

Keywords