Create, sign & decode BTC transactions with minimum deps.

Overview

micro-btc-signer

Create, sign & decode BTC transactions with minimum deps.

  • 🪶 Small: ~2.2K lines
  • Create transactions, inputs, outputs, sign them
  • No network code: allows simpler audits and offline usage
  • Classic & SegWit support: P2PK, P2PKH, P2WPKH, P2SH, P2WSH, P2MS
  • 🧪 Schnorr & Taproot BIP340/BIP341: P2TR, P2TR-NS, P2TR-MS
  • 📨 BIP174 PSBT
  • Multisig support

The library is new and has not been battle-tested, use at your own risk.

Check out all web3 utility libraries: micro-eth-signer, micro-btc-signer, micro-sol-signer, micro-web3, tx-tor-broadcaster

Usage

npm install micro-btc-signer
import * as btc from 'micro-btc-signer';

Payments

BTC has several UTXO types:

  • P2PK: Legacy, from 2010
  • P2PKH, P2SH, P2MS: Classic
  • P2WPKH, P2WSH: classic, SegWit
  • P2TR: Taproot, recommended

P2PK (Pay To Public Key)

Old script, doesn't have address at all. Should be wrapped in P2SH/P2WSH/P2SH-P2WSH.

const uncompressed = hex.decode(
  '04ad90e5b6bc86b3ec7fac2c5fbda7423fc8ef0d58df594c773fa05e2c281b2bfe877677c668bd13603944e34f4818ee03cadd81a88542b8b4d5431264180e2c28'
);

deepStrictEqual(btc.p2pk(uncompressed), {
  type: 'pk',
  script: hex.decode(
    '4104ad90e5b6bc86b3ec7fac2c5fbda7423fc8ef0d58df594c773fa05e2c281b2bfe877677c668bd13603944e34f4818ee03cadd81a88542b8b4d5431264180e2c28ac'
  ),
});

P2PKH (Public Key Hash)

Classic address (pre-SegWit)

const PubKey = hex.decode('030000000000000000000000000000000000000000000000000000000000000001');
deepStrictEqual(btc.p2pkh(PubKey), {
  type: 'pkh',
  address: '134D6gYy8DsR5m4416BnmgASuMBqKvogQh',
  script: hex.decode('76a914168b992bcfc44050310b3a94bd0771136d0b28d188ac'),
});
// P2SH-P2PKH
deepStrictEqual(btc.p2sh(btc.p2pkh(PubKey)), {
  type: 'sh',
  address: '3EPhLJ1FuR2noj6qrTs4YvepCvB6sbShoV',
  script: hex.decode('a9148b530b962725af3bb7c818f197c619db3f71495087'),
  redeemScript: hex.decode('76a914168b992bcfc44050310b3a94bd0771136d0b28d188ac'),
});
// P2WSH-P2PKH
deepStrictEqual(btc.p2wsh(btc.p2pkh(PubKey)), {
  type: 'wsh',
  address: 'bc1qhxtthndg70cthfasy8y4qlk9h7r3006azn9md0fad5dg9hh76nkqaufnuz',
  script: hex.decode('0020b996bbcda8f3f0bba7b021c9507ec5bf8717bf5d14cbb6bd3d6d1a82defed4ec'),
  witnessScript: hex.decode('76a914168b992bcfc44050310b3a94bd0771136d0b28d188ac'),
});
// P2SH-P2WSH-P2PKH
deepStrictEqual(btc.p2sh(btc.p2wsh(btc.p2pkh(PubKey))), {
  type: 'sh',
  address: '3EHxWHyLv5Seu5Cd6D1cH56jLKxSi3ps8C',
  script: hex.decode('a9148a3d36fb710a9c7cae06cfcdf39792ff5773e8f187'),
  redeemScript: hex.decode('0020b996bbcda8f3f0bba7b021c9507ec5bf8717bf5d14cbb6bd3d6d1a82defed4ec'),
  witnessScript: hex.decode('76a914168b992bcfc44050310b3a94bd0771136d0b28d188ac'),
});

P2WPKH (Witness Public Key Hash)

Same as P2PKH, but for SegWit V0. Basic bech32 address. Cannot be wrapped in P2WSH.

const PubKey = hex.decode('030000000000000000000000000000000000000000000000000000000000000001');
deepStrictEqual(btc.p2wpkh(PubKey), {
  type: 'wpkh',
  address: 'bc1qz69ej270c3q9qvgt822t6pm3zdksk2x35j2jlm',
  script: hex.decode('0014168b992bcfc44050310b3a94bd0771136d0b28d1'),
});
// P2SH-P2WPKH
deepStrictEqual(btc.p2sh(btc.p2wpkh(PubKey)), {
  type: 'sh',
  address: '3BCuRViGCTXmQjyJ9zjeRUYrdZTUa38zjC',
  script: hex.decode('a91468602f2db7b7d7cdcd2639ab6bf7f5bfe828e53f87'),
  redeemScript: hex.decode('0014168b992bcfc44050310b3a94bd0771136d0b28d1'),
});

P2SH (Script Hash)

Classic (pre-SegWit) script address. Useful for multisig and other smart-contracts. Takes full output of other payments, not just script.

NOTE: redeemScript should be added to transaction input in order to spend.

const PubKey = hex.decode('030000000000000000000000000000000000000000000000000000000000000001');
// Wrap P2PKH in P2SH
deepStrictEqual(btc.p2sh(btc.p2pkh(PubKey)), {
  type: 'sh',
  address: '3EPhLJ1FuR2noj6qrTs4YvepCvB6sbShoV',
  script: hex.decode('a9148b530b962725af3bb7c818f197c619db3f71495087'),
  redeemScript: hex.decode('76a914168b992bcfc44050310b3a94bd0771136d0b28d188ac'),
});

P2WSH (Witness Script Hash)

Same as P2SH but for SegWit V0.

