Skip to content

Commit 0aa7206

Browse files
committed
Merge #16944: gui: create PSBT with watch-only wallet
c6dd565 [gui] watch-only wallet: copy PSBT to clipboard (Sjors Provoost) 39465d5 [wallet] add fillPSBT to interface (Sjors Provoost) 848f889 [gui] send: include watch-only (Sjors Provoost) 40537f0 [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 bitcoin/bitcoin@c6dd565 meshcollider: re-ACK c6dd565 Tree-SHA512: ebc3da0737e33b255ed926191b84569aedb6097d14868662bd5dce726ce3048e86e9a31eba987b10dffe1482b35c21ae1cd595c2caa4634bc4cf78a826a83852
2 parents 8aac85d + c6dd565 commit 0aa7206

File tree

6 files changed

+85
-23
lines changed

6 files changed

+85
-23
lines changed

src/interfaces/wallet.cpp

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@
1818
#include <wallet/feebumper.h>
1919
#include <wallet/fees.h>
2020
#include <wallet/ismine.h>
21-
#include <wallet/rpcwallet.h>
2221
#include <wallet/load.h>
22+
#include <wallet/psbtwallet.h>
23+
#include <wallet/rpcwallet.h>
2324
#include <wallet/wallet.h>
2425

2526
#include <memory>
@@ -357,6 +358,14 @@ class WalletImpl : public Wallet
357358
}
358359
return {};
359360
}
361+
TransactionError fillPSBT(PartiallySignedTransaction& psbtx,
362+
bool& complete,
363+
int sighash_type = 1 /* SIGHASH_ALL */,
364+
bool sign = true,
365+
bool bip32derivs = false) override
366+
{
367+
return FillPSBT(m_wallet.get(), psbtx, complete, sighash_type, sign, bip32derivs);
368+
}
360369
WalletBalances getBalances() override
361370
{
362371
const auto bal = m_wallet->GetBalance();

src/interfaces/wallet.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
#include <functional>
1515
#include <map>
1616
#include <memory>
17+
#include <psbt.h>
1718
#include <stdint.h>
1819
#include <string>
1920
#include <tuple>
@@ -194,6 +195,13 @@ class Wallet
194195
bool& in_mempool,
195196
int& num_blocks) = 0;
196197

198+
//! Fill PSBT.
199+
virtual TransactionError fillPSBT(PartiallySignedTransaction& psbtx,
200+
bool& complete,
201+
int sighash_type = 1 /* SIGHASH_ALL */,
202+
bool sign = true,
203+
bool bip32derivs = false) = 0;
204+
197205
//! Get balances.
198206
virtual WalletBalances getBalances() = 0;
199207

src/qt/sendcoinsdialog.cpp

Lines changed: 58 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,12 @@
2121
#include <chainparams.h>
2222
#include <interfaces/node.h>
2323
#include <key_io.h>
24-
#include <wallet/coincontrol.h>
25-
#include <ui_interface.h>
26-
#include <txmempool.h>
2724
#include <policy/fees.h>
25+
#include <txmempool.h>
26+
#include <ui_interface.h>
27+
#include <wallet/coincontrol.h>
2828
#include <wallet/fees.h>
29+
#include <wallet/psbtwallet.h>
2930

3031
#include <QFontMetrics>
3132
#include <QScrollBar>
@@ -186,6 +187,11 @@ void SendCoinsDialog::setModel(WalletModel *_model)
186187
// set default rbf checkbox state
187188
ui->optInRBF->setCheckState(Qt::Checked);
188189

190+
if (model->privateKeysDisabled()) {
191+
ui->sendButton->setText(tr("Cr&eate Unsigned"));
192+
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));
193+
}
194+
189195
// set the smartfee-sliders default value (wallets default conf.target or last stored value)
190196
QSettings settings;
191197
if (settings.value("nSmartFeeSliderPosition").toInt() != 0) {
@@ -305,9 +311,19 @@ void SendCoinsDialog::on_sendButton_clicked()
305311
formatted.append(recipientElement);
306312
}
307313

308-
QString questionString = tr("Are you sure you want to send?");
314+
QString questionString;
315+
if (model->privateKeysDisabled()) {
316+
questionString.append(tr("Do you want to draft this transaction?"));
317+
} else {
318+
questionString.append(tr("Are you sure you want to send?"));
319+
}
320+
309321
questionString.append("<br /><span style='font-size:10pt;'>");
310-
questionString.append(tr("Please, review your transaction."));
322+
if (model->privateKeysDisabled()) {
323+
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));
324+
} else {
325+
questionString.append(tr("Please, review your transaction."));
326+
}
311327
questionString.append("</span>%1");
312328

