Skip to content
Bitcoin Roots

v31.1-roots.1 · Commit 6: [PATCH 06/17] feat(wallet): preserve Roots signing and spend controls on v31

src/qt/sweepdialog.cpp

Before: /dev/null · After: src/qt/sweepdialog.cpp

File metadata
new file mode 100644
index 0000000000000000000000000000000000000000..6bf558960f395dbeaa0094ef79a37067f4afee56

Download original patch · Release notes

Diff rows 1–273 of 273. Line numbers refer to the original before/after files.

+ Added− RemovedUnmarked lines are context.

Scroll the code area horizontally to read long lines. Before is on the left; after is on the right. Unified view fits narrow screens more easily.

src/qt/sweepdialog.cpp ADDED
@@ -0,0 +1,273 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 <support/cleanse.h>
11
+ #include <util/translation.h>
12
+ #include <primitives/transaction_identifier.h>
13
+
14
+ #include <QByteArray>
15
+ #include <QCheckBox>
16
+ #include <QDialogButtonBox>
17
+ #include <QFormLayout>
18
+ #include <QKeyEvent>
19
+ #include <QKeySequence>
20
+ #include <QLabel>
21
+ #include <QLineEdit>
22
+ #include <QMessageBox>
23
+ #include <QPushButton>
24
+ #include <QSignalBlocker>
25
+ #include <QVBoxLayout>
26
+
27
+ namespace {
28
+ class SecureLineEdit final : public QLineEdit
29
+ {
30
+ public:
31
+ using QLineEdit::QLineEdit;
32
+
33
+ void ClearText()
34
+ {
35
+ setText(QString(text().size(), QChar{' '}));
36
+ clear();
37
+ }
38
+
39
+ SecureString TakeText()
40
+ {
41
+ QByteArray encoded{text().toLatin1()};
42
+ SecureString result{encoded.constData(), static_cast<size_t>(encoded.size())};
43
+ if (!encoded.isEmpty()) memory_cleanse(encoded.data(), encoded.size());
44
+ // QLineEdit does not expose its internal buffer for cleansing. Replace
45
+ // its visible contents before clearing; Qt does not guarantee physical
46
+ // erasure of every internal QString or undo-buffer copy.
47
+ ClearText();
48
+ return result;
49
+ }
50
+
51
+ protected:
52
+ void keyPressEvent(QKeyEvent* event) override
53
+ {
54
+ if (event->matches(QKeySequence::Copy) || event->matches(QKeySequence::Cut)) {
55
+ event->accept();
56
+ return;
57
+ }
58
+ QLineEdit::keyPressEvent(event);
59
+ }
60
+ };
61
+ } // namespace
62
+
63
+ SweepDialog::SweepDialog(WalletModel* wallet_model, QWidget* parent)
64
+ : QDialog(parent), m_wallet_model(wallet_model)
65
+ {
66
+ setWindowTitle(tr("Sweep Private Key"));
67
+ setModal(true);
68
+ auto* layout = new QVBoxLayout(this);
69
+ 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. Keep an independent backup until the transaction confirms."), this);
70
+ warning->setWordWrap(true);
71
+ layout->addWidget(warning);
72
+ auto* form = new QFormLayout;
73
+ m_private_key = new SecureLineEdit(this);
74
+ m_private_key->setObjectName("sweepPrivateKey");
75
+ m_private_key->setEchoMode(QLineEdit::Password);
76
+ m_private_key->setContextMenuPolicy(Qt::NoContextMenu);
77
+ m_private_key->setDragEnabled(false);
78
+ m_private_key->setAccessibleName(tr("Private key"));
79
+ m_private_key->setAccessibleDescription(tr("Masked private key. Copy, cut, and context-menu actions are disabled."));
80
+ auto* reveal = new QCheckBox(tr("Show private key"), this);
81
+ reveal->setObjectName("sweepReveal");
82
+ reveal->setAccessibleName(tr("Show private key"));
83
+ reveal->setAccessibleDescription(tr("Temporarily reveal the private key on screen. Copy and cut remain disabled."));
84
+ m_destination = new QLineEdit(this);
85
+ m_destination->setObjectName("sweepDestination");
86
+ m_destination->setAccessibleName(tr("Destination address"));
87
+ m_destination->setAccessibleDescription(tr("A spendable destination address controlled by the selected wallet. Watch-only and private-key-disabled wallets are not eligible."));
88
+ form->addRow(tr("Private key:"), m_private_key);
89
+ form->addRow(QString{}, reveal);
90
+ form->addRow(tr("Destination:"), m_destination);
91
+ layout->addLayout(form);
92
+ m_result = new QLabel(this);
93
+ m_result->setObjectName("sweepResult");
94
+ m_result->setWordWrap(true);
95
+ m_result->setAccessibleName(tr("Sweep preview"));
96
+ m_result->setAccessibleDescription(tr("Non-secret sweep preview, status, and error details."));
97
+ layout->addWidget(m_result);
98
+ auto* buttons = new QDialogButtonBox(QDialogButtonBox::Cancel, this);
99
+ m_preview = buttons->addButton(tr("Preview"), QDialogButtonBox::ActionRole);
100
+ m_broadcast = buttons->addButton(tr("Broadcast sweep"), QDialogButtonBox::AcceptRole);
101
+ m_preview->setObjectName("sweepPreview");
102
+ m_broadcast->setObjectName("sweepBroadcast");
103
+ m_broadcast->setEnabled(false);
104
+ m_preview->setAccessibleName(tr("Preview sweep"));
105
+ m_broadcast->setAccessibleName(tr("Broadcast sweep"));
106
+ m_preview->setAccessibleDescription(tr("Scan eligible supported coins and create a transaction preview without broadcasting. Review its fee, destination, and Replace-By-Fee state."));
107
+ m_broadcast->setAccessibleDescription(tr("Broadcast the currently displayed sweep transaction. This cannot be undone and may fail if chain state or fees changed."));
108
+ layout->addWidget(buttons);
109
+ setTabOrder(m_private_key, reveal);
110
+ setTabOrder(reveal, m_destination);
111
+ setTabOrder(m_destination, m_preview);
112
+ setTabOrder(m_preview, m_broadcast);
113
+ connect(m_preview, &QPushButton::clicked, this, &SweepDialog::preview);
114
+ connect(m_broadcast, &QPushButton::clicked, this, &SweepDialog::broadcast);
115
+ connect(buttons, &QDialogButtonBox::rejected, this, &SweepDialog::reject);
116
+ connect(m_private_key, &QLineEdit::textChanged, this, &SweepDialog::invalidate);
117
+ connect(m_destination, &QLineEdit::textChanged, this, &SweepDialog::invalidate);
118
+ connect(reveal, &QCheckBox::toggled, this, [this](bool checked) {
119
+ m_private_key->setEchoMode(checked ? QLineEdit::Normal : QLineEdit::Password);
120
+ });
121
+ connect(this, &QDialog::finished, this, &SweepDialog::clearSensitive);
122
+ connect(m_wallet_model, &WalletModel::unload, this, &SweepDialog::clearSensitive);
123
+ connect(m_wallet_model, &WalletModel::sweepRequestInvalidated, this, [this](uint64_t) {
124
+ m_request_generation = 0;
125
+ const QSignalBlocker block{m_private_key};
126
+ static_cast<SecureLineEdit*>(m_private_key)->ClearText();
127
+ clearPrivateKeyCache();
128
+ m_preview_txid.clear();
129
+ setBusy(false);
130
+ m_preview_current = false;
131
+ m_broadcast->setEnabled(false);
132
+ m_result->setText(tr("Sweep state changed. Enter the private key and create a fresh preview."));
133
+ });
134
+ connect(m_wallet_model, &WalletModel::sweepFinished, this, &SweepDialog::handleSweepFinished);
135
+ }
136
+
137
+ SweepDialog::~SweepDialog() { clearSensitive(); }
138
+
139
+ void SweepDialog::clearPrivateKeyCache()
140
+ {
141
+ // clear() alone retains storage, including the old secret beyond size().
142
+ // Wipe live bytes first, including short-string storage, then release any
143
+ // allocation through secure_allocator's cleansing deallocator.
144
+ if (!m_private_key_cache.empty()) memory_cleanse(m_private_key_cache.data(), m_private_key_cache.size());
145
+ SecureString{}.swap(m_private_key_cache);
146
+ }
147
+
148
+ void SweepDialog::invalidate()
149
+ {
150
+ if (m_request_generation != 0 && !m_wallet_model->invalidateSweepRequests()) {
151
+ m_result->setText(tr("Sweep broadcast submission has started and can no longer be cancelled. Wait for its result."));
152
+ return;
153
+ }
154
+ m_request_generation = 0;
155
+ clearPrivateKeyCache();
156
+ m_preview_txid.clear();
157
+ m_preview_current = false;
158
+ m_broadcast->setEnabled(false);
159
+ m_result->setText(tr("A new preview is required before broadcasting."));
160
+ }
161
+
162
+ void SweepDialog::setBusy(bool busy)
163
+ {
164
+ m_busy = busy;
165
+ m_private_key->setEnabled(!busy);
166
+ m_destination->setEnabled(!busy);
167
+ m_preview->setEnabled(!busy);
168
+ if (busy) m_broadcast->setEnabled(false);
169
+ }
170
+
171
+ void SweepDialog::setResult(const QString& text, bool can_broadcast)
172
+ {
173
+ m_result->setText(text);
174
+ m_preview_current = can_broadcast;
175
+ m_broadcast->setEnabled(can_broadcast);
176
+ }
177
+
178
+ void SweepDialog::preview()
179
+ {
180
+ clearPrivateKeyCache();
181
+ const CTxDestination destination{DecodeDestination(m_destination->text().toStdString())};
182
+ if (!IsValidDestination(destination)) { setResult(tr("Enter a valid destination address."), false); return; }
183
+ const QSignalBlocker block{m_private_key};
184
+ m_private_key_cache = static_cast<SecureLineEdit*>(m_private_key)->TakeText();
185
+ if (m_private_key_cache.empty()) { setResult(tr("Enter a private key."), false); return; }
186
+ setBusy(true);
187
+ m_result->setText(tr("Scanning for eligible coins and preparing a preview…"));
188
+ m_request_generation = m_wallet_model->requestSweep(m_private_key_cache, destination, false);
189
+ if (m_request_generation == 0) {
190
+ clearPrivateKeyCache();
191
+ setBusy(false);
192
+ setResult(tr("Another sweep broadcast is already being submitted."), false);
193
+ }
194
+ }
195
+
196
+ void SweepDialog::broadcast()
197
+ {
198
+ if (!m_preview_current || m_private_key_cache.empty()) return;
199
+ const auto answer = QMessageBox::warning(this, tr("Broadcast sweep"),
200
+ tr("Broadcast this exact preview now? This action is irreversible and may fail if the chain state or fee conditions changed."),
201
+ QMessageBox::Cancel | QMessageBox::Yes, QMessageBox::Cancel);
202
+ if (answer != QMessageBox::Yes) return;
203
+ const CTxDestination destination{DecodeDestination(m_destination->text().toStdString())};
204
+ const auto expected_txid{Txid::FromHex(m_preview_txid.toStdString())};
205
+ if (!expected_txid) {
206
+ clearPrivateKeyCache();
207
+ setResult(tr("The preview is no longer valid. Create a fresh preview."), false);
208
+ return;
209
+ }
210
+ setBusy(true);
211
+ m_preview_current = false;
212
+ m_result->setText(tr("Rechecking the preview and broadcasting the sweep…"));
213
+ m_request_generation = m_wallet_model->requestSweep(m_private_key_cache, destination, true, expected_txid);
214
+ if (m_request_generation == 0) {
215
+ clearPrivateKeyCache();
216
+ setBusy(false);
217
+ setResult(tr("Sweep broadcast submission has already started. Wait for its result."), false);
218
+ }
219
+ }
220
+
221
+ void SweepDialog::reject()
222
+ {
223
+ if (m_request_generation != 0 && !m_wallet_model->invalidateSweepRequests()) {
224
+ m_result->setText(tr("Sweep broadcast submission has started and can no longer be cancelled. Wait for its result."));
225
+ return;
226
+ }
227
+ m_request_generation = 0;
228
+ clearSensitive();
229
+ QDialog::reject();
230
+ }
231
+
232
+ void SweepDialog::handleSweepFinished(uint64_t generation, bool broadcast, bool success, const QString& error,
233
+ qint64 amount, qint64 fee, quint64 inputs, qint64 vsize, bool rbf, const QString& txid)
234
+ {
235
+ if (generation != m_request_generation) return;
236
+ m_request_generation = 0;
237
+ setBusy(false);
238
+ if (!success) {
239
+ setResult(broadcast
240
+ ? tr("Broadcast failed: %1. Create a fresh preview before trying again.").arg(error)
241
+ : tr("Preview failed: %1").arg(error), false);
242
+ clearPrivateKeyCache();
243
+ m_preview_txid.clear();
244
+ return;
245
+ }
246
+ if (broadcast) {
247
+ setResult(tr("Sweep broadcast. Transaction %1 was submitted.").arg(txid), false);
248
+ clearSensitive();
249
+ return;
250
+ }
251
+ m_preview_txid = txid;
252
+ const qint64 fee_rate{vsize > 0 ? (fee + vsize - 1) / vsize : 0};
253
+ 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.")
254
+ .arg(m_wallet_model->getDisplayName(), m_destination->text())
255
+ .arg(inputs).arg(BitcoinUnits::formatWithUnit(m_wallet_model->getOptionsModel()->getDisplayUnit(), amount))
256
+ .arg(BitcoinUnits::formatWithUnit(m_wallet_model->getOptionsModel()->getDisplayUnit(), fee))
257
+ .arg(BitcoinUnits::formatWithUnit(m_wallet_model->getOptionsModel()->getDisplayUnit(), amount - fee))
258
+ .arg(fee_rate)
259
+ .arg(rbf ? tr("signaled") : tr("not signaled")), true);
260
+ }
261
+
262
+ void SweepDialog::clearSensitive()
263
+ {
264
+ if (m_request_generation != 0 && !m_wallet_model->invalidateSweepRequests()) return;
265
+ m_request_generation = 0;
266
+ const QSignalBlocker block{m_private_key};
267
+ static_cast<SecureLineEdit*>(m_private_key)->ClearText();
268
+ clearPrivateKeyCache();
269
+ m_preview_txid.clear();
270
+ m_preview_current = false;
271
+ setBusy(false);
272
+ m_broadcast->setEnabled(false);
273
+ }