Skip to content

Commit 4f802e5

Browse files
committed
Merge #17509: gui: save and load PSBT
764bfe4 [psbt] add file size limit (Sjors Provoost) 1cd8dc2 [gui] load PSBT (Sjors Provoost) f689530 [gui] save PSBT to file (Sjors Provoost) 1d05a9d Move DEFAULT_MAX_RAW_TX_FEE_RATE to node/transaction.h (Sjors Provoost) 86e22d2 [util] GetFileSize (Sjors Provoost) 6ab3aad [gui] send dialog: split on_sendButton_clicked (Sjors Provoost) Pull request description: This adds: * a dialog after Create Unsigned, which lets you save a PSBT file in binary format, e.g. to an SD card * a "Load PSBT" menu entry lets you pick a PSBT file. We broadcast the transaction if complete ## Save flow <img width="482" alt="Schermafbeelding 2020-01-04 om 20 39 34" src="https://user-images.githubusercontent.com/10217/71765684-ba60d580-2f32-11ea-8dea-0c4398eb6e15.png"> <img width="287" alt="Schermafbeelding 2020-01-04 om 20 40 35" src="https://user-images.githubusercontent.com/10217/71765677-a0bf8e00-2f32-11ea-8172-12dfd34a89f3.png"> <img width="594" alt="Schermafbeelding 2020-01-04 om 20 41 12" src="https://user-images.githubusercontent.com/10217/71765681-aa48f600-2f32-11ea-8e2c-c4f6bf9f5309.png"> <img width="632" alt="Schermafbeelding 2020-01-04 om 20 41 28" src="https://user-images.githubusercontent.com/10217/71765691-d19fc300-2f32-11ea-97ff-70f5dd59987a.png"> By default the file name contains the destination address(es) and amount(s). We only use the binary format for files, in order to avoid compatibility hell. If we do want to add base64 file format support, we should use a different extension for that (`.psbt64`?). ## Load flow Select a file: <img width="649" alt="Schermafbeelding 2020-01-04 om 21 08 57" src="https://user-images.githubusercontent.com/10217/71766089-2ba28780-2f37-11ea-875d-074794b5707d.png"> Offer to send if complete: <img width="308" alt="Schermafbeelding 2020-01-04 om 21 09 06" src="https://user-images.githubusercontent.com/10217/71766088-2a715a80-2f37-11ea-807d-394c8b840c59.png"> Tell user if signatures are missing, offer to copy to clipboard: <img width="308" alt="Schermafbeelding 2020-01-04 om 21 15 57" src="https://user-images.githubusercontent.com/10217/71766115-702e2300-2f37-11ea-9f62-a6ede499c0fa.png"> Incomplete for another reason: <img width="309" alt="Schermafbeelding 2020-01-04 om 21 07 51" src="https://user-images.githubusercontent.com/10217/71766090-2c3b1e00-2f37-11ea-8a22-6188377b67a1.png"> ACKs for top commit: instagibbs: re-ACK bitcoin/bitcoin@764bfe4 achow101: ACK 764bfe4 jb55: Tested ACK 764bfe4 jonatack: ACK 764bfe4 promag: Code review ACK 764bfe4. Tree-SHA512: d284ed6895f3a271fb8ff879aac388ad217ddc13f72074725608e1c3d6d90650f6dc9e9e254479544dd71fc111516b02c8ff92158153208dc40fb2726b37d063
2 parents 47b94a3 + 764bfe4 commit 4f802e5

13 files changed

+221
-52
lines changed

src/node/transaction.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,19 @@
66
#define BITCOIN_NODE_TRANSACTION_H
77

88
#include <attributes.h>
9+
#include <policy/feerate.h>
910
#include <primitives/transaction.h>
1011
#include <util/error.h>
1112

1213
struct NodeContext;
1314

