Merge #18027: "PSBT Operations" dialog

931dd47608 Make lint-spelling.py happy (Glenn Willen)
11a0ffb29d [gui] Load PSBT from clipboard (Glenn Willen)
a6cb0b0c29 [gui] PSBT Operations Dialog (sign & broadcast) (Glenn Willen)
5dd0c03ffa FillPSBT: report number of inputs signed (or would sign) (Glenn Willen)
9e7b23b733 Improve TransactionErrorString messages. (Glenn Willen)

Pull request description:

  Add a "PSBT Operations" dialog, reached from the "Load PSBT..." menu item, giving options to sign or broadcast the loaded PSBT as appropriate, as well as copying the result to the clipboard or saving it to a file.

  This is based on Sjors' #17509, and depends on that PR going in first. (It effectively replaces the small "load PSBT" dialog from that PR with a more feature-rich one.)

  Some notes:
  * The way I display status information is maybe unusual (a status bar, rather than messageboxes.) I think it's helpful to have the information in it be persistent rather than transitory. But if people dislike it, I would probably move the "current state of the transaction" info to the top line of the main label, and the "what action just happened, and did it succeed" info into a messagebox.
  * I don't really know much about the translation/localization stuff. I put tr() in all the places it seemed like it ought to go. I did not attempt to translate the result of TransactionErrorString (which is shared by GUI and non-GUI code); I don't know if that's correct, but it matches the "error messages in logs should be googleable in English" heuristic. I don't know whether there are things I should be doing to reduce translator effort (like minimizing the total number of distinct message strings I use, or something.)
  * I don't really know how (if?) automated testing is applied to GUI code. I can make a list of PSBTs exercising all the codepaths for manual testing, if that's the right approach. Input appreciated.

ACKs for top commit:
  instagibbs:
    tested ACK 931dd47608
  Sjors:
    re-tACK 931dd47608
  jb55:
    ACK 931dd47608
  achow101:
    ACK 931dd47608

Tree-SHA512: ade52471a2242f839a8bd6a1fd231443cc4b43bb9c1de3fb5ace7c5eb59eca99b1f2e9f17dfdb4b08d84d91f5fd65677db1433dd03eef51c7774963ef4e2e74f
This commit is contained in:
Samuel Dobson 2020-06-21 22:56:58 +12:00
commit c27330897d
No known key found for this signature in database
GPG key ID: D300116E1C875A3D
24 changed files with 585 additions and 89 deletions

View file

@ -35,6 +35,7 @@
<ClCompile Include="..\..\src\qt\paymentserver.cpp" />
<ClCompile Include="..\..\src\qt\peertablemodel.cpp" />
<ClCompile Include="..\..\src\qt\platformstyle.cpp" />
<ClCompile Include="..\..\src\qt\psbtoperationsdialog.cpp" />
<ClCompile Include="..\..\src\qt\qrimagewidget.cpp" />
<ClCompile Include="..\..\src\qt\qvalidatedlineedit.cpp" />
<ClCompile Include="..\..\src\qt\qvaluecombobox.cpp" />
@ -87,6 +88,7 @@
<ClCompile Include="$(GeneratedFilesOutDir)\moc\moc_paymentserver.cpp" />
<ClCompile Include="$(GeneratedFilesOutDir)\moc\moc_peertablemodel.cpp" />
<ClCompile Include="$(GeneratedFilesOutDir)\moc\moc_platformstyle.cpp" />
<ClCompile Include="$(GeneratedFilesOutDir)\moc\moc_psbtoperationsdialog.cpp" />
<ClCompile Include="$(GeneratedFilesOutDir)\moc\moc_qrimagewidget.cpp" />
<ClCompile Include="$(GeneratedFilesOutDir)\moc\moc_qvalidatedlineedit.cpp" />
<ClCompile Include="$(GeneratedFilesOutDir)\moc\moc_qvaluecombobox.cpp" />

View file

@ -25,6 +25,7 @@ QT_FORMS_UI = \
qt/forms/openuridialog.ui \
qt/forms/optionsdialog.ui \
qt/forms/overviewpage.ui \
qt/forms/psbtoperationsdialog.ui \
qt/forms/receivecoinsdialog.ui \
qt/forms/receiverequestdialog.ui \
qt/forms/debugwindow.ui \
@ -61,6 +62,7 @@ QT_MOC_CPP = \
qt/moc_overviewpage.cpp \
qt/moc_peertablemodel.cpp \
qt/moc_paymentserver.cpp \
qt/moc_psbtoperationsdialog.cpp \
qt/moc_qrimagewidget.cpp \
qt/moc_qvalidatedlineedit.cpp \
qt/moc_qvaluecombobox.cpp \
@ -132,6 +134,7 @@ BITCOIN_QT_H = \
qt/paymentserver.h \
qt/peertablemodel.h \
qt/platformstyle.h \
qt/psbtoperationsdialog.h \
qt/qrimagewidget.h \
qt/qvalidatedlineedit.h \
qt/qvaluecombobox.h \
@ -245,6 +248,7 @@ BITCOIN_QT_WALLET_CPP = \
qt/openuridialog.cpp \
qt/overviewpage.cpp \
qt/paymentserver.cpp \
qt/psbtoperationsdialog.cpp \
qt/qrimagewidget.cpp \
qt/receivecoinsdialog.cpp \
qt/receiverequestdialog.cpp \

