buildUnsignedTransaction function

RawTransaction buildUnsignedTransaction({
  1. required TransferIntent<UtxoFeeInformation> intent,
  2. required UTXONetworkType networkType,
  3. required HDWalletPath walletPath,
  4. required Iterable<UTXOTransaction> txList,
  5. required Amount feePerByte,
  6. required Iterable<String> changeAddresses,
  7. List<ElectrumOutput>? preChosenUTXOs,
})

Useful: https://btcinformation.org/en/developer-reference#raw-transaction-format https://github.com/bitcoin/bips/blob/master/bip-0144.mediawiki

Implementation

RawTransaction buildUnsignedTransaction({
  required TransferIntent<UtxoFeeInformation> intent,
  required UTXONetworkType networkType,
  required HDWalletPath walletPath,
  required Iterable<UTXOTransaction> txList,
  required Amount feePerByte,
  required Iterable<String> changeAddresses,

  /// Pre chosen UTXOs to deterministly choose the UTXOs
  /// if null, the UTXOs will be chosen randomly
  List<ElectrumOutput>? preChosenUTXOs,
}) {
  if (txList.isEmpty) {
    throw SendFailure("No transactions");
  }

  var targetValue = intent.amount.value;

  if (targetValue < BigInt.zero) {
    throw SendFailure("targetValue < 0");
  }

  if (targetValue < networkType.dustTreshhold.legacy.toBI &&
      walletPath.purpose != HDWalletPurpose.BIP84) {
    throw SendFailure(
      "targetValue < DUST_THRESHOLD: ${networkType.dustTreshhold.legacy}",
    );
  }
  if (walletPath.purpose == HDWalletPurpose.BIP84 &&
      targetValue < networkType.dustTreshhold.segwit.toBI) {
    throw SendFailure(
      "targetValue < DUST_THRESHOLD_BIP84: ${networkType.dustTreshhold.segwit}",
    );
  }

  final allUTXOs = extractUTXOs(txList: txList);

  if (allUTXOs.isEmpty) {
    throw const SendFailure("no UTXOs"); // should be never reached
  }

  const lockTime = 0;
  const validFrom = 0; // EC8 specific
  const validUntil = 0; // EC8 specific
  final version = networkType.txVersion;

  final chosenUTXOs = preChosenUTXOs ??
      singleRandomDrawUTXOSelection(
        allUTXOs.keys.toList(),
        targetValue,
      );

  Logger.log("Chosen UTXOs: ${chosenUTXOs}");

  var chosenUTXOsMap = {
    for (final utxo in chosenUTXOs) utxo: allUTXOs[utxo]!,
  };

  var (totalInputValue, inputMap) = buildInputs(chosenUTXOsMap, networkType);

  if (totalInputValue < targetValue) {
    throw SendFailure("Not enough funds to pay targetValue $targetValue");
  }
  if (inputMap.keys.isEmpty) {
    throw SendFailure("No inputs");
  }

  final targetAddress = intent.recipient;

  final changeAddress = findUnusedAddress(
    addresses: changeAddresses,
    txs: txList,
  );

  ///
  /// Build Dummy TX
  ///

  final dummyOutputs = buildOutputs(
    recipient: intent.recipient,
    value: targetValue,
    changeAddress: changeAddress,
    changeValue: BigInt.one,
    networkType: networkType,
  );

  var dummyTx = buildDummyTx(
    networkType: networkType,
    walletPath: walletPath,
    inputMap: inputMap,
    dummyOutputs: dummyOutputs,
  );

  ///
  /// Build Outputs again with the estimated size
  ///

  var estimatedFee = calculateFee(tx: dummyTx, feePerByte: feePerByte);

  var changeValue = totalInputValue - targetValue - estimatedFee;

  if (changeValue < BigInt.zero) {
    targetValue -= changeValue.abs();
    if (targetValue < networkType.dustTreshhold.legacy.toBI) {
      /// Ad addidional UTXO to cover the fee
      targetValue = intent.amount.value;
      final additionalUTXO = fillUpToTargetAmount(
        chosenUTXOs,
        allUTXOs.keys.toList(),
        targetValue + estimatedFee * BigInt.two,
      );

      chosenUTXOsMap = {
        for (final utxo in additionalUTXO) utxo: allUTXOs[utxo]!,
      };

      (totalInputValue, inputMap) = buildInputs(chosenUTXOsMap, networkType);

      dummyTx = buildDummyTx(
        networkType: networkType,
        walletPath: walletPath,
        inputMap: inputMap,
        dummyOutputs: dummyOutputs,
      );

      estimatedFee = calculateFee(tx: dummyTx, feePerByte: feePerByte);
    }

    changeValue = totalInputValue - targetValue - estimatedFee;
    if (changeValue < BigInt.zero)
      throw SendFailure("Not enough funds to pay targetValue $targetValue");
  }

  assert(
    totalInputValue == targetValue + changeValue + estimatedFee,
    "Total Input Value does not match Total Output Value",
  );

  Logger.log("Estimated Fee: $estimatedFee");

  final outputs = buildOutputs(
    recipient: targetAddress,
    value: targetValue,
    changeAddress: changeAddress,
    changeValue: changeValue,
    networkType: networkType,
  );

  ///
  /// Build final transaction
  ///

  var tx = RawTransaction.build(
    version: version,
    lockTime: lockTime,
    validFrom: validFrom,
    validUntil: validUntil,
    inputMap: inputMap,
    outputs: outputs,
  );

  if (tx.totalOutputValue + estimatedFee != totalInputValue) {
    throw SendFailure(
      "Total Output Value does not match Total Input Value",
    );
  }

  return tx;
}