Merge #16944: gui: create PSBT with watch-only wallet
c6dd565c88[gui] watch-only wallet: copy PSBT to clipboard (Sjors Provoost)39465d545d[wallet] add fillPSBT to interface (Sjors Provoost)848f889208[gui] send: include watch-only (Sjors Provoost)40537f0909[wallet] ListCoins: include watch-only for wallets without private keys (Sjors Provoost) Pull request description: For wallets with `WALLET_FLAG_DISABLE_PRIVATE_KEYS` this makes the watch-only balance available on the send screen (including coin selection). Instead of sending a transaction it generates a PSBT. The user can take this PSBT and process it with [HWI](https://github.com/bitcoin-core/HWI) or put it an SD card for hardware wallets that support that. The PSBT is copied to the clipboard. This was the easiest approach; we can add a dialog later to display it, as well as an option to save to disk. ACKs for top commit: instagibbs: test and code review ACK https://github.com/bitcoin/bitcoin/pull/16944/commits/c6dd565c8820aa8a98b190621e10c6e2821a9ecc meshcollider: re-ACKc6dd565c88Tree-SHA512: ebc3da0737e33b255ed926191b84569aedb6097d14868662bd5dce726ce3048e86e9a31eba987b10dffe1482b35c21ae1cd595c2caa4634bc4cf78a826a83852
This commit is contained in:
6 files changed
+85
-23
No files matched your search
@@ -18,8 +18,9 @@
|
|||||||
#include <wallet/feebumper.h>
|
#include <wallet/feebumper.h>
|
||||||
#include <wallet/fees.h>
|
#include <wallet/fees.h>
|
||||||
#include <wallet/ismine.h>
|
#include <wallet/ismine.h>
|
||||||
#include <wallet/rpcwallet.h>
|
|
||||||
#include <wallet/load.h>
|
#include <wallet/load.h>
|
||||||
|
#include <wallet/psbtwallet.h>
|
||||||
|
#include <wallet/rpcwallet.h>
|
||||||
#include <wallet/wallet.h>
|
#include <wallet/wallet.h>
|
||||||
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
@@ -357,6 +358,14 @@ public:
|
|||||||
}
|
}
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
TransactionError fillPSBT(PartiallySignedTransaction& psbtx,
|
||||||
|
bool& complete,
|
||||||
|
int sighash_type = 1 /* SIGHASH_ALL */,
|
||||||
|
bool sign = true,
|
||||||
|
bool bip32derivs = false) override
|
||||||
|
{
|
||||||
|
return FillPSBT(m_wallet.get(), psbtx, complete, sighash_type, sign, bip32derivs);
|
||||||
|
}
|
||||||
WalletBalances getBalances() override
|
WalletBalances getBalances() override
|
||||||
{
|
{
|
||||||
const auto bal = m_wallet->GetBalance();
|
const auto bal = m_wallet->GetBalance();
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
#include <functional>
|
#include <functional>
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <psbt.h>
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <tuple>
|
#include <tuple>
|
||||||
@@ -194,6 +195,13 @@ public:
|
|||||||
bool& in_mempool,
|
bool& in_mempool,
|
||||||
int& num_blocks) = 0;
|
int& num_blocks) = 0;
|
||||||
|
|
||||||
|
//! Fill PSBT.
|
||||||
|
virtual TransactionError fillPSBT(PartiallySignedTransaction& psbtx,
|
||||||
|
bool& complete,
|
||||||
|
int sighash_type = 1 /* SIGHASH_ALL */,
|
||||||
|
bool sign = true,
|
||||||
|
bool bip32derivs = false) = 0;
|
||||||
|
|
||||||
//! Get balances.
|
//! Get balances.
|
||||||
virtual WalletBalances getBalances() = 0;
|
virtual WalletBalances getBalances() = 0;
|
||||||
|
|
||||||
|
|||||||
+58
-18
@@ -21,11 +21,12 @@
|
|||||||
#include <chainparams.h>
|
#include <chainparams.h>
|
||||||
#include <interfaces/node.h>
|
#include <interfaces/node.h>
|
||||||
#include <key_io.h>
|
#include <key_io.h>
|
||||||
#include <wallet/coincontrol.h>
|
|
||||||
#include <ui_interface.h>
|
|
||||||
#include <txmempool.h>
|
|
||||||
#include <policy/fees.h>
|
#include <policy/fees.h>
|
||||||
|
#include <txmempool.h>
|
||||||
|
#include <ui_interface.h>
|
||||||
|
#include <wallet/coincontrol.h>
|
||||||
#include <wallet/fees.h>
|
#include <wallet/fees.h>
|
||||||
|
#include <wallet/psbtwallet.h>
|
||||||
|
|
||||||
#include <QFontMetrics>
|
#include <QFontMetrics>
|
||||||
#include <QScrollBar>
|
#include <QScrollBar>
|
||||||
@@ -186,6 +187,11 @@ void SendCoinsDialog::setModel(WalletModel *_model)
|
|||||||
// set default rbf checkbox state
|
// set default rbf checkbox state
|
||||||
ui->optInRBF->setCheckState(Qt::Checked);
|
ui->optInRBF->setCheckState(Qt::Checked);
|
||||||
|
|
||||||
|
if (model->privateKeysDisabled()) {
|
||||||
|
ui->sendButton->setText(tr("Cr&eate Unsigned"));
|
||||||
|
ui->sendButton->setToolTip(tr("Creates a Partially Signed Bitcoin Transaction (PSBT) for use with e.g. an offline %1 wallet, or a PSBT-compatible hardware wallet.").arg(PACKAGE_NAME));
|
||||||
|
}
|
||||||
|
|
||||||
// set the smartfee-sliders default value (wallets default conf.target or last stored value)
|
// set the smartfee-sliders default value (wallets default conf.target or last stored value)
|
||||||
QSettings settings;
|
QSettings settings;
|
||||||
if (settings.value("nSmartFeeSliderPosition").toInt() != 0) {
|
if (settings.value("nSmartFeeSliderPosition").toInt() != 0) {
|
||||||
@@ -305,9 +311,19 @@ void SendCoinsDialog::on_sendButton_clicked()
|
|||||||
formatted.append(recipientElement);
|
formatted.append(recipientElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
QString questionString = tr("Are you sure you want to send?");
|
QString questionString;
|
||||||
|
if (model->privateKeysDisabled()) {
|
||||||
|
questionString.append(tr("Do you want to draft this transaction?"));
|
||||||
|
} else {
|
||||||
|
questionString.append(tr("Are you sure you want to send?"));
|
||||||
|
}
|
||||||
|
|
||||||
questionString.append("<br /><span style='font-size:10pt;'>");
|
questionString.append("<br /><span style='font-size:10pt;'>");
|
||||||
questionString.append(tr("Please, review your transaction."));
|
if (model->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));
|
||||||
|
} else {
|
||||||
|
questionString.append(tr("Please, review your transaction."));
|
||||||
|
}
|
||||||
questionString.append("</span>%1");
|
questionString.append("</span>%1");
|
||||||
|
|
||||||
if(txFee > 0)
|
if(txFee > 0)
|
||||||
@@ -358,8 +374,9 @@ void SendCoinsDialog::on_sendButton_clicked()
|
|||||||
} else {
|
} else {
|
||||||
questionString = questionString.arg("<br /><br />" + formatted.at(0));
|
questionString = questionString.arg("<br /><br />" + formatted.at(0));
|
||||||
}
|
}
|
||||||
|
const QString confirmation = model->privateKeysDisabled() ? tr("Confirm transaction proposal") : tr("Confirm send coins");
|
||||||
SendConfirmationDialog confirmationDialog(tr("Confirm send coins"), questionString, informative_text, detailed_text, SEND_CONFIRM_DELAY, this);
|
const QString confirmButtonText = model->privateKeysDisabled() ? tr("Copy PSBT to clipboard") : tr("Send");
|
||||||
|
SendConfirmationDialog confirmationDialog(confirmation, questionString, informative_text, detailed_text, SEND_CONFIRM_DELAY, confirmButtonText, this);
|
||||||
confirmationDialog.exec();
|
confirmationDialog.exec();
|
||||||
QMessageBox::StandardButton retval = static_cast<QMessageBox::StandardButton>(confirmationDialog.result());
|
QMessageBox::StandardButton retval = static_cast<QMessageBox::StandardButton>(confirmationDialog.result());
|
||||||
|
|
||||||
@@ -369,17 +386,35 @@ void SendCoinsDialog::on_sendButton_clicked()
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// now send the prepared transaction
|
bool send_failure = false;
|
||||||
WalletModel::SendCoinsReturn sendStatus = model->sendCoins(currentTransaction);
|
if (model->privateKeysDisabled()) {
|
||||||
// process sendStatus and on error generate message shown to user
|
CMutableTransaction mtx = CMutableTransaction{*(currentTransaction.getWtx())};
|
||||||
processSendCoinsReturn(sendStatus);
|
PartiallySignedTransaction psbtx(mtx);
|
||||||
|
bool complete = false;
|
||||||
|
const TransactionError err = model->wallet().fillPSBT(psbtx, complete, SIGHASH_ALL, false /* sign */, true /* bip32derivs */);
|
||||||
|
assert(!complete);
|
||||||
|
assert(err == TransactionError::OK);
|
||||||
|
// 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);
|
||||||
|
} else {
|
||||||
|
// now send the prepared transaction
|
||||||
|
WalletModel::SendCoinsReturn sendStatus = model->sendCoins(currentTransaction);
|
||||||
|
// process sendStatus and on error generate message shown to user
|
||||||
|
processSendCoinsReturn(sendStatus);
|
||||||
|
|
||||||
if (sendStatus.status == WalletModel::OK)
|
if (sendStatus.status == WalletModel::OK) {
|
||||||
{
|
Q_EMIT coinsSent(currentTransaction.getWtx()->GetHash());
|
||||||
|
} else {
|
||||||
|
send_failure = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!send_failure) {
|
||||||
accept();
|
accept();
|
||||||
CoinControlDialog::coinControl()->UnSelectAll();
|
CoinControlDialog::coinControl()->UnSelectAll();
|
||||||
coinControlUpdateLabels();
|
coinControlUpdateLabels();
|
||||||
Q_EMIT coinsSent(currentTransaction.getWtx()->GetHash());
|
|
||||||
}
|
}
|
||||||
fNewRecipientAllowed = true;
|
fNewRecipientAllowed = true;
|
||||||
}
|
}
|
||||||
@@ -611,6 +646,9 @@ void SendCoinsDialog::useAvailableBalance(SendCoinsEntry* entry)
|
|||||||
coin_control = *CoinControlDialog::coinControl();
|
coin_control = *CoinControlDialog::coinControl();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Include watch-only for wallets without private key
|
||||||
|
coin_control.fAllowWatchOnly = model->privateKeysDisabled();
|
||||||
|
|
||||||
// Calculate available amount to send.
|
// Calculate available amount to send.
|
||||||
CAmount amount = model->wallet().getAvailableBalance(coin_control);
|
CAmount amount = model->wallet().getAvailableBalance(coin_control);
|
||||||
for (int i = 0; i < ui->entries->count(); ++i) {
|
for (int i = 0; i < ui->entries->count(); ++i) {
|
||||||
@@ -663,6 +701,8 @@ void SendCoinsDialog::updateCoinControlState(CCoinControl& ctrl)
|
|||||||
// Either custom fee will be used or if not selected, the confirmation target from dropdown box
|
// Either custom fee will be used or if not selected, the confirmation target from dropdown box
|
||||||
ctrl.m_confirm_target = getConfTargetForIndex(ui->confTargetSelector->currentIndex());
|
ctrl.m_confirm_target = getConfTargetForIndex(ui->confTargetSelector->currentIndex());
|
||||||
ctrl.m_signal_bip125_rbf = ui->optInRBF->isChecked();
|
ctrl.m_signal_bip125_rbf = ui->optInRBF->isChecked();
|
||||||
|
// Include watch-only for wallets without private key
|
||||||
|
ctrl.fAllowWatchOnly = model->privateKeysDisabled();
|
||||||
}
|
}
|
||||||
|
|
||||||
void SendCoinsDialog::updateSmartFeeLabel()
|
void SendCoinsDialog::updateSmartFeeLabel()
|
||||||
@@ -870,8 +910,8 @@ void SendCoinsDialog::coinControlUpdateLabels()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SendConfirmationDialog::SendConfirmationDialog(const QString& title, const QString& text, const QString& informative_text, const QString& detailed_text, int _secDelay, QWidget* parent)
|
SendConfirmationDialog::SendConfirmationDialog(const QString& title, const QString& text, const QString& informative_text, const QString& detailed_text, int _secDelay, const QString& _confirmButtonText, QWidget* parent)
|
||||||
: QMessageBox(parent), secDelay(_secDelay)
|
: QMessageBox(parent), secDelay(_secDelay), confirmButtonText(_confirmButtonText)
|
||||||
{
|
{
|
||||||
setIcon(QMessageBox::Question);
|
setIcon(QMessageBox::Question);
|
||||||
setWindowTitle(title); // On macOS, the window title is ignored (as required by the macOS Guidelines).
|
setWindowTitle(title); // On macOS, the window title is ignored (as required by the macOS Guidelines).
|
||||||
@@ -908,11 +948,11 @@ void SendConfirmationDialog::updateYesButton()
|
|||||||
if(secDelay > 0)
|
if(secDelay > 0)
|
||||||
{
|
{
|
||||||
yesButton->setEnabled(false);
|
yesButton->setEnabled(false);
|
||||||
yesButton->setText(tr("Send") + " (" + QString::number(secDelay) + ")");
|
yesButton->setText(confirmButtonText + " (" + QString::number(secDelay) + ")");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
yesButton->setEnabled(true);
|
yesButton->setEnabled(true);
|
||||||
yesButton->setText(tr("Send"));
|
yesButton->setText(confirmButtonText);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -108,7 +108,7 @@ class SendConfirmationDialog : public QMessageBox
|
|||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
SendConfirmationDialog(const QString& title, const QString& text, const QString& informative_text = "", const QString& detailed_text = "", int secDelay = SEND_CONFIRM_DELAY, QWidget* parent = nullptr);
|
SendConfirmationDialog(const QString& title, const QString& text, const QString& informative_text = "", const QString& detailed_text = "", int secDelay = SEND_CONFIRM_DELAY, const QString& confirmText = "Send", QWidget* parent = nullptr);
|
||||||
int exec();
|
int exec();
|
||||||
|
|
||||||
private Q_SLOTS:
|
private Q_SLOTS:
|
||||||
@@ -119,6 +119,7 @@ private:
|
|||||||
QAbstractButton *yesButton;
|
QAbstractButton *yesButton;
|
||||||
QTimer countDownTimer;
|
QTimer countDownTimer;
|
||||||
int secDelay;
|
int secDelay;
|
||||||
|
QString confirmButtonText;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // BITCOIN_QT_SENDCOINSDIALOG_H
|
#endif // BITCOIN_QT_SENDCOINSDIALOG_H
|
||||||
@@ -183,7 +183,7 @@ WalletModel::SendCoinsReturn WalletModel::prepareTransaction(WalletModelTransact
|
|||||||
std::string strFailReason;
|
std::string strFailReason;
|
||||||
|
|
||||||
auto& newTx = transaction.getWtx();
|
auto& newTx = transaction.getWtx();
|
||||||
newTx = m_wallet->createTransaction(vecSend, coinControl, true /* sign */, nChangePosRet, nFeeRequired, strFailReason);
|
newTx = m_wallet->createTransaction(vecSend, coinControl, !privateKeysDisabled() /* sign */, nChangePosRet, nFeeRequired, strFailReason);
|
||||||
transaction.setTransactionFee(nFeeRequired);
|
transaction.setTransactionFee(nFeeRequired);
|
||||||
if (fSubtractFeeFromAmount && newTx)
|
if (fSubtractFeeFromAmount && newTx)
|
||||||
transaction.reassignAmounts(nChangePosRet);
|
transaction.reassignAmounts(nChangePosRet);
|
||||||
|
|||||||
@@ -2164,7 +2164,7 @@ std::map<CTxDestination, std::vector<COutput>> CWallet::ListCoins(interfaces::Ch
|
|||||||
|
|
||||||
for (const COutput& coin : availableCoins) {
|
for (const COutput& coin : availableCoins) {
|
||||||
CTxDestination address;
|
CTxDestination address;
|
||||||
if (coin.fSpendable &&
|
if ((coin.fSpendable || (IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS) && coin.fSolvable)) &&
|
||||||
ExtractDestination(FindNonChangeParentOutput(*coin.tx->tx, coin.i).scriptPubKey, address)) {
|
ExtractDestination(FindNonChangeParentOutput(*coin.tx->tx, coin.i).scriptPubKey, address)) {
|
||||||
result[address].emplace_back(std::move(coin));
|
result[address].emplace_back(std::move(coin));
|
||||||
}
|
}
|
||||||
@@ -2172,12 +2172,16 @@ std::map<CTxDestination, std::vector<COutput>> CWallet::ListCoins(interfaces::Ch
|
|||||||
|
|
||||||
std::vector<COutPoint> lockedCoins;
|
std::vector<COutPoint> lockedCoins;
|
||||||
ListLockedCoins(lockedCoins);
|
ListLockedCoins(lockedCoins);
|
||||||
|
// Include watch-only for wallets without private keys
|
||||||
|
const bool include_watch_only = IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS);
|
||||||
|
const isminetype is_mine_filter = include_watch_only ? ISMINE_WATCH_ONLY : ISMINE_SPENDABLE;
|
||||||
for (const COutPoint& output : lockedCoins) {
|
for (const COutPoint& output : lockedCoins) {
|
||||||
auto it = mapWallet.find(output.hash);
|
auto it = mapWallet.find(output.hash);
|
||||||
if (it != mapWallet.end()) {
|
if (it != mapWallet.end()) {
|
||||||
int depth = it->second.GetDepthInMainChain();
|
int depth = it->second.GetDepthInMainChain();
|
||||||
if (depth >= 0 && output.n < it->second.tx->vout.size() &&
|
if (depth >= 0 && output.n < it->second.tx->vout.size() &&
|
||||||
IsMine(it->second.tx->vout[output.n]) == ISMINE_SPENDABLE) {
|
IsMine(it->second.tx->vout[output.n]) == is_mine_filter
|
||||||
|
) {
|
||||||
CTxDestination address;
|
CTxDestination address;
|
||||||
if (ExtractDestination(FindNonChangeParentOutput(*it->second.tx, output.n).scriptPubKey, address)) {
|
if (ExtractDestination(FindNonChangeParentOutput(*it->second.tx, output.n).scriptPubKey, address)) {
|
||||||
result[address].emplace_back(
|
result[address].emplace_back(
|
||||||
|
|||||||
Reference in new issue
Block a user