mirror of
https://github.com/Retropex/bitcoin.git
synced 2025-05-28 21:12:29 +02:00
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-ACK764bfe4cba
achow101: ACK764bfe4cba
jb55: Tested ACK764bfe4cba
jonatack: ACK764bfe4c
promag: Code review ACK764bfe4cba
. Tree-SHA512: d284ed6895f3a271fb8ff879aac388ad217ddc13f72074725608e1c3d6d90650f6dc9e9e254479544dd71fc111516b02c8ff92158153208dc40fb2726b37d063
This commit is contained in:
commit
4f802e59a0
@ -6,11 +6,19 @@
|
||||
#define BITCOIN_NODE_TRANSACTION_H
|
||||
|
||||
#include <attributes.h>
|
||||
#include <policy/feerate.h>
|
||||
#include <primitives/transaction.h>
|
||||
#include <util/error.h>
|
||||
|
||||
struct NodeContext;
|
||||
|
||||
/** Maximum fee rate for sendrawtransaction and testmempoolaccept RPC calls.
|
||||
* Also used by the GUI when broadcasting a completed PSBT.
|
||||
* By default, a transaction with a fee rate higher than this will be rejected
|
||||
* by these RPCs and the GUI. This can be overridden with the maxfeerate argument.
|
||||
*/
|
||||
static const CFeeRate DEFAULT_MAX_RAW_TX_FEE_RATE{COIN / 10};
|
||||
|
||||
/**
|
||||
* Submit a transaction to the mempool and (optionally) relay it to all P2P peers.
|
||||
*
|
||||
|
@ -40,6 +40,10 @@ static constexpr uint8_t PSBT_OUT_BIP32_DERIVATION = 0x02;
|
||||
// as a 0 length key which indicates that this is the separator. The separator has no value.
|
||||
static constexpr uint8_t PSBT_SEPARATOR = 0x00;
|
||||
|
||||
// BIP 174 does not specify a maximum file size, but we set a limit anyway
|
||||
// to prevent reading a stream indefinately and running out of memory.
|
||||
const std::streamsize MAX_FILE_SIZE_PSBT = 100000000; // 100 MiB
|
||||
|
||||
/** A structure for PSBTs which contain per-input information */
|
||||
struct PSBTInput
|
||||
{
|
||||
|
@ -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()
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
|
@ -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 */
|
||||
|
@ -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);
|
||||
|
@ -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.
|
||||
|
||||
|
@ -40,12 +40,6 @@
|
||||
|
||||
#include <univalue.h>
|
||||
|
||||
/** Maximum fee rate for sendrawtransaction and testmempoolaccept.
|
||||
* By default, a transaction with a fee rate higher than this will be rejected
|
||||
* by the RPCs. This can be overridden with the maxfeerate argument.
|
||||
*/
|
||||
static const CFeeRate DEFAULT_MAX_RAW_TX_FEE_RATE{COIN / 10};
|
||||
|
||||
static void TxToJSON(const CTransaction& tx, const uint256 hashBlock, UniValue& entry)
|
||||
{
|
||||
// Call into TxToUniv() in bitcoin-common to decode the transaction hex.
|
||||
|
@ -141,6 +141,12 @@ bool CheckDiskSpace(const fs::path& dir, uint64_t additional_bytes)
|
||||
return free_bytes_available >= min_disk_space + additional_bytes;
|
||||
}
|
||||
|
||||
std::streampos GetFileSize(const char* path, std::streamsize max) {
|
||||
std::ifstream file(path, std::ios::binary);
|
||||
file.ignore(max);
|
||||
return file.gcount();
|
||||
}
|
||||
|
||||
/**
|
||||
* Interpret a string argument as a boolean.
|
||||
*
|
||||
|
@ -63,6 +63,14 @@ void UnlockDirectory(const fs::path& directory, const std::string& lockfile_name
|
||||
bool DirIsWritable(const fs::path& directory);
|
||||
bool CheckDiskSpace(const fs::path& dir, uint64_t additional_bytes = 0);
|
||||
|
||||
/** Get the size of a file by scanning it.
|
||||
*
|
||||
* @param[in] path The file path
|
||||
* @param[in] max Stop seeking beyond this limit
|
||||
* @return The file size or max
|
||||
*/
|
||||
std::streampos GetFileSize(const char* path, std::streamsize max = std::numeric_limits<std::streamsize>::max());
|
||||
|
||||
/** Release all directory locks. This is used for unit testing only, at runtime
|
||||
* the global destructor will take care of the locks.
|
||||
*/
|
||||
|
Loading…
Reference in New Issue
Block a user