transactiondesc.cpp raw

   1  // Copyright (c) 2011-2022 The Limenka developers
   2  // Distributed under the MIT software license, see the accompanying
   3  // file COPYING or http://www.opensource.org/licenses/mit-license.php.
   4  
   5  #include <qt/transactiondesc.h>
   6  
   7  #include <qt/limenkaunits.h>
   8  #include <qt/guiutil.h>
   9  #include <qt/paymentserver.h>
  10  #include <qt/transactionrecord.h>
  11  
  12  #include <common/system.h>
  13  #include <consensus/consensus.h>
  14  #include <interfaces/node.h>
  15  #include <interfaces/wallet.h>
  16  #include <key_io.h>
  17  #include <logging.h>
  18  #include <policy/policy.h>
  19  #include <validation.h>
  20  #include <wallet/types.h>
  21  
  22  #include <stdint.h>
  23  #include <string>
  24  
  25  #include <QLatin1String>
  26  
  27  using wallet::ISMINE_ALL;
  28  using wallet::ISMINE_SPENDABLE;
  29  using wallet::ISMINE_WATCH_ONLY;
  30  using wallet::isminetype;
  31  
  32  QString TransactionDesc::FormatTxStatus(const interfaces::WalletTxStatus& status, bool inMempool)
  33  {
  34      int depth = status.depth_in_main_chain;
  35      if (depth < 0) {
  36          /*: Text explaining the current status of a transaction, shown in the
  37              status field of the details window for this transaction. This status
  38              represents an unconfirmed transaction that conflicts with a confirmed
  39              transaction. */
  40          return tr("conflicted with a transaction with %1 confirmations").arg(-depth);
  41      } else if (depth == 0) {
  42          QString s;
  43          if (inMempool) {
  44              /*: Text explaining the current status of a transaction, shown in the
  45                  status field of the details window for this transaction. This status
  46                  represents an unconfirmed transaction that is in the memory pool. */
  47              s = tr("0/unconfirmed, in memory pool");
  48          } else {
  49              /*: Text explaining the current status of a transaction, shown in the
  50                  status field of the details window for this transaction. This status
  51                  represents an unconfirmed transaction that is not in the memory pool. */
  52              s = tr("0/unconfirmed, not in memory pool");
  53          }
  54          if (status.is_abandoned) {
  55              /*: Text explaining the current status of a transaction, shown in the
  56                  status field of the details window for this transaction. This
  57                  status represents an abandoned transaction. */
  58              s += QLatin1String(", ") + tr("abandoned");
  59          }
  60          return s;
  61      } else if (depth < TransactionRecord::RecommendedNumConfirmations || status.is_assumed) {
  62          /*: Text explaining the current status of a transaction, shown in the
  63              status field of the details window for this transaction. This
  64              status represents a transaction confirmed in at least one block,
  65              but less than 6 blocks. */
  66          return tr("%1/unconfirmed").arg(depth);
  67      } else {
  68          /*: Text explaining the current status of a transaction, shown in the
  69              status field of the details window for this transaction. This status
  70              represents a transaction confirmed in 6 or more blocks. */
  71          return tr("%1 confirmations").arg(depth);
  72      }
  73  }
  74  
  75  // Takes an encoded PaymentRequest as a string and tries to find the Common Name of the X.509 certificate
  76  // used to sign the PaymentRequest.
  77  bool GetPaymentRequestMerchant(const std::string& pr, QString& merchant)
  78  {
  79      // Search for the supported pki type strings
  80      if (pr.find(std::string({0x12, 0x0b}) + "x509+sha256") != std::string::npos || pr.find(std::string({0x12, 0x09}) + "x509+sha1") != std::string::npos) {
  81          // We want the common name of the Subject of the cert. This should be the second occurrence
  82          // of the bytes 0x0603550403. The first occurrence of those is the common name of the issuer.
  83          // After those bytes will be either 0x13 or 0x0C, then length, then either the ascii or utf8
  84          // string with the common name which is the merchant name
  85          size_t cn_pos = pr.find({0x06, 0x03, 0x55, 0x04, 0x03});
  86          if (cn_pos != std::string::npos) {
  87              cn_pos = pr.find({0x06, 0x03, 0x55, 0x04, 0x03}, cn_pos + 5);
  88              if (cn_pos != std::string::npos) {
  89                  cn_pos += 5;
  90                  if (pr[cn_pos] == 0x13 || pr[cn_pos] == 0x0c) {
  91                      cn_pos++; // Consume the type
  92                      int str_len = pr[cn_pos];
  93                      cn_pos++; // Consume the string length
  94                      merchant = QString::fromUtf8(pr.data() + cn_pos, str_len);
  95                      return true;
  96                  }
  97              }
  98          }
  99      }
 100      return false;
 101  }
 102  
 103  QString TransactionDesc::toHTML(interfaces::Node& node, interfaces::Wallet& wallet, TransactionRecord* rec, LimenkaUnit unit, const QFont& font_for_money)
 104  {
 105      int numBlocks;
 106      interfaces::WalletTxStatus status;
 107      interfaces::WalletOrderForm orderForm;
 108      bool inMempool;
 109      interfaces::WalletTx wtx = wallet.getWalletTxDetails(rec->hash, status, orderForm, inMempool, numBlocks);
 110  
 111      QString strHTML;
 112  
 113      strHTML.reserve(4000);
 114      strHTML += "<html><font face='verdana, arial, helvetica, sans-serif'>";
 115  
 116      int64_t nTime = wtx.time;
 117      CAmount nCredit = wtx.credit;
 118      CAmount nDebit = wtx.debit;
 119      CAmount nNet = nCredit - nDebit;
 120  
 121      strHTML += "<b>" + tr("Status") + ":</b> " + FormatTxStatus(status, inMempool);
 122      strHTML += "<br>";
 123  
 124      strHTML += "<b>" + tr("Date") + ":</b> " + (nTime ? GUIUtil::dateTimeStr(nTime) : "") + "<br>";
 125  
 126      //
 127      // From
 128      //
 129      if (wtx.is_coinbase)
 130      {
 131          strHTML += "<b>" + tr("Source") + ":</b> " + tr("Generated") + "<br>";
 132      }
 133      else if (wtx.value_map.count("from") && !wtx.value_map["from"].empty())
 134      {
 135          // Online transaction
 136          strHTML += "<b>" + tr("From") + ":</b> " + GUIUtil::HtmlEscape(wtx.value_map["from"]) + "<br>";
 137      }
 138      else
 139      {
 140          // Offline transaction
 141          if (nNet > 0)
 142          {
 143              // Credit
 144              CTxDestination address = DecodeDestination(rec->address);
 145              if (IsValidDestination(address)) {
 146                  std::string name;
 147                  isminetype ismine;
 148                  if (wallet.getAddress(address, &name, &ismine, /* purpose= */ nullptr))
 149                  {
 150                      strHTML += "<b>" + tr("From") + ":</b> " + tr("unknown") + "<br>";
 151                      strHTML += "<b>" + tr("To") + ":</b> ";
 152                      strHTML += GUIUtil::HtmlEscape(rec->address);
 153                      QString addressOwned = ismine == ISMINE_SPENDABLE ? tr("own address") : tr("watch-only");
 154                      if (!name.empty())
 155                          strHTML += " (" + addressOwned + ", " + tr("label") + ": " + GUIUtil::HtmlEscape(name) + ")";
 156                      else
 157                          strHTML += " (" + addressOwned + ")";
 158                      strHTML += "<br>";
 159                  }
 160              }
 161          }
 162      }
 163  
 164      //
 165      // To
 166      //
 167      if (wtx.value_map.count("to") && !wtx.value_map["to"].empty())
 168      {
 169          // Online transaction
 170          std::string strAddress = wtx.value_map["to"];
 171          strHTML += "<b>" + tr("To") + ":</b> ";
 172          CTxDestination dest = DecodeDestination(strAddress);
 173          std::string name;
 174          if (wallet.getAddress(
 175                  dest, &name, /* is_mine= */ nullptr, /* purpose= */ nullptr) && !name.empty())
 176              strHTML += GUIUtil::HtmlEscape(name) + " ";
 177          strHTML += GUIUtil::HtmlEscape(strAddress) + "<br>";
 178      }
 179  
 180      //
 181      // Amount
 182      //
 183      if (wtx.is_coinbase && nCredit == 0)
 184      {
 185          //
 186          // Coinbase
 187          //
 188          CAmount nUnmatured = 0;
 189          for (const CTxOut& txout : wtx.tx->vout)
 190              nUnmatured += wallet.getCredit(txout, ISMINE_ALL);
 191          strHTML += "<b>" + tr("Credit") + ":</b> ";
 192          if (status.is_in_main_chain)
 193              strHTML += LimenkaUnits::formatHtmlWithUnit(font_for_money, unit, nUnmatured)+ " (" + tr("matures in %n more block(s)", "", status.blocks_to_maturity) + ")";
 194          else
 195              strHTML += "(" + tr("not accepted") + ")";
 196          strHTML += "<br>";
 197      }
 198      else if (nNet > 0)
 199      {
 200          //
 201          // Credit
 202          //
 203          strHTML += "<b>" + tr("Credit") + ":</b> " + LimenkaUnits::formatHtmlWithUnit(font_for_money, unit, nNet) + "<br>";
 204      }
 205      else
 206      {
 207          isminetype fAllFromMe = ISMINE_SPENDABLE;
 208          for (const isminetype mine : wtx.txin_is_mine)
 209          {
 210              if(fAllFromMe > mine) fAllFromMe = mine;
 211          }
 212  
 213          isminetype fAllToMe = ISMINE_SPENDABLE;
 214          for (const isminetype mine : wtx.txout_is_mine)
 215          {
 216              if(fAllToMe > mine) fAllToMe = mine;
 217          }
 218  
 219          if (fAllFromMe)
 220          {
 221              if(fAllFromMe & ISMINE_WATCH_ONLY)
 222                  strHTML += "<b>" + tr("From") + ":</b> " + tr("watch-only") + "<br>";
 223  
 224              //
 225              // Debit
 226              //
 227              auto mine = wtx.txout_is_mine.begin();
 228              for (const CTxOut& txout : wtx.tx->vout)
 229              {
 230                  // Ignore change
 231                  isminetype toSelf = *(mine++);
 232                  if ((toSelf == ISMINE_SPENDABLE) && (fAllFromMe == ISMINE_SPENDABLE))
 233                      continue;
 234  
 235                  if (!wtx.value_map.count("to") || wtx.value_map["to"].empty())
 236                  {
 237                      // Offline transaction
 238                      CTxDestination address;
 239                      if (ExtractDestination(txout.scriptPubKey, address))
 240                      {
 241                          strHTML += "<b>" + tr("To") + ":</b> ";
 242                          std::string name;
 243                          if (wallet.getAddress(
 244                                  address, &name, /* is_mine= */ nullptr, /* purpose= */ nullptr) && !name.empty())
 245                              strHTML += GUIUtil::HtmlEscape(name) + " ";
 246                          strHTML += GUIUtil::HtmlEscape(EncodeDestination(address));
 247                          if(toSelf == ISMINE_SPENDABLE)
 248                              strHTML += " (" + tr("own address") + ")";
 249                          else if(toSelf & ISMINE_WATCH_ONLY)
 250                              strHTML += " (" + tr("watch-only") + ")";
 251                          strHTML += "<br>";
 252                      }
 253                  }
 254  
 255                  strHTML += "<b>" + tr("Debit") + ":</b> " + LimenkaUnits::formatHtmlWithUnit(font_for_money, unit, -txout.nValue) + "<br>";
 256                  if(toSelf)
 257                      strHTML += "<b>" + tr("Credit") + ":</b> " + LimenkaUnits::formatHtmlWithUnit(font_for_money, unit, txout.nValue) + "<br>";
 258              }
 259  
 260              if (fAllToMe)
 261              {
 262                  // Payment to self
 263                  CAmount nChange = wtx.change;
 264                  CAmount nValue = nCredit - nChange;
 265                  strHTML += "<b>" + tr("Total debit") + ":</b> " + LimenkaUnits::formatHtmlWithUnit(font_for_money, unit, -nValue) + "<br>";
 266                  strHTML += "<b>" + tr("Total credit") + ":</b> " + LimenkaUnits::formatHtmlWithUnit(font_for_money, unit, nValue) + "<br>";
 267              }
 268  
 269              CAmount nTxFee = nDebit - wtx.tx->GetValueOut();
 270              if (nTxFee > 0)
 271                  strHTML += "<b>" + tr("Transaction fee") + ":</b> " + LimenkaUnits::formatHtmlWithUnit(font_for_money, unit, -nTxFee) + "<br>";
 272          }
 273          else
 274          {
 275              //
 276              // Mixed debit transaction
 277              //
 278              auto mine = wtx.txin_is_mine.begin();
 279              for (const CTxIn& txin : wtx.tx->vin) {
 280                  if (*(mine++)) {
 281                      strHTML += "<b>" + tr("Debit") + ":</b> " + LimenkaUnits::formatHtmlWithUnit(font_for_money, unit, -wallet.getDebit(txin, ISMINE_ALL)) + "<br>";
 282                  }
 283              }
 284              mine = wtx.txout_is_mine.begin();
 285              for (const CTxOut& txout : wtx.tx->vout) {
 286                  if (*(mine++)) {
 287                      strHTML += "<b>" + tr("Credit") + ":</b> " + LimenkaUnits::formatHtmlWithUnit(font_for_money, unit, wallet.getCredit(txout, ISMINE_ALL)) + "<br>";
 288                  }
 289              }
 290          }
 291      }
 292  
 293      strHTML += "<b>" + tr("Net amount") + ":</b> " + LimenkaUnits::formatHtmlWithUnit(font_for_money, unit, nNet, true) + "<br>";
 294  
 295      //
 296      // Message
 297      //
 298      if (wtx.value_map.count("message") && !wtx.value_map["message"].empty())
 299          strHTML += "<br><b>" + tr("Message") + ":</b><br>" + GUIUtil::HtmlEscape(wtx.value_map["message"], true) + "<br>";
 300      if (wtx.value_map.count("comment") && !wtx.value_map["comment"].empty())
 301          strHTML += "<br><b>" + tr("Comment") + ":</b><br>" + GUIUtil::HtmlEscape(wtx.value_map["comment"], true) + "<br>";
 302  
 303      strHTML += "<b>" + tr("Transaction ID") + ":</b> " + rec->getTxHash() + "<br>";
 304      strHTML += "<b>" + tr("Transaction total size") + ":</b> " + QString::number(wtx.tx->GetTotalSize()) + " bytes<br>";
 305      strHTML += "<b>" + tr("Transaction virtual size") + ":</b> " + QString::number(GetVirtualTransactionSize(*wtx.tx)) + " bytes<br>";
 306      strHTML += "<b>" + tr("Output index") + ":</b> " + QString::number(rec->getOutputIndex()) + "<br>";
 307  
 308      // Message from normal limenka:URI (limenka:123...?message=example)
 309      for (const std::pair<std::string, std::string>& r : orderForm) {
 310          if (r.first == "Message")
 311              strHTML += "<br><b>" + tr("Message") + ":</b><br>" + GUIUtil::HtmlEscape(r.second, true) + "<br>";
 312  
 313          //
 314          // PaymentRequest info:
 315          //
 316          if (r.first == "PaymentRequest")
 317          {
 318              QString merchant;
 319              if (!GetPaymentRequestMerchant(r.second, merchant)) {
 320                  merchant.clear();
 321              } else {
 322                  merchant = tr("%1 (Certificate was not verified)").arg(merchant);
 323              }
 324              if (!merchant.isNull()) {
 325                  strHTML += "<b>" + tr("Merchant") + ":</b> " + GUIUtil::HtmlEscape(merchant) + "<br>";
 326              }
 327          }
 328      }
 329  
 330      if (wtx.is_coinbase)
 331      {
 332          quint32 numBlocksToMaturity = COINBASE_MATURITY +  1;
 333          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>";
 334      }
 335  
 336      //
 337      // Debug view
 338      //
 339      if (node.getLogCategories() != BCLog::NONE)
 340      {
 341          strHTML += "<hr><br>" + tr("Debug information") + "<br><br>";
 342          for (const CTxIn& txin : wtx.tx->vin)
 343              if(wallet.txinIsMine(txin))
 344                  strHTML += "<b>" + tr("Debit") + ":</b> " + LimenkaUnits::formatHtmlWithUnit(font_for_money, unit, -wallet.getDebit(txin, ISMINE_ALL)) + "<br>";
 345          for (const CTxOut& txout : wtx.tx->vout)
 346              if(wallet.txoutIsMine(txout))
 347                  strHTML += "<b>" + tr("Credit") + ":</b> " + LimenkaUnits::formatHtmlWithUnit(font_for_money, unit, wallet.getCredit(txout, ISMINE_ALL)) + "<br>";
 348  
 349          strHTML += "<br><b>" + tr("Transaction") + ":</b><br>";
 350          strHTML += GUIUtil::HtmlEscape(wtx.tx->ToString(), true);
 351  
 352          strHTML += "<br><b>" + tr("Inputs") + ":</b>";
 353          strHTML += "<ul>";
 354  
 355          for (const CTxIn& txin : wtx.tx->vin)
 356          {
 357              COutPoint prevout = txin.prevout;
 358  
 359              if (auto prev{node.getUnspentOutput(prevout)}) {
 360                  {
 361                      strHTML += "<li>";
 362                      const CTxOut& vout = prev->out;
 363                      CTxDestination address;
 364                      if (ExtractDestination(vout.scriptPubKey, address))
 365                      {
 366                          std::string name;
 367                          if (wallet.getAddress(address, &name, /* is_mine= */ nullptr, /* purpose= */ nullptr) && !name.empty())
 368                              strHTML += GUIUtil::HtmlEscape(name) + " ";
 369                          strHTML += QString::fromStdString(EncodeDestination(address));
 370                      }
 371                      strHTML = strHTML + " " + tr("Amount") + "=" + LimenkaUnits::formatHtmlWithUnit(font_for_money, unit, vout.nValue);
 372                      strHTML = strHTML + " IsMine=" + (wallet.txoutIsMine(vout) & ISMINE_SPENDABLE ? tr("true") : tr("false")) + "</li>";
 373                      strHTML = strHTML + " IsWatchOnly=" + (wallet.txoutIsMine(vout) & ISMINE_WATCH_ONLY ? tr("true") : tr("false")) + "</li>";
 374                  }
 375              }
 376          }
 377  
 378          strHTML += "</ul>";
 379      }
 380  
 381      strHTML += "</font></html>";
 382      return strHTML;
 383  }
 384