Giter VIP home page Giter VIP logo

scure-btc-signer's Introduction

scure-btc-signer

Audited & minimal library for creating, signing & decoding Bitcoin transactions.

  • ๐Ÿ”’ Audited by an independent security firm
  • โœ๏ธ Create transactions, inputs, outputs, sign them
  • ๐Ÿ“ก No network code: simplified audits and offline usage
  • ๐Ÿ”€ UTXO selection with different strategies
  • ๐ŸŽป Classic & SegWit: P2PK, P2PKH, P2WPKH, P2SH, P2WSH, P2MS
  • ๐Ÿงช Schnorr & Taproot BIP340/BIP341: P2TR, P2TR-NS, P2TR-MS
  • ๐Ÿ“จ BIP174 PSBT
  • ๐Ÿ—ณ๏ธ Easy ordinals and inscriptions
  • ๐Ÿชถ 3300 lines

Initial development has been funded by Ryan Shea.

For discussions, questions and support, visit GitHub Discussions section of the repository.

Check out all web3 utility libraries: ETH, BTC, SOL, ordinals

This library belongs to scure

scure โ€” audited micro-libraries.

Usage

npm install @scure/btc-signer

We support all major platforms and runtimes. For Deno, ensure to use npm specifier. For React Native, you may need a polyfill for crypto.getRandomValues.

import * as btc from '@scure/btc-signer';
// import * as btc from "npm:@scure/[email protected]"; // Deno

Payments

BTC has several UTXO types:

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

For test examples, the usage is as following:

npm install @scure/btc-signer @scure/base assert
import * as btc from '@scure/btc-signer';
import { hex } from '@scure/base';
import { deepStrictEqual, throws } from 'assert';

P2PK (Pay To Public Key)

Legacy script, doesn't have an address. Must be wrapped in P2SH / P2WSH / P2SH-P2WSH. Not recommended.

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

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

P2PKH (Public Key Hash)

Classic (pre-SegWit) address.

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)

SegWit V0 version of P2PKH. Basic bech32 address. Can't 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 advanced use-cases. Consumes full output of other payments โ€” NOT only script.

Required tx input fields to make it spendable: redeemScript

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)

SegWit V0 version of P2SH.

Required tx input fields to make it spendable: witnessScript

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.

Required tx input fields to make it spendable: redeemScript, witnessScript

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 / segwit (pre-taproot) M-of-N Multisig. Doesn't have an address, must be wrapped in P2SH / P2WSH / P2SH-P2WSH.

Duplicate public keys are not accepted to reduce mistakes. Use flag allowSamePubkeys to override the behavior, for cases like 2-of-[A,A,B,C], which can be signed by A or (B and C).

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.

Consumes 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.

Required tx input fields to make it spendable: tapInternalKey, tapMerkleRoot, tapLeafScript

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'),
});

const clean = (x) => ({ type: x.type, address: x.address, script: hex.encode(x.script) });

const PubKey2 = hex.decode('0202020202020202020202020202020202020202020202020202020202020202');
const PubKey3 = hex.decode('1212121212121212121212121212121212121212121212121212121212121212');
// Nested P2TR, owner of private key for any of PubKeys can spend whole
// 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(
  clean(btc.p2tr(undefined, [btc.p2tr_pk(PubKey), btc.p2tr_pk(PubKey2), btc.p2tr_pk(PubKey3)])),
  {
    type: 'tr',
    // weights for bitcoinjs-lib: [3,2,1]
    address: 'bc1pj2uvajyygyu2zw0rg0d6yxdsc920kzc5pamfgtlqepe30za922cqjjmkta',
    script: '512092b8cec8844138a139e343dba219b0c154fb0b140f76942fe0c873178ba552b0',
  }
);
// If scriptsTree is already binary tree, it will be used as-is
deepStrictEqual(
  clean(btc.p2tr(undefined, [btc.p2tr_pk(PubKey2), [btc.p2tr_pk(PubKey), btc.p2tr_pk(PubKey3)]])),
  {
    type: 'tr',
    // default weights for bitcoinjs-lib
    address: 'bc1pvue6sk9efyvcvpzzqkg8at4qy2u67zj7rj5sfsy573m7alxavqjqucc26a',
    script: '51206733a858b9491986044205907eaea022b9af0a5e1ca904c094f477eefcdd6024',
  }
);

P2TR-NS (Taproot multisig)

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

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

Duplicate public keys are not accepted to reduce mistakes. Use flag allowSamePubkeys to override the behavior, for cases like 2-of-[A,A,B,C], which can be signed by A or (B and C).

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

// Simple 3-of-3 multisig
// Creates a single script that requires all three pubkeys: [PubKey, PubKey2, PubKey3]
deepStrictEqual(btc.p2tr_ns(3, [PubKey, PubKey2, PubKey3]), [
  {
    type: 'tr_ns',
    script: hex.decode(
      '200101010101010101010101010101010101010101010101010101010101010101ad200202020202020202020202020202020202020202020202020202020202020202ad201212121212121212121212121212121212121212121212121212121212121212ac'
    ),
  },
]);
// Simple 2-of-3 multisig
// If M (pubkeys required) is less than N (# of pubkeys), then multiple scripts are created: [[PubKey, PubKey2], [PubKey, PubKey3], [PubKey2, PubKey3]]
const clean = (x) => ({ type: x.type, address: x.address, script: hex.encode(x.script) });
deepStrictEqual(clean(btc.p2tr(undefined, btc.p2tr_ns(2, [PubKey, PubKey2, PubKey3]))), {
  type: 'tr',
  address: 'bc1pevfcmnkqqq09a4n0fs8c7mwlc6r4efqpvgyqpjvegllavgw235fq3kz7a0',
  script: '5120cb138dcec0001e5ed66f4c0f8f6ddfc6875ca401620800c99947ffd621ca8d12',
});

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

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

