// Copyright (c) 2011-2022 The Bitcoin Core developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include 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) { /*: Text explaining the current status of a transaction, shown in the status field of the details window for this transaction. This status represents an unconfirmed transaction that conflicts with a confirmed transaction. */ return tr("conflicted with a transaction with %1 confirmations").arg(-depth); } else if (depth == 0) { QString s; if (inMempool) { /*: Text explaining the current status of a transaction, shown in the status field of the details window for this transaction. This status represents an unconfirmed transaction that is in the memory pool. */ s = tr("0/unconfirmed, in memory pool"); } else { /*: Text explaining the current status of a transaction, shown in the status field of the details window for this transaction. This status represents an unconfirmed transaction that is not in the memory pool. */ s = tr("0/unconfirmed, not in memory pool"); } if (status.is_abandoned) { /*: Text explaining the current status of a transaction, shown in the status field of the details window for this transaction. This status represents an abandoned transaction. */ s += QLatin1String(", ") + tr("abandoned"); } return s; } else if (depth < 6) { /*: Text explaining the current status of a transaction, shown in the status field of the details window for this transaction. This status represents a transaction confirmed in at least one block, but less than 6 blocks. */ return tr("%1/unconfirmed").arg(depth); } else { /*: Text explaining the current status of a transaction, shown in the status field of the details window for this transaction. This status represents a transaction confirmed in 6 or more blocks. */ 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 += ""; int64_t nTime = wtx.time; CAmount nCredit = wtx.credit; CAmount nDebit = wtx.debit; CAmount nNet = nCredit - nDebit; strHTML += "" + tr("Status") + ": " + FormatTxStatus(status, inMempool); strHTML += "
"; strHTML += "" + tr("Date") + ": " + (nTime ? GUIUtil::dateTimeStr(nTime) : "") + "
"; // // From // if (wtx.is_coinbase) { strHTML += "" + tr("Source") + ": " + tr("Generated") + "
"; } else if (wtx.value_map.count("from") && !wtx.value_map["from"].empty()) { // Online transaction strHTML += "" + tr("From") + ": " + GUIUtil::HtmlEscape(wtx.value_map["from"]) + "
"; } 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 += "" + tr("From") + ": " + tr("unknown") + "
"; strHTML += "" + tr("To") + ": "; 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 += "
"; } } } } // // To // if (wtx.value_map.count("to") && !wtx.value_map["to"].empty()) { // Online transaction std::string strAddress = wtx.value_map["to"]; strHTML += "" + tr("To") + ": "; 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) + "
"; } // // 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 += "" + tr("Credit") + ": "; 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 += "
"; } else if (nNet > 0) { // // Credit // strHTML += "" + tr("Credit") + ": " + BitcoinUnits::formatHtmlWithUnit(unit, nNet) + "
"; } 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 += "" + tr("From") + ": " + tr("watch-only") + "
"; // // 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 += "" + tr("To") + ": "; 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 += " (" + tr("own address") + ")"; else if(toSelf & ISMINE_WATCH_ONLY) strHTML += " (" + tr("watch-only") + ")"; strHTML += "
"; } } strHTML += "" + tr("Debit") + ": " + BitcoinUnits::formatHtmlWithUnit(unit, -txout.nValue) + "
"; if(toSelf) strHTML += "" + tr("Credit") + ": " + BitcoinUnits::formatHtmlWithUnit(unit, txout.nValue) + "
"; } if (fAllToMe) { // Payment to self CAmount nChange = wtx.change; CAmount nValue = nCredit - nChange; strHTML += "" + tr("Total debit") + ": " + BitcoinUnits::formatHtmlWithUnit(unit, -nValue) + "
"; strHTML += "" + tr("Total credit") + ": " + BitcoinUnits::formatHtmlWithUnit(unit, nValue) + "
"; } CAmount nTxFee = nDebit - wtx.tx->GetValueOut(); if (nTxFee > 0) strHTML += "" + tr("Transaction fee") + ": " + BitcoinUnits::formatHtmlWithUnit(unit, -nTxFee) + "
"; } else { // // Mixed debit transaction // auto mine = wtx.txin_is_mine.begin(); for (const CTxIn& txin : wtx.tx->vin) { if (*(mine++)) { strHTML += "" + tr("Debit") + ": " + BitcoinUnits::formatHtmlWithUnit(unit, -wallet.getDebit(txin, ISMINE_ALL)) + "
"; } } mine = wtx.txout_is_mine.begin(); for (const CTxOut& txout : wtx.tx->vout) { if (*(mine++)) { strHTML += "" + tr("Credit") + ": " + BitcoinUnits::formatHtmlWithUnit(unit, wallet.getCredit(txout, ISMINE_ALL)) + "
"; } } } } strHTML += "" + tr("Net amount") + ": " + BitcoinUnits::formatHtmlWithUnit(unit, nNet, true) + "
"; // // Message // if (wtx.value_map.count("message") && !wtx.value_map["message"].empty()) strHTML += "
" + tr("Message") + ":
" + GUIUtil::HtmlEscape(wtx.value_map["message"], true) + "
"; if (wtx.value_map.count("comment") && !wtx.value_map["comment"].empty()) strHTML += "
" + tr("Comment") + ":
" + GUIUtil::HtmlEscape(wtx.value_map["comment"], true) + "
"; strHTML += "" + tr("Transaction ID") + ": " + rec->getTxHash() + "
"; strHTML += "" + tr("Transaction total size") + ": " + QString::number(wtx.tx->GetTotalSize()) + " bytes
"; strHTML += "" + tr("Transaction virtual size") + ": " + QString::number(GetVirtualTransactionSize(*wtx.tx)) + " bytes
"; strHTML += "" + tr("Output index") + ": " + QString::number(rec->getOutputIndex()) + "
"; // Message from normal bitcoin:URI (bitcoin:123...?message=example) for (const std::pair& r : orderForm) { if (r.first == "Message") strHTML += "
" + tr("Message") + ":
" + GUIUtil::HtmlEscape(r.second, true) + "
"; // // PaymentRequest info: // if (r.first == "PaymentRequest") { QString merchant; if (!GetPaymentRequestMerchant(r.second, merchant)) { merchant.clear(); } else { merchant = tr("%1 (Certificate was not verified)").arg(merchant); } if (!merchant.isNull()) { strHTML += "" + tr("Merchant") + ": " + GUIUtil::HtmlEscape(merchant) + "
"; } } } if (wtx.is_coinbase) { quint32 numBlocksToMaturity = COINBASE_MATURITY + 1; strHTML += "
" + 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)) + "
"; } // // Debug view // if (node.getLogCategories() != BCLog::NONE) { strHTML += "