View file

@ -335,9 +335,10 @@ public:
bool sign,
bool bip32derivs,
PartiallySignedTransaction& psbtx,
bool& complete) override
bool& complete,
size_t* n_signed) override
{
return m_wallet->FillPSBT(psbtx, complete, sighash_type, sign, bip32derivs);
return m_wallet->FillPSBT(psbtx, complete, sighash_type, sign, bip32derivs, n_signed);
}
WalletBalances getBalances() override
{

View file

@ -197,7 +197,8 @@ public:
bool sign,
bool bip32derivs,
PartiallySignedTransaction& psbtx,
bool& complete) = 0;
bool& complete,
size_t* n_signed) = 0;
//! Get balances.
virtual WalletBalances getBalances() = 0;

View file

@ -214,6 +214,17 @@ bool PSBTInputSigned(const PSBTInput& input)
return !input.final_script_sig.empty() || !input.final_script_witness.IsNull();
}
size_t CountPSBTUnsignedInputs(const PartiallySignedTransaction& psbt) {
size_t count = 0;
for (const auto& input : psbt.inputs) {
if (!PSBTInputSigned(input)) {
count++;
}
}
return count;
}
void UpdatePSBTOutput(const SigningProvider& provider, PartiallySignedTransaction& psbt, int index)
{
const CTxOut& out = psbt.tx->vout.at(index);

View file

@ -579,6 +579,9 @@ bool PSBTInputSigned(const PSBTInput& input);
/** Signs a PSBTInput, verifying that all provided data matches what is being signed. */
bool SignPSBTInput(const SigningProvider& provider, PartiallySignedTransaction& psbt, int index, int sighash = SIGHASH_ALL, SignatureData* out_sigdata = nullptr, bool use_dummy = false);
/** Counts the unsigned inputs of a PSBT. */
size_t CountPSBTUnsignedInputs(const PartiallySignedTransaction& psbt);
/** Updates a PSBTOutput with information from provider.
*
* This fills in the redeem_script, witness_script, and hd_keypaths where possible.

View file

@ -321,8 +321,10 @@ 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 = new QAction(tr("&Load PSBT from file..."), this);
m_load_psbt_action->setStatusTip(tr("Load Partially Signed Bitcoin Transaction"));
m_load_psbt_clipboard_action = new QAction(tr("Load PSBT from clipboard..."), this);
m_load_psbt_clipboard_action->setStatusTip(tr("Load Partially Signed Bitcoin Transaction from clipboard"));
openRPCConsoleAction = new QAction(tr("Node window"), this);
openRPCConsoleAction->setStatusTip(tr("Open node debugging and diagnostic console"));
@ -381,6 +383,7 @@ void BitcoinGUI::createActions()
connect(signMessageAction, &QAction::triggered, [this]{ showNormalIfMinimized(); });
connect(signMessageAction, &QAction::triggered, [this]{ gotoSignMessageTab(); });
connect(m_load_psbt_action, &QAction::triggered, [this]{ gotoLoadPSBT(); });
connect(m_load_psbt_clipboard_action, &QAction::triggered, [this]{ gotoLoadPSBT(true); });
connect(verifyMessageAction, &QAction::triggered, [this]{ showNormalIfMinimized(); });
connect(verifyMessageAction, &QAction::triggered, [this]{ gotoVerifyMessageTab(); });
connect(usedSendingAddressesAction, &QAction::triggered, walletFrame, &WalletFrame::usedSendingAddresses);
@ -459,6 +462,7 @@ void BitcoinGUI::createMenuBar()
file->addAction(signMessageAction);
file->addAction(verifyMessageAction);
file->addAction(m_load_psbt_action);
file->addAction(m_load_psbt_clipboard_action);
file->addSeparator();
}
file->addAction(quitAction);
@ -878,9 +882,9 @@ void BitcoinGUI::gotoVerifyMessageTab(QString addr)
{
if (walletFrame) walletFrame->gotoVerifyMessageTab(addr);
}
void BitcoinGUI::gotoLoadPSBT()
void BitcoinGUI::gotoLoadPSBT(bool from_clipboard)
{
if (walletFrame) walletFrame->gotoLoadPSBT();
if (walletFrame) walletFrame->gotoLoadPSBT(from_clipboard);
}
#endif // ENABLE_WALLET

View file

@ -139,6 +139,7 @@ private:
QAction* signMessageAction = nullptr;
QAction* verifyMessageAction = nullptr;
QAction* m_load_psbt_action = nullptr;
QAction* m_load_psbt_clipboard_action = nullptr;
QAction* aboutAction = nullptr;
QAction* receiveCoinsAction = nullptr;
QAction* receiveCoinsMenuAction = nullptr;
@ -278,8 +279,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();
/** Load Partially Signed Bitcoin Transaction from file or clipboard */
void gotoLoadPSBT(bool from_clipboard = false);
/** Show open dialog */
void openClicked();

View file

@ -0,0 +1,148 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>PSBTOperationsDialog</class>
<widget class="QDialog" name="PSBTOperationsDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>585</width>
<height>327</height>
</rect>
</property>
<property name="windowTitle">
<string>Dialog</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing">
<number>12</number>
</property>
<property name="sizeConstraint">
<enum>QLayout::SetDefaultConstraint</enum>
</property>
<property name="bottomMargin">
<number>12</number>
</property>
<item>
<layout class="QVBoxLayout" name="mainDialogLayout">
<property name="spacing">
<number>5</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="statusBar">
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="autoFillBackground">
<bool>false</bool>
</property>
<property name="styleSheet">
<string notr="true"/>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<widget class="QTextEdit" name="transactionDescription">
<property name="undoRedoEnabled">
<bool>false</bool>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="buttonRowLayout">
<property name="spacing">
<number>5</number>
</property>
<item>
<widget class="QPushButton" name="signTransactionButton">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="font">
<font>
<weight>50</weight>
<bold>false</bold>
</font>
</property>
<property name="text">
<string>Sign Tx</string>
</property>
<property name="autoDefault">
<bool>true</bool>
</property>
<property name="default">
<bool>false</bool>
</property>
<property name="flat">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="broadcastTransactionButton">
<property name="text">
<string>Broadcast Tx</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="copyToClipboardButton">
<property name="text">
<string>Copy to Clipboard</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="saveButton">
<property name="text">
<string>Save...</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="closeButton">
<property name="text">
<string>Close</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View file

@ -0,0 +1,268 @@
// Copyright (c) 2011-2020 The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#include <qt/psbtoperationsdialog.h>
#include <core_io.h>
#include <interfaces/node.h>
#include <key_io.h>
#include <node/psbt.h>
#include <policy/policy.h>
#include <qt/bitcoinunits.h>
#include <qt/forms/ui_psbtoperationsdialog.h>
#include <qt/guiutil.h>
#include <qt/optionsmodel.h>
#include <util/strencodings.h>
#include <iostream>
PSBTOperationsDialog::PSBTOperationsDialog(
QWidget* parent, WalletModel* wallet_model, ClientModel* client_model) : QDialog(parent),
m_ui(new Ui::PSBTOperationsDialog),
m_wallet_model(wallet_model),
m_client_model(client_model)
{
m_ui->setupUi(this);
setWindowTitle("PSBT Operations");
connect(m_ui->signTransactionButton, &QPushButton::clicked, this, &PSBTOperationsDialog::signTransaction);
connect(m_ui->broadcastTransactionButton, &QPushButton::clicked, this, &PSBTOperationsDialog::broadcastTransaction);
connect(m_ui->copyToClipboardButton, &QPushButton::clicked, this, &PSBTOperationsDialog::copyToClipboard);
connect(m_ui->saveButton, &QPushButton::clicked, this, &PSBTOperationsDialog::saveTransaction);
connect(m_ui->closeButton, &QPushButton::clicked, this, &PSBTOperationsDialog::close);
m_ui->signTransactionButton->setEnabled(false);
m_ui->broadcastTransactionButton->setEnabled(false);
}
PSBTOperationsDialog::~PSBTOperationsDialog()
{
delete m_ui;
}
void PSBTOperationsDialog::openWithPSBT(PartiallySignedTransaction psbtx)
{
m_transaction_data = psbtx;
bool complete;
size_t n_could_sign;
FinalizePSBT(psbtx); // Make sure all existing signatures are fully combined before checking for completeness.
TransactionError err = m_wallet_model->wallet().fillPSBT(SIGHASH_ALL, false /* sign */, true /* bip32derivs */, m_transaction_data, complete, &n_could_sign);
if (err != TransactionError::OK) {
showStatus(tr("Failed to load transaction: %1")
.arg(QString::fromStdString(TransactionErrorString(err).translated)), StatusLevel::ERR);
return;
}
m_ui->broadcastTransactionButton->setEnabled(complete);
m_ui->signTransactionButton->setEnabled(!complete && !m_wallet_model->wallet().privateKeysDisabled() && n_could_sign > 0);
updateTransactionDisplay();
}
void PSBTOperationsDialog::signTransaction()
{
bool complete;
size_t n_signed;
TransactionError err = m_wallet_model->wallet().fillPSBT(SIGHASH_ALL, true /* sign */, true /* bip32derivs */, m_transaction_data, complete, &n_signed);
if (err != TransactionError::OK) {
showStatus(tr("Failed to sign transaction: %1")
.arg(QString::fromStdString(TransactionErrorString(err).translated)), StatusLevel::ERR);
return;
}
updateTransactionDisplay();
if (!complete && n_signed < 1) {
showStatus(tr("Could not sign any more inputs."), StatusLevel::WARN);
} else if (!complete) {
showStatus(tr("Signed %1 inputs, but more signatures are still required.").arg(n_signed),
StatusLevel::INFO);
} else {
showStatus(tr("Signed transaction successfully. Transaction is ready to broadcast."),
StatusLevel::INFO);
m_ui->broadcastTransactionButton->setEnabled(true);
}
}
void PSBTOperationsDialog::broadcastTransaction()
{
CMutableTransaction mtx;
if (!FinalizeAndExtractPSBT(m_transaction_data, mtx)) {
// This is never expected to fail unless we were given a malformed PSBT
// (e.g. with an invalid signature.)
showStatus(tr("Unknown error processing transaction."), StatusLevel::ERR);
return;
}
CTransactionRef tx = MakeTransactionRef(mtx);
std::string err_string;
TransactionError error = BroadcastTransaction(
*m_client_model->node().context(), tx, err_string, DEFAULT_MAX_RAW_TX_FEE_RATE.GetFeePerK(), /* relay */ true, /* await_callback */ false);
if (error == TransactionError::OK) {
showStatus(tr("Transaction broadcast successfully! Transaction ID: %1")
.arg(QString::fromStdString(tx->GetHash().GetHex())), StatusLevel::INFO);
} else {
showStatus(tr("Transaction broadcast failed: %1")
.arg(QString::fromStdString(TransactionErrorString(error).translated)), StatusLevel::ERR);
}
}
void PSBTOperationsDialog::copyToClipboard() {
CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION);
ssTx << m_transaction_data;
GUIUtil::setClipboard(EncodeBase64(ssTx.str()).c_str());
showStatus(tr("PSBT copied to clipboard."), StatusLevel::INFO);
}
void PSBTOperationsDialog::saveTransaction() {
CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION);
ssTx << m_transaction_data;
QString selected_filter;
QString filename_suggestion = "";
bool first = true;
for (const CTxOut& out : m_transaction_data.tx->vout) {
if (!first) {
filename_suggestion.append("-");
}
CTxDestination address;
ExtractDestination(out.scriptPubKey, address);
QString amount = BitcoinUnits::format(m_wallet_model->getOptionsModel()->getDisplayUnit(), out.nValue);
QString address_str = QString::fromStdString(EncodeDestination(address));
filename_suggestion.append(address_str + "-" + amount);
first = false;
}
filename_suggestion.append(".psbt");
QString filename = GUIUtil::getSaveFileName(this,
tr("Save Transaction Data"), filename_suggestion,
tr("Partially Signed Transaction (Binary) (*.psbt)"), &selected_filter);
if (filename.isEmpty()) {
return;
}
std::ofstream out(filename.toLocal8Bit().data());
out << ssTx.str();
out.close();
showStatus(tr("PSBT saved to disk."), StatusLevel::INFO);
}
void PSBTOperationsDialog::updateTransactionDisplay() {
m_ui->transactionDescription->setText(QString::fromStdString(renderTransaction(m_transaction_data)));
showTransactionStatus(m_transaction_data);
}
std::string PSBTOperationsDialog::renderTransaction(const PartiallySignedTransaction &psbtx)
{
QString tx_description = "";
CAmount totalAmount = 0;
for (const CTxOut& out : psbtx.tx->vout) {
CTxDestination address;
ExtractDestination(out.scriptPubKey, address);
totalAmount += out.nValue;
tx_description.append(tr(" * Sends %1 to %2")
.arg(BitcoinUnits::formatWithUnit(BitcoinUnits::BTC, out.nValue))
.arg(QString::fromStdString(EncodeDestination(address))));
tx_description.append("<br>");
}
PSBTAnalysis analysis = AnalyzePSBT(psbtx);
tx_description.append(" * ");
if (!*analysis.fee) {
// This happens if the transaction is missing input UTXO information.
tx_description.append(tr("Unable to calculate transaction fee or total transaction amount."));
} else {
tx_description.append(tr("Pays transaction fee: "));
tx_description.append(BitcoinUnits::formatWithUnit(BitcoinUnits::BTC, *analysis.fee));
// add total amount in all subdivision units
tx_description.append("<hr />");
QStringList alternativeUnits;
for (const BitcoinUnits::Unit u : BitcoinUnits::availableUnits())
{
if(u != m_client_model->getOptionsModel()->getDisplayUnit()) {
alternativeUnits.append(BitcoinUnits::formatHtmlWithUnit(u, totalAmount));
}
}
tx_description.append(QString("<b>%1</b>: <b>%2</b>").arg(tr("Total Amount"))
.arg(BitcoinUnits::formatHtmlWithUnit(m_client_model->getOptionsModel()->getDisplayUnit(), totalAmount)));
tx_description.append(QString("<br /><span style='font-size:10pt; font-weight:normal;'>(=%1)</span>")
.arg(alternativeUnits.join(" " + tr("or") + " ")));
}
size_t num_unsigned = CountPSBTUnsignedInputs(psbtx);
if (num_unsigned > 0) {
tx_description.append("<br><br>");
tx_description.append(tr("Transaction has %1 unsigned inputs.").arg(QString::number(num_unsigned)));
}
return tx_description.toStdString();
}
void PSBTOperationsDialog::showStatus(const QString &msg, StatusLevel level) {
m_ui->statusBar->setText(msg);
switch (level) {
case StatusLevel::INFO: {
m_ui->statusBar->setStyleSheet("QLabel { background-color : lightgreen }");
break;
}
case StatusLevel::WARN: {
m_ui->statusBar->setStyleSheet("QLabel { background-color : orange }");
break;
}
case StatusLevel::ERR: {
m_ui->statusBar->setStyleSheet("QLabel { background-color : red }");
break;
}
}
m_ui->statusBar->show();
}
size_t PSBTOperationsDialog::couldSignInputs(const PartiallySignedTransaction &psbtx) {
size_t n_signed;
bool complete;
TransactionError err = m_wallet_model->wallet().fillPSBT(SIGHASH_ALL, false /* sign */, false /* bip32derivs */, m_transaction_data, complete, &n_signed);
if (err != TransactionError::OK) {
return 0;
}
return n_signed;
}
void PSBTOperationsDialog::showTransactionStatus(const PartiallySignedTransaction &psbtx) {
PSBTAnalysis analysis = AnalyzePSBT(psbtx);
size_t n_could_sign = couldSignInputs(psbtx);
switch (analysis.next) {
case PSBTRole::UPDATER: {
showStatus(tr("Transaction is missing some information about inputs."), StatusLevel::WARN);
break;
}
case PSBTRole::SIGNER: {
QString need_sig_text = tr("Transaction still needs signature(s).");
StatusLevel level = StatusLevel::INFO;
if (m_wallet_model->wallet().privateKeysDisabled()) {
need_sig_text += " " + tr("(But this wallet cannot sign transactions.)");
level = StatusLevel::WARN;
} else if (n_could_sign < 1) {
need_sig_text += " " + tr("(But this wallet does not have the right keys.)"); // XXX wording
level = StatusLevel::WARN;
}
showStatus(need_sig_text, level);
break;
}
case PSBTRole::FINALIZER:
case PSBTRole::EXTRACTOR: {
showStatus(tr("Transaction is fully signed and ready for broadcast."), StatusLevel::INFO);
break;
}
default: {
showStatus(tr("Transaction status is unknown."), StatusLevel::ERR);
break;
}
}
}

View file

@ -0,0 +1,54 @@
// Copyright (c) 2011-2020 The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#ifndef BITCOIN_QT_PSBTOPERATIONSDIALOG_H
#define BITCOIN_QT_PSBTOPERATIONSDIALOG_H
#include <QDialog>
#include <psbt.h>
#include <qt/clientmodel.h>
#include <qt/walletmodel.h>
namespace Ui {
class PSBTOperationsDialog;
}
/** Dialog showing transaction details. */
class PSBTOperationsDialog : public QDialog
{
Q_OBJECT
public:
explicit PSBTOperationsDialog(QWidget* parent, WalletModel* walletModel, ClientModel* clientModel);
~PSBTOperationsDialog();
void openWithPSBT(PartiallySignedTransaction psbtx);
public Q_SLOTS:
void signTransaction();
void broadcastTransaction();
void copyToClipboard();
void saveTransaction();
private:
Ui::PSBTOperationsDialog* m_ui;
PartiallySignedTransaction m_transaction_data;
WalletModel* m_wallet_model;
ClientModel* m_client_model;
enum class StatusLevel {
INFO,
WARN,
ERR
};
size_t couldSignInputs(const PartiallySignedTransaction &psbtx);
void updateTransactionDisplay();
std::string renderTransaction(const PartiallySignedTransaction &psbtx);
void showStatus(const QString &msg, StatusLevel level);
void showTransactionStatus(const PartiallySignedTransaction &psbtx);
};
#endif // BITCOIN_QT_PSBTOPERATIONSDIALOG_H