Duplicate public keys are not accepted to reduce mistakes. Use flag allowSamePubkeys to override the behavior, for cases like 2-of-[A,A,B,C], which can be signed by A or (B and C).

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'
  ),
});
// Creates a single script for [PubKey, PubKey2, PubKey3]
const clean = (x) => ({ type: x.type, address: x.address, script: hex.encode(x.script) });
deepStrictEqual(clean(btc.p2tr(undefined, btc.p2tr_ms(2, [PubKey, PubKey2, PubKey3]))), {
  type: 'tr',
  address: 'bc1p6m2xevckax9zucumnnyvu4xhxem66ugc5r2zlw2a20s0hxnutl8qfef23s',
  script: '5120d6d46cb316e98a2e639b9cc8ce54d73677ad7118a0d42fb95d53e0fb9a7c5fce',
});

P2TR-PK (Taproot single P2PK script)

Specific case of p2tr_ns(1, [pubkey]), which is the same as the BTC descriptor: tr($H,pk(PUBKEY))

const PubKey = hex.decode('0101010101010101010101010101010101010101010101010101010101010101');
// P2PK for taproot
const clean = (x) => ({ type: x.type, address: x.address, script: hex.encode(x.script) });
deepStrictEqual(clean(btc.p2tr(undefined, [btc.p2tr_pk(PubKey)])), {
  type: 'tr',
  address: 'bc1pfj6w68w3v2f4pkzesc9tsqfvy5znw5qgydwa832v3v83vjn76kdsmr4360',
  script: '51204cb4ed1dd1629350d859860ab8012c2505375008235dd3c54c8b0f164a7ed59b',
});

Transaction

Encode/decode

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

We strip 'unknown' keys inside PSBT, they needed for new version/features support, however any unsupported feature/new version can significantly break assumptions about code. If you have use-case where they are needed, create a github issue.

PSBTv2 features tx_modifiable and taproot+bip32 are not supported yet.

// 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

We have txid (BE) instead of hash (LE) in transactions. We can support both, but txid is consistent across block explorers, while some explorers treat hash as txid - so hash is not consistent.

Use getInput and inputsLength to read information about inputs: they return a copy. This is necessary to avoid accidental modification of internal structures without calling methods (addInput/updateInput) that will verify correctness.

type TransactionInput = {
  txid?: 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({ txid: new Uint8Array(32), index: 0 });
deepStrictEqual(tx.inputs[0], {
  txid: new Uint8Array(32),
  index: 0,
  sequence: btc.DEFAULT_SEQUENCE,
});
// Update basic value
tx.updateInput(0, { index: 10 });
deepStrictEqual(tx.inputs[0], {
  txid: new Uint8Array(32),
  index: 10,
  sequence: btc.DEFAULT_SEQUENCE,
});
// Add value as hex
tx.addInput({
  txid: '0000000000000000000000000000000000000000000000000000000000000000',
  index: 0,
});
deepStrictEqual(tx.inputs[2], {
  txid: 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], {
  txid: new Uint8Array(32),
  index: 10,
  sequence: btc.DEFAULT_SEQUENCE,
});

// Read inputs
for (let i = 0; i < tx.inputsLength; i++) {
  console.log('I', tx.getInput(i));
}

Outputs

addOutputAddress uses bigint amounts, which means satoshis - NOT btc. If you need btc representation, use Decimal:

const amountSatoshi = btc.Decimal.decode('1.5'); // 1.5 btc in satoshi

Use getOutput and outputsLength to read outputs information. This methods returns copy of output, instead of internal representation. This is necessary to avoid accidental modification of internal structures without calling methods (addOutput/updateOutput) that will verify correctness.

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: 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,
});

// Read outputs
for (let i = 0; i < tx.outputsLength; i++) {
  console.log('O', tx.getOutput(i));
}

Basic transaction sign

