psbtoperationsdialog.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/psbtoperationsdialog.h>
   6  
   7  #include <common/messages.h>
   8  #include <core_io.h>
   9  #include <interfaces/node.h>
  10  #include <key_io.h>
  11  #include <node/psbt.h>
  12  #include <node/types.h>
  13  #include <policy/policy.h>
  14  #include <qt/limenkaunits.h>
  15  #include <qt/forms/ui_psbtoperationsdialog.h>
  16  #include <qt/guiutil.h>
  17  #include <qt/optionsmodel.h>
  18  #include <util/fs.h>
  19  #include <util/strencodings.h>
  20  
  21  #include <fstream>
  22  #include <iostream>
  23  #include <string>
  24  
  25  using common::TransactionErrorString;
  26  using node::AnalyzePSBT;
  27  using node::DEFAULT_MAX_RAW_TX_FEE_RATE;
  28  using node::PSBTAnalysis;
  29  using node::TransactionError;
  30  
  31  PSBTOperationsDialog::PSBTOperationsDialog(
  32      QWidget* parent, WalletModel* wallet_model, ClientModel* client_model) : QDialog(parent, GUIUtil::dialog_flags),
  33                                                                               m_ui(new Ui::PSBTOperationsDialog),
  34                                                                               m_wallet_model(wallet_model),
  35                                                                               m_client_model(client_model)
  36  {
  37      m_ui->setupUi(this);
  38  
  39      connect(m_ui->signTransactionButton, &QPushButton::clicked, this, &PSBTOperationsDialog::signTransaction);
  40      connect(m_ui->broadcastTransactionButton, &QPushButton::clicked, this, &PSBTOperationsDialog::broadcastTransaction);
  41      connect(m_ui->copyToClipboardButton, &QPushButton::clicked, this, &PSBTOperationsDialog::copyToClipboard);
  42      connect(m_ui->saveButton, &QPushButton::clicked, this, &PSBTOperationsDialog::saveTransaction);
  43  
  44      connect(m_ui->closeButton, &QPushButton::clicked, this, &PSBTOperationsDialog::close);
  45  
  46      m_ui->signTransactionButton->setEnabled(false);
  47      m_ui->broadcastTransactionButton->setEnabled(false);
  48  }
  49  
  50  PSBTOperationsDialog::~PSBTOperationsDialog()
  51  {
  52      delete m_ui;
  53  }
  54  
  55  void PSBTOperationsDialog::openWithPSBT(PartiallySignedTransaction psbtx)
  56  {
  57      m_transaction_data = psbtx;
  58  
  59      bool complete = FinalizePSBT(psbtx); // Make sure all existing signatures are fully combined before checking for completeness.
  60      if (m_wallet_model) {
  61          size_t n_could_sign;
  62          const auto err{m_wallet_model->wallet().fillPSBT(SIGHASH_ALL, /*sign=*/false, /*bip32derivs=*/true, &n_could_sign, m_transaction_data, complete)};
  63          if (err) {
  64              showStatus(tr("Failed to load transaction: %1")
  65                             .arg(QString::fromStdString(PSBTErrorString(*err).translated)),
  66                         StatusLevel::ERR);
  67              return;
  68          }
  69          m_ui->signTransactionButton->setEnabled(!complete && !m_wallet_model->wallet().privateKeysDisabled() && n_could_sign > 0);
  70      } else {
  71          m_ui->signTransactionButton->setEnabled(false);
  72      }
  73  
  74      m_ui->broadcastTransactionButton->setEnabled(complete);
  75  
  76      updateTransactionDisplay();
  77  }
  78  
  79  void PSBTOperationsDialog::signTransaction()
  80  {
  81      bool complete;
  82      size_t n_signed;
  83  
  84      WalletModel::UnlockContext ctx(m_wallet_model->requestUnlock());
  85  
  86      const auto err{m_wallet_model->wallet().fillPSBT(SIGHASH_DEFAULT, /*sign=*/true, /*bip32derivs=*/true, &n_signed, m_transaction_data, complete)};
  87  
  88      if (err) {
  89          showStatus(tr("Failed to sign transaction: %1")
  90              .arg(QString::fromStdString(PSBTErrorString(*err).translated)), StatusLevel::ERR);
  91          return;
  92      }
  93  
  94      updateTransactionDisplay();
  95  
  96      if (!complete && !ctx.isValid()) {
  97          showStatus(tr("Cannot sign inputs while wallet is locked."), StatusLevel::WARN);
  98      } else if (!complete && n_signed < 1) {
  99          showStatus(tr("Could not sign any more inputs."), StatusLevel::WARN);
 100      } else if (!complete) {
 101          showStatus(tr("Signed %n input(s), but more signatures are still required.", "", n_signed),
 102              StatusLevel::INFO);
 103      } else {
 104          showStatus(tr("Signed transaction successfully. Transaction is ready to broadcast."),
 105              StatusLevel::INFO);
 106          m_ui->broadcastTransactionButton->setEnabled(true);
 107      }
 108  }
 109  
 110  void PSBTOperationsDialog::broadcastTransaction()
 111  {
 112      CMutableTransaction mtx;
 113      if (!FinalizeAndExtractPSBT(m_transaction_data, mtx)) {
 114          // This is never expected to fail unless we were given a malformed PSBT
 115          // (e.g. with an invalid signature.)
 116          showStatus(tr("Unknown error processing transaction."), StatusLevel::ERR);
 117          return;
 118      }
 119  
 120      CTransactionRef tx = MakeTransactionRef(mtx);
 121      std::string err_string;
 122      TransactionError error =
 123          m_client_model->node().broadcastTransaction(tx, DEFAULT_MAX_RAW_TX_FEE_RATE, err_string);
 124  
 125      if (error == TransactionError::OK) {
 126          showStatus(tr("Transaction broadcast successfully! Transaction ID: %1")
 127              .arg(QString::fromStdString(tx->GetHash().GetHex())), StatusLevel::INFO);
 128      } else {
 129          showStatus(tr("Transaction broadcast failed: %1")
 130              .arg(QString::fromStdString(TransactionErrorString(error).translated)), StatusLevel::ERR);
 131      }
 132  }
 133  
 134  void PSBTOperationsDialog::copyToClipboard() {
 135      DataStream ssTx{};
 136      ssTx << m_transaction_data;
 137      GUIUtil::setClipboard(EncodeBase64(ssTx.str()).c_str());
 138      showStatus(tr("PSBT copied to clipboard."), StatusLevel::INFO);
 139  }
 140  
 141  void PSBTOperationsDialog::saveTransaction() {
 142      DataStream ssTx{};
 143      ssTx << m_transaction_data;
 144  
 145      const LimenkaUnits::Unit unit = m_client_model->getOptionsModel()->getDisplayUnit();
 146      QString selected_filter;
 147      QString filename_suggestion = "";
 148      bool first = true;
 149      for (const CTxOut& out : m_transaction_data.tx->vout) {
 150          if (!first) {
 151              filename_suggestion.append("-");
 152          }
 153          CTxDestination address;
 154          ExtractDestination(out.scriptPubKey, address);
 155          QString amount = LimenkaUnits::format(unit, out.nValue, /*plussign=*/false, LimenkaUnits::SeparatorStyle::NEVER);
 156          if (unit != LimenkaUnits::Unit::BTC) amount += LimenkaUnits::shortName(unit);  // NOTE: no space
 157          QString address_str = QString::fromStdString(EncodeDestination(address));
 158          filename_suggestion.append(address_str + "-" + amount);
 159          first = false;
 160      }
 161      filename_suggestion.append(".psbt");
 162      QString filename = GUIUtil::getSaveFileName(this,
 163          tr("Save Transaction Data"), filename_suggestion,
 164          //: Expanded name of the binary PSBT file format. See: BIP 174.
 165          tr("Partially Signed Transaction (Binary)") + QLatin1String(" (*.psbt)"), &selected_filter);
 166      if (filename.isEmpty()) {
 167          return;
 168      }
 169      std::ofstream out{filename.toLocal8Bit().data(), std::ofstream::out | std::ofstream::binary};
 170      out << ssTx.str();
 171      out.close();
 172      showStatus(tr("PSBT saved to disk."), StatusLevel::INFO);
 173  }
 174  
 175  void PSBTOperationsDialog::updateTransactionDisplay() {
 176      m_ui->transactionDescription->setText(renderTransaction(m_transaction_data));
 177      showTransactionStatus(m_transaction_data);
 178  }
 179  
 180  QString PSBTOperationsDialog::renderTransaction(const PartiallySignedTransaction &psbtx)
 181  {
 182      const QFont font_for_money_BTC = m_client_model->getOptionsModel()->getFontForMoney(LimenkaUnit::BTC);
 183      QString tx_description;
 184      QLatin1String bullet_point(" * ");
 185      CAmount totalAmount = 0;
 186      for (const CTxOut& out : psbtx.tx->vout) {
 187          CTxDestination address;
 188          ExtractDestination(out.scriptPubKey, address);
 189          totalAmount += out.nValue;
 190          tx_description.append(bullet_point).append(tr("Sends %1 to %2")
 191              .arg(LimenkaUnits::formatHtmlWithUnit(font_for_money_BTC, LimenkaUnit::BTC, out.nValue))
 192              .arg(QString::fromStdString(EncodeDestination(address))));
 193          // Check if the address is one of ours
 194          if (m_wallet_model != nullptr && m_wallet_model->wallet().txoutIsMine(out)) tx_description.append(" (" + tr("own address") + ")");
 195          tx_description.append("<br>");
 196      }
 197  
 198      PSBTAnalysis analysis = AnalyzePSBT(psbtx);
 199      tx_description.append(bullet_point);
 200      if (!*analysis.fee) {
 201          // This happens if the transaction is missing input UTXO information.
 202          tx_description.append(tr("Unable to calculate transaction fee or total transaction amount."));
 203      } else {
 204          tx_description.append(tr("Pays transaction fee: "));
 205          tx_description.append(LimenkaUnits::formatHtmlWithUnit(font_for_money_BTC, LimenkaUnit::BTC, *analysis.fee));
 206  
 207          // add total amount in all subdivision units
 208          tx_description.append("<hr />");
 209          QStringList alternativeUnits;
 210          for (const LimenkaUnits::Unit u : LimenkaUnits::availableUnits())
 211          {
 212              if(u != m_client_model->getOptionsModel()->getDisplayUnit()) {
 213                  const QFont font_for_money_u = m_client_model->getOptionsModel()->getFontForMoney(u);
 214                  alternativeUnits.append(LimenkaUnits::formatHtmlWithUnit(font_for_money_u, u, totalAmount));
 215              }
 216          }
 217          const LimenkaUnit display_unit = m_client_model->getOptionsModel()->getDisplayUnit();
 218          const QFont font_for_money = m_client_model->getOptionsModel()->getFontForMoney(display_unit);
 219          tx_description.append(QString("<b>%1</b>: <b>%2</b>").arg(tr("Total Amount"))
 220              .arg(LimenkaUnits::formatHtmlWithUnit(font_for_money, display_unit, totalAmount)));
 221          tx_description.append(QString("<br /><span style='font-size:10pt; font-weight:normal;'>(=%1)</span>")
 222              .arg(alternativeUnits.join(" " + tr("or") + " ")));
 223      }
 224  
 225      size_t num_unsigned = CountPSBTUnsignedInputs(psbtx);
 226      if (num_unsigned > 0) {
 227          tx_description.append("<br><br>");
 228          tx_description.append(tr("Transaction has %n unsigned input(s).", "", num_unsigned));
 229      }
 230  
 231      return tx_description;
 232  }
 233  
 234  void PSBTOperationsDialog::showStatus(const QString &msg, StatusLevel level) {
 235      m_ui->statusBar->setText(msg);
 236      switch (level) {
 237          case StatusLevel::INFO: {
 238              m_ui->statusBar->setStyleSheet("QLabel { background-color : lightgreen }");
 239              break;
 240          }
 241          case StatusLevel::WARN: {
 242              m_ui->statusBar->setStyleSheet("QLabel { background-color : orange }");
 243              break;
 244          }
 245          case StatusLevel::ERR: {
 246              m_ui->statusBar->setStyleSheet("QLabel { background-color : red }");
 247              break;
 248          }
 249      }
 250      m_ui->statusBar->show();
 251  }
 252  
 253  size_t PSBTOperationsDialog::couldSignInputs(const PartiallySignedTransaction &psbtx) {
 254      if (!m_wallet_model) {
 255          return 0;
 256      }
 257  
 258      size_t n_signed;
 259      bool complete;
 260      const auto err{m_wallet_model->wallet().fillPSBT(SIGHASH_ALL, /*sign=*/false, /*bip32derivs=*/false, &n_signed, m_transaction_data, complete)};
 261  
 262      if (err) {
 263          return 0;
 264      }
 265      return n_signed;
 266  }
 267  
 268  void PSBTOperationsDialog::showTransactionStatus(const PartiallySignedTransaction &psbtx) {
 269      PSBTAnalysis analysis = AnalyzePSBT(psbtx);
 270      size_t n_could_sign = couldSignInputs(psbtx);
 271  
 272      switch (analysis.next) {
 273          case PSBTRole::UPDATER: {
 274              showStatus(tr("Transaction is missing some information about inputs."), StatusLevel::WARN);
 275              break;
 276          }
 277          case PSBTRole::SIGNER: {
 278              QString need_sig_text = tr("Transaction still needs signature(s).");
 279              StatusLevel level = StatusLevel::INFO;
 280              if (!m_wallet_model) {
 281                  need_sig_text += " " + tr("(But no wallet is loaded.)");
 282                  level = StatusLevel::WARN;
 283              } else if (m_wallet_model->wallet().privateKeysDisabled()) {
 284                  need_sig_text += " " + tr("(But this wallet cannot sign transactions.)");
 285                  level = StatusLevel::WARN;
 286              } else if (n_could_sign < 1) {
 287                  need_sig_text += " " + tr("(But this wallet does not have the right keys.)"); // XXX wording
 288                  level = StatusLevel::WARN;
 289              }
 290              showStatus(need_sig_text, level);
 291              break;
 292          }
 293          case PSBTRole::FINALIZER:
 294          case PSBTRole::EXTRACTOR: {
 295              showStatus(tr("Transaction is fully signed and ready for broadcast."), StatusLevel::INFO);
 296              break;
 297          }
 298          default: {
 299              showStatus(tr("Transaction status is unknown."), StatusLevel::ERR);
 300              break;
 301          }
 302      }
 303  }
 304