bitcoinjs / bitcoinjs-lib

A javascript Bitcoin library for node.js and browsers.
MIT License
5.71k stars 2.11k forks source link

No inputs were signed #2181

Closed Venegrad closed 3 days ago

Venegrad commented 4 days ago

Hi guys, ran into a problem I've been racking my brains about. I can't understand what is the reason for the error No inputs were signed ? I seem to do everything correctly, but I still get the error. Where did I go wrong.

I'm trying to send 0.0003 BTC from 1NgYkcaMXpjVbKmt43dhJSNrqnVFTojpZV to bc1qn0ja80zlajtsx0c774uvdcf9e984xev20pwfel.

import * as bitcoin from 'bitcoinjs-lib';
import { ECPairFactory } from 'ecpair';
import * as ecc from 'tiny-secp256k1';
import { rfetch } from '../rfetch';

const ECPair = ECPairFactory(ecc);

const BTC = async (toAddress, amountBTC, privateKeyHex, fromAddress) => {
  const satoshiPerBTC = 1e8;
  const amountSatoshis = Math.ceil(amountBTC * satoshiPerBTC);

  if (!fromAddress || !toAddress || !privateKeyHex) {
    throw new Error('Sender address, recipient address, or private key is missing.');
  }

  const privateKeyBuffer = Buffer.from(privateKeyHex, 'hex');
  const keyPair = ECPair.fromPrivateKey(privateKeyBuffer, { network: bitcoin.networks.bitcoin });

  const utxos = await fetchUTXOs(fromAddress);
  if (!utxos.length) throw new Error('No available UTXOs.');

  const totalAvailable = utxos.reduce((sum, utxo) => sum + utxo.value, 0);
  console.log('Total available balance (satoshis):', totalAvailable);

  const fee = calculateFee(utxos.length, 2); // 2 outputs: transfer + change
  const totalRequired = amountSatoshis + fee;

  if (totalRequired > totalAvailable) {
    throw new Error(`Insufficient funds: required ${totalRequired}, available ${totalAvailable}.`);
  }

  const rawTransactions = await Promise.all(
    utxos.map((utxo) => fetchRawTransaction(utxo.txid))
  );

  const psbt = new bitcoin.Psbt({ network: bitcoin.networks.bitcoin });

  utxos.forEach((utxo, index) => {
    const rawTx = rawTransactions[index];
    const tx = bitcoin.Transaction.fromHex(rawTx);
    const output = tx.outs[utxo.vout];
    const scriptType = classifyOutputScript(output.script);

    console.log(`Adding input: ${utxo.txid}:${utxo.vout} (${scriptType})`);

    if (scriptType === 'pubkeyhash') {
      psbt.addInput({
        hash: utxo.txid,
        index: utxo.vout,
        nonWitnessUtxo: Buffer.from(rawTx, 'hex'),
      });
    } else if (scriptType === 'witnesspubkeyhash') {
      psbt.addInput({
        hash: utxo.txid,
        index: utxo.vout,
        witnessUtxo: {
          script: output.script,
          value: utxo.value,
        },
      });
    } else {
      throw new Error(`Unsupported script type: ${scriptType}`);
    }
  });

  psbt.addOutput({ address: toAddress, value: amountSatoshis });

  const change = totalAvailable - amountSatoshis - fee;
  if (change > 0) {
    if (change < 546) {
      throw new Error('Change is below the minimum amount (546 satoshis).');
    }
    psbt.addOutput({ address: fromAddress, value: change });
  }

  psbt.signAllInputs(keyPair);
  psbt.validateSignaturesOfAllInputs();
  psbt.finalizeAllInputs();

  const rawTx = psbt.extractTransaction().toHex();
  console.log('Raw transaction:', rawTx);

  const broadcastResponse = await broadcastTransaction(rawTx);
  console.log('Broadcast response:', broadcastResponse);

  return broadcastResponse;
};

const fetchUTXOs = async (address) => {
  const url = `https://btcbook.nownodes.io/api/v2/utxo/${address}`;
  return await rfetch(url);
};

const fetchRawTransaction = async (txid) => {
  const url = `https://btcbook.nownodes.io/api/v2/tx/${txid}?includeHex=true`;
  return await rfetch(url).then((res) => res.hex);
};

const broadcastTransaction = async (rawTx) => {
  const url = 'https://btcbook.nownodes.io/api/v2/sendtx/';
  return await rfetch(url, { txHex: rawTx });
};

const classifyOutputScript = (script) => {
  try {
    if (bitcoin.payments.p2pkh({ output: script })) return 'pubkeyhash';
    if (bitcoin.payments.p2wpkh({ output: script })) return 'witnesspubkeyhash';
  } catch (e) {
    return 'unknown';
  }
};

const calculateFee = (inputCount, outputCount) => {
  const txSize = inputCount * 180 + outputCount * 34 + 10;
  const feeRate = 10; // Cost per byte (satoshis/byte)
  return txSize * feeRate;
};

export default BTC;
junderw commented 3 days ago

Everything looks fine, check if privateKeyHex and fromAddress match.