diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index 86149e415f2..346e9893ae9 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -1498,7 +1498,17 @@ UniValue listtransactions(const UniValue& params, bool fHelp) UniValue ret(UniValue::VARR); std::list acentries; - CWallet::TxItems txOrdered = pwalletMain->OrderedTxItems(acentries, strAccount); + + // Perf: we only ever return the newest (nCount+nFrom) rows, so first try a limit-aware + // pass that builds only the newest (nCount+nFrom) wallet items instead of the entire + // ordered log. A single item can expand to 0..N output rows, so if the limited window is + // a strict subset of the wallet and still cannot fill (nCount+nFrom) rows, we fall back + // to the full unlimited pass to guarantee byte-identical output. + const int nWindow = nCount + nFrom; + bool fLimited = nWindow > 0; + CWallet::TxItems txOrdered = fLimited + ? pwalletMain->OrderedTxItems(acentries, strAccount, nWindow) + : pwalletMain->OrderedTxItems(acentries, strAccount); // iterate backwards until we have nCount items to return: for (CWallet::TxItems::reverse_iterator it = txOrdered.rbegin(); it != txOrdered.rend(); ++it) @@ -1510,7 +1520,27 @@ UniValue listtransactions(const UniValue& params, bool fHelp) if (pacentry != 0) AcentryToJSON(*pacentry, strAccount, ret); - if ((int)ret.size() >= (nCount+nFrom)) break; + if ((int)ret.size() >= nWindow) break; + } + + // If the limited window may have dropped older items that we still needed to fill the + // requested rows, redo with the complete ordered log (same result as the original code). + if (fLimited && (int)ret.size() < nWindow && (int)txOrdered.size() >= nWindow) + { + ret.clear(); + ret.setArray(); + txOrdered = pwalletMain->OrderedTxItems(acentries, strAccount); + for (CWallet::TxItems::reverse_iterator it = txOrdered.rbegin(); it != txOrdered.rend(); ++it) + { + CWalletTx *const pwtx = (*it).second.first; + if (pwtx != 0) + ListTransactions(*pwtx, strAccount, 0, true, ret, filter); + CAccountingEntry *const pacentry = (*it).second.second; + if (pacentry != 0) + AcentryToJSON(*pacentry, strAccount, ret); + + if ((int)ret.size() >= nWindow) break; + } } // ret is newest to oldest diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index 10602ebf5d6..1915b297394 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -1274,7 +1274,7 @@ int64_t CWallet::IncOrderPosNext(CWalletDB *pwalletdb) return nRet; } -CWallet::TxItems CWallet::OrderedTxItems(std::list& acentries, std::string strAccount) +CWallet::TxItems CWallet::OrderedTxItems(std::list& acentries, std::string strAccount, int nLimit) { AssertLockHeld(cs_wallet); // mapWallet CWalletDB walletdb(strWalletFile); @@ -1282,18 +1282,30 @@ CWallet::TxItems CWallet::OrderedTxItems(std::list& acentries, // First: get all CWalletTx and CAccountingEntry into a sorted-by-order multimap. TxItems txOrdered; + // Perf: when nLimit > 0 the caller only needs the newest nLimit items (largest + // nOrderPos), so we trim the smallest as we go and never grow past nLimit. This keeps + // the exact same newest-suffix the caller would have iterated, at a fraction of the + // memory/insertion cost on large wallets. The full path (nLimit <= 0) is unchanged. + // Note: a single item can expand to multiple (or zero) output rows downstream, so the + // caller treats a trimmed result as a hint and falls back to an unlimited call if it + // cannot fill its window. + // // Note: maintaining indices in the database of (account,time) --> txid and (account, time) --> acentry // would make this much faster for applications that do this a lot. for (map::iterator it = mapWallet.begin(); it != mapWallet.end(); ++it) { CWalletTx* wtx = &((*it).second); txOrdered.insert(make_pair(wtx->nOrderPos, TxPair(wtx, (CAccountingEntry*)0))); + if (nLimit > 0 && (int)txOrdered.size() > nLimit) + txOrdered.erase(txOrdered.begin()); } acentries.clear(); walletdb.ListAccountCreditDebit(strAccount, acentries); BOOST_FOREACH(CAccountingEntry& entry, acentries) { txOrdered.insert(make_pair(entry.nOrderPos, TxPair((CWalletTx*)0, &entry))); + if (nLimit > 0 && (int)txOrdered.size() > nLimit) + txOrdered.erase(txOrdered.begin()); } return txOrdered; @@ -3336,9 +3348,16 @@ bool CWallet::SelectCoinsMinConf(const CAmount& nTargetValue, int nConfMine, int bool CWallet::SelectCoins(const CAmount& nTargetValue, set >& setCoinsRet, CAmount& nValueRet, bool& fOnlyCoinbaseCoinsRet, bool& fNeedCoinbaseCoinsRet, const CCoinControl* coinControl) const { // Output parameter fOnlyCoinbaseCoinsRet is set to true when the only available coins are coinbase utxos. + // Perf: a single AvailableCoins pass with fIncludeCoinBase=true yields the full set; the + // no-coinbase set differs only by excluding coinbase utxos (the lone filter difference in + // AvailableCoins is fIncludeCoinBase), so we partition here instead of scanning mapWallet twice. vector vCoinsNoCoinbase, vCoinsWithCoinbase; - AvailableCoins(vCoinsNoCoinbase, true, coinControl, false, false); AvailableCoins(vCoinsWithCoinbase, true, coinControl, false, true); + vCoinsNoCoinbase.reserve(vCoinsWithCoinbase.size()); + for (const COutput& out : vCoinsWithCoinbase) { + if (!out.tx->IsCoinBase()) + vCoinsNoCoinbase.push_back(out); + } fOnlyCoinbaseCoinsRet = vCoinsNoCoinbase.size() == 0 && vCoinsWithCoinbase.size() > 0; // If coinbase utxos can only be sent to zaddrs, exclude any coinbase utxos from coin selection. diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index 84c081f09e1..78fdf98644f 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -1212,8 +1212,15 @@ class CWallet : public CCryptoKeyStore, public CValidationInterface * Get the wallet's activity log * @return multimap of ordered transactions and accounting entries * @warning Returned pointers are *only* valid within the scope of passed acentries + * + * When nLimit > 0, only the newest nLimit items (largest nOrderPos) are returned; + * this is a perf hint for callers (e.g. listtransactions) that consume newest-first + * and need only a bounded window. Callers MUST treat a result smaller than the + * whole wallet as potentially under-supplied and fall back to an unlimited call if + * they cannot fill their requested window, since a single tx may yield 0..N output + * rows. With nLimit <= 0 (default) the full ordered log is returned, unchanged. */ - TxItems OrderedTxItems(std::list& acentries, std::string strAccount = ""); + TxItems OrderedTxItems(std::list& acentries, std::string strAccount = "", int nLimit = 0); void MarkDirty(); bool UpdateNullifierNoteMap();