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: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
|
// don't care about sorting if using all utxos
|
||||||
if (!coinControl) {
|
} else {
|
||||||
// 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,73 +206,21 @@ 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(
|
|
||||||
[recipientAddress],
|
|
||||||
[satoshisBeingUsed - 1],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
))
|
|
||||||
.vSize!;
|
|
||||||
int feeForOneOutput = satsPerVByte != null
|
|
||||||
? (satsPerVByte * vSizeForOneOutput)
|
|
||||||
: estimateTxFee(
|
|
||||||
vSize: vSizeForOneOutput,
|
|
||||||
feeRatePerKB: selectedTxFeeRate,
|
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(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final int vSizeForOneOutput;
|
final int vSizeForOneOutput;
|
||||||
|
@ -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 != null
|
||||||
? (satsPerVByte * vSizeForOneOutput)
|
? (satsPerVByte * vSizeForOneOutput)
|
||||||
: estimateTxFee(
|
: estimateTxFee(
|
||||||
vSize: vSizeForOneOutput,
|
vSize: vSizeForOneOutput,
|
||||||
feeRatePerKB: selectedTxFeeRate,
|
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 != null
|
||||||
? (satsPerVByte * vSizeForTwoOutPuts)
|
? (satsPerVByte * vSizeForTwoOutPuts)
|
||||||
: estimateTxFee(
|
: estimateTxFee(
|
||||||
vSize: vSizeForTwoOutPuts,
|
vSize: vSizeForTwoOutPuts,
|
||||||
feeRatePerKB: selectedTxFeeRate,
|
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,127 +449,92 @@ 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,
|
|
||||||
);
|
|
||||||
Logging.instance.log(
|
|
||||||
'Difference (fee being paid): ${satoshisBeingUsed - satoshiAmountToSend} sats',
|
|
||||||
level: LogLevel.Info,
|
level: LogLevel.Info,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return await singleOutputTxn();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
Logging.instance
|
||||||
.log('Estimated fee: $feeForOneOutput', level: LogLevel.Info);
|
.log("Attempting to send all $cryptoCurrency", level: LogLevel.Info);
|
||||||
final txn = await buildTransaction(
|
if (txData.recipients!.length != 1) {
|
||||||
|
throw Exception(
|
||||||
|
"Send all to more than one recipient not yet supported",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final int vSizeForOneOutput = (await buildTransaction(
|
||||||
utxoSigningData: utxoSigningData,
|
utxoSigningData: utxoSigningData,
|
||||||
txData: txData.copyWith(
|
txData: txData.copyWith(
|
||||||
recipients: await _helperRecipientsConvert(
|
recipients: await _helperRecipientsConvert(
|
||||||
recipientsArray,
|
[recipientAddress],
|
||||||
recipientsAmtArray,
|
[satoshisBeingUsed - BigInt.one],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
))
|
||||||
|
.vSize!;
|
||||||
|
BigInt feeForOneOutput = BigInt.from(
|
||||||
|
satsPerVByte != null
|
||||||
|
? (satsPerVByte * vSizeForOneOutput)
|
||||||
|
: estimateTxFee(
|
||||||
|
vSize: vSizeForOneOutput,
|
||||||
|
feeRatePerKB: feeRatePerKB,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
return txn.copyWith(
|
if (satsPerVByte == null) {
|
||||||
fee: Amount(
|
final roughEstimate = roughFeeEstimate(
|
||||||
rawValue: BigInt.from(satoshisBeingUsed - satoshiAmountToSend),
|
utxoSigningData.length,
|
||||||
fractionDigits: cryptoCurrency.fractionDigits,
|
1,
|
||||||
),
|
feeRatePerKB,
|
||||||
usedUTXOs: utxoSigningData.map((e) => e.utxo).toList(),
|
).raw;
|
||||||
|
if (feeForOneOutput < roughEstimate) {
|
||||||
|
feeForOneOutput = roughEstimate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final amount = satoshiAmountToSend - feeForOneOutput;
|
||||||
|
|
||||||
|
if (amount.isNegative) {
|
||||||
|
throw Exception(
|
||||||
|
"Estimated fee ($feeForOneOutput sats) is greater than balance!",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// No additional outputs needed since adding one would mean that it'd be smaller than cryptoCurrency.dustLimit sats
|
final data = await buildTransaction(
|
||||||
// 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(
|
txData: txData.copyWith(
|
||||||
recipients: await _helperRecipientsConvert(
|
recipients: await _helperRecipientsConvert(
|
||||||
recipientsArray,
|
[recipientAddress],
|
||||||
recipientsAmtArray,
|
[amount],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
utxoSigningData: utxoSigningData,
|
||||||
);
|
);
|
||||||
|
|
||||||
return txn.copyWith(
|
return data.copyWith(
|
||||||
fee: Amount(
|
fee: Amount(
|
||||||
rawValue: BigInt.from(satoshisBeingUsed - satoshiAmountToSend),
|
rawValue: feeForOneOutput,
|
||||||
fractionDigits: cryptoCurrency.fractionDigits,
|
fractionDigits: cryptoCurrency.fractionDigits,
|
||||||
),
|
),
|
||||||
usedUTXOs: utxoSigningData.map((e) => e.utxo).toList(),
|
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<List<SigningData>> fetchBuildTxData(
|
Future<List<SigningData>> fetchBuildTxData(
|
||||||
List<UTXO> utxosToUse,
|
List<UTXO> utxosToUse,
|
||||||
|
|
Loading…
Reference in a new issue