313329
if(txFee > 0)
@@ -358,8 +374,9 @@ void SendCoinsDialog::on_sendButton_clicked()
358374
} else {
359375
questionString = questionString.arg("<br /><br />" + formatted.at(0));
360376
}
361-
362-
SendConfirmationDialog confirmationDialog(tr("Confirm send coins"), questionString, informative_text, detailed_text, SEND_CONFIRM_DELAY, this);
377+
const QString confirmation = model->privateKeysDisabled() ? tr("Confirm transaction proposal") : tr("Confirm send coins");
378+
const QString confirmButtonText = model->privateKeysDisabled() ? tr("Copy PSBT to clipboard") : tr("Send");
379+
SendConfirmationDialog confirmationDialog(confirmation, questionString, informative_text, detailed_text, SEND_CONFIRM_DELAY, confirmButtonText, this);
363380
confirmationDialog.exec();
364381
QMessageBox::StandardButton retval = static_cast<QMessageBox::StandardButton>(confirmationDialog.result());
365382

@@ -369,17 +386,35 @@ void SendCoinsDialog::on_sendButton_clicked()
369386
return;
370387
}
371388

372-
// now send the prepared transaction
373-
WalletModel::SendCoinsReturn sendStatus = model->sendCoins(currentTransaction);
374-
// process sendStatus and on error generate message shown to user
375-
processSendCoinsReturn(sendStatus);
389+
bool send_failure = false;
390+
if (model->privateKeysDisabled()) {
391+
CMutableTransaction mtx = CMutableTransaction{*(currentTransaction.getWtx())};
392+
PartiallySignedTransaction psbtx(mtx);
393+
bool complete = false;
394+
const TransactionError err = model->wallet().fillPSBT(psbtx, complete, SIGHASH_ALL, false /* sign */, true /* bip32derivs */);
395+
assert(!complete);
396+
assert(err == TransactionError::OK);
397+
// Serialize the PSBT
398+
CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION);
399+
ssTx << psbtx;
400+
GUIUtil::setClipboard(EncodeBase64(ssTx.str()).c_str());
401+
Q_EMIT message(tr("PSBT copied"), "Copied to clipboard", CClientUIInterface::MSG_INFORMATION);
402+
} else {
403+
// now send the prepared transaction
404+
WalletModel::SendCoinsReturn sendStatus = model->sendCoins(currentTransaction);
405+
// process sendStatus and on error generate message shown to user
406+
processSendCoinsReturn(sendStatus);
376407

377-
if (sendStatus.status == WalletModel::OK)
378-
{
408+
if (sendStatus.status == WalletModel::OK) {
409+
Q_EMIT coinsSent(currentTransaction.getWtx()->GetHash());
410+
} else {
411+
send_failure = true;
412+
}
413+
}
414+
if (!send_failure) {
379415
accept();
380416
CoinControlDialog::coinControl()->UnSelectAll();
381417
coinControlUpdateLabels();
382-
Q_EMIT coinsSent(currentTransaction.getWtx()->GetHash());
383418
}
384419
fNewRecipientAllowed = true;
385420
}
@@ -611,6 +646,9 @@ void SendCoinsDialog::useAvailableBalance(SendCoinsEntry* entry)
611646
coin_control = *CoinControlDialog::coinControl();
612647
}
613648

649+
// Include watch-only for wallets without private key
650+
coin_control.fAllowWatchOnly = model->privateKeysDisabled();
651+
614652
// Calculate available amount to send.
615653
CAmount amount = model->wallet().getAvailableBalance(coin_control);
616654
for (int i = 0; i < ui->entries->count(); ++i) {
@@ -663,6 +701,8 @@ void SendCoinsDialog::updateCoinControlState(CCoinControl& ctrl)
663701
// Either custom fee will be used or if not selected, the confirmation target from dropdown box
664702
ctrl.m_confirm_target = getConfTargetForIndex(ui->confTargetSelector->currentIndex());
665703
ctrl.m_signal_bip125_rbf = ui->optInRBF->isChecked();
704+
// Include watch-only for wallets without private key
705+
ctrl.fAllowWatchOnly = model->privateKeysDisabled();
666706
}
667707

668708
void SendCoinsDialog::updateSmartFeeLabel()
@@ -870,8 +910,8 @@ void SendCoinsDialog::coinControlUpdateLabels()
870910
}
871911
}
872912

