mirror of
https://github.com/cypherstack/stack_wallet.git
synced 2024-11-16 17:27:39 +00:00
WIP comprehensive full RBF
This commit is contained in:
parent
b1f5a1ec53
commit
b7b28115b2
1 changed files with 196 additions and 38 deletions
|
@ -6,6 +6,7 @@ import '../../../models/isar/models/blockchain_data/v2/transaction_v2.dart';
|
||||||
import '../../../models/isar/models/isar_models.dart';
|
import '../../../models/isar/models/isar_models.dart';
|
||||||
import '../../../utilities/amount/amount.dart';
|
import '../../../utilities/amount/amount.dart';
|
||||||
import '../../../utilities/enums/fee_rate_type_enum.dart';
|
import '../../../utilities/enums/fee_rate_type_enum.dart';
|
||||||
|
import '../../../utilities/logger.dart';
|
||||||
import '../../crypto_currency/interfaces/electrumx_currency_interface.dart';
|
import '../../crypto_currency/interfaces/electrumx_currency_interface.dart';
|
||||||
import '../../models/tx_data.dart';
|
import '../../models/tx_data.dart';
|
||||||
import 'electrumx_interface.dart';
|
import 'electrumx_interface.dart';
|
||||||
|
@ -41,6 +42,13 @@ mixin RbfInterface<T extends ElectrumXCurrencyInterface>
|
||||||
required TransactionV2 oldTransaction,
|
required TransactionV2 oldTransaction,
|
||||||
required int newRate,
|
required int newRate,
|
||||||
}) async {
|
}) async {
|
||||||
|
final note = await mainDB.isar.transactionNotes
|
||||||
|
.where()
|
||||||
|
.walletIdEqualTo(walletId)
|
||||||
|
.filter()
|
||||||
|
.txidEqualTo(oldTransaction.txid)
|
||||||
|
.findFirst();
|
||||||
|
|
||||||
final Set<UTXO> utxos = {};
|
final Set<UTXO> utxos = {};
|
||||||
for (final input in oldTransaction.inputs) {
|
for (final input in oldTransaction.inputs) {
|
||||||
final utxo = UTXO(
|
final utxo = UTXO(
|
||||||
|
@ -62,51 +70,56 @@ mixin RbfInterface<T extends ElectrumXCurrencyInterface>
|
||||||
utxos.add(utxo);
|
utxos.add(utxo);
|
||||||
}
|
}
|
||||||
|
|
||||||
Amount sendAmount = oldTransaction.getAmountSentFromThisWallet(
|
final List<TxRecipient> recipients = [];
|
||||||
fractionDigits: cryptoCurrency.fractionDigits,
|
for (final output in oldTransaction.outputs) {
|
||||||
);
|
if (output.addresses.length != 1) {
|
||||||
|
throw UnsupportedError(
|
||||||
|
"Unexpected output.addresses.length: ${output.addresses.length}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final address = output.addresses.first;
|
||||||
|
final addressModel = await mainDB.getAddress(walletId, address);
|
||||||
|
final isChange = addressModel?.subType == AddressSubType.change;
|
||||||
|
|
||||||
// TODO: fix fragile firstWhere (or at least add some error checking)
|
recipients.add(
|
||||||
final address = oldTransaction.outputs
|
(
|
||||||
.firstWhere(
|
address: address,
|
||||||
(e) => e.value == sendAmount.raw,
|
amount: Amount(
|
||||||
)
|
rawValue: output.value,
|
||||||
.addresses
|
fractionDigits: cryptoCurrency.fractionDigits),
|
||||||
.first;
|
isChange: isChange,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final oldFee = oldTransaction
|
||||||
|
.getFee(fractionDigits: cryptoCurrency.fractionDigits)
|
||||||
|
.raw;
|
||||||
final inSum = utxos
|
final inSum = utxos
|
||||||
.map((e) => BigInt.from(e.value))
|
.map((e) => BigInt.from(e.value))
|
||||||
.fold(BigInt.zero, (p, e) => p + e);
|
.fold(BigInt.zero, (p, e) => p + e);
|
||||||
|
|
||||||
if (oldTransaction
|
final noChange =
|
||||||
.getFee(fractionDigits: cryptoCurrency.fractionDigits)
|
recipients.map((e) => e.isChange).fold(false, (p, e) => p || e) ==
|
||||||
.raw +
|
false;
|
||||||
sendAmount.raw ==
|
final otherAvailableUtxos = await mainDB
|
||||||
inSum) {
|
.getUTXOs(walletId)
|
||||||
sendAmount = Amount(
|
|
||||||
rawValue: oldTransaction
|
|
||||||
.getFee(fractionDigits: cryptoCurrency.fractionDigits)
|
|
||||||
.raw +
|
|
||||||
sendAmount.raw,
|
|
||||||
fractionDigits: cryptoCurrency.fractionDigits,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final note = await mainDB.isar.transactionNotes
|
|
||||||
.where()
|
|
||||||
.walletIdEqualTo(walletId)
|
|
||||||
.filter()
|
.filter()
|
||||||
.txidEqualTo(oldTransaction.txid)
|
.usedIsNull()
|
||||||
.findFirst();
|
.or()
|
||||||
|
.usedEqualTo(false)
|
||||||
|
.findAll();
|
||||||
|
|
||||||
final txData = TxData(
|
final height = await chainHeight;
|
||||||
recipients: [
|
otherAvailableUtxos.removeWhere(
|
||||||
(
|
(e) => !e.isConfirmed(
|
||||||
address: address,
|
height,
|
||||||
amount: sendAmount,
|
cryptoCurrency.minConfirms,
|
||||||
isChange: false,
|
),
|
||||||
),
|
);
|
||||||
],
|
|
||||||
|
TxData txData = TxData(
|
||||||
|
recipients: recipients,
|
||||||
feeRateType: FeeRateType.custom,
|
feeRateType: FeeRateType.custom,
|
||||||
satsPerVByte: newRate,
|
satsPerVByte: newRate,
|
||||||
utxos: utxos,
|
utxos: utxos,
|
||||||
|
@ -114,6 +127,151 @@ mixin RbfInterface<T extends ElectrumXCurrencyInterface>
|
||||||
note: note?.value ?? "",
|
note: note?.value ?? "",
|
||||||
);
|
);
|
||||||
|
|
||||||
return await prepareSend(txData: txData);
|
if (otherAvailableUtxos.isEmpty && noChange && recipients.length == 1) {
|
||||||
|
// safe to assume send all?
|
||||||
|
txData = txData.copyWith(
|
||||||
|
recipients: [
|
||||||
|
(
|
||||||
|
address: recipients.first.address,
|
||||||
|
amount: Amount(
|
||||||
|
rawValue: inSum,
|
||||||
|
fractionDigits: cryptoCurrency.fractionDigits,
|
||||||
|
),
|
||||||
|
isChange: false,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
Logging.instance.log(
|
||||||
|
"RBF on assumed send all",
|
||||||
|
level: LogLevel.Debug,
|
||||||
|
);
|
||||||
|
return await prepareSend(txData: txData);
|
||||||
|
} else if (txData.recipients!.where((e) => e.isChange).length == 1) {
|
||||||
|
final newFee = BigInt.from(oldTransaction.vSize! * newRate);
|
||||||
|
final feeDifferenceRequired = newFee - oldFee;
|
||||||
|
if (feeDifferenceRequired < BigInt.zero) {
|
||||||
|
throw Exception("Negative new fee in RBF found");
|
||||||
|
} else if (feeDifferenceRequired == BigInt.zero) {
|
||||||
|
throw Exception("New fee in RBF has not changed at all");
|
||||||
|
}
|
||||||
|
|
||||||
|
final indexOfChangeOutput =
|
||||||
|
txData.recipients!.indexWhere((e) => e.isChange);
|
||||||
|
|
||||||
|
final removed = txData.recipients!.removeAt(indexOfChangeOutput);
|
||||||
|
|
||||||
|
BigInt newChangeAmount = removed.amount.raw - feeDifferenceRequired;
|
||||||
|
|
||||||
|
if (newChangeAmount >= BigInt.zero) {
|
||||||
|
if (newChangeAmount >= cryptoCurrency.dustLimit.raw) {
|
||||||
|
// yay we have enough
|
||||||
|
// update recipients
|
||||||
|
txData.recipients!.insert(
|
||||||
|
indexOfChangeOutput,
|
||||||
|
(
|
||||||
|
address: removed.address,
|
||||||
|
amount: Amount(
|
||||||
|
rawValue: newChangeAmount,
|
||||||
|
fractionDigits: cryptoCurrency.fractionDigits,
|
||||||
|
),
|
||||||
|
isChange: removed.isChange,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
Logging.instance.log(
|
||||||
|
"RBF with same utxo set with increased fee and reduced change",
|
||||||
|
level: LogLevel.Debug,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// new change amount is less than dust limit.
|
||||||
|
// TODO: check if worth adding another utxo?
|
||||||
|
// depending on several factors, it may be cheaper to just add]
|
||||||
|
// the dust to the fee...
|
||||||
|
// we'll do that for now... aka remove the change output entirely
|
||||||
|
// which now that I think about it, will reduce the size of the tx...
|
||||||
|
// oh well...
|
||||||
|
|
||||||
|
// do nothing here as we already removed the change output above
|
||||||
|
Logging.instance.log(
|
||||||
|
"RBF with same utxo set with increased fee and no change",
|
||||||
|
level: LogLevel.Debug,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return await buildTransaction(
|
||||||
|
txData: txData.copyWith(
|
||||||
|
usedUTXOs: txData.utxos!.toList(),
|
||||||
|
fee: Amount(
|
||||||
|
rawValue: newFee,
|
||||||
|
fractionDigits: cryptoCurrency.fractionDigits,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
utxoSigningData: await fetchBuildTxData(txData.utxos!.toList()),
|
||||||
|
);
|
||||||
|
|
||||||
|
// if change amount is negative
|
||||||
|
} else {
|
||||||
|
// we need more utxos
|
||||||
|
if (otherAvailableUtxos.isEmpty) {
|
||||||
|
throw Exception("Insufficient funds to pay for increased fee");
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<UTXO> extraUtxos = [];
|
||||||
|
for (int i = 0; i < otherAvailableUtxos.length; i++) {
|
||||||
|
final utxoToAdd = otherAvailableUtxos[i];
|
||||||
|
newChangeAmount += BigInt.from(utxoToAdd.value);
|
||||||
|
extraUtxos.add(utxoToAdd);
|
||||||
|
|
||||||
|
if (newChangeAmount >= cryptoCurrency.dustLimit.raw) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newChangeAmount < cryptoCurrency.dustLimit.raw) {
|
||||||
|
throw Exception("Insufficient funds to pay for increased fee");
|
||||||
|
}
|
||||||
|
txData.recipients!.insert(
|
||||||
|
indexOfChangeOutput,
|
||||||
|
(
|
||||||
|
address: removed.address,
|
||||||
|
amount: Amount(
|
||||||
|
rawValue: newChangeAmount,
|
||||||
|
fractionDigits: cryptoCurrency.fractionDigits,
|
||||||
|
),
|
||||||
|
isChange: removed.isChange,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final newUtxoSet = {
|
||||||
|
...txData.utxos!,
|
||||||
|
...extraUtxos,
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: remove assert
|
||||||
|
assert(newUtxoSet.length == txData.utxos!.length + extraUtxos.length);
|
||||||
|
|
||||||
|
Logging.instance.log(
|
||||||
|
"RBF with ${extraUtxos.length} extra utxo(s)"
|
||||||
|
" added to pay for the new fee",
|
||||||
|
level: LogLevel.Debug,
|
||||||
|
);
|
||||||
|
|
||||||
|
return await buildTransaction(
|
||||||
|
txData: txData.copyWith(
|
||||||
|
utxos: newUtxoSet,
|
||||||
|
usedUTXOs: newUtxoSet.toList(),
|
||||||
|
fee: Amount(
|
||||||
|
rawValue: newFee,
|
||||||
|
fractionDigits: cryptoCurrency.fractionDigits,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
utxoSigningData: await fetchBuildTxData(newUtxoSet.toList()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// TODO handle building a tx here in this case
|
||||||
|
throw Exception(
|
||||||
|
"Unexpected number of change outputs found:"
|
||||||
|
" ${txData.recipients!.where((e) => e.isChange).length}",
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue