From 7ddce4065eead5fc9fa715062efcb7f95e3bceb7 Mon Sep 17 00:00:00 2001 From: psolstice Date: Tue, 11 Jun 2024 09:12:18 +0200 Subject: [PATCH] Automate spend from spark to EX-address in Qt (#1448) * Automation of sending private balance to an exchange address * Fixed fee calculation * Changed wording of messages --- src/qt/sendcoinsdialog.cpp | 136 +++++++++++++++++++++++++++++++++++-- src/qt/walletmodel.cpp | 6 ++ src/qt/walletmodel.h | 1 + src/wallet/coincontrol.h | 3 + src/wallet/wallet.cpp | 2 +- 5 files changed, 142 insertions(+), 6 deletions(-) diff --git a/src/qt/sendcoinsdialog.cpp b/src/qt/sendcoinsdialog.cpp index 8880bf0af7..66b380fea5 100644 --- a/src/qt/sendcoinsdialog.cpp +++ b/src/qt/sendcoinsdialog.cpp @@ -292,7 +292,6 @@ void SendCoinsDialog::on_sendButton_clicked() } // prepare transaction for getting txFee earlier - WalletModelTransaction currentTransaction(recipients); std::vector transactions; WalletModel::SendCoinsReturn prepareStatus; std::vector> wtxAndFees; @@ -310,13 +309,68 @@ void SendCoinsDialog::on_sendButton_clicked() ctrl.nConfirmTarget = 0; int sparkAddressCount = 0; + int exchangeAddressCount = 0; for(int i = 0; i < recipients.size(); ++i){ - bool check = model->validateSparkAddress(recipients[i].address); - if(check) { + if (model->validateSparkAddress(recipients[i].address)) sparkAddressCount++; + if (model->validateExchangeAddress(recipients[i].address)) + exchangeAddressCount++; + } + + bool fGoThroughTransparentAddress = false; + __decltype(recipients) exchangeRecipients; + CScript intermediateAddressScript; + CAmount extraFee = 0; + + if (fAnonymousMode && exchangeAddressCount > 0) { + CAmount exchangeAddressAmount = 0; + // if the transaction is performed in two stages through the intermediate address we need to calculate the size of the second transaction + uint32_t secondTxSize = 8 /*CTransaction: nVersion, nLockTime*/ + 1 /*vinSize*/ + 148 /*vin[0]*/ + 20 /*safety*/ + 1 /*voutSize*/; + + fGoThroughTransparentAddress = true; + + // remove exchange addresses from recipients array and add them to exchangeRecipients array + for(int i = 0; i < recipients.size(); ){ + if (model->validateExchangeAddress(recipients[i].address)) { + exchangeAddressAmount += recipients[i].amount; + // we use different fee calculation system and therefore can't reliably do the calculation + // of fee for the second transaction if some of recipients have this flag set + recipients[i].fSubtractFeeFromAmount = false; + exchangeRecipients.push_back(recipients[i]); + + secondTxSize += 8 /*amount*/ + 1 /*scriptSize*/ + 26 /*scriptPubKey*/; + + recipients.erase(recipients.begin() + i); + } + else { + ++i; + } + } + + LOCK2(cs_main, pwalletMain->cs_wallet); + // create a new transparent address and add it to the recipients array + if (!pwalletMain->IsLocked()) { + pwalletMain->TopUpKeyPool(); + } + CPubKey newKey; + if (!pwalletMain->GetKeyFromPool(newKey)) { + fNewRecipientAllowed = true; + return; } + pwalletMain->SetAddressBook(newKey.GetID(), "", "receive"); + intermediateAddressScript = GetScriptForDestination(newKey.GetID()); + + extraFee = CWallet::GetMinimumFee(secondTxSize, 0, mempool); + + SendCoinsRecipient newRecipient; + newRecipient.address = CBitcoinAddress(newKey.GetID()).ToString().c_str(); + newRecipient.amount = exchangeAddressAmount + extraFee; + newRecipient.fSubtractFeeFromAmount = false; + recipients.push_back(newRecipient); } + WalletModelTransaction currentTransaction(recipients); + CAmount mintSparkAmount = 0; CAmount txFee = 0; CAmount totalAmount = 0; @@ -364,6 +418,14 @@ void SendCoinsDialog::on_sendButton_clicked() return; } + // If the transaction is performed in two stages through the intermediate address we need to show the real + // recipients (for informational purposes), replacing the intermediate transparent address with the exchange address(es) + __decltype(recipients) realRecipients = recipients; + if (fGoThroughTransparentAddress) { + realRecipients.erase(realRecipients.end() - 1); + realRecipients.append(exchangeRecipients); + } + // Format confirmation message QStringList formatted; if ((fAnonymousMode == false) && (recipients.size() == sparkAddressCount) && spark::IsSparkAllowed()) @@ -407,7 +469,7 @@ void SendCoinsDialog::on_sendButton_clicked() formatted.append(recipientElement); } } else if ((fAnonymousMode == true) && (recipients.size() == 1) && spark::IsSparkAllowed()) { - for (auto &rcp : recipients) + for (auto &rcp : realRecipients) { // generate bold amount string CAmount namount = rcp.amount; @@ -434,7 +496,7 @@ void SendCoinsDialog::on_sendButton_clicked() formatted.append(recipientElement); } } else { - for (const SendCoinsRecipient &rcp : currentTransaction.getRecipients()) + for (auto &rcp : realRecipients) { // generate bold amount string QString amount = "" + BitcoinUnits::formatHtmlWithUnit(model->getOptionsModel()->getDisplayUnit(), rcp.amount); @@ -459,6 +521,14 @@ void SendCoinsDialog::on_sendButton_clicked() formatted.append(recipientElement); } } + + if (fGoThroughTransparentAddress) { + QString transparentAddress = "" + recipients[recipients.size()-1].address + ""; + formatted.append("
"); + formatted.append(tr("EX-addresses can only receive FIRO from transparent addresses.

" + "Your FIRO will go from Spark to a newly generated transparent address %1 and then immediately be sent to the EX-address.").arg(transparentAddress)); + } + QString questionString = tr("Are you sure you want to send?"); questionString.append("

%1"); double txSize; @@ -484,6 +554,15 @@ void SendCoinsDialog::on_sendButton_clicked() // append transaction size questionString.append(" (" + QString::number(txSize / 1000) + " kB)"); + + if (fGoThroughTransparentAddress) { + QString feeString; + feeString.append(""); + feeString.append(BitcoinUnits::formatHtmlWithUnit(model->getOptionsModel()->getDisplayUnit(), extraFee)); + feeString.append(""); + + questionString.append(tr(". An additional transaction fee of %1 will apply to complete the send from the transparent address to the EX-address.").arg(feeString)); + } } // add total amount in all subdivision units @@ -556,6 +635,53 @@ void SendCoinsDialog::on_sendButton_clicked() CoinControlDialog::coinControl->UnSelectAll(); coinControlUpdateLabels(); } + + // Launch the second stage of the transaction if needed + if (fGoThroughTransparentAddress) { + // prepare the coin control so the transaction will use (by default) only the transparent address + // created in the first stage + COutPoint outpoint; + outpoint.hash = currentTransaction.getTransaction()->GetHash(); + outpoint.n = UINT_MAX; + + const auto &vout = currentTransaction.getTransaction()->tx->vout; + for (size_t i = 0; i < vout.size(); i++) { + if (vout[i].scriptPubKey == intermediateAddressScript) { + outpoint.n = i; + break; + } + } + + if (outpoint.n == UINT_MAX) { + sendStatus.status = WalletModel::InvalidAddress; + sendStatus.reasonCommitFailed = "Intermediate address was not found in the transaction"; + fNewRecipientAllowed = true; + return; + } + + CCoinControl ctrl; + ctrl.fAllowOtherInputs = false; + ctrl.fNoChange = true; + ctrl.Select(outpoint); + + WalletModelTransaction secondTransaction(exchangeRecipients); + + prepareStatus = model->prepareTransaction(secondTransaction, &ctrl); + + // process prepareStatus and on error generate message shown to user + processSendCoinsReturn(prepareStatus, + BitcoinUnits::formatWithUnit(model->getOptionsModel()->getDisplayUnit(), currentTransaction.getTransactionFee())); + + if(prepareStatus.status != WalletModel::OK) { + fNewRecipientAllowed = true; + return; + } + + sendStatus = model->sendCoins(secondTransaction); + // process sendStatus and on error generate message shown to user + processSendCoinsReturn(sendStatus); + } + fNewRecipientAllowed = true; } diff --git a/src/qt/walletmodel.cpp b/src/qt/walletmodel.cpp index e6908f973d..66a8538941 100644 --- a/src/qt/walletmodel.cpp +++ b/src/qt/walletmodel.cpp @@ -272,6 +272,12 @@ bool WalletModel::validateAddress(const QString &address) return addressParsed.IsValid(); } +bool WalletModel::validateExchangeAddress(const QString &address) +{ + CBitcoinAddress addressParsed(address.toStdString()); + return addressParsed.IsValid() && addressParsed.Get().type() == typeid(CExchangeKeyID); +} + WalletModel::SendCoinsReturn WalletModel::prepareTransaction(WalletModelTransaction &transaction, const CCoinControl *coinControl) { CAmount total = 0; diff --git a/src/qt/walletmodel.h b/src/qt/walletmodel.h index f05b3829f0..f7e85ef62c 100644 --- a/src/qt/walletmodel.h +++ b/src/qt/walletmodel.h @@ -153,6 +153,7 @@ class WalletModel : public QObject // Check address for validity bool validateAddress(const QString &address); + bool validateExchangeAddress(const QString &address); bool validateSparkAddress(const QString &address); std::pair getSparkBalance(); diff --git a/src/wallet/coincontrol.h b/src/wallet/coincontrol.h index 064a2b9d99..feed073c26 100644 --- a/src/wallet/coincontrol.h +++ b/src/wallet/coincontrol.h @@ -24,6 +24,8 @@ class CCoinControl { public: CTxDestination destChange; + //! If true, don't use any change + bool fNoChange; //! If false, allows unselected inputs, but requires all selected inputs be used bool fAllowOtherInputs; //! Includes watch only addresses which match the ISMINE_WATCH_SOLVABLE criteria @@ -49,6 +51,7 @@ class CCoinControl void SetNull() { destChange = CNoDestination(); + fNoChange = false; fAllowOtherInputs = false; fRequireAllInputs = true; fAllowWatchOnly = false; diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index 0aa4b89c96..b1f5715403 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -4450,7 +4450,7 @@ bool CWallet::CreateTransaction(const std::vector& vecSend, CWalletT const CAmount nChange = nValueIn - nValueToSelect; CTxOut newTxOut; - if (nChange > 0) + if (nChange > 0 && !(coinControl && coinControl->fNoChange)) { // Fill a vout to ourself // TODO: pass in scriptChange instead of reservekey so