diff --git a/src/qt/forms/sendcoinsentry.ui b/src/qt/forms/sendcoinsentry.ui index 361f182f0a..11a74f1155 100644 --- a/src/qt/forms/sendcoinsentry.ui +++ b/src/qt/forms/sendcoinsentry.ui @@ -221,7 +221,7 @@ - + Message: @@ -231,15 +231,54 @@ - - + + - A message that was attached to the firo: URI which will be stored with the transaction for your reference. Note: This message will not be sent over the Firo network. + Optional message for this transaction - - Qt::PlainText + + + + + + 0 - + + + + + + + + + + 5 + + + 5 + + + 10 + + + 10 + + + margin-left:-30px;margin-right:-10px;margin-top:2px; + + + + + + + + + + color: #FFA800; margin-left:-10px; + + + + diff --git a/src/qt/sendcoinsdialog.cpp b/src/qt/sendcoinsdialog.cpp index 3bfbfbc6a7..fd3d658217 100644 --- a/src/qt/sendcoinsdialog.cpp +++ b/src/qt/sendcoinsdialog.cpp @@ -265,6 +265,7 @@ void SendCoinsDialog::on_sendButton_clicked() } ctx = dialog->getUnlockContext(); } + recipient.message = entry->getValue().message; recipients.append(recipient); } else @@ -532,6 +533,18 @@ void SendCoinsDialog::on_sendButton_clicked() QString questionString = tr("Are you sure you want to send?"); questionString.append(warningMessage); questionString.append("