NOTE: witnessScript should be added to transaction input in order to spend.

const PubKey = hex.decode('030000000000000000000000000000000000000000000000000000000000000001');
deepStrictEqual(btc.p2wsh(btc.p2pkh(PubKey)), {
  type: 'wsh',
  address: 'bc1qhxtthndg70cthfasy8y4qlk9h7r3006azn9md0fad5dg9hh76nkqaufnuz',
  script: hex.decode('0020b996bbcda8f3f0bba7b021c9507ec5bf8717bf5d14cbb6bd3d6d1a82defed4ec'),
  witnessScript: hex.decode('76a914168b992bcfc44050310b3a94bd0771136d0b28d188ac'),
});

P2SH-P2WSH

Not really script type, but construction of P2WSH inside P2SH.

NOTE: both reedemScript and witnessScript should be added to transaction in order to spend.

const PubKey = hex.decode('030000000000000000000000000000000000000000000000000000000000000001');
deepStrictEqual(btc.p2sh(btc.p2wsh(btc.p2pkh(PubKey))), {
  type: 'sh',
  address: '3EHxWHyLv5Seu5Cd6D1cH56jLKxSi3ps8C',
  script: hex.decode('a9148a3d36fb710a9c7cae06cfcdf39792ff5773e8f187'),
  redeemScript: hex.decode('0020b996bbcda8f3f0bba7b021c9507ec5bf8717bf5d14cbb6bd3d6d1a82defed4ec'),
  witnessScript: hex.decode('76a914168b992bcfc44050310b3a94bd0771136d0b28d188ac'),
});

P2MS (classic multisig)

Classic (pre-taproot) M-of-N Multisig, doesn't have an address, should be wrapped in P2SH/P2WSH/P2SH-P2WSH.

const PubKeys = [
  hex.decode('030000000000000000000000000000000000000000000000000000000000000001'),
  hex.decode('030000000000000000000000000000000000000000000000000000000000000002'),
  hex.decode('030000000000000000000000000000000000000000000000000000000000000003'),
];
// Multisig 2-of-3 wrapped in P2SH
deepStrictEqual(btc.p2sh(btc.p2ms(2, PubKeys)), {
  type: 'sh',
  address: '3G4AeQtzCLoDAyv2eb3UVTG5atfkyHtuRn',
  script: hex.decode('a9149d91c6de4eacde72a7cc86bff98d1915b3c7818f87'),
  redeemScript: hex.decode(
    '5221030000000000000000000000000000000000000000000000000000000000000001210300000000000000000000000000000000000000000000000000000000000000022103000000000000000000000000000000000000000000000000000000000000000353ae'
  ),
});
// Multisig 2-of-3 wrapped in P2WSH
deepStrictEqual(btc.p2wsh(btc.p2ms(2, PubKeys)), {
  type: 'wsh',
  address: 'bc1qwnhzkn8wcyyrnfyfcp7555urssu5dq0rmnvg70hg02z3nxgg4f0qljmr2h',
  script: hex.decode('002074ee2b4ceec10839a489c07d4a538384394681e3dcd88f3ee87a85199908aa5e'),
  witnessScript: hex.decode(
    '5221030000000000000000000000000000000000000000000000000000000000000001210300000000000000000000000000000000000000000000000000000000000000022103000000000000000000000000000000000000000000000000000000000000000353ae'
  ),
});
// Multisig 2-of-3 wrapped in P2SH-P2WSH
deepStrictEqual(btc.p2sh(btc.p2wsh(btc.p2ms(2, PubKeys))), {
  type: 'sh',
  address: '3HKWSo57kmcJZ3h43pXS3m5UESR4wXcWTd',
  script: hex.decode('a914ab70ab84b12b891364b4b2a14ca813cac308b24287'),
  redeemScript: hex.decode('002074ee2b4ceec10839a489c07d4a538384394681e3dcd88f3ee87a85199908aa5e'),
  witnessScript: hex.decode(
    '5221030000000000000000000000000000000000000000000000000000000000000001210300000000000000000000000000000000000000000000000000000000000000022103000000000000000000000000000000000000000000000000000000000000000353ae'
  ),
});
// Useful util: wraps P2MS in P2SH or P2WSH
deepStrictEqual(btc.p2sh(btc.p2ms(2, PubKeys)), btc.multisig(2, PubKeys));
deepStrictEqual(btc.p2wsh(btc.p2ms(2, PubKeys)), btc.multisig(2, PubKeys, undefined, true));
// Sorted multisig (BIP67)
deepStrictEqual(btc.p2sh(btc.p2ms(2, PubKeys)), btc.sortedMultisig(2, PubKeys));
deepStrictEqual(btc.p2wsh(btc.p2ms(2, PubKeys)), btc.sortedMultisig(2, PubKeys, true));

P2TR (Taproot)

TapRoot (SegWit V1) script which replaces both public key and script types from previous versions.

NOTE: it takes p2tr(PubKey?, ScriptTree?) and works as PubKey OR ScriptTree, which means if you use any spendable PubKey and ScriptTree of multi-sig, owner of private key for PubKey will be able to spend output. If PubKey is undefined we use static unspendable PubKey by default, which leaks information about script type. However, any dynamic unspendable keys will require complex interaction to sign multi-sig wallets, and there is no BIP/PSBT fields for that yet.

NOTE: tapInternalKey, tapMerkleRoot, tapLeafScript should be added to transaction input in order to spend.

