Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions key-wallet-ffi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ eddsa = ["dashcore/eddsa", "key-wallet/eddsa"]
bls = ["dashcore/bls", "key-wallet/bls"]

[dependencies]
key-wallet = { path = "../key-wallet" }
key-wallet = { path = "../key-wallet", features = ["keep_txs_in_memory"] }
key-wallet-manager = { path = "../key-wallet-manager" }
dashcore = { path = "../dash" }
dash-network = { path = "../dash-network", features = ["ffi"] }
Expand All @@ -33,6 +33,6 @@ hex = "0.4"
cbindgen = "0.29"

[dev-dependencies]
key-wallet = { path = "../key-wallet", features = ["test-utils"] }
key-wallet = { path = "../key-wallet", features = ["test-utils", "keep_txs_in_memory"] }
key-wallet-manager = { path = "../key-wallet-manager", features = ["test-utils"] }
hex = "0.4"
2 changes: 1 addition & 1 deletion key-wallet-manager/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ bls = ["key-wallet/bls"]
eddsa = ["key-wallet/eddsa"]

[dependencies]
key-wallet = { path = "../key-wallet", default-features = false }
key-wallet = { path = "../key-wallet", default-features = false, features = ["keep_txs_in_memory"] }
dashcore = { path = "../dash" }
async-trait = "0.1"
tokio = { version = "1", features = ["macros", "rt", "sync"] }
Expand Down
5 changes: 4 additions & 1 deletion key-wallet/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ bip38 = ["scrypt", "aes", "bs58", "rand"]
eddsa = ["dashcore/eddsa"]
bls = ["dashcore/bls"]
test-utils = ["dashcore/test-utils"]
# Keep per-account transaction history (`ManagedCoreKeysAccount::transactions`) in memory.
# Disable to skip storing transaction records when only UTXOs and balances matter.
keep_txs_in_memory = []

[dependencies]
internals = { path = "../internals", package = "dashcore-private" }
Expand Down Expand Up @@ -46,7 +49,7 @@ async-trait = "0.1"
[dev-dependencies]
dashcore = { path="../dash", features = ["test-utils"] }
hex = "0.4"
key-wallet = { path = ".", features = ["test-utils", "bip38", "serde", "bincode", "eddsa", "bls"] }
key-wallet = { path = ".", default-features = false, features = ["test-utils", "bip38", "serde", "bincode", "eddsa", "bls", "getrandom"] }
rand = { version = "0.8", features = ["std", "std_rng"] }
test-case = "3.3"
tokio = { version = "1", features = ["macros", "rt"] }
18 changes: 16 additions & 2 deletions key-wallet/src/managed_account/managed_account_trait.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
//! depend on funds bookkeeping (balance / UTXOs / spent outpoints) lives here
//! as default-method implementations so it is written exactly once.

#[cfg(feature = "keep_txs_in_memory")]
use std::collections::BTreeMap;

