bitcoinjs / bitcoinjs-lib

A javascript Bitcoin library for node.js and browsers.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

can not sign input with my keypair

MovsesDev opened this issue · comments

import { BuildTransactionOptions } from './common';
import { getEnvConfig } from 'src/config';
import * as bitcoin from 'bitcoinjs-lib';
import { Network } from 'bitcoinjs-lib';
import ECPairFactory from 'ecpair';
import * as ecc from '@bitcoin-js/tiny-secp256k1-asmjs';
const ECPair = ECPairFactory(ecc);
import axios, { Axios } from 'axios';
import pLimit from 'p-limit';
import BigNumber from 'bignumber.js';
import { FeeType } from 'src/store/settings/settings.slice';
import { bitcoinApi } from 'src/api/bitcoin';
import { BlockchainProvider } from './interface/blockchainProvider';
import {
  EstimateEthereumFeeRequest,
  EstimateEthereumFeeResponse,
} from 'definitions/generated/wallet/tatum/ethereum/v1/ethereum';
import { EstimateTronFeeResponse } from 'definitions/generated/wallet/tatum/tron/v1/tron';
import * as assert from 'assert';

const blockStreamLimit = pLimit(3);

type IUtxo = any;

type IBitcoinNetworkFee = any;

type IUtxoBlockStreamResponse = any;

type BitcoinFeeRecomendedResponse = any;

type IBuildBitcoinTransactionDataArgs = {
  from: string;
  to: string;
  fee: FeeType;
  amount: string;
  feeValue: any;
};

type IBitcoinTransactionData = any;

const DEFAULT_OUTPUT_LENGTH = 2;
const INPUT_SIZE_BYTES = 148;
const OUTPUT_SIZE_BYTES = 32;
const ADDITIONAL_SIZE_BYTES = 10;

const sortUTXO = (a: IUtxo, b: IUtxo): number => {
  const aAmount = Number(a.satoshiAmount);
  const bAmount = Number(b.satoshiAmount);

  return bAmount - aAmount;
};

export class BitcoinService implements BlockchainProvider {
  private readonly blockStreamInstance: Axios;
  private readonly memPoolInstance: Axios;
  private readonly isTestnet: boolean;
  private readonly network: Network;

  constructor() {
    this.isTestnet = !getEnvConfig().isMainnet;
    this.network = this.isTestnet
      ? bitcoin.networks.testnet
      : bitcoin.networks.bitcoin;

    this.blockStreamInstance = axios.create({
      baseURL: `https://blockstream.info${
        this.isTestnet ? '/testnet' : ''
      }/api`,
    });

    this.memPoolInstance = axios.create({
      baseURL: 'https://mempool.space/api',
    });
  }

  getTransactions = (address: string) => {
    return bitcoinApi
      .getTransactions({ address })
      .then(res => res)
      .catch(() => {
        // Hack for backend not found error
        return {
          transactions: [],
        };
      });
  };

  getTransaction = (hash: string) => {
    return bitcoinApi.getTransaction({ hash });
  };

  async getBalance(address: string) {
    const res = await bitcoinApi.getBalance({ address });
    return res.result;
  }
  feeAdapter = (
    res: EstimateTronFeeResponse | EstimateEthereumFeeResponse,
    decimals: number,
  ) => {
    return {
      estimateFees: {
        slow: BigNumber(res.result.estimations.safe).dividedBy(100_000_000),
        standard: BigNumber(res.result.estimations.standard).dividedBy(
          100_000_000,
        ),
        fast: BigNumber(res.result.estimations.fast).dividedBy(100_000_000),
      },
    };
  };

  getContractData = (address: string) => {
    return bitcoinApi.getContractInfo({ address }).then(res => res.result);
  };

  estimateFee = (data: EstimateEthereumFeeRequest) => {
    console.log(data);
    return bitcoinApi.estimateFee({
      // ...data,
      // to: data.contractAddress === 'native' ? data.to : data.contractAddress,
      contractAddress:
        data.contractAddress === 'native' ? undefined : data.contractAddress,
      amount: data.amount,
      from: data.from,
    });
  };

  public async sendTransaction(txOptions: BuildTransactionOptions, pk: string) {
    try {
      const estimatedFee = await this.estimateFee({
        to: txOptions.to,
        from: txOptions.from,
        amount: txOptions.value,
      });

      const feeValue = this.feeAdapter(estimatedFee, 8).estimateFees[
        txOptions.feeType
      ];
      const txBuildParams = await this.buildTransactionParams({
        from: txOptions.from,
        to: txOptions.to,
        fee: txOptions.feeType,
        amount: txOptions.value,
        feeValue,
      });
      console.log('txOptions1', txOptions);
      console.log('txOptions2', txBuildParams);

      console.log('feeValue', Number(feeValue));
      console.log('feeValue', feeValue);

      const psbt = new bitcoin.Psbt({ network: this.network });
      console.log('index', txBuildParams.utxo[0].index);

      psbt.addInput({
        hash: txBuildParams.utxo[0].txId,
        index: txBuildParams.utxo[0].index,
        witnessUtxo: {
          script: Buffer.from(txBuildParams.scriptPubKey, 'hex'),
          value: Number(this.toSatoshi(txBuildParams.total)),
        },
      });
      psbt.addOutput({
        address: txBuildParams.to,
        value: Number(
          this.toSatoshi(BigNumber(txBuildParams.total).minus(feeValue)),
        ),
      });
      console.log(
        'value',
        Number(this.toSatoshi(BigNumber(txBuildParams.total).minus(feeValue))),
      );

      console.log('keyPair', pk);
      const keyPair = ECPair.fromPrivateKey(Buffer.from(pk, 'hex'), {
        network: this.network,
      });
      console.log('keyPair', keyPair);

      psbt.signInput(0, keyPair);
      psbt.finalizeAllInputs();
      const tx = psbt.extractTransaction();
      return bitcoinApi.broadcast({ data: tx.toHex() });
    } catch (e) {
      console.log('AAA', e);
    }
  }

