PHP WebShell

Текущая директория: /opt/BitGoJS/modules/sdk-api/src/v1

Просмотр файла: transactionBuilder.ts

/**
 * @hidden
 */

/**
 */
//
// TransactionBuilder
// A utility for building and signing transactions
//
// Copyright 2014, BitGo, Inc.  All Rights Reserved.
//

import { bip32 } from '@bitgo/utxo-lib';
import * as utxolib from '@bitgo/utxo-lib';
import _ from 'lodash';
import { VirtualSizes } from '@bitgo/unspents';
import debugLib = require('debug');
const debug = debugLib('bitgo:v1:txb');
import { common, getAddressP2PKH, getNetwork, sanitizeLegacyPath } from '@bitgo/sdk-core';
import { verifyAddress } from './verifyAddress';
import { tryPromise } from '../util';

interface BaseOutput {
  amount: number;
  travelInfo?: any;
}

interface AddressOutput extends BaseOutput {
  address: string;
}

interface ScriptOutput extends BaseOutput {
  script: Buffer;
}

type Output = AddressOutput | ScriptOutput;

interface BitGoUnspent {
  value: number;
  tx_hash: Buffer;
  tx_output_n: number;
}

//
// TransactionBuilder
// @params:
//   wallet:  a wallet object to send from
//   recipients: array of recipient objects and the amount to send to each e.g. [{address: '38BKDNZbPcLogvVbcx2ekJ9E6Vv94DqDqw', amount: 1500}, {address: '36eL8yQqCn1HMRmVFFo49t2PJ3pai8wQam', amount: 2000}]
//   fee: the fee to use with this transaction.  if not provided, a default, minimum fee will be used.
//   feeRate: the amount of fee per kilobyte - optional - specify either fee, feeRate, or feeTxConfirmTarget but not more than one
//   feeTxConfirmTarget: calculate the fees per kilobyte such that the transaction will be confirmed in this number of blocks
//   maxFeeRate: The maximum fee per kb to use in satoshis, for safety purposes when using dynamic fees
//   minConfirms: the minimum confirmations an output must have before spending
//   forceChangeAtEnd: force the change address to be the last output
//   changeAddress: specify the change address rather than generate a new one
//   noSplitChange: set to true to disable automatic change splitting for purposes of unspent management
//   targetWalletUnspents: specify a number of target unspents to maintain in the wallet (currently defaulted to 8 by the server)
//   validate: extra verification of the change addresses, which is always done server-side and is redundant client-side (defaults true)
//   minUnspentSize: The minimum size in satoshis of unspent to use (to prevent spending unspents worth less than fee added). Defaults to 0.
//   feeSingleKeySourceAddress: Use this single key address to pay fees
//   feeSingleKeyWIF: Use the address based on this private key to pay fees
//   unspentsFetchParams: Extra parameters to use for fetching unspents for this transaction
//   unspents: array of unspent objects to use while constructing the transaction instead of fetching from the API
exports.createTransaction = function (params) {
  const minConfirms = params.minConfirms || 0;
  const validate = params.validate === undefined ? true : params.validate;
  let recipients: { address: string; amount: number; script?: string; travelInfo?: any }[] = [];
  let opReturns: { message: string; amount: number }[] = [];
  let extraChangeAmounts: number[] = [];
  let estTxSize: number;
  let travelInfos;

  // Sanity check the arguments passed in
  if (
    !_.isObject(params.wallet) ||
    (params.fee && !_.isNumber(params.fee)) ||
    (params.feeRate && !_.isNumber(params.feeRate)) ||
    !_.isInteger(minConfirms) ||
    (params.forceChangeAtEnd && !_.isBoolean(params.forceChangeAtEnd)) ||
    (params.changeAddress && !_.isString(params.changeAddress)) ||
    (params.noSplitChange && !_.isBoolean(params.noSplitChange)) ||
    (params.targetWalletUnspents && !_.isInteger(params.targetWalletUnspents)) ||
    (validate && !_.isBoolean(validate)) ||
    (params.enforceMinConfirmsForChange && !_.isBoolean(params.enforceMinConfirmsForChange)) ||
    (params.minUnspentSize && !_.isNumber(params.minUnspentSize)) ||
    (params.maxFeeRate && !_.isNumber(params.maxFeeRate)) ||
    // this should be an array and its length must be at least 1
    (params.unspents && (!Array.isArray(params.unspents) || params.unspents.length < 1)) ||
    (params.feeTxConfirmTarget && !_.isInteger(params.feeTxConfirmTarget)) ||
    (params.instant && !_.isBoolean(params.instant)) ||
    (params.bitgoFee && !_.isObject(params.bitgoFee)) ||
    (params.unspentsFetchParams && !_.isObject(params.unspentsFetchParams))
  ) {
    throw new Error('invalid argument');
  }

  const bitgo = params.wallet.bitgo;
  const constants = bitgo.getConstants();
  const network = getNetwork(common.Environments[bitgo.getEnv()].network);

  // The user can specify a seperate, single-key wallet for the purposes of paying miner's fees
  // When creating a transaction this can be specified as an input address or the private key in WIF
  let feeSingleKeySourceAddress;
  let feeSingleKeyInputAmount = 0;
  if (params.feeSingleKeySourceAddress) {
    try {
      utxolib.address.fromBase58Check(params.feeSingleKeySourceAddress, network);
      feeSingleKeySourceAddress = params.feeSingleKeySourceAddress;
    } catch (e) {
      throw new Error('invalid bitcoin address: ' + params.feeSingleKeySourceAddress);
    }
  }

  if (params.feeSingleKeyWIF) {
    const feeSingleKey = utxolib.ECPair.fromWIF(params.feeSingleKeyWIF, network as utxolib.BitcoinJSNetwork);
    feeSingleKeySourceAddress = getAddressP2PKH(feeSingleKey);
    // If the user specifies both, check to make sure the feeSingleKeySourceAddress corresponds to the address of feeSingleKeyWIF
    if (params.feeSingleKeySourceAddress && params.feeSingleKeySourceAddress !== feeSingleKeySourceAddress) {
      throw new Error(
        'feeSingleKeySourceAddress: ' +
          params.feeSingleKeySourceAddress +
          ' did not correspond to address of feeSingleKeyWIF: ' +
          feeSingleKeySourceAddress
      );
    }
  }

  if (!_.isObject(params.recipients)) {
    throw new Error('recipients must be array of { address: abc, amount: 100000 } objects');
  }

  let feeParamsDefined = 0;
  if (!_.isUndefined(params.fee)) {
    feeParamsDefined++;
  }

  if (!_.isUndefined(params.feeRate)) {
    feeParamsDefined++;
  }

  if (!_.isUndefined(params.feeTxConfirmTarget)) {
    feeParamsDefined++;
  }

  if (feeParamsDefined > 1) {
    throw new Error('cannot specify more than one of fee, feeRate and feeTxConfirmTarget');
  }

  if (_.isUndefined(params.maxFeeRate)) {
    params.maxFeeRate = constants.maxFeeRate;
  }

  // Convert the old format of params.recipients (dictionary of address:amount) to new format: { destinationAddress, amount }
  if (!(params.recipients instanceof Array)) {
    recipients = [];
    Object.keys(params.recipients).forEach(function (destinationAddress) {
      const amount = params.recipients[destinationAddress];
      recipients.push({ address: destinationAddress, amount: amount });
    });
  } else {
    recipients = params.recipients;
  }

  if (params.opReturns) {
    if (!(params.opReturns instanceof Array)) {
      opReturns = [];
      Object.keys(params.opReturns).forEach(function (message) {
        const amount = params.opReturns[message];
        opReturns.push({ message, amount });
      });
    } else {
      opReturns = params.opReturns;
    }
  }

  if (recipients.length === 0 && opReturns.length === 0) {
    throw new Error('must have at least one recipient');
  }

  let fee = params.fee;
  let feeRate = params.feeRate;

  // Flag indicating whether this class will compute the fee
  const shouldComputeBestFee = _.isUndefined(fee);

  let totalOutputAmount = 0;

  recipients.forEach(function (recipient) {
    if (_.isString(recipient.address)) {
      if (!verifyAddress(recipient.address, network)) {
        throw new Error('invalid bitcoin address: ' + recipient.address);
      }
      if (!!recipient.script) {
        // A script was provided as well - validate that the address corresponds to that
        if (utxolib.address.toOutputScript(recipient.address, network).toString('hex') !== recipient.script) {
          throw new Error(
            'both script and address provided but they did not match: ' + recipient.address + ' ' + recipient.script
          );
        }
      }
    }
    if (!_.isInteger(recipient.amount) || recipient.amount < 0) {
      throw new Error('invalid amount for ' + recipient.address + ': ' + recipient.amount);
    }
    totalOutputAmount += recipient.amount;
  });

  opReturns.forEach(function (opReturn) {
    totalOutputAmount += opReturn.amount;
  });

  let bitgoFeeInfo = params.bitgoFee;
  if (bitgoFeeInfo && (!_.isInteger(bitgoFeeInfo.amount) || !_.isString(bitgoFeeInfo.address))) {
    throw new Error('invalid bitgoFeeInfo');
  }

  // The total amount needed for this transaction.
  let totalAmount = totalOutputAmount + (fee || 0);

  // The list of unspent transactions being used in this transaction.
  let unspents;

  // the total number of unspents on this wallet
  let totalUnspentsCount;

  // the number of unspents we fetched from the server, before filtering
  let fetchedUnspentsCount;

  // The list of unspent transactions being used with zero-confirmations
  let zeroConfUnspentTxIds;

  // The sum of the input values for this transaction.
  let inputAmount;

  let changeOutputs: Output[] = [];

  let containsUncompressedPublicKeys = false;

  // The transaction.
  let transaction = utxolib.bitgo.createTransactionBuilderForNetwork(network);

  const getBitGoFee = function () {
    return tryPromise(function () {
      if (bitgoFeeInfo) {
        return;
      }
      return params.wallet.getBitGoFee({ amount: totalOutputAmount, instant: params.instant }).then(function (result) {
        if (result && result.fee > 0) {
          bitgoFeeInfo = {
            amount: result.fee,
          };
        }
      });
    }).then(function () {
      if (bitgoFeeInfo && bitgoFeeInfo.amount > 0) {
        totalAmount += bitgoFeeInfo.amount;
      }
    });
  };

  const getBitGoFeeAddress = function () {
    return tryPromise(function () {
      // If we don't have bitgoFeeInfo, or address is already set, don't get a new one
      if (!bitgoFeeInfo || bitgoFeeInfo.address) {
        return;
      }
      return bitgo.getBitGoFeeAddress().then(function (result) {
        bitgoFeeInfo.address = result.address;
      });
    });
  };

  // Get a dynamic fee estimate from the BitGo server if feeTxConfirmTarget
  // is specified or if no fee-related params are specified
  const getDynamicFeeRateEstimate = function () {
    if (params.feeTxConfirmTarget || !feeParamsDefined) {
      return bitgo
        .estimateFee({
          numBlocks: params.feeTxConfirmTarget,
          maxFee: params.maxFeeRate,
          inputs: zeroConfUnspentTxIds,
          txSize: estTxSize,
          cpfpAware: true,
        })
        .then(function (result) {
          const estimatedFeeRate = result.cpfpFeePerKb;
          const minimum = params.instant
            ? Math.max(constants.minFeeRate, constants.minInstantFeeRate)
            : constants.minFeeRate;
          // 5 satoshis per byte
          // it is worth noting that the padding only applies when the threshold is crossed, but not when the delta is less than the padding
          const padding = 5000;
          if (estimatedFeeRate < minimum) {
            console.log(
              new Date() +
                ': Error when estimating fee for send from ' +
                params.wallet.id() +
                ', it was too low - ' +
                estimatedFeeRate
            );
            feeRate = minimum + padding;
          } else if (estimatedFeeRate > params.maxFeeRate) {
            feeRate = params.maxFeeRate - padding;
          } else {
            feeRate = estimatedFeeRate;
          }
          return feeRate;
        })
        .catch(function (e) {
          // sanity check failed on tx size
          if (_.includes(e.message, 'invalid txSize')) {
            return Promise.reject(e);
          } else {
            // couldn't estimate the fee, proceed using the default
            feeRate = constants.fallbackFeeRate;
            console.log('Error estimating fee for send from ' + params.wallet.id() + ': ' + e.message);
            return Promise.resolve();
          }
        });
    }
  };

  // Get the unspents for the sending wallet.
  const getUnspents = function () {
    if (params.unspents) {
      // we just wanna use custom unspents
      unspents = params.unspents;
      return;
    }

    // Get enough unspents for the requested amount
    const options = _.merge({}, params.unspentsFetchParams || {}, {
      target: totalAmount,
      minSize: params.minUnspentSize || 0,
      instant: params.instant, // insist on instant unspents only
      targetWalletUnspents: params.targetWalletUnspents,
    });
    if (params.instant) {
      options.instant = params.instant; // insist on instant unspents only
    }

    return params.wallet.unspentsPaged(options).then(function (results) {
      console.log(`Unspents fetched\n:  ${JSON.stringify(results, null, 2)}`);
      totalUnspentsCount = results.total;
      fetchedUnspentsCount = results.count;
      unspents = results.unspents.filter(function (u) {
        const confirms = u.confirmations || 0;
        if (!params.enforceMinConfirmsForChange && u.isChange) {
          return true;
        }
        return confirms >= minConfirms;
      });

      // abort early if there's no viable unspents, because it won't be possible to create the txn later
      if (unspents.length === 0) {
        throw Error('0 unspents available for transaction creation');
      }

      // create array of unconfirmed unspent ID strings of the form "txHash:outputIndex"
      zeroConfUnspentTxIds = _(results.unspents)
        .filter(function (u) {
          return !u.confirmations;
        })
        .map(function (u) {
          return u.tx_hash + ':' + u.tx_output_n;
        })
        .value();
      if (_.isEmpty(zeroConfUnspentTxIds)) {
        // we don't want to pass an empty array of inputs to the server, because it assumes if the
        // inputs arguments exists, it contains values
        zeroConfUnspentTxIds = undefined;
      }

      // For backwards compatibility, respect the old splitChangeSize=0 parameter
      if (!params.noSplitChange && params.splitChangeSize !== 0) {
        extraChangeAmounts = results.extraChangeAmounts || [];
      }
    });
  };

  // Get the unspents for the single key fee address
  let feeSingleKeyUnspents: BitGoUnspent[] = [];
  const getUnspentsForSingleKey = function () {
    if (feeSingleKeySourceAddress) {
      let feeTarget = 0.01e8;
      if (params.instant) {
        feeTarget += totalAmount * 0.001;
      }
      return bitgo
        .get(bitgo.url('/address/' + feeSingleKeySourceAddress + '/unspents?target=' + feeTarget))
        .then(function (response) {
          if (response.body.total <= 0) {
            throw new Error('No unspents available in single key fee source');
          }
          feeSingleKeyUnspents = response.body.unspents;
        });
    }
  };

  let minerFeeInfo: any = {};
  let txInfo: any = {};

  // Iterate unspents, sum the inputs, and save _inputs with the total
  // input amount and final list of inputs to use with the transaction.
  let feeSingleKeyUnspentsUsed: BitGoUnspent[] = [];

  const collectInputs = function () {
    if (!unspents.length) {
      throw new Error('no unspents available on wallet');
    }
    inputAmount = 0;

    // Calculate the cost of spending a single input, i.e. the smallest economical unspent value
    return tryPromise(function () {
      if (_.isNumber(params.feeRate) || _.isNumber(params.originalFeeRate)) {
        return !_.isUndefined(params.feeRate) ? params.feeRate : params.originalFeeRate;
      } else {
        return bitgo
          .estimateFee({
            numBlocks: params.feeTxConfirmTarget,
            maxFee: params.maxFeeRate,
          })
          .then(function (feeRateEstimate) {
            return feeRateEstimate.feePerKb;
          });
      }
    })
      .then(function (feeRate) {
        // Don't spend inputs that cannot pay for their own cost.
        let minInputValue = 0;
        if (_.isInteger(params.minUnspentSize)) {
          minInputValue = params.minUnspentSize;
        }

        let prunedUnspentCount = 0;
        const originalUnspentCount = unspents.length;
        unspents = _.filter(unspents, function (unspent) {
          const isSegwitInput = !!unspent.witnessScript;
          const currentInputSize = isSegwitInput ? VirtualSizes.txP2shP2wshInputSize : VirtualSizes.txP2shInputSize;
          const feeBasedMinInputValue = (feeRate * currentInputSize) / 1000;
          const currentMinInputValue = Math.max(minInputValue, feeBasedMinInputValue);
          if (currentMinInputValue > unspent.value) {
            // pruning unspent
            const pruneDetails = {
              generalMinInputValue: minInputValue,
              feeBasedMinInputValue,
              currentMinInputValue,
              feeRate,
              inputSize: currentInputSize,
              unspent: unspent,
            };
            debug(`pruning unspent: ${JSON.stringify(pruneDetails, null, 4)}`);
            prunedUnspentCount++;
            return false;
          }
          return true;
        });

        if (prunedUnspentCount > 0) {
          debug(`pruned ${prunedUnspentCount} out of ${originalUnspentCount} unspents`);
        }

        if (unspents.length === 0) {
          throw new Error('insufficient funds');
        }
        let segwitInputCount = 0;
        unspents.every(function (unspent) {
          if (unspent.witnessScript) {
            segwitInputCount++;
          }
          inputAmount += unspent.value;
          transaction.addInput(unspent.tx_hash, unspent.tx_output_n, 0xffffffff);

          return inputAmount < (feeSingleKeySourceAddress ? totalOutputAmount : totalAmount);
        });

        // if paying fees from an external single key wallet, add the inputs
        if (feeSingleKeySourceAddress) {
          // collect the amount used in the fee inputs so we can get change later
          feeSingleKeyInputAmount = 0;
          feeSingleKeyUnspentsUsed = [];
          feeSingleKeyUnspents.every(function (unspent) {
            feeSingleKeyInputAmount += unspent.value;
            inputAmount += unspent.value;
            transaction.addInput(unspent.tx_hash, unspent.tx_output_n);
            feeSingleKeyUnspentsUsed.push(unspent);
            // use the fee wallet to pay miner fees and potentially instant fees
            return feeSingleKeyInputAmount < fee + (bitgoFeeInfo ? bitgoFeeInfo.amount : 0);
          });
        }

        txInfo = {
          nP2shInputs: transaction.tx.ins.length - (feeSingleKeySourceAddress ? 1 : 0) - segwitInputCount,
          nP2shP2wshInputs: segwitInputCount,
          nP2pkhInputs: feeSingleKeySourceAddress ? 1 : 0,
          // add single key source address change
          nOutputs:
            recipients.length +
            1 + // recipients and change
            extraChangeAmounts.length + // extra change splitting
            (bitgoFeeInfo && bitgoFeeInfo.amount > 0 ? 1 : 0) + // add output for bitgo fee
            (feeSingleKeySourceAddress ? 1 : 0),
        };

        // As per the response of get unspents API, for v1 safe wallets redeemScript is returned
        // in the response in hex format
        containsUncompressedPublicKeys = unspents.some(
          (u) => u.redeemScript.length === 201 * 2 /* hex length is twice the length in bytes */
        );

        estTxSize = estimateTransactionSize({
          containsUncompressedPublicKeys,
          nP2shInputs: txInfo.nP2shInputs,
          nP2shP2wshInputs: txInfo.nP2shP2wshInputs,
          nP2pkhInputs: txInfo.nP2pkhInputs,
          nOutputs: txInfo.nOutputs,
        });
      })
      .then(getDynamicFeeRateEstimate)
      .then(function () {
        minerFeeInfo = exports.calculateMinerFeeInfo({
          bitgo: params.wallet.bitgo,
          containsUncompressedPublicKeys,
          feeRate: feeRate,
          nP2shInputs: txInfo.nP2shInputs,
          nP2shP2wshInputs: txInfo.nP2shP2wshInputs,
          nP2pkhInputs: txInfo.nP2pkhInputs,
          nOutputs: txInfo.nOutputs,
        });

        if (shouldComputeBestFee) {
          const approximateFee = minerFeeInfo.fee;
          const shouldRecurse = _.isUndefined(fee) || approximateFee > fee;
          fee = approximateFee;
          // Recompute totalAmount from scratch
          totalAmount = fee + totalOutputAmount;
          if (bitgoFeeInfo) {
            totalAmount += bitgoFeeInfo.amount;
          }
          if (shouldRecurse) {
            // if fee changed, re-collect inputs
            inputAmount = 0;
            transaction = utxolib.bitgo.createTransactionBuilderForNetwork(network);
            return collectInputs();
          }
        }

        const totalFee = fee + (bitgoFeeInfo ? bitgoFeeInfo.amount : 0);

        if (feeSingleKeySourceAddress) {
          const summedSingleKeyUnspents = _.sumBy(feeSingleKeyUnspents, 'value');
          if (totalFee > summedSingleKeyUnspents) {
            const err: any = new Error(
              'Insufficient fee amount available in single key fee source: ' + summedSingleKeyUnspents
            );
            err.result = {
              fee: fee,
              feeRate: feeRate,
              estimatedSize: minerFeeInfo.size,
              available: inputAmount,
              bitgoFee: bitgoFeeInfo,
              txInfo: txInfo,
            };
            return Promise.reject(err);
          }
        }

        if (inputAmount < (feeSingleKeySourceAddress ? totalOutputAmount : totalAmount)) {
          // The unspents we're using for inputs do not have sufficient value on them to
          // satisfy the user's requested spend amount. That may be because the wallet's balance
          // is simply too low, or it might be that the wallet's balance is sufficient but
          // we didn't fetch enough unspents. Too few unspents could result from the wallet
          // having many small unspents and we hit our limit on the number of inputs we can use
          // in a txn, or it might have been that the filters the user passed in (like minConfirms)
          // disqualified too many of the unspents
          let err;
          if (totalUnspentsCount === fetchedUnspentsCount) {
            // we fetched every unspent the wallet had, but it still wasn't enough
            err = new Error('Insufficient funds');
          } else {
            // we weren't able to fetch all the unspents on the wallet
            err = new Error(
              `Transaction size too large due to too many unspents. Can send only ${inputAmount} satoshis in this transaction`
            );
          }
          err.result = {
            fee: fee,
            feeRate: feeRate,
            estimatedSize: minerFeeInfo.size,
            available: inputAmount,
            bitgoFee: bitgoFeeInfo,
            txInfo: txInfo,
          };
          return Promise.reject(err);
        }
      });
  };

  // Add the outputs for this transaction.
  const collectOutputs = function () {
    if (minerFeeInfo.size >= 90000) {
      throw new Error('transaction too large: estimated size ' + minerFeeInfo.size + ' bytes');
    }

    const outputs: Output[] = [];

    recipients.forEach(function (recipient) {
      let script;
      if (_.isString(recipient.address)) {
        script = utxolib.address.toOutputScript(recipient.address, network);
      } else if (_.isObject(recipient.script)) {
        script = recipient.script;
      } else {
        throw new Error('neither recipient address nor script was provided');
      }

      // validate travelInfo if it exists
      let travelInfo;
      if (!_.isEmpty(recipient.travelInfo)) {
        travelInfo = recipient.travelInfo;
        // Better to avoid trouble now, before tx is created
        bitgo.travelRule().validateTravelInfo(travelInfo);
      }

      outputs.push({
        script: script,
        amount: recipient.amount,
        travelInfo: travelInfo,
      });
    });

    opReturns.forEach(function ({ message, amount }) {
      const script = utxolib.script.fromASM('OP_RETURN ' + Buffer.from(message).toString('hex'));
      outputs.push({ script, amount });
    });

    const getChangeOutputs = function (changeAmount: number): Output[] | Promise<Output[]> {
      if (changeAmount < 0) {
        throw new Error('negative change amount: ' + changeAmount);
      }

      const result: Output[] = [];
      // if we paid fees from a single key wallet, return the fee change first
      if (feeSingleKeySourceAddress) {
        const feeSingleKeyWalletChangeAmount =
          feeSingleKeyInputAmount - (fee + (bitgoFeeInfo ? bitgoFeeInfo.amount : 0));
        if (feeSingleKeyWalletChangeAmount >= constants.minOutputSize) {
          result.push({ address: feeSingleKeySourceAddress, amount: feeSingleKeyWalletChangeAmount });
          changeAmount = changeAmount - feeSingleKeyWalletChangeAmount;
        }
      }

      if (changeAmount < constants.minOutputSize) {
        // Give it to the miners
        return result;
      }

      if (params.wallet.type() === 'safe') {
        return params.wallet.addresses().then(function (response) {
          result.push({ address: response.addresses[0].address, amount: changeAmount });
          return result;
        });
      }

      let extraChangeTotal = _.sum(extraChangeAmounts);
      // Sanity check
      if (extraChangeTotal > changeAmount) {
        extraChangeAmounts = [];
        extraChangeTotal = 0;
      }

      // copy and add remaining change amount
      const allChangeAmounts = extraChangeAmounts.slice(0);
      allChangeAmounts.push(changeAmount - extraChangeTotal);

      // Recursive async func to add all change outputs
      const addChangeOutputs = function (): Output[] | Promise<Output[]> {
        const thisAmount = allChangeAmounts.shift();
        if (!thisAmount) {
          return result;
        }
        return tryPromise(function () {
          if (params.changeAddress) {
            // If user passed a change address, use it for all outputs
            return params.changeAddress;
          } else {
            // Otherwise create a new address per output, for privacy
            // determine if segwit or not
            const changeChain = params.wallet.getChangeChain(params);
            return params.wallet.createAddress({ chain: changeChain, validate: validate }).then(function (result) {
              return result.address;
            });
          }
        }).then(function (address) {
          result.push({ address: address, amount: thisAmount });
          return addChangeOutputs();
        });
      };

      return addChangeOutputs();
    };

    // Add change output(s) and instant fee output if applicable
    return tryPromise(function () {
      return getChangeOutputs(inputAmount - totalAmount);
    }).then(function (result) {
      changeOutputs = result;
      const extraOutputs = changeOutputs.concat([]); // copy the array
      if (bitgoFeeInfo && bitgoFeeInfo.amount > 0) {
        extraOutputs.push(bitgoFeeInfo);
      }
      extraOutputs.forEach(function (output) {
        if ((output as AddressOutput).address) {
          (output as ScriptOutput).script = utxolib.address.toOutputScript((output as AddressOutput).address, network);
        }

        // decide where to put the outputs - default is to randomize unless forced to end
        const outputIndex = params.forceChangeAtEnd ? outputs.length : _.random(0, outputs.length);
        outputs.splice(outputIndex, 0, output);
      });

      // Add all outputs to the transaction
      outputs.forEach(function (output) {
        transaction.addOutput((output as ScriptOutput).script, output.amount);
      });

      travelInfos = _(outputs)
        .map(function (output, index) {
          const result = output.travelInfo;
          if (!result) {
            return undefined;
          }
          result.outputIndex = index;
          return result;
        })
        .filter()
        .value();
    });
  };

  // Serialize the transaction, returning what is needed to sign it
  const serialize = function () {
    // only need to return the unspents that were used and just the chainPath, redeemScript, and instant flag
    const pickedUnspents: any = _.map(unspents, function (unspent) {
      return _.pick(unspent, ['chainPath', 'redeemScript', 'instant', 'witnessScript', 'script', 'value']);
    });
    const prunedUnspents = _.slice(pickedUnspents, 0, transaction.tx.ins.length - feeSingleKeyUnspentsUsed.length);
    _.each(feeSingleKeyUnspentsUsed, function (feeUnspent) {
      prunedUnspents.push({ redeemScript: false, chainPath: false }); // mark as false to signify a non-multisig address
    });
    const result: any = {
      transactionHex: transaction.buildIncomplete().toHex(),
      unspents: prunedUnspents,
      fee: fee,
      changeAddresses: changeOutputs.map(function (co) {
        return _.pick(co, ['address', 'path', 'amount']);
      }),
      walletId: params.wallet.id(),
      walletKeychains: params.wallet.keychains,
      feeRate: feeRate,
      instant: params.instant,
      bitgoFee: bitgoFeeInfo,
      estimatedSize: minerFeeInfo.size,
      txInfo: txInfo,
      travelInfos: travelInfos,
    };

    // Add for backwards compatibility
    if (result.instant && bitgoFeeInfo) {
      result.instantFee = _.pick(bitgoFeeInfo, ['amount', 'address']);
    }

    return result;
  };

  return tryPromise(function () {
    return getBitGoFee();
  })
    .then(function () {
      return Promise.all([getBitGoFeeAddress(), getUnspents(), getUnspentsForSingleKey()]);
    })
    .then(collectInputs)
    .then(collectOutputs)
    .then(serialize);
};

/**
 * Estimate the size of a transaction in bytes based on the number of
 * inputs and outputs present.
 * @params params {
 *   nP2shInputs: number of P2SH (multisig) inputs
 *   nP2pkhInputs: number of P2PKH (single sig) inputs
 *   nOutputs: number of outputs
 * }
 *
 * @returns size: estimated size of the transaction in bytes
 */
const estimateTransactionSize = function (params) {
  if (!_.isInteger(params.nP2shInputs) || params.nP2shInputs < 0) {
    throw new Error('expecting positive nP2shInputs');
  }
  if (!_.isInteger(params.nP2pkhInputs) || params.nP2pkhInputs < 0) {
    throw new Error('expecting positive nP2pkhInputs to be numeric');
  }
  if (!_.isInteger(params.nP2shP2wshInputs) || params.nP2shP2wshInputs < 0) {
    throw new Error('expecting positive nP2shP2wshInputs to be numeric');
  }
  if (params.nP2shInputs + params.nP2shP2wshInputs < 1) {
    throw new Error('expecting at least one nP2shInputs or nP2shP2wshInputs');
  }
  if (!_.isInteger(params.nOutputs) || params.nOutputs < 1) {
    throw new Error('expecting positive nOutputs');
  }

  // The size of an uncompressed public key is 32 bytes more than the compressed key,
  // and hence, needs to be accounted for in the transaction size estimation.
  const uncompressedPublicKeysTripleCorrectionFactor = 32 * 3;

  return (
    // This is not quite accurate - if there is a mix of inputs scripts where some used
    // compressed keys and some used uncompressed keys, we would overestimate the size.
    // Since we don't have mixed input sets, this should not be an issue in practice.
    (VirtualSizes.txP2shInputSize +
      (params.containsUncompressedPublicKeys ? uncompressedPublicKeysTripleCorrectionFactor : 0)) *
      params.nP2shInputs +
    VirtualSizes.txP2shP2wshInputSize * (params.nP2shP2wshInputs || 0) +
    VirtualSizes.txP2pkhInputSizeUncompressedKey * (params.nP2pkhInputs || 0) +
    VirtualSizes.txP2pkhOutputSize * params.nOutputs +
    // if the tx contains at least one segwit input, the tx overhead is increased by 1
    VirtualSizes.txOverheadSize +
    (params.nP2shP2wshInputs > 0 ? 1 : 0)
  );
};