%1"); + bool firstMessage = true; + for (const auto& rec : recipients) { + if (!rec.message.isEmpty()) { + if (firstMessage) { + questionString.append("
" + tr("Messages") + ":
"); + firstMessage = false; + } + QString sanitizedMsg = GUIUtil::HtmlEscape(rec.message, true); + questionString.append("• " + sanitizedMsg + "
"); + } + } + double txSize; if ((fAnonymousMode == false) && (recipients.size() == sparkAddressCount) && spark::IsSparkAllowed()) { diff --git a/src/qt/sendcoinsentry.cpp b/src/qt/sendcoinsentry.cpp index c250db4d21..26bd96b33c 100644 --- a/src/qt/sendcoinsentry.cpp +++ b/src/qt/sendcoinsentry.cpp @@ -11,6 +11,7 @@ #include "optionsmodel.h" #include "platformstyle.h" #include "walletmodel.h" +#include "../spark/sparkwallet.h" #include "../wallet/wallet.h" #include @@ -29,6 +30,7 @@ SendCoinsEntry::SendCoinsEntry(const PlatformStyle *_platformStyle, QWidget *par QIcon icon_; icon_.addFile(QString::fromUtf8(":/icons/ic_warning"), QSize(), QIcon::Normal, QIcon::On); ui->iconWarning->setPixmap(icon_.pixmap(18, 18)); + ui->iconMessageWarning->setPixmap(icon_.pixmap(18, 18)); ui->addressBookButton->setIcon(platformStyle->SingleColorIcon(":/icons/address-book")); ui->pasteButton->setIcon(platformStyle->SingleColorIcon(":/icons/editpaste")); @@ -55,6 +57,11 @@ SendCoinsEntry::SendCoinsEntry(const PlatformStyle *_platformStyle, QWidget *par connect(ui->deleteButton, &QToolButton::clicked, this, &SendCoinsEntry::deleteClicked); connect(ui->deleteButton_is, &QToolButton::clicked, this, &SendCoinsEntry::deleteClicked); connect(ui->deleteButton_s, &QToolButton::clicked, this, &SendCoinsEntry::deleteClicked); + connect(ui->messageTextLabel, &QLineEdit::textChanged, this, &SendCoinsEntry::on_MemoTextChanged); + + ui->messageLabel->setVisible(false); + ui->messageTextLabel->setVisible(false); + ui->iconMessageWarning->setVisible(false); } SendCoinsEntry::~SendCoinsEntry() @@ -62,6 +69,31 @@ SendCoinsEntry::~SendCoinsEntry() delete ui; } +void SendCoinsEntry::on_MemoTextChanged(const QString &text) +{ + const spark::Params* params = spark::Params::get_default(); + int maxLength = params->get_memo_bytes(); + bool isOverLimit = text.length() > maxLength; + + if (isOverLimit) { + ui->messageWarning->setText(QString("Message exceeds %1 bytes limit").arg(maxLength)); + ui->messageWarning->setVisible(true); + ui->messageTextLabel->setStyleSheet("border: 1px solid red;"); + ui->iconMessageWarning->setVisible(true); + } else { + QString sanitized = text; + sanitized.remove(QRegExp("[\\x00-\\x1F\\x7F]")); + if (sanitized != text) { + ui->messageTextLabel->setText(sanitized); + return; + } + ui->messageWarning->clear(); + ui->messageWarning->setVisible(false); + ui->messageTextLabel->setStyleSheet(""); + ui->iconMessageWarning->setVisible(false); + } +} + void SendCoinsEntry::on_pasteButton_clicked() { // Paste text from clipboard into recipient field @@ -85,6 +117,13 @@ void SendCoinsEntry::on_payTo_textChanged(const QString &address) { updateLabel(address); setWarning(fAnonymousMode); + + bool isSparkAddress = false; + if (model) { + isSparkAddress = model->validateSparkAddress(address); + } + ui->messageLabel->setVisible(isSparkAddress); + ui->messageTextLabel->setVisible(isSparkAddress); } void SendCoinsEntry::setModel(WalletModel *_model) diff --git a/src/qt/sendcoinsentry.h b/src/qt/sendcoinsentry.h index 2e89f0fd5e..4611bd0027 100644 --- a/src/qt/sendcoinsentry.h +++ b/src/qt/sendcoinsentry.h @@ -63,6 +63,7 @@ public Q_SLOTS: private Q_SLOTS: void deleteClicked(); void on_payTo_textChanged(const QString &address); + void on_MemoTextChanged(const QString &text); void on_addressBookButton_clicked(); void on_pasteButton_clicked(); void updateDisplayUnit(); diff --git a/src/qt/transactiondesc.cpp b/src/qt/transactiondesc.cpp index a13eb7e347..deaabaf85f 100644 --- a/src/qt/transactiondesc.cpp +++ b/src/qt/transactiondesc.cpp @@ -311,10 +311,44 @@ QString TransactionDesc::toHTML(CWallet *wallet, CWalletTx &wtx, TransactionReco strHTML += "" + tr("Transaction total size") + ": " + QString::number(wtx.tx->GetTotalSize()) + " bytes
"; strHTML += "" + tr("Output index") + ": " + QString::number(rec->getOutputIndex()) + "
"; - // Message from normal firo:URI (firo:123...?message=example) - for (const PAIRTYPE(std::string, std::string)& r : wtx.vOrderForm) - if (r.first == "Message") - strHTML += "
" + tr("Message") + ":
" + GUIUtil::HtmlEscape(r.second, true) + "
"; + isminetype fAllFromMe = ISMINE_SPENDABLE; + bool foundSparkOutput = false; + + for (const CTxIn& txin : wtx.tx->vin) { + isminetype mine = wallet->IsMine(txin, *wtx.tx); + fAllFromMe = std::min(fAllFromMe, mine); + } + + bool firstMessage = true; + if (fAllFromMe) { + for (const CTxOut& txout : wtx.tx->vout) { + if (wtx.IsChange(txout)) continue; + + CSparkOutputTx sparkOutput; + if (wallet->GetSparkOutputTx(txout.scriptPubKey, sparkOutput)) { + if (!sparkOutput.memo.empty()) { + foundSparkOutput = true; + if (firstMessage) { + strHTML += "
" + tr("Messages") + ":
"; + firstMessage = false; + } + strHTML += "• " + GUIUtil::HtmlEscape(sparkOutput.memo, true) + "
"; + } + } + } + } + + if (!foundSparkOutput && wallet->sparkWallet) { + for (const auto& [id, meta] : wallet->sparkWallet->getMintMap()) { + if (meta.txid == rec->hash && !meta.memo.empty()) { + if (firstMessage) { + strHTML += "
" + tr("Messages") + ":
"; + firstMessage = false; + } + strHTML += "• " + GUIUtil::HtmlEscape(meta.memo, true) + "
"; + } + } + } if (wtx.IsCoinBase()) { diff --git a/src/qt/walletmodel.cpp b/src/qt/walletmodel.cpp index 66a8538941..c625cf4b77 100644 --- a/src/qt/walletmodel.cpp +++ b/src/qt/walletmodel.cpp @@ -1384,7 +1384,7 @@ WalletModel::SendCoinsReturn WalletModel::prepareMintSparkTransaction(std::vecto address.decode(rcp.address.toStdString()); spark::MintedCoinData data; data.address = address; - data.memo = ""; + data.memo = rcp.message.toStdString(); data.v = rcp.amount; outputs.push_back(data); total += rcp.amount; @@ -1481,7 +1481,7 @@ WalletModel::SendCoinsReturn WalletModel::prepareSpendSparkTransaction(WalletMod address.decode(rcp.address.toStdString()); spark::OutputCoinData data; data.address = address; - data.memo = ""; + data.memo = rcp.message.toStdString(); data.v = rcp.amount; privateRecipients.push_back(std::make_pair(data, rcp.fSubtractFeeFromAmount)); } else { diff --git a/src/spark/primitives.h b/src/spark/primitives.h index 7929e73b80..2bfa0c72bb 100644 --- a/src/spark/primitives.h +++ b/src/spark/primitives.h @@ -87,6 +87,7 @@ class CSparkOutputTx { public: std::string address; + std::string memo; int64_t amount; CSparkOutputTx() @@ -97,6 +98,7 @@ class CSparkOutputTx void SetNull() { address = ""; + memo = ""; amount = 0; } @@ -105,6 +107,15 @@ class CSparkOutputTx inline void SerializationOp(Stream& s, Operation ser_action) { READWRITE(address); READWRITE(amount); + if (ser_action.ForRead()) { + if (!s.empty()) { + READWRITE(memo); + } else { + memo = ""; + } + } else { + READWRITE(memo); + } } }; diff --git a/src/spark/sparkwallet.cpp b/src/spark/sparkwallet.cpp index 382cb6c62d..9e5e0a49e8 100644 --- a/src/spark/sparkwallet.cpp +++ b/src/spark/sparkwallet.cpp @@ -742,7 +742,12 @@ std::vector CSparkWallet::CreateSparkMintRecipients( script.insert(script.end(), serializedCoins[i].begin(), serializedCoins[i].end()); unsigned char network = spark::GetNetworkType(); std::string addr = outputs[i].address.encode(network); - CRecipient recipient = {script, CAmount(outputs[i].v), false, addr}; + std::string memo = outputs[i].memo; + const std::size_t max_memo_size = outputs[i].address.get_params()->get_memo_bytes(); + if (memo.length() > max_memo_size) { + throw std::runtime_error(strprintf("Memo exceeds maximum length of %d bytes", max_memo_size)); + } + CRecipient recipient = {script, CAmount(outputs[i].v), false, addr, memo}; results.emplace_back(recipient); } @@ -1093,6 +1098,7 @@ bool CSparkWallet::CreateSparkMintTransactions( CSparkOutputTx output; output.address = recipient.address; output.amount = recipient.nAmount; + output.memo = recipient.memo; walletdb.WriteSparkOutputTx(recipient.scriptPubKey, output); break; } @@ -1530,6 +1536,7 @@ CWalletTx CSparkWallet::CreateSparkSpendTransaction( CSparkOutputTx output; output.address = privOutputs[i].address.encode(network); output.amount = privOutputs[i].v; + output.memo = privOutputs[i].memo; walletdb.WriteSparkOutputTx(script, output); tx.vout.push_back(CTxOut(0, script)); i++; diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index f9397f5150..f1f1d0c6a9 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -190,6 +190,7 @@ struct CRecipient CAmount nAmount; bool fSubtractFeeFromAmount; std::string address; + std::string memo; }; typedef std::map mapValue_t;