#pragma once #include "wallet_state.h" #include #include #include #include #include #include #include struct sqlite3; namespace dragonx { namespace data { 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; }; // 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; 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 = {}); // 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