Skip to content
Merged
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
44 changes: 35 additions & 9 deletions packages/rs-platform-wallet/src/spv/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use dashcore::sml::llmq_type::LLMQType;
use dashcore::{QuorumHash, Transaction};

use dash_spv::network::PeerNetworkManager;
use dash_spv::storage::DiskStorageManager;
use dash_spv::storage::{DiskStorageManager, StorageManager};
use dash_spv::sync::SyncProgress;
use dash_spv::{ClientConfig, DashSpvClient, EventHandler, Hash};

Expand All @@ -31,6 +31,7 @@ pub struct SpvRuntime {
event_manager: Arc<PlatformEventManager>,
wallet_manager: Arc<RwLock<WalletManager<PlatformWalletInfo>>>,
client: RwLock<Option<SpvClient>>,
last_config: RwLock<Option<ClientConfig>>,
task: Mutex<Option<JoinHandle<()>>>,
}
/// Classify a dash-spv broadcast failure per the
Expand Down Expand Up @@ -69,6 +70,7 @@ impl SpvRuntime {
event_manager,
wallet_manager,
client: RwLock::new(None),
last_config: RwLock::new(None),
task: Mutex::new(None),
}
}
Expand Down Expand Up @@ -96,6 +98,9 @@ impl SpvRuntime {
// platform event manager's own handler list).
let event_handlers: Vec<Arc<dyn EventHandler>> =
vec![Arc::clone(&self.event_manager) as Arc<dyn EventHandler>];

let retained_config = config.clone();

let spv_client = DashSpvClient::new(
config,
network_manager,
Expand All @@ -108,6 +113,7 @@ impl SpvRuntime {

let mut client = self.client.write().await;
*client = Some(spv_client);
*self.last_config.write().await = Some(retained_config);

Ok(())
}
Expand Down Expand Up @@ -283,17 +289,37 @@ impl SpvRuntime {

/// Clear all persisted SPV storage (headers, filters, state).
///
/// The SPV client must be running to perform this operation.
/// If the SPVClient is running it will be stopped and the
/// storage will be cleaned. If it is not running a tmp
/// Storage Manager built from the cached config will be used.
pub async fn clear_storage(&self) -> Result<(), PlatformWalletError> {
let client_guard = self.client.read().await;
let client = client_guard.as_ref().ok_or(PlatformWalletError::SpvError(
"SPV Client not started".to_string(),
))?;
// Fast path: a live client holds the storage lock; clear through it.
{
let client_guard = self.client.read().await;
if let Some(client) = client_guard.as_ref() {
return client
.clear_storage()
.await
.map_err(|e| PlatformWalletError::SpvError(e.to_string()));
}
}

client
.clear_storage()
let config = self.last_config.read().await.clone().ok_or_else(|| {
PlatformWalletError::SpvError(
"SPV storage location unknown; start the client at least once before clearing"
.to_string(),
)
})?;

let mut storage = DiskStorageManager::new(&config)
.await
.map_err(|e| PlatformWalletError::SpvError(e.to_string()))
.map_err(|e| PlatformWalletError::SpvError(e.to_string()))?;
StorageManager::clear(&mut storage)
.await
.map_err(|e| PlatformWalletError::SpvError(e.to_string()))?;
StorageManager::shutdown(&mut storage).await;

Ok(())
}

/// Update the running SPV client's configuration.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -237,8 +237,6 @@ extension PlatformWalletManager {
}

/// Clear all persisted SPV storage (headers, filters, state).
///
/// The SPV client must be running.
public func clearSpvStorage() throws {
try platform_wallet_manager_spv_clear_storage(handle).check()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import XCTest
@testable import SwiftDashSDK

/// Regression: clearing SPV storage must work *after* the client is stopped
final class SpvClearStorageIntegrationTests: IntegrationTestCase {
func testClearStorageAfterStopWipesDisk() async throws {
try await env.walletManager.startSpv(config: env.spvConfig)
try await env.walletManager.waitUntilUpToDate(
height: try await env.coreRPC.getBlockCount(),
timeout: 30
)

let before = Self.dirSignature(env.spvDataDir)
XCTAssertFalse(
before.isEmpty,
"expected synced SPV data on disk before clearing"
)

try await env.walletManager.stopSpv()
let running = try await env.walletManager.isSpvRunning()
XCTAssertFalse(running, "SPV should be stopped")

try await env.walletManager.clearSpvStorage()

let after = Self.dirSignature(env.spvDataDir)
XCTAssertNotEqual(
after, before,
"clearSpvStorage after stopSpv must wipe the synced data on disk"
)

// The stopped-path clear must release the storage lock: a fresh start
// on the same data dir must still succeed (no leftover lockfile).
try await env.walletManager.startSpv(config: env.spvConfig)
try await env.walletManager.stopSpv()
}

/// Sorted "relativePath:size" list of every regular file under `path`.
/// Mirrors the harness's own cache signature so a wipe is observable as a
/// change in the file/size set.
private static func dirSignature(_ path: String) -> String {
let fm = FileManager.default
let root = URL(fileURLWithPath: path).resolvingSymlinksInPath()
let rootCount = root.pathComponents.count

guard let walker = fm.enumerator(
at: root,
includingPropertiesForKeys: [.isRegularFileKey, .fileSizeKey]
) else { return "" }

var entries: [String] = []
for case let url as URL in walker {
let values = try? url.resourceValues(forKeys: [.isRegularFileKey, .fileSizeKey])
guard values?.isRegularFile == true else { continue }

let rel = url.resolvingSymlinksInPath().pathComponents
.dropFirst(rootCount)
.joined(separator: "/")

entries.append("\(rel):\(values?.fileSize ?? 0)")
}

return entries.sorted().joined(separator: "\n")
}
}
Loading