#[cfg(feature = "keep_txs_in_memory")]
use crate::account::TransactionRecord;
#[cfg(feature = "bls")]
use crate::derivation_bls_bip32::ExtendedBLSPubKey;
Expand Down Expand Up @@ -41,12 +43,24 @@ pub trait ManagedAccountTrait {
/// Check if this is a watch-only account
fn is_watch_only(&self) -> bool;

/// Get transactions
/// Get transactions.
///
/// Only available when the `keep_txs_in_memory` Cargo feature is enabled.
#[cfg(feature = "keep_txs_in_memory")]
fn transactions(&self) -> &BTreeMap<Txid, TransactionRecord>;

/// Get mutable transactions
/// Get mutable transactions.
///
/// Only available when the `keep_txs_in_memory` Cargo feature is enabled.
#[cfg(feature = "keep_txs_in_memory")]
fn transactions_mut(&mut self) -> &mut BTreeMap<Txid, TransactionRecord>;

/// Returns `true` if this account has already processed `txid`.
///
/// Backed by an always-present "processed" set, so this works in both
/// feature configurations.
fn has_transaction(&self, txid: &Txid) -> bool;

/// Return the current monitor revision.
///
/// Bumped whenever the monitored address set changes (e.g. new addresses
Expand Down
25 changes: 23 additions & 2 deletions key-wallet/src/managed_account/managed_core_funds_account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -235,19 +235,26 @@ impl ManagedCoreFundsAccount {

/// Re-process an existing transaction with updated context (e.g., mempool→block confirmation)
/// and potentially new address matches from gap limit rescans.
///
/// Deduplication uses the always-present `processed_txids` set on the
/// inner keys account. With the `keep_txs_in_memory` Cargo feature off,
/// no per-tx record is stored, so we cannot detect a confirmation
/// status transition; we still refresh UTXO state and report no change.
pub(crate) fn confirm_transaction(
&mut self,
tx: &Transaction,
account_match: &AccountMatch,
context: TransactionContext,
transaction_type: TransactionType,
) -> bool {
if !self.keys.transactions().contains_key(&tx.txid()) {
if !self.keys.has_transaction(&tx.txid()) {
self.record_transaction(tx, account_match, context, transaction_type);
return true;
}

#[cfg_attr(not(feature = "keep_txs_in_memory"), allow(unused_mut))]
let mut changed = false;
#[cfg(feature = "keep_txs_in_memory")]
if let Some(tx_record) = self.keys.transactions_mut().get_mut(&tx.txid()) {
debug_assert_eq!(
tx_record.transaction_type,
Expand All @@ -265,6 +272,8 @@ impl ManagedCoreFundsAccount {
changed = !was_confirmed;
}
}
#[cfg(not(feature = "keep_txs_in_memory"))]
let _ = transaction_type;
self.update_utxos(tx, account_match, context);
changed
}
Expand Down Expand Up @@ -372,7 +381,7 @@ impl ManagedCoreFundsAccount {
);

let record = tx_record.clone();
self.keys.transactions_mut().insert(tx.txid(), tx_record);
self.keys.insert_transaction(tx.txid(), tx_record);

self.update_utxos(tx, account_match, context);
record
Expand Down Expand Up @@ -608,14 +617,20 @@ impl ManagedAccountTrait for ManagedCoreFundsAccount {
self.keys.is_watch_only()
}

#[cfg(feature = "keep_txs_in_memory")]
fn transactions(&self) -> &BTreeMap<Txid, TransactionRecord> {
self.keys.transactions()
}

#[cfg(feature = "keep_txs_in_memory")]
fn transactions_mut(&mut self) -> &mut BTreeMap<Txid, TransactionRecord> {
self.keys.transactions_mut()
}

fn has_transaction(&self, txid: &Txid) -> bool {
self.keys.has_transaction(txid)
}

fn monitor_revision(&self) -> u64 {
self.keys.monitor_revision()
}
Expand All @@ -640,13 +655,19 @@ impl<'de> Deserialize<'de> for ManagedCoreFundsAccount {

let helper = Helper::deserialize(deserializer)?;

// `spent_outpoints` is rebuilt from stored transactions, which only
// exist when the `keep_txs_in_memory` Cargo feature is enabled. When
// the feature is off, start empty.
#[cfg(feature = "keep_txs_in_memory")]
let spent_outpoints = helper
.keys
.transactions()
.values()
.flat_map(|record| &record.transaction.input)
.map(|input| input.previous_output)
.collect();
#[cfg(not(feature = "keep_txs_in_memory"))]
let spent_outpoints = HashSet::new();

Ok(ManagedCoreFundsAccount {
keys: helper.keys,
Expand Down
107 changes: 105 additions & 2 deletions key-wallet/src/managed_account/managed_core_keys_account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ use crate::Network;
use dashcore::Txid;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[cfg(any(feature = "keep_txs_in_memory", feature = "serde"))]
use std::collections::BTreeMap;
use std::collections::HashSet;

/// Managed core keys account with mutable state but no funds tracking.
///
Expand All @@ -31,16 +33,29 @@ use std::collections::BTreeMap;
/// Most behavior comes from [`ManagedAccountTrait`] default methods; this
/// type only owns the primitive state.
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub struct ManagedCoreKeysAccount {
/// Account type with embedded address pools and index
managed_account_type: ManagedAccountType,
/// Network this account belongs to
network: Network,
/// Whether this is a watch-only account
is_watch_only: bool,
/// Transaction history for this account
/// Transaction history for this account.
///
/// Only present when the `keep_txs_in_memory` Cargo feature is enabled.
/// With the feature off, processed transactions update UTXOs and balance
/// (on the funds variant) but no per-tx history is retained here.
#[cfg(feature = "keep_txs_in_memory")]
transactions: BTreeMap<Txid, TransactionRecord>,
/// Txids of every transaction this account has already processed.
///
/// Always populated regardless of `keep_txs_in_memory`, so dedup of
/// re-events (mempool→block, late IS-lock arrivals) works in either
/// feature configuration. Rebuilt from `transactions` during
/// deserialization when the feature is enabled.
#[cfg_attr(feature = "serde", serde(skip))]
processed_txids: HashSet<Txid>,
/// Revision counter incremented when the monitored address set changes
/// (e.g. new addresses generated). Used to detect bloom filter staleness.
#[cfg_attr(feature = "serde", serde(skip))]
Expand All @@ -58,11 +73,63 @@ impl ManagedCoreKeysAccount {
managed_account_type,
network,
is_watch_only,
#[cfg(feature = "keep_txs_in_memory")]
transactions: BTreeMap::new(),
processed_txids: HashSet::new(),
monitor_revision: 0,
}
}

/// Returns `true` if this account has already processed `txid`.
///
/// Backed by an always-present `processed_txids` set, so this works
/// regardless of whether the `keep_txs_in_memory` Cargo feature is
/// enabled. Use [`Self::get_transaction`] to retrieve the full record
/// (only available when the feature is enabled).
pub fn has_transaction(&self, txid: &Txid) -> bool {
self.processed_txids.contains(txid)
}

/// Returns the stored transaction record for `txid`, if any.
///
/// Always returns `None` when the `keep_txs_in_memory` Cargo feature is
/// disabled, since no records are retained.
#[cfg(feature = "keep_txs_in_memory")]
pub fn get_transaction(&self, txid: &Txid) -> Option<&TransactionRecord> {
self.transactions.get(txid)
}

/// Always returns `None` because the `keep_txs_in_memory` Cargo feature
/// is disabled, so no records are retained.
#[cfg(not(feature = "keep_txs_in_memory"))]
pub fn get_transaction(&self, _txid: &Txid) -> Option<&TransactionRecord> {
None
}

/// Insert a transaction record. Used by the funds variant; always
/// updates `processed_txids` regardless of feature.
#[cfg(feature = "keep_txs_in_memory")]
pub(crate) fn insert_transaction(&mut self, txid: Txid, record: TransactionRecord) {
self.processed_txids.insert(txid);
self.transactions.insert(txid, record);
}

/// Insert a transaction record. With the feature off, only the
/// `processed_txids` set is updated.
#[cfg(not(feature = "keep_txs_in_memory"))]
pub(crate) fn insert_transaction(&mut self, txid: Txid, _record: TransactionRecord) {
self.processed_txids.insert(txid);
}

/// Forget that `txid` was ever processed. Test-only escape hatch for
/// simulating a "lost record" state.
#[cfg(test)]
pub(crate) fn forget_transaction(&mut self, txid: &Txid) {
self.processed_txids.remove(txid);
#[cfg(feature = "keep_txs_in_memory")]
self.transactions.remove(txid);
}

/// Create a `ManagedCoreKeysAccount` from an [`Account`](super::super::Account).
pub fn from_account(account: &super::super::Account) -> Self {
let key_source = address_pool::KeySource::Public(account.account_xpub);
Expand Down Expand Up @@ -139,14 +206,20 @@ impl ManagedAccountTrait for ManagedCoreKeysAccount {
self.is_watch_only
}

#[cfg(feature = "keep_txs_in_memory")]
fn transactions(&self) -> &BTreeMap<Txid, TransactionRecord> {
&self.transactions
}

#[cfg(feature = "keep_txs_in_memory")]
fn transactions_mut(&mut self) -> &mut BTreeMap<Txid, TransactionRecord> {
&mut self.transactions
}

fn has_transaction(&self, txid: &Txid) -> bool {
self.processed_txids.contains(txid)
}

fn monitor_revision(&self) -> u64 {
self.monitor_revision
}
Expand All @@ -155,3 +228,33 @@ impl ManagedAccountTrait for ManagedCoreKeysAccount {
self.monitor_revision += 1;
}
}

#[cfg(feature = "serde")]
impl<'de> Deserialize<'de> for ManagedCoreKeysAccount {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
struct Helper {
managed_account_type: ManagedAccountType,
network: Network,
is_watch_only: bool,
#[serde(default)]
transactions: BTreeMap<Txid, TransactionRecord>,
}

let helper = Helper::deserialize(deserializer)?;
let processed_txids: HashSet<Txid> = helper.transactions.keys().copied().collect();

Ok(ManagedCoreKeysAccount {
managed_account_type: helper.managed_account_type,
network: helper.network,
is_watch_only: helper.is_watch_only,
#[cfg(feature = "keep_txs_in_memory")]
transactions: helper.transactions,
processed_txids,
monitor_revision: 0,
})
}
}
14 changes: 11 additions & 3 deletions key-wallet/src/test_utils/wallet.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
use dashcore::{Address, Network, Transaction, Txid};
#[cfg(feature = "keep_txs_in_memory")]
use dashcore::Txid;
use dashcore::{Address, Network, Transaction};

use crate::{
account::{ManagedCoreFundsAccount, TransactionRecord},
managed_account::managed_account_trait::ManagedAccountTrait,
account::ManagedCoreFundsAccount,
transaction_checking::{TransactionCheckResult, TransactionContext, WalletTransactionChecker},
wallet::{initialization::WalletAccountCreationOptions, ManagedWalletInfo},
ExtendedPubKey, Utxo, Wallet,
};
#[cfg(feature = "keep_txs_in_memory")]
use crate::{
account::TransactionRecord, managed_account::managed_account_trait::ManagedAccountTrait,
};

impl ManagedWalletInfo {
pub fn dummy(id: u8) -> Self {
Expand Down Expand Up @@ -61,6 +66,9 @@ impl TestWalletContext {
}

/// Returns a transaction record by txid from the first BIP44 account.
///
/// Only available when the `keep_txs_in_memory` Cargo feature is enabled.
#[cfg(feature = "keep_txs_in_memory")]
pub fn transaction(&self, txid: &Txid) -> &TransactionRecord {
self.bip44_account().transactions().get(txid).expect("Should have transaction")
}
Expand Down
Loading
Loading