mirror of
https://github.com/bitcoin/bitcoin.git
synced 2026-01-20 23:29:12 +01:00
Instead of having the caller have to figure out the correct sane default to provide to FillPSBT, have FillPSBT do that by having it take the sighash type as an optional. This further allows it to distinguish between an explicit sighash type being provided and expecting the default value to be used.
298 lines
12 KiB
C++
298 lines
12 KiB
C++
// Copyright (c) 2011-2022 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 <common/messages.h>
|
|
#include <core_io.h>
|
|
#include <interfaces/node.h>
|
|
#include <key_io.h>
|
|
#include <node/psbt.h>
|
|
#include <node/types.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/fs.h>
|
|
#include <util/strencodings.h>
|
|
|
|
#include <fstream>
|
|
#include <iostream>
|
|
#include <string>
|
|
|
|
using common::TransactionErrorString;
|
|
using node::AnalyzePSBT;
|
|
using node::DEFAULT_MAX_RAW_TX_FEE_RATE;
|
|
using node::PSBTAnalysis;
|
|
using node::TransactionError;
|
|
|
|
PSBTOperationsDialog::PSBTOperationsDialog(
|
|
QWidget* parent, WalletModel* wallet_model, ClientModel* client_model) : QDialog(parent, GUIUtil::dialog_flags),
|
|
m_ui(new Ui::PSBTOperationsDialog),
|
|
m_wallet_model(wallet_model),
|
|
m_client_model(client_model)
|
|
{
|
|
m_ui->setupUi(this);
|
|
|
|
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 = FinalizePSBT(psbtx); // Make sure all existing signatures are fully combined before checking for completeness.
|
|
if (m_wallet_model) {
|
|
size_t n_could_sign;
|
|
const auto err{m_wallet_model->wallet().fillPSBT(std::nullopt, /*sign=*/false, /*bip32derivs=*/true, &n_could_sign, m_transaction_data, complete)};
|
|
if (err) {
|
|
showStatus(tr("Failed to load transaction: %1")
|
|
.arg(QString::fromStdString(PSBTErrorString(*err).translated)),
|
|
StatusLevel::ERR);
|
|
return;
|
|
}
|
|
m_ui->signTransactionButton->setEnabled(!complete && !m_wallet_model->wallet().privateKeysDisabled() && n_could_sign > 0);
|
|
} else {
|
|
m_ui->signTransactionButton->setEnabled(false);
|
|
}
|
|
|
|
m_ui->broadcastTransactionButton->setEnabled(complete);
|
|
|
|
updateTransactionDisplay();
|
|
}
|
|
|
|
void PSBTOperationsDialog::signTransaction()
|
|
{
|
|
bool complete;
|
|
size_t n_signed;
|
|
|
|
WalletModel::UnlockContext ctx(m_wallet_model->requestUnlock());
|
|
|
|
const auto err{m_wallet_model->wallet().fillPSBT(std::nullopt, /*sign=*/true, /*bip32derivs=*/true, &n_signed, m_transaction_data, complete)};
|
|
|
|
if (err) {
|
|
showStatus(tr("Failed to sign transaction: %1")
|
|
.arg(QString::fromStdString(PSBTErrorString(*err).translated)), StatusLevel::ERR);
|
|
return;
|
|
}
|
|
|
|
updateTransactionDisplay();
|
|
|
|
if (!complete && !ctx.isValid()) {
|
|
showStatus(tr("Cannot sign inputs while wallet is locked."), StatusLevel::WARN);
|
|
} else 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 =
|
|
m_client_model->node().broadcastTransaction(tx, DEFAULT_MAX_RAW_TX_FEE_RATE.GetFeePerK(), err_string);
|
|
|
|
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() {
|
|
DataStream ssTx{};
|
|
ssTx << m_transaction_data;
|
|
GUIUtil::setClipboard(EncodeBase64(ssTx.str()).c_str());
|
|
showStatus(tr("PSBT copied to clipboard."), StatusLevel::INFO);
|
|
}
|
|
|
|
void PSBTOperationsDialog::saveTransaction() {
|
|
DataStream ssTx{};
|
|
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_client_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,
|
|
//: Expanded name of the binary PSBT file format. See: BIP 174.
|
|
tr("Partially Signed Transaction (Binary)") + QLatin1String(" (*.psbt)"), &selected_filter);
|
|
if (filename.isEmpty()) {
|
|
return;
|
|
}
|
|
std::ofstream out{filename.toLocal8Bit().data(), std::ofstream::out | std::ofstream::binary};
|
|
out << ssTx.str();
|
|
out.close();
|
|
showStatus(tr("PSBT saved to disk."), StatusLevel::INFO);
|
|
}
|
|
|
|
void PSBTOperationsDialog::updateTransactionDisplay() {
|
|
m_ui->transactionDescription->setText(renderTransaction(m_transaction_data));
|
|
showTransactionStatus(m_transaction_data);
|
|
}
|
|
|
|
QString PSBTOperationsDialog::renderTransaction(const PartiallySignedTransaction &psbtx)
|
|
{
|
|
QString tx_description;
|
|
QLatin1String bullet_point(" * ");
|
|
CAmount totalAmount = 0;
|
|
for (const CTxOut& out : psbtx.tx->vout) {
|
|
CTxDestination address;
|
|
ExtractDestination(out.scriptPubKey, address);
|
|
totalAmount += out.nValue;
|
|
tx_description.append(bullet_point).append(tr("Sends %1 to %2")
|
|
.arg(BitcoinUnits::formatWithUnit(BitcoinUnit::BTC, out.nValue))
|
|
.arg(QString::fromStdString(EncodeDestination(address))));
|
|
// Check if the address is one of ours
|
|
if (m_wallet_model != nullptr && m_wallet_model->wallet().txoutIsMine(out)) tx_description.append(" (" + tr("own address") + ")");
|
|
tx_description.append("<br>");
|
|
}
|
|
|
|
PSBTAnalysis analysis = AnalyzePSBT(psbtx);
|
|
tx_description.append(bullet_point);
|
|
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(BitcoinUnit::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;
|
|
}
|
|
|
|
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) {
|
|
if (!m_wallet_model) {
|
|
return 0;
|
|
}
|
|
|
|
size_t n_signed;
|
|
bool complete;
|
|
const auto err{m_wallet_model->wallet().fillPSBT(std::nullopt, /*sign=*/false, /*bip32derivs=*/false, &n_signed, m_transaction_data, complete)};
|
|
|
|
if (err) {
|
|
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) {
|
|
need_sig_text += " " + tr("(But no wallet is loaded.)");
|
|
level = StatusLevel::WARN;
|
|
} else 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;
|
|
}
|
|
}
|
|
}
|