15+
/** Maximum fee rate for sendrawtransaction and testmempoolaccept RPC calls.
16+
* Also used by the GUI when broadcasting a completed PSBT.
17+
* By default, a transaction with a fee rate higher than this will be rejected
18+
* by these RPCs and the GUI. This can be overridden with the maxfeerate argument.
19+
*/
20+
static const CFeeRate DEFAULT_MAX_RAW_TX_FEE_RATE{COIN / 10};
21+
1422
/**
1523
* Submit a transaction to the mempool and (optionally) relay it to all P2P peers.
1624
*

src/psbt.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ static constexpr uint8_t PSBT_OUT_BIP32_DERIVATION = 0x02;
4040
// as a 0 length key which indicates that this is the separator. The separator has no value.
4141
static constexpr uint8_t PSBT_SEPARATOR = 0x00;
4242

43+
// BIP 174 does not specify a maximum file size, but we set a limit anyway
44+
// to prevent reading a stream indefinately and running out of memory.
45+
const std::streamsize MAX_FILE_SIZE_PSBT = 100000000; // 100 MiB
46+
4347
/** A structure for PSBTs which contain per-input information */
4448
struct PSBTInput
4549
{

src/qt/bitcoingui.cpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,8 @@ void BitcoinGUI::createActions()
317317
signMessageAction->setStatusTip(tr("Sign messages with your Bitcoin addresses to prove you own them"));
318318
verifyMessageAction = new QAction(tr("&Verify message..."), this);
319319
verifyMessageAction->setStatusTip(tr("Verify messages to ensure they were signed with specified Bitcoin addresses"));
320+
m_load_psbt_action = new QAction(tr("Load PSBT..."), this);
321+
m_load_psbt_action->setStatusTip(tr("Load Partially Signed Bitcoin Transaction"));
320322

321323
openRPCConsoleAction = new QAction(tr("Node window"), this);
322324
openRPCConsoleAction->setStatusTip(tr("Open node debugging and diagnostic console"));
@@ -366,6 +368,7 @@ void BitcoinGUI::createActions()
366368
connect(changePassphraseAction, &QAction::triggered, walletFrame, &WalletFrame::changePassphrase);
367369
connect(signMessageAction, &QAction::triggered, [this]{ showNormalIfMinimized(); });
368370
connect(signMessageAction, &QAction::triggered, [this]{ gotoSignMessageTab(); });
371+
connect(m_load_psbt_action, &QAction::triggered, [this]{ gotoLoadPSBT(); });
369372
connect(verifyMessageAction, &QAction::triggered, [this]{ showNormalIfMinimized(); });
370373
connect(verifyMessageAction, &QAction::triggered, [this]{ gotoVerifyMessageTab(); });
371374
connect(usedSendingAddressesAction, &QAction::triggered, walletFrame, &WalletFrame::usedSendingAddresses);
@@ -438,6 +441,7 @@ void BitcoinGUI::createMenuBar()
438441
file->addAction(backupWalletAction);
439442
file->addAction(signMessageAction);
440443
file->addAction(verifyMessageAction);
444+
file->addAction(m_load_psbt_action);
441445
file->addSeparator();
442446
}
443447
file->addAction(quitAction);
@@ -854,6 +858,10 @@ void BitcoinGUI::gotoVerifyMessageTab(QString addr)
854858
{
855859
if (walletFrame) walletFrame->gotoVerifyMessageTab(addr);
856860
}
861+
void BitcoinGUI::gotoLoadPSBT()
862+
{
863+
if (walletFrame) walletFrame->gotoLoadPSBT();
864+
}
857865
#endif // ENABLE_WALLET
858866

859867
void BitcoinGUI::updateNetworkState()

src/qt/bitcoingui.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ class BitcoinGUI : public QMainWindow
135135
QAction* usedReceivingAddressesAction = nullptr;
136136
QAction* signMessageAction = nullptr;
137137
QAction* verifyMessageAction = nullptr;
138+
QAction* m_load_psbt_action = nullptr;
138139
QAction* aboutAction = nullptr;
139140
QAction* receiveCoinsAction = nullptr;
140141
QAction* receiveCoinsMenuAction = nullptr;
@@ -270,6 +271,8 @@ public Q_SLOTS:
270271
void gotoSignMessageTab(QString addr = "");
271272
/** Show Sign/Verify Message dialog and switch to verify message tab */
272273
void gotoVerifyMessageTab(QString addr = "");
274+
/** Show load Partially Signed Bitcoin Transaction dialog */
275+
void gotoLoadPSBT();
273276