873-
SendConfirmationDialog::SendConfirmationDialog(const QString& title, const QString& text, const QString& informative_text, const QString& detailed_text, int _secDelay, QWidget* parent)
874-
: QMessageBox(parent), secDelay(_secDelay)
913+
SendConfirmationDialog::SendConfirmationDialog(const QString& title, const QString& text, const QString& informative_text, const QString& detailed_text, int _secDelay, const QString& _confirmButtonText, QWidget* parent)
914+
: QMessageBox(parent), secDelay(_secDelay), confirmButtonText(_confirmButtonText)
875915
{
876916
setIcon(QMessageBox::Question);
877917
setWindowTitle(title); // On macOS, the window title is ignored (as required by the macOS Guidelines).
@@ -908,11 +948,11 @@ void SendConfirmationDialog::updateYesButton()
908948
if(secDelay > 0)
909949
{
910950
yesButton->setEnabled(false);
911-
yesButton->setText(tr("Send") + " (" + QString::number(secDelay) + ")");
951+
yesButton->setText(confirmButtonText + " (" + QString::number(secDelay) + ")");
912952
}
913953
else
914954
{
915955
yesButton->setEnabled(true);
916-
yesButton->setText(tr("Send"));
956+
yesButton->setText(confirmButtonText);
917957
}
918958
}

src/qt/sendcoinsdialog.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ class SendConfirmationDialog : public QMessageBox
108108
Q_OBJECT
109109

110110
public:
111-
SendConfirmationDialog(const QString& title, const QString& text, const QString& informative_text = "", const QString& detailed_text = "", int secDelay = SEND_CONFIRM_DELAY, QWidget* parent = nullptr);
111+
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);
112112
int exec();
113113

114114
private Q_SLOTS:
@@ -119,6 +119,7 @@ private Q_SLOTS:
119119
QAbstractButton *yesButton;
120120
QTimer countDownTimer;
121121
int secDelay;
122+
QString confirmButtonText;
122123
};
123124

124125
#endif // BITCOIN_QT_SENDCOINSDIALOG_H

src/qt/walletmodel.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ WalletModel::SendCoinsReturn WalletModel::prepareTransaction(WalletModelTransact
183183
std::string strFailReason;
184184

185185
auto& newTx = transaction.getWtx();
186-
newTx = m_wallet->createTransaction(vecSend, coinControl, true /* sign */, nChangePosRet, nFeeRequired, strFailReason);
186+
newTx = m_wallet->createTransaction(vecSend, coinControl, !privateKeysDisabled() /* sign */, nChangePosRet, nFeeRequired, strFailReason);
187187
transaction.setTransactionFee(nFeeRequired);
188188
if (fSubtractFeeFromAmount && newTx)
189189
transaction.reassignAmounts(nChangePosRet);

src/wallet/wallet.cpp

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2164,20 +2164,24 @@ std::map<CTxDestination, std::vector<COutput>> CWallet::ListCoins(interfaces::Ch
21642164

21652165
for (const COutput& coin : availableCoins) {
21662166
CTxDestination address;
2167-
if (coin.fSpendable &&
2167+
if ((coin.fSpendable || (IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS) && coin.fSolvable)) &&
21682168
ExtractDestination(FindNonChangeParentOutput(*coin.tx->tx, coin.i).scriptPubKey, address)) {
21692169
result[address].emplace_back(std::move(coin));
21702170
}
21712171
}
21722172

21732173
std::vector<COutPoint> lockedCoins;
21742174
ListLockedCoins(lockedCoins);
2175+
// Include watch-only for wallets without private keys
2176+
const bool include_watch_only = IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS);
2177+
const isminetype is_mine_filter = include_watch_only ? ISMINE_WATCH_ONLY : ISMINE_SPENDABLE;
21752178
for (const COutPoint& output : lockedCoins) {
21762179
auto it = mapWallet.find(output.hash);
21772180
if (it != mapWallet.end()) {
21782181
int depth = it->second.GetDepthInMainChain();
21792182
if (depth >= 0 && output.n < it->second.tx->vout.size() &&
2180-
IsMine(it->second.tx->vout[output.n]) == ISMINE_SPENDABLE) {
2183+
IsMine(it->second.tx->vout[output.n]) == is_mine_filter
2184+
) {
21812185
CTxDestination address;
21822186
if (ExtractDestination(FindNonChangeParentOutput(*it->second.tx, output.n).scriptPubKey, address)) {
21832187
result[address].emplace_back(

0 commit comments

Comments
 (0)