/**
 * Calculate the fee and estimated size in bytes for a transaction.
 * @params params {
 *   bitgo: bitgo object
 *   feeRate: satoshis per kilobyte
 *   nP2shInputs: number of P2SH (multisig) inputs
 *   nP2pkhInputs: number of P2PKH (single sig) inputs
 *   nOutputs: number of outputs
 * }
 *
 * @returns {
 *   size: estimated size of the transaction in bytes
 *   fee: estimated fee in satoshis for the transaction
 *   feeRate: fee rate that was used to estimate the fee for the transaction
 * }
 */
exports.calculateMinerFeeInfo = function (params) {
  const feeRateToUse = params.feeRate || params.bitgo.getConstants().fallbackFeeRate;
  const estimatedSize = estimateTransactionSize(params);

  return {
    size: estimatedSize,
    fee: Math.ceil((estimatedSize * feeRateToUse) / 1000),
    feeRate: feeRateToUse,
  };
};

/*
 * Given a transaction hex, unspent information (chain path and redeem scripts), and the keychain xprv,
 * perform key derivation and sign the inputs in the transaction based on the unspent information provided
 *
 * @params:
 *  transactionHex serialized form of the transaction in hex
 *  unspents array of unspent information, where each unspent is a chainPath and redeemScript with the same
 *  index as the inputs in the transactionHex
 *  keychain Keychain containing the xprv to sign with. For legacy support of safe wallets, keychain can
 also be a WIF private key.
 *  signingKey private key in WIF for safe wallets, when keychain is unavailable
 *  validate client-side signature verification - can be disabled for improved performance (signatures
 *           are still validated server-side).
 *  feeSingleKeyWIF Use the address based on this private key to pay fees
 * @returns {*}
 */