const PubKey = hex.decode('0101010101010101010101010101010101010101010101010101010101010101');
// Key Path Spend (owned of private key for PubKey can spend)
deepStrictEqual(btc.p2tr(PubKey), {
  type: 'tr',
  address: 'bc1p7yu5dsly83jg5tkxcljsa30vnpdpl22wr6rty98t6x6p6ekz2gkqzf2t2s',
  script: hex.decode('5120f13946c3e43c648a2ec6c7e50ec5ec985a1fa94e1e86b214ebd1b41d66c2522c'),
  tweakedPubkey: hex.decode('f13946c3e43c648a2ec6c7e50ec5ec985a1fa94e1e86b214ebd1b41d66c2522c'),
  tapInternalKey: hex.decode('0101010101010101010101010101010101010101010101010101010101010101'),
  tapMerkleRoot: hex.decode(''),
});

const PubKey2 = hex.decode('0202020202020202020202020202020202020202020202020202020202020202');
const PubKey3 = hex.decode('1212121212121212121212121212121212121212121212121212121212121212');
// Nested P2TR, owner of private key for any of PubKeys can spend whole
// NOTE: by default P2TR expects binary tree, but btc.p2tr can build it if list of scripts passed.
// Also, you can include {weight: N} to scripts to create differently balanced tree.
deepStrictEqual(btc.p2tr(undefined, [btc.p2tr(PubKey), btc.p2tr(PubKey2), btc.p2tr(PubKey3)]), {
  type: 'tr',
  // weights for bitcoinjs-lib: [3,2,1]
  address: 'bc1p58hcmfcjaee0jwzlgluzw86paw0h7sqmw2c8yq8t4wleqlqdn3qqv3rxf0',
  script: hex.decode('5120a1ef8da712ee72f9385f47f8271f41eb9f7f401b72b07200ebabbf907c0d9c40'),
});
// If scriptsTree is already binary tree, it will be used as-is
deepStrictEqual(btc.p2tr(undefined, [btc.p2tr(PubKey2), [btc.p2tr(PubKey), btc.p2tr(PubKey3)]]), {
  type: 'tr',
  // default weights for bitcoinjs-lib
  address: 'bc1pepwhs2tvnn6uj9eqy8kqdwjk2n3r8wjkunqcmahvkn4r2uyvzsxqqae82s',
  script: hex.decode('5120c85d78296c9cf5c9172021ec06ba5654e233ba56e4c18df6ecb4ea35708c140c'),
});

P2TR-NS (Taproot multisig)

Taproot N-of-N multisig ([<PubKeys[0:n-1]> CHECKSIGVERIFY] <PubKeys[n-1]> CHECKSIG).

_ NOTE _: First arg is M, if M!=PubKeys.length, it will create multi-leaf M-of-N taproot script tree. This allows to reveal only M PubKeys on spend, without any information about others. Fast for cases like 15-of-20, extremely slow for cases like 5-of-20.

const PubKey = hex.decode('0101010101010101010101010101010101010101010101010101010101010101');
const PubKey2 = hex.decode('0202020202020202020202020202020202020202020202020202020202020202');
const PubKey3 = hex.decode('1212121212121212121212121212121212121212121212121212121212121212');

// Simple 3-of-3 multisig
deepStrictEqual(btc.p2tr_ns(3, [PubKey, PubKey2, PubKey3]), [
  {
    type: 'tr_ns',
    script: hex.decode(
      '200101010101010101010101010101010101010101010101010101010101010101ad200202020202020202020202020202020202020202020202020202020202020202ad201212121212121212121212121212121212121212121212121212121212121212ac'
    ),
  },
]);
// M!==pubkeys.length -> creates scripts for [[PubKey, PubKey2], [PubKey, PubKey3], [PubKey2, PubKey3]]
deepStrictEqual(btc.p2tr(undefined, btc.p2tr_ns(2, [PubKey, PubKey2, PubKey3])), {
  type: 'tr',
  address: 'bc1pevfcmnkqqq09a4n0fs8c7mwlc6r4efqpvgyqpjvegllavgw235fq3kz7a0',
  script: hex.decode('5120cb138dcec0001e5ed66f4c0f8f6ddfc6875ca401620800c99947ffd621ca8d12'),
});

P2TR-MS (Taproot M-of-N multisig)

M-of-N single leaf TapRoot multisig (<PubKeys[0]> CHECKSIG [<PubKeys[1:n]> CHECKSIGADD] <M> NUMEQUAL)

NOTE: experimental, use at your own risk.

const PubKey = hex.decode('0101010101010101010101010101010101010101010101010101010101010101');
const PubKey2 = hex.decode('0202020202020202020202020202020202020202020202020202020202020202');
const PubKey3 = hex.decode('1212121212121212121212121212121212121212121212121212121212121212');
// 2-of-3 TapRoot multisig
deepStrictEqual(btc.p2tr_ms(2, [PubKey, PubKey2, PubKey3]), {
  type: 'tr_ms',
  script: hex.decode(
    '200101010101010101010101010101010101010101010101010101010101010101ac200202020202020202020202020202020202020202020202020202020202020202ba201212121212121212121212121212121212121212121212121212121212121212ba529c'
  ),
});

Transaction

Encode/decode

NOTE: we support both PSBTv0 and draft PSBTv2 (there is no PSBTv1). If PSBTv2 transaction encoded into PSBTv1, all PSBTv2 fields will be stripped.

// Decode
Transaction.fromRaw(raw: Bytes, opts: TxOpts = {}); // Raw tx
Transaction.fromPSBT(psbt: Bytes, opts: TxOpts = {}); // PSBT tx
// Encode
tx.unsignedTx; // Bytes of raw unsigned tx
tx.hex; // hex encoded signed raw tx
tx.toPSBT(ver = this.PSBTVersion); // PSBT

Inputs

