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