Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ class AppState: ObservableObject {
/// this counter and the spawned task only clears the flag when its
/// captured id still matches.
private var networkSwitchRequestID: UInt64 = 0
@Published private(set) var sdkRebuildRequestID: UInt64 = 0
@Published private(set) var readySDKRequestID: UInt64 = 0

@Published var currentNetwork: Network {
didSet {
Expand Down Expand Up @@ -53,6 +55,7 @@ class AppState: ObservableObject {
private func beginNetworkSwitch() {
networkSwitchRequestID &+= 1
let requestID = networkSwitchRequestID
sdkRebuildRequestID = requestID
isSwitchingNetwork = true
Task {
await switchNetwork(to: currentNetwork, requestID: requestID)
Expand Down Expand Up @@ -162,6 +165,7 @@ class AppState: ObservableObject {
// Load known contracts into the SDK's trusted provider
await loadKnownContractsIntoSDK(sdk: newSDK, modelContext: modelContext)
guard isCurrent(requestID) else { return }
readySDKRequestID = requestID

isLoading = false
} catch {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,12 +119,26 @@ struct ContentView: View {
}
.tag(RootTab.settings)
}
.disabled(platformState.isSwitchingNetwork)
.overlay(alignment: .top) {
let state = walletManager.spvProgress.overallState
if state == .syncing || state == .waitingForConnections {
GlobalSyncIndicator(showDetails: selectedTab == .sync && appUIState.showWalletsSyncDetails)
}
}
.overlay {
if platformState.isSwitchingNetwork {
ZStack {
Color.black.opacity(0.12)
.ignoresSafeArea()
ProgressView("Switching to \(platformState.currentNetwork.displayName)...")
.padding(.horizontal, 20)
.padding(.vertical, 14)
.background(.regularMaterial)
.clipShape(RoundedRectangle(cornerRadius: 12))
}
}
}
.onAppear { checkForOrphanMnemonic() }
.onChange(of: persistentWallets.count) { _, _ in
checkForOrphanMnemonic()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ final class AppUIState: ObservableObject {
@Published var showWalletsSyncDetails: Bool = true
}

private struct PendingWalletManagerActivation: Equatable {
let requestID: UInt64
let network: Network
}

@main
struct SwiftExampleAppApp: App {
// SwiftData container — shared across services and views.
Expand Down Expand Up @@ -52,6 +57,7 @@ struct SwiftExampleAppApp: App {
@State private var isInitialized = false
@State private var bootstrapError: Error?
@State private var bootstrapTask: Task<Void, Never>?
@State private var pendingWalletManagerActivation: PendingWalletManagerActivation?

/// Resolver that backs the platform-wallet-ffi `MnemonicResolverHandle`
/// for shielded wallet binding. Reuses the default `WalletStorage`
Expand Down Expand Up @@ -131,46 +137,70 @@ struct SwiftExampleAppApp: App {
})) { _, _ in
rebindWalletScopedServices()
}
// Network switch: activate the per-network manager
// first (the store lazy-creates one configured with
// a fresh SDK if this is the first time we see this
// network), then rebind the wallet-scoped services
// against it. Order matters — `rebindWalletScopedServices`
// reads `walletManager.firstWallet`, which has to
// resolve to the new network's manager before it
// runs.
.onChange(of: platformState.currentNetwork) { _, newNetwork in
activateManager(for: newNetwork)
rebindWalletScopedServices()
// Remember every SDK rebuild request, including
// same-network ones (for example regtest endpoint
// flips), but do not activate a wallet manager until
// AppState publishes the matching rebuilt SDK.
.onChange(of: platformState.sdkRebuildRequestID) { _, requestID in
guard requestID != 0 else { return }
pendingWalletManagerActivation = PendingWalletManagerActivation(
requestID: requestID,
network: platformState.currentNetwork
)
}
// Activate once the rebuilt SDK that corresponds to
// the pending request has been published.
.onChange(of: platformState.readySDKRequestID) { _, readyRequestID in
guard let pending = pendingWalletManagerActivation,
readyRequestID == pending.requestID,
pending.network == platformState.currentNetwork
else { return }
if activateManager(for: pending.network) {
pendingWalletManagerActivation = nil
rebindWalletScopedServices()
}
}
.onChange(of: platformState.isSwitchingNetwork) { _, isSwitching in
guard !isSwitching,
let pending = pendingWalletManagerActivation,
pending.requestID != platformState.readySDKRequestID
else { return }
pendingWalletManagerActivation = nil
}
}
}

/// Lazy-create + cache a `PlatformWalletManager` for `network`,
/// configured against a freshly-built per-network SDK. No-ops on
/// the already-active network. Called from bootstrap and from
/// `currentNetwork.onChange`.
///
/// The SDK is built locally rather than read from `platformState.sdk`
/// because the SwiftUI `.onChange` handler that calls this fires
/// synchronously the moment `currentNetwork` changes, while
/// `AppState.switchNetwork` rebuilds `platformState.sdk`
/// asynchronously — at this instant the shared SDK still points at
/// the previous network. `PlatformWalletManager` is network-locked
/// to its configure-time SDK for its lifetime and the cache is
/// never invalidated, so capturing the stale reference would
/// permanently bind the new network's manager to the previous
/// network's backend. Mirrors `WalletManagerStore.backgroundManager(for:)`.
/// configured against the SDK currently published by `AppState`.
/// No-ops on the already-active network. Called from bootstrap
/// and after a pending network change observes `isSwitchingNetwork`
/// drop, which guarantees the SDK has already been rebuilt for
/// the selected network.
@MainActor
private func activateManager(for network: Network) {
private func activateManager(for network: Network) -> Bool {
guard let sdk = platformState.sdk else {
SDKLogger.error(
"Cannot activate wallet manager for \(network.displayName): "
+ "no SDK available (still bootstrapping?)"
)
return false
}
guard sdk.network == network else {
SDKLogger.error(
"Cannot activate wallet manager for \(network.displayName): "
+ "SDK is still bound to \(sdk.network.displayName)"
)
return false
}
do {
let sdk = try SDK(network: network)
try walletManagerStore.activate(network: network, sdk: sdk)
return true
} catch {
SDKLogger.error(
"Failed to activate wallet manager for "
+ "\(network.displayName): \(error.localizedDescription)"
)
return false
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ import SwiftDashSDK
/// dev-focused example app where flipping is common.
@MainActor
final class WalletManagerStore: ObservableObject {
private struct ManagerEntry {
let sdkIdentity: ObjectIdentifier
let manager: PlatformWalletManager
}

/// Currently-active manager. Reassigned when the user switches
/// networks; SwiftUI re-injects this into the env object so
/// every `@EnvironmentObject var walletManager:
Expand All @@ -54,7 +59,7 @@ final class WalletManagerStore: ObservableObject {

/// Per-network managers. Lazily populated on first activation
/// of each network; lookup is O(1).
private var managers: [Network: PlatformWalletManager] = [:]
private var managers: [Network: ManagerEntry] = [:]

/// SwiftData container shared across every manager. Each
/// manager's persistence handler narrows its `loadWalletList`
Expand Down Expand Up @@ -85,9 +90,10 @@ final class WalletManagerStore: ObservableObject {
/// network) that need a manager for a non-active network without
/// triggering a user-visible network switch.
func activate(network: Network, sdk: SDK, makeActive: Bool = true) throws {
if let existing = managers[network] {
if makeActive && existing !== activeManager {
activeManager = existing
let sdkIdentity = ObjectIdentifier(sdk)
if let existing = managers[network], existing.sdkIdentity == sdkIdentity {
if makeActive && existing.manager !== activeManager {
activeManager = existing.manager
}
return
}
Expand All @@ -107,7 +113,10 @@ final class WalletManagerStore: ObservableObject {
+ "\(network.displayName): \(error.localizedDescription)"
)
}
managers[network] = manager
managers[network] = ManagerEntry(
sdkIdentity: sdkIdentity,
manager: manager
)
if makeActive {
activeManager = manager
}
Expand All @@ -118,7 +127,7 @@ final class WalletManagerStore: ObservableObject {
/// Memory Explorer) that want to inspect a specific network's
/// state without forcing it active.
func manager(for network: Network) -> PlatformWalletManager? {
managers[network]
managers[network]?.manager
}

/// Get-or-build the manager for `network` without changing the
Expand All @@ -135,11 +144,11 @@ final class WalletManagerStore: ObservableObject {
/// the testnet manager lands as a testnet row).
func backgroundManager(for network: Network) throws -> PlatformWalletManager {
if let existing = managers[network] {
return existing
return existing.manager
}
let sdk = try SDK(network: network)
try activate(network: network, sdk: sdk, makeActive: false)
guard let manager = managers[network] else {
guard let manager = managers[network]?.manager else {
throw PlatformWalletError.invalidParameter(
"Failed to materialize manager for \(network.displayName)"
)
Expand Down
Loading