type TransactionInput = {
  hash: Bytes,
  index: number,
  nonWitnessUtxo?: <RawTransactionBytesOrHex>,
  witnessUtxo?: {script?: Bytes; amount: bigint},
  partialSig?: [Bytes, Bytes][]; // [PubKey, Signature]
  sighashType?: P.U32LE,
  redeemScript?: Bytes,
  witnessScript?: Bytes,
  bip32Derivation?: [Bytes, {fingerprint: number; path: number[]}]; // [PubKey, DeriviationPath]
  finalScriptSig?: Bytes,
  finalScriptWitness?: Bytes[],
  porCommitment?: Bytes,
  sequence?: number,
  requiredTimeLocktime?: number,
  requiredHeightLocktime?: number,
  tapKeySig?: Bytes,
  tapScriptSig?: [Bytes, Bytes][]; // [PubKeySchnorr, LeafHash]
  // [ControlBlock, ScriptWithVersion]
  tapLeafScript?: [{version: number; internalKey: Bytes; merklePath: Bytes[]}, Bytes];
  tapInternalKey?: Bytes,
  tapMerkleRoot?: Bytes,
};

tx.addInput(input: TransactionInput): number;
tx.updateInput(idx: number, input: TransactionInput);

// Input
tx.addInput({ hash: new Uint8Array(32), index: 0 });
deepStrictEqual(tx.inputs[0], {
  hash: new Uint8Array(32),
  index: 0,
  sequence: btc.DEFAULT_SEQUENCE,
});
// Update basic value
tx.updateInput(0, { index: 10 });
deepStrictEqual(tx.inputs[0], {
  hash: new Uint8Array(32),
  index: 10,
  sequence: btc.DEFAULT_SEQUENCE,
});
// Add value as hex
tx.addInput({
  hash: '0000000000000000000000000000000000000000000000000000000000000000',
  index: 0,
});
deepStrictEqual(tx.inputs[2], {
  hash: new Uint8Array(32),
  index: 0,
  sequence: btc.DEFAULT_SEQUENCE,
});
// Update key map
const pubKey = hex.decode('030000000000000000000000000000000000000000000000000000000000000001');
const bip1 = [pubKey, { fingerprint: 5, path: [1, 2, 3] }];
const pubKey2 = hex.decode('030000000000000000000000000000000000000000000000000000000000000002');
const bip2 = [pubKey2, { fingerprint: 6, path: [4, 5, 6] }];
const pubKey3 = hex.decode('030000000000000000000000000000000000000000000000000000000000000003');
const bip3 = [pubKey3, { fingerprint: 7, path: [7, 8, 9] }];
// Add K-V
tx.updateInput(0, { bip32Derivation: [bip1] });
deepStrictEqual(tx.inputs[0].bip32Derivation, [bip1]);
// Add another K-V
tx.updateInput(0, { bip32Derivation: [bip2] });
deepStrictEqual(tx.inputs[0].bip32Derivation, [bip1, bip2]);
// Delete K-V
tx.updateInput(0, { bip32Derivation: [[pubKey, undefined]] });
deepStrictEqual(tx.inputs[0].bip32Derivation, [bip2]);
// Second add of same k-v does nothing
tx.updateInput(0, { bip32Derivation: [bip2] });
deepStrictEqual(tx.inputs[0].bip32Derivation, [bip2]);
// Second add of k-v with different value breaks
throws(() => tx.updateInput(0, { bip32Derivation: [[pubKey2, bip1[1]]] }));
tx.updateInput(0, { bip32Derivation: [bip1, bip2, bip3] });
// Preserves order (re-ordered on PSBT encoding)
deepStrictEqual(tx.inputs[0].bip32Derivation, [bip2, bip1, bip3]);
// PSBT encoding re-order k-v
const tx2 = btc.Transaction.fromPSBT(tx.toPSBT());
deepStrictEqual(tx2.inputs[0].bip32Derivation, [bip1, bip2, bip3]);
// Remove field
tx.updateInput(0, { bip32Derivation: undefined });
deepStrictEqual(tx.inputs[0], {
  hash: new Uint8Array(32),
  index: 10,
  sequence: btc.DEFAULT_SEQUENCE,
});

Outputs

type TransactionOutput = {
  script: Bytes,
  amount: bigint,
  redeemScript?: Bytes,
  witnessScript?: Bytes,
  bip32Derivation?: [Bytes, {fingerprint: number; path: number[]}]; // [PubKey, DeriviationPath]
  tapInternalKey?: Bytes,
};

tx.addOutput(o: TransactionOutput): number;
tx.updateOutput(idx: number, output: TransactionOutput);
tx.addOutputAddress(address: string, amount: string | bigint, network = NETWORK): number;

const compressed = hex.decode(
  '030000000000000000000000000000000000000000000000000000000000000001'
);
const script = btc.p2pkh(compressed).script;
tx.addOutput({ script, amount: 100n });
deepStrictEqual(tx.outputs[0], {
  script,
  amount: 100n,
});
// Update basic value
tx.updateOutput(0, { amount: 200n });
deepStrictEqual(tx.outputs[0], {
  script,
  amount: 200n,
});
// Add K-V
tx.updateOutput(0, { bip32Derivation: [bip1] });
deepStrictEqual(tx.outputs[0].bip32Derivation, [bip1]);
// Add another K-V
tx.updateOutput(0, { bip32Derivation: [bip2] });
deepStrictEqual(tx.outputs[0].bip32Derivation, [bip1, bip2]);
// Delete K-V
tx.updateOutput(0, { bip32Derivation: [[pubKey, undefined]] });
deepStrictEqual(tx.outputs[0].bip32Derivation, [bip2]);
// Second add of same k-v does nothing
tx.updateOutput(0, { bip32Derivation: [bip2] });
deepStrictEqual(tx.outputs[0].bip32Derivation, [bip2]);
// Second add of k-v with different value breaks
throws(() => tx.updateOutput(0, { bip32Derivation: [[pubKey2, bip1[1]]] }));
tx.updateOutput(0, { bip32Derivation: [bip1, bip2, bip3] });
// Preserves order (re-ordered on PSBT encoding)
deepStrictEqual(tx.outputs[0].bip32Derivation, [bip2, bip1, bip3]);
// PSBT encoding re-order k-v
const tx3 = btc.Transaction.fromPSBT(tx.toPSBT());
deepStrictEqual(tx3.outputs[0].bip32Derivation, [bip1, bip2, bip3]);
// Remove field
tx.updateOutput(0, { bip32Derivation: undefined });
deepStrictEqual(tx.outputs[0], {
  script,
  amount: 200n,
});

