clean up coin selection somewhat

This commit is contained in:
julian 2024-06-04 15:07:31 -06:00
parent 7cef4c45eb
commit 7637e81408

View file

@ -4,6 +4,7 @@ import 'dart:typed_data';
import 'package:coinlib_flutter/coinlib_flutter.dart' as coinlib; import 'package:coinlib_flutter/coinlib_flutter.dart' as coinlib;
import 'package:isar/isar.dart'; import 'package:isar/isar.dart';
import '../../../electrumx_rpc/cached_electrumx_client.dart'; import '../../../electrumx_rpc/cached_electrumx_client.dart';
import '../../../electrumx_rpc/client_manager.dart'; import '../../../electrumx_rpc/client_manager.dart';
import '../../../electrumx_rpc/electrumx_client.dart'; import '../../../electrumx_rpc/electrumx_client.dart';
@ -64,7 +65,10 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
} }
Future<List<({String address, Amount amount, bool isChange})>> Future<List<({String address, Amount amount, bool isChange})>>
_helperRecipientsConvert(List<String> addrs, List<int> satValues) async { _helperRecipientsConvert(
List<String> addrs,
List<BigInt> satValues,
) async {
final List<({String address, Amount amount, bool isChange})> results = []; final List<({String address, Amount amount, bool isChange})> results = [];
for (int i = 0; i < addrs.length; i++) { for (int i = 0; i < addrs.length; i++) {
@ -72,7 +76,7 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
( (
address: addrs[i], address: addrs[i],
amount: Amount( amount: Amount(
rawValue: BigInt.from(satValues[i]), rawValue: satValues[i],
fractionDigits: cryptoCurrency.fractionDigits, fractionDigits: cryptoCurrency.fractionDigits,
), ),
isChange: (await mainDB.isar.addresses isChange: (await mainDB.isar.addresses
@ -105,44 +109,47 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
// TODO: multiple recipients one day // TODO: multiple recipients one day
assert(txData.recipients!.length == 1); assert(txData.recipients!.length == 1);
if (coinControl && utxos == null) {
throw Exception("Coin control used where utxos is null!");
}
final recipientAddress = txData.recipients!.first.address; final recipientAddress = txData.recipients!.first.address;
final satoshiAmountToSend = txData.amount!.raw.toInt(); final satoshiAmountToSend = txData.amount!.raw;
final int? satsPerVByte = txData.satsPerVByte; final int? satsPerVByte = txData.satsPerVByte;
final selectedTxFeeRate = txData.feeRateAmount!; final selectedTxFeeRate = txData.feeRateAmount!;
final List<UTXO> availableOutputs = final List<UTXO> availableOutputs =
utxos ?? await mainDB.getUTXOs(walletId).findAll(); utxos ?? await mainDB.getUTXOs(walletId).findAll();
final currentChainHeight = await chainHeight; final currentChainHeight = await chainHeight;
final List<UTXO> spendableOutputs = [];
int spendableSatoshiValue = 0;
// Build list of spendable outputs and totaling their satoshi amount final spendableOutputs = availableOutputs
for (final utxo in availableOutputs) { .where(
if (utxo.isBlocked == false && (e) =>
utxo.isConfirmed(currentChainHeight, cryptoCurrency.minConfirms) && !e.isBlocked &&
utxo.used != true) { (e.used != true) &&
spendableOutputs.add(utxo); e.isConfirmed(currentChainHeight, cryptoCurrency.minConfirms),
spendableSatoshiValue += utxo.value; )
} .toList();
final spendableSatoshiValue =
spendableOutputs.fold(BigInt.zero, (p, e) => p + BigInt.from(e.value));
if (spendableSatoshiValue < satoshiAmountToSend) {
throw Exception("Insufficient balance");
} else if (spendableSatoshiValue == satoshiAmountToSend && !isSendAll) {
throw Exception("Insufficient balance to pay transaction fee");
} }
if (coinControl) { if (coinControl) {
if (spendableOutputs.length < availableOutputs.length) { if (spendableOutputs.length < availableOutputs.length) {
throw ArgumentError("Attempted to use an unavailable utxo"); throw ArgumentError("Attempted to use an unavailable utxo");
} }
} // don't care about sorting if using all utxos
} else {
// don't care about sorting if using all utxos
if (!coinControl) {
// sort spendable by age (oldest first) // sort spendable by age (oldest first)
spendableOutputs.sort( spendableOutputs.sort(
(a, b) => (b.blockTime ?? currentChainHeight) (a, b) => (b.blockTime ?? currentChainHeight)
.compareTo((a.blockTime ?? currentChainHeight)), .compareTo((a.blockTime ?? currentChainHeight)),
); );
// Null check operator changed to null assignment in order to resolve a
// `Null check operator used on a null value` error. currentChainHeight
// used in order to sort these unconfirmed outputs as the youngest, but we
// could just as well use currentChainHeight + 1.
} }
Logging.instance.log( Logging.instance.log(
@ -161,26 +168,10 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
); );
Logging.instance Logging.instance
.log("satoshiAmountToSend: $satoshiAmountToSend", level: LogLevel.Info); .log("satoshiAmountToSend: $satoshiAmountToSend", level: LogLevel.Info);
// If the amount the user is trying to send is smaller than the amount that they have spendable,
// then return 1, which indicates that they have an insufficient balance.
if (spendableSatoshiValue < satoshiAmountToSend) {
// return 1;
throw Exception("Insufficient balance");
// If the amount the user wants to send is exactly equal to the amount they can spend, then return
// 2, which indicates that they are not leaving enough over to pay the transaction fee
} else if (spendableSatoshiValue == satoshiAmountToSend && !isSendAll) {
throw Exception("Insufficient balance to pay transaction fee");
// return 2;
}
// If neither of these statements pass, we assume that the user has a spendable balance greater
// than the amount they're attempting to send. Note that this value still does not account for
// the added transaction fee, which may require an extra input and will need to be checked for
// later on.
// Possible situation right here BigInt satoshisBeingUsed = BigInt.zero;
int satoshisBeingUsed = 0;
int inputsBeingConsumed = 0; int inputsBeingConsumed = 0;
List<UTXO> utxoObjectsToUse = []; final List<UTXO> utxoObjectsToUse = [];
if (!coinControl) { if (!coinControl) {
for (var i = 0; for (var i = 0;
@ -188,7 +179,7 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
i < spendableOutputs.length; i < spendableOutputs.length;
i++) { i++) {
utxoObjectsToUse.add(spendableOutputs[i]); utxoObjectsToUse.add(spendableOutputs[i]);
satoshisBeingUsed += spendableOutputs[i].value; satoshisBeingUsed += BigInt.from(spendableOutputs[i].value);
inputsBeingConsumed += 1; inputsBeingConsumed += 1;
} }
for (int i = 0; for (int i = 0;
@ -196,12 +187,13 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
inputsBeingConsumed < spendableOutputs.length; inputsBeingConsumed < spendableOutputs.length;
i++) { i++) {
utxoObjectsToUse.add(spendableOutputs[inputsBeingConsumed]); utxoObjectsToUse.add(spendableOutputs[inputsBeingConsumed]);
satoshisBeingUsed += spendableOutputs[inputsBeingConsumed].value; satoshisBeingUsed +=
BigInt.from(spendableOutputs[inputsBeingConsumed].value);
inputsBeingConsumed += 1; inputsBeingConsumed += 1;
} }
} else { } else {
satoshisBeingUsed = spendableSatoshiValue; satoshisBeingUsed = spendableSatoshiValue;
utxoObjectsToUse = spendableOutputs; utxoObjectsToUse.addAll(spendableOutputs);
inputsBeingConsumed = spendableOutputs.length; inputsBeingConsumed = spendableOutputs.length;
} }
@ -214,72 +206,20 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
// numberOfOutputs' length must always be equal to that of recipientsArray and recipientsAmtArray // numberOfOutputs' length must always be equal to that of recipientsArray and recipientsAmtArray
final List<String> recipientsArray = [recipientAddress]; final List<String> recipientsArray = [recipientAddress];
final List<int> recipientsAmtArray = [satoshiAmountToSend]; final List<BigInt> recipientsAmtArray = [satoshiAmountToSend];
// gather required signing data // gather required signing data
final utxoSigningData = await fetchBuildTxData(utxoObjectsToUse); final utxoSigningData = await fetchBuildTxData(utxoObjectsToUse);
if (isSendAll) { if (isSendAll) {
Logging.instance return await _sendAllBuilder(
.log("Attempting to send all $cryptoCurrency", level: LogLevel.Info); txData: txData,
if (txData.recipients!.length != 1) { recipientAddress: recipientAddress,
throw Exception( satoshiAmountToSend: satoshiAmountToSend,
"Send all to more than one recipient not yet supported", satoshisBeingUsed: satoshisBeingUsed,
);
}
final int vSizeForOneOutput = (await buildTransaction(
utxoSigningData: utxoSigningData, utxoSigningData: utxoSigningData,
txData: txData.copyWith( satsPerVByte: satsPerVByte,
recipients: await _helperRecipientsConvert( feeRatePerKB: selectedTxFeeRate,
[recipientAddress],
[satoshisBeingUsed - 1],
),
),
))
.vSize!;
int feeForOneOutput = satsPerVByte != null
? (satsPerVByte * vSizeForOneOutput)
: estimateTxFee(
vSize: vSizeForOneOutput,
feeRatePerKB: selectedTxFeeRate,
);
if (satsPerVByte == null) {
final int roughEstimate = roughFeeEstimate(
spendableOutputs.length,
1,
selectedTxFeeRate,
).raw.toInt();
if (feeForOneOutput < roughEstimate) {
feeForOneOutput = roughEstimate;
}
}
final int amount = satoshiAmountToSend - feeForOneOutput;
if (amount < 0) {
throw Exception(
"Estimated fee ($feeForOneOutput sats) is greater than balance!",
);
}
final data = await buildTransaction(
txData: txData.copyWith(
recipients: await _helperRecipientsConvert(
[recipientAddress],
[amount],
),
),
utxoSigningData: utxoSigningData,
);
return data.copyWith(
fee: Amount(
rawValue: BigInt.from(feeForOneOutput),
fractionDigits: cryptoCurrency.fractionDigits,
),
usedUTXOs: utxoSigningData.map((e) => e.utxo).toList(),
); );
} }
@ -290,7 +230,7 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
txData: txData.copyWith( txData: txData.copyWith(
recipients: await _helperRecipientsConvert( recipients: await _helperRecipientsConvert(
[recipientAddress], [recipientAddress],
[satoshisBeingUsed - 1], [satoshisBeingUsed - BigInt.one],
), ),
), ),
)) ))
@ -301,6 +241,9 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
} }
final int vSizeForTwoOutPuts; final int vSizeForTwoOutPuts;
BigInt maxBI(BigInt a, BigInt b) => a > b ? a : b;
try { try {
vSizeForTwoOutPuts = (await buildTransaction( vSizeForTwoOutPuts = (await buildTransaction(
utxoSigningData: utxoSigningData, utxoSigningData: utxoSigningData,
@ -309,7 +252,10 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
[recipientAddress, (await getCurrentChangeAddress())!.value], [recipientAddress, (await getCurrentChangeAddress())!.value],
[ [
satoshiAmountToSend, satoshiAmountToSend,
max(0, satoshisBeingUsed - satoshiAmountToSend - 1), maxBI(
BigInt.zero,
satoshisBeingUsed - (satoshiAmountToSend + BigInt.one),
),
], ],
), ),
), ),
@ -321,53 +267,112 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
} }
// Assume 1 output, only for recipient and no change // Assume 1 output, only for recipient and no change
final feeForOneOutput = satsPerVByte != null final feeForOneOutput = BigInt.from(
? (satsPerVByte * vSizeForOneOutput) satsPerVByte != null
: estimateTxFee( ? (satsPerVByte * vSizeForOneOutput)
vSize: vSizeForOneOutput, : estimateTxFee(
feeRatePerKB: selectedTxFeeRate, vSize: vSizeForOneOutput,
); feeRatePerKB: selectedTxFeeRate,
),
);
// Assume 2 outputs, one for recipient and one for change // Assume 2 outputs, one for recipient and one for change
final feeForTwoOutputs = satsPerVByte != null final feeForTwoOutputs = BigInt.from(
? (satsPerVByte * vSizeForTwoOutPuts) satsPerVByte != null
: estimateTxFee( ? (satsPerVByte * vSizeForTwoOutPuts)
vSize: vSizeForTwoOutPuts, : estimateTxFee(
feeRatePerKB: selectedTxFeeRate, vSize: vSizeForTwoOutPuts,
); feeRatePerKB: selectedTxFeeRate,
),
);
Logging.instance Logging.instance.log(
.log("feeForTwoOutputs: $feeForTwoOutputs", level: LogLevel.Info); "feeForTwoOutputs: $feeForTwoOutputs",
Logging.instance level: LogLevel.Info,
.log("feeForOneOutput: $feeForOneOutput", level: LogLevel.Info); );
Logging.instance.log(
"feeForOneOutput: $feeForOneOutput",
level: LogLevel.Info,
);
if (satoshisBeingUsed - satoshiAmountToSend > feeForOneOutput) { final difference = satoshisBeingUsed - satoshiAmountToSend;
if (satoshisBeingUsed - satoshiAmountToSend >
feeForOneOutput + cryptoCurrency.dustLimit.raw.toInt()) { Future<TxData> singleOutputTxn() async {
// Here, we know that theoretically, we may be able to include another output(change) but we first need to Logging.instance.log(
// factor in the value of this output in satoshis. 'Input size: $satoshisBeingUsed',
final int changeOutputSize = level: LogLevel.Info,
satoshisBeingUsed - satoshiAmountToSend - feeForTwoOutputs; );
// We check to see if the user can pay for the new transaction with 2 outputs instead of one. If they can and Logging.instance.log(
// the second output's size > cryptoCurrency.dustLimit satoshis, we perform the mechanics required to properly generate and use a new 'Recipient output size: $satoshiAmountToSend',
// change address. level: LogLevel.Info,
if (changeOutputSize > cryptoCurrency.dustLimit.raw.toInt() && );
satoshisBeingUsed - satoshiAmountToSend - changeOutputSize == Logging.instance.log(
feeForTwoOutputs) { 'Fee being paid: $difference sats',
level: LogLevel.Info,
);
Logging.instance.log(
'Estimated fee: $feeForOneOutput',
level: LogLevel.Info,
);
final txnData = await buildTransaction(
utxoSigningData: utxoSigningData,
txData: txData.copyWith(
recipients: await _helperRecipientsConvert(
recipientsArray,
recipientsAmtArray,
),
),
);
return txnData.copyWith(
fee: Amount(
rawValue: feeForOneOutput,
fractionDigits: cryptoCurrency.fractionDigits,
),
usedUTXOs: utxoSigningData.map((e) => e.utxo).toList(),
);
}
// no change output required
if (difference == feeForOneOutput) {
Logging.instance.log('1 output in tx', level: LogLevel.Info);
return await singleOutputTxn();
} else if (difference < feeForOneOutput) {
Logging.instance.log(
'Cannot pay tx fee - checking for more outputs and trying again',
level: LogLevel.Warning,
);
// try adding more outputs
if (spendableOutputs.length > inputsBeingConsumed) {
return coinSelection(
txData: txData,
isSendAll: isSendAll,
additionalOutputs: additionalOutputs + 1,
utxos: utxos,
coinControl: coinControl,
);
}
throw Exception("Insufficient balance to pay transaction fee");
} else {
if (difference > (feeForOneOutput + cryptoCurrency.dustLimit.raw)) {
final changeOutputSize = difference - feeForTwoOutputs;
// check if possible to add the change output
if (changeOutputSize > cryptoCurrency.dustLimit.raw &&
difference - changeOutputSize == feeForTwoOutputs) {
// generate new change address if current change address has been used // generate new change address if current change address has been used
await checkChangeAddressForTransactions(); await checkChangeAddressForTransactions();
final String newChangeAddress = final String newChangeAddress =
(await getCurrentChangeAddress())!.value; (await getCurrentChangeAddress())!.value;
int feeBeingPaid = BigInt feeBeingPaid = difference - changeOutputSize;
satoshisBeingUsed - satoshiAmountToSend - changeOutputSize;
// add change output
recipientsArray.add(newChangeAddress); recipientsArray.add(newChangeAddress);
recipientsAmtArray.add(changeOutputSize); recipientsAmtArray.add(changeOutputSize);
// At this point, we have the outputs we're going to use, the amounts to send along with which addresses
// we intend to send these amounts to. We have enough to send instructions to build the transaction.
Logging.instance.log('2 outputs in tx', level: LogLevel.Info); Logging.instance.log('2 outputs in tx', level: LogLevel.Info);
Logging.instance Logging.instance.log(
.log('Input size: $satoshisBeingUsed', level: LogLevel.Info); 'Input size: $satoshisBeingUsed',
level: LogLevel.Info,
);
Logging.instance.log( Logging.instance.log(
'Recipient output size: $satoshiAmountToSend', 'Recipient output size: $satoshiAmountToSend',
level: LogLevel.Info, level: LogLevel.Info,
@ -380,10 +385,12 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
'Difference (fee being paid): $feeBeingPaid sats', 'Difference (fee being paid): $feeBeingPaid sats',
level: LogLevel.Info, level: LogLevel.Info,
); );
Logging.instance Logging.instance.log(
.log('Estimated fee: $feeForTwoOutputs', level: LogLevel.Info); 'Estimated fee: $feeForTwoOutputs',
level: LogLevel.Info,
);
var txn = await buildTransaction( TxData txnData = await buildTransaction(
utxoSigningData: utxoSigningData, utxoSigningData: utxoSigningData,
txData: txData.copyWith( txData: txData.copyWith(
recipients: await _helperRecipientsConvert( recipients: await _helperRecipientsConvert(
@ -394,13 +401,12 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
); );
// make sure minimum fee is accurate if that is being used // make sure minimum fee is accurate if that is being used
if (txn.vSize! - feeBeingPaid == 1) { if (BigInt.from(txnData.vSize!) - feeBeingPaid == BigInt.one) {
final int changeOutputSize = final changeOutputSize = difference - BigInt.from(txnData.vSize!);
satoshisBeingUsed - satoshiAmountToSend - txn.vSize!; feeBeingPaid = difference - changeOutputSize;
feeBeingPaid =
satoshisBeingUsed - satoshiAmountToSend - changeOutputSize;
recipientsAmtArray.removeLast(); recipientsAmtArray.removeLast();
recipientsAmtArray.add(changeOutputSize); recipientsAmtArray.add(changeOutputSize);
Logging.instance.log( Logging.instance.log(
'Adjusted Input size: $satoshisBeingUsed', 'Adjusted Input size: $satoshisBeingUsed',
level: LogLevel.Info, level: LogLevel.Info,
@ -421,7 +427,8 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
'Adjusted Estimated fee: $feeForTwoOutputs', 'Adjusted Estimated fee: $feeForTwoOutputs',
level: LogLevel.Info, level: LogLevel.Info,
); );
txn = await buildTransaction(
txnData = await buildTransaction(
utxoSigningData: utxoSigningData, utxoSigningData: utxoSigningData,
txData: txData.copyWith( txData: txData.copyWith(
recipients: await _helperRecipientsConvert( recipients: await _helperRecipientsConvert(
@ -432,9 +439,9 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
); );
} }
return txn.copyWith( return txnData.copyWith(
fee: Amount( fee: Amount(
rawValue: BigInt.from(feeBeingPaid), rawValue: feeBeingPaid,
fractionDigits: cryptoCurrency.fractionDigits, fractionDigits: cryptoCurrency.fractionDigits,
), ),
usedUTXOs: utxoSigningData.map((e) => e.utxo).toList(), usedUTXOs: utxoSigningData.map((e) => e.utxo).toList(),
@ -442,126 +449,91 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
} else { } else {
// Something went wrong here. It either overshot or undershot the estimated fee amount or the changeOutputSize // Something went wrong here. It either overshot or undershot the estimated fee amount or the changeOutputSize
// is smaller than or equal to cryptoCurrency.dustLimit. Revert to single output transaction. // is smaller than or equal to cryptoCurrency.dustLimit. Revert to single output transaction.
Logging.instance.log('1 output in tx', level: LogLevel.Info);
Logging.instance
.log('Input size: $satoshisBeingUsed', level: LogLevel.Info);
Logging.instance.log( Logging.instance.log(
'Recipient output size: $satoshiAmountToSend', 'Reverting to 1 output in tx',
level: LogLevel.Info, level: LogLevel.Info,
); );
Logging.instance.log(
'Difference (fee being paid): ${satoshisBeingUsed - satoshiAmountToSend} sats',
level: LogLevel.Info,
);
Logging.instance
.log('Estimated fee: $feeForOneOutput', level: LogLevel.Info);
final txn = await buildTransaction(
utxoSigningData: utxoSigningData,
txData: txData.copyWith(
recipients: await _helperRecipientsConvert(
recipientsArray,
recipientsAmtArray,
),
),
);
return txn.copyWith( return await singleOutputTxn();
fee: Amount(
rawValue: BigInt.from(satoshisBeingUsed - satoshiAmountToSend),
fractionDigits: cryptoCurrency.fractionDigits,
),
usedUTXOs: utxoSigningData.map((e) => e.utxo).toList(),
);
} }
} else {
// No additional outputs needed since adding one would mean that it'd be smaller than cryptoCurrency.dustLimit sats
// which makes it uneconomical to add to the transaction. Here, we pass data directly to instruct
// the wallet to begin crafting the transaction that the user requested.
Logging.instance.log('1 output in tx', level: LogLevel.Info);
Logging.instance
.log('Input size: $satoshisBeingUsed', level: LogLevel.Info);
Logging.instance.log(
'Recipient output size: $satoshiAmountToSend',
level: LogLevel.Info,
);
Logging.instance.log(
'Difference (fee being paid): ${satoshisBeingUsed - satoshiAmountToSend} sats',
level: LogLevel.Info,
);
Logging.instance
.log('Estimated fee: $feeForOneOutput', level: LogLevel.Info);
final txn = await buildTransaction(
utxoSigningData: utxoSigningData,
txData: txData.copyWith(
recipients: await _helperRecipientsConvert(
recipientsArray,
recipientsAmtArray,
),
),
);
return txn.copyWith(
fee: Amount(
rawValue: BigInt.from(satoshisBeingUsed - satoshiAmountToSend),
fractionDigits: cryptoCurrency.fractionDigits,
),
usedUTXOs: utxoSigningData.map((e) => e.utxo).toList(),
);
} }
} else if (satoshisBeingUsed - satoshiAmountToSend == feeForOneOutput) {
// In this scenario, no additional change output is needed since inputs - outputs equal exactly
// what we need to pay for fees. Here, we pass data directly to instruct the wallet to begin
// crafting the transaction that the user requested.
Logging.instance.log('1 output in tx', level: LogLevel.Info);
Logging.instance
.log('Input size: $satoshisBeingUsed', level: LogLevel.Info);
Logging.instance.log(
'Recipient output size: $satoshiAmountToSend',
level: LogLevel.Info,
);
Logging.instance.log(
'Fee being paid: ${satoshisBeingUsed - satoshiAmountToSend} sats',
level: LogLevel.Info,
);
Logging.instance
.log('Estimated fee: $feeForOneOutput', level: LogLevel.Info);
final txn = await buildTransaction(
utxoSigningData: utxoSigningData,
txData: txData.copyWith(
recipients: await _helperRecipientsConvert(
recipientsArray,
recipientsAmtArray,
),
),
);
return txn.copyWith(
fee: Amount(
rawValue: BigInt.from(feeForOneOutput),
fractionDigits: cryptoCurrency.fractionDigits,
),
usedUTXOs: utxoSigningData.map((e) => e.utxo).toList(),
);
} else {
// Remember that returning 2 indicates that the user does not have a sufficient balance to
// pay for the transaction fee. Ideally, at this stage, we should check if the user has any
// additional outputs they're able to spend and then recalculate fees.
Logging.instance.log(
'Cannot pay tx fee - checking for more outputs and trying again',
level: LogLevel.Warning,
);
// try adding more outputs
if (spendableOutputs.length > inputsBeingConsumed) {
return coinSelection(
txData: txData,
isSendAll: isSendAll,
additionalOutputs: additionalOutputs + 1,
utxos: utxos,
coinControl: coinControl,
);
}
throw Exception("Insufficient balance to pay transaction fee");
// return 2;
} }
return txData;
}
Future<TxData> _sendAllBuilder({
required TxData txData,
required String recipientAddress,
required BigInt satoshiAmountToSend,
required BigInt satoshisBeingUsed,
required List<SigningData> utxoSigningData,
required int? satsPerVByte,
required int feeRatePerKB,
}) async {
Logging.instance
.log("Attempting to send all $cryptoCurrency", level: LogLevel.Info);
if (txData.recipients!.length != 1) {
throw Exception(
"Send all to more than one recipient not yet supported",
);
}
final int vSizeForOneOutput = (await buildTransaction(
utxoSigningData: utxoSigningData,
txData: txData.copyWith(
recipients: await _helperRecipientsConvert(
[recipientAddress],
[satoshisBeingUsed - BigInt.one],
),
),
))
.vSize!;
BigInt feeForOneOutput = BigInt.from(
satsPerVByte != null
? (satsPerVByte * vSizeForOneOutput)
: estimateTxFee(
vSize: vSizeForOneOutput,
feeRatePerKB: feeRatePerKB,
),
);
if (satsPerVByte == null) {
final roughEstimate = roughFeeEstimate(
utxoSigningData.length,
1,
feeRatePerKB,
).raw;
if (feeForOneOutput < roughEstimate) {
feeForOneOutput = roughEstimate;
}
}
final amount = satoshiAmountToSend - feeForOneOutput;
if (amount.isNegative) {
throw Exception(
"Estimated fee ($feeForOneOutput sats) is greater than balance!",
);
}
final data = await buildTransaction(
txData: txData.copyWith(
recipients: await _helperRecipientsConvert(
[recipientAddress],
[amount],
),
),
utxoSigningData: utxoSigningData,
);
return data.copyWith(
fee: Amount(
rawValue: feeForOneOutput,
fractionDigits: cryptoCurrency.fractionDigits,
),
usedUTXOs: utxoSigningData.map((e) => e.utxo).toList(),
);
} }
Future<List<SigningData>> fetchBuildTxData( Future<List<SigningData>> fetchBuildTxData(