  public async buildTransactionParams({
    from,
    to,
    amount,
    fee = 'slow',
    feeValue,
  }: IBuildBitcoinTransactionDataArgs): Promise<IBitcoinTransactionData> {
    const allUtxo = await this.getAllAddressUtxos(from);
    const rawTxs = await this.getTXS(from);

    return {
      from,
      to,
      scriptPubKey: rawTxs[0].vin[0].prevout.scriptpubkey,
      ...this.estimateTransactionFeeByUtxo(allUtxo, amount, feeValue),
    };
  }

  private estimateTransactionFeeByUtxo(
    utxo: IUtxo[],
    amount: string,
    feeValue: BigNumber,
  ) {
    const value = new BigNumber(this.toSatoshi(amount));

    const result = utxo.reduce(
      (acc, item) => {
        const { total, inputLength } = acc;

        // const currentSatoshiFee = this.calculateFee({ inputLength, feeRate });
        // acc.currentSatoshiFee = currentSatoshiFee;

        if (total.lt(value.plus(feeValue))) {
          acc.utxo.push(item);
          acc.total = total.plus(item.satoshiAmount);
          acc.inputLength = inputLength + 1;
        }

        return acc;
      },
      {
        utxo: [] as IUtxo[],
        total: new BigNumber(0),
        inputLength: 1,
        currentSatoshiFee: new BigNumber(0),
      },
    );

    if (result.total.lt(value.plus(result.currentSatoshiFee))) {
      const unspent = this.toBtc(result.total);
      const need = this.toBtc(value.plus(result.currentSatoshiFee));

      throw new Error(
        `INSUFFICIENT_BALANCE. Unspent - ${unspent}, need - ${need}`,
      );
    }

    return {
      utxo: result.utxo,
      total: this.toBtc(result.total),
      fee: this.toBtc(result.currentSatoshiFee),
      amount,
    };
  }

  public calculateFee(args: {
    inputLength: number;
    feeRate: BigNumber;
    outputLength?: number;
  }): BigNumber {
    const { inputLength, feeRate, outputLength = DEFAULT_OUTPUT_LENGTH } = args;

    const inputSize = new BigNumber(inputLength).multipliedBy(INPUT_SIZE_BYTES);
    const outputSize = new BigNumber(outputLength).multipliedBy(
      OUTPUT_SIZE_BYTES,
    );
    const otherBytes = new BigNumber(ADDITIONAL_SIZE_BYTES).plus(inputLength);

    return inputSize.plus(outputSize).plus(otherBytes).multipliedBy(feeRate);
  }

  private async getAllAddressUtxos(from: string): Promise<IUtxo[]> {
    const rawUtxo = await this.getUTXO(from);

    console.log('rawutxo', rawUtxo);

    if (!rawUtxo.length) {
      throw new Error('Insufficent balance');
    }

    return rawUtxo
      .map(item => ({
        txId: item.txid,
        satoshiAmount: new BigNumber(item.value).toString(),
        index: item.vout,
      }))
      .sort(sortUTXO);
  }

  public async getNetworkFee() {
    return this.memPoolInstance
      .get<BitcoinFeeRecomendedResponse>('/v1/fees/recommended')
      .then(r => ({
        slow: r.data.economyFee,
        standard: r.data.hourFee,
        fast: r.data.fastestFee,
      }));
  }

  public getUTXO(address: string): Promise<any[]> {
    return blockStreamLimit(() =>
      this.blockStreamInstance
        .get<any[]>(`/address/${address}/utxo`)
        .then(r => r.data),
    );
  }

  public getTXS(address: string): Promise<IUtxoBlockStreamResponse[]> {
    return blockStreamLimit(() =>
      this.blockStreamInstance
        .get<IUtxoBlockStreamResponse[]>(`/address/${address}/txs`)
        .then(r => r.data),
    );
  }

  public toBtc(val: string | number | BigNumber): string {
    return new BigNumber(val).dividedBy(100_000_000).toString();
  }

  public toSatoshi(val: string | number | BigNumber): string {
    return new BigNumber(val).multipliedBy(100_000_000).toString();
  }
}

export default new BitcoinService();

txOptions.from and pk are not referring to the same keypair.

Actually: scriptPubKey: rawTxs[0].vin[0].prevout.scriptpubkey, this is the part that's wrong.

You are assuming that the first input of the first tx returned by the API MUST be the same key.

There is no guarantee that every input and every output of the transactions returned by the txs endpoint are the same key.

so what should i do?

what value has to be for scriptPubKey?

The output script associated with the from address.