274277
/** Show open dialog */
275278
void openClicked();

src/qt/sendcoinsdialog.cpp

Lines changed: 90 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -219,11 +219,8 @@ SendCoinsDialog::~SendCoinsDialog()
219219
delete ui;
220220
}
221221

222-
void SendCoinsDialog::on_sendButton_clicked()
222+
bool SendCoinsDialog::PrepareSendText(QString& question_string, QString& informative_text, QString& detailed_text)
223223
{
224-
if(!model || !model->getOptionsModel())
225-
return;
226-
227224
QList<SendCoinsRecipient> recipients;
228225
bool valid = true;
229226

@@ -246,7 +243,7 @@ void SendCoinsDialog::on_sendButton_clicked()
246243

247244
if(!valid || recipients.isEmpty())
248245
{
249-
return;
246+
return false;
250247
}
251248

252249
fNewRecipientAllowed = false;
@@ -255,11 +252,11 @@ void SendCoinsDialog::on_sendButton_clicked()
255252
{
256253
// Unlock wallet was cancelled
257254
fNewRecipientAllowed = true;
258-
return;
255+
return false;
259256
}
260257

261258
// prepare transaction for getting txFee earlier
262-
WalletModelTransaction currentTransaction(recipients);
259+
m_current_transaction = MakeUnique<WalletModelTransaction>(recipients);
263260
WalletModel::SendCoinsReturn prepareStatus;
264261

265262
// Always use a CCoinControl instance, use the CoinControlDialog instance if CoinControl has been enabled
@@ -269,22 +266,20 @@ void SendCoinsDialog::on_sendButton_clicked()
269266

270267
updateCoinControlState(ctrl);
271268

272-
prepareStatus = model->prepareTransaction(currentTransaction, ctrl);
269+
prepareStatus = model->prepareTransaction(*m_current_transaction, ctrl);
273270

274271
// process prepareStatus and on error generate message shown to user
275272
processSendCoinsReturn(prepareStatus,
276-
BitcoinUnits::formatWithUnit(model->getOptionsModel()->getDisplayUnit(), currentTransaction.getTransactionFee()));
273+
BitcoinUnits::formatWithUnit(model->getOptionsModel()->getDisplayUnit(), m_current_transaction->getTransactionFee()));
277274

278275
if(prepareStatus.status != WalletModel::OK) {
279276
fNewRecipientAllowed = true;
280-
return;
277+
return false;
281278
}
282279

283-
CAmount txFee = currentTransaction.getTransactionFee();
284-
285-
// Format confirmation message
280+
CAmount txFee = m_current_transaction->getTransactionFee();
286281
QStringList formatted;
287-
for (const SendCoinsRecipient &rcp : currentTransaction.getRecipients())
282+
for (const SendCoinsRecipient &rcp : m_current_transaction->getRecipients())
288283
{
289284
// generate amount string with wallet name in case of multiwallet
290285
QString amount = BitcoinUnits::formatWithUnit(model->getOptionsModel()->getDisplayUnit(), rcp.amount);
@@ -311,72 +306,82 @@ void SendCoinsDialog::on_sendButton_clicked()
311306
formatted.append(recipientElement);
312307
}
313308

314-
QString questionString;
315309
if (model->wallet().privateKeysDisabled()) {
316-
questionString.append(tr("Do you want to draft this transaction?"));
310+
question_string.append(tr("Do you want to draft this transaction?"));
317311
} else {
318-
questionString.append(tr("Are you sure you want to send?"));
312+
question_string.append(tr("Are you sure you want to send?"));
319313
}
320314