View file

@ -392,7 +392,7 @@ void SendCoinsDialog::on_sendButton_clicked()
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);
const TransactionError err = model->wallet().fillPSBT(SIGHASH_ALL, false /* sign */, true /* bip32derivs */, psbtx, complete, nullptr);
assert(!complete);
assert(err == TransactionError::OK);
// Serialize the PSBT

View file

@ -165,11 +165,11 @@ void WalletFrame::gotoVerifyMessageTab(QString addr)
walletView->gotoVerifyMessageTab(addr);
}
void WalletFrame::gotoLoadPSBT()
void WalletFrame::gotoLoadPSBT(bool from_clipboard)
{
WalletView *walletView = currentWalletView();
if (walletView) {
walletView->gotoLoadPSBT();
walletView->gotoLoadPSBT(from_clipboard);
}
}

View file

@ -79,7 +79,7 @@ public Q_SLOTS:
void gotoVerifyMessageTab(QString addr = "");
/** Load Partially Signed Bitcoin Transaction */
void gotoLoadPSBT();
void gotoLoadPSBT(bool from_clipboard = false);
/** Encrypt the wallet */
void encryptWallet(bool status);

View file

@ -536,7 +536,7 @@ bool WalletModel::bumpFee(uint256 hash, uint256& new_hash)
if (create_psbt) {
PartiallySignedTransaction psbtx(mtx);
bool complete = false;
const TransactionError err = wallet().fillPSBT(SIGHASH_ALL, false /* sign */, true /* bip32derivs */, psbtx, complete);
const TransactionError err = wallet().fillPSBT(SIGHASH_ALL, false /* sign */, true /* bip32derivs */, psbtx, complete, nullptr);
if (err != TransactionError::OK || complete) {
QMessageBox::critical(nullptr, tr("Fee bump error"), tr("Can't draft transaction."));
return false;

View file

@ -11,6 +11,7 @@
#include <qt/askpassphrasedialog.h>
#include <qt/clientmodel.h>
#include <qt/guiutil.h>
#include <qt/psbtoperationsdialog.h>
#include <qt/optionsmodel.h>
#include <qt/overviewpage.h>
#include <qt/platformstyle.h>
@ -27,6 +28,8 @@
#include <QAction>
#include <QActionGroup>
#include <QApplication>
#include <QClipboard>
#include <QFileDialog>
#include <QHBoxLayout>
#include <QProgressDialog>
@ -204,78 +207,42 @@ void WalletView::gotoVerifyMessageTab(QString addr)
signVerifyMessageDialog->setAddress_VM(addr);
}
void WalletView::gotoLoadPSBT()
void WalletView::gotoLoadPSBT(bool from_clipboard)
{
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::string data;
if (from_clipboard) {
std::string raw = QApplication::clipboard()->text().toStdString();
bool invalid;
data = DecodeBase64(raw, &invalid);
if (invalid) {
Q_EMIT message(tr("Error"), tr("Unable to decode PSBT from clipboard (invalid base64)"), CClientUIInterface::MSG_ERROR);
return;
}
} else {
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);
data = std::string(std::istreambuf_iterator<char>{in}, {});
}
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);
Q_EMIT message(tr("Error"), tr("Unable to decode PSBT") + "\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 successfully."), 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);
}
PSBTOperationsDialog* dlg = new PSBTOperationsDialog(this, walletModel, clientModel);
dlg->openWithPSBT(psbtx);
dlg->setAttribute(Qt::WA_DeleteOnClose);
dlg->exec();
}
bool WalletView::handlePaymentRequest(const SendCoinsRecipient& recipient)