" + tr("Debug information") + "

"; for (const CTxIn& txin : wtx.tx->vin) if(wallet.txinIsMine(txin)) strHTML += "" + tr("Debit") + ": " + BitcoinUnits::formatHtmlWithUnit(unit, -wallet.getDebit(txin, ISMINE_ALL)) + "
"; for (const CTxOut& txout : wtx.tx->vout) if(wallet.txoutIsMine(txout)) strHTML += "" + tr("Credit") + ": " + BitcoinUnits::formatHtmlWithUnit(unit, wallet.getCredit(txout, ISMINE_ALL)) + "
"; strHTML += "
" + tr("Transaction") + ":
"; strHTML += GUIUtil::HtmlEscape(wtx.tx->ToString(), true); strHTML += "
" + tr("Inputs") + ":"; strHTML += "
    "; for (const CTxIn& txin : wtx.tx->vin) { COutPoint prevout = txin.prevout; if (auto prev{node.getUnspentOutput(prevout)}) { { strHTML += "
  • "; 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")) + "
  • "; strHTML = strHTML + " IsWatchOnly=" + (wallet.txoutIsMine(vout) & ISMINE_WATCH_ONLY ? tr("true") : tr("false")) + ""; } } } strHTML += "
"; } strHTML += "
"; return strHTML; }