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 @@ -64,6 +66,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 @@ -173,6 +176,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 @@ -138,17 +144,35 @@ 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
}
// Devnet→devnet rebuild from OptionsView: when the user
// edits the quorum URL / devnet name the SDK is rebuilt
Expand All @@ -166,30 +190,36 @@ struct SwiftExampleAppApp: App {
}

/// 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
Loading