321-
questionString.append("<br /><span style='font-size:10pt;'>");
315+
question_string.append("<br /><span style='font-size:10pt;'>");
322316
if (model->wallet().privateKeysDisabled()) {
323-
questionString.append(tr("Please, review your transaction proposal. This will produce a Partially Signed Bitcoin Transaction (PSBT) which you can copy and then sign with e.g. an offline %1 wallet, or a PSBT-compatible hardware wallet.").arg(PACKAGE_NAME));
317+
question_string.append(tr("Please, review your transaction proposal. This will produce a Partially Signed Bitcoin Transaction (PSBT) which you can save or copy and then sign with e.g. an offline %1 wallet, or a PSBT-compatible hardware wallet.").arg(PACKAGE_NAME));
324318
} else {
325-
questionString.append(tr("Please, review your transaction."));
319+
question_string.append(tr("Please, review your transaction."));
326320
}
327-
questionString.append("</span>%1");
321+
question_string.append("</span>%1");
328322

329323
if(txFee > 0)
330324
{
331325
// append fee string if a fee is required
332-
questionString.append("<hr /><b>");
333-
questionString.append(tr("Transaction fee"));
334-
questionString.append("</b>");
326+
question_string.append("<hr /><b>");
327+
question_string.append(tr("Transaction fee"));
328+
question_string.append("</b>");
335329

336330
// append transaction size
337-
questionString.append(" (" + QString::number((double)currentTransaction.getTransactionSize() / 1000) + " kB): ");
331+
question_string.append(" (" + QString::number((double)m_current_transaction->getTransactionSize() / 1000) + " kB): ");
338332

339333
// append transaction fee value
340-
questionString.append("<span style='color:#aa0000; font-weight:bold;'>");
341-
questionString.append(BitcoinUnits::formatHtmlWithUnit(model->getOptionsModel()->getDisplayUnit(), txFee));
342-
questionString.append("</span><br />");
334+
question_string.append("<span style='color:#aa0000; font-weight:bold;'>");
335+
question_string.append(BitcoinUnits::formatHtmlWithUnit(model->getOptionsModel()->getDisplayUnit(), txFee));
336+
question_string.append("</span><br />");
343337

344338
// append RBF message according to transaction's signalling
345-
questionString.append("<span style='font-size:10pt; font-weight:normal;'>");
339+
question_string.append("<span style='font-size:10pt; font-weight:normal;'>");
346340
if (ui->optInRBF->isChecked()) {
347-
questionString.append(tr("You can increase the fee later (signals Replace-By-Fee, BIP-125)."));
341+
question_string.append(tr("You can increase the fee later (signals Replace-By-Fee, BIP-125)."));
348342
} else {
349-
questionString.append(tr("Not signalling Replace-By-Fee, BIP-125."));
343+
question_string.append(tr("Not signalling Replace-By-Fee, BIP-125."));
350344
}
351-
questionString.append("</span>");
345+
question_string.append("</span>");
352346
}
353347

