diff --git a/src/interfaces/wallet.cpp b/src/interfaces/wallet.cpp index 0e8852301ca8..6eb060e04c53 100644 --- a/src/interfaces/wallet.cpp +++ b/src/interfaces/wallet.cpp @@ -211,6 +211,11 @@ class WalletImpl : public Wallet LOCK2(cs_main, m_wallet.cs_wallet); return m_wallet.IsLockedCoin(output.hash, output.n); } + bool isSpent(const uint256& hash, unsigned int n) override + { + LOCK2(cs_main, m_wallet.cs_wallet); + return m_wallet.IsSpent(hash, n); + } void listLockedCoins(std::vector& outputs) override { LOCK2(cs_main, m_wallet.cs_wallet); diff --git a/src/interfaces/wallet.h b/src/interfaces/wallet.h index d6899f2aa565..70a945f7439e 100644 --- a/src/interfaces/wallet.h +++ b/src/interfaces/wallet.h @@ -128,6 +128,9 @@ class Wallet //! Return whether coin is locked. virtual bool isLockedCoin(const COutPoint& output) = 0; + //! Return whether coin is Spent. + virtual bool isSpent(const uint256& hash, unsigned int n) = 0; + //! List locked coins. virtual void listLockedCoins(std::vector& outputs) = 0; diff --git a/src/kernelrecord.cpp b/src/kernelrecord.cpp index 5a1223474db5..c67af1df59d9 100644 --- a/src/kernelrecord.cpp +++ b/src/kernelrecord.cpp @@ -23,19 +23,21 @@ bool KernelRecord::showTransaction() /* * Decompose CWallet transaction to model kernel records. */ -vector KernelRecord::decomposeOutput(const COutPoint& output, const interfaces::WalletTxOut& out) +vector KernelRecord::decomposeOutput(const interfaces::WalletTx& wtx) { + uint256 hash = wtx.tx->GetHash(); + const std::vector outs = wtx.tx->vout; + std::vector isMine = wtx.txout_is_mine; vector parts; - uint256 hash = output.hash; - uint32_t n = output.n; - int64_t nTime = out.time; - int64_t nValue = out.txout.nValue; - CTxDestination address; - std::string addrStr; - ExtractDestination(out.txout.scriptPubKey, address); - addrStr = EncodeDestination(address); - - parts.push_back(KernelRecord(hash, n, nTime, addrStr, nValue)); + int64_t nTime = wtx.time; + + for(uint32_t n = 0; n < outs.size(); n++){ + if(isMine[n] == isminetype::ISMINE_SPENDABLE){ + int64_t nValue = outs[n].nValue; + std::string addrStr = EncodeDestination(wtx.txout_address[n]); + parts.emplace_back(hash, n, nTime, addrStr, nValue); + } + } return parts; } diff --git a/src/kernelrecord.h b/src/kernelrecord.h index bc8bce6afb22..72a5eab7c185 100644 --- a/src/kernelrecord.h +++ b/src/kernelrecord.h @@ -31,8 +31,7 @@ class KernelRecord } static bool showTransaction(); - static std::vector decomposeOutput(const COutPoint& output, const interfaces::WalletTxOut& out); - + static std::vector decomposeOutput(const interfaces::WalletTx& wtx); uint256 hash; uint32_t n; diff --git a/src/qt/guiconstants.h b/src/qt/guiconstants.h index 323f50158282..7f0fdcb92ff6 100644 --- a/src/qt/guiconstants.h +++ b/src/qt/guiconstants.h @@ -8,9 +8,6 @@ /* Milliseconds between model updates */ static const int MODEL_UPDATE_DELAY = 250; -/* Milliseconds between mintingtablemodel updates */ -static const int MODEL_MINTING_UPDATE_DELAY = 1000; - /* AskPassphraseDialog -- Maximum passphrase length */ static const int MAX_PASSPHRASE_SIZE = 1024; diff --git a/src/qt/mintingtablemodel.cpp b/src/qt/mintingtablemodel.cpp index d5f1194620a5..6079f6d7d004 100644 --- a/src/qt/mintingtablemodel.cpp +++ b/src/qt/mintingtablemodel.cpp @@ -61,8 +61,28 @@ struct TxLessThan { return a.hash < b.hash || (a.hash == b.hash && a.n < b.n); } + bool operator()(const KernelRecord &a, const std::pair &b) const + { + return a.hash < b.first || (a.hash == b.first && a.n < b.second); + } + bool operator()(const std::pair &a, const KernelRecord &b) const + { + return a.first < b.hash || (a.first == b.hash && a.second < b.n); + } + bool operator()(const KernelRecord &a, const uint256 &b) const + { + return a.hash < b; + } + bool operator()(const uint256 &a, const KernelRecord &b) const + { + return a < b.hash; + } }; +bool compareWtx(const interfaces::WalletTx &a, const interfaces::WalletTx &b) +{ + return a.time < b.time; +} // Private implementation class MintingTablePriv { @@ -79,66 +99,131 @@ class MintingTablePriv * this is sorted by sha256. */ QList cachedWallet; + QList> procQueue; /* Query entire wallet anew from core. */ - void refreshWallet(interfaces::Wallet& wallet) + void refreshWallet() { qDebug() << "MintingTablePriv::refreshWallet"; - // Make mask - QList mask; - for(int i = 0; i < cachedWallet.size(); i++) + int dequeue = std::min(procQueue.size(), 100); // limit 100 + while(dequeue-- > 0) { - mask.append(false); + // dequeue one event + std::pair pair = procQueue[0]; + procEvent(pair.first, pair.second); + procQueue.removeAt(0); + } + } + + void procEvent(uint256 hash, int status) + { + // Find bounds of this transaction in model + QList::iterator lower = qLowerBound( + cachedWallet.begin(), cachedWallet.end(), hash, TxLessThan()); + QList::iterator upper = qUpperBound( + cachedWallet.begin(), cachedWallet.end(), hash, TxLessThan()); + int lowerIndex = (lower - cachedWallet.begin()); + int upperIndex = (upper - cachedWallet.begin()); + + interfaces::WalletTx wtx = parent->walletModel->wallet().getWalletTx(hash); + interfaces::WalletTxStatus tx_status; + int num_blocks; + int64_t block_time; + bool success = parent->walletModel->wallet().tryGetTxStatus(hash, tx_status, num_blocks, block_time); + + if(!success){ + // not found status + return; } - // Update and add records - for (const auto& coins : wallet.listCoins()) + if(status == CT_NEW) { - for (const auto& outpair : coins.second) + // requeue if the transaction is coinbase and immature + if(tx_status.is_coinbase && tx_status.blocks_to_maturity > 0) { - const COutPoint& output = std::get<0>(outpair); - const interfaces::WalletTxOut& out = std::get<1>(outpair); - std::vector txList = KernelRecord::decomposeOutput(output, out); - if(KernelRecord::showTransaction()) - { + procQueue.append(std::make_pair(hash, status)); + return; + } - for(const KernelRecord& kr : txList) - { - QList::iterator lower = qLowerBound( - cachedWallet.begin(), cachedWallet.end(), kr, TxLessThan()); - QList::iterator upper = qUpperBound( - cachedWallet.begin(), cachedWallet.end(), kr, TxLessThan()); - int lowerIndex = (lower - cachedWallet.begin()); - bool inModel = (lower != upper); - - if(inModel) - { - cachedWallet.replace(lowerIndex, kr); - mask.replace(lowerIndex, true); - } - else - { - parent->beginInsertRows(QModelIndex(), lowerIndex, lowerIndex); - cachedWallet.insert(lowerIndex, kr); - mask.insert(lowerIndex, true); - parent->endInsertRows(); + // spent + const std::vector ins = wtx.tx->vin; + const std::vector isMine = wtx.txin_is_mine; + for(uint32_t i = 0; i < ins.size(); i++) + { + if(isMine[i] == isminetype::ISMINE_SPENDABLE) + { + uint256 phash = ins[i].prevout.hash; + uint32_t n = ins[i].prevout.n; + + for(int i = 0; i < cachedWallet.size(); i++){ + if(cachedWallet[i].hash == phash && cachedWallet[i].n == n){ + parent->beginRemoveRows(QModelIndex(), i, i); + cachedWallet.removeAt(i); + parent->endRemoveRows(); + break; } } } } + + // append + int offsetLower = 0; + for(const KernelRecord& kr : KernelRecord::decomposeOutput(wtx)) + { + if(parent->walletModel->wallet().isSpent(kr.hash, kr.n)) // spent + { + continue; + } + + parent->beginInsertRows(QModelIndex(), lowerIndex + offsetLower, lowerIndex + offsetLower); + cachedWallet.insert(lowerIndex + offsetLower, kr); + parent->endInsertRows(); + offsetLower++; + } + } + else if(status == CT_UPDATED) + { + // nothing to do } - // Delete old records - for(int i = 0; i < mask.size(); i++) + else if(status == CT_DELETED) { - if(mask.at(i) == false) + // this status is not thrown + parent->beginRemoveRows(QModelIndex(), lowerIndex, upperIndex - 1); + for(int i = lowerIndex; i < upperIndex; i++) { - parent->beginRemoveRows(QModelIndex(), i, i); cachedWallet.removeAt(i); - parent->endRemoveRows(); + } + parent->endRemoveRows(); + + for(uint32_t n = 0; n <= wtx.tx->vin.size(); n++) + { + if(wtx.txin_is_mine[n] == isminetype::ISMINE_SPENDABLE) + { + interfaces::WalletTx prev_wtx = parent->walletModel->wallet().getWalletTx(wtx.tx->vin[n].prevout.hash); + + std::vector krs = KernelRecord::decomposeOutput(prev_wtx); + const KernelRecord& kr = krs[wtx.tx->vin[n].prevout.n]; + + QList::iterator prev_lower = qLowerBound( + cachedWallet.begin(), cachedWallet.end(), kr, TxLessThan()); + QList::iterator prev_upper = qUpperBound( + cachedWallet.begin(), cachedWallet.end(), kr, TxLessThan()); + int prev_lowerIndex = (prev_lower - cachedWallet.begin()); + + if(prev_lower == prev_upper) // not in model + { + parent->beginInsertRows(QModelIndex(), prev_lowerIndex, prev_lowerIndex); + cachedWallet.insert(prev_lowerIndex, kr); + parent->endInsertRows(); + } + } } } + else{ + qDebug() << "MintingTablePriv::procEvent : Unexpected status"; + } } int size() @@ -184,17 +269,27 @@ MintingTableModel::MintingTableModel(const PlatformStyle *_platformStyle, Wallet { columns << tr("Transaction") << tr("Address") << tr("Balance") << tr("Age") << tr("CoinDay") << tr("MintProbability") << tr("MintReward"); - priv->refreshWallet(walletModel->wallet()); + // Initialize records + std::vector wtxs = walletModel->wallet().getWalletTxs(); + std::sort(wtxs.begin(), wtxs.end(), compareWtx); + for (const auto& wtx : wtxs) + { + priv->procEvent(wtx.tx->GetHash(), CT_NEW); + } + + subscribeToCoreSignals(); + priv->refreshWallet(); QTimer *timer = new QTimer(this); connect(timer, SIGNAL(timeout()), this, SLOT(updateAge())); - timer->start(MODEL_MINTING_UPDATE_DELAY); + timer->start(MODEL_UPDATE_DELAY); connect(walletModel->getOptionsModel(), SIGNAL(displayUnitChanged(int)), this, SLOT(updateDisplayUnit())); } MintingTableModel::~MintingTableModel() { + unsubscribeFromCoreSignals(); delete priv; } @@ -203,8 +298,10 @@ void MintingTableModel::updateTransaction(const QString &hash, int status, bool uint256 updated; updated.SetHex(hash.toStdString()); - priv->refreshWallet(walletModel->wallet()); - mintingProxyModel->invalidate(); // Force deletion of empty rows + // CT_NEW -> add + // !showTransaction -> delete + // CT_UPDATED -> nothing to do + priv->procQueue.append(std::make_pair(updated, showTransaction ? status : CT_DELETED)); } void MintingTableModel::updateAge() @@ -213,7 +310,7 @@ void MintingTableModel::updateAge() Q_EMIT dataChanged(index(0, CoinDay), index(priv->size()-1, CoinDay)); Q_EMIT dataChanged(index(0, MintProbability), index(priv->size()-1, MintProbability)); Q_EMIT dataChanged(index(0, MintReward), index(priv->size()-1, MintReward)); - priv->refreshWallet(walletModel->wallet()); + priv->refreshWallet(); } void MintingTableModel::setMintingProxyModel(MintingFilterProxy *mintingProxy) @@ -448,3 +545,58 @@ void MintingTableModel::updateDisplayUnit() // emit dataChanged to update Balance column with the current unit Q_EMIT dataChanged(index(0, Balance), index(priv->size()-1, Balance)); } + +// queue notifications to show a non freezing progress dialog e.g. for rescan +struct TransactionNotification +{ +public: + TransactionNotification() {} + TransactionNotification(uint256 _hash, ChangeType _status, bool _showTransaction): + hash(_hash), status(_status), showTransaction(_showTransaction) {} + + void invoke(QObject *mtm) + { + QString strHash = QString::fromStdString(hash.GetHex()); + qDebug() << "NotifyTransactionChanged: " + strHash + " status= " + QString::number(status); + QMetaObject::invokeMethod(mtm, "updateTransaction", Qt::QueuedConnection, + Q_ARG(QString, strHash), + Q_ARG(int, status), + Q_ARG(bool, showTransaction)); + } +private: + uint256 hash; + ChangeType status; + bool showTransaction; +}; + +static bool fQueueNotifications = false; +static std::vector< TransactionNotification > vQueueNotifications; + +static void NotifyTransactionChanged(MintingTableModel *mtm, const uint256 &hash, ChangeType status) +{ + // Find transaction in wallet + // Determine whether to show transaction or not (determine this here so that no relocking is needed in GUI thread) + bool showTransaction = KernelRecord::showTransaction(); + + TransactionNotification notification(hash, status, showTransaction); + + if (fQueueNotifications) + { + vQueueNotifications.push_back(notification); + return; + } + notification.invoke(mtm); +} + +void MintingTableModel::subscribeToCoreSignals() +{ + // Connect signals to wallet + m_handler_transaction_changed = walletModel->wallet().handleTransactionChanged(boost::bind(NotifyTransactionChanged, this, _1, _2)); +} + +void MintingTableModel::unsubscribeFromCoreSignals() +{ + // Disconnect signals from wallet + m_handler_transaction_changed->disconnect(); +} + diff --git a/src/qt/mintingtablemodel.h b/src/qt/mintingtablemodel.h index aab33968a8b2..ddb8e4bce232 100644 --- a/src/qt/mintingtablemodel.h +++ b/src/qt/mintingtablemodel.h @@ -47,6 +47,7 @@ class MintingTableModel : public QAbstractTableModel private: WalletModel *walletModel; + std::unique_ptr m_handler_transaction_changed; QStringList columns; int mintingInterval; MintingTablePriv *priv; @@ -64,6 +65,9 @@ class MintingTableModel : public QAbstractTableModel QString formatTxCoinDay(const KernelRecord *wtx) const; QString formatTxPoSReward(KernelRecord *wtx) const; + void subscribeToCoreSignals(); + void unsubscribeFromCoreSignals(); + public Q_SLOTS: void updateTransaction(const QString &hash, int status, bool showTransaction); void updateAge();