View file

@ -84,7 +84,7 @@ 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();
void gotoLoadPSBT(bool from_clipboard = false);
/** Show incoming transaction notification for new transactions.

View file

@ -14,7 +14,7 @@ bilingual_str TransactionErrorString(const TransactionError err)
case TransactionError::OK:
return Untranslated("No error");
case TransactionError::MISSING_INPUTS:
return Untranslated("Missing inputs");
return Untranslated("Inputs missing or spent");
case TransactionError::ALREADY_IN_CHAIN:
return Untranslated("Transaction already in block chain");
case TransactionError::P2P_DISABLED:
@ -24,11 +24,11 @@ bilingual_str TransactionErrorString(const TransactionError err)
case TransactionError::MEMPOOL_ERROR:
return Untranslated("AcceptToMemoryPool failed");
case TransactionError::INVALID_PSBT:
return Untranslated("PSBT is not sane");
return Untranslated("PSBT is not well-formed");
case TransactionError::PSBT_MISMATCH:
return Untranslated("PSBTs not compatible (different transactions)");
case TransactionError::SIGHASH_MISMATCH:
return Untranslated("Specified sighash value does not match existing value");
return Untranslated("Specified sighash value does not match value stored in PSBT");
case TransactionError::MAX_FEE_EXCEEDED:
return Untranslated("Fee exceeds maximum configured by -maxtxfee");
// no default case, so the compiler can warn about missing cases

View file

@ -584,8 +584,11 @@ SigningResult LegacyScriptPubKeyMan::SignMessage(const std::string& message, con
return SigningResult::SIGNING_FAILED;
}
TransactionError LegacyScriptPubKeyMan::FillPSBT(PartiallySignedTransaction& psbtx, int sighash_type, bool sign, bool bip32derivs) const
TransactionError LegacyScriptPubKeyMan::FillPSBT(PartiallySignedTransaction& psbtx, int sighash_type, bool sign, bool bip32derivs, int* n_signed) const
{
if (n_signed) {
*n_signed = 0;
}
for (unsigned int i = 0; i < psbtx.tx->vin.size(); ++i) {
const CTxIn& txin = psbtx.tx->vin[i];
PSBTInput& input = psbtx.inputs.at(i);
@ -616,6 +619,14 @@ TransactionError LegacyScriptPubKeyMan::FillPSBT(PartiallySignedTransaction& psb
SignatureData sigdata;
input.FillSignatureData(sigdata);
SignPSBTInput(HidingSigningProvider(this, !sign, !bip32derivs), psbtx, i, sighash_type);
bool signed_one = PSBTInputSigned(input);
if (n_signed && (signed_one || !sign)) {
// If sign is false, we assume that we _could_ sign if we get here. This
// will never have false negatives; it is hard to tell under what i
// circumstances it could have false positives.
(*n_signed)++;
}
}
// Fill in the bip32 keypaths and redeemscripts for the outputs so that hardware wallets can identify change
@ -2062,8 +2073,11 @@ SigningResult DescriptorScriptPubKeyMan::SignMessage(const std::string& message,
return SigningResult::OK;
}
TransactionError DescriptorScriptPubKeyMan::FillPSBT(PartiallySignedTransaction& psbtx, int sighash_type, bool sign, bool bip32derivs) const
TransactionError DescriptorScriptPubKeyMan::FillPSBT(PartiallySignedTransaction& psbtx, int sighash_type, bool sign, bool bip32derivs, int* n_signed) const
{
if (n_signed) {
*n_signed = 0;
}
for (unsigned int i = 0; i < psbtx.tx->vin.size(); ++i) {
const CTxIn& txin = psbtx.tx->vin[i];
PSBTInput& input = psbtx.inputs.at(i);
@ -2115,6 +2129,14 @@ TransactionError DescriptorScriptPubKeyMan::FillPSBT(PartiallySignedTransaction&
}
SignPSBTInput(HidingSigningProvider(keys.get(), !sign, !bip32derivs), psbtx, i, sighash_type);
bool signed_one = PSBTInputSigned(input);
if (n_signed && (signed_one || !sign)) {
// If sign is false, we assume that we _could_ sign if we get here. This
// will never have false negatives; it is hard to tell under what i
// circumstances it could have false positives.
(*n_signed)++;
}
}
// Fill in the bip32 keypaths and redeemscripts for the outputs so that hardware wallets can identify change

View file

@ -234,7 +234,7 @@ public:
/** Sign a message with the given script */
virtual SigningResult SignMessage(const std::string& message, const PKHash& pkhash, std::string& str_sig) const { return SigningResult::SIGNING_FAILED; };
/** Adds script and derivation path information to a PSBT, and optionally signs it. */
virtual TransactionError FillPSBT(PartiallySignedTransaction& psbt, int sighash_type = 1 /* SIGHASH_ALL */, bool sign = true, bool bip32derivs = false) const { return TransactionError::INVALID_PSBT; }
virtual TransactionError FillPSBT(PartiallySignedTransaction& psbt, int sighash_type = 1 /* SIGHASH_ALL */, bool sign = true, bool bip32derivs = false, int* n_signed = nullptr) const { return TransactionError::INVALID_PSBT; }
virtual uint256 GetID() const { return uint256(); }
@ -393,7 +393,7 @@ public:
bool SignTransaction(CMutableTransaction& tx, const std::map<COutPoint, Coin>& coins, int sighash, std::map<int, std::string>& input_errors) const override;
SigningResult SignMessage(const std::string& message, const PKHash& pkhash, std::string& str_sig) const override;
TransactionError FillPSBT(PartiallySignedTransaction& psbt, int sighash_type = 1 /* SIGHASH_ALL */, bool sign = true, bool bip32derivs = false) const override;
TransactionError FillPSBT(PartiallySignedTransaction& psbt, int sighash_type = 1 /* SIGHASH_ALL */, bool sign = true, bool bip32derivs = false, int* n_signed = nullptr) const override;
uint256 GetID() const override;
@ -596,7 +596,7 @@ public:
bool SignTransaction(CMutableTransaction& tx, const std::map<COutPoint, Coin>& coins, int sighash, std::map<int, std::string>& input_errors) const override;
SigningResult SignMessage(const std::string& message, const PKHash& pkhash, std::string& str_sig) const override;
TransactionError FillPSBT(PartiallySignedTransaction& psbt, int sighash_type = 1 /* SIGHASH_ALL */, bool sign = true, bool bip32derivs = false) const override;
TransactionError FillPSBT(PartiallySignedTransaction& psbt, int sighash_type = 1 /* SIGHASH_ALL */, bool sign = true, bool bip32derivs = false, int* n_signed = nullptr) const override;
uint256 GetID() const override;

