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