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 }