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(std::nullopt, /*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(std::nullopt, /*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.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(std::nullopt, /*sign=*/false, /*bip32derivs=*/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 }