View file

@ -2476,8 +2476,11 @@ bool CWallet::SignTransaction(CMutableTransaction& tx, const std::map<COutPoint,
return false;
}
TransactionError CWallet::FillPSBT(PartiallySignedTransaction& psbtx, bool& complete, int sighash_type, bool sign, bool bip32derivs) const
TransactionError CWallet::FillPSBT(PartiallySignedTransaction& psbtx, bool& complete, int sighash_type, bool sign, bool bip32derivs, size_t * n_signed) const
{
if (n_signed) {
*n_signed = 0;
}
LOCK(cs_wallet);
// Get all of the previous transactions
for (unsigned int i = 0; i < psbtx.tx->vin.size(); ++i) {
@ -2508,10 +2511,15 @@ TransactionError CWallet::FillPSBT(PartiallySignedTransaction& psbtx, bool& comp
// Fill in information from ScriptPubKeyMans
for (ScriptPubKeyMan* spk_man : GetAllScriptPubKeyMans()) {
TransactionError res = spk_man->FillPSBT(psbtx, sighash_type, sign, bip32derivs);
int n_signed_this_spkm = 0;
TransactionError res = spk_man->FillPSBT(psbtx, sighash_type, sign, bip32derivs, &n_signed_this_spkm);
if (res != TransactionError::OK) {
return res;
}
if (n_signed) {
(*n_signed) += n_signed_this_spkm;
}
}
// Complete if every input is now signed

View file

@ -964,7 +964,8 @@ public:
bool& complete,
int sighash_type = 1 /* SIGHASH_ALL */,
bool sign = true,
bool bip32derivs = true) const;
bool bip32derivs = true,
size_t* n_signed = nullptr) const;
/**
* Create a new transaction paying the recipients with a set of coins

View file

@ -475,7 +475,7 @@ class PSBTTest(BitcoinTestFramework):
assert_equal(analysis['next'], 'creator')
assert_equal(analysis['error'], 'PSBT is not valid. Input 0 specifies invalid prevout')
assert_raises_rpc_error(-25, 'Missing inputs', self.nodes[0].walletprocesspsbt, 'cHNidP8BAJoCAAAAAkvEW8NnDtdNtDpsmze+Ht2LH35IJcKv00jKAlUs21RrAwAAAAD/////S8Rbw2cO1020OmybN74e3Ysffkglwq/TSMoCVSzbVGsBAAAAAP7///8CwLYClQAAAAAWABSNJKzjaUb3uOxixsvh1GGE3fW7zQD5ApUAAAAAFgAUKNw0x8HRctAgmvoevm4u1SbN7XIAAAAAAAEAnQIAAAACczMa321tVHuN4GKWKRncycI22aX3uXgwSFUKM2orjRsBAAAAAP7///9zMxrfbW1Ue43gYpYpGdzJwjbZpfe5eDBIVQozaiuNGwAAAAAA/v///wIA+QKVAAAAABl2qRT9zXUVA8Ls5iVqynLHe5/vSe1XyYisQM0ClQAAAAAWABRmWQUcjSjghQ8/uH4Bn/zkakwLtAAAAAAAAQEfQM0ClQAAAAAWABRmWQUcjSjghQ8/uH4Bn/zkakwLtAAAAA==')
assert_raises_rpc_error(-25, 'Inputs missing or spent', self.nodes[0].walletprocesspsbt, 'cHNidP8BAJoCAAAAAkvEW8NnDtdNtDpsmze+Ht2LH35IJcKv00jKAlUs21RrAwAAAAD/////S8Rbw2cO1020OmybN74e3Ysffkglwq/TSMoCVSzbVGsBAAAAAP7///8CwLYClQAAAAAWABSNJKzjaUb3uOxixsvh1GGE3fW7zQD5ApUAAAAAFgAUKNw0x8HRctAgmvoevm4u1SbN7XIAAAAAAAEAnQIAAAACczMa321tVHuN4GKWKRncycI22aX3uXgwSFUKM2orjRsBAAAAAP7///9zMxrfbW1Ue43gYpYpGdzJwjbZpfe5eDBIVQozaiuNGwAAAAAA/v///wIA+QKVAAAAABl2qRT9zXUVA8Ls5iVqynLHe5/vSe1XyYisQM0ClQAAAAAWABRmWQUcjSjghQ8/uH4Bn/zkakwLtAAAAAAAAQEfQM0ClQAAAAAWABRmWQUcjSjghQ8/uH4Bn/zkakwLtAAAAA==')
if __name__ == '__main__':
PSBTTest().main()

View file

@ -14,3 +14,4 @@ setban
hist
ser
unselect
lowercased