Basic transaction sign

const privKey = hex.decode('0101010101010101010101010101010101010101010101010101010101010101');
const txP2WPKH = new btc.Transaction();
for (const inp of TX_TEST_INPUTS) {
  txP2WPKH.addInput({
    hash: inp.hash,
    index: inp.index,
    witnessUtxo: {
      amount: inp.amount,
      script: btc.p2wpkh(secp256k1.getPublicKey(privKey, true)).script,
    },
  });
}
for (const [address, amount] of TX_TEST_OUTPUTS) tx32.addOutputAddress(address, amount);
deepStrictEqual(hex.encode(tx32.unsignedTx), RAW_TX_HEX);
txP2WPKH.sign(privKey);
txP2WPKH.finalize();
deepStrictEqual(txP2WPKH.id, 'cbb94443b19861df0824914fa654212facc071854e0df6f7388b482a6394526d');
deepStrictEqual(
  txP2WPKH.hex,
  '010000000001033edaa6c4e0740ae334dbb5857dd8c6faf6ea5196760652ad7033ed9031c261c00000000000ffffffff0d9ae8a4191b3ba5a2b856c21af0f7a4feb97957ae80725ef38a933c906519a20000000000ffffffffc7a4a37d38c2b0de3d3b3e8d8e8a331977c12532fc2a4632df27a89c311ee2fa0000000000ffffffff03e8030000000000001976a91406afd46bcdfd22ef94ac122aa11f241244a37ecc88ac881300000000000017a914a860f76561c85551594c18eecceffaee8c4822d7876b24000000000000160014e8df018c7e326cc253faac7e46cdc51e68542c4202473044022024e7b1a6ae19a95c69c192745db09cc54385a80cc7684570cfbf2da84cbbfa0802205ad55efb2019a1aa6edc03cf243989ea428c4d216699cbae2cfaf3c26ddef5650121031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f0247304402204415ef16f341e888ca2483b767b47fcf22977b6d673c3f7c6cae2f6b4bc2ac08022055be98747345b02a6f40edcc2f80390dcef4efe57b38c1bb7d16bdbca710abfd0121031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f02473044022069769fb5c97a7dd9401dbd3f6d32a38fe82bc8934c49c7c4cd3b39c6d120080c02202c181604203dc45c10e5290ded103195fae117d7fb0db19cdc411e73a76da6cb0121031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f00000000'
);

BIP174 PSBT multi-sig example

const testnet = {
  wif: 0xef,
  bip32: {
    public: 0x043587cf,
    private: 0x04358394,
  },
};
// The private keys in the tests below are derived from the following master private key:
const epriv =
  'tprv8ZgxMBicQKsPd9TeAdPADNnSyH9SSUUbTVeFszDE23Ki6TBB5nCefAdHkK8Fm3qMQR6sHwA56zqRmKmxnHk37JkiFzvncDqoKmPWubu7hDF';
const hdkey = bip32.HDKey.fromExtendedKey(epriv, testnet.bip32);
// const seed = 'cUkG8i1RFfWGWy5ziR11zJ5V4U4W3viSFCfyJmZnvQaUsd1xuF3T';
const tx = new btc.Transaction(2);
// A creator creating a PSBT for a transaction which creates the following outputs:
tx.addOutput({ script: '0014d85c2b71d0060b09c9886aeb815e50991dda124d', amount: '1.49990000' });
tx.addOutput({ script: '001400aea9a2e5f0f876a588df5546e8742d1d87008f', amount: '1.00000000' });
// and spends the following inputs:
// NOTE: spec uses txId instead of txHash
const rev = (t) => hex.encode(hex.decode(t).reverse());
tx.addInput({
  hash: rev('75ddabb27b8845f5247975c8a5ba7c6f336c4570708ebe230caf6db5217ae858'),
  index: 0,
});
tx.addInput({
  hash: rev('1dea7cd05979072a3578cab271c02244ea8a090bbb46aa680a65ecd027048d83'),
  index: 1,
});
// must create this PSBT:
const psbt1 = tx.toPSBT();
// Given the above PSBT, an updater with only the following:
const tx2 = btc.Transaction.fromPSBT(psbt1);
tx2.updateInput(0, {
  nonWitnessUtxo:
    '0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f618765000000',
  redeemScript:
    '5221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae',
  bip32Derivation: [
    [
      '029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f',
      { fingerprint: hdkey.fingerprint, path: btc.bip32Path("m/0'/0'/0'") },
    ],
    [
      '02dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d7',
      { fingerprint: hdkey.fingerprint, path: btc.bip32Path("m/0'/0'/1'") },
    ],
  ],
});
tx2.updateInput(1, {
  // use witness utxo ({script, amount})
  witnessUtxo: btc.RawTx.decode(
    hex.decode(
      '0200000000010158e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd7501000000171600145f275f436b09a8cc9a2eb2a2f528485c68a56323feffffff02d8231f1b0100000017a914aed962d6654f9a2b36608eb9d64d2b260db4f1118700c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e88702483045022100a22edcc6e5bc511af4cc4ae0de0fcd75c7e04d8c1c3a8aa9d820ed4b967384ec02200642963597b9b1bc22c75e9f3e117284a962188bf5e8a74c895089046a20ad770121035509a48eb623e10aace8bfd0212fdb8a8e5af3c94b0b133b95e114cab89e4f7965000000'
    )
  ).outputs[1],
  redeemScript: '00208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903',
  witnessScript:
    '522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae',
  bip32Derivation: [
    [
      '03089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc',
      { fingerprint: hdkey.fingerprint, path: btc.bip32Path("m/0'/0'/2'") },
    ],
    [
      '023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e73',
      { fingerprint: hdkey.fingerprint, path: btc.bip32Path("m/0'/0'/3'") },
    ],
  ],
});
tx2.updateOutput(0, {
  bip32Derivation: [
    [
      '03a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca58771',
      { fingerprint: hdkey.fingerprint, path: btc.bip32Path("m/0'/0'/4'") },
    ],
  ],
});
tx2.updateOutput(1, {
  bip32Derivation: [
    [
      '027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b50051096',
      { fingerprint: hdkey.fingerprint, path: btc.bip32Path("m/0'/0'/5'") },
    ],
  ],
});
// Must create this PSBT:
const psbt2 = tx2.toPSBT();
// An updater which adds SIGHASH_ALL to the above PSBT must create this PSBT:
const tx3 = btc.Transaction.fromPSBT(psbt2);
for (let i = 0; i < tx3.inputs.length; i++)
  tx3.updateInput(i, { sighashType: btc.SignatureHash.ALL });
