Skip to content

Commit c0fc4ca

Browse files
more work
1 parent 7d2f8d4 commit c0fc4ca

File tree

15 files changed

+412
-54
lines changed

15 files changed

+412
-54
lines changed

dash-spv-ffi/include/dash_spv_ffi.h

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -710,6 +710,44 @@ struct FFIFilterMatches *dash_spv_ffi_client_get_filter_matched_heights(struct F
710710
uint32_t end_height)
711711
;
712712

713+
/**
714+
* Get the total count of transactions across all wallets.
715+
*
716+
* This returns the persisted transaction count from the wallet,
717+
* not the ephemeral sync statistics. Use this to show how many
718+
* blocks contained relevant transactions for the user's wallets.
719+
*
720+
* # Parameters
721+
* - `client`: Valid pointer to an FFIDashSpvClient
722+
*
723+
* # Returns
724+
* - Transaction count (0 or higher)
725+
* - Returns 0 if client not initialized or wallet not available
726+
*
727+
* # Safety
728+
* - `client` must be a valid, non-null pointer
729+
*/
730+
uintptr_t dash_spv_ffi_client_get_transaction_count(struct FFIDashSpvClient *client) ;
731+
732+
/**
733+
* Get the count of blocks that contained relevant transactions.
734+
*
735+
* This counts unique block heights from the wallet's transaction history,
736+
* representing how many blocks actually had transactions for the user's wallets.
737+
* This is a persistent metric that survives app restarts.
738+
*
739+
* # Parameters
740+
* - `client`: Valid pointer to an FFIDashSpvClient
741+
*
742+
* # Returns
743+
* - Count of blocks with transactions (0 or higher)
744+
* - Returns 0 if client not initialized or wallet not available
745+
*
746+
* # Safety
747+
* - `client` must be a valid, non-null pointer
748+
*/
749+
uintptr_t dash_spv_ffi_client_get_blocks_with_transactions_count(struct FFIDashSpvClient *client) ;
750+
713751
struct FFIClientConfig *dash_spv_ffi_config_new(FFINetwork network) ;
714752

715753
struct FFIClientConfig *dash_spv_ffi_config_mainnet(void) ;

dash-spv-ffi/src/client.rs

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1770,3 +1770,104 @@ pub unsafe extern "C" fn dash_spv_ffi_client_get_filter_matched_heights(
17701770

17711771
result.unwrap_or(std::ptr::null_mut())
17721772
}
1773+
1774+
/// Get the total count of transactions across all wallets.
1775+
///
1776+
/// This returns the persisted transaction count from the wallet,
1777+
/// not the ephemeral sync statistics. Use this to show how many
1778+
/// blocks contained relevant transactions for the user's wallets.
1779+
///
1780+
/// # Parameters
1781+
/// - `client`: Valid pointer to an FFIDashSpvClient
1782+
///
1783+
/// # Returns
1784+
/// - Transaction count (0 or higher)
1785+
/// - Returns 0 if client not initialized or wallet not available
1786+
///
1787+
/// # Safety
1788+
/// - `client` must be a valid, non-null pointer
1789+
#[no_mangle]
1790+
pub unsafe extern "C" fn dash_spv_ffi_client_get_transaction_count(
1791+
client: *mut FFIDashSpvClient,
1792+
) -> usize {
1793+
null_check!(client, 0);
1794+
1795+
let client = &(*client);
1796+
let inner = client.inner.clone();
1797+
1798+
let result = client.runtime.block_on(async {
1799+
// Get wallet without taking the client
1800+
let guard = inner.lock().unwrap();
1801+
match guard.as_ref() {
1802+
Some(spv_client) => {
1803+
// Access wallet and get transaction count
1804+
let wallet = spv_client.wallet();
1805+
let wallet_guard = wallet.read().await;
1806+
let tx_history = wallet_guard.transaction_history();
1807+
tx_history.len()
1808+
}
1809+
None => {
1810+
tracing::warn!("Client not initialized when querying transaction count");
1811+
0
1812+
}
1813+
}
1814+
});
1815+
1816+
result
1817+
}
1818+
1819+
/// Get the count of blocks that contained relevant transactions.
1820+
///
1821+
/// This counts unique block heights from the wallet's transaction history,
1822+
/// representing how many blocks actually had transactions for the user's wallets.
1823+
/// This is a persistent metric that survives app restarts.
1824+
///
1825+
/// # Parameters
1826+
/// - `client`: Valid pointer to an FFIDashSpvClient
1827+
///
1828+
/// # Returns
1829+
/// - Count of blocks with transactions (0 or higher)
1830+
/// - Returns 0 if client not initialized or wallet not available
1831+
///
1832+
/// # Safety
1833+
/// - `client` must be a valid, non-null pointer
1834+
#[no_mangle]
1835+
pub unsafe extern "C" fn dash_spv_ffi_client_get_blocks_with_transactions_count(
1836+
client: *mut FFIDashSpvClient,
1837+
) -> usize {
1838+
null_check!(client, 0);
1839+
1840+
let client = &(*client);
1841+
let inner = client.inner.clone();
1842+
1843+
let result = client.runtime.block_on(async {
1844+
// Get wallet without taking the client
1845+
let guard = inner.lock().unwrap();
1846+
match guard.as_ref() {
1847+
Some(spv_client) => {
1848+
// Access wallet and get unique block heights
1849+
let wallet = spv_client.wallet();
1850+
let wallet_guard = wallet.read().await;
1851+
let tx_history = wallet_guard.transaction_history();
1852+
1853+
// Count unique block heights (confirmed transactions only)
1854+
let mut unique_heights = std::collections::HashSet::new();
1855+
for tx in tx_history {
1856+
if let Some(height) = tx.height {
1857+
unique_heights.insert(height);
1858+
}
1859+
}
1860+
1861+
unique_heights.len()
1862+
}
1863+
None => {
1864+
tracing::warn!(
1865+
"Client not initialized when querying blocks with transactions count"
1866+
);
1867+
0
1868+
}
1869+
}
1870+
});
1871+
1872+
result
1873+
}