const privKey = hex.decode('0101010101010101010101010101010101010101010101010101010101010101');
const txP2WPKH = new btc.Transaction();
for (const inp of TX_TEST_INPUTS) {
  txP2WPKH.addInput({
    txid: inp.txid,
    index: inp.index,
    witnessUtxo: {
      amount: inp.amount,
      script: btc.p2wpkh(secp256k1.getPublicKey(privKey, true)).script,
    },
  });
}
for (const [address, amount] of TX_TEST_OUTPUTS) txP2WPKH.addOutputAddress(address, amount);
deepStrictEqual(hex.encode(txP2WPKH.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();
// A creator creating a PSBT for a transaction which creates the following outputs:
tx.addOutput({
  script: '0014d85c2b71d0060b09c9886aeb815e50991dda124d',
  amount: btc.Decimal.decode('1.49990000'),
});
tx.addOutput({
  script: '001400aea9a2e5f0f876a588df5546e8742d1d87008f',
  amount: btc.Decimal.decode('1.00000000'),
});
// and spends the following inputs:
tx.addInput({
  txid: '75ddabb27b8845f5247975c8a5ba7c6f336c4570708ebe230caf6db5217ae858',
  index: 0,
});
tx.addInput({
  txid: '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.SigHash.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')
*/
// 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'
  )
);

UTXO selection

UTXO selection is the process of choosing which UTXOs to use as inputs when making an on-chain bitcoin payment. The library:

  • can create tx, integrated with the signer
  • ensures change address is always specified
  • supports bip69
  • supports segwit + taproot
  • calculates weight with good precision
  • implements multiple strategies

Taproot estimation is precise, but you have to pass sighash if you want to use non-default one, because it changes signature size. For complex taproot trees you need to filter tapLeafScript to include only leafs which you can sign we estimate size with smallest leaf (same as finalization), but in specific case keys for this leaf can be unavailable (complex multisig)

Oldest / Newest expects UTXO provided in historical order (oldest first), otherwise we have no way to detect age of tx.

Strategies

Strategy selection is complicated. Best should be: exactBiggest/accumSmallest.

exactBiggest/accumBiggest creates tx with smallest fees, but it breaks big outputs to small ones, which in the end will create a lot of outputs close to dust.

  • default: good for privacy, same as exactBiggest/accumBiggest
  • all: send all coins to change address (consolidation)
  • accum: accumulates inputs until the target value (+fees) is reached, skipping detrimental inputs
  • exact: accumulates inputs until the target value (+fees) is matched, does not accumulate inputs that go over the target value (within a threshold)
  • accumNewest
  • accumOldest
  • accumSmallest
  • accumBiggest
  • exactNewest/accumNewest
  • exactNewest/accumOldest
  • exactNewest/accumSmallest
  • exactNewest/accumBiggest
  • exactOldest/accumNewest
  • exactOldest/accumOldest
  • exactOldest/accumSmallest
  • exactOldest/accumBiggest
  • exactSmallest/accumNewest
  • exactSmallest/accumOldest
  • exactSmallest/accumSmallest
  • exactSmallest/accumBiggest
  • exactBiggest/accumNewest
  • exactBiggest/accumOldest
  • exactBiggest/accumSmallest
  • exactBiggest/accumBiggest

Example

const privKey = hex.decode('0101010101010101010101010101010101010101010101010101010101010101');
const pubKey = secp256k1.getPublicKey(privKey, true);
const spend = btc.p2wpkh(pubKey, regtest);
const utxo = [
  {
    ...spend, // add witness/redeem scripts from spend
    // Get txid, index from explorer/network
    txid: hex.decode('0af50a00a22f74ece24c12cd667c290d3a35d48124a69f4082700589172a3aa2'),
    index: 0,
    // utxo tx information
    // script can be used from spend itself or from explorer
    witnessUtxo: { script: spend.script, amount: 100_000n }, // value in satoshi
  },
  {
    ...spend,
    txid: hex.decode('0af50a00a22f74ece24c12cd667c290d3a35d48124a69f4082700589172a3aa2'),
    index: 1,
    witnessUtxo: { script: spend.script, amount: btc.Decimal.decode('1.5') }, // value in btc
  },
  // {
  //   ...spend,
  //   txid: hex.decode('75ddabb27b8845f5247975c8a5ba7c6f336c4570708ebe230caf6db5217ae858'),
  //   index: 0,
  //   // tx hex from blockchain (required for non-SegWit UTXO)
  //   nonWitnessUtxo: hex.decode(
  //     '0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f618765000000'
  //   ),
  // },
];
const outputs = [
  { address: '2MvpbAgedBzJUBZWesDwdM7p3FEkBEwq3n3', amount: 50_000n }, // amount in satoshi
  {
    address: 'bcrt1pw53jtgez0wf69n06fchp0ctk48620zdscnrj8heh86wykp9mv20q7vd3gm',
    amount: btc.Decimal.decode('0.5'), // amount in btc
  },
];
// Send all utxo to specific address (consolidation):
// const selected = btc.selectUTXO(utxo, [], 'all', {
//   changeAddress: 'bcrt1pea3850rzre54e53eh7suwmrwc66un6nmu9npd7eqrhd6g4lh8uqsxcxln8', ...
const selected = btc.selectUTXO(utxo, outputs, 'default', {
  changeAddress: 'bcrt1pea3850rzre54e53eh7suwmrwc66un6nmu9npd7eqrhd6g4lh8uqsxcxln8', // required, address to send change
  feePerByte: 2n, // require, fee per vbyte in satoshi
  bip69: true, // lexicographical Indexing of Transaction Inputs and Outputs
  createTx: true, // create tx with selected inputs/outputs
  network: regtest,
});
// NOTE: 'selected' will 'undefined' if there is not enough funds
deepStrictEqual(selected.fee, 394n); // estimated fee
deepStrictEqual(selected.change, true); // change address used
deepStrictEqual(selected.outputs, [
  { address: '2MvpbAgedBzJUBZWesDwdM7p3FEkBEwq3n3', amount: 50000n },
  {
    address: 'bcrt1pw53jtgez0wf69n06fchp0ctk48620zdscnrj8heh86wykp9mv20q7vd3gm',
    amount: 50_000_000n,
  },
  // Change address
  // NOTE: with bip69 it is not necessarily last item in outputs
  {
    address: 'bcrt1pea3850rzre54e53eh7suwmrwc66un6nmu9npd7eqrhd6g4lh8uqsxcxln8',
    amount: 99_949_606n,
  },
]);
// No need to create tx manually!
const { tx } = selected;
tx.sign(privKey);
tx.finalize();
deepStrictEqual(tx.id, 'b702078d65edd65a84b2a97a669df5631b06f42a67b0d7090e540b02cc65aed5');
// real tx fee, can be bigger than estimated, since we expect signatures of maximal size
deepStrictEqual(tx.fee, 394n);

Ordinals and custom scripts

We support custom scripts. You can pass it as last argument to p2tr.

Ordinals, inscriptions, runes and others are handled by a separate package for now: micro-ordinals.

The package contains:

  • Real code for ordinals / inscriptions
  • CLI tool that allows to upload files as inscriptions
  • Example usage of custom scripts

Utils

secp256k1 keys

import { pubSchnorr, signSchnorr } from '@scure/btc-signer/utils';
import { pubECDSA, signECDSA } from '@scure/btc-signer/utils';
import { randomPrivateKeyBytes } from '@scure/btc-signer/utils';

const priv = randomPrivateKeyBytes();
const pub = pubSchnorr(priv);

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 compressed 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'
);