354348
// add total amount in all subdivision units
355-
questionString.append("<hr />");
356-
CAmount totalAmount = currentTransaction.getTotalTransactionAmount() + txFee;
349+
question_string.append("<hr />");
350+
CAmount totalAmount = m_current_transaction->getTotalTransactionAmount() + txFee;
357351
QStringList alternativeUnits;
358352
for (const BitcoinUnits::Unit u : BitcoinUnits::availableUnits())
359353
{
360354
if(u != model->getOptionsModel()->getDisplayUnit())
361355
alternativeUnits.append(BitcoinUnits::formatHtmlWithUnit(u, totalAmount));
362356
}
363-
questionString.append(QString("<b>%1</b>: <b>%2</b>").arg(tr("Total Amount"))
357+
question_string.append(QString("<b>%1</b>: <b>%2</b>").arg(tr("Total Amount"))
364358
.arg(BitcoinUnits::formatHtmlWithUnit(model->getOptionsModel()->getDisplayUnit(), totalAmount)));
365-
questionString.append(QString("<br /><span style='font-size:10pt; font-weight:normal;'>(=%1)</span>")
359+
question_string.append(QString("<br /><span style='font-size:10pt; font-weight:normal;'>(=%1)</span>")
366360
.arg(alternativeUnits.join(" " + tr("or") + " ")));
367361

368-
QString informative_text;
369-
QString detailed_text;
370362
if (formatted.size() > 1) {
371-
questionString = questionString.arg("");
363+
question_string = question_string.arg("");
372364
informative_text = tr("To review recipient list click \"Show Details...\"");
373365
detailed_text = formatted.join("\n\n");
374366
} else {
375-
questionString = questionString.arg("<br /><br />" + formatted.at(0));
367+
question_string = question_string.arg("<br /><br />" + formatted.at(0));
376368
}
369+
370+
return true;
371+
}
372+
373+
void SendCoinsDialog::on_sendButton_clicked()
374+
{
375+
if(!model || !model->getOptionsModel())
376+
return;
377+
378+
QString question_string, informative_text, detailed_text;
379+
if (!PrepareSendText(question_string, informative_text, detailed_text)) return;
380+
assert(m_current_transaction);
381+
377382
const QString confirmation = model->wallet().privateKeysDisabled() ? tr("Confirm transaction proposal") : tr("Confirm send coins");
378-
const QString confirmButtonText = model->wallet().privateKeysDisabled() ? tr("Copy PSBT to clipboard") : tr("Send");
379-
SendConfirmationDialog confirmationDialog(confirmation, questionString, informative_text, detailed_text, SEND_CONFIRM_DELAY, confirmButtonText, this);
383+
const QString confirmButtonText = model->wallet().privateKeysDisabled() ? tr("Create Unsigned") : tr("Send");
384+
SendConfirmationDialog confirmationDialog(confirmation, question_string, informative_text, detailed_text, SEND_CONFIRM_DELAY, confirmButtonText, this);
380385
confirmationDialog.exec();
381386
QMessageBox::StandardButton retval = static_cast<QMessageBox::StandardButton>(confirmationDialog.result());
382387

@@ -388,7 +393,7 @@ void SendCoinsDialog::on_sendButton_clicked()
388393

389394
bool send_failure = false;
390395
if (model->wallet().privateKeysDisabled()) {
391-
CMutableTransaction mtx = CMutableTransaction{*(currentTransaction.getWtx())};
396+
CMutableTransaction mtx = CMutableTransaction{*(m_current_transaction->getWtx())};
392397
PartiallySignedTransaction psbtx(mtx);
393398
bool complete = false;
394399
const TransactionError err = model->wallet().fillPSBT(SIGHASH_ALL, false /* sign */, true /* bip32derivs */, psbtx, complete);
@@ -398,15 +403,51 @@ void SendCoinsDialog::on_sendButton_clicked()
398403
CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION);
399404
ssTx << psbtx;
400405
GUIUtil::setClipboard(EncodeBase64(ssTx.str()).c_str());
401-
Q_EMIT message(tr("PSBT copied"), "Copied to clipboard", CClientUIInterface::MSG_INFORMATION);
406+
QMessageBox msgBox;
407+
msgBox.setText("Unsigned Transaction");
408+
msgBox.setInformativeText("The PSBT has been copied to the clipboard. You can also save it.");
409+
msgBox.setStandardButtons(QMessageBox::Save | QMessageBox::Discard);
410+
msgBox.setDefaultButton(QMessageBox::Discard);
411+
switch (msgBox.exec()) {
412+
case QMessageBox::Save: {
413+
QString selectedFilter;
414+
QString fileNameSuggestion = "";
415+
bool first = true;
416+
for (const SendCoinsRecipient &rcp : m_current_transaction->getRecipients()) {
417+
if (!first) {
418+
fileNameSuggestion.append(" - ");
419+
}
420+
QString labelOrAddress = rcp.label.isEmpty() ? rcp.address : rcp.label;
421+
QString amount = BitcoinUnits::formatWithUnit(model->getOptionsModel()->getDisplayUnit(), rcp.amount);
422+
fileNameSuggestion.append(labelOrAddress + "-" + amount);
423+
first = false;
424+
}
425+
fileNameSuggestion.append(".psbt");
426+
QString filename = GUIUtil::getSaveFileName(this,
427+
tr("Save Transaction Data"), fileNameSuggestion,
428+
tr("Partially Signed Transaction (Binary) (*.psbt)"), &selectedFilter);
429+
if (filename.isEmpty()) {
430+
return;
431+
}
432+
std::ofstream out(filename.toLocal8Bit().data());
433+
out << ssTx.str();
434+
out.close();
435+
Q_EMIT message(tr("PSBT saved"), "PSBT saved to disk", CClientUIInterface::MSG_INFORMATION);
436+
break;
437+
}
438+
case QMessageBox::Discard:
439+
break;
440+
default:
441+
assert(false);
442+
}
402443
} else {
403444
// now send the prepared transaction
404-
WalletModel::SendCoinsReturn sendStatus = model->sendCoins(currentTransaction);
445+
WalletModel::SendCoinsReturn sendStatus = model->sendCoins(*m_current_transaction);
405446
// process sendStatus and on error generate message shown to user
406447
processSendCoinsReturn(sendStatus);
407448

408449
if (sendStatus.status == WalletModel::OK) {
409-
Q_EMIT coinsSent(currentTransaction.getWtx()->GetHash());
450+
Q_EMIT coinsSent(m_current_transaction->getWtx()->GetHash());
410451
} else {
411452
send_failure = true;
412453
}
@@ -417,10 +458,13 @@ void SendCoinsDialog::on_sendButton_clicked()
417458
coinControlUpdateLabels();
418459
}
419460
fNewRecipientAllowed = true;
461+
m_current_transaction.reset();
420462
}
421463

