v29.4-roots.4 · Commit 24: [PATCH 24/37] feat(qt): add guarded private key sweep dialog
src/qt/sweepdialog.cpp
Before: /dev/null · After: src/qt/sweepdialog.cpp
File metadata
new file mode 100644 index 0000000000000000000000000000000000000000..1d04cf13c2ca0db845eedb56dfd911708879e769
Download original patch · Release notes
Diff rows 1–216 of 216. Line numbers refer to the original before/after files.
+ Added− RemovedUnmarked lines are context.
Scroll the code area horizontally to read long lines. The two line-number columns refer to before and after.
src/qt/sweepdialog.cpp
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
// Copyright (c) 2026 The Bitcoin Roots developers
|
|
2
|
+
// Distributed under the MIT software license, see the accompanying file COPYING or http://www.opensource.org/licenses/MIT.
|
|
3
|
+
|
|
4
|
+
#include <qt/sweepdialog.h>
|
|
5
|
+
|
|
6
|
+
#include <key_io.h>
|
|
7
|
+
#include <qt/bitcoinunits.h>
|
|
8
|
+
#include <qt/optionsmodel.h>
|
|
9
|
+
#include <qt/walletmodel.h>
|
|
10
|
+
#include <util/translation.h>
|
|
11
|
+
#include <uint256.h>
|
|
12
|
+
|
|
13
|
+
#include <QDialogButtonBox>
|
|
14
|
+
#include <QCheckBox>
|
|
15
|
+
#include <QFormLayout>
|
|
16
|
+
#include <QKeyEvent>
|
|
17
|
+
#include <QKeySequence>
|
|
18
|
+
#include <QLabel>
|
|
19
|
+
#include <QLineEdit>
|
|
20
|
+
#include <QMessageBox>
|
|
21
|
+
#include <QPushButton>
|
|
22
|
+
#include <QSignalBlocker>
|
|
23
|
+
#include <QVBoxLayout>
|
|
24
|
+
|
|
25
|
+
namespace {
|
|
26
|
+
class SecureLineEdit final : public QLineEdit
|
|
27
|
+
{
|
|
28
|
+
public:
|
|
29
|
+
using QLineEdit::QLineEdit;
|
|
30
|
+
|
|
31
|
+
protected:
|
|
32
|
+
void keyPressEvent(QKeyEvent* event) override
|
|
33
|
+
{
|
|
34
|
+
if (event->matches(QKeySequence::Copy) || event->matches(QKeySequence::Cut)) {
|
|
35
|
+
event->accept();
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
QLineEdit::keyPressEvent(event);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
} // namespace
|
|
42
|
+
|
|
43
|
+
SweepDialog::SweepDialog(WalletModel* wallet_model, QWidget* parent)
|
|
44
|
+
: QDialog(parent), m_wallet_model(wallet_model)
|
|
45
|
+
{
|
|
46
|
+
setWindowTitle(tr("Sweep Private Key"));
|
|
47
|
+
setModal(true);
|
|
48
|
+
auto* layout = new QVBoxLayout(this);
|
|
49
|
+
auto* warning = new QLabel(tr("A private key is a bearer secret. It is used only for this sweep and is never saved in this wallet."), this);
|
|
50
|
+
warning->setWordWrap(true);
|
|
51
|
+
layout->addWidget(warning);
|
|
52
|
+
auto* form = new QFormLayout;
|
|
53
|
+
m_private_key = new SecureLineEdit(this);
|
|
54
|
+
m_private_key->setObjectName("sweepPrivateKey");
|
|
55
|
+
m_private_key->setEchoMode(QLineEdit::Password);
|
|
56
|
+
m_private_key->setContextMenuPolicy(Qt::NoContextMenu);
|
|
57
|
+
m_private_key->setDragEnabled(false);
|
|
58
|
+
m_private_key->setAccessibleName(tr("Private key"));
|
|
59
|
+
m_private_key->setAccessibleDescription(tr("Masked private key. Copy, cut, and context-menu actions are disabled."));
|
|
60
|
+
auto* reveal = new QCheckBox(tr("Show private key"), this);
|
|
61
|
+
reveal->setObjectName("sweepReveal");
|
|
62
|
+
reveal->setAccessibleName(tr("Show private key"));
|
|
63
|
+
reveal->setAccessibleDescription(tr("Temporarily reveal the private key on screen. Copy and cut remain disabled."));
|
|
64
|
+
m_destination = new QLineEdit(this);
|
|
65
|
+
m_destination->setObjectName("sweepDestination");
|
|
66
|
+
m_destination->setAccessibleName(tr("Destination address"));
|
|
67
|
+
m_destination->setAccessibleDescription(tr("A destination address controlled by the selected wallet."));
|
|
68
|
+
form->addRow(tr("Private key:"), m_private_key);
|
|
69
|
+
form->addRow(QString{}, reveal);
|
|
70
|
+
form->addRow(tr("Destination:"), m_destination);
|
|
71
|
+
layout->addLayout(form);
|
|
72
|
+
m_result = new QLabel(this);
|
|
73
|
+
m_result->setObjectName("sweepResult");
|
|
74
|
+
m_result->setWordWrap(true);
|
|
75
|
+
m_result->setAccessibleName(tr("Sweep preview"));
|
|
76
|
+
m_result->setAccessibleDescription(tr("Non-secret sweep preview, status, and error details."));
|
|
77
|
+
layout->addWidget(m_result);
|
|
78
|
+
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Cancel, this);
|
|
79
|
+
m_preview = buttons->addButton(tr("Preview"), QDialogButtonBox::ActionRole);
|
|
80
|
+
m_broadcast = buttons->addButton(tr("Broadcast sweep"), QDialogButtonBox::AcceptRole);
|
|
81
|
+
m_preview->setObjectName("sweepPreview");
|
|
82
|
+
m_broadcast->setObjectName("sweepBroadcast");
|
|
83
|
+
m_broadcast->setEnabled(false);
|
|
84
|
+
m_preview->setAccessibleName(tr("Preview sweep"));
|
|
85
|
+
m_broadcast->setAccessibleName(tr("Broadcast sweep"));
|
|
86
|
+
m_preview->setAccessibleDescription(tr("Scan for eligible coins and create a transaction preview without broadcasting."));
|
|
87
|
+
m_broadcast->setAccessibleDescription(tr("Broadcast the currently displayed sweep transaction. This cannot be undone."));
|
|
88
|
+
layout->addWidget(buttons);
|
|
89
|
+
setTabOrder(m_private_key, reveal);
|
|
90
|
+
setTabOrder(reveal, m_destination);
|
|
91
|
+
setTabOrder(m_destination, m_preview);
|
|
92
|
+
setTabOrder(m_preview, m_broadcast);
|
|
93
|
+
connect(m_preview, &QPushButton::clicked, this, &SweepDialog::preview);
|
|
94
|
+
connect(m_broadcast, &QPushButton::clicked, this, &SweepDialog::broadcast);
|
|
95
|
+
connect(buttons, &QDialogButtonBox::rejected, this, &SweepDialog::reject);
|
|
96
|
+
connect(m_private_key, &QLineEdit::textChanged, this, &SweepDialog::invalidate);
|
|
97
|
+
connect(m_destination, &QLineEdit::textChanged, this, &SweepDialog::invalidate);
|
|
98
|
+
connect(reveal, &QCheckBox::toggled, this, [this](bool checked) {
|
|
99
|
+
m_private_key->setEchoMode(checked ? QLineEdit::Normal : QLineEdit::Password);
|
|
100
|
+
});
|
|
101
|
+
connect(this, &QDialog::finished, this, &SweepDialog::clearSensitive);
|
|
102
|
+
connect(m_wallet_model, &WalletModel::unload, this, &SweepDialog::clearSensitive);
|
|
103
|
+
connect(m_wallet_model, &WalletModel::sweepRequestInvalidated, this, [this](uint64_t) {
|
|
104
|
+
m_request_generation = 0;
|
|
105
|
+
const QSignalBlocker block{m_private_key};
|
|
106
|
+
m_private_key->clear();
|
|
107
|
+
m_private_key_cache.clear();
|
|
108
|
+
m_preview_txid.clear();
|
|
109
|
+
setBusy(false);
|
|
110
|
+
m_preview_current = false;
|
|
111
|
+
m_broadcast->setEnabled(false);
|
|
112
|
+
m_result->setText(tr("Sweep state changed. Enter the private key and create a fresh preview."));
|
|
113
|
+
});
|
|
114
|
+
connect(m_wallet_model, &WalletModel::sweepFinished, this, &SweepDialog::handleSweepFinished);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
SweepDialog::~SweepDialog() { clearSensitive(); }
|
|
118
|
+
|
|
119
|
+
void SweepDialog::invalidate()
|
|
120
|
+
{
|
|
121
|
+
if (m_request_generation != 0) m_wallet_model->invalidateSweepRequests();
|
|
122
|
+
m_request_generation = 0;
|
|
123
|
+
m_private_key_cache.clear();
|
|
124
|
+
m_preview_txid.clear();
|
|
125
|
+
m_preview_current = false;
|
|
126
|
+
m_broadcast->setEnabled(false);
|
|
127
|
+
m_result->setText(tr("A new preview is required before broadcasting."));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
void SweepDialog::setBusy(bool busy)
|
|
131
|
+
{
|
|
132
|
+
m_busy = busy;
|
|
133
|
+
m_private_key->setEnabled(!busy);
|
|
134
|
+
m_destination->setEnabled(!busy);
|
|
135
|
+
m_preview->setEnabled(!busy);
|
|
136
|
+
if (busy) m_broadcast->setEnabled(false);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
void SweepDialog::setResult(const QString& text, bool can_broadcast)
|
|
140
|
+
{
|
|
141
|
+
m_result->setText(text);
|
|
142
|
+
m_preview_current = can_broadcast;
|
|
143
|
+
m_broadcast->setEnabled(can_broadcast);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
void SweepDialog::preview()
|
|
147
|
+
{
|
|
148
|
+
const CTxDestination destination{DecodeDestination(m_destination->text().toStdString())};
|
|
149
|
+
if (!IsValidDestination(destination)) { setResult(tr("Enter a valid destination address."), false); return; }
|
|
150
|
+
m_private_key_cache = SecureString{m_private_key->text().toStdString()};
|
|
151
|
+
if (m_private_key_cache.empty()) { setResult(tr("Enter a private key."), false); return; }
|
|
152
|
+
const QSignalBlocker block{m_private_key};
|
|
153
|
+
m_private_key->clear();
|
|
154
|
+
setBusy(true);
|
|
155
|
+
m_result->setText(tr("Scanning for eligible coins and preparing a preview…"));
|
|
156
|
+
m_request_generation = m_wallet_model->requestSweep(m_private_key_cache, destination, false);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
void SweepDialog::broadcast()
|
|
160
|
+
{
|
|
161
|
+
if (!m_preview_current || m_private_key_cache.empty()) return;
|
|
162
|
+
const auto answer = QMessageBox::warning(this, tr("Broadcast sweep"),
|
|
163
|
+
tr("Broadcast this exact preview now? This action is irreversible and may fail if the chain state or fee conditions changed."),
|
|
164
|
+
QMessageBox::Cancel | QMessageBox::Yes, QMessageBox::Cancel);
|
|
165
|
+
if (answer != QMessageBox::Yes) return;
|
|
166
|
+
const CTxDestination destination{DecodeDestination(m_destination->text().toStdString())};
|
|
167
|
+
const auto expected_txid{uint256::FromHex(m_preview_txid.toStdString())};
|
|
168
|
+
if (!expected_txid) { setResult(tr("The preview is no longer valid. Create a fresh preview."), false); return; }
|
|
169
|
+
setBusy(true);
|
|
170
|
+
m_preview_current = false;
|
|
171
|
+
m_result->setText(tr("Rechecking the preview and broadcasting the sweep…"));
|
|
172
|
+
m_request_generation = m_wallet_model->requestSweep(m_private_key_cache, destination, true, expected_txid);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
void SweepDialog::handleSweepFinished(uint64_t generation, bool broadcast, bool success, const QString& error,
|
|
176
|
+
qint64 amount, qint64 fee, quint64 inputs, qint64 vsize, bool rbf, const QString& txid)
|
|
177
|
+
{
|
|
178
|
+
if (generation != m_request_generation) return;
|
|
179
|
+
m_request_generation = 0;
|
|
180
|
+
setBusy(false);
|
|
181
|
+
if (!success) {
|
|
182
|
+
setResult(broadcast
|
|
183
|
+
? tr("Broadcast failed: %1. Create a fresh preview before trying again.").arg(error)
|
|
184
|
+
: tr("Preview failed: %1").arg(error), false);
|
|
185
|
+
m_private_key_cache.clear();
|
|
186
|
+
m_preview_txid.clear();
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (broadcast) {
|
|
190
|
+
setResult(tr("Sweep broadcast. Transaction %1 was submitted.").arg(txid), false);
|
|
191
|
+
clearSensitive();
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
m_preview_txid = txid;
|
|
195
|
+
const qint64 fee_rate{vsize > 0 ? (fee + vsize - 1) / vsize : 0};
|
|
196
|
+
setResult(tr("Wallet: %1\nDestination: %2\n%3 input(s), total %4, fee %5, net received %6.\nFee rate: %7 sat/vB. Replace-By-Fee: %8. Review these facts before irreversible broadcast.")
|
|
197
|
+
.arg(m_wallet_model->getDisplayName(), m_destination->text())
|
|
198
|
+
.arg(inputs).arg(BitcoinUnits::formatWithUnit(m_wallet_model->getOptionsModel()->getDisplayUnit(), amount))
|
|
199
|
+
.arg(BitcoinUnits::formatWithUnit(m_wallet_model->getOptionsModel()->getDisplayUnit(), fee))
|
|
200
|
+
.arg(BitcoinUnits::formatWithUnit(m_wallet_model->getOptionsModel()->getDisplayUnit(), amount - fee))
|
|
201
|
+
.arg(fee_rate)
|
|
202
|
+
.arg(rbf ? tr("signaled") : tr("not signaled")), true);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
void SweepDialog::clearSensitive()
|
|
206
|
+
{
|
|
207
|
+
if (m_request_generation != 0) m_wallet_model->invalidateSweepRequests();
|
|
208
|
+
m_request_generation = 0;
|
|
209
|
+
const QSignalBlocker block{m_private_key};
|
|
210
|
+
m_private_key->clear();
|
|
211
|
+
m_private_key_cache.clear();
|
|
212
|
+
m_preview_txid.clear();
|
|
213
|
+
m_preview_current = false;
|
|
214
|
+
setBusy(false);
|
|
215
|
+
m_broadcast->setEnabled(false);
|
|
216
|
+
}
|