From 54767947ed7e722ba763bd3dc818501ce32dc1cf Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Fri, 6 Jan 2023 13:55:47 -0600 Subject: [PATCH] WIP notif txn --- lib/services/coins/coin_paynym_extension.dart | 266 +++++++++++++++++- .../coins/dogecoin/dogecoin_wallet.dart | 22 +- 2 files changed, 269 insertions(+), 19 deletions(-) diff --git a/lib/services/coins/coin_paynym_extension.dart b/lib/services/coins/coin_paynym_extension.dart index 6136171c2..743e1336f 100644 --- a/lib/services/coins/coin_paynym_extension.dart +++ b/lib/services/coins/coin_paynym_extension.dart @@ -13,6 +13,20 @@ import 'package:stackwallet/models/paymint/utxo_model.dart'; import 'package:stackwallet/services/coins/dogecoin/dogecoin_wallet.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/format.dart'; +import 'package:tuple/tuple.dart'; + +class SWException with Exception { + SWException(this.message); + + final String message; + + @override + toString() => message; +} + +class InsufficientBalanceException extends SWException { + InsufficientBalanceException(super.message); +} extension PayNym on DogecoinWallet { // fetch or generate this wallet's bip47 payment code @@ -55,12 +69,187 @@ extension PayNym on DogecoinWallet { // Future<Map<String, dynamic>> prepareNotificationTransaction( // String targetPaymentCode) async {} - Future<String> createNotificationTx( - String targetPaymentCodeString, - List<UtxoObject> utxosToUse, - int dustLimit, - ) async { - final utxoSigningData = await fetchBuildTxData(utxosToUse); + Future<Map<String, dynamic>> buildNotificationTx({ + required int selectedTxFeeRate, + required String targetPaymentCodeString, + int additionalOutputs = 0, + List<UtxoObject>? utxos, + }) async { + const amountToSend = DUST_LIMIT; + final List<UtxoObject> availableOutputs = utxos ?? outputsList; + final List<UtxoObject> 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].blocked == false && + availableOutputs[i].status.confirmed == 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.status.confirmations.compareTo(a.status.confirmations)); + + int satoshisBeingUsed = 0; + int outputsBeingUsed = 0; + List<UtxoObject> 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 (feeForNoChange < vSizeForNoChange * 1000) { + feeForNoChange = vSizeForNoChange * 1000; + } + if (feeForWithChange < vSizeForWithChange * 1000) { + feeForWithChange = vSizeForWithChange * 1000; + } + + if (satoshisBeingUsed - amountToSend > feeForNoChange + DUST_LIMIT) { + // 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 >= DUST_LIMIT && + satoshisBeingUsed - amountToSend - changeAmount == feeForWithChange) { + final txn = await _createNotificationTx( + targetPaymentCodeString: targetPaymentCodeString, + utxosToUse: utxoObjectsToUse, + utxoSigningData: utxoSigningData, + change: changeAmount, + ); + + int feeBeingPaid = satoshisBeingUsed - amountToSend - changeAmount; + + Map<String, dynamic> transactionObject = { + "hex": txn.item1, + "recipientPaynym": targetPaymentCodeString, + // "recipientAmt": recipientsAmtArray[0], + "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, + // "recipientAmt": recipientsAmtArray[0], + "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, + utxoSigningData: utxoSigningData, + change: 0, + ); + + int feeBeingPaid = satoshisBeingUsed - amountToSend; + + Map<String, dynamic> transactionObject = { + "hex": txn.item1, + "recipientPaynym": targetPaymentCodeString, + // "recipientAmt": recipientsAmtArray[0], + "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 buildNotificationTx( + selectedTxFeeRate: selectedTxFeeRate, + targetPaymentCodeString: targetPaymentCodeString, + additionalOutputs: additionalOutputs + 1, + ); + } else { + throw InsufficientBalanceException( + "Remaining balance does not cover the network fee."); + } + } + } + + // return tuple with string value equal to the raw tx hex and the int value + // equal to its vSize + Future<Tuple2<String, int>> _createNotificationTx({ + required String targetPaymentCodeString, + required List<UtxoObject> utxosToUse, + required Map<String, dynamic> utxoSigningData, + required int change, + }) async { final targetPaymentCode = PaymentCode.fromPaymentCode(targetPaymentCodeString, network); final myCode = await getPaymentCode(); @@ -109,19 +298,80 @@ extension PayNym on DogecoinWallet { txPointIndex, ); - txb.addOutput(targetPaymentCode.notificationAddress(), dustLimit); + txb.addOutput(targetPaymentCode.notificationAddress(), DUST_LIMIT); txb.addOutput(opReturnScript, 0); // 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(DerivePathType.bip44); + final String changeAddress = + await getCurrentAddressForChain(1, DerivePathType.bip44); + txb.addOutput(changeAddress, change); + } txb.sign( vin: 0, keyPair: myKeyPair, ); + // sign rest of possible inputs + for (var i = 1; i < utxosToUse.length - 1; i++) { + final txid = utxosToUse[i].txid; + txb.sign( + vin: i, + keyPair: utxoSigningData[txid]["keyPair"] as ECPair, + // witnessValue: utxosToUse[i].value, + ); + } + final builtTx = txb.build(); - return builtTx.toHex(); + return Tuple2(builtTx.toHex(), builtTx.virtualSize()); + } + + Future<bool> hasConfirmedNotificationTxSentTo( + String paymentCodeString) async { + final targetPaymentCode = + PaymentCode.fromPaymentCode(paymentCodeString, network); + final targetNotificationAddress = targetPaymentCode.notificationAddress(); + + final myTxHistory = (await transactionData) + .getAllTransactions() + .entries + .map((e) => e.value) + .where((e) => + e.txType == "Sent" && e.address == targetNotificationAddress); + + return myTxHistory.isNotEmpty; + } + + // fetch paynym notification tx meta data + Set<Map<String, dynamic>> getPaynymNotificationTxInfo() { + final set = DB.instance.get<dynamic>( + boxName: walletId, key: "paynymNotificationTxInfo") as Set? ?? + {}; + + return Set<Map<String, dynamic>>.from(set); + } + + // add/update paynym notification tx meta data entry + Future<void> updatePaynymNotificationInfo({ + required String txid, + required bool confirmed, + required String paymentCodeString, + }) async { + final data = getPaynymNotificationTxInfo(); + data.add({ + "txid": txid, + "confirmed": confirmed, + "paymentCodeString": paymentCodeString, + }); + await DB.instance.put<dynamic>( + boxName: walletId, + key: "paynymNotificationTxInfo", + value: data, + ); } } diff --git a/lib/services/coins/dogecoin/dogecoin_wallet.dart b/lib/services/coins/dogecoin/dogecoin_wallet.dart index 3a5c9fb99..a28d027e3 100644 --- a/lib/services/coins/dogecoin/dogecoin_wallet.dart +++ b/lib/services/coins/dogecoin/dogecoin_wallet.dart @@ -204,7 +204,7 @@ class DogecoinWallet extends CoinServiceAPI { @override Future<String> get currentReceivingAddress => _currentReceivingAddressP2PKH ??= - _getCurrentAddressForChain(0, DerivePathType.bip44); + getCurrentAddressForChain(0, DerivePathType.bip44); Future<String>? _currentReceivingAddressP2PKH; @@ -776,7 +776,7 @@ class DogecoinWallet extends CoinServiceAPI { } GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.2, walletId)); - await _checkChangeAddressForTransactions(DerivePathType.bip44); + await checkChangeAddressForTransactions(DerivePathType.bip44); GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.3, walletId)); await _checkCurrentReceivingAddressesForTransactions(); @@ -1446,7 +1446,7 @@ class DogecoinWallet extends CoinServiceAPI { /// Returns the latest receiving/change (external/internal) address for the wallet depending on [chain] /// and /// [chain] - Use 0 for receiving (external), 1 for change (internal). Should not be any other value! - Future<String> _getCurrentAddressForChain( + Future<String> getCurrentAddressForChain( int chain, DerivePathType derivePathType) async { // Here, we assume that chain == 1 if it isn't 0 String arrayKey = chain == 0 ? "receivingAddresses" : "changeAddresses"; @@ -1801,7 +1801,7 @@ class DogecoinWallet extends CoinServiceAPI { DerivePathType derivePathType) async { try { final String currentExternalAddr = - await _getCurrentAddressForChain(0, derivePathType); + await getCurrentAddressForChain(0, derivePathType); final int txCount = await getTxCount(address: currentExternalAddr); Logging.instance.log( 'Number of txs for current receiving address $currentExternalAddr: $txCount', @@ -1850,11 +1850,11 @@ class DogecoinWallet extends CoinServiceAPI { } } - Future<void> _checkChangeAddressForTransactions( + Future<void> checkChangeAddressForTransactions( DerivePathType derivePathType) async { try { final String currentExternalAddr = - await _getCurrentAddressForChain(1, derivePathType); + await getCurrentAddressForChain(1, derivePathType); final int txCount = await getTxCount(address: currentExternalAddr); Logging.instance.log( 'Number of txs for current change address $currentExternalAddr: $txCount', @@ -1916,7 +1916,7 @@ class DogecoinWallet extends CoinServiceAPI { Future<void> _checkCurrentChangeAddressesForTransactions() async { try { for (final type in DerivePathType.values) { - await _checkChangeAddressForTransactions(type); + await checkChangeAddressForTransactions(type); } } catch (e, s) { Logging.instance.log( @@ -2480,7 +2480,7 @@ class DogecoinWallet extends CoinServiceAPI { utxoSigningData: utxoSigningData, recipients: [ _recipientAddress, - await _getCurrentAddressForChain(1, DerivePathType.bip44), + await getCurrentAddressForChain(1, DerivePathType.bip44), ], satoshiAmounts: [ satoshiAmountToSend, @@ -2532,9 +2532,9 @@ class DogecoinWallet extends CoinServiceAPI { satoshisBeingUsed - satoshiAmountToSend - changeOutputSize == feeForTwoOutputs) { // generate new change address if current change address has been used - await _checkChangeAddressForTransactions(DerivePathType.bip44); + await checkChangeAddressForTransactions(DerivePathType.bip44); final String newChangeAddress = - await _getCurrentAddressForChain(1, DerivePathType.bip44); + await getCurrentAddressForChain(1, DerivePathType.bip44); int feeBeingPaid = satoshisBeingUsed - satoshiAmountToSend - changeOutputSize; @@ -2685,7 +2685,7 @@ class DogecoinWallet extends CoinServiceAPI { }; return transactionObject; } else { - // Remember that returning 2 indicates that the user does not have a sufficient balance to + // Remember that returning 2 iTndicates 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(