/ src / qt / psbtoperationsdialog.cpp
psbtoperationsdialog.cpp
  1  // Copyright (c) 2011-present The Bitcoin Core 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/bitcoinunits.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({.sign = false, .bip32_derivs= 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({.sign = true, .bip32_derivs = 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.GetFeePerK(), 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      QString selected_filter;
146      QString filename_suggestion = "";
147      bool first = true;
148      for (const CTxOut& out : m_transaction_data.tx->vout) {
149          if (!first) {
150              filename_suggestion.append("-");
151          }
152          CTxDestination address;
153          ExtractDestination(out.scriptPubKey, address);
154          QString amount = BitcoinUnits::format(m_client_model->getOptionsModel()->getDisplayUnit(), out.nValue);
155          QString address_str = QString::fromStdString(EncodeDestination(address));
156          filename_suggestion.append(address_str + "-" + amount);
157          first = false;
158      }
159      filename_suggestion.append(".psbt");
160      QString filename = GUIUtil::getSaveFileName(this,
161          tr("Save Transaction Data"), filename_suggestion,
162          //: Expanded name of the binary PSBT file format. See: BIP 174.
163          tr("Partially Signed Transaction (Binary)") + QLatin1String(" (*.psbt)"), &selected_filter);
164      if (filename.isEmpty()) {
165          return;
166      }
167      std::ofstream out{filename.toLocal8Bit().data(), std::ofstream::out | std::ofstream::binary};
168      out << ssTx.str();
169      out.close();
170      showStatus(tr("PSBT saved to disk."), StatusLevel::INFO);
171  }
172  
173  void PSBTOperationsDialog::updateTransactionDisplay() {
174      m_ui->transactionDescription->setText(renderTransaction(m_transaction_data));
175      showTransactionStatus(m_transaction_data);
176  }
177  
178  QString PSBTOperationsDialog::renderTransaction(const PartiallySignedTransaction &psbtx)
179  {
180      QString tx_description;
181      QLatin1String bullet_point(" * ");
182      CAmount totalAmount = 0;
183      for (const CTxOut& out : psbtx.tx->vout) {
184          CTxDestination address;
185          ExtractDestination(out.scriptPubKey, address);
186          totalAmount += out.nValue;
187          tx_description.append(bullet_point).append(tr("Sends %1 to %2")
188              .arg(BitcoinUnits::formatWithUnit(BitcoinUnit::BTC, out.nValue))
189              .arg(QString::fromStdString(EncodeDestination(address))));
190          // Check if the address is one of ours
191          if (m_wallet_model != nullptr && m_wallet_model->wallet().txoutIsMine(out)) tx_description.append(" (" + tr("own address") + ")");
192          tx_description.append("<br>");
193      }
194  
195      PSBTAnalysis analysis = AnalyzePSBT(psbtx);
196      tx_description.append(bullet_point);
197      if (!*analysis.fee) {
198          // This happens if the transaction is missing input UTXO information.
199          tx_description.append(tr("Unable to calculate transaction fee or total transaction amount."));
200      } else {
201          tx_description.append(tr("Pays transaction fee: "));
202          tx_description.append(BitcoinUnits::formatWithUnit(BitcoinUnit::BTC, *analysis.fee));
203  
204          // add total amount in all subdivision units
205          tx_description.append("<hr />");
206          QStringList alternativeUnits;
207          for (const BitcoinUnits::Unit u : BitcoinUnits::availableUnits())
208          {
209              if(u != m_client_model->getOptionsModel()->getDisplayUnit()) {
210                  alternativeUnits.append(BitcoinUnits::formatHtmlWithUnit(u, totalAmount));
211              }
212          }
213          tx_description.append(QString("<b>%1</b>: <b>%2</b>").arg(tr("Total Amount"))
214              .arg(BitcoinUnits::formatHtmlWithUnit(m_client_model->getOptionsModel()->getDisplayUnit(), totalAmount)));
215          tx_description.append(QString("<br /><span style='font-size:10pt; font-weight:normal;'>(=%1)</span>")
216              .arg(alternativeUnits.join(" " + tr("or") + " ")));
217      }
218  
219      size_t num_unsigned = CountPSBTUnsignedInputs(psbtx);
220      if (num_unsigned > 0) {
221          tx_description.append("<br><br>");
222          tx_description.append(tr("Transaction has %n unsigned input(s).", "", num_unsigned));
223      }
224  
225      return tx_description;
226  }
227  
228  void PSBTOperationsDialog::showStatus(const QString &msg, StatusLevel level) {
229      m_ui->statusBar->setText(msg);
230      switch (level) {
231          case StatusLevel::INFO: {
232              m_ui->statusBar->setStyleSheet("QLabel { background-color : lightgreen }");
233              break;
234          }
235          case StatusLevel::WARN: {
236              m_ui->statusBar->setStyleSheet("QLabel { background-color : orange }");
237              break;
238          }
239          case StatusLevel::ERR: {
240              m_ui->statusBar->setStyleSheet("QLabel { background-color : red }");
241              break;
242          }
243      }
244      m_ui->statusBar->show();
245  }
246  
247  size_t PSBTOperationsDialog::couldSignInputs(const PartiallySignedTransaction &psbtx) {
248      if (!m_wallet_model) {
249          return 0;
250      }
251  
252      size_t n_signed;
253      bool complete;
254      const auto err{m_wallet_model->wallet().fillPSBT({.sign = false, .bip32_derivs = false}, &n_signed, m_transaction_data, complete)};
255  
256      if (err) {
257          return 0;
258      }
259      return n_signed;
260  }
261  
262  void PSBTOperationsDialog::showTransactionStatus(const PartiallySignedTransaction &psbtx) {
263      PSBTAnalysis analysis = AnalyzePSBT(psbtx);
264      size_t n_could_sign = couldSignInputs(psbtx);
265  
266      switch (analysis.next) {
267          case PSBTRole::UPDATER: {
268              showStatus(tr("Transaction is missing some information about inputs."), StatusLevel::WARN);
269              break;
270          }
271          case PSBTRole::SIGNER: {
272              QString need_sig_text = tr("Transaction still needs signature(s).");
273              StatusLevel level = StatusLevel::INFO;
274              if (!m_wallet_model) {
275                  need_sig_text += " " + tr("(But no wallet is loaded.)");
276                  level = StatusLevel::WARN;
277              } else if (m_wallet_model->wallet().privateKeysDisabled()) {
278                  need_sig_text += " " + tr("(But this wallet cannot sign transactions.)");
279                  level = StatusLevel::WARN;
280              } else if (n_could_sign < 1) {
281                  need_sig_text += " " + tr("(But this wallet does not have the right keys.)"); // XXX wording
282                  level = StatusLevel::WARN;
283              }
284              showStatus(need_sig_text, level);
285              break;
286          }
287          case PSBTRole::FINALIZER:
288          case PSBTRole::EXTRACTOR: {
289              showStatus(tr("Transaction is fully signed and ready for broadcast."), StatusLevel::INFO);
290              break;
291          }
292          default: {
293              showStatus(tr("Transaction status is unknown."), StatusLevel::ERR);
294              break;
295          }
296      }
297  }