const psbt3 = tx3.toPSBT();
/*
  Given the above updated PSBT, a signer that supports SIGHASH_ALL for P2PKH and P2WPKH spends and uses RFC6979 for nonce generation and has the following keys:
  - cP53pDbR5WtAD8dYAW9hhTjuvvTVaEiQBdrz9XPrgLBeRFiyCbQr (m/0'/0'/0')
  - cR6SXDoyfQrcp4piaiHE97Rsgta9mNhGTen9XeonVgwsh4iSgw6d (m/0'/0'/2')
*/
// NOTE: we don't use HDKey, because it will everything because of bip32 derivation
const tx4 = btc.Transaction.fromPSBT(psbt3);
tx4.sign(btc.WIF(testnet).decode('cP53pDbR5WtAD8dYAW9hhTjuvvTVaEiQBdrz9XPrgLBeRFiyCbQr'));
tx4.sign(btc.WIF(testnet).decode('cR6SXDoyfQrcp4piaiHE97Rsgta9mNhGTen9XeonVgwsh4iSgw6d'));
// must create this PSBT:
const psbt4 = tx4.toPSBT();
// Given the above updated PSBT, a signer with the following keys:
// cT7J9YpCwY3AVRFSjN6ukeEeWY6mhpbJPxRaDaP5QTdygQRxP9Au (m/0'/0'/1')
// cNBc3SWUip9PPm1GjRoLEJT6T41iNzCYtD7qro84FMnM5zEqeJsE (m/0'/0'/3')
const tx5 = btc.Transaction.fromPSBT(psbt3);
tx5.sign(btc.WIF(testnet).decode('cT7J9YpCwY3AVRFSjN6ukeEeWY6mhpbJPxRaDaP5QTdygQRxP9Au'));
tx5.sign(btc.WIF(testnet).decode('cNBc3SWUip9PPm1GjRoLEJT6T41iNzCYtD7qro84FMnM5zEqeJsE'));
// must create this PSBT:
const psbt5 = tx5.toPSBT();
// Given both of the above PSBTs, a combiner must create this PSBT:
const psbt6 = btc.PSBTCombine([psbt4, psbt5]);
// Given the above PSBT, an input finalizer must create this PSBT:
const tx7 = btc.Transaction.fromPSBT(psbt6);
tx7.finalize();
const psbt7 = tx7.toPSBT();
// Given the above PSBT, a transaction extractor must create this Bitcoin transaction:
const tx8 = btc.Transaction.fromPSBT(psbt7);
deepStrictEqual(
  tx8.extract(),
  hex.decode(
    '0200000000010258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd7500000000da00473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752aeffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d01000000232200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f000400473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f01473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d20147522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae00000000'
  )
);

Utils

getAddress

Returns common addresses from privateKey

const privKey = hex.decode('0101010101010101010101010101010101010101010101010101010101010101');
deepStrictEqual(btc.getAddress('pkh', privKey), '1C6Rc3w25VHud3dLDamutaqfKWqhrLRTaD'); // P2PKH (legacy address)
deepStrictEqual(btc.getAddress('wpkh', privKey), 'bc1q0xcqpzrky6eff2g52qdye53xkk9jxkvrh6yhyw'); // SegWit V0 address
deepStrictEqual(
  btc.getAddress('tr', priv),
  'bc1p33wm0auhr9kkahzd6l0kqj85af4cswn276hsxg6zpz85xe2r0y8syx4e5t'
); // TapRoot KeyPathSpend

WIF

Encoding/decoding of WIF privateKeys. Only compessed keys are supported for now.

const privKey = hex.decode('0101010101010101010101010101010101010101010101010101010101010101');
deepStrictEqual(btc.WIF().encode(privKey), 'KwFfNUhSDaASSAwtG7ssQM1uVX8RgX5GHWnnLfhfiQDigjioWXHH');
deepStrictEqual(
  hex.encode(btc.WIF().decode('KwFfNUhSDaASSAwtG7ssQM1uVX8RgX5GHWnnLfhfiQDigjioWXHH')),
  '0101010101010101010101010101010101010101010101010101010101010101'
);

Script

Encoding/decoding bitcoin scripts