dash-spv-ffi/tests/unit/test_client_lifecycle.rs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,4 +241,61 @@ mod tests {
241241
}
242242
}
243243
}
244+
245+
#[test]
246+
#[serial]
247+
fn test_transaction_count_with_empty_wallet() {
248+
unsafe {
249+
let (config, _temp_dir) = create_test_config_with_dir();
250+
let client = dash_spv_ffi_client_new(config);
251+
assert!(!client.is_null(), "Client creation failed");
252+
253+
// Should return 0 for a new wallet with no transactions
254+
let tx_count = dash_spv_ffi_client_get_transaction_count(client);
255+
assert_eq!(tx_count, 0, "Expected 0 transactions for new wallet");
256+
257+
// Cleanup
258+
dash_spv_ffi_client_destroy(client);
259+
dash_spv_ffi_config_destroy(config);
260+
}
261+
}
262+
263+
#[test]
264+
#[serial]
265+
fn test_blocks_with_transactions_count_with_empty_wallet() {
266+
unsafe {
267+
let (config, _temp_dir) = create_test_config_with_dir();
268+
let client = dash_spv_ffi_client_new(config);
269+
assert!(!client.is_null(), "Client creation failed");
270+
271+
// Should return 0 for a new wallet with no transactions
272+
let block_count = dash_spv_ffi_client_get_blocks_with_transactions_count(client);
273+
assert_eq!(block_count, 0, "Expected 0 blocks for new wallet");
274+
275+
// Cleanup
276+
dash_spv_ffi_client_destroy(client);
277+
dash_spv_ffi_config_destroy(config);
278+
}
279+
}
280+
281+
#[test]
282+
#[serial]
283+
fn test_transaction_count_with_null_client() {
284+
unsafe {
285+
// Should handle null client gracefully
286+
let tx_count = dash_spv_ffi_client_get_transaction_count(std::ptr::null_mut());
287+
assert_eq!(tx_count, 0, "Expected 0 for null client");
288+
}
289+
}
290+
291+
#[test]
292+
#[serial]
293+
fn test_blocks_count_with_null_client() {
294+
unsafe {
295+
// Should handle null client gracefully
296+
let block_count =
297+
dash_spv_ffi_client_get_blocks_with_transactions_count(std::ptr::null_mut());
298+
assert_eq!(block_count, 0, "Expected 0 for null client");
299+
}
300+
}
244301
}

dash-spv-ffi/tests/unit/test_type_conversions.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ mod tests {
172172
last_masternode_diff_height: None,
173173
sync_base_height: 0,
174174
synced_from_checkpoint: false,
175+
filter_matches: std::collections::BTreeMap::new(),
175176
};
176177

177178
let ffi_state = FFIChainState::from(state);