exports.signTransaction = function (params) {
  let keychain = params.keychain; // duplicate so as to not mutate below

  const validate = params.validate === undefined ? true : params.validate;
  let privKey;
  if (!_.isString(params.transactionHex)) {
    throw new Error('expecting the transaction hex as a string');
  }
  if (!Array.isArray(params.unspents)) {
    throw new Error('expecting the unspents array');
  }
  if (!_.isBoolean(validate)) {
    throw new Error('expecting validate to be a boolean');
  }
  let network = getNetwork();
  const enableBCH = _.isBoolean(params.forceBCH) && params.forceBCH === true;

  if (!_.isObject(keychain) || !_.isString((keychain as any).xprv)) {
    if (_.isString(params.signingKey)) {
      privKey = utxolib.ECPair.fromWIF(params.signingKey, network as utxolib.BitcoinJSNetwork);
      keychain = undefined;
    } else {
      throw new Error('expecting the keychain object with xprv');
    }
  }

  let feeSingleKey;
  if (params.feeSingleKeyWIF) {
    feeSingleKey = utxolib.ECPair.fromWIF(params.feeSingleKeyWIF, network as utxolib.BitcoinJSNetwork);
  }

  debug('Network: %O', network);

  if (enableBCH) {
    debug('Enabling BCH…');
    network = utxolib.networks.bitcoincash;
    debug('New network: %O', network);
  }

  const transaction = utxolib.bitgo.createTransactionFromHex(params.transactionHex, network);
  if (transaction.ins.length !== params.unspents.length) {
    throw new Error('length of unspents array should equal to the number of transaction inputs');
  }

  // decorate transaction with input values for TransactionBuilder instantiation
  const isUtxoTx = _.isObject(transaction) && Array.isArray((transaction as any).ins);
  const areValidUnspents = _.isObject(params) && Array.isArray((params as any).unspents);
  if (isUtxoTx && areValidUnspents) {
    // extend the transaction inputs with the values
    const inputValues = _.map((params as any).unspents, (u) => _.pick(u, 'value'));
    transaction.ins.map((currentItem, index) => _.extend(currentItem, inputValues[index]));
  }

  let rootExtKey;
  if (keychain) {
    rootExtKey = bip32.fromBase58(keychain.xprv);
  }

  const txb = utxolib.bitgo.createTransactionBuilderFromTransaction(transaction);

  for (let index = 0; index < txb.tx.ins.length; ++index) {
    const currentUnspent = params.unspents[index];
    if (currentUnspent.redeemScript === false) {
      // this is the input from a single key fee address
      if (!feeSingleKey) {
        throw new Error('single key address used in input but feeSingleKeyWIF not provided');
      }

      if (enableBCH) {
        feeSingleKey.network = network;
      }

      txb.sign(index, feeSingleKey);
      continue;
    }

    if (currentUnspent.witnessScript && enableBCH) {
      throw new Error('BCH does not support segwit inputs');
    }

    const chainPath = currentUnspent.chainPath;
    if (rootExtKey) {
      const { walletSubPath = '/0/0' } = keychain;
      const path = sanitizeLegacyPath(keychain.path + walletSubPath + chainPath);
      debug(
        'derived user key path "%s" using keychain path "%s", walletSubPath "%s", keychain walletSubPath "%s" and chainPath "%s"',
        path,
        keychain.path,
        walletSubPath,
        keychain.walletSubPath,
        chainPath
      );
      privKey = rootExtKey.derivePath(path);
    }

    privKey.network = network;

    // subscript is the part of the output script after the OP_CODESEPARATOR.
    // Since we are only ever signing p2sh outputs, which do not have
    // OP_CODESEPARATORS, it is always the output script.
    const subscript = Buffer.from(currentUnspent.redeemScript, 'hex');
    currentUnspent.validationScript = subscript;

    // In order to sign with bitcoinjs-lib, we must use its transaction
    // builder, confusingly named the same exact thing as our transaction
    // builder, but with inequivalent behavior.
    try {
      const witnessScript = currentUnspent.witnessScript ? Buffer.from(currentUnspent.witnessScript, 'hex') : undefined;
      const sigHash = utxolib.bitgo.getDefaultSigHash(network);
      txb.sign(index, privKey, subscript, sigHash, currentUnspent.value, witnessScript);
      debug(`Signed transaction input ${index}`);
    } catch (e) {
      // try fallback derivation path (see BG-46497)
      let fallbackSigningSuccessful = false;
      try {
        const fallbackPath = sanitizeLegacyPath(keychain.path + chainPath);
        debug(
          'derived fallback user key path "%s" using keychain path "%s" and chainPath "%s"',
          fallbackPath,
          keychain.path,
          chainPath
        );
        privKey = rootExtKey.derivePath(fallbackPath);
        const witnessScript = currentUnspent.witnessScript
          ? Buffer.from(currentUnspent.witnessScript, 'hex')
          : undefined;
        const sigHash = utxolib.bitgo.getDefaultSigHash(network);
        txb.sign(index, privKey, subscript, sigHash, currentUnspent.value, witnessScript);
        fallbackSigningSuccessful = true;
      } catch (fallbackError) {
        debug('input sign failed for fallback path: %s', fallbackError.message);
      }
      // we need to know what's causing this
      if (!fallbackSigningSuccessful) {
        e.result = {
          unspent: currentUnspent,
        };
        e.message = `Failed to sign input #${index} - ${e.message} - ${JSON.stringify(e.result, null, 4)} - \n${
          e.stack
        }`;
        debug('input sign failed: %s', e.message);
        return Promise.reject(e);
      }
    }
  }

  const partialTransaction = txb.buildIncomplete();

  if (validate) {
    partialTransaction.ins.forEach((input, index) => {
      const signatureCount = utxolib.bitgo
        .getSignatureVerifications(partialTransaction, index, params.unspents[index].value)
        .filter((v) => v.signedBy !== undefined).length;
      debug(`Signature count for input ${index}: ${signatureCount}`);
      if (signatureCount < 1) {
        throw new Error('expected at least one valid signature');
      }
      if (params.fullLocalSigning && signatureCount < 2) {
        throw new Error('fullLocalSigning set: expected at least two valid signatures');
      }
    });
  }

  return Promise.resolve({
    transactionHex: partialTransaction.toHex(),
  });
};

Выполнить команду


Для локальной разработки. Не используйте в интернете!