Security

The library has been independently audited:

UTXO selection functionality has not been audited yet. Commit 58d4554 split the library from one into several files to ease future maintainability.

Supply chain security

  1. Commits are signed with PGP keys, to prevent forgery. Make sure to verify commit signatures.
  2. Releases are transparent and built on GitHub CI. Make sure to verify provenance logs
  3. Rare releasing is followed. The less often it is done, the less code dependents would need to audit
  4. Dependencies are minimal:
    • All deps are prevented from automatic updates and have locked-down version ranges. Every update is checked with npm-diff
    • Updates themselves are rare, to ensure rogue updates are not catched accidentally
    • noble-hashes provides hashing functionality
    • noble-curves provides elliptic curve cryptography
    • scure-base provides bech32 / base64
    • micro-packed provides binary encoding - it has not been audited
  5. devDependencies are only used if you want to contribute to the repo. They are disabled for end-users:
    • scure-bip32, micro-packed-debugger and micro-should are developed by the same author and follow identical security practices
    • prettier (linter), fast-check (property-based testing) and typescript are used for code quality, vector generation and ts compilation. The packages are big, which makes it hard to audit their source code thoroughly and fully

We consider infrastructure attacks like rogue NPM modules very important; that's why it's crucial to minimize the amount of 3rd-party dependencies & native bindings. If your app uses 500 dependencies, any dep could get hacked and you'll be downloading malware with every install. Our goal is to minimize this attack vector.

If you see anything unusual: investigate and report.

License

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

scure-btc-signer's People

Contributors

bordalix avatar dogebonker avatar kyranjamie avatar mahnunchik avatar omahs avatar paulmillr avatar ph101pp avatar shea256 avatar victorkirov avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

scure-btc-signer's Issues

Transaction inputs and outputs marked as private. Why?

Any good reason for inputs and outputs on Transaction being private?

Context:

I need to inspect the inputs and outputs of a PSBT that I receive from the outside.

If on VSCode I do:

const tx = btc.Transaction.fromPSBT(psbt);
console.log(tx.inputs); // generates error

I get the error:

Property 'inputs' is private and only accessible within class 'Transaction'

I can hack it by changing to

const tx: any = btc.Transaction.fromPSBT(psbt);
console.log(tx.inputs); // no error

but the question remains: any good reason for those being private?

How to addOutput for unspendable outputs?

Hi, I'm porting code from bitcoinjs to your library.

In bitcoinjs I can create an unspendable, OP_RETURN, output like this;

const data = globalThis.Buffer.from(stringy, 'utf8');
const embed = payments.embed({ data: [data] }); // import from bitcoinjs
psbt.addOutput({ script: embed.output, value: 0 });

Anything you can point me to figure the equivalent here greatly appreciated?

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!

Ordinal Script Encode

Could you provide me a example encode code for this script?

OP_FALSE
OP_IF
  OP_PUSH "ord"
  OP_PUSH 1
  OP_PUSH "text/plain;charset=utf-8"
  OP_PUSH 2
  OP_PUSH 0xff
  OP_PUSH 0
  OP_PUSH "Hello, world!"
OP_ENDIF

I can't find some commands from OP code list

import in node js throws error

Hey, I'm struggling to import the library into my node js application. I've tried both micro-btc-signer and the new @scure/btc-signer packages.

node -v v19.8.0

E.g. for @Scure library

npm install @scure/btc-signer
import btc from '@scure/btc-signer';

or trying dynamic imports;

const btc = await import('@scure/btc-signer');
or
const {TEST_NETWORK, NETWORK,Address,OutScript} = await import('@scure/btc-signer')

gives persistent error;

Error [ERR_REQUIRE_ESM]: require() of ES Module ../node_modules/micro-btc-signer/index.js from .../bridge-api/src/lib/bitcoin/rpc_transaction.ts not supported.

I imagine it's something misconfigured my end but I've have been bashing my head against it for several hours, so raising in case its a known issue?

Can't loop over tx outputs: Wrong input index=2 at Transaction.checkInputIdx