dash-spv/src/client/block_processor.rs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -177,11 +177,17 @@ impl<W: WalletInterface + Send + Sync + 'static, S: StorageManager + Send + Sync
177177
} => {
178178
// Check compact filter with wallet
179179
let mut wallet = self.wallet.write().await;
180-
let matches =
180+
let matched_wallet_ids =
181181
wallet.check_compact_filter(&filter, &block_hash, self.network).await;
182182

183-
if matches {
184-
tracing::info!("🎯 Compact filter matched for block {}", block_hash);
183+
let has_matches = !matched_wallet_ids.is_empty();
184+
185+
if has_matches {
186+
tracing::info!(
187+
"🎯 Compact filter matched for block {} ({} wallet(s))",
188+
block_hash,
189+
matched_wallet_ids.len()
190+
);
185191
drop(wallet);
186192
// Emit event if filter matched
187193
let _ = self.event_tx.send(SpvEvent::CompactFilterMatched {
@@ -196,7 +202,7 @@ impl<W: WalletInterface + Send + Sync + 'static, S: StorageManager + Send + Sync
196202
drop(wallet);
197203
}
198204

199-
let _ = response_tx.send(Ok(matches));
205+
let _ = response_tx.send(Ok(has_matches));
200206
}
201207
}
202208
}

dash-spv/src/client/block_processor_test.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,9 @@ mod tests {
6868
_filter: &dashcore::bip158::BlockFilter,
6969
_block_hash: &dashcore::BlockHash,
7070
_network: Network,
71-
) -> bool {
72-
// Return true for all filters in test
73-
true
71+
) -> Vec<[u8; 32]> {
72+
// Return a test wallet ID for all filters in test
73+
vec![[1u8; 32]]
7474
}
7575

7676
async fn describe(&self, _network: Network) -> String {
@@ -291,9 +291,9 @@ mod tests {
291291
_filter: &dashcore::bip158::BlockFilter,
292292
_block_hash: &dashcore::BlockHash,
293293
_network: Network,
294-
) -> bool {
295-
// Always return false - filter doesn't match
296-
false
294+
) -> Vec<[u8; 32]> {
295+
// Return empty vector - filter doesn't match
296+
Vec::new()
297297
}
298298

299299
async fn describe(&self, _network: Network) -> String {

dash-spv/src/storage/disk/filters.rs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,66 @@ impl DiskStorageManager {
182182
Ok(*self.cached_filter_tip_height.read().await)
183183
}
184184

185+
/// Get the highest stored compact filter height by scanning the filters directory.
186+
/// This checks which filters are actually persisted on disk, not just filter headers.
187+
///
188+
/// Returns None if no filters are stored, otherwise returns the highest height found.
189+
///
190+
/// Note: This only counts individual filter files ({height}.dat), not segment files.
191+
pub async fn get_stored_filter_height(&self) -> StorageResult<Option<u32>> {
192+
let filters_dir = self.base_path.join("filters");
193+
194+
// If filters directory doesn't exist, no filters are stored
195+
if !filters_dir.exists() {
196+
return Ok(None);
197+
}
198+
199+
let mut max_height: Option<u32> = None;
200+
201+
// Read directory entries
202+
let mut entries = tokio::fs::read_dir(&filters_dir).await?;
203+
204+
while let Some(entry) = entries.next_entry().await? {
205+
let path = entry.path();
206+
207+
// Skip if not a file
208+
if !path.is_file() {
209+
continue;
210+
}
211+
212+
// Check if it's a .dat file
213+
if let Some(extension) = path.extension() {
214+
if extension == "dat" {
215+
// Extract height from filename (format: "{height}.dat")
216+
if let Some(filename) = path.file_stem() {
217+
if let Some(filename_str) = filename.to_str() {
218+
// Only parse if filename is PURELY numeric (not "filter_segment_0001")
219+
// This ensures we only count individual filter files, not segments
220+
if filename_str.chars().all(|c| c.is_ascii_digit()) {
221+
if let Ok(height) = filename_str.parse::<u32>() {
222+
if height > 2_000_000 {
223+
// Sanity check - testnet/mainnet should never exceed 2M blocks
224+
tracing::warn!(
225+
"Found suspiciously high filter file: {}.dat (height {}), ignoring",
226+
filename_str,
227+
height
228+
);
229+
continue;
230+
}
231+
max_height = Some(
232+
max_height.map_or(height, |current| current.max(height)),
233+
);
234+
}
235+
}
236+
}
237+
}
238+
}
239+
}
240+
}
241+
242+
Ok(max_height)
243+
}
244+
185245
/// Store a compact filter.
186246
pub async fn store_filter(&mut self, height: u32, filter: &[u8]) -> StorageResult<()> {
187247
let path = self.base_path.join(format!("filters/{}.dat", height));

dash-spv/src/storage/disk/state.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -547,6 +547,10 @@ impl StorageManager for DiskStorageManager {
547547
Self::get_filter_tip_height(self).await
548548
}
549549

550+
async fn get_stored_filter_height(&self) -> StorageResult<Option<u32>> {
551+
Self::get_stored_filter_height(self).await
552+
}
553+
550554
async fn store_masternode_state(&mut self, state: &MasternodeState) -> StorageResult<()> {
551555
Self::store_masternode_state(self, state).await
552556
}

dash-spv/src/storage/memory.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,15 @@ impl StorageManager for MemoryStorageManager {
258258
}
259259
}
260260

261+
async fn get_stored_filter_height(&self) -> StorageResult<Option<u32>> {
262+
// For memory storage, find the highest filter in the HashMap
263+
if self.filters.is_empty() {
264+
Ok(None)
265+
} else {
266+
Ok(self.filters.keys().max().copied())
267+
}
268+
}
269+
261270
async fn store_masternode_state(&mut self, state: &MasternodeState) -> StorageResult<()> {
262271
self.masternode_state = Some(state.clone());
263272
Ok(())

dash-spv/src/storage/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,10 @@ pub trait StorageManager: Send + Sync {
127127
/// Get the current filter tip blockchain height.
128128
async fn get_filter_tip_height(&self) -> StorageResult<Option<u32>>;
129129

130+
/// Get the highest stored compact filter height by checking which filters are persisted.
131+
/// This is distinct from filter header tip - it shows which filters are actually downloaded.
132+
async fn get_stored_filter_height(&self) -> StorageResult<Option<u32>>;
133+
130134
/// Store masternode state.
131135
async fn store_masternode_state(&mut self, state: &MasternodeState) -> StorageResult<()>;
132136

0 commit comments

Comments
 (0)