mirror of
https://github.com/feather-wallet/feather.git
synced 2024-11-16 17:27:38 +00:00
receive: improve wallet file corruption detection
This commit is contained in:
parent
4756f0dbe7
commit
f97fa90521
11 changed files with 180 additions and 50 deletions
|
@ -1591,7 +1591,12 @@ void MainWindow::onInitiateTransaction() {
|
||||||
void MainWindow::onKeysCorrupted() {
|
void MainWindow::onKeysCorrupted() {
|
||||||
if (!m_criticalWarningShown) {
|
if (!m_criticalWarningShown) {
|
||||||
m_criticalWarningShown = true;
|
m_criticalWarningShown = true;
|
||||||
Utils::showError(this, "Wallet keys are corrupted", "WARNING!\n\nTo prevent LOSS OF FUNDS do NOT continue to use this wallet file.\n\nRestore your wallet from seed.\n\nPlease report this incident to the Feather developers.\n\nWARNING!");
|
Utils::showError(this, "Potential wallet file corruption detected",
|
||||||
|
"WARNING!\n\n"
|
||||||
|
"To prevent LOSS OF FUNDS do NOT continue to use this wallet file.\n\n"
|
||||||
|
"Restore your wallet from seed, keys, or device.\n\n"
|
||||||
|
"Please report this incident to the Feather developers.\n\n"
|
||||||
|
"WARNING!", {}, "report_an_issue");
|
||||||
m_sendWidget->disallowSending();
|
m_sendWidget->disallowSending();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,7 +30,7 @@ ReceiveWidget::ReceiveWidget(Wallet *wallet, QWidget *parent)
|
||||||
ui->addresses->header()->setSectionResizeMode(SubaddressModel::Address, QHeaderView::ResizeToContents);
|
ui->addresses->header()->setSectionResizeMode(SubaddressModel::Address, QHeaderView::ResizeToContents);
|
||||||
ui->addresses->header()->setSectionResizeMode(SubaddressModel::Label, QHeaderView::Stretch);
|
ui->addresses->header()->setSectionResizeMode(SubaddressModel::Label, QHeaderView::Stretch);
|
||||||
|
|
||||||
connect(ui->addresses->selectionModel(), &QItemSelectionModel::currentChanged, [=](QModelIndex current, QModelIndex prev){
|
connect(ui->addresses->selectionModel(), &QItemSelectionModel::selectionChanged, [=](const QItemSelection &selected, const QItemSelection &deselected){
|
||||||
this->updateQrCode();
|
this->updateQrCode();
|
||||||
});
|
});
|
||||||
connect(m_model, &SubaddressModel::modelReset, [this](){
|
connect(m_model, &SubaddressModel::modelReset, [this](){
|
||||||
|
@ -74,12 +74,12 @@ ReceiveWidget::ReceiveWidget(Wallet *wallet, QWidget *parent)
|
||||||
m_showTransactionsAction = new QAction("Show transactions");
|
m_showTransactionsAction = new QAction("Show transactions");
|
||||||
connect(m_showTransactionsAction, &QAction::triggered, this, &ReceiveWidget::onShowTransactions);
|
connect(m_showTransactionsAction, &QAction::triggered, this, &ReceiveWidget::onShowTransactions);
|
||||||
connect(ui->addresses, &QTreeView::customContextMenuRequested, this, &ReceiveWidget::showContextMenu);
|
connect(ui->addresses, &QTreeView::customContextMenuRequested, this, &ReceiveWidget::showContextMenu);
|
||||||
|
connect(ui->addresses, &SubaddressView::copyAddress, this, &ReceiveWidget::copyAddress);
|
||||||
connect(ui->btn_generateSubaddress, &QPushButton::clicked, this, &ReceiveWidget::generateSubaddress);
|
|
||||||
|
|
||||||
connect(ui->qrCode, &ClickableLabel::clicked, this, &ReceiveWidget::showQrCodeDialog);
|
connect(ui->qrCode, &ClickableLabel::clicked, this, &ReceiveWidget::showQrCodeDialog);
|
||||||
connect(ui->search, &QLineEdit::textChanged, this, &ReceiveWidget::setSearchFilter);
|
connect(ui->search, &QLineEdit::textChanged, this, &ReceiveWidget::setSearchFilter);
|
||||||
|
|
||||||
|
connect(ui->btn_generateSubaddress, &QPushButton::clicked, this, &ReceiveWidget::generateSubaddress);
|
||||||
connect(ui->btn_createPaymentRequest, &QPushButton::clicked, this, &ReceiveWidget::createPaymentRequest);
|
connect(ui->btn_createPaymentRequest, &QPushButton::clicked, this, &ReceiveWidget::createPaymentRequest);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -104,9 +104,32 @@ void ReceiveWidget::focusSearchbar() {
|
||||||
ui->search->setFocus();
|
ui->search->setFocus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QString ReceiveWidget::getAddress(quint32 minorIndex) {
|
||||||
|
bool ok;
|
||||||
|
QString reason;
|
||||||
|
QString address = m_wallet->getAddressSafe(m_wallet->currentSubaddressAccount(), minorIndex, ok, reason);
|
||||||
|
|
||||||
|
if (!ok) {
|
||||||
|
Utils::showError(this, "Unable to get address",
|
||||||
|
QString("Reason: %1\n\n"
|
||||||
|
"WARNING!\n\n"
|
||||||
|
"Potential wallet file corruption detected.\n\n"
|
||||||
|
"To prevent LOSS OF FUNDS do NOT continue to use this wallet file.\n\n"
|
||||||
|
"Restore your wallet from seed, keys, or device.\n\n"
|
||||||
|
"Please report this incident to the Feather developers.\n\n"
|
||||||
|
"WARNING!").arg(reason), {}, "report_an_issue");
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return address;
|
||||||
|
}
|
||||||
|
|
||||||
void ReceiveWidget::copyAddress() {
|
void ReceiveWidget::copyAddress() {
|
||||||
QModelIndex index = ui->addresses->currentIndex();
|
SubaddressRow* row = this->currentEntry();
|
||||||
Utils::copyColumn(&index, SubaddressModel::Address);
|
if (!row) return;
|
||||||
|
|
||||||
|
QString address = this->getAddress(row->getRow());
|
||||||
|
Utils::copyToClipboard(address);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ReceiveWidget::copyLabel() {
|
void ReceiveWidget::copyLabel() {
|
||||||
|
@ -115,7 +138,7 @@ void ReceiveWidget::copyLabel() {
|
||||||
}
|
}
|
||||||
|
|
||||||
void ReceiveWidget::editLabel() {
|
void ReceiveWidget::editLabel() {
|
||||||
QModelIndex index = ui->addresses->currentIndex().siblingAtColumn(m_model->ModelColumn::Label);
|
QModelIndex index = ui->addresses->currentIndex().siblingAtColumn(SubaddressModel::ModelColumn::Label);
|
||||||
ui->addresses->setCurrentIndex(index);
|
ui->addresses->setCurrentIndex(index);
|
||||||
ui->addresses->edit(index);
|
ui->addresses->edit(index);
|
||||||
}
|
}
|
||||||
|
@ -164,24 +187,20 @@ void ReceiveWidget::showContextMenu(const QPoint &point) {
|
||||||
}
|
}
|
||||||
|
|
||||||
void ReceiveWidget::createPaymentRequest() {
|
void ReceiveWidget::createPaymentRequest() {
|
||||||
QModelIndex index = ui->addresses->currentIndex();
|
SubaddressRow* row = this->currentEntry();
|
||||||
if (!index.isValid()) {
|
if (!row) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString address = index.model()->data(index.siblingAtColumn(SubaddressModel::Address), Qt::UserRole).toString();
|
QString address = this->getAddress(row->getRow());
|
||||||
|
|
||||||
PaymentRequestDialog dialog{this, m_wallet, address};
|
PaymentRequestDialog dialog{this, m_wallet, address};
|
||||||
dialog.exec();
|
dialog.exec();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ReceiveWidget::onShowTransactions() {
|
void ReceiveWidget::onShowTransactions() {
|
||||||
QModelIndex index = ui->addresses->currentIndex();
|
SubaddressRow* row = this->currentEntry();
|
||||||
if (!index.isValid()) {
|
if (!row) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString address = index.model()->data(index.siblingAtColumn(SubaddressModel::Address), Qt::UserRole).toString();
|
QString address = this->getAddress(row->getRow());
|
||||||
emit showTransactions(address);
|
emit showTransactions(address);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -209,14 +228,14 @@ void ReceiveWidget::generateSubaddress() {
|
||||||
}
|
}
|
||||||
|
|
||||||
void ReceiveWidget::updateQrCode(){
|
void ReceiveWidget::updateQrCode(){
|
||||||
QModelIndex index = ui->addresses->currentIndex();
|
SubaddressRow* row = this->currentEntry();
|
||||||
if (!index.isValid()) {
|
if (!row) {
|
||||||
ui->qrCode->clear();
|
ui->qrCode->clear();
|
||||||
ui->btn_createPaymentRequest->hide();
|
ui->btn_createPaymentRequest->hide();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
QString address = index.model()->data(index.siblingAtColumn(SubaddressModel::Address), Qt::UserRole).toString();
|
QString address = this->getAddress(row->getRow());
|
||||||
const QrCode qrc(address, QrCode::Version::AUTO, QrCode::ErrorCorrectionLevel::MEDIUM);
|
const QrCode qrc(address, QrCode::Version::AUTO, QrCode::ErrorCorrectionLevel::MEDIUM);
|
||||||
|
|
||||||
int width = ui->qrCode->width() - 4;
|
int width = ui->qrCode->width() - 4;
|
||||||
|
@ -227,11 +246,10 @@ void ReceiveWidget::updateQrCode(){
|
||||||
}
|
}
|
||||||
|
|
||||||
void ReceiveWidget::showQrCodeDialog() {
|
void ReceiveWidget::showQrCodeDialog() {
|
||||||
QModelIndex index = ui->addresses->currentIndex();
|
SubaddressRow* row = this->currentEntry();
|
||||||
if (!index.isValid()) {
|
if (!row) return;
|
||||||
return;
|
|
||||||
}
|
QString address = this->getAddress(row->getRow());
|
||||||
QString address = index.model()->data(index.siblingAtColumn(SubaddressModel::Address), Qt::UserRole).toString();
|
|
||||||
QrCode qr(address, QrCode::Version::AUTO, QrCode::ErrorCorrectionLevel::HIGH);
|
QrCode qr(address, QrCode::Version::AUTO, QrCode::ErrorCorrectionLevel::HIGH);
|
||||||
QrCodeDialog dialog{this, &qr, "Address"};
|
QrCodeDialog dialog{this, &qr, "Address"};
|
||||||
dialog.exec();
|
dialog.exec();
|
||||||
|
|
|
@ -31,6 +31,7 @@ public:
|
||||||
void focusSearchbar();
|
void focusSearchbar();
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
|
QString getAddress(quint32 minorIndex);
|
||||||
void copyAddress();
|
void copyAddress();
|
||||||
void copyLabel();
|
void copyLabel();
|
||||||
void editLabel();
|
void editLabel();
|
||||||
|
|
|
@ -120,33 +120,53 @@ bool Subaddress::refresh(quint32 accountIndex)
|
||||||
emit refreshStarted();
|
emit refreshStarted();
|
||||||
|
|
||||||
this->clearRows();
|
this->clearRows();
|
||||||
for (qsizetype i = 0; i < m_wallet2->get_num_subaddresses(accountIndex); ++i)
|
|
||||||
|
bool potentialWalletFileCorruption = false;
|
||||||
|
|
||||||
|
for (quint32 i = 0; i < m_wallet2->get_num_subaddresses(accountIndex); ++i)
|
||||||
{
|
{
|
||||||
QString address = QString::fromStdString(m_wallet2->get_subaddress_as_str({accountIndex, (uint32_t)i}));
|
cryptonote::subaddress_index index = {accountIndex, i};
|
||||||
|
cryptonote::account_public_address address = m_wallet2->get_subaddress(index);
|
||||||
|
|
||||||
|
// Make sure we have previously generated Di
|
||||||
|
auto idx = m_wallet2->get_subaddress_index(address);
|
||||||
|
if (!idx) {
|
||||||
|
potentialWalletFileCorruption = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify mapping
|
||||||
|
if (idx != index) {
|
||||||
|
potentialWalletFileCorruption = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString addressStr = QString::fromStdString(cryptonote::get_account_address_as_str(m_wallet2->nettype(), !index.is_zero(), address));
|
||||||
|
|
||||||
auto* row = new SubaddressRow{this,
|
auto* row = new SubaddressRow{this,
|
||||||
i,
|
i,
|
||||||
address,
|
addressStr,
|
||||||
QString::fromStdString(m_wallet2->get_subaddress_label({accountIndex, (uint32_t)i})),
|
QString::fromStdString(m_wallet2->get_subaddress_label(index)),
|
||||||
m_wallet2->get_subaddress_used({accountIndex, (uint32_t)i}),
|
m_wallet2->get_subaddress_used({accountIndex, (uint32_t)i}),
|
||||||
this->isHidden(address),
|
this->isHidden(addressStr),
|
||||||
this->isPinned(address)
|
this->isPinned(addressStr)
|
||||||
};
|
};
|
||||||
|
|
||||||
m_rows.append(row);
|
m_rows.append(row);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure keys are intact. We NEVER want to display incorrect addresses in case of memory corruption.
|
// Make sure keys are intact. We NEVER want to display incorrect addresses in case of memory corruption.
|
||||||
bool keysCorrupt = m_wallet2->get_device_type() == hw::device::SOFTWARE && !m_wallet2->verify_keys();
|
potentialWalletFileCorruption = potentialWalletFileCorruption || (m_wallet2->get_device_type() == hw::device::SOFTWARE && !m_wallet2->verify_keys());
|
||||||
|
|
||||||
if (keysCorrupt) {
|
if (potentialWalletFileCorruption) {
|
||||||
clearRows();
|
|
||||||
LOG_ERROR("KEY INCONSISTENCY DETECTED, WALLET IS IN CORRUPT STATE.");
|
LOG_ERROR("KEY INCONSISTENCY DETECTED, WALLET IS IN CORRUPT STATE.");
|
||||||
|
clearRows();
|
||||||
|
emit corrupted();
|
||||||
}
|
}
|
||||||
|
|
||||||
emit refreshFinished();
|
emit refreshFinished();
|
||||||
|
|
||||||
return !keysCorrupt;
|
return !potentialWalletFileCorruption;
|
||||||
}
|
}
|
||||||
|
|
||||||
qsizetype Subaddress::count() const
|
qsizetype Subaddress::count() const
|
||||||
|
|
|
@ -46,6 +46,7 @@ public:
|
||||||
signals:
|
signals:
|
||||||
void refreshStarted() const;
|
void refreshStarted() const;
|
||||||
void refreshFinished() const;
|
void refreshFinished() const;
|
||||||
|
void corrupted() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
explicit Subaddress(Wallet *wallet, tools::wallet2 *wallet2, QObject *parent);
|
explicit Subaddress(Wallet *wallet, tools::wallet2 *wallet2, QObject *parent);
|
||||||
|
|
|
@ -82,6 +82,10 @@ Wallet::Wallet(Monero::Wallet *wallet, QObject *parent)
|
||||||
connect(this, &Wallet::updated, this, &Wallet::onUpdated);
|
connect(this, &Wallet::updated, this, &Wallet::onUpdated);
|
||||||
connect(this, &Wallet::heightsRefreshed, this, &Wallet::onHeightsRefreshed);
|
connect(this, &Wallet::heightsRefreshed, this, &Wallet::onHeightsRefreshed);
|
||||||
connect(this, &Wallet::transactionCommitted, this, &Wallet::onTransactionCommitted);
|
connect(this, &Wallet::transactionCommitted, this, &Wallet::onTransactionCommitted);
|
||||||
|
|
||||||
|
connect(m_subaddress, &Subaddress::corrupted, [this]{
|
||||||
|
emit keysCorrupted();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// #################### Status ####################
|
// #################### Status ####################
|
||||||
|
@ -185,6 +189,85 @@ QString Wallet::address(quint32 accountIndex, quint32 addressIndex) const {
|
||||||
return QString::fromStdString(m_wallet2->get_subaddress_as_str({accountIndex, addressIndex}));
|
return QString::fromStdString(m_wallet2->get_subaddress_as_str({accountIndex, addressIndex}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QString Wallet::getAddressSafe(quint32 accountIndex, quint32 addressIndex, bool &ok, QString &reason) const {
|
||||||
|
ok = false;
|
||||||
|
|
||||||
|
// If we copy an address to clipboard or create a QR code, there must not be a spark of doubt that
|
||||||
|
// the address belongs to our wallet.
|
||||||
|
|
||||||
|
// This function does a number of sanity checks, some seemingly unnecessary or redundant to ensure
|
||||||
|
// that, yes, we own this address. It provides basic protection against memory corruption errors.
|
||||||
|
|
||||||
|
if (accountIndex >= this->numSubaddressAccounts()) {
|
||||||
|
reason = "Account index exceeds number of pre-computed subaddress accounts";
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (addressIndex >= this->numSubaddresses(accountIndex)) {
|
||||||
|
reason = "Address index exceeds number of pre-computed subaddresses";
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Realistically, nobody will have more than 1M subaddresses or accounts in their wallet.
|
||||||
|
if (accountIndex >= 1000000) {
|
||||||
|
reason = "Account index exceeds safety limit";
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (addressIndex >= 1000000) {
|
||||||
|
reason = "Address index exceeds safety limit";
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// subaddress public spendkey (Di) = Hs(secret viewkey || subaddress index)G + primary address public spendkey (B)
|
||||||
|
// subaddress public viewkey (Ci) = D * secret viewkey (a)
|
||||||
|
|
||||||
|
if (!m_wallet2->verify_keys()) {
|
||||||
|
reason = "Unable to verify viewkey";
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
cryptonote::subaddress_index index = {accountIndex, addressIndex};
|
||||||
|
cryptonote::account_public_address address = m_wallet2->get_subaddress(index);
|
||||||
|
|
||||||
|
// Make sure we have previously generated Di
|
||||||
|
auto idx = m_wallet2->get_subaddress_index(address);
|
||||||
|
if (!idx) {
|
||||||
|
reason = "No mapping found for this subaddress public spendkey";
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify mapping
|
||||||
|
if (idx != index) {
|
||||||
|
reason = "Invalid subaddress public spendkey mapping";
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recompute address
|
||||||
|
cryptonote::account_public_address address2 = m_wallet2->get_subaddress(idx.value());
|
||||||
|
if (address != address2) {
|
||||||
|
reason = "Recomputed address does not match original address";
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string address_str = m_wallet2->get_subaddress_as_str(index);
|
||||||
|
|
||||||
|
// Make sure address is parseable
|
||||||
|
cryptonote::address_parse_info info;
|
||||||
|
if (!cryptonote::get_account_address_from_str(info, m_wallet2->nettype(), address_str)) {
|
||||||
|
reason = "Unable to parse address";
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (info.address != address) {
|
||||||
|
reason = "Parsed address does not match original address";
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
ok = true;
|
||||||
|
return QString::fromStdString(address_str);
|
||||||
|
}
|
||||||
|
|
||||||
SubaddressIndex Wallet::subaddressIndex(const QString &address) const {
|
SubaddressIndex Wallet::subaddressIndex(const QString &address) const {
|
||||||
std::pair<uint32_t, uint32_t> i;
|
std::pair<uint32_t, uint32_t> i;
|
||||||
if (!m_walletImpl->subaddressIndex(address.toStdString(), i)) {
|
if (!m_walletImpl->subaddressIndex(address.toStdString(), i)) {
|
||||||
|
@ -479,14 +562,7 @@ void Wallet::onRefreshed(bool success, const QString &message) {
|
||||||
void Wallet::refreshModels() {
|
void Wallet::refreshModels() {
|
||||||
m_history->refresh();
|
m_history->refresh();
|
||||||
m_coins->refresh();
|
m_coins->refresh();
|
||||||
bool r = this->subaddress()->refresh(this->currentSubaddressAccount());
|
this->subaddress()->refresh(this->currentSubaddressAccount());
|
||||||
|
|
||||||
if (!r) {
|
|
||||||
// This should only happen if wallet keys got corrupted or were tampered with
|
|
||||||
// The list of subaddresses is wiped to prevent loss of funds
|
|
||||||
// Notify MainWindow to display an error message
|
|
||||||
emit keysCorrupted();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// #################### Hardware wallet ####################
|
// #################### Hardware wallet ####################
|
||||||
|
|
|
@ -154,8 +154,8 @@ public:
|
||||||
void updateBalance();
|
void updateBalance();
|
||||||
|
|
||||||
// ##### Subaddresses and Accounts #####
|
// ##### Subaddresses and Accounts #####
|
||||||
//! returns wallet's public address
|
|
||||||
QString address(quint32 accountIndex, quint32 addressIndex) const;
|
QString address(quint32 accountIndex, quint32 addressIndex) const;
|
||||||
|
QString getAddressSafe(quint32 accountIndex, quint32 addressIndex, bool &ok, QString &reason) const;
|
||||||
|
|
||||||
//! returns the subaddress index of the address
|
//! returns the subaddress index of the address
|
||||||
SubaddressIndex subaddressIndex(const QString &address) const;
|
SubaddressIndex subaddressIndex(const QString &address) const;
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
#include "SubaddressRow.h"
|
#include "SubaddressRow.h"
|
||||||
|
|
||||||
qsizetype SubaddressRow::getRow() const {
|
quint32 SubaddressRow::getRow() const {
|
||||||
return m_row;
|
return m_row;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ class SubaddressRow : public QObject
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
SubaddressRow(QObject *parent, qsizetype row, const QString& address, const QString &label, bool used, bool hidden, bool pinned)
|
SubaddressRow(QObject *parent, quint32 row, const QString& address, const QString &label, bool used, bool hidden, bool pinned)
|
||||||
: QObject(parent)
|
: QObject(parent)
|
||||||
, m_row(row)
|
, m_row(row)
|
||||||
, m_address(address)
|
, m_address(address)
|
||||||
|
@ -20,7 +20,7 @@ public:
|
||||||
, m_hidden(hidden)
|
, m_hidden(hidden)
|
||||||
, m_pinned(pinned) {}
|
, m_pinned(pinned) {}
|
||||||
|
|
||||||
qsizetype getRow() const;
|
[[nodiscard]] quint32 getRow() const;
|
||||||
const QString& getAddress() const;
|
const QString& getAddress() const;
|
||||||
const QString& getLabel() const;
|
const QString& getLabel() const;
|
||||||
bool isUsed() const;
|
bool isUsed() const;
|
||||||
|
@ -28,7 +28,7 @@ public:
|
||||||
bool isPinned() const;
|
bool isPinned() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
qsizetype m_row;
|
quint32 m_row;
|
||||||
QString m_address;
|
QString m_address;
|
||||||
QString m_label;
|
QString m_label;
|
||||||
bool m_used = false;
|
bool m_used = false;
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
#include "SubaddressView.h"
|
#include "SubaddressView.h"
|
||||||
|
|
||||||
#include "utils/Utils.h"
|
#include "utils/Utils.h"
|
||||||
|
#include "SubaddressModel.h"
|
||||||
|
|
||||||
SubaddressView::SubaddressView(QWidget *parent) : QTreeView(parent) {
|
SubaddressView::SubaddressView(QWidget *parent) : QTreeView(parent) {
|
||||||
|
|
||||||
|
@ -15,8 +16,12 @@ void SubaddressView::keyPressEvent(QKeyEvent *event){
|
||||||
if(!selectedIndexes().isEmpty()){
|
if(!selectedIndexes().isEmpty()){
|
||||||
if(event->matches(QKeySequence::Copy)){
|
if(event->matches(QKeySequence::Copy)){
|
||||||
QModelIndex index = this->currentIndex();
|
QModelIndex index = this->currentIndex();
|
||||||
|
if (index.column() == SubaddressModel::ModelColumn::Address) {
|
||||||
|
emit copyAddress();
|
||||||
|
} else {
|
||||||
Utils::copyColumn(&index, index.column());
|
Utils::copyColumn(&index, index.column());
|
||||||
}
|
}
|
||||||
|
}
|
||||||
else
|
else
|
||||||
QTreeView::keyPressEvent(event);
|
QTreeView::keyPressEvent(event);
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,10 +10,14 @@
|
||||||
|
|
||||||
class SubaddressView : public QTreeView
|
class SubaddressView : public QTreeView
|
||||||
{
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
SubaddressView(QWidget* parent = nullptr);
|
SubaddressView(QWidget* parent = nullptr);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void copyAddress();
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void keyPressEvent(QKeyEvent *event);
|
void keyPressEvent(QKeyEvent *event);
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue