#pragma once #include "wallet_state.h" #include #include #include #include #include #include #include struct sqlite3; namespace dragonx { namespace data { // Persisted z_listsentbyaddress watermark, so a relaunch runs a windowed sweep instead of a full one. // height (W) = tip of the last successful sweep; anchorHeight (A) = W - margin, the bottom edge the window // covered; anchorHash (H_A) = getblockhash(A) at that time, verified on the next load. backlog = txids found // but not yet enriched (must persist WITH W, or a pure z->z send whose txid lives only in a sweep result is // lost once the window moves past it). The backoff fields keep a relaunch from immediately re-running a full // sweep that keeps failing; daemonId resets the backoff on a version change. (Namespace-scope, not nested, so // it is a complete type for the default argument of replace().) struct SentSweepState { int height = -1; int anchorHeight = -1; std::string anchorHash; std::vector backlog; int fullFailures = 0; long fullRetryDelaySec = 0; std::string daemonId; std::int64_t lastFullTime = 0; // when the last successful FULL sweep finished: drives the weekly int lastFullHeight = -1; // safety-net re-sweep that catches changes the per-event guards miss bool valid() const { return height >= 0; } }; class TransactionHistoryCache { public: struct LoadResult { bool loaded = false; bool invalidated = false; int tipHeight = 0; std::string tipHash; std::time_t updatedAt = 0; std::vector transactions; std::unordered_map shieldedScanHeights; SentSweepState sentSweep; }; // Everything one snapshot write needs, BY VALUE, so it can run on the history writer thread after the UI // state it came from has moved on (and after lockKey() zeroed the live key). Move-only; the private key // copy is wiped by writeSnapshot() and, for a job that never ran (superseded), by the destructor. struct SnapshotJob { std::string walletIdentity; std::array key{}; int tipHeight = 0; std::string tipHash; std::vector transactions; std::time_t updatedAt = 0; std::unordered_map shieldedScanHeights; SentSweepState sentSweep; SnapshotJob() = default; SnapshotJob(const SnapshotJob&) = delete; SnapshotJob& operator=(const SnapshotJob&) = delete; SnapshotJob(SnapshotJob&&) = default; SnapshotJob& operator=(SnapshotJob&&) = default; ~SnapshotJob(); }; TransactionHistoryCache(); explicit TransactionHistoryCache(std::string databasePath); ~TransactionHistoryCache(); TransactionHistoryCache(const TransactionHistoryCache&) = delete; TransactionHistoryCache& operator=(const TransactionHistoryCache&) = delete; static std::string defaultDatabasePath(); static std::string walletIdentityFromAddresses(const std::vector& shieldedAddresses, const std::vector& transparentAddresses); static std::string walletIdentityHash(const std::string& walletIdentity); bool ensureOpen(); bool unlockWithPassphrase(const std::string& walletIdentity, const std::string& passphrase); // Unlock an UNENCRYPTED wallet's cache without Argon2id: the "secret" is derivable from the DB, so a // cheap domain-separated BLAKE2b KDF is security-equivalent and avoids a ~50-100 ms UI-thread stall. bool unlockUnencrypted(const std::string& walletIdentity); void lockKey(); bool hasKey() const; bool isUnlockedFor(const std::string& walletIdentity) const; // Copy of the live key if it is unlocked for `walletIdentity` (keyOut untouched otherwise). Caller wipes it. bool captureKey(const std::string& walletIdentity, std::array& keyOut) const; // Non-reversible id of the key last installed. Survives lockKey(), so re-unlocking with the same // passphrase gives the same id and a passphrase change gives another. 0 = never unlocked. std::uint64_t keyId() const; LoadResult load(const std::string& walletIdentity, int currentTipHeight, const std::string& currentTipHash); bool replace(const std::string& walletIdentity, int tipHeight, const std::string& tipHash, const std::vector& transactions, std::time_t updatedAt, const std::unordered_map& shieldedScanHeights = {}, const SentSweepState& sentSweep = SentSweepState{}); // Thread-safe snapshot write (the history writer thread; replace() wraps it): JSON + AEAD with no lock // held, then INSERT OR REPLACE + prune under the DB lock. Always wipes job.key. False for a keyless job. bool writeSnapshot(SnapshotJob&& job); void clearWallet(const std::string& walletIdentity); int snapshotCount(); private: bool exec(const char* sql); bool createSchema(); std::vector getOrCreateSalt(const std::string& walletHash); static bool deriveKeyFast(const std::string& walletHash, const std::vector& salt, std::array& keyOut); // BLAKE2b, label-separated static bool deriveKey(const std::string& passphrase, const std::vector& salt, std::array& keyOut); static bool encryptPayload(const std::array& key, const std::string& walletHash, const std::string& plainText, std::vector& nonce, std::vector& cipherText); bool decryptPayload(const std::string& walletHash, const std::vector& nonce, const std::vector& cipherText, std::string& plainText) const; bool readSnapshot(const std::string& walletHash, int& tipHeight, std::string& tipHash, std::time_t& updatedAt, std::vector& nonce, std::vector& cipherText); void clearWalletByHash(const std::string& walletHash); // Delete snapshot + salt rows for every wallet hash except the live one, bounding the // DB so generating a new address (which changes the hash) doesn't orphan history forever. // Also keeps `alsoKeepWalletHash` (the wallet unlocked now, if any): a delayed writer job for the previous // wallet must not delete the salt of the wallet just switched to. void pruneOtherWallets(const std::string& keepWalletHash, const std::string& alsoKeepWalletHash); void close(); // Lock order: db_mutex_ then key_mutex_. key_mutex_ is never held across I/O or the KDF. Only the UI // thread changes the key fields; the writer thread reads unlocked_wallet_hash_ (prune) under both locks. mutable std::recursive_mutex db_mutex_; // db_ and every SQL statement mutable std::mutex key_mutex_; // key_, key_ready_, unlocked_wallet_hash_, key_id_ sqlite3* db_ = nullptr; std::string database_path_; std::array key_{}; bool key_ready_ = false; std::string unlocked_wallet_hash_; std::uint64_t key_id_ = 0; }; } // namespace data } // namespace dragonx