diff --git a/lib/pages/paynym/dialogs/paynym_details_popup.dart b/lib/pages/paynym/dialogs/paynym_details_popup.dart index 729ed5941..c6dedeb39 100644 --- a/lib/pages/paynym/dialogs/paynym_details_popup.dart +++ b/lib/pages/paynym/dialogs/paynym_details_popup.dart @@ -26,6 +26,7 @@ import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/loading_indicator.dart'; import 'package:stackwallet/widgets/rounded_container.dart'; +import 'package:stackwallet/widgets/stack_dialog.dart'; import 'package:tuple/tuple.dart'; class PaynymDetailsPopup extends ConsumerStatefulWidget { @@ -102,6 +103,20 @@ class _PaynymDetailsPopupState extends ConsumerState { _showInsufficientFundsInfo = true; }); return; + } catch (e) { + if (mounted) { + canPop = true; + Navigator.of(context).pop(); + } + + await showDialog( + context: context, + builder: (context) => StackOkDialog( + title: "Error", + message: e.toString(), + ), + ); + return; } if (mounted) { diff --git a/lib/services/mixins/paynym_wallet_interface.dart b/lib/services/mixins/paynym_wallet_interface.dart index 4626d6ef2..774c7c609 100644 --- a/lib/services/mixins/paynym_wallet_interface.dart +++ b/lib/services/mixins/paynym_wallet_interface.dart @@ -398,135 +398,162 @@ mixin PaynymWalletInterface { int additionalOutputs = 0, List? utxos, }) async { - final amountToSend = _dustLimitP2PKH; - final List availableOutputs = - utxos ?? await _db.getUTXOs(_walletId).findAll(); - final List spendableOutputs = []; - int spendableSatoshiValue = 0; + try { + final amountToSend = _dustLimitP2PKH; + final List availableOutputs = + utxos ?? await _db.getUTXOs(_walletId).findAll(); + final List spendableOutputs = []; + int spendableSatoshiValue = 0; - // Build list of spendable outputs and totaling their satoshi amount - for (var i = 0; i < availableOutputs.length; i++) { - if (availableOutputs[i].isBlocked == false && - availableOutputs[i] - .isConfirmed(await _getChainHeight(), _minConfirms) == - true) { - spendableOutputs.add(availableOutputs[i]); - spendableSatoshiValue += availableOutputs[i].value; + // Build list of spendable outputs and totaling their satoshi amount + for (var i = 0; i < availableOutputs.length; i++) { + if (availableOutputs[i].isBlocked == false && + availableOutputs[i] + .isConfirmed(await _getChainHeight(), _minConfirms) == + true) { + spendableOutputs.add(availableOutputs[i]); + spendableSatoshiValue += availableOutputs[i].value; + } } - } - if (spendableSatoshiValue < amountToSend) { - // insufficient balance - throw InsufficientBalanceException( - "Spendable balance is less than the minimum required for a notification transaction."); - } else if (spendableSatoshiValue == amountToSend) { - // insufficient balance due to missing amount to cover fee - throw InsufficientBalanceException( - "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 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 (spendableSatoshiValue < amountToSend) { + // insufficient balance + throw InsufficientBalanceException( + "Spendable balance is less than the minimum required for a notification transaction."); + } else if (spendableSatoshiValue == amountToSend) { + // insufficient balance due to missing amount to cover fee + throw InsufficientBalanceException( + "Remaining balance does not cover the network fee."); } - 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 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) { - // try to add change output due to "left over" amount being greater than - // the estimated fee + the dust limit - int changeAmount = satoshisBeingUsed - amountToSend - feeForWithChange; + // 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; + } - // check estimates are correct and build notification tx - if (changeAmount >= _dustLimitP2PKH && - satoshisBeingUsed - amountToSend - changeAmount == feeForWithChange) { - var txn = await _createNotificationTx( - targetPaymentCodeString: targetPaymentCodeString, - utxosToUse: utxoObjectsToUse, - utxoSigningData: utxoSigningData, - change: changeAmount, - ); + // gather required signing data + final utxoSigningData = await _fetchBuildTxData(utxoObjectsToUse); - 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 - if (txn.item2 - feeBeingPaid == 1) { - changeAmount -= 1; - feeBeingPaid += 1; - txn = await _createNotificationTx( + 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; + } + } + + 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, utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, change: changeAmount, ); - } - Map 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 + int feeBeingPaid = satoshisBeingUsed - amountToSend - changeAmount; + + // make sure minimum fee is accurate if that is being used + if (txn.item2 - feeBeingPaid == 1) { + changeAmount -= 1; + feeBeingPaid += 1; + txn = await _createNotificationTx( + targetPaymentCodeString: targetPaymentCodeString, + utxosToUse: utxoObjectsToUse, + utxoSigningData: utxoSigningData, + change: changeAmount, + ); + } + + Map 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 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( targetPaymentCodeString: targetPaymentCodeString, utxosToUse: utxoObjectsToUse, @@ -544,40 +571,22 @@ mixin PaynymWalletInterface { "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( - targetPaymentCodeString: targetPaymentCodeString, - utxosToUse: utxoObjectsToUse, - utxoSigningData: utxoSigningData, - change: 0, - ); - - int feeBeingPaid = satoshisBeingUsed - amountToSend; - - Map 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 { - throw InsufficientBalanceException( - "Remaining balance does not cover the network fee."); + // 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 { + throw InsufficientBalanceException( + "Remaining balance does not cover the network fee."); + } } + } catch (e) { + rethrow; } } @@ -588,96 +597,109 @@ mixin PaynymWalletInterface { required List utxosToUse, required Map utxoSigningData, required int change, + int? dustLimit, }) async { - final targetPaymentCode = - PaymentCode.fromPaymentCode(targetPaymentCodeString, _network); - final myCode = await getPaymentCode(DerivePathType.bip44); + try { + final targetPaymentCode = + PaymentCode.fromPaymentCode(targetPaymentCodeString, _network); + final myCode = await getPaymentCode(DerivePathType.bip44); - final utxo = utxosToUse.first; - final txPoint = utxo.txid.fromHex.toList(); - final txPointIndex = utxo.vout; + final utxo = utxosToUse.first; + final txPoint = utxo.txid.fromHex.toList(); + final txPointIndex = utxo.vout; - final rev = Uint8List(txPoint.length + 4); - Util.copyBytes(Uint8List.fromList(txPoint), 0, rev, 0, txPoint.length); - final buffer = rev.buffer.asByteData(); - buffer.setUint32(txPoint.length, txPointIndex, Endian.little); + final rev = Uint8List(txPoint.length + 4); + Util.copyBytes(Uint8List.fromList(txPoint), 0, rev, 0, txPoint.length); + final buffer = rev.buffer.asByteData(); + 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( - myKeyPair.privateKey!, - targetPaymentCode.notificationPublicKey(), - ); + final S = SecretPoint( + myKeyPair.privateKey!, + targetPaymentCode.notificationPublicKey(), + ); - final blindingMask = PaymentCode.getMask(S.ecdhSecret(), rev); + final blindingMask = PaymentCode.getMask(S.ecdhSecret(), rev); - final blindedPaymentCode = PaymentCode.blind( - payload: myCode.getPayload(), - mask: blindingMask, - unBlind: false, - ); + final blindedPaymentCode = PaymentCode.blind( + payload: myCode.getPayload(), + mask: blindingMask, + unBlind: false, + ); - final opReturnScript = bscript.compile([ - (op.OPS["OP_RETURN"] as int), - blindedPaymentCode, - ]); + final opReturnScript = bscript.compile([ + (op.OPS["OP_RETURN"] as int), + blindedPaymentCode, + ]); - // build a notification tx - final txb = btc_dart.TransactionBuilder(network: _network); - txb.setVersion(1); + // build a notification tx + final txb = btc_dart.TransactionBuilder(network: _network); + 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( utxo.txid, - utxo.vout, + txPointIndex, null, utxoSigningData[utxo.txid]["output"] as Uint8List, ); - } - // todo: modify address once segwit support is in our bip47 - txb.addOutput( - targetPaymentCode.notificationAddressP2PKH(), _dustLimitP2PKH); - txb.addOutput(opReturnScript, 0); + // add rest of possible inputs + for (var i = 1; i < utxosToUse.length; i++) { + final utxo = utxosToUse[i]; + txb.addInput( + utxo.txid, + utxo.vout, + null, + utxoSigningData[utxo.txid]["output"] as Uint8List, + ); + } - // TODO: add possible change output and mark output as dangerous - if (change > 0) { - // generate new change address if current change address has been used - await _checkChangeAddressForTransactions(); - final String changeAddress = await _getCurrentChangeAddress(); - txb.addOutput(changeAddress, change); - } + // todo: modify address once segwit support is in our bip47 + txb.addOutput( + targetPaymentCode.notificationAddressP2PKH(), + dustLimit ?? _dustLimitP2PKH, + ); + txb.addOutput(opReturnScript, 0); - txb.sign( - vin: 0, - keyPair: myKeyPair, - witnessValue: utxo.value, - witnessScript: utxoSigningData[utxo.txid]["redeemScript"] as Uint8List?, - ); + // TODO: add possible change output and mark output as dangerous + if (change > 0) { + // generate new change address if current change address has been used + await _checkChangeAddressForTransactions(); + 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( - vin: i, - keyPair: utxoSigningData[txid]["keyPair"] as btc_dart.ECPair, - witnessValue: utxosToUse[i].value, + vin: 0, + keyPair: myKeyPair, + witnessValue: utxo.value, 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 broadcastNotificationTx(