psbtoperationsdialog.cpp raw
1 // Copyright (c) 2011-2022 The Limenka 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/limenkaunits.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(SIGHASH_ALL, /*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(SIGHASH_DEFAULT, /*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, 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 const LimenkaUnits::Unit unit = m_client_model->getOptionsModel()->getDisplayUnit();
146 QString selected_filter;
147 QString filename_suggestion = "";
148 bool first = true;
149 for (const CTxOut& out : m_transaction_data.tx->vout) {
150 if (!first) {
151 filename_suggestion.append("-");
152 }
153 CTxDestination address;
154 ExtractDestination(out.scriptPubKey, address);
155 QString amount = LimenkaUnits::format(unit, out.nValue, /*plussign=*/false, LimenkaUnits::SeparatorStyle::NEVER);
156 if (unit != LimenkaUnits::Unit::BTC) amount += LimenkaUnits::shortName(unit); // NOTE: no space
157 QString address_str = QString::fromStdString(EncodeDestination(address));
158 filename_suggestion.append(address_str + "-" + amount);
159 first = false;
160 }
161 filename_suggestion.append(".psbt");
162 QString filename = GUIUtil::getSaveFileName(this,
163 tr("Save Transaction Data"), filename_suggestion,
164 //: Expanded name of the binary PSBT file format. See: BIP 174.
165 tr("Partially Signed Transaction (Binary)") + QLatin1String(" (*.psbt)"), &selected_filter);
166 if (filename.isEmpty()) {
167 return;
168 }
169 std::ofstream out{filename.toLocal8Bit().data(), std::ofstream::out | std::ofstream::binary};
170 out << ssTx.str();
171 out.close();
172 showStatus(tr("PSBT saved to disk."), StatusLevel::INFO);
173 }
174
175 void PSBTOperationsDialog::updateTransactionDisplay() {
176 m_ui->transactionDescription->setText(renderTransaction(m_transaction_data));
177 showTransactionStatus(m_transaction_data);
178 }
179
180 QString PSBTOperationsDialog::renderTransaction(const PartiallySignedTransaction &psbtx)
181 {
182 const QFont font_for_money_BTC = m_client_model->getOptionsModel()->getFontForMoney(LimenkaUnit::BTC);
183 QString tx_description;
184 QLatin1String bullet_point(" * ");
185 CAmount totalAmount = 0;
186 for (const CTxOut& out : psbtx.tx->vout) {
187 CTxDestination address;
188 ExtractDestination(out.scriptPubKey, address);
189 totalAmount += out.nValue;
190 tx_description.append(bullet_point).append(tr("Sends %1 to %2")
191 .arg(LimenkaUnits::formatHtmlWithUnit(font_for_money_BTC, LimenkaUnit::BTC, out.nValue))
192 .arg(QString::fromStdString(EncodeDestination(address))));
193 // Check if the address is one of ours
194 if (m_wallet_model != nullptr && m_wallet_model->wallet().txoutIsMine(out)) tx_description.append(" (" + tr("own address") + ")");
195 tx_description.append("<br>");
196 }
197
198 PSBTAnalysis analysis = AnalyzePSBT(psbtx);
199 tx_description.append(bullet_point);
200 if (!*analysis.fee) {
201 // This happens if the transaction is missing input UTXO information.
202 tx_description.append(tr("Unable to calculate transaction fee or total transaction amount."));
203 } else {
204 tx_description.append(tr("Pays transaction fee: "));
205 tx_description.append(LimenkaUnits::formatHtmlWithUnit(font_for_money_BTC, LimenkaUnit::BTC, *analysis.fee));
206
207 // add total amount in all subdivision units
208 tx_description.append("<hr />");
209 QStringList alternativeUnits;
210 for (const LimenkaUnits::Unit u : LimenkaUnits::availableUnits())
211 {
212 if(u != m_client_model->getOptionsModel()->getDisplayUnit()) {
213 const QFont font_for_money_u = m_client_model->getOptionsModel()->getFontForMoney(u);
214 alternativeUnits.append(LimenkaUnits::formatHtmlWithUnit(font_for_money_u, u, totalAmount));
215 }
216 }
217 const LimenkaUnit display_unit = m_client_model->getOptionsModel()->getDisplayUnit();
218 const QFont font_for_money = m_client_model->getOptionsModel()->getFontForMoney(display_unit);
219 tx_description.append(QString("<b>%1</b>: <b>%2</b>").arg(tr("Total Amount"))
220 .arg(LimenkaUnits::formatHtmlWithUnit(font_for_money, display_unit, totalAmount)));
221 tx_description.append(QString("<br /><span style='font-size:10pt; font-weight:normal;'>(=%1)</span>")
222 .arg(alternativeUnits.join(" " + tr("or") + " ")));
223 }
224
225 size_t num_unsigned = CountPSBTUnsignedInputs(psbtx);
226 if (num_unsigned > 0) {
227 tx_description.append("<br><br>");
228 tx_description.append(tr("Transaction has %n unsigned input(s).", "", num_unsigned));
229 }
230
231 return tx_description;
232 }
233
234 void PSBTOperationsDialog::showStatus(const QString &msg, StatusLevel level) {
235 m_ui->statusBar->setText(msg);
236 switch (level) {
237 case StatusLevel::INFO: {
238 m_ui->statusBar->setStyleSheet("QLabel { background-color : lightgreen }");
239 break;
240 }
241 case StatusLevel::WARN: {
242 m_ui->statusBar->setStyleSheet("QLabel { background-color : orange }");
243 break;
244 }
245 case StatusLevel::ERR: {
246 m_ui->statusBar->setStyleSheet("QLabel { background-color : red }");
247 break;
248 }
249 }
250 m_ui->statusBar->show();
251 }
252
253 size_t PSBTOperationsDialog::couldSignInputs(const PartiallySignedTransaction &psbtx) {
254 if (!m_wallet_model) {
255 return 0;
256 }
257
258 size_t n_signed;
259 bool complete;
260 const auto err{m_wallet_model->wallet().fillPSBT(SIGHASH_ALL, /*sign=*/false, /*bip32derivs=*/false, &n_signed, m_transaction_data, complete)};
261
262 if (err) {
263 return 0;
264 }
265 return n_signed;
266 }
267
268 void PSBTOperationsDialog::showTransactionStatus(const PartiallySignedTransaction &psbtx) {
269 PSBTAnalysis analysis = AnalyzePSBT(psbtx);
270 size_t n_could_sign = couldSignInputs(psbtx);
271
272 switch (analysis.next) {
273 case PSBTRole::UPDATER: {
274 showStatus(tr("Transaction is missing some information about inputs."), StatusLevel::WARN);
275 break;
276 }
277 case PSBTRole::SIGNER: {
278 QString need_sig_text = tr("Transaction still needs signature(s).");
279 StatusLevel level = StatusLevel::INFO;
280 if (!m_wallet_model) {
281 need_sig_text += " " + tr("(But no wallet is loaded.)");
282 level = StatusLevel::WARN;
283 } else if (m_wallet_model->wallet().privateKeysDisabled()) {
284 need_sig_text += " " + tr("(But this wallet cannot sign transactions.)");
285 level = StatusLevel::WARN;
286 } else if (n_could_sign < 1) {
287 need_sig_text += " " + tr("(But this wallet does not have the right keys.)"); // XXX wording
288 level = StatusLevel::WARN;
289 }
290 showStatus(need_sig_text, level);
291 break;
292 }
293 case PSBTRole::FINALIZER:
294 case PSBTRole::EXTRACTOR: {
295 showStatus(tr("Transaction is fully signed and ready for broadcast."), StatusLevel::INFO);
296 break;
297 }
298 default: {
299 showStatus(tr("Transaction status is unknown."), StatusLevel::ERR);
300 break;
301 }
302 }
303 }
304