Skip to content
Bitcoin Roots

v30.3-roots.1 · Commit 24: [PATCH 24/68] 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..7674def1ca6030ebcb3be2d97e32b043573fd782

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 <primitives/transaction_identifier.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{Txid::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
+ }