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