bitcoin/src/qt/transactiondesc.cpp
Hennadii Stepanov 72477ebb11
Merge bitcoin-core/gui#556: refactor: Make BitcoinUnits::Unit a scoped enum
0e5dedbc9eb54105ab9b0c4ce1f57afa55bcb5b6 qt/wallettests: sort includes (William Casarin)
0554251d660caa1c3f5f44ae1d9fa3c23d2aac18 qt: Skip displayUnitChanged signal if unit is not actually changed (Hennadii Stepanov)
ffbc2fe459034024cb2fce9fd94bff457b7a7d49 qt, refactor: Remove default cases for scoped enum (Hennadii Stepanov)
152d5bad50f145af922011f6ec1fd9afd9076ceb qt, refactor: Remove BitcoinUnits::valid function (Hennadii Stepanov)
aa23960fdf1deff321ecea435026c87db78498fb qt, refactor: Make BitcoinUnits::Unit a scoped enum (Hennadii Stepanov)
75832fdc37ea3fe9cf515bd1946e220fe07a440b qt: Use QVariant instead of int for BitcoinUnit in QSettings (Hennadii Stepanov)

Pull request description:

  This is a rebased version of #60

  Since Qt 5.5 there are [means](https://doc.qt.io/qt-5/qobject.html#Q_ENUM) to register an enum type with the meta-object system (such enum still lacks an ability to interact with [QSettings::setValue()](https://doc.qt.io/qt-5/qsettings.html#setValue) and [QSettings::value()](https://doc.qt.io/qt-5/qsettings.html#value) without defined stream operators).

  In order to reduce global namespace polluting and to force strong type checking, this PR makes BitcoinUnits::Unit a scoped enum (typedef BitcoinUnits::Unit BitcoinUnit;).

  No behavior change.

ACKs for top commit:
  jonatack:
    ACK 0e5dedbc9eb54105ab9b0c4ce1f57afa55bcb5b6, review and debug build of each commit after rebase on current master, lightly tested running the GUI, changing units a few times, and verifying persistence after restarting
  promag:
    Code review ACK 0e5dedbc9eb54105ab9b0c4ce1f57afa55bcb5b6

Tree-SHA512: 39ec0d7e4f0b9b25be287888121a8db6b282339674e37ec3a3554da63a9e22d6fe079e8310ca289b2a0356a19b3c7e55afa17d09dd34e0f222177f603bb053a3
2022-04-15 11:51:22 +02:00

361 lines
14 KiB
C++

// Copyright (c) 2011-2021 The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#ifdef HAVE_CONFIG_H
#include <config/bitcoin-config.h>
#endif
#include <qt/transactiondesc.h>
#include <qt/bitcoinunits.h>
#include <qt/guiutil.h>
#include <qt/paymentserver.h>
#include <qt/transactionrecord.h>
#include <consensus/consensus.h>
#include <interfaces/node.h>
#include <interfaces/wallet.h>
#include <key_io.h>
#include <policy/policy.h>
#include <util/system.h>
#include <validation.h>
#include <wallet/ismine.h>
#include <stdint.h>
#include <string>
#include <QLatin1String>
using wallet::ISMINE_ALL;
using wallet::ISMINE_SPENDABLE;
using wallet::ISMINE_WATCH_ONLY;
using wallet::isminetype;
QString TransactionDesc::FormatTxStatus(const interfaces::WalletTxStatus& status, bool inMempool)
{
int depth = status.depth_in_main_chain;
if (depth < 0) {
return tr("conflicted with a transaction with %1 confirmations").arg(-depth);
} else if (depth == 0) {
const QString abandoned{status.is_abandoned ? QLatin1String(", ") + tr("abandoned") : QString()};
return tr("0/unconfirmed, %1").arg(inMempool ? tr("in memory pool") : tr("not in memory pool")) + abandoned;
} else if (depth < 6) {
return tr("%1/unconfirmed").arg(depth);
} else {
return tr("%1 confirmations").arg(depth);
}
}
// Takes an encoded PaymentRequest as a string and tries to find the Common Name of the X.509 certificate
// used to sign the PaymentRequest.
bool GetPaymentRequestMerchant(const std::string& pr, QString& merchant)
{
// Search for the supported pki type strings
if (pr.find(std::string({0x12, 0x0b}) + "x509+sha256") != std::string::npos || pr.find(std::string({0x12, 0x09}) + "x509+sha1") != std::string::npos) {
// We want the common name of the Subject of the cert. This should be the second occurrence
// of the bytes 0x0603550403. The first occurrence of those is the common name of the issuer.
// After those bytes will be either 0x13 or 0x0C, then length, then either the ascii or utf8
// string with the common name which is the merchant name
size_t cn_pos = pr.find({0x06, 0x03, 0x55, 0x04, 0x03});
if (cn_pos != std::string::npos) {
cn_pos = pr.find({0x06, 0x03, 0x55, 0x04, 0x03}, cn_pos + 5);
if (cn_pos != std::string::npos) {
cn_pos += 5;
if (pr[cn_pos] == 0x13 || pr[cn_pos] == 0x0c) {
cn_pos++; // Consume the type
int str_len = pr[cn_pos];
cn_pos++; // Consume the string length
merchant = QString::fromUtf8(pr.data() + cn_pos, str_len);
return true;
}
}
}
}
return false;
}
QString TransactionDesc::toHTML(interfaces::Node& node, interfaces::Wallet& wallet, TransactionRecord* rec, BitcoinUnit unit)
{
int numBlocks;
interfaces::WalletTxStatus status;
interfaces::WalletOrderForm orderForm;
bool inMempool;
interfaces::WalletTx wtx = wallet.getWalletTxDetails(rec->hash, status, orderForm, inMempool, numBlocks);
QString strHTML;
strHTML.reserve(4000);
strHTML += "<html><font face='verdana, arial, helvetica, sans-serif'>";
int64_t nTime = wtx.time;
CAmount nCredit = wtx.credit;
CAmount nDebit = wtx.debit;
CAmount nNet = nCredit - nDebit;
strHTML += "<b>" + tr("Status") + ":</b> " + FormatTxStatus(status, inMempool);
strHTML += "<br>";
strHTML += "<b>" + tr("Date") + ":</b> " + (nTime ? GUIUtil::dateTimeStr(nTime) : "") + "<br>";
//
// From
//
if (wtx.is_coinbase)
{
strHTML += "<b>" + tr("Source") + ":</b> " + tr("Generated") + "<br>";
}
else if (wtx.value_map.count("from") && !wtx.value_map["from"].empty())
{
// Online transaction
strHTML += "<b>" + tr("From") + ":</b> " + GUIUtil::HtmlEscape(wtx.value_map["from"]) + "<br>";
}
else
{
// Offline transaction
if (nNet > 0)
{
// Credit
CTxDestination address = DecodeDestination(rec->address);
if (IsValidDestination(address)) {
std::string name;
isminetype ismine;
if (wallet.getAddress(address, &name, &ismine, /* purpose= */ nullptr))
{
strHTML += "<b>" + tr("From") + ":</b> " + tr("unknown") + "<br>";
strHTML += "<b>" + tr("To") + ":</b> ";
strHTML += GUIUtil::HtmlEscape(rec->address);
QString addressOwned = ismine == ISMINE_SPENDABLE ? tr("own address") : tr("watch-only");
if (!name.empty())
strHTML += " (" + addressOwned + ", " + tr("label") + ": " + GUIUtil::HtmlEscape(name) + ")";
else
strHTML += " (" + addressOwned + ")";
strHTML += "<br>";
}
}
}
}
//
// To
//
if (wtx.value_map.count("to") && !wtx.value_map["to"].empty())
{
// Online transaction
std::string strAddress = wtx.value_map["to"];
strHTML += "<b>" + tr("To") + ":</b> ";
CTxDestination dest = DecodeDestination(strAddress);
std::string name;
if (wallet.getAddress(
dest, &name, /* is_mine= */ nullptr, /* purpose= */ nullptr) && !name.empty())
strHTML += GUIUtil::HtmlEscape(name) + " ";
strHTML += GUIUtil::HtmlEscape(strAddress) + "<br>";
}
//
// Amount
//
if (wtx.is_coinbase && nCredit == 0)
{
//
// Coinbase
//
CAmount nUnmatured = 0;
for (const CTxOut& txout : wtx.tx->vout)
nUnmatured += wallet.getCredit(txout, ISMINE_ALL);
strHTML += "<b>" + tr("Credit") + ":</b> ";
if (status.is_in_main_chain)
strHTML += BitcoinUnits::formatHtmlWithUnit(unit, nUnmatured)+ " (" + tr("matures in %n more block(s)", "", status.blocks_to_maturity) + ")";
else
strHTML += "(" + tr("not accepted") + ")";
strHTML += "<br>";
}
else if (nNet > 0)
{
//
// Credit
//
strHTML += "<b>" + tr("Credit") + ":</b> " + BitcoinUnits::formatHtmlWithUnit(unit, nNet) + "<br>";
}
else
{
isminetype fAllFromMe = ISMINE_SPENDABLE;
for (const isminetype mine : wtx.txin_is_mine)
{
if(fAllFromMe > mine) fAllFromMe = mine;
}
isminetype fAllToMe = ISMINE_SPENDABLE;
for (const isminetype mine : wtx.txout_is_mine)
{
if(fAllToMe > mine) fAllToMe = mine;
}
if (fAllFromMe)
{
if(fAllFromMe & ISMINE_WATCH_ONLY)
strHTML += "<b>" + tr("From") + ":</b> " + tr("watch-only") + "<br>";
//
// Debit
//
auto mine = wtx.txout_is_mine.begin();
for (const CTxOut& txout : wtx.tx->vout)
{
// Ignore change
isminetype toSelf = *(mine++);
if ((toSelf == ISMINE_SPENDABLE) && (fAllFromMe == ISMINE_SPENDABLE))
continue;
if (!wtx.value_map.count("to") || wtx.value_map["to"].empty())
{
// Offline transaction
CTxDestination address;
if (ExtractDestination(txout.scriptPubKey, address))
{
strHTML += "<b>" + tr("To") + ":</b> ";
std::string name;
if (wallet.getAddress(
address, &name, /* is_mine= */ nullptr, /* purpose= */ nullptr) && !name.empty())
strHTML += GUIUtil::HtmlEscape(name) + " ";
strHTML += GUIUtil::HtmlEscape(EncodeDestination(address));
if(toSelf == ISMINE_SPENDABLE)
strHTML += " (own address)";
else if(toSelf & ISMINE_WATCH_ONLY)
strHTML += " (watch-only)";
strHTML += "<br>";
}
}
strHTML += "<b>" + tr("Debit") + ":</b> " + BitcoinUnits::formatHtmlWithUnit(unit, -txout.nValue) + "<br>";
if(toSelf)
strHTML += "<b>" + tr("Credit") + ":</b> " + BitcoinUnits::formatHtmlWithUnit(unit, txout.nValue) + "<br>";
}
if (fAllToMe)
{
// Payment to self
CAmount nChange = wtx.change;
CAmount nValue = nCredit - nChange;
strHTML += "<b>" + tr("Total debit") + ":</b> " + BitcoinUnits::formatHtmlWithUnit(unit, -nValue) + "<br>";
strHTML += "<b>" + tr("Total credit") + ":</b> " + BitcoinUnits::formatHtmlWithUnit(unit, nValue) + "<br>";
}
CAmount nTxFee = nDebit - wtx.tx->GetValueOut();
if (nTxFee > 0)
strHTML += "<b>" + tr("Transaction fee") + ":</b> " + BitcoinUnits::formatHtmlWithUnit(unit, -nTxFee) + "<br>";
}
else
{
//
// Mixed debit transaction
//
auto mine = wtx.txin_is_mine.begin();
for (const CTxIn& txin : wtx.tx->vin) {
if (*(mine++)) {
strHTML += "<b>" + tr("Debit") + ":</b> " + BitcoinUnits::formatHtmlWithUnit(unit, -wallet.getDebit(txin, ISMINE_ALL)) + "<br>";
}
}
mine = wtx.txout_is_mine.begin();
for (const CTxOut& txout : wtx.tx->vout) {
if (*(mine++)) {
strHTML += "<b>" + tr("Credit") + ":</b> " + BitcoinUnits::formatHtmlWithUnit(unit, wallet.getCredit(txout, ISMINE_ALL)) + "<br>";
}
}
}
}
strHTML += "<b>" + tr("Net amount") + ":</b> " + BitcoinUnits::formatHtmlWithUnit(unit, nNet, true) + "<br>";
//
// Message
//
if (wtx.value_map.count("message") && !wtx.value_map["message"].empty())
strHTML += "<br><b>" + tr("Message") + ":</b><br>" + GUIUtil::HtmlEscape(wtx.value_map["message"], true) + "<br>";
if (wtx.value_map.count("comment") && !wtx.value_map["comment"].empty())
strHTML += "<br><b>" + tr("Comment") + ":</b><br>" + GUIUtil::HtmlEscape(wtx.value_map["comment"], true) + "<br>";
strHTML += "<b>" + tr("Transaction ID") + ":</b> " + rec->getTxHash() + "<br>";
strHTML += "<b>" + tr("Transaction total size") + ":</b> " + QString::number(wtx.tx->GetTotalSize()) + " bytes<br>";
strHTML += "<b>" + tr("Transaction virtual size") + ":</b> " + QString::number(GetVirtualTransactionSize(*wtx.tx)) + " bytes<br>";
strHTML += "<b>" + tr("Output index") + ":</b> " + QString::number(rec->getOutputIndex()) + "<br>";
// Message from normal bitcoin:URI (bitcoin:123...?message=example)
for (const std::pair<std::string, std::string>& r : orderForm) {
if (r.first == "Message")
strHTML += "<br><b>" + tr("Message") + ":</b><br>" + GUIUtil::HtmlEscape(r.second, true) + "<br>";
//
// PaymentRequest info:
//
if (r.first == "PaymentRequest")
{
QString merchant;
if (!GetPaymentRequestMerchant(r.second, merchant)) {
merchant.clear();
} else {
merchant += tr(" (Certificate was not verified)");
}
if (!merchant.isNull()) {
strHTML += "<b>" + tr("Merchant") + ":</b> " + GUIUtil::HtmlEscape(merchant) + "<br>";
}
}
}
if (wtx.is_coinbase)
{
quint32 numBlocksToMaturity = COINBASE_MATURITY + 1;
strHTML += "<br>" + tr("Generated coins must mature %1 blocks before they can be spent. When you generated this block, it was broadcast to the network to be added to the block chain. If it fails to get into the chain, its state will change to \"not accepted\" and it won't be spendable. This may occasionally happen if another node generates a block within a few seconds of yours.").arg(QString::number(numBlocksToMaturity)) + "<br>";
}
//
// Debug view
//
if (node.getLogCategories() != BCLog::NONE)
{
strHTML += "<hr><br>" + tr("Debug information") + "<br><br>";
for (const CTxIn& txin : wtx.tx->vin)
if(wallet.txinIsMine(txin))
strHTML += "<b>" + tr("Debit") + ":</b> " + BitcoinUnits::formatHtmlWithUnit(unit, -wallet.getDebit(txin, ISMINE_ALL)) + "<br>";
for (const CTxOut& txout : wtx.tx->vout)
if(wallet.txoutIsMine(txout))
strHTML += "<b>" + tr("Credit") + ":</b> " + BitcoinUnits::formatHtmlWithUnit(unit, wallet.getCredit(txout, ISMINE_ALL)) + "<br>";
strHTML += "<br><b>" + tr("Transaction") + ":</b><br>";
strHTML += GUIUtil::HtmlEscape(wtx.tx->ToString(), true);
strHTML += "<br><b>" + tr("Inputs") + ":</b>";
strHTML += "<ul>";
for (const CTxIn& txin : wtx.tx->vin)
{
COutPoint prevout = txin.prevout;
Coin prev;
if(node.getUnspentOutput(prevout, prev))
{
{
strHTML += "<li>";
const CTxOut &vout = prev.out;
CTxDestination address;
if (ExtractDestination(vout.scriptPubKey, address))
{
std::string name;
if (wallet.getAddress(address, &name, /* is_mine= */ nullptr, /* purpose= */ nullptr) && !name.empty())
strHTML += GUIUtil::HtmlEscape(name) + " ";
strHTML += QString::fromStdString(EncodeDestination(address));
}
strHTML = strHTML + " " + tr("Amount") + "=" + BitcoinUnits::formatHtmlWithUnit(unit, vout.nValue);
strHTML = strHTML + " IsMine=" + (wallet.txoutIsMine(vout) & ISMINE_SPENDABLE ? tr("true") : tr("false")) + "</li>";
strHTML = strHTML + " IsWatchOnly=" + (wallet.txoutIsMine(vout) & ISMINE_WATCH_ONLY ? tr("true") : tr("false")) + "</li>";
}
}
}
strHTML += "</ul>";
}
strHTML += "</font></html>";
return strHTML;
}