deepStrictEqual(
  btc.Script.decode(
    hex.decode(
      '5221030000000000000000000000000000000000000000000000000000000000000001210300000000000000000000000000000000000000000000000000000000000000022103000000000000000000000000000000000000000000000000000000000000000353ae'
    )
  ).map((i) => (P.isBytes(i) ? hex.encode(i) : i)),
  [
    'OP_2',
    '030000000000000000000000000000000000000000000000000000000000000001',
    '030000000000000000000000000000000000000000000000000000000000000002',
    '030000000000000000000000000000000000000000000000000000000000000003',
    'OP_3',
    'CHECKMULTISIG',
  ]
);
deepStrictEqual(
  hex.encode(
    btc.Script.encode([
      'OP_2',
      hex.decode('030000000000000000000000000000000000000000000000000000000000000001'),
      hex.decode('030000000000000000000000000000000000000000000000000000000000000002'),
      hex.decode('030000000000000000000000000000000000000000000000000000000000000003'),
      'OP_3',
      'CHECKMULTISIG',
    ])
  ),
  '5221030000000000000000000000000000000000000000000000000000000000000001210300000000000000000000000000000000000000000000000000000000000000022103000000000000000000000000000000000000000000000000000000000000000353ae'
);

OutScript

Encoding/decoding of output scripts

deepStrictEqual(
  btc.OutScript.decode(
    hex.decode(
      '5221030000000000000000000000000000000000000000000000000000000000000001210300000000000000000000000000000000000000000000000000000000000000022103000000000000000000000000000000000000000000000000000000000000000353ae'
    )
  ),
  {
    type: 'ms',
    m: 2,
    pubkeys: [
      '030000000000000000000000000000000000000000000000000000000000000001',
      '030000000000000000000000000000000000000000000000000000000000000002',
      '030000000000000000000000000000000000000000000000000000000000000003',
    ].map(hex.decode),
  }
);
deepStrictEqual(
  hex.encode(
    btc.OutScript.encode({
      type: 'ms',
      m: 2,
      pubkeys: [
        '030000000000000000000000000000000000000000000000000000000000000001',
        '030000000000000000000000000000000000000000000000000000000000000002',
        '030000000000000000000000000000000000000000000000000000000000000003',
      ].map(hex.decode),
    })
  ),
  '5221030000000000000000000000000000000000000000000000000000000000000001210300000000000000000000000000000000000000000000000000000000000000022103000000000000000000000000000000000000000000000000000000000000000353ae'
);

License

