Merge #17509: gui: save and load PSBT

764bfe4cba [psbt] add file size limit (Sjors Provoost)
1cd8dc2556 [gui] load PSBT (Sjors Provoost)
f6895301f7 [gui] save PSBT to file (Sjors Provoost)
1d05a9d80b Move DEFAULT_MAX_RAW_TX_FEE_RATE to node/transaction.h (Sjors Provoost)
86e22d23bb [util] GetFileSize (Sjors Provoost)
6ab3aad9a5 [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  https://github.com/bitcoin/bitcoin/pull/17509/commits/764bfe4cba35c24f7627cc425d9e7eba56e98964
  achow101:
    ACK 764bfe4cba
  jb55:
    Tested ACK 764bfe4cba
  jonatack:
    ACK 764bfe4c
  promag:
    Code review ACK 764bfe4cba.

Tree-SHA512: d284ed6895f3a271fb8ff879aac388ad217ddc13f72074725608e1c3d6d90650f6dc9e9e254479544dd71fc111516b02c8ff92158153208dc40fb2726b37d063
This commit is contained in:
Samuel Dobson committed 2020-04-23 13:16:23 +12:00
commit 4f802e59a0
13 files changed
+221 -52

No files matched your search

+8
View File
@@ -317,6 +317,8 @@ void BitcoinGUI::createActions()
signMessageAction->setStatusTip(tr("Sign messages with your Bitcoin addresses to prove you own them"));
verifyMessageAction = new QAction(tr("&Verify message..."), this);
verifyMessageAction->setStatusTip(tr("Verify messages to ensure they were signed with specified Bitcoin addresses"));
m_load_psbt_action = new QAction(tr("Load PSBT..."), this);
m_load_psbt_action->setStatusTip(tr("Load Partially Signed Bitcoin Transaction"));
openRPCConsoleAction = new QAction(tr("Node window"), this);
openRPCConsoleAction->setStatusTip(tr("Open node debugging and diagnostic console"));
@@ -366,6 +368,7 @@ void BitcoinGUI::createActions()
connect(changePassphraseAction, &QAction::triggered, walletFrame, &WalletFrame::changePassphrase);
connect(signMessageAction, &QAction::triggered, [this]{ showNormalIfMinimized(); });
connect(signMessageAction, &QAction::triggered, [this]{ gotoSignMessageTab(); });
connect(m_load_psbt_action, &QAction::triggered, [this]{ gotoLoadPSBT(); });
connect(verifyMessageAction, &QAction::triggered, [this]{ showNormalIfMinimized(); });
connect(verifyMessageAction, &QAction::triggered, [this]{ gotoVerifyMessageTab(); });
connect(usedSendingAddressesAction, &QAction::triggered, walletFrame, &WalletFrame::usedSendingAddresses);
@@ -438,6 +441,7 @@ void BitcoinGUI::createMenuBar()
file->addAction(backupWalletAction);
file->addAction(signMessageAction);
file->addAction(verifyMessageAction);
file->addAction(m_load_psbt_action);
file->addSeparator();
}
file->addAction(quitAction);
@@ -854,6 +858,10 @@ void BitcoinGUI::gotoVerifyMessageTab(QString addr)
{
if (walletFrame) walletFrame->gotoVerifyMessageTab(addr);
}
void BitcoinGUI::gotoLoadPSBT()
{
if (walletFrame) walletFrame->gotoLoadPSBT();
}
#endif // ENABLE_WALLET
void BitcoinGUI::updateNetworkState()
+3
View File
@@ -135,6 +135,7 @@ private:
QAction* usedReceivingAddressesAction = nullptr;
QAction* signMessageAction = nullptr;
QAction* verifyMessageAction = nullptr;
QAction* m_load_psbt_action = nullptr;
QAction* aboutAction = nullptr;
QAction* receiveCoinsAction = nullptr;
QAction* receiveCoinsMenuAction = nullptr;
@@ -270,6 +271,8 @@ public Q_SLOTS:
void gotoSignMessageTab(QString addr = "");
/** Show Sign/Verify Message dialog and switch to verify message tab */
void gotoVerifyMessageTab(QString addr = "");
/** Show load Partially Signed Bitcoin Transaction dialog */
void gotoLoadPSBT();
/** Show open dialog */
void openClicked();
+90 -46
View File
@@ -219,11 +219,8 @@ SendCoinsDialog::~SendCoinsDialog()
delete ui;
}
void SendCoinsDialog::on_sendButton_clicked()
bool SendCoinsDialog::PrepareSendText(QString& question_string, QString& informative_text, QString& detailed_text)
{
if(!model || !model->getOptionsModel())
return;
QList<SendCoinsRecipient> recipients;
bool valid = true;
@@ -246,7 +243,7 @@ void SendCoinsDialog::on_sendButton_clicked()
if(!valid || recipients.isEmpty())
{
return;
return false;
}
fNewRecipientAllowed = false;
@@ -255,11 +252,11 @@ void SendCoinsDialog::on_sendButton_clicked()
{
// Unlock wallet was cancelled
fNewRecipientAllowed = true;
return;
return false;
}
// prepare transaction for getting txFee earlier
WalletModelTransaction currentTransaction(recipients);
m_current_transaction = MakeUnique<WalletModelTransaction>(recipients);
WalletModel::SendCoinsReturn prepareStatus;
// Always use a CCoinControl instance, use the CoinControlDialog instance if CoinControl has been enabled
@@ -269,22 +266,20 @@ void SendCoinsDialog::on_sendButton_clicked()
updateCoinControlState(ctrl);
prepareStatus = model->prepareTransaction(currentTransaction, ctrl);
prepareStatus = model->prepareTransaction(*m_current_transaction, ctrl);
// process prepareStatus and on error generate message shown to user
processSendCoinsReturn(prepareStatus,
BitcoinUnits::formatWithUnit(model->getOptionsModel()->getDisplayUnit(), currentTransaction.getTransactionFee()));
BitcoinUnits::formatWithUnit(model->getOptionsModel()->getDisplayUnit(), m_current_transaction->getTransactionFee()));
if(prepareStatus.status != WalletModel::OK) {
fNewRecipientAllowed = true;
return;
return false;
}
CAmount txFee = currentTransaction.getTransactionFee();
// Format confirmation message
CAmount txFee = m_current_transaction->getTransactionFee();
QStringList formatted;
for (const SendCoinsRecipient &rcp : currentTransaction.getRecipients())
for (const SendCoinsRecipient &rcp : m_current_transaction->getRecipients())
{
// generate amount string with wallet name in case of multiwallet
QString amount = BitcoinUnits::formatWithUnit(model->getOptionsModel()->getDisplayUnit(), rcp.amount);
@@ -311,72 +306,82 @@ void SendCoinsDialog::on_sendButton_clicked()
formatted.append(recipientElement);
}
QString questionString;
if (model->wallet().privateKeysDisabled()) {
questionString.append(tr("Do you want to draft this transaction?"));
question_string.append(tr("Do you want to draft this transaction?"));
} else {
questionString.append(tr("Are you sure you want to send?"));
question_string.append(tr("Are you sure you want to send?"));
}
questionString.append("<br /><span style='font-size:10pt;'>");
question_string.append("<br /><span style='font-size:10pt;'>");
if (model->wallet().privateKeysDisabled()) {
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));
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));
} else {
questionString.append(tr("Please, review your transaction."));
question_string.append(tr("Please, review your transaction."));
}
questionString.append("</span>%1");
question_string.append("</span>%1");
if(txFee > 0)
{
// append fee string if a fee is required
questionString.append("<hr /><b>");
questionString.append(tr("Transaction fee"));
questionString.append("</b>");
question_string.append("<hr /><b>");
question_string.append(tr("Transaction fee"));
question_string.append("</b>");
// append transaction size
questionString.append(" (" + QString::number((double)currentTransaction.getTransactionSize() / 1000) + " kB): ");
question_string.append(" (" + QString::number((double)m_current_transaction->getTransactionSize() / 1000) + " kB): ");
// append transaction fee value
questionString.append("<span style='color:#aa0000; font-weight:bold;'>");
questionString.append(BitcoinUnits::formatHtmlWithUnit(model->getOptionsModel()->getDisplayUnit(), txFee));
questionString.append("</span><br />");
question_string.append("<span style='color:#aa0000; font-weight:bold;'>");
question_string.append(BitcoinUnits::formatHtmlWithUnit(model->getOptionsModel()->getDisplayUnit(), txFee));
question_string.append("</span><br />");
// append RBF message according to transaction's signalling
questionString.append("<span style='font-size:10pt; font-weight:normal;'>");
question_string.append("<span style='font-size:10pt; font-weight:normal;'>");
if (ui->optInRBF->isChecked()) {
questionString.append(tr("You can increase the fee later (signals Replace-By-Fee, BIP-125)."));
question_string.append(tr("You can increase the fee later (signals Replace-By-Fee, BIP-125)."));
} else {
questionString.append(tr("Not signalling Replace-By-Fee, BIP-125."));
question_string.append(tr("Not signalling Replace-By-Fee, BIP-125."));
}
questionString.append("</span>");
question_string.append("</span>");
}
// add total amount in all subdivision units
questionString.append("<hr />");
CAmount totalAmount = currentTransaction.getTotalTransactionAmount() + txFee;
question_string.append("<hr />");
CAmount totalAmount = m_current_transaction->getTotalTransactionAmount() + txFee;
QStringList alternativeUnits;
for (const BitcoinUnits::Unit u : BitcoinUnits::availableUnits())
{
if(u != model->getOptionsModel()->getDisplayUnit())
alternativeUnits.append(BitcoinUnits::formatHtmlWithUnit(u, totalAmount));
}
questionString.append(QString("<b>%1</b>: <b>%2</b>").arg(tr("Total Amount"))
question_string.append(QString("<b>%1</b>: <b>%2</b>").arg(tr("Total Amount"))
.arg(BitcoinUnits::formatHtmlWithUnit(model->getOptionsModel()->getDisplayUnit(), totalAmount)));
questionString.append(QString("<br /><span style='font-size:10pt; font-weight:normal;'>(=%1)</span>")
question_string.append(QString("<br /><span style='font-size:10pt; font-weight:normal;'>(=%1)</span>")
.arg(alternativeUnits.join(" " + tr("or") + " ")));
QString informative_text;
QString detailed_text;
if (formatted.size() > 1) {
questionString = questionString.arg("");
question_string = question_string.arg("");
informative_text = tr("To review recipient list click \"Show Details...\"");
detailed_text = formatted.join("\n\n");
} else {
questionString = questionString.arg("<br /><br />" + formatted.at(0));
question_string = question_string.arg("<br /><br />" + formatted.at(0));
}
return true;
}
void SendCoinsDialog::on_sendButton_clicked()
{
if(!model || !model->getOptionsModel())
return;
QString question_string, informative_text, detailed_text;
if (!PrepareSendText(question_string, informative_text, detailed_text)) return;
assert(m_current_transaction);
const QString confirmation = model->wallet().privateKeysDisabled() ? tr("Confirm transaction proposal") : tr("Confirm send coins");
const QString confirmButtonText = model->wallet().privateKeysDisabled() ? tr("Copy PSBT to clipboard") : tr("Send");
SendConfirmationDialog confirmationDialog(confirmation, questionString, informative_text, detailed_text, SEND_CONFIRM_DELAY, confirmButtonText, this);
const QString confirmButtonText = model->wallet().privateKeysDisabled() ? tr("Create Unsigned") : tr("Send");
SendConfirmationDialog confirmationDialog(confirmation, question_string, informative_text, detailed_text, SEND_CONFIRM_DELAY, confirmButtonText, this);
confirmationDialog.exec();
QMessageBox::StandardButton retval = static_cast<QMessageBox::StandardButton>(confirmationDialog.result());
@@ -388,7 +393,7 @@ void SendCoinsDialog::on_sendButton_clicked()
bool send_failure = false;
if (model->wallet().privateKeysDisabled()) {
CMutableTransaction mtx = CMutableTransaction{*(currentTransaction.getWtx())};
CMutableTransaction mtx = CMutableTransaction{*(m_current_transaction->getWtx())};
PartiallySignedTransaction psbtx(mtx);
bool complete = false;
const TransactionError err = model->wallet().fillPSBT(SIGHASH_ALL, false /* sign */, true /* bip32derivs */, psbtx, complete);
@@ -398,15 +403,51 @@ void SendCoinsDialog::on_sendButton_clicked()
CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION);
ssTx << psbtx;
GUIUtil::setClipboard(EncodeBase64(ssTx.str()).c_str());
Q_EMIT message(tr("PSBT copied"), "Copied to clipboard", CClientUIInterface::MSG_INFORMATION);
QMessageBox msgBox;
msgBox.setText("Unsigned Transaction");
msgBox.setInformativeText("The PSBT has been copied to the clipboard. You can also save it.");
msgBox.setStandardButtons(QMessageBox::Save | QMessageBox::Discard);
msgBox.setDefaultButton(QMessageBox::Discard);
switch (msgBox.exec()) {
case QMessageBox::Save: {
QString selectedFilter;
QString fileNameSuggestion = "";
bool first = true;
for (const SendCoinsRecipient &rcp : m_current_transaction->getRecipients()) {
if (!first) {
fileNameSuggestion.append(" - ");
}
QString labelOrAddress = rcp.label.isEmpty() ? rcp.address : rcp.label;
QString amount = BitcoinUnits::formatWithUnit(model->getOptionsModel()->getDisplayUnit(), rcp.amount);
fileNameSuggestion.append(labelOrAddress + "-" + amount);
first = false;
}
fileNameSuggestion.append(".psbt");
QString filename = GUIUtil::getSaveFileName(this,
tr("Save Transaction Data"), fileNameSuggestion,
tr("Partially Signed Transaction (Binary) (*.psbt)"), &selectedFilter);
if (filename.isEmpty()) {
return;
}
std::ofstream out(filename.toLocal8Bit().data());
out << ssTx.str();
out.close();
Q_EMIT message(tr("PSBT saved"), "PSBT saved to disk", CClientUIInterface::MSG_INFORMATION);
break;
}
case QMessageBox::Discard:
break;
default:
assert(false);
}
} else {
// now send the prepared transaction
WalletModel::SendCoinsReturn sendStatus = model->sendCoins(currentTransaction);
WalletModel::SendCoinsReturn sendStatus = model->sendCoins(*m_current_transaction);
// process sendStatus and on error generate message shown to user
processSendCoinsReturn(sendStatus);
if (sendStatus.status == WalletModel::OK) {
Q_EMIT coinsSent(currentTransaction.getWtx()->GetHash());
Q_EMIT coinsSent(m_current_transaction->getWtx()->GetHash());
} else {
send_failure = true;
}
@@ -417,10 +458,13 @@ void SendCoinsDialog::on_sendButton_clicked()
coinControlUpdateLabels();
}
fNewRecipientAllowed = true;
m_current_transaction.reset();
}
void SendCoinsDialog::clear()
{
m_current_transaction.reset();
// Clear coin control settings
CoinControlDialog::coinControl()->UnSelectAll();
ui->checkBoxCoinControlChange->setChecked(false);
+3
View File
@@ -60,6 +60,7 @@ private:
Ui::SendCoinsDialog *ui;
ClientModel *clientModel;
WalletModel *model;
std::unique_ptr<WalletModelTransaction> m_current_transaction;
bool fNewRecipientAllowed;
bool fFeeMinimized;
const PlatformStyle *platformStyle;
@@ -69,6 +70,8 @@ private:
// Additional parameter msgArg can be used via .arg(msgArg).
void processSendCoinsReturn(const WalletModel::SendCoinsReturn &sendCoinsReturn, const QString &msgArg = QString());
void minimizeFeeSection(bool fMinimize);
// Format confirmation message
bool PrepareSendText(QString& question_string, QString& informative_text, QString& detailed_text);
void updateFeeMinimizedLabel();
// Update the passed in CCoinControl with state from the GUI
void updateCoinControlState(CCoinControl& ctrl);
+8
View File
@@ -163,6 +163,14 @@ void WalletFrame::gotoVerifyMessageTab(QString addr)
walletView->gotoVerifyMessageTab(addr);
}
void WalletFrame::gotoLoadPSBT()
{
WalletView *walletView = currentWalletView();
if (walletView) {
walletView->gotoLoadPSBT();
}
}
void WalletFrame::encryptWallet(bool status)
{
WalletView *walletView = currentWalletView();
+3
View File
@@ -78,6 +78,9 @@ public Q_SLOTS:
/** Show Sign/Verify Message dialog and switch to verify message tab */
void gotoVerifyMessageTab(QString addr = "");
/** Load Partially Signed Bitcoin Transaction */
void gotoLoadPSBT();
/** Encrypt the wallet */
void encryptWallet(bool status);
/** Backup the wallet */
+78
View File
@@ -4,6 +4,9 @@
#include <qt/walletview.h>
#include <node/psbt.h>
#include <node/transaction.h>
#include <policy/policy.h>
#include <qt/addressbookpage.h>
#include <qt/askpassphrasedialog.h>
#include <qt/clientmodel.h>
@@ -20,6 +23,7 @@
#include <interfaces/node.h>
#include <ui_interface.h>
#include <util/strencodings.h>
#include <QAction>
#include <QActionGroup>
@@ -197,6 +201,80 @@ void WalletView::gotoVerifyMessageTab(QString addr)
signVerifyMessageDialog->setAddress_VM(addr);
}
void WalletView::gotoLoadPSBT()
{
QString filename = GUIUtil::getOpenFileName(this,
tr("Load Transaction Data"), QString(),
tr("Partially Signed Transaction (*.psbt)"), nullptr);
if (filename.isEmpty()) return;
if (GetFileSize(filename.toLocal8Bit().data(), MAX_FILE_SIZE_PSBT) == MAX_FILE_SIZE_PSBT) {
Q_EMIT message(tr("Error"), tr("PSBT file must be smaller than 100 MiB"), CClientUIInterface::MSG_ERROR);
return;
}
std::ifstream in(filename.toLocal8Bit().data(), std::ios::binary);
std::string data(std::istreambuf_iterator<char>{in}, {});
std::string error;
PartiallySignedTransaction psbtx;
if (!DecodeRawPSBT(psbtx, data, error)) {
Q_EMIT message(tr("Error"), tr("Unable to decode PSBT file") + "\n" + QString::fromStdString(error), CClientUIInterface::MSG_ERROR);
return;
}
CMutableTransaction mtx;
bool complete = false;
PSBTAnalysis analysis = AnalyzePSBT(psbtx);
QMessageBox msgBox;
msgBox.setText("PSBT");
switch (analysis.next) {
case PSBTRole::CREATOR:
case PSBTRole::UPDATER:
msgBox.setInformativeText("PSBT is incomplete. Copy to clipboard for manual inspection?");
break;
case PSBTRole::SIGNER:
msgBox.setInformativeText("Transaction needs more signatures. Copy to clipboard?");
break;
case PSBTRole::FINALIZER:
case PSBTRole::EXTRACTOR:
complete = FinalizeAndExtractPSBT(psbtx, mtx);
if (complete) {
msgBox.setInformativeText(tr("Would you like to send this transaction?"));
} else {
// The analyzer missed something, e.g. if there are final_scriptSig/final_scriptWitness
// but with invalid signatures.
msgBox.setInformativeText(tr("There was an unexpected problem processing the PSBT. Copy to clipboard for manual inspection?"));
}
}
msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::Cancel);
switch (msgBox.exec()) {
case QMessageBox::Yes: {
if (complete) {
std::string err_string;
CTransactionRef tx = MakeTransactionRef(mtx);
TransactionError result = BroadcastTransaction(*clientModel->node().context(), tx, err_string, DEFAULT_MAX_RAW_TX_FEE_RATE.GetFeePerK(), /* relay */ true, /* wait_callback */ false);
if (result == TransactionError::OK) {
Q_EMIT message(tr("Success"), tr("Broadcasted transaction sucessfully."), CClientUIInterface::MSG_INFORMATION | CClientUIInterface::MODAL);
} else {
Q_EMIT message(tr("Error"), QString::fromStdString(err_string), CClientUIInterface::MSG_ERROR);
}
} else {
// Serialize the PSBT
CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION);
ssTx << psbtx;
GUIUtil::setClipboard(EncodeBase64(ssTx.str()).c_str());
Q_EMIT message(tr("PSBT copied"), "Copied to clipboard", CClientUIInterface::MSG_INFORMATION);
return;
}
}
case QMessageBox::Cancel:
break;
default:
assert(false);
}
}
bool WalletView::handlePaymentRequest(const SendCoinsRecipient& recipient)
{
return sendCoinsPage->handlePaymentRequest(recipient);
+2
View File
@@ -83,6 +83,8 @@ public Q_SLOTS:
void gotoSignMessageTab(QString addr = "");
/** Show Sign/Verify Message dialog and switch to verify message tab */
void gotoVerifyMessageTab(QString addr = "");
/** Load Partially Signed Bitcoin Transaction */
void gotoLoadPSBT();
/** Show incoming transaction notification for new transactions.