Skip to content

Commit 942b80d

Browse files
feat: balance calculations (#144)
* refactor(spv): share ChainState between client and header sync - Eliminate duplicate ChainState initialization by sharing a single Arc<RwLock<ChainState>> across client and header sync. - HeaderSyncManagerWithReorg now holds shared state and no longer constructs its own ChainState. - SequentialSyncManager::new signature updated to accept the shared ChainState and plumb it through. - Client passes its ChainState to sync manager and removes the client-side header copy path. - Read/write updated to use RwLock guards; added lightweight cached checkpoint flags in header sync. This removes the duplicate "Initialized ChainState" logs and unifies state as a single source of truth. * fix(spv): correct off-by-one in cached absolute height - total_headers_synced should be base + headers_len - 1 when headers exist, or base/0 when empty. - Prevents overstating height by 1 in both checkpoint and normal sync paths. * feat(spv): enhance wallet transaction logging and UTXO management - Introduced logging for wallet transactions, capturing net balance changes and transaction context (mempool or block). - Implemented UTXO ingestion for outputs that pay to monitored addresses, ensuring accurate tracking of spendable accounts. - Removed UTXOs that are spent by transaction inputs, improving wallet state management. - Added periodic logging of detected transactions and wallet balances to enhance monitoring capabilities. * fix --------- Co-authored-by: pasta <pasta@dashboost.org>
1 parent 0355506 commit 942b80d

File tree

4 files changed

+200
-9
lines changed

4 files changed

+200
-9
lines changed

dash-spv/src/main.rs

Lines changed: 108 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ async fn run() -> Result<(), Box<dyn std::error::Error>> {
224224

225225
// Create the wallet manager
226226
let mut wallet_manager = WalletManager::<ManagedWalletInfo>::new();
227-
wallet_manager.create_wallet_from_mnemonic(
227+
let wallet_id = wallet_manager.create_wallet_from_mnemonic(
228228
"enemy check owner stumble unaware debris suffer peanut good fabric bleak outside",
229229
"",
230230
&[network],
@@ -261,6 +261,7 @@ async fn run() -> Result<(), Box<dyn std::error::Error>> {
261261
wallet,
262262
enable_terminal_ui,
263263
&matches,
264+
wallet_id,
264265
)
265266
.await?;
266267
} else {
@@ -278,6 +279,7 @@ async fn run() -> Result<(), Box<dyn std::error::Error>> {
278279
wallet,
279280
enable_terminal_ui,
280281
&matches,
282+
wallet_id,
281283
)
282284
.await?;
283285
}
@@ -289,8 +291,16 @@ async fn run() -> Result<(), Box<dyn std::error::Error>> {
289291
process::exit(1);
290292
}
291293
};
292-
run_client(config, network_manager, storage_manager, wallet, enable_terminal_ui, &matches)
293-
.await?;
294+
run_client(
295+
config,
296+
network_manager,
297+
storage_manager,
298+
wallet,
299+
enable_terminal_ui,
300+
&matches,
301+
wallet_id,
302+
)
303+
.await?;
294304
}
295305

296306
Ok(())
@@ -303,6 +313,7 @@ async fn run_client<S: dash_spv::storage::StorageManager + Send + Sync + 'static
303313
wallet: Arc<tokio::sync::RwLock<WalletManager<ManagedWalletInfo>>>,
304314
enable_terminal_ui: bool,
305315
matches: &clap::ArgMatches,
316+
wallet_id: [u8; 32],
306317
) -> Result<(), Box<dyn std::error::Error>> {
307318
// Create and start the client
308319
let mut client =
@@ -358,6 +369,100 @@ async fn run_client<S: dash_spv::storage::StorageManager + Send + Sync + 'static
358369

359370
tracing::info!("SPV client started successfully");
360371

372+
// Set up event logging: count detected transactions and log wallet balances periodically
373+
// Take the client's event receiver and spawn a logger task
374+
if let Some(mut event_rx) = client.take_event_receiver() {
375+
let wallet_for_logger = wallet.clone();
376+
let network_for_logger = config.network;
377+
let wallet_id_for_logger = wallet_id;
378+
tokio::spawn(async move {
379+
use dash_spv::types::SpvEvent;
380+
let mut total_detected_block_txs: u64 = 0;
381+
let mut total_detected_mempool_txs: u64 = 0;
382+
let mut last_snapshot = std::time::Instant::now();
383+
let snapshot_interval = std::time::Duration::from_secs(10);
384+
385+
loop {
386+
tokio::select! {
387+
maybe_event = event_rx.recv() => {
388+
match maybe_event {
389+
Some(SpvEvent::BlockProcessed { relevant_transactions, .. }) => {
390+
if relevant_transactions > 0 {
391+
total_detected_block_txs = total_detected_block_txs.saturating_add(relevant_transactions as u64);
392+
tracing::info!(
393+
"Detected {} wallet-relevant tx(s) in block; cumulative (blocks): {}",
394+
relevant_transactions,
395+
total_detected_block_txs
396+
);
397+
}
398+
}
399+
Some(SpvEvent::MempoolTransactionAdded { .. }) => {
400+
total_detected_mempool_txs = total_detected_mempool_txs.saturating_add(1);
401+
tracing::info!(
402+
"Detected wallet-relevant mempool tx; cumulative (mempool): {}",
403+
total_detected_mempool_txs
404+
);
405+
}
406+
Some(_) => { /* ignore other events */ }
407+
None => break, // sender closed
408+
}
409+
}
410+
// Also do a periodic snapshot while events are flowing
411+
_ = tokio::time::sleep(snapshot_interval) => {
412+
// Log snapshot if interval has elapsed
413+
if last_snapshot.elapsed() >= snapshot_interval {
414+
let (tx_count, confirmed, unconfirmed, locked, total, derived_incoming) = {
415+
let mgr = wallet_for_logger.read().await;
416+
// Count transactions via network state for the selected network
417+
let txs = mgr
418+
.get_network_state(network_for_logger)
419+
.map(|ns| ns.transactions.len())
420+
.unwrap_or(0);
421+
422+
// Read wallet balance from the managed wallet info
423+
let wb = mgr.get_wallet_balance(&wallet_id_for_logger).ok();
424+
let (c, u, l, t) = wb.map(|b| (b.confirmed, b.unconfirmed, b.locked, b.total)).unwrap_or((0, 0, 0, 0));
425+
426+
// Derive a conservative incoming total by summing tx outputs to our addresses.
427+
let incoming_sum = if let Some(ns) = mgr.get_network_state(network_for_logger) {
428+
let addrs = mgr.monitored_addresses(network_for_logger);
429+
let addr_set: std::collections::HashSet<_> = addrs.into_iter().collect();
430+
let mut sum_incoming: u64 = 0;
431+
for rec in ns.transactions.values() {
432+
for out in &rec.transaction.output {
433+
if let Ok(out_addr) = dashcore::Address::from_script(&out.script_pubkey, network_for_logger) {
434+
if addr_set.contains(&out_addr) {
435+
sum_incoming = sum_incoming.saturating_add(out.value);
436+
}
437+
}
438+
}
439+
}
440+
sum_incoming
441+
} else { 0 };
442+
443+
(txs, c, u, l, t, incoming_sum)
444+
};
445+
tracing::info!(
446+
"Wallet tx summary: detected={} (blocks={} + mempool={}), balances: confirmed={} unconfirmed={} locked={} total={}, derived_incoming_total={} (approx)",
447+
tx_count,
448+
total_detected_block_txs,
449+
total_detected_mempool_txs,
450+
confirmed,
451+
unconfirmed,
452+
locked,
453+
total,
454+
derived_incoming
455+
);
456+
last_snapshot = std::time::Instant::now();
457+
}
458+
}
459+
}
460+
}
461+
});
462+
} else {
463+
tracing::warn!("Event channel not available; transaction/balance logging disabled");
464+
}
465+
361466
// Add watch addresses if specified
362467
if let Some(addresses) = matches.get_many::<String>("watch-address") {
363468
for addr_str in addresses {

dash-spv/src/sync/headers_with_reorg.rs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1122,10 +1122,15 @@ impl<S: StorageManager + Send + Sync + 'static, N: NetworkManager + Send + Sync
11221122
) {
11231123
self.cached_synced_from_checkpoint = synced_from_checkpoint;
11241124
self.cached_sync_base_height = sync_base_height;
1125-
if synced_from_checkpoint && sync_base_height > 0 {
1126-
self.total_headers_synced = sync_base_height + headers_len;
1125+
// Absolute blockchain tip height = base + headers_len - 1 (if any headers exist)
1126+
self.total_headers_synced = if headers_len == 0 {
1127+
if synced_from_checkpoint {
1128+
sync_base_height
1129+
} else {
1130+
0
1131+
}
11271132
} else {
1128-
self.total_headers_synced = headers_len;
1129-
}
1133+
sync_base_height.saturating_add(headers_len).saturating_sub(1)
1134+
};
11301135
}
11311136
}

key-wallet/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ serde_json = { version = "1.0", optional = true }
4242
hex = { version = "0.4"}
4343
hkdf = { version = "0.12", default-features = false }
4444
zeroize = { version = "1.8", features = ["derive"] }
45+
tracing = "0.1"
4546

4647
[dev-dependencies]
4748
hex = "0.4"
48-
key-wallet = { path = ".", features = ["bip38", "serde", "bincode", "eddsa", "bls"] }
49+
key-wallet = { path = ".", features = ["bip38", "serde", "bincode", "eddsa", "bls"] }

key-wallet/src/transaction_checking/wallet_checker.rs

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@ use super::transaction_router::TransactionRouter;
88
use crate::wallet::immature_transaction::ImmatureTransaction;
99
use crate::wallet::managed_wallet_info::wallet_info_interface::WalletInfoInterface;
1010
use crate::wallet::managed_wallet_info::ManagedWalletInfo;
11-
use crate::{Network, Wallet};
11+
use crate::{Network, Utxo, Wallet};
1212
use dashcore::blockdata::transaction::Transaction;
1313
use dashcore::BlockHash;
14+
use dashcore::{Address as DashAddress, OutPoint};
1415
use dashcore_hashes::Hash;
1516

1617
/// Context for transaction processing
@@ -187,6 +188,60 @@ impl WalletTransactionChecker for ManagedWalletInfo {
187188

188189
account.transactions.insert(tx.txid(), tx_record);
189190

191+
// Ingest UTXOs for outputs that pay to our addresses and
192+
// remove UTXOs that are spent by this transaction's inputs.
193+
// Only apply for spendable account types (Standard, CoinJoin).
194+
match &mut account.account_type {
195+
crate::managed_account::managed_account_type::ManagedAccountType::Standard { .. }
196+
| crate::managed_account::managed_account_type::ManagedAccountType::CoinJoin { .. } => {
197+
// Build a set of addresses involved for fast membership tests
198+
let mut involved_addrs = alloc::collections::BTreeSet::new();
199+
for info in account_match.account_type_match.all_involved_addresses() {
200+
involved_addrs.insert(info.address.clone());
201+
}
202+
203+
// Determine confirmation state and block height for UTXOs
204+
let (is_confirmed, utxo_height) = match context {
205+
TransactionContext::Mempool => (false, 0u32),
206+
TransactionContext::InBlock { height, .. }
207+
| TransactionContext::InChainLockedBlock { height, .. } => (true, height),
208+
};
209+
210+
// Insert UTXOs for matching outputs
211+
let txid = tx.txid();
212+
for (vout, output) in tx.output.iter().enumerate() {
213+
if let Ok(addr) = DashAddress::from_script(&output.script_pubkey, network) {
214+
if involved_addrs.contains(&addr) {
215+
let outpoint = OutPoint { txid, vout: vout as u32 };
216+
// Construct TxOut clone explicitly to avoid trait assumptions
217+
let txout = dashcore::TxOut {
218+
value: output.value,
219+
script_pubkey: output.script_pubkey.clone(),
220+
};
221+
let mut utxo = Utxo::new(
222+
outpoint,
223+
txout,
224+
addr,
225+
utxo_height,
226+
tx.is_coin_base(),
227+
);
228+
utxo.is_confirmed = is_confirmed;
229+
account.utxos.insert(outpoint, utxo);
230+
}
231+
}
232+
}
233+
234+
// Remove any UTXOs that are being spent by this transaction
235+
for input in &tx.input {
236+
// If this input spends one of our UTXOs, remove it
237+
account.utxos.remove(&input.previous_output);
238+
}
239+
}
240+
_ => {
241+
// Skip UTXO ingestion for identity/provider accounts
242+
}
243+
}
244+
190245
// Mark involved addresses as used
191246
for address_info in
192247
account_match.account_type_match.all_involved_addresses()
@@ -238,6 +293,31 @@ impl WalletTransactionChecker for ManagedWalletInfo {
238293

239294
// Update cached balance
240295
self.update_balance();
296+
297+
// Emit a concise log for this detected transaction with net wallet change
298+
let wallet_net: i64 =
299+
(result.total_received as i64) - (result.total_sent as i64);
300+
let ctx = match context {
301+
TransactionContext::Mempool => "mempool".to_string(),
302+
TransactionContext::InBlock {
303+
height,
304+
..
305+
} => alloc::format!("block {}", height),
306+
TransactionContext::InChainLockedBlock {
307+
height,
308+
..
309+
} => {
310+
alloc::format!("chainlocked block {}", height)
311+
}
312+
};
313+
tracing::info!(
314+
txid = %tx.txid(),
315+
context = %ctx,
316+
net_change = wallet_net,
317+
received = result.total_received,
318+
sent = result.total_sent,
319+
"Wallet transaction detected: net balance change"
320+
);
241321
}
242322
}
243323

0 commit comments

Comments
 (0)