@@ -9,6 +9,17 @@ class LightningService {
99 private var node : Node ?
1010 var currentWalletIndex : Int = 0
1111
12+ // MARK: - Stale monitor recovery (one-time recovery for channel monitor desync)
13+
14+ private static let staleMonitorRecoveryAttemptedKey = " staleMonitorRecoveryAttempted "
15+
16+ /// Whether we've already attempted stale monitor recovery (prevents infinite retry).
17+ /// Persisted so the retry only happens once, even across app restarts.
18+ private static var staleMonitorRecoveryAttempted : Bool {
19+ get { UserDefaults . standard. bool ( forKey: staleMonitorRecoveryAttemptedKey) }
20+ set { UserDefaults . standard. set ( newValue, forKey: staleMonitorRecoveryAttemptedKey) }
21+ }
22+
1223 private let syncStatusChangedSubject = PassthroughSubject < UInt64 , Never > ( )
1324
1425 private var channelCache : [ String : ChannelDetails ] = [ : ]
@@ -124,22 +135,59 @@ class LightningService {
124135 builder. setEntropyBip39Mnemonic ( mnemonic: mnemonic, passphrase: passphrase)
125136
126137 try await ServiceQueue . background ( . ldk) {
127- if !lnurlAuthServerUrl. isEmpty {
128- self . node = try builder. buildWithVssStore (
129- vssUrl: vssUrl,
130- storeId: storeId,
131- lnurlAuthServerUrl: lnurlAuthServerUrl,
132- fixedHeaders: [ : ]
133- )
134- } else {
135- self . node = try builder. buildWithVssStoreAndFixedHeaders (
136- vssUrl: vssUrl,
137- storeId: storeId,
138- fixedHeaders: [ : ]
138+ do {
139+ if !lnurlAuthServerUrl. isEmpty {
140+ self . node = try builder. buildWithVssStore (
141+ vssUrl: vssUrl,
142+ storeId: storeId,
143+ lnurlAuthServerUrl: lnurlAuthServerUrl,
144+ fixedHeaders: [ : ]
145+ )
146+ } else {
147+ self . node = try builder. buildWithVssStoreAndFixedHeaders (
148+ vssUrl: vssUrl,
149+ storeId: storeId,
150+ fixedHeaders: [ : ]
151+ )
152+ }
153+ } catch let error as BuildError {
154+ guard case . ReadFailed = error, !Self. staleMonitorRecoveryAttempted else {
155+ throw error
156+ }
157+
158+ // Build failed with ReadFailed — likely a stale ChannelMonitor (DangerousValue).
159+ // Retry once with accept_stale_channel_monitors to recover.
160+ Logger . warn (
161+ " Build failed with ReadFailed. Retrying with accept_stale_channel_monitors for one-time recovery. " ,
162+ context: " Recovery "
139163 )
164+ Self . staleMonitorRecoveryAttempted = true
165+ builder. setAcceptStaleChannelMonitors ( accept: true )
166+
167+ if !lnurlAuthServerUrl. isEmpty {
168+ self . node = try builder. buildWithVssStore (
169+ vssUrl: vssUrl,
170+ storeId: storeId,
171+ lnurlAuthServerUrl: lnurlAuthServerUrl,
172+ fixedHeaders: [ : ]
173+ )
174+ } else {
175+ self . node = try builder. buildWithVssStoreAndFixedHeaders (
176+ vssUrl: vssUrl,
177+ storeId: storeId,
178+ fixedHeaders: [ : ]
179+ )
180+ }
181+ Logger . info ( " Stale monitor recovery: build succeeded with accept_stale " , context: " Recovery " )
140182 }
141183 }
142184
185+ // Mark recovery as attempted after any successful build (whether recovery was needed or not).
186+ // This ensures unaffected users never trigger the retry path on future startups.
187+ if !Self. staleMonitorRecoveryAttempted {
188+ Self . staleMonitorRecoveryAttempted = true
189+ }
190+
143191 Logger . info ( " LDK node setup " )
144192
145193 // Clear memory
0 commit comments