422464
void SendCoinsDialog::clear()
423465
{
466+
m_current_transaction.reset();
467+
424468
// Clear coin control settings
425469
CoinControlDialog::coinControl()->UnSelectAll();
426470
ui->checkBoxCoinControlChange->setChecked(false);

src/qt/sendcoinsdialog.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ public Q_SLOTS:
6060
Ui::SendCoinsDialog *ui;
6161
ClientModel *clientModel;
6262
WalletModel *model;
63+
std::unique_ptr<WalletModelTransaction> m_current_transaction;
6364
bool fNewRecipientAllowed;
6465
bool fFeeMinimized;
6566
const PlatformStyle *platformStyle;
@@ -69,6 +70,8 @@ public Q_SLOTS:
6970
// Additional parameter msgArg can be used via .arg(msgArg).
7071
void processSendCoinsReturn(const WalletModel::SendCoinsReturn &sendCoinsReturn, const QString &msgArg = QString());
7172
void minimizeFeeSection(bool fMinimize);
73+
// Format confirmation message
74+
bool PrepareSendText(QString& question_string, QString& informative_text, QString& detailed_text);
7275
void updateFeeMinimizedLabel();
7376
// Update the passed in CCoinControl with state from the GUI
7477
void updateCoinControlState(CCoinControl& ctrl);

src/qt/walletframe.cpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,14 @@ void WalletFrame::gotoVerifyMessageTab(QString addr)
163163
walletView->gotoVerifyMessageTab(addr);
164164
}
165165

166+
void WalletFrame::gotoLoadPSBT()
167+
{
168+
WalletView *walletView = currentWalletView();
169+
if (walletView) {
170+
walletView->gotoLoadPSBT();
171+
}
172+
}
173+
166174
void WalletFrame::encryptWallet(bool status)
167175
{
168176
WalletView *walletView = currentWalletView();

src/qt/walletframe.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,9 @@ public Q_SLOTS:
7878
/** Show Sign/Verify Message dialog and switch to verify message tab */
7979
void gotoVerifyMessageTab(QString addr = "");
8080

81+
/** Load Partially Signed Bitcoin Transaction */
82+
void gotoLoadPSBT();
83+
8184
/** Encrypt the wallet */
8285
void encryptWallet(bool status);
8386
/** Backup the wallet */

0 commit comments

Comments
 (0)