Bitcoin Core  27.99.0
P2P Digital Currency
walletcontroller.cpp
Go to the documentation of this file.
1 // Copyright (c) 2019-2022 The Bitcoin Core developers
2 // Distributed under the MIT software license, see the accompanying
3 // file COPYING or http://www.opensource.org/licenses/mit-license.php.
4 
5 #include <qt/walletcontroller.h>
6 
8 #include <qt/clientmodel.h>
10 #include <qt/guiconstants.h>
11 #include <qt/guiutil.h>
12 #include <qt/walletmodel.h>
13 
14 #include <external_signer.h>
15 #include <interfaces/handler.h>
16 #include <interfaces/node.h>
17 #include <util/string.h>
18 #include <util/threadnames.h>
19 #include <util/translation.h>
20 #include <wallet/wallet.h>
21 
22 #include <algorithm>
23 #include <chrono>
24 
25 #include <QApplication>
26 #include <QMessageBox>
27 #include <QMetaObject>
28 #include <QMutexLocker>
29 #include <QThread>
30 #include <QTimer>
31 #include <QWindow>
32 
37 
38 WalletController::WalletController(ClientModel& client_model, const PlatformStyle* platform_style, QObject* parent)
39  : QObject(parent)
40  , m_activity_thread(new QThread(this))
41  , m_activity_worker(new QObject)
42  , m_client_model(client_model)
43  , m_node(client_model.node())
44  , m_platform_style(platform_style)
45  , m_options_model(client_model.getOptionsModel())
46 {
47  m_handler_load_wallet = m_node.walletLoader().handleLoadWallet([this](std::unique_ptr<interfaces::Wallet> wallet) {
48  getOrCreateWallet(std::move(wallet));
49  });
50 
51  m_activity_worker->moveToThread(m_activity_thread);
52  m_activity_thread->start();
53  QTimer::singleShot(0, m_activity_worker, []() {
54  util::ThreadRename("qt-walletctrl");
55  });
56 }
57 
58 // Not using the default destructor because not all member types definitions are
59 // available in the header, just forward declared.
61 {
62  m_activity_thread->quit();
63  m_activity_thread->wait();
64  delete m_activity_worker;
65 }
66 
67 std::map<std::string, bool> WalletController::listWalletDir() const
68 {
69  QMutexLocker locker(&m_mutex);
70  std::map<std::string, bool> wallets;
71  for (const std::string& name : m_node.walletLoader().listWalletDir()) {
72  wallets[name] = false;
73  }
74  for (WalletModel* wallet_model : m_wallets) {
75  auto it = wallets.find(wallet_model->wallet().getWalletName());
76  if (it != wallets.end()) it->second = true;
77  }
78  return wallets;
79 }
80 
81 void WalletController::closeWallet(WalletModel* wallet_model, QWidget* parent)
82 {
83  QMessageBox box(parent);
84  box.setWindowTitle(tr("Close wallet"));
85  box.setText(tr("Are you sure you wish to close the wallet <i>%1</i>?").arg(GUIUtil::HtmlEscape(wallet_model->getDisplayName())));
86  box.setInformativeText(tr("Closing the wallet for too long can result in having to resync the entire chain if pruning is enabled."));
87  box.setStandardButtons(QMessageBox::Yes|QMessageBox::Cancel);
88  box.setDefaultButton(QMessageBox::Yes);
89  if (box.exec() != QMessageBox::Yes) return;
90 
91  // First remove wallet from node.
92  wallet_model->wallet().remove();
93  // Now release the model.
94  removeAndDeleteWallet(wallet_model);
95 }
96 
97 void WalletController::closeAllWallets(QWidget* parent)
98 {
99  QMessageBox::StandardButton button = QMessageBox::question(parent, tr("Close all wallets"),
100  tr("Are you sure you wish to close all wallets?"),
101  QMessageBox::Yes|QMessageBox::Cancel,
102  QMessageBox::Yes);
103  if (button != QMessageBox::Yes) return;
104 
105  QMutexLocker locker(&m_mutex);
106  for (WalletModel* wallet_model : m_wallets) {
107  wallet_model->wallet().remove();
108  Q_EMIT walletRemoved(wallet_model);
109  delete wallet_model;
110  }
111  m_wallets.clear();
112 }
113 
114 WalletModel* WalletController::getOrCreateWallet(std::unique_ptr<interfaces::Wallet> wallet)
115 {
116  QMutexLocker locker(&m_mutex);
117 
118  // Return model instance if exists.
119  if (!m_wallets.empty()) {
120  std::string name = wallet->getWalletName();
121  for (WalletModel* wallet_model : m_wallets) {
122  if (wallet_model->wallet().getWalletName() == name) {
123  return wallet_model;
124  }
125  }
126  }
127 
128  // Instantiate model and register it.
129  WalletModel* wallet_model = new WalletModel(std::move(wallet), m_client_model, m_platform_style,
130  nullptr /* required for the following moveToThread() call */);
131 
132  // Move WalletModel object to the thread that created the WalletController
133  // object (GUI main thread), instead of the current thread, which could be
134  // an outside wallet thread or RPC thread sending a LoadWallet notification.
135  // This ensures queued signals sent to the WalletModel object will be
136  // handled on the GUI event loop.
137  wallet_model->moveToThread(thread());
138  // setParent(parent) must be called in the thread which created the parent object. More details in #18948.
139  QMetaObject::invokeMethod(this, [wallet_model, this] {
140  wallet_model->setParent(this);
142 
143  m_wallets.push_back(wallet_model);
144 
145  // WalletModel::startPollBalance needs to be called in a thread managed by
146  // Qt because of startTimer. Considering the current thread can be a RPC
147  // thread, better delegate the calling to Qt with Qt::AutoConnection.
148  const bool called = QMetaObject::invokeMethod(wallet_model, "startPollBalance");
149  assert(called);
150 
151  connect(wallet_model, &WalletModel::unload, this, [this, wallet_model] {
152  // Defer removeAndDeleteWallet when no modal widget is active.
153  // TODO: remove this workaround by removing usage of QDialog::exec.
154  if (QApplication::activeModalWidget()) {
155  connect(qApp, &QApplication::focusWindowChanged, wallet_model, [this, wallet_model]() {
156  if (!QApplication::activeModalWidget()) {
157  removeAndDeleteWallet(wallet_model);
158  }
159  }, Qt::QueuedConnection);
160  } else {
161  removeAndDeleteWallet(wallet_model);
162  }
163  }, Qt::QueuedConnection);
164 
165  // Re-emit coinsSent signal from wallet model.
166  connect(wallet_model, &WalletModel::coinsSent, this, &WalletController::coinsSent);
167 
168  Q_EMIT walletAdded(wallet_model);
169 
170  return wallet_model;
171 }
172 
174 {
175  // Unregister wallet model.
176  {
177  QMutexLocker locker(&m_mutex);
178  m_wallets.erase(std::remove(m_wallets.begin(), m_wallets.end(), wallet_model));
179  }
180  Q_EMIT walletRemoved(wallet_model);
181  // Currently this can trigger the unload since the model can hold the last
182  // CWallet shared pointer.
183  delete wallet_model;
184 }
185 
186 WalletControllerActivity::WalletControllerActivity(WalletController* wallet_controller, QWidget* parent_widget)
187  : QObject(wallet_controller)
188  , m_wallet_controller(wallet_controller)
189  , m_parent_widget(parent_widget)
190 {
191  connect(this, &WalletControllerActivity::finished, this, &QObject::deleteLater);
192 }
193 
194 void WalletControllerActivity::showProgressDialog(const QString& title_text, const QString& label_text, bool show_minimized)
195 {
196  auto progress_dialog = new QProgressDialog(m_parent_widget);
197  progress_dialog->setAttribute(Qt::WA_DeleteOnClose);
198  connect(this, &WalletControllerActivity::finished, progress_dialog, &QWidget::close);
199 
200  progress_dialog->setWindowTitle(title_text);
201  progress_dialog->setLabelText(label_text);
202  progress_dialog->setRange(0, 0);
203  progress_dialog->setCancelButton(nullptr);
204  progress_dialog->setWindowModality(Qt::ApplicationModal);
205  GUIUtil::PolishProgressDialog(progress_dialog);
206  // The setValue call forces QProgressDialog to start the internal duration estimation.
207  // See details in https://bugreports.qt.io/browse/QTBUG-47042.
208  progress_dialog->setValue(0);
209  // When requested, launch dialog minimized
210  if (show_minimized) progress_dialog->showMinimized();
211 }
212 
213 CreateWalletActivity::CreateWalletActivity(WalletController* wallet_controller, QWidget* parent_widget)
214  : WalletControllerActivity(wallet_controller, parent_widget)
215 {
217 }
218 
220 {
221  delete m_create_wallet_dialog;
222  delete m_passphrase_dialog;
223 }
224 
226 {
228  m_passphrase_dialog->setWindowModality(Qt::ApplicationModal);
229  m_passphrase_dialog->show();
230 
231  connect(m_passphrase_dialog, &QObject::destroyed, [this] {
232  m_passphrase_dialog = nullptr;
233  });
234  connect(m_passphrase_dialog, &QDialog::accepted, [this] {
235  createWallet();
236  });
237  connect(m_passphrase_dialog, &QDialog::rejected, [this] {
238  Q_EMIT finished();
239  });
240 }
241 
243 {
245  //: Title of window indicating the progress of creation of a new wallet.
246  tr("Create Wallet"),
247  /*: Descriptive text of the create wallet progress window which indicates
248  to the user which wallet is currently being created. */
249  tr("Creating Wallet <b>%1</b>…").arg(m_create_wallet_dialog->walletName().toHtmlEscaped()));
250 
251  std::string name = m_create_wallet_dialog->walletName().toStdString();
252  uint64_t flags = 0;
253  // Enable descriptors by default.
257  }
260  }
263  }
264 
265  QTimer::singleShot(500ms, worker(), [this, name, flags] {
267 
268  if (wallet) {
270  } else {
272  }
273 
274  QTimer::singleShot(500ms, this, &CreateWalletActivity::finish);
275  });
276 }
277 
279 {
280  if (!m_error_message.empty()) {
281  QMessageBox::critical(m_parent_widget, tr("Create wallet failed"), QString::fromStdString(m_error_message.translated));
282  } else if (!m_warning_message.empty()) {
283  QMessageBox::warning(m_parent_widget, tr("Create wallet warning"), QString::fromStdString(Join(m_warning_message, Untranslated("\n")).translated));
284  }
285 
287 
288  Q_EMIT finished();
289 }
290 
292 {
294 
295  std::vector<std::unique_ptr<interfaces::ExternalSigner>> signers;
296  try {
297  signers = node().listExternalSigners();
298  } catch (const std::runtime_error& e) {
299  QMessageBox::critical(nullptr, tr("Can't list signers"), e.what());
300  }
301  if (signers.size() > 1) {
302  QMessageBox::critical(nullptr, tr("Too many external signers found"), QString::fromStdString("More than one external signer found. Please connect only one at a time."));
303  signers.clear();
304  }
306 
307  m_create_wallet_dialog->setWindowModality(Qt::ApplicationModal);
308  m_create_wallet_dialog->show();
309 
310  connect(m_create_wallet_dialog, &QObject::destroyed, [this] {
311  m_create_wallet_dialog = nullptr;
312  });
313  connect(m_create_wallet_dialog, &QDialog::rejected, [this] {
314  Q_EMIT finished();
315  });
316  connect(m_create_wallet_dialog, &QDialog::accepted, [this] {
318  askPassphrase();
319  } else {
320  createWallet();
321  }
322  });
323 }
324 
325 OpenWalletActivity::OpenWalletActivity(WalletController* wallet_controller, QWidget* parent_widget)
326  : WalletControllerActivity(wallet_controller, parent_widget)
327 {
328 }
329 
331 {
332  if (!m_error_message.empty()) {
333  QMessageBox::critical(m_parent_widget, tr("Open wallet failed"), QString::fromStdString(m_error_message.translated));
334  } else if (!m_warning_message.empty()) {
335  QMessageBox::warning(m_parent_widget, tr("Open wallet warning"), QString::fromStdString(Join(m_warning_message, Untranslated("\n")).translated));
336  }
337 
338  if (m_wallet_model) Q_EMIT opened(m_wallet_model);
339 
340  Q_EMIT finished();
341 }
342 
343 void OpenWalletActivity::open(const std::string& path)
344 {
345  QString name = path.empty() ? QString("["+tr("default wallet")+"]") : QString::fromStdString(path);
346 
348  //: Title of window indicating the progress of opening of a wallet.
349  tr("Open Wallet"),
350  /*: Descriptive text of the open wallet progress window which indicates
351  to the user which wallet is currently being opened. */
352  tr("Opening Wallet <b>%1</b>…").arg(name.toHtmlEscaped()));
353 
354  QTimer::singleShot(0, worker(), [this, path] {
356 
357  if (wallet) {
359  } else {
361  }
362 
363  QTimer::singleShot(0, this, &OpenWalletActivity::finish);
364  });
365 }
366 
367 LoadWalletsActivity::LoadWalletsActivity(WalletController* wallet_controller, QWidget* parent_widget)
368  : WalletControllerActivity(wallet_controller, parent_widget)
369 {
370 }
371 
372 void LoadWalletsActivity::load(bool show_loading_minimized)
373 {
375  //: Title of progress window which is displayed when wallets are being loaded.
376  tr("Load Wallets"),
377  /*: Descriptive text of the load wallets progress window which indicates to
378  the user that wallets are currently being loaded.*/
379  tr("Loading wallets…"),
380  /*show_minimized=*/show_loading_minimized);
381 
382  QTimer::singleShot(0, worker(), [this] {
383  for (auto& wallet : node().walletLoader().getWallets()) {
385  }
386 
387  QTimer::singleShot(0, this, [this] { Q_EMIT finished(); });
388  });
389 }
390 
391 RestoreWalletActivity::RestoreWalletActivity(WalletController* wallet_controller, QWidget* parent_widget)
392  : WalletControllerActivity(wallet_controller, parent_widget)
393 {
394 }
395 
396 void RestoreWalletActivity::restore(const fs::path& backup_file, const std::string& wallet_name)
397 {
398  QString name = QString::fromStdString(wallet_name);
399 
401  //: Title of progress window which is displayed when wallets are being restored.
402  tr("Restore Wallet"),
403  /*: Descriptive text of the restore wallets progress window which indicates to
404  the user that wallets are currently being restored.*/
405  tr("Restoring Wallet <b>%1</b>…").arg(name.toHtmlEscaped()));
406 
407  QTimer::singleShot(0, worker(), [this, backup_file, wallet_name] {
408  auto wallet{node().walletLoader().restoreWallet(backup_file, wallet_name, m_warning_message)};
409 
410  if (wallet) {
412  } else {
414  }
415 
416  QTimer::singleShot(0, this, &RestoreWalletActivity::finish);
417  });
418 }
419 
421 {
422  if (!m_error_message.empty()) {
423  //: Title of message box which is displayed when the wallet could not be restored.
424  QMessageBox::critical(m_parent_widget, tr("Restore wallet failed"), QString::fromStdString(m_error_message.translated));
425  } else if (!m_warning_message.empty()) {
426  //: Title of message box which is displayed when the wallet is restored with some warning.
427  QMessageBox::warning(m_parent_widget, tr("Restore wallet warning"), QString::fromStdString(Join(m_warning_message, Untranslated("\n")).translated));
428  } else {
429  //: Title of message box which is displayed when the wallet is successfully restored.
430  QMessageBox::information(m_parent_widget, tr("Restore wallet message"), QString::fromStdString(Untranslated("Wallet restored successfully \n").translated));
431  }
432 
434 
435  Q_EMIT finished();
436 }
437 
439 {
440  // Warn the user about migration
441  QMessageBox box(m_parent_widget);
442  box.setWindowTitle(tr("Migrate wallet"));
443  box.setText(tr("Are you sure you wish to migrate the wallet <i>%1</i>?").arg(GUIUtil::HtmlEscape(wallet_model->getDisplayName())));
444  box.setInformativeText(tr("Migrating the wallet will convert this wallet to one or more descriptor wallets. A new wallet backup will need to be made.\n"
445  "If this wallet contains any watchonly scripts, a new wallet will be created which contains those watchonly scripts.\n"
446  "If this wallet contains any solvable but not watched scripts, a different and new wallet will be created which contains those scripts.\n\n"
447  "The migration process will create a backup of the wallet before migrating. This backup file will be named "
448  "<wallet name>-<timestamp>.legacy.bak and can be found in the directory for this wallet. In the event of "
449  "an incorrect migration, the backup can be restored with the \"Restore Wallet\" functionality."));
450  box.setStandardButtons(QMessageBox::Yes|QMessageBox::Cancel);
451  box.setDefaultButton(QMessageBox::Yes);
452  if (box.exec() != QMessageBox::Yes) return;
453 
454  // Get the passphrase if it is encrypted regardless of it is locked or unlocked. We need the passphrase itself.
455  SecureString passphrase;
456  WalletModel::EncryptionStatus enc_status = wallet_model->getEncryptionStatus();
457  if (enc_status == WalletModel::EncryptionStatus::Locked || enc_status == WalletModel::EncryptionStatus::Unlocked) {
459  dlg.setModel(wallet_model);
460  dlg.exec();
461  }
462 
463  // GUI needs to remove the wallet so that it can actually be unloaded by migration
464  const std::string name = wallet_model->wallet().getWalletName();
466 
467  showProgressDialog(tr("Migrate Wallet"), tr("Migrating Wallet <b>%1</b>…").arg(GUIUtil::HtmlEscape(name)));
468 
469  QTimer::singleShot(0, worker(), [this, name, passphrase] {
470  auto res{node().walletLoader().migrateWallet(name, passphrase)};
471 
472  if (res) {
473  m_success_message = tr("The wallet '%1' was migrated successfully.").arg(GUIUtil::HtmlEscape(res->wallet->getWalletName()));
474  if (res->watchonly_wallet_name) {
475  m_success_message += QChar(' ') + tr("Watchonly scripts have been migrated to a new wallet named '%1'.").arg(GUIUtil::HtmlEscape(res->watchonly_wallet_name.value()));
476  }
477  if (res->solvables_wallet_name) {
478  m_success_message += QChar(' ') + tr("Solvable but not watched scripts have been migrated to a new wallet named '%1'.").arg(GUIUtil::HtmlEscape(res->solvables_wallet_name.value()));
479  }
480  m_wallet_model = m_wallet_controller->getOrCreateWallet(std::move(res->wallet));
481  } else {
483  }
484 
485  QTimer::singleShot(0, this, &MigrateWalletActivity::finish);
486  });
487 }
488 
490 {
491  if (!m_error_message.empty()) {
492  QMessageBox::critical(m_parent_widget, tr("Migration failed"), QString::fromStdString(m_error_message.translated));
493  } else {
494  QMessageBox::information(m_parent_widget, tr("Migration Successful"), m_success_message);
495  }
496 
498 
499  Q_EMIT finished();
500 }
node::NodeContext m_node
Definition: bitcoin-gui.cpp:37
int flags
Definition: bitcoin-tx.cpp:530
Multifunctional dialog to ask for passphrases.
void setModel(WalletModel *model)
@ Unlock
Ask passphrase and unlock.
@ Encrypt
Ask passphrase twice and encrypt.
Model for Bitcoin network client.
Definition: clientmodel.h:54
AskPassphraseDialog * m_passphrase_dialog
CreateWalletDialog * m_create_wallet_dialog
CreateWalletActivity(WalletController *wallet_controller, QWidget *parent_widget)
void created(WalletModel *wallet_model)
Dialog for creating wallets.
bool isMakeBlankWalletChecked() const
QString walletName() const
bool isDisablePrivateKeysChecked() const
void setSigners(const std::vector< std::unique_ptr< interfaces::ExternalSigner >> &signers)
bool isEncryptWalletChecked() const
bool isExternalSignerChecked() const
void load(bool show_loading_minimized)
LoadWalletsActivity(WalletController *wallet_controller, QWidget *parent_widget)
void migrate(WalletModel *wallet_model)
void migrated(WalletModel *wallet_model)
void opened(WalletModel *wallet_model)
OpenWalletActivity(WalletController *wallet_controller, QWidget *parent_widget)
void open(const std::string &path)
void restore(const fs::path &backup_file, const std::string &wallet_name)
void restored(WalletModel *wallet_model)
RestoreWalletActivity(WalletController *wallet_controller, QWidget *parent_widget)
std::vector< bilingual_str > m_warning_message
QObject * worker() const
WalletController *const m_wallet_controller
interfaces::Node & node() const
void showProgressDialog(const QString &title_text, const QString &label_text, bool show_minimized=false)
WalletControllerActivity(WalletController *wallet_controller, QWidget *parent_widget)
QWidget *const m_parent_widget
Controller between interfaces::Node, WalletModel instances and the GUI.
WalletController(ClientModel &client_model, const PlatformStyle *platform_style, QObject *parent)
WalletModel * getOrCreateWallet(std::unique_ptr< interfaces::Wallet > wallet)
ClientModel & m_client_model
void removeAndDeleteWallet(WalletModel *wallet_model)
void walletAdded(WalletModel *wallet_model)
void closeAllWallets(QWidget *parent=nullptr)
std::unique_ptr< interfaces::Handler > m_handler_load_wallet
QThread *const m_activity_thread
std::map< std::string, bool > listWalletDir() const
Returns all wallet names in the wallet dir mapped to whether the wallet is loaded.
void coinsSent(WalletModel *wallet_model, SendCoinsRecipient recipient, QByteArray transaction)
QObject *const m_activity_worker
void walletRemoved(WalletModel *wallet_model)
const PlatformStyle *const m_platform_style
interfaces::Node & m_node
void closeWallet(WalletModel *wallet_model, QWidget *parent=nullptr)
std::vector< WalletModel * > m_wallets
Interface to Bitcoin wallet from Qt view code.
Definition: walletmodel.h:48
interfaces::Wallet & wallet() const
Definition: walletmodel.h:138
EncryptionStatus getEncryptionStatus() const
void coinsSent(WalletModel *wallet, SendCoinsRecipient recipient, QByteArray transaction)
QString getDisplayName() const
void unload()
Path class wrapper to block calls to the fs::path(std::string) implicit constructor and the fs::path:...
Definition: fs.h:33
virtual std::vector< std::unique_ptr< ExternalSigner > > listExternalSigners()=0
Return list of external signers (attached devices which can sign transactions).
virtual WalletLoader & walletLoader()=0
Get wallet loader.
virtual std::string getWalletName()=0
Get wallet name.
virtual void remove()=0
virtual util::Result< std::unique_ptr< Wallet > > createWallet(const std::string &name, const SecureString &passphrase, uint64_t wallet_creation_flags, std::vector< bilingual_str > &warnings)=0
Create new wallet.
virtual std::vector< std::string > listWalletDir()=0
Return available wallets in wallet directory.
virtual util::Result< std::unique_ptr< Wallet > > loadWallet(const std::string &name, std::vector< bilingual_str > &warnings)=0
Load existing wallet.
virtual util::Result< WalletMigrationResult > migrateWallet(const std::string &name, const SecureString &passphrase)=0
Migrate a wallet.
virtual util::Result< std::unique_ptr< Wallet > > restoreWallet(const fs::path &backup_file, const std::string &wallet_name, std::vector< bilingual_str > &warnings)=0
Restore backup wallet.
virtual std::unique_ptr< Handler > handleLoadWallet(LoadWalletFn fn)=0
static const int MAX_PASSPHRASE_SIZE
Definition: guiconstants.h:20
Qt::ConnectionType blockingGUIThreadConnection()
Get connection type to call object slot in GUI thread with invokeMethod.
Definition: guiutil.cpp:381
QString HtmlEscape(const QString &str, bool fMultiLine)
Definition: guiutil.cpp:252
void PolishProgressDialog(QProgressDialog *dialog)
Definition: guiutil.cpp:897
Definition: init.h:25
bilingual_str ErrorString(const Result< T > &result)
Definition: result.h:81
void ThreadRename(std::string &&)
Rename a thread both in terms of an internal (in-memory) name as well as its system thread name.
Definition: threadnames.cpp:59
@ WALLET_FLAG_EXTERNAL_SIGNER
Indicates that the wallet needs an external signer.
Definition: walletutil.h:77
@ WALLET_FLAG_DESCRIPTORS
Indicate that this wallet supports DescriptorScriptPubKeyMan.
Definition: walletutil.h:74
@ WALLET_FLAG_DISABLE_PRIVATE_KEYS
Definition: walletutil.h:51
@ WALLET_FLAG_BLANK_WALLET
Flag set when a wallet contains no HD seed and no private keys, scripts, addresses,...
Definition: walletutil.h:71
const char * name
Definition: rest.cpp:50
std::basic_string< char, std::char_traits< char >, secure_allocator< char > > SecureString
Definition: secure.h:58
auto Join(const C &container, const S &separator, UnaryOp unary_op)
Join all container items.
Definition: string.h:69
bool empty() const
Definition: translation.h:29
std::string translated
Definition: translation.h:20
bilingual_str Untranslated(std::string original)
Mark a bilingual_str as untranslated.
Definition: translation.h:48
assert(!tx.IsCoinBase())