public static getFee(rawtx): bigint {
		let tx = btc.Transaction.fromPSBT(Buffer.from(rawtx, 'hex'))
		console.log(tx)
		let inputs = []
		for (let i = 0; i < tx.inputsLength; i++) {
			inputs.push(tx.getInput(i))
		}
		console.log('inputs', inputs)
		let outputs = []
		for (let j = 0; j < tx.outputsLength; j++) {
			console.log({ j, outputsLength: tx.outputsLength })
			try {
				// outputs.push(tx.getOutput(j))
				const o = tx.getOutput(j)
				outputs = [...outputs, o]
			} catch (e) {
				console.log('e)
			}
		}
		console.log('outputs', outputs)

		const sumInputs = inputs.reduce((sum, input) => sum + input.witnessUtxo.amount, BigInt(0))
		const sumOutputs = outputs.reduce((sum, output) => sum + output.amount, BigInt(0))
		console.log(sumInputs, sumOutputs)
		const fee = sumInputs - sumOutputs

		console.log({ fee })

		return BigInt(fee)
	}

Looping over transaction outputs fails some of the time even though the index is clearly inbounds.

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.

Unnecessary search for public key in witnessScript in `signIdx` function.

There is a part of code in the signIdx function that searches for the public key of the signer in the witnessScript and throws an error if the public key is not found:

else {
      // only compressed keys are supported for now
      const pubKey = u.pubECDSA(privateKey);
      // TODO: replace with explicit checks
      // Check if script has public key or its has inside
      let hasPubkey = false;
      const pubKeyHash = u.hash160(pubKey);
      for (const i of Script.decode(inputType.lastScript)) {
        if (isBytes(i) && (P.equalBytes(i, pubKey) || P.equalBytes(i, pubKeyHash)))
          hasPubkey = true;
      }
      if (!hasPubkey) throw new Error(`Input script doesn't have pubKey: ${inputType.lastScript}`);

Is this check really necessary? I have a witnessScript that only contains addresses and opcodes. The subscripts are standard PayToAddr subscripts, so there are no public keys in them.

Would it be possible to have this check removed, since it prevents the signing process for no good reason?

For context my input is a wsh-unknown type. Since the unknown type script can be arbitrary, I don't see a reason why it has to contain the public key of the signer.

Is there any support for custom scripts like Hash Time Lock Contracts?

Hi,

I would like to use this library to implement a Hash Time Lock Contract (HTLC). Although I have been able to lock an amount of BTC to the HTLC through a P2WSH transaction using the library, I am struggling to create the transaction that unlocks it.

For context the HTLC script hex is : 6376a9147b0bbdf24f26e0b06609b74344842b31a540e6f088ac6704748b320ab1750014a322914d649da49325b44c915af682da7a7bd43a

which decodes to :

script                                   |  stack 
-----------------------------------------+--------
OP_IF                                    | 
OP_DUP                                   | 
OP_HASH160                               | 
7b0bbdf24f26e0b06609b74344842b31a540e6f0 | 
OP_EQUALVERIFY                           | 
OP_CHECKSIG                              | 
OP_ELSE                                  | 
748b320a                                 | 
OP_CHECKLOCKTIMEVERIFY                   | 
OP_DROP                                  | 
0                                        | 
a322914d649da49325b44c915af682da7a7bd43a | 

So as you can see the first branch OP_IF consists of a P2PKH script for the recipient to redeem the locked amount, and the OP_ELSE branch consists of P2WPKH script for the sender to reclaim the locked amount after lock time expiry (OP_CHECKLOCKTIMEVERIFY ).

I am having issues reclaiming the BTC from the sender account. To reclaim this BTC you need to provide in the witness:
<senderSignature> <senderPublicKey> 0 <htlcScriptAsRawData> .

The issue I have is that I don't know where to place this 0 value using the library. This value is important as it activates the OP_ELSE branch of the HTLC script and needs to be at the top of the stack during the witness script execution.

Signing an input seems to take care of placing the <senderSignature> and <senderPublicKey> values into the final witness stack, but I can't seem to find any functionality in the library to include other values, like the 0 value in the input script. I cannot include this 0 value in the witnessScript field of the input because that field needs to be the original htlcScript , whose hash will be compared against the hash in the scriptPubKey, so I don't see any place where I can add this value to the witness stack.

Is that something that is considered too advanced and which the current version of the library does not support?

This is my code for reference:

export function createReclaimPsbt(reclaimerAddress: string, reclaimerPubKey: string,  htlcAddress: string, htlcAmount: bigint, htlcTimeout: bigint,  htlcScript: Buffer, htlcOutput: UTXO, network: any) {
    const htlcScriptHash = sha256(htlcScript);
    const scriptPubKey = btc.Script.encode(["OP_0", htlcScriptHash]); // the scriptPubKey for the HTLC UTXO we want to reclaim


    const lockTime = Number(htlcTimeout) + 1;
    const tx = new btc.Transaction({
      allowUnknownOutputs: true,
      lockTime : lockTime,  // the HTLC output can be reclaimed after the timeout 
      version: 0,
    });

    // input
    tx.addInput({
      txid: htlcOutput.txid,
      index: htlcOutput.vout,
      witnessUtxo: {
        script: scriptPubKey ,
        amount: BigInt(htlcAmount),
      },
      witnessScript : htlcScript,
      sequence: 0xfffffffe, // non-final sequence number needed to enable locktime
      sighashType: btc.SigHash.ALL,
    });

    const fee = 300n; // set the miner fee amount
    const reclaimedAmount = BigInt(htlcAmount) - fee;

    tx.addOutputAddress(reclaimerAddress, reclaimedAmount, network);  
    const psbt = tx.toPSBT(0);
    const psbtHex = hex.encode(psbt);

    console.log("txHex = " + tx.hex);
    console.log("psbtHex = " + psbtHex);
    const psbtB64 = base64.encode(psbt);
    return psbtB64;  
}

Supports React Native or Expo?

I am trying to include it into an Expo app, but I keep getting TypeError: Conversion from 'BigInt' to 'number' is not allowed even if I try to override the global object BigInt with big-integer library from npm

Taproot script path spend doesn't work as expected.

I'm writing a simple demo showing the usage of "Taproot script path spend".
(like this article A Guide to creating TapRoot Scripts with bitcoinjs-lib but using scure-btc-signer)

const internalPrivateKey = secp256k1.utils.randomPrivateKey()
const internalPublicKey = schnorr.getPublicKey(internalPrivateKey)

// pay-to-pubkey spend path
const script = btc.Script.encode([ internalPublicKey, 'CHECKSIG' ])
const scriptP2tr = btc.p2tr(internalPublicKey, { script }, btc.TEST_NETWORK, true)

// deposit some btc to scriptP2tr.address
// ...

// spend it
const tx = new btc.Transaction({ allowUnknowInput: true })
tx.addInput({
  txid: 'xxx',
  index: 0,
  witnessUtxo: {
    amount: 10000n,
    script: scriptP2tr.script,
  },
  tapLeafScript: scriptP2tr.tapLeafScript,
  sequence: 0xfffffffd,
})
tx.addOutputAddress(
  'tb1pxxx',
  9000n,
  btc.TEST_NETWORK,
)
tx.signIdx(internalPrivateKey, 0)
tx.finalize()
console.log('tx:', tx.id, tx.hex)

It throws with the error: "No taproot scripts signed".

The main issue is here:

scure-btc-signer/index.ts

Lines 2301 to 2309 in e9fdfbc

const { pubKey, privKey } = getTaprootKeys(
privateKey,
schnorrPub,
cb.internalKey,
P.EMPTY // Because we cannot have nested taproot tree
);
const pos = scriptDecoded.findIndex((i) => isBytes(i) && P.equalBytes(i, pubKey));
// Skip if there is no public key in tapLeafScript
if (pos === -1) continue;

getTaprootKeys function changes the privateKey and publicKey (tweaked with the empty merkleRoot because the schnorrPub and cb.internalKey are the same: internalPublicKey here). So the new tweaked publicKey doesn't match the one in the script (const script = btc.Script.encode([ internalPublicKey, 'CHECKSIG' ])) and it skips thinking there is no public key in tapLeafScript.

If I change the codes eliminating the tweak (getTaprootKeys function does here), keeping the privateKey and publicKey the internal one (internalPrivateKey and internalPublicKey), the demo codes work as expected.

My question is that why tweak the keys here if schnorrPub and cb.internalKey are the same? In my demo, they are the same, being internalPublicKey.

P2TR: invalid leaf script=tr

I have faced with the following error while running examples from the readme: Error: P2TR: invalid leaf script=tr

scure-btc-signer/README.md

Lines 283 to 293 in 26216a9

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'),
});