MIT (c) Paul Miller (https://paulmillr.com), see LICENSE file.

You might also like...

Encode/Decode Bot Protections Payload

decode.antibot.to Open source tools to help you decode/encode sensor data. Features Browser decoding/encoding API decoding/encoding Usage PerimeterX E

Dec 25, 2022

A library with different methods to encode and decode data.

encryption_lib for Deno A library with different methods to encode and decode data. Usage Example: Caesar Cipher import { Caesar } from "https://deno.

Feb 3, 2022

A pure JavaScript QRCode encode and decode library.

QRCode A pure JavaScript QRCode encode and decode library. QRCode guide and demo QRCode guide QRCode example QRCode example use worker Modify from kaz

Nov 28, 2022

An interceptor to validate and decode Pub/Sub push messages for endpoints

NestJS GCP Pub/Sub Interceptor Provides an Interceptor for NestJS to automagically validate and unwrap HTTP push messages from Google Cloud Platform's

Dec 15, 2022

Sample code for ETH Sign In

Sign in with Ethereum Sample code for ETH Sign In at https://acik-kaynak.org/oauth-guzel-peki-ethereumu-denediniz-mi/ The related code for Sign in wit

Jan 2, 2023

Demodal is a browser extension that automatically removes content blocking modals including paywalls, discount offers, promts to sign up or enter your email address and more.

Demodal Demodal is a browser extension that automatically removes content blocking modals including paywalls, discount offers, promts to sign up or en

Jan 4, 2023

Firebase SDK 9 + Google Sign In + Chrome Extension Manifest Version 3 + Webpack

Firebase SDK 9 + Google Sign In + Chrome Extension Manifest Version 3 + Webpack Demo Find this Chrome Extension Setup and working demo here or on Yout

Dec 28, 2022

Development of a Sign Up page form in HTML

Project: Trybewarts Project developed studying in Trybe. Technologies and tools used HTML CSS FlexBox JavaScript Kanban / Scrum Project objective Deve

Jun 14, 2022

An easy and simple way to manage your financial transactions.

An easy and simple way to manage your financial transactions.

MyWallet An easy and simple way to manage your financial transactions. With MyWallet you can track your incomes and expenses and always keep track of

Nov 16, 2022
Comments
  • Input hash bytes order

    Input hash bytes order

    I've faced with the issue that the byte order is messed up in hash input parameter.

    Let's look at the example from the tests: https://github.com/paulmillr/micro-btc-signer/blob/main/test/basic.test.js#L52

    Transaction: https://blockstream.info/tx/c061c23190ed3370ad5206769651eaf6fac6d87d85b5db34e30a74e0c4a6da3e

    • Transaction ID txid is: c061c23190ed3370ad5206769651eaf6fac6d87d85b5db34e30a74e0c4a6da3e
    • Transaction hash should be reverse order of bytes.
    const hash = hex.decode('c061c23190ed3370ad5206769651eaf6fac6d87d85b5db34e30a74e0c4a6da3e').reverse();
    // 3edaa6c4e0740ae334dbb5857dd8c6faf6ea5196760652ad7033ed9031c261c0
    

    When hash is passed as bytes in reverse order of original txid:

    import * as btc from 'micro-btc-signer';
    import { hex } from '@scure/base';
    
    const tx = new btc.Transaction();
    
    tx.addInput({
      hash: hex.decode('c061c23190ed3370ad5206769651eaf6fac6d87d85b5db34e30a74e0c4a6da3e').reverse(),
      index: 0,
    });
    
    console.log(hex.encode(tx.unsignedTx));
    // 0200000001c061c23190ed3370ad5206769651eaf6fac6d87d85b5db34e30a74e0c4a6da3e0000000000ffffffff0000000000
    

    It produces transaction with reversed bytes one more time.

    Expected transaction hex is:

    // 02000000013edaa6c4e0740ae334dbb5857dd8c6faf6ea5196760652ad7033ed9031c261c00000000000ffffffff0000000000
    

    Some libraries treats string parameter as txid and converts it on the fly but Buffer/butes parameter keeps as is.

    When string is passed as hash:

    tx.addInput({
      hash: 'c061c23190ed3370ad5206769651eaf6fac6d87d85b5db34e30a74e0c4a6da3e',
      index: 0,
    });
    
    console.log(hex.encode(tx.unsignedTx));
    // 0200000001c061c23190ed3370ad5206769651eaf6fac6d87d85b5db34e30a74e0c4a6da3e0000000000ffffffff0000000000
    

    It produces the same wrong order of bytes as it is in original txid.

    opened by mahnunchik 1
  • Support Bitcoin scripts in Taproot constructs

    Support Bitcoin scripts in Taproot constructs

    hey @paulmillr! First things first, thank you for this lib and your work in general! I'm trying to use to use micro-btc-signer and noticed that - unless I missed something when using / reading this lib - there's a limitation in the current implementation, when crafting taproot constructs. Per the code, it looks like we can only create CHECKSIGVERIFY based multisigs constructs and I was wondering why. If I try to craft a set of conditions involving a HTLC, the lib is aborting my construct. Reproducer:

    const keypairFromSecret = (hexSecretKey: string): Keypair => {
        let secretKey = hex.decode(hexSecretKey);
        let schnorrPublicKey = secp.schnorr.getPublicKey(secretKey);
        return {
            schnorrPublicKey,
            secretKey,
        }
    }
    
    let aliceKeyPair = keypairFromSecret("2bd806c97f0e00af1a1fc3328fa763a9269723c8db8fac4f93af71db186d6e90");
    let bobKeyPair = keypairFromSecret("81b637d8fcd2c6da6359e6963113a1170de795e4b725b84d1e0b4cfd9ec58ce9");
    let internalKeyPair = keypairFromSecret("1229101a0fcf2104e8808dab35661134aa5903867d44deb73ce1c7e4eb925be8");
    let preimage = sha256(hex.decode("107661134f21fc7c02223d50ab9eb3600bc3ffc3712423a1e47bb1f9a9dbf55f"))
    
    let scriptAlice = new Uint8Array([0x02, 144, 0x00, btc.OP.CHECKSEQUENCEVERIFY, btc.OP.DROP, 0x20, ...aliceKeyPair.schnorrPublicKey, 0xac]);
    
    let scriptBob = new Uint8Array([btc.OP.SHA256, 0x20, ...preimage, btc.OP.EQUALVERIFY, 0x20, ...bobKeyPair.schnorrPublicKey, 0xac]);
    
    let taprootTree = btc.taprootListToTree([{
        script: scriptAlice,
        leafVersion: 0xc0
      },  {
        script: scriptBob,
        leafVersion: 0xc0 
      }
    ])
    
    let taprootCommitment = btc.p2tr(internalKeyPair.schnorrPublicKey, taprootTree);
    // Execution aborted with
    // Error: Reader(): Error: OutScript.encode/tr_ns: wrong element
    

    I could be miss using the lib, please let me know if I'm missing something. Thanks!

    opened by lgalabru 0
Releases(0.1.2)
Owner
Paul Miller
Paul Miller
CLI utility that broadcasts BTC, ETH, SOL, ZEC & XMR transactions through TOR using public block explorers

tx-tor-broadcaster CLI utility that broadcasts BTC, ETH, SOL, ZEC & XMR transactions through TOR using public block explorers. Provides a great degree

Paul Miller 58 Dec 25, 2022
A plugin for Strapi Headless CMS that provides ability to sign-in/sign-up to an application by link had sent to email.

Strapi PasswordLess Plugin A plugin for Strapi Headless CMS that provides ability to sign-in/sign-up to an application by link had sent to email. A pl

Andrey Kucherenko 51 Dec 12, 2022
A comprehensive user sign-up/sign-in system

Nodejs Login System You want a comprehensive user sign-up/sign-in system I strongly suggest you take a look at this repo. The System Includes Welcome

Furkan Gulsen 8 Aug 1, 2022
MultiSafe is a shared crypto wallet for managing Stacks (STX) and Bitcoin (BTC).

MultiSafe MultiSafe is a shared crypto wallet for managing Stacks (STX) and Bitcoin (BTC). Deploy a MultiSafe https://app.multisafe.xyz/ Features Curr

Trust Machines 22 Dec 26, 2022
Trustless BTC-ETH exchange.

# Silver Portal ⚠️ This is an experimental prototype for testnet use only. The basic idea Silver Portal lets you swap ether and bitcoin, trustlessly.

DC 24 Sep 2, 2022
✨ Properly credit deps and assets in your projects in a pretty way.

Acknowledgements ✨ Properly credit deps and assets in your projects in a pretty way. About Acknowledgments is a CLI tool that generates a JSON file wi

Clembs 5 Sep 1, 2022
Minimum project with Hono.

Hono minimum project This is a minimum project with Hono[炎]. Features Minimum TypeScript Esbuild to build miniflare 2.x to develop Wrangler 2.0 Beta t

Yusuke Wada 60 Dec 25, 2022
💀 A bare-minimum solution to solve CORS problem via proxy API

?? CORS Hijacker A bare-minimum solution to solve CORS problem via proxy API Base URL https://cors-hijacker.vercel.app/ Get Get Basic HTML Endpoint: $

Irfan Maulana 35 Nov 4, 2022
Benefit cards API, create and store card data and log transactions

Valex ?? Benefit cards for companies and employees! ?? Tech used Overview An API to store benefit cards from companies to employees and log transactio

Felipe Ventura 2 Apr 25, 2022
Lucid is a library, which allows you to create Cardano transactions and off-chain code for your Plutus contracts in JavaScript and Node.js.

Lucid is a library, which allows you to create Cardano transactions and off-chain code for your Plutus contracts in JavaScript and Node.js.

Berry 243 Jan 8, 2023