mirror of
https://github.com/cypherstack/stack_wallet.git
synced 2024-11-16 17:27:39 +00:00
clean up coin selection somewhat
This commit is contained in:
parent
7cef4c45eb
commit
7637e81408
1 changed files with 235 additions and 263 deletions
|
@ -4,6 +4,7 @@ import 'dart:typed_data';
|
|||
|
||||
import 'package:coinlib_flutter/coinlib_flutter.dart' as coinlib;
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
import '../../../electrumx_rpc/cached_electrumx_client.dart';
|
||||
import '../../../electrumx_rpc/client_manager.dart';
|
||||
import '../../../electrumx_rpc/electrumx_client.dart';
|
||||
|
@ -64,7 +65,10 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
|
|||
}
|
||||
|
||||
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 = [];
|
||||
|
||||
for (int i = 0; i < addrs.length; i++) {
|
||||
|
@ -72,7 +76,7 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
|
|||
(
|
||||
address: addrs[i],
|
||||
amount: Amount(
|
||||
rawValue: BigInt.from(satValues[i]),
|
||||
rawValue: satValues[i],
|
||||
fractionDigits: cryptoCurrency.fractionDigits,
|
||||
),
|
||||
isChange: (await mainDB.isar.addresses
|
||||
|
@ -105,44 +109,47 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
|
|||
// TODO: multiple recipients one day
|
||||
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 satoshiAmountToSend = txData.amount!.raw.toInt();
|
||||
final satoshiAmountToSend = txData.amount!.raw;
|
||||
final int? satsPerVByte = txData.satsPerVByte;
|
||||
final selectedTxFeeRate = txData.feeRateAmount!;
|
||||
|
||||
final List<UTXO> availableOutputs =
|
||||
utxos ?? await mainDB.getUTXOs(walletId).findAll();
|
||||
final currentChainHeight = await chainHeight;
|
||||
final List<UTXO> spendableOutputs = [];
|
||||
int spendableSatoshiValue = 0;
|
||||
|
||||
// Build list of spendable outputs and totaling their satoshi amount
|
||||
for (final utxo in availableOutputs) {
|
||||
if (utxo.isBlocked == false &&
|
||||
utxo.isConfirmed(currentChainHeight, cryptoCurrency.minConfirms) &&
|
||||
utxo.used != true) {
|
||||
spendableOutputs.add(utxo);
|
||||
spendableSatoshiValue += utxo.value;
|
||||
}
|
||||
final spendableOutputs = availableOutputs
|
||||
.where(
|
||||
(e) =>
|
||||
!e.isBlocked &&
|
||||
(e.used != true) &&
|
||||
e.isConfirmed(currentChainHeight, cryptoCurrency.minConfirms),
|
||||
)
|
||||
.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 (spendableOutputs.length < availableOutputs.length) {
|
||||
throw ArgumentError("Attempted to use an unavailable utxo");
|
||||
}
|
||||
}
|
||||
|
||||
// don't care about sorting if using all utxos
|
||||
if (!coinControl) {
|
||||
// don't care about sorting if using all utxos
|
||||
} else {
|
||||
// sort spendable by age (oldest first)
|
||||
spendableOutputs.sort(
|
||||
(a, b) => (b.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(
|
||||
|
@ -161,26 +168,10 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
|
|||
);
|
||||
Logging.instance
|
||||
.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
|
||||
int satoshisBeingUsed = 0;
|
||||
BigInt satoshisBeingUsed = BigInt.zero;
|
||||
int inputsBeingConsumed = 0;
|
||||
List<UTXO> utxoObjectsToUse = [];
|
||||
final List<UTXO> utxoObjectsToUse = [];
|
||||
|
||||
if (!coinControl) {
|
||||
for (var i = 0;
|
||||
|
@ -188,7 +179,7 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
|
|||
i < spendableOutputs.length;
|
||||
i++) {
|
||||
utxoObjectsToUse.add(spendableOutputs[i]);
|
||||
satoshisBeingUsed += spendableOutputs[i].value;
|
||||
satoshisBeingUsed += BigInt.from(spendableOutputs[i].value);
|
||||
inputsBeingConsumed += 1;
|
||||
}
|
||||
for (int i = 0;
|
||||
|
@ -196,12 +187,13 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
|
|||
inputsBeingConsumed < spendableOutputs.length;
|
||||
i++) {
|
||||
utxoObjectsToUse.add(spendableOutputs[inputsBeingConsumed]);
|
||||
satoshisBeingUsed += spendableOutputs[inputsBeingConsumed].value;
|
||||
satoshisBeingUsed +=
|
||||
BigInt.from(spendableOutputs[inputsBeingConsumed].value);
|
||||
inputsBeingConsumed += 1;
|
||||
}
|
||||
} else {
|
||||
satoshisBeingUsed = spendableSatoshiValue;
|
||||
utxoObjectsToUse = spendableOutputs;
|
||||
utxoObjectsToUse.addAll(spendableOutputs);
|
||||
inputsBeingConsumed = spendableOutputs.length;
|
||||
}
|
||||
|
||||
|
@ -214,72 +206,20 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
|
|||
|
||||
// numberOfOutputs' length must always be equal to that of recipientsArray and recipientsAmtArray
|
||||
final List<String> recipientsArray = [recipientAddress];
|
||||
final List<int> recipientsAmtArray = [satoshiAmountToSend];
|
||||
final List<BigInt> recipientsAmtArray = [satoshiAmountToSend];
|
||||
|
||||
// gather required signing data
|
||||
final utxoSigningData = await fetchBuildTxData(utxoObjectsToUse);
|
||||
|
||||
if (isSendAll) {
|
||||
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(
|
||||
return await _sendAllBuilder(
|
||||
txData: txData,
|
||||
recipientAddress: recipientAddress,
|
||||
satoshiAmountToSend: satoshiAmountToSend,
|
||||
satoshisBeingUsed: satoshisBeingUsed,
|
||||
utxoSigningData: utxoSigningData,
|
||||
txData: txData.copyWith(
|
||||
recipients: await _helperRecipientsConvert(
|
||||
[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(),
|
||||
satsPerVByte: satsPerVByte,
|
||||
feeRatePerKB: selectedTxFeeRate,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -290,7 +230,7 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
|
|||
txData: txData.copyWith(
|
||||
recipients: await _helperRecipientsConvert(
|
||||
[recipientAddress],
|
||||
[satoshisBeingUsed - 1],
|
||||
[satoshisBeingUsed - BigInt.one],
|
||||
),
|
||||
),
|
||||
))
|
||||
|
@ -301,6 +241,9 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
|
|||
}
|
||||
|
||||
final int vSizeForTwoOutPuts;
|
||||
|
||||
BigInt maxBI(BigInt a, BigInt b) => a > b ? a : b;
|
||||
|
||||
try {
|
||||
vSizeForTwoOutPuts = (await buildTransaction(
|
||||
utxoSigningData: utxoSigningData,
|
||||
|
@ -309,7 +252,10 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
|
|||
[recipientAddress, (await getCurrentChangeAddress())!.value],
|
||||
[
|
||||
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
|
||||
final feeForOneOutput = satsPerVByte != null
|
||||
? (satsPerVByte * vSizeForOneOutput)
|
||||
: estimateTxFee(
|
||||
vSize: vSizeForOneOutput,
|
||||
feeRatePerKB: selectedTxFeeRate,
|
||||
);
|
||||
final feeForOneOutput = BigInt.from(
|
||||
satsPerVByte != null
|
||||
? (satsPerVByte * vSizeForOneOutput)
|
||||
: estimateTxFee(
|
||||
vSize: vSizeForOneOutput,
|
||||
feeRatePerKB: selectedTxFeeRate,
|
||||
),
|
||||
);
|
||||
// Assume 2 outputs, one for recipient and one for change
|
||||
final feeForTwoOutputs = satsPerVByte != null
|
||||
? (satsPerVByte * vSizeForTwoOutPuts)
|
||||
: estimateTxFee(
|
||||
vSize: vSizeForTwoOutPuts,
|
||||
feeRatePerKB: selectedTxFeeRate,
|
||||
);
|
||||
final feeForTwoOutputs = BigInt.from(
|
||||
satsPerVByte != null
|
||||
? (satsPerVByte * vSizeForTwoOutPuts)
|
||||
: estimateTxFee(
|
||||
vSize: vSizeForTwoOutPuts,
|
||||
feeRatePerKB: selectedTxFeeRate,
|
||||
),
|
||||
);
|
||||
|
||||
Logging.instance
|
||||
.log("feeForTwoOutputs: $feeForTwoOutputs", level: LogLevel.Info);
|
||||
Logging.instance
|
||||
.log("feeForOneOutput: $feeForOneOutput", level: LogLevel.Info);
|
||||
Logging.instance.log(
|
||||
"feeForTwoOutputs: $feeForTwoOutputs",
|
||||
level: LogLevel.Info,
|
||||
);
|
||||
Logging.instance.log(
|
||||
"feeForOneOutput: $feeForOneOutput",
|
||||
level: LogLevel.Info,
|
||||
);
|
||||
|
||||
if (satoshisBeingUsed - satoshiAmountToSend > feeForOneOutput) {
|
||||
if (satoshisBeingUsed - satoshiAmountToSend >
|
||||
feeForOneOutput + cryptoCurrency.dustLimit.raw.toInt()) {
|
||||
// Here, we know that theoretically, we may be able to include another output(change) but we first need to
|
||||
// factor in the value of this output in satoshis.
|
||||
final int changeOutputSize =
|
||||
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
|
||||
// the second output's size > cryptoCurrency.dustLimit satoshis, we perform the mechanics required to properly generate and use a new
|
||||
// change address.
|
||||
if (changeOutputSize > cryptoCurrency.dustLimit.raw.toInt() &&
|
||||
satoshisBeingUsed - satoshiAmountToSend - changeOutputSize ==
|
||||
feeForTwoOutputs) {
|
||||
final difference = satoshisBeingUsed - satoshiAmountToSend;
|
||||
|
||||
Future<TxData> singleOutputTxn() async {
|
||||
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: $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
|
||||
await checkChangeAddressForTransactions();
|
||||
final String newChangeAddress =
|
||||
(await getCurrentChangeAddress())!.value;
|
||||
|
||||
int feeBeingPaid =
|
||||
satoshisBeingUsed - satoshiAmountToSend - changeOutputSize;
|
||||
BigInt feeBeingPaid = difference - changeOutputSize;
|
||||
|
||||
// add change output
|
||||
recipientsArray.add(newChangeAddress);
|
||||
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('Input size: $satoshisBeingUsed', level: LogLevel.Info);
|
||||
Logging.instance.log(
|
||||
'Input size: $satoshisBeingUsed',
|
||||
level: LogLevel.Info,
|
||||
);
|
||||
Logging.instance.log(
|
||||
'Recipient output size: $satoshiAmountToSend',
|
||||
level: LogLevel.Info,
|
||||
|
@ -380,10 +385,12 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
|
|||
'Difference (fee being paid): $feeBeingPaid sats',
|
||||
level: LogLevel.Info,
|
||||
);
|
||||
Logging.instance
|
||||
.log('Estimated fee: $feeForTwoOutputs', level: LogLevel.Info);
|
||||
Logging.instance.log(
|
||||
'Estimated fee: $feeForTwoOutputs',
|
||||
level: LogLevel.Info,
|
||||
);
|
||||
|
||||
var txn = await buildTransaction(
|
||||
TxData txnData = await buildTransaction(
|
||||
utxoSigningData: utxoSigningData,
|
||||
txData: txData.copyWith(
|
||||
recipients: await _helperRecipientsConvert(
|
||||
|
@ -394,13 +401,12 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
|
|||
);
|
||||
|
||||
// make sure minimum fee is accurate if that is being used
|
||||
if (txn.vSize! - feeBeingPaid == 1) {
|
||||
final int changeOutputSize =
|
||||
satoshisBeingUsed - satoshiAmountToSend - txn.vSize!;
|
||||
feeBeingPaid =
|
||||
satoshisBeingUsed - satoshiAmountToSend - changeOutputSize;
|
||||
if (BigInt.from(txnData.vSize!) - feeBeingPaid == BigInt.one) {
|
||||
final changeOutputSize = difference - BigInt.from(txnData.vSize!);
|
||||
feeBeingPaid = difference - changeOutputSize;
|
||||
recipientsAmtArray.removeLast();
|
||||
recipientsAmtArray.add(changeOutputSize);
|
||||
|
||||
Logging.instance.log(
|
||||
'Adjusted Input size: $satoshisBeingUsed',
|
||||
level: LogLevel.Info,
|
||||
|
@ -421,7 +427,8 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
|
|||
'Adjusted Estimated fee: $feeForTwoOutputs',
|
||||
level: LogLevel.Info,
|
||||
);
|
||||
txn = await buildTransaction(
|
||||
|
||||
txnData = await buildTransaction(
|
||||
utxoSigningData: utxoSigningData,
|
||||
txData: txData.copyWith(
|
||||
recipients: await _helperRecipientsConvert(
|
||||
|
@ -432,9 +439,9 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
|
|||
);
|
||||
}
|
||||
|
||||
return txn.copyWith(
|
||||
return txnData.copyWith(
|
||||
fee: Amount(
|
||||
rawValue: BigInt.from(feeBeingPaid),
|
||||
rawValue: feeBeingPaid,
|
||||
fractionDigits: cryptoCurrency.fractionDigits,
|
||||
),
|
||||
usedUTXOs: utxoSigningData.map((e) => e.utxo).toList(),
|
||||
|
@ -442,126 +449,91 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
|
|||
} else {
|
||||
// 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.
|
||||
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',
|
||||
'Reverting to 1 output in tx',
|
||||
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(),
|
||||
);
|
||||
return await singleOutputTxn();
|
||||
}
|
||||
} 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(
|
||||
|
|
Loading…
Reference in a new issue