notification tx absurd fees error fix when estimating a tx size

This commit is contained in:
julian 2023-02-22 15:06:45 -06:00
parent 55ed68a89d
commit 6bb133c552
2 changed files with 250 additions and 213 deletions

View file

@ -26,6 +26,7 @@ import 'package:stackwallet/widgets/desktop/primary_button.dart';
import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/desktop/secondary_button.dart';
import 'package:stackwallet/widgets/loading_indicator.dart'; import 'package:stackwallet/widgets/loading_indicator.dart';
import 'package:stackwallet/widgets/rounded_container.dart'; import 'package:stackwallet/widgets/rounded_container.dart';
import 'package:stackwallet/widgets/stack_dialog.dart';
import 'package:tuple/tuple.dart'; import 'package:tuple/tuple.dart';
class PaynymDetailsPopup extends ConsumerStatefulWidget { class PaynymDetailsPopup extends ConsumerStatefulWidget {
@ -102,6 +103,20 @@ class _PaynymDetailsPopupState extends ConsumerState<PaynymDetailsPopup> {
_showInsufficientFundsInfo = true; _showInsufficientFundsInfo = true;
}); });
return; return;
} catch (e) {
if (mounted) {
canPop = true;
Navigator.of(context).pop();
}
await showDialog<void>(
context: context,
builder: (context) => StackOkDialog(
title: "Error",
message: e.toString(),
),
);
return;
} }
if (mounted) { if (mounted) {

View file

@ -398,135 +398,162 @@ mixin PaynymWalletInterface {
int additionalOutputs = 0, int additionalOutputs = 0,
List<UTXO>? utxos, List<UTXO>? utxos,
}) async { }) async {
final amountToSend = _dustLimitP2PKH; try {
final List<UTXO> availableOutputs = final amountToSend = _dustLimitP2PKH;
utxos ?? await _db.getUTXOs(_walletId).findAll(); final List<UTXO> availableOutputs =
final List<UTXO> spendableOutputs = []; utxos ?? await _db.getUTXOs(_walletId).findAll();
int spendableSatoshiValue = 0; final List<UTXO> spendableOutputs = [];
int spendableSatoshiValue = 0;
// Build list of spendable outputs and totaling their satoshi amount // Build list of spendable outputs and totaling their satoshi amount
for (var i = 0; i < availableOutputs.length; i++) { for (var i = 0; i < availableOutputs.length; i++) {
if (availableOutputs[i].isBlocked == false && if (availableOutputs[i].isBlocked == false &&
availableOutputs[i] availableOutputs[i]
.isConfirmed(await _getChainHeight(), _minConfirms) == .isConfirmed(await _getChainHeight(), _minConfirms) ==
true) { true) {
spendableOutputs.add(availableOutputs[i]); spendableOutputs.add(availableOutputs[i]);
spendableSatoshiValue += availableOutputs[i].value; spendableSatoshiValue += availableOutputs[i].value;
}
} }
}
if (spendableSatoshiValue < amountToSend) { if (spendableSatoshiValue < amountToSend) {
// insufficient balance // insufficient balance
throw InsufficientBalanceException( throw InsufficientBalanceException(
"Spendable balance is less than the minimum required for a notification transaction."); "Spendable balance is less than the minimum required for a notification transaction.");
} else if (spendableSatoshiValue == amountToSend) { } else if (spendableSatoshiValue == amountToSend) {
// insufficient balance due to missing amount to cover fee // insufficient balance due to missing amount to cover fee
throw InsufficientBalanceException( throw InsufficientBalanceException(
"Remaining balance does not cover the network fee."); "Remaining balance does not cover the network fee.");
}
// sort spendable by age (oldest first)
spendableOutputs.sort((a, b) => b.blockTime!.compareTo(a.blockTime!));
int satoshisBeingUsed = 0;
int outputsBeingUsed = 0;
List<UTXO> utxoObjectsToUse = [];
for (int i = 0;
satoshisBeingUsed < amountToSend && i < spendableOutputs.length;
i++) {
utxoObjectsToUse.add(spendableOutputs[i]);
satoshisBeingUsed += spendableOutputs[i].value;
outputsBeingUsed += 1;
}
// add additional outputs if required
for (int i = 0;
i < additionalOutputs && outputsBeingUsed < spendableOutputs.length;
i++) {
utxoObjectsToUse.add(spendableOutputs[outputsBeingUsed]);
satoshisBeingUsed += spendableOutputs[outputsBeingUsed].value;
outputsBeingUsed += 1;
}
// gather required signing data
final utxoSigningData = await _fetchBuildTxData(utxoObjectsToUse);
final int vSizeForNoChange = (await _createNotificationTx(
targetPaymentCodeString: targetPaymentCodeString,
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData,
change: 0))
.item2;
final int vSizeForWithChange = (await _createNotificationTx(
targetPaymentCodeString: targetPaymentCodeString,
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData,
change: satoshisBeingUsed - amountToSend))
.item2;
// Assume 2 outputs, for recipient and payment code script
int feeForNoChange = _estimateTxFee(
vSize: vSizeForNoChange,
feeRatePerKB: selectedTxFeeRate,
);
// Assume 3 outputs, for recipient, payment code script, and change
int feeForWithChange = _estimateTxFee(
vSize: vSizeForWithChange,
feeRatePerKB: selectedTxFeeRate,
);
if (_coin == Coin.dogecoin || _coin == Coin.dogecoinTestNet) {
if (feeForNoChange < vSizeForNoChange * 1000) {
feeForNoChange = vSizeForNoChange * 1000;
} }
if (feeForWithChange < vSizeForWithChange * 1000) {
feeForWithChange = vSizeForWithChange * 1000; // sort spendable by age (oldest first)
spendableOutputs.sort((a, b) => b.blockTime!.compareTo(a.blockTime!));
int satoshisBeingUsed = 0;
int outputsBeingUsed = 0;
List<UTXO> utxoObjectsToUse = [];
for (int i = 0;
satoshisBeingUsed < amountToSend && i < spendableOutputs.length;
i++) {
utxoObjectsToUse.add(spendableOutputs[i]);
satoshisBeingUsed += spendableOutputs[i].value;
outputsBeingUsed += 1;
} }
}
if (satoshisBeingUsed - amountToSend > feeForNoChange + _dustLimitP2PKH) { // add additional outputs if required
// try to add change output due to "left over" amount being greater than for (int i = 0;
// the estimated fee + the dust limit i < additionalOutputs && outputsBeingUsed < spendableOutputs.length;
int changeAmount = satoshisBeingUsed - amountToSend - feeForWithChange; i++) {
utxoObjectsToUse.add(spendableOutputs[outputsBeingUsed]);
satoshisBeingUsed += spendableOutputs[outputsBeingUsed].value;
outputsBeingUsed += 1;
}
// check estimates are correct and build notification tx // gather required signing data
if (changeAmount >= _dustLimitP2PKH && final utxoSigningData = await _fetchBuildTxData(utxoObjectsToUse);
satoshisBeingUsed - amountToSend - changeAmount == feeForWithChange) {
var txn = await _createNotificationTx(
targetPaymentCodeString: targetPaymentCodeString,
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData,
change: changeAmount,
);
int feeBeingPaid = satoshisBeingUsed - amountToSend - changeAmount; final int vSizeForNoChange = (await _createNotificationTx(
targetPaymentCodeString: targetPaymentCodeString,
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData,
change: 0,
dustLimit:
satoshisBeingUsed, // override amount to get around absurd fees error
))
.item2;
// make sure minimum fee is accurate if that is being used final int vSizeForWithChange = (await _createNotificationTx(
if (txn.item2 - feeBeingPaid == 1) { targetPaymentCodeString: targetPaymentCodeString,
changeAmount -= 1; utxosToUse: utxoObjectsToUse,
feeBeingPaid += 1; utxoSigningData: utxoSigningData,
txn = await _createNotificationTx( change: satoshisBeingUsed - amountToSend,
))
.item2;
// Assume 2 outputs, for recipient and payment code script
int feeForNoChange = _estimateTxFee(
vSize: vSizeForNoChange,
feeRatePerKB: selectedTxFeeRate,
);
// Assume 3 outputs, for recipient, payment code script, and change
int feeForWithChange = _estimateTxFee(
vSize: vSizeForWithChange,
feeRatePerKB: selectedTxFeeRate,
);
if (_coin == Coin.dogecoin || _coin == Coin.dogecoinTestNet) {
if (feeForNoChange < vSizeForNoChange * 1000) {
feeForNoChange = vSizeForNoChange * 1000;
}
if (feeForWithChange < vSizeForWithChange * 1000) {
feeForWithChange = vSizeForWithChange * 1000;
}
}
if (satoshisBeingUsed - amountToSend > feeForNoChange + _dustLimitP2PKH) {
// try to add change output due to "left over" amount being greater than
// the estimated fee + the dust limit
int changeAmount = satoshisBeingUsed - amountToSend - feeForWithChange;
// check estimates are correct and build notification tx
if (changeAmount >= _dustLimitP2PKH &&
satoshisBeingUsed - amountToSend - changeAmount ==
feeForWithChange) {
var txn = await _createNotificationTx(
targetPaymentCodeString: targetPaymentCodeString, targetPaymentCodeString: targetPaymentCodeString,
utxosToUse: utxoObjectsToUse, utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData, utxoSigningData: utxoSigningData,
change: changeAmount, change: changeAmount,
); );
}
Map<String, dynamic> transactionObject = { int feeBeingPaid = satoshisBeingUsed - amountToSend - changeAmount;
"hex": txn.item1,
"recipientPaynym": targetPaymentCodeString, // make sure minimum fee is accurate if that is being used
"amount": amountToSend, if (txn.item2 - feeBeingPaid == 1) {
"fee": feeBeingPaid, changeAmount -= 1;
"vSize": txn.item2, feeBeingPaid += 1;
}; txn = await _createNotificationTx(
return transactionObject; targetPaymentCodeString: targetPaymentCodeString,
} else { utxosToUse: utxoObjectsToUse,
// something broke during fee estimation or the change amount is smaller utxoSigningData: utxoSigningData,
// than the dust limit. Try without change change: changeAmount,
);
}
Map<String, dynamic> transactionObject = {
"hex": txn.item1,
"recipientPaynym": targetPaymentCodeString,
"amount": amountToSend,
"fee": feeBeingPaid,
"vSize": txn.item2,
};
return transactionObject;
} else {
// something broke during fee estimation or the change amount is smaller
// than the dust limit. Try without change
final txn = await _createNotificationTx(
targetPaymentCodeString: targetPaymentCodeString,
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData,
change: 0,
);
int feeBeingPaid = satoshisBeingUsed - amountToSend;
Map<String, dynamic> transactionObject = {
"hex": txn.item1,
"recipientPaynym": targetPaymentCodeString,
"amount": amountToSend,
"fee": feeBeingPaid,
"vSize": txn.item2,
};
return transactionObject;
}
} else if (satoshisBeingUsed - amountToSend >= feeForNoChange) {
// since we already checked if we need to add a change output we can just
// build without change here
final txn = await _createNotificationTx( final txn = await _createNotificationTx(
targetPaymentCodeString: targetPaymentCodeString, targetPaymentCodeString: targetPaymentCodeString,
utxosToUse: utxoObjectsToUse, utxosToUse: utxoObjectsToUse,
@ -544,40 +571,22 @@ mixin PaynymWalletInterface {
"vSize": txn.item2, "vSize": txn.item2,
}; };
return transactionObject; return transactionObject;
}
} else if (satoshisBeingUsed - amountToSend >= feeForNoChange) {
// since we already checked if we need to add a change output we can just
// build without change here
final txn = await _createNotificationTx(
targetPaymentCodeString: targetPaymentCodeString,
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData,
change: 0,
);
int feeBeingPaid = satoshisBeingUsed - amountToSend;
Map<String, dynamic> transactionObject = {
"hex": txn.item1,
"recipientPaynym": targetPaymentCodeString,
"amount": amountToSend,
"fee": feeBeingPaid,
"vSize": txn.item2,
};
return transactionObject;
} else {
// if we get here we do not have enough funds to cover the tx total so we
// check if we have any more available outputs and try again
if (spendableOutputs.length > outputsBeingUsed) {
return prepareNotificationTx(
selectedTxFeeRate: selectedTxFeeRate,
targetPaymentCodeString: targetPaymentCodeString,
additionalOutputs: additionalOutputs + 1,
);
} else { } else {
throw InsufficientBalanceException( // if we get here we do not have enough funds to cover the tx total so we
"Remaining balance does not cover the network fee."); // check if we have any more available outputs and try again
if (spendableOutputs.length > outputsBeingUsed) {
return prepareNotificationTx(
selectedTxFeeRate: selectedTxFeeRate,
targetPaymentCodeString: targetPaymentCodeString,
additionalOutputs: additionalOutputs + 1,
);
} else {
throw InsufficientBalanceException(
"Remaining balance does not cover the network fee.");
}
} }
} catch (e) {
rethrow;
} }
} }
@ -588,96 +597,109 @@ mixin PaynymWalletInterface {
required List<UTXO> utxosToUse, required List<UTXO> utxosToUse,
required Map<String, dynamic> utxoSigningData, required Map<String, dynamic> utxoSigningData,
required int change, required int change,
int? dustLimit,
}) async { }) async {
final targetPaymentCode = try {
PaymentCode.fromPaymentCode(targetPaymentCodeString, _network); final targetPaymentCode =
final myCode = await getPaymentCode(DerivePathType.bip44); PaymentCode.fromPaymentCode(targetPaymentCodeString, _network);
final myCode = await getPaymentCode(DerivePathType.bip44);
final utxo = utxosToUse.first; final utxo = utxosToUse.first;
final txPoint = utxo.txid.fromHex.toList(); final txPoint = utxo.txid.fromHex.toList();
final txPointIndex = utxo.vout; final txPointIndex = utxo.vout;
final rev = Uint8List(txPoint.length + 4); final rev = Uint8List(txPoint.length + 4);
Util.copyBytes(Uint8List.fromList(txPoint), 0, rev, 0, txPoint.length); Util.copyBytes(Uint8List.fromList(txPoint), 0, rev, 0, txPoint.length);
final buffer = rev.buffer.asByteData(); final buffer = rev.buffer.asByteData();
buffer.setUint32(txPoint.length, txPointIndex, Endian.little); buffer.setUint32(txPoint.length, txPointIndex, Endian.little);
final myKeyPair = utxoSigningData[utxo.txid]["keyPair"] as btc_dart.ECPair; final myKeyPair =
utxoSigningData[utxo.txid]["keyPair"] as btc_dart.ECPair;
final S = SecretPoint( final S = SecretPoint(
myKeyPair.privateKey!, myKeyPair.privateKey!,
targetPaymentCode.notificationPublicKey(), targetPaymentCode.notificationPublicKey(),
); );
final blindingMask = PaymentCode.getMask(S.ecdhSecret(), rev); final blindingMask = PaymentCode.getMask(S.ecdhSecret(), rev);
final blindedPaymentCode = PaymentCode.blind( final blindedPaymentCode = PaymentCode.blind(
payload: myCode.getPayload(), payload: myCode.getPayload(),
mask: blindingMask, mask: blindingMask,
unBlind: false, unBlind: false,
); );
final opReturnScript = bscript.compile([ final opReturnScript = bscript.compile([
(op.OPS["OP_RETURN"] as int), (op.OPS["OP_RETURN"] as int),
blindedPaymentCode, blindedPaymentCode,
]); ]);
// build a notification tx // build a notification tx
final txb = btc_dart.TransactionBuilder(network: _network); final txb = btc_dart.TransactionBuilder(network: _network);
txb.setVersion(1); txb.setVersion(1);
txb.addInput(
utxo.txid,
txPointIndex,
null,
utxoSigningData[utxo.txid]["output"] as Uint8List,
);
// add rest of possible inputs
for (var i = 1; i < utxosToUse.length; i++) {
final utxo = utxosToUse[i];
txb.addInput( txb.addInput(
utxo.txid, utxo.txid,
utxo.vout, txPointIndex,
null, null,
utxoSigningData[utxo.txid]["output"] as Uint8List, utxoSigningData[utxo.txid]["output"] as Uint8List,
); );
}
// todo: modify address once segwit support is in our bip47 // add rest of possible inputs
txb.addOutput( for (var i = 1; i < utxosToUse.length; i++) {
targetPaymentCode.notificationAddressP2PKH(), _dustLimitP2PKH); final utxo = utxosToUse[i];
txb.addOutput(opReturnScript, 0); txb.addInput(
utxo.txid,
utxo.vout,
null,
utxoSigningData[utxo.txid]["output"] as Uint8List,
);
}
// TODO: add possible change output and mark output as dangerous // todo: modify address once segwit support is in our bip47
if (change > 0) { txb.addOutput(
// generate new change address if current change address has been used targetPaymentCode.notificationAddressP2PKH(),
await _checkChangeAddressForTransactions(); dustLimit ?? _dustLimitP2PKH,
final String changeAddress = await _getCurrentChangeAddress(); );
txb.addOutput(changeAddress, change); txb.addOutput(opReturnScript, 0);
}
txb.sign( // TODO: add possible change output and mark output as dangerous
vin: 0, if (change > 0) {
keyPair: myKeyPair, // generate new change address if current change address has been used
witnessValue: utxo.value, await _checkChangeAddressForTransactions();
witnessScript: utxoSigningData[utxo.txid]["redeemScript"] as Uint8List?, final String changeAddress = await _getCurrentChangeAddress();
); txb.addOutput(changeAddress, change);
}
// sign rest of possible inputs
for (var i = 1; i < utxosToUse.length; i++) {
final txid = utxosToUse[i].txid;
txb.sign( txb.sign(
vin: i, vin: 0,
keyPair: utxoSigningData[txid]["keyPair"] as btc_dart.ECPair, keyPair: myKeyPair,
witnessValue: utxosToUse[i].value, witnessValue: utxo.value,
witnessScript: utxoSigningData[utxo.txid]["redeemScript"] as Uint8List?, witnessScript: utxoSigningData[utxo.txid]["redeemScript"] as Uint8List?,
); );
// sign rest of possible inputs
for (var i = 1; i < utxosToUse.length; i++) {
final txid = utxosToUse[i].txid;
txb.sign(
vin: i,
keyPair: utxoSigningData[txid]["keyPair"] as btc_dart.ECPair,
witnessValue: utxosToUse[i].value,
witnessScript:
utxoSigningData[utxo.txid]["redeemScript"] as Uint8List?,
);
}
final builtTx = txb.build();
return Tuple2(builtTx.toHex(), builtTx.virtualSize());
} catch (e, s) {
Logging.instance.log(
"_createNotificationTx(): $e\n$s",
level: LogLevel.Error,
);
rethrow;
} }
final builtTx = txb.build();
return Tuple2(builtTx.toHex(), builtTx.virtualSize());
} }
Future<String> broadcastNotificationTx( Future<String> broadcastNotificationTx(