I'm not sure:

  • I'm wrong?
  • There is mistake in the readme
  • There is error in the source code

How to build custom scripts?

I'm trying to create a custom script that has some arbitrary data followed by a multisig address paying to one of two public keys, where; pubkey1 is user defined (either ecdsa or schnorr derived) and pubkey2 is a taproot (segwit v1).

The script is should end up something like;

pmnt = btc.p2tr_ms(1, [hex.decode(pubkey1), hex.decode(pubkey2)])
or
pmnt = btc.p2ms(1, [hex.decode(pubkey1), hex.decode(pubkey2)])

with the multi-sig wrapped in a custom script via a p2wsh address (or p2tr address?) e.g.

const wsh2 = {
    type: 'wsh',
    script: btc.Script.encode([<data>, 'DROP', pmnt.script])
}
const script = btc.p2wsh(wsh2, this.net);

My question is whether this is feasible - ie is this the correct way to compile custom scripts? And also which combinations of pubkeys and multi sig payment descriptors are currently possible / supported by scure btc-signer ?

Deduct fee from outputs or add option to send max

I want to have something like subtractfeefromamount in selectUTXO in the case I want to send the exact amount in the UTXOs currently it silently fails and returns undefined. Perhaps there is another way to build this but since I have to define the outputs ahead of time I cannot estimate the fees beforehand to subtract there.

Feature: return hash in P2Ret for hash based payments

It would be helpful to have original hash included in returned P2Ret for hash based payments: p2sh, p2pkh, p2wsh, p2wpkh.

type P2Ret = {
  type: string;
  script: Bytes;
  address?: string;
  // for p2sh, p2pkh, p2wsh, p2wpkh
  hash?: Bytes,
  redeemScript?: Bytes;
  witnessScript?: Bytes;
};

cjs exports

the library is esm only right now, we should update build to reflect the other libs so we have both mjs and js

Question about generating taproot addresses

Hey there,

wondering why these two methods yield different bech32m addresses?

    const script = btc.p2tr(xOnlyPubKey, undefined, net)
    const addr = btc.Address(net).encode({type: 'tr', pubkey: xOnlyPubKey})
    expect(script.address).equals(addr)

this test fails but we expected it to pass - how are the two different addresses connected ?

`addOutputAddress` doesn't validate address checksum

addOutputAddress method uses Address.decode which uses base58 instead base58check.

Checksum bytes stripped away.

In addition the same address is decoded multiple times.

scure-btc-signer/index.ts

Lines 1486 to 1499 in 98fd1c6

const data = base58.decode(address);
if (data.length !== 25) throw new Error('Invalid base58 address');
// Pay To Public Key Hash
if (data[0] === network.pubKeyHash) {
const bytes = base58.decode(address);
return { type: 'pkh', hash: bytes.slice(1, bytes.length - 4) };
} else if (data[0] === network.scriptHash) {
const bytes = base58.decode(address);
return {
type: 'sh',
hash: base58.decode(address).slice(1, bytes.length - 4),
};
}
throw new Error(`Invalid address prefix=${data[0]}`);

It seems address should be decoded the following way:

const data = base58check.decode(address);
if (data.length !== 21) throw new Error('Invalid base58 address');
// Pay To Public Key Hash
if (data[0] === network.pubKeyHash) {
  return { type: 'pkh', hash: data.slice(1) };
} else if (data[0] === network.scriptHash) {
  return {
    type: 'sh',
    hash: data.slice(1),
  };
}

Bug: incorrect weight estimation for wph, wsh, and tr transactions

Incorrect weight estimation for wph, wsh, and tr transactions.

import * as btc from '@scure/btc-signer';
import assert from 'assert/strict';
import bitcoinjs from 'bitcoinjs-lib';
import { hex } from '@scure/base';
import { secp256k1 } from '@noble/curves/secp256k1';

const privKey = hex.decode('0101010101010101010101010101010101010101010101010101010101010101');
const txP2WPKH = new btc.Transaction();
txP2WPKH.addInput({
  txid: '0101010101010101010101010101010101010101010101010101010101010101',
  index: 0,
  witnessUtxo: {
    amount: 1234567890n,
    script: btc.p2wpkh(secp256k1.getPublicKey(privKey, true)).script,
  },
});

txP2WPKH.addOutputAddress(btc.p2wpkh(secp256k1.getPublicKey(privKey, true)).address, 1n);

txP2WPKH.sign(privKey);
txP2WPKH.finalize();

// 274 !== 438
assert.equal(txP2WPKH.weight, bitcoinjs.Transaction.fromHex(txP2WPKH.hex).weight());

allowUnkowOutput not passed through fromRaw when calling addInput

Adding the third output of a tx with an unknown (op_return) output and unable because the tx options {allowUnknowOutput:true} are not passed through this line;

const PSBTInputCoder = P.validate(PSBTKeyMap(PSBTInput), (i) => {
   ..
   ..
    const tx = Transaction.fromRaw(RawTx.encode(i.nonWitnessUtxo));
   ..
   ..

causes

Uncaught (in promise) Error: Transaction/output: unknown output script type, there is a chance that input is unspendable. Pass allowUnkownScript=true, if you sure
    at _Transaction.normalizeOutput (index.ts:1984:13)
    at _Transaction.addOutput (index.ts:1994:28)
    at _Transaction.fromRaw (index.ts:1673:40)

Adding third output of dae5bf18ef1bbbfe153276335e3718f9ee91a91fc3f902e75b657a442d471b10

const txFromUtxo = btc.Transaction.fromRaw(hex.decode(utxo.tx.hex), {allowUnknowInput:true, allowUnknowOutput: true})
const outputToSpend = txFromUtxo.getOutput(utxo.vout)
const spendScr = btc.OutScript.decode(outputToSpend.script)
if (spendScr.type === 'wpkh') {
	const nextI:btc.TransactionInput = {
		txid: hex.decode(utxo.txid),
		index: utxo.vout,
		nonWitnessUtxo: utxo.tx.hex
	}
	transaction.addInput(nextI);
}

make preimageWitness* methods public

@paulmillr
Is there a good reason why these methods are private? We are building an offline hardware wallet where this library is used for transaction creation, and creating the hashes that the offline hardware will sign. This library will then updateInputs and finally submit transactions. Currently we are forced to import dynamically so typescript doesn't complain that we are using a private method. Would hate to maintain a fork just for this.

Support script type 'ms' to encode an Address

When calling btc.Address.encode() we are currently running into errors bc type ms throws the error, Unknown address type=ms. I attempt to resolve in our code in this PR: leather-io/extension#4060. However, I'm not sure it is the best way to handle it and would prefer it be part of your lib if possible. It would be great if all types were supported.

Issues with Deno

Merry Christmas to all,

im trying to use this library to handle tx using deno but im getting some errors when trying to decode an unsigned hex tx:

import * as btc from "npm:@scure/[email protected]";


export const decode = (tx: any) => {
    const decoded_tx = btc.Transaction.fromRaw(tx);

    console.log(decoded_tx);

};

const txToTest = "hex_tx_here";

decode(txToTest);

error:

error: Uncaught (in promise) TypeError: this.data.subarray is not a function
at Reader.bytes

Refactoring: duplicate isBytes function

Function isBytes is present in btc-signer module and in imported micro-packed module:

const isBytes = (b: unknown): b is Bytes => b instanceof Uint8Array;

https://github.com/paulmillr/micro-packed/blob/66999bf00f2054272299a9e06b68e4e861bced00/index.ts#L34

Both versions of function are used accross module:

Merging two PSBTs finalise fails with no partialSig

Hey there,

I'm merging a tx from two PSBTs with two inputs signed independently in two Electrum wallets.

I can see the decoded psbt contains final_scriptwitness for each input after signing.

However on merging and finalising I get the error Not enough partial sign;

    const txA = btc.Transaction.fromPSBT(base64.decode(quote.acceptPSBTAlice))
    const txB = btc.Transaction.fromPSBT(base64.decode(quote.acceptPSBTBob))
    const psbt = btc.PSBTCombine([txA.toPSBT(), txB.toPSBT()]);
    const txC = btc.Transaction.fromPSBT(psbt)
    txC.finalize()

Am i doing something wrong here? Or do I just need to update the inputs manually with the final_scriptwitness data e.g.

updateInput(idx, final_scriptwitness) ?

thanks in advance!

PS I'm leaving SIGHASHTYPE as default and am setting {allowUnknowInput: true, allowUnknowOutput: true} when creating the tx.

PSBTS for ordinals & fees

How can I create a PSBTS for inscribe ordinals (svg format) and send a fee to another address for that? (btc) ref

Taproot signing - tapBip32Derivation unsupported

Hi,

I am trying to sign a simple taproot PSBT transaction generated by sparrow wallet (one input, 2 outputs) and I hit this error:
if (input.tapBip32Derivation) throw new Error('tapBip32Derivation unsupported');

Is this planned to be implemented?

Thanks!

Support for Bitcoin Descriptors

There has been a rise in the adoption of Bitcoin descriptors across wallets. Bitcoin descriptors enable wallet interoperability amongst wallets that support scripts like multisig and miniscript. I'd love to see this package support descriptors in a minimalistic way. Some functionalities that will be useful include:

  • Importing a descriptor and parsing it
  • Generating addresses from descriptors
  • Ability to sign transactions using descriptors

Thank you for considering this feature!

Create OP_DATA for output

Looking at how ordinals that is popular on bitcoin, can we use this library to embed text and even file on OP_PUSHDATA1. Looking forward if this example can be shown in this libary.

Narrower return types than `P2Ret`

Currently P2Ret acts as a catch-all for the payment methods. It'd be more developer/typescript friendly to avoid the optional props, and return narrower payment-specific types.

For example, if I call the p2sh method, the compiler should know that redeemScript is defined. Usage of the library would be cleaner, in that fewer potentially undefined checks are needed.

(and thanks for all the great work Paul, this lib is a godsend)

Transaction.vsize is incorrect for non cloned transactions

Noticed when making ordinals transactions, all input and outputs are taproot.

// setup taproot tx
...
clone = tx.clone()
tx.sign()
tx.finalize()
console.log(tx.vzise) // A
clone.sign()
clone.finalize()
console.log(clone.vzise) // B

B is correct and is greater than A.

"there is a chance that input is unspendable" with Xverse wallet

I have been chasing this error I see in Xverse for 6 days:

    if (
      res.script &&
      !this.opts.allowUnknownOutputs &&
      OutScript.decode(res.script).type === 'unknown'
    ) {
      throw new Error(
        'Transaction/output: unknown output script type, there is a chance that input is unspendable. Pass allowUnknownOutputs=true, if you sure'
      );
    }

'Transaction/output: unknown output script type, there is a chance that input is unspendable. Pass allowUnknownOutputs=true, if you sure'

First, I'm so glad I found you! Google couldn't find this. Been pulling my hair out.

When I try to list a "rare sats" utxo for-sale on MagicEden.io, Xverse wallet (Chrome extension) tries to sign & finalize the PSBT but fails and pops the error above.
However, Xverse has no problem spending/transferring the problematic utxo to another Xverse address. Moreover that receiving Xverse wallet can list the rare sats utxo OK.

I searched here and found what might be the smoking gun:
#59
Thank you @radicleart

It seems an OP_RETURN output can trigger this error, and the transaction I'm having trouble with has an op_return output.
However, I am trying to list/spend one "normal" p2tr utxo, not the op_return.

So my best theory right now is that Xverse or btc-signer is reading ALL utxo's of my transaction and then choking when it hits the op_return.
Can anyone confirm if this is the case please?
Why would btc-signer need to read all outputs when Xverse is trying to spend just one normal utxo?

And if true, would it be an acceptable solution for btc-signer to quietly ignore op_return outputs, instead of throwing an Error?
How would Xverse pass allowUnknownOutputs=true? Maybe I can work with them, if there's an example.

Txn with the utxo's that fail when I try to list them for-sale on Magic Eden:
https://mempool.space/tx/20a3c79e7e421122036e0efafcf5414840b5295e7ae479e1af488d17f12d9734
(op_return is the last output)
Listing any of the 330 sats utxo's throws the error.

Xverse code that raises the btc-signer error "there is a chance that input is unspendable":
https://github.com/secretkeylabs/xverse-core/blob/1caa304fe67064b6a70c0292332b46e0f7c8a2b3/transactions/psbt.ts#L136

When I search the MagicEden discord server, I see two other users who have hit this error last month.
I also found a user suffering from this on Twitter/X, search: allowUnknownScript

I am willing to tip this project (or dev) if we can collaborate on a solution. Thank you

Custom Script Tapscript not working?

That's probably my fault;
Overall the library is way more straightforward and battery packed than other fragmented libraries, but I'm struggling to make a simple tapscript work (which seems pretty straightforward with bitcoinjs or cmd/tapscript, at the cost of fragmentation)...

I'm creating a tapscript with a custom script that will pay for another transaction on a meta protocol:

 const { publicKey } = getP2TRWallet()
  const taprootInternalKey = ecdsaPublicKeyToSchnorr(publicKey)
  const outputScript = btc.Script.encode([
    taprootInternalKey,
    btc.OP.CHECKSIG,
    btc.OP.OP_0,
    btc.OP.IF,
   randomId,
    btc.OP.ENDIF
  ])

  const taprootPayment = btc.p2tr(
    taprootInternalKey,
    [
      {
        script: outputScript,
        leafVersion: 192,
      }
    ],
    network,
    true
  )
  return taprootPayment

and then, I pay to taprootPayment.address. Of course. This work flawlessly, the problem only really happens when I create the transaction to spend from this tapscript, in which the input is:

tx.addInput({
  txid: hex.decode(utxo.txid),
  index: utxo.vout,
  witnessUtxo: {
    script: tapscriptCommitment.script,
    amount: BigInt(utxo.value)
  },
  ...tapscriptCommitment,
  sighashType: btc.SigHash.ALL,
  sequence: 0xfffffffd
})

And then I proceed to build the outputs etc, sign the transaction, and submit to the blockchain. ALL GOOD.

Except that the tapscript does not get embedded on the hex of the transaction, check here: https://mempool.space/testnet/tx/f8d7bf9143bb8f41f0934ee35791ae75ebe6ffa4bb23de3287b635ecd8d9eb8f (clicking on details, you can see that there's no P2TR tapscript)

My best bet, is that I'm creating the custom script wrong in some manner. Still getting used to the library to consolidate on my own projects.

Would appreciate any help you can provide @paulmillr ๐Ÿ™

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.