11package to.bitkit.services
22
3+ import android.content.Context
34import com.synonym.bitkitcore.AddressType
5+ import dagger.hilt.android.qualifiers.ApplicationContext
46import kotlinx.coroutines.CoroutineDispatcher
57import kotlinx.coroutines.Job
68import kotlinx.coroutines.cancelAndJoin
@@ -71,6 +73,7 @@ typealias NodeEventHandler = suspend (Event) -> Unit
7173@Suppress(" LargeClass" , " TooManyFunctions" )
7274@Singleton
7375class LightningService @Inject constructor(
76+ @ApplicationContext private val context : Context ,
7477 @BgDispatcher private val bgDispatcher : CoroutineDispatcher ,
7578 private val keychain : Keychain ,
7679 private val vssStoreIdProvider : VssStoreIdProvider ,
@@ -81,8 +84,17 @@ class LightningService @Inject constructor(
8184 companion object {
8285 private const val TAG = " LightningService"
8386 private const val NODE_ID_PREVIEW_LEN = 20
87+ private const val STALE_MONITOR_RECOVERY_ATTEMPTED_KEY = " staleMonitorRecoveryAttempted"
8488 }
8589
90+ private val prefs by lazy {
91+ context.getSharedPreferences(" lightning_recovery" , Context .MODE_PRIVATE )
92+ }
93+
94+ private var staleMonitorRecoveryAttempted: Boolean
95+ get() = prefs.getBoolean(STALE_MONITOR_RECOVERY_ATTEMPTED_KEY , false )
96+ set(value) = prefs.edit().putBoolean(STALE_MONITOR_RECOVERY_ATTEMPTED_KEY , value).apply ()
97+
8698 @Volatile
8799 var node: Node ? = null
88100
@@ -177,25 +189,53 @@ class LightningService @Inject constructor(
177189 passphrase = keychain.loadString(Keychain .Key .BIP39_PASSPHRASE .name),
178190 )
179191 }
180- try {
181- val vssStoreId = vssStoreIdProvider.getVssStoreId(walletIndex)
182- val vssUrl = Env .vssServerUrl
183- val lnurlAuthServerUrl = Env .lnurlAuthServerUrl
184- val fixedHeaders = emptyMap<String , String >()
185- Logger .verbose(
186- " Building node with \n\t vssUrl: '$vssUrl '\n\t lnurlAuthServerUrl: '$lnurlAuthServerUrl '" ,
192+ val vssStoreId = vssStoreIdProvider.getVssStoreId(walletIndex)
193+ val vssUrl = Env .vssServerUrl
194+ val lnurlAuthServerUrl = Env .lnurlAuthServerUrl
195+ val fixedHeaders = emptyMap<String , String >()
196+ Logger .verbose(
197+ " Building node with \n\t vssUrl: '$vssUrl '\n\t lnurlAuthServerUrl: '$lnurlAuthServerUrl '" ,
198+ context = TAG ,
199+ )
200+
201+ fun buildNode () = if (lnurlAuthServerUrl.isNotEmpty()) {
202+ builder.buildWithVssStore(vssUrl, vssStoreId, lnurlAuthServerUrl, fixedHeaders)
203+ } else {
204+ builder.buildWithVssStoreAndFixedHeaders(vssUrl, vssStoreId, fixedHeaders)
205+ }
206+
207+ val node = try {
208+ buildNode()
209+ } catch (e: BuildException .ReadFailed ) {
210+ if (staleMonitorRecoveryAttempted) throw LdkError (e)
211+
212+ // Build failed with ReadFailed — likely a stale ChannelMonitor (DangerousValue).
213+ // Retry once with accept_stale_channel_monitors for one-time recovery.
214+ Logger .warn(
215+ " Build failed with ReadFailed. Retrying with accept_stale_channel_monitors for one-time recovery." ,
216+ e,
187217 context = TAG ,
188218 )
189- if (lnurlAuthServerUrl.isNotEmpty()) {
190- builder.buildWithVssStore(vssUrl, vssStoreId, lnurlAuthServerUrl, fixedHeaders)
191- } else {
192- builder.buildWithVssStoreAndFixedHeaders(vssUrl, vssStoreId, fixedHeaders)
219+ staleMonitorRecoveryAttempted = true
220+ builder.setAcceptStaleChannelMonitors(true )
221+ try {
222+ val recovered = buildNode()
223+ Logger .info(" Stale monitor recovery: build succeeded with accept_stale" , context = TAG )
224+ recovered
225+ } catch (retryError: BuildException ) {
226+ throw LdkError (retryError)
193227 }
194228 } catch (e: BuildException ) {
195229 throw LdkError (e)
196- } finally {
197- // TODO: cleanup sensitive data after implementing a `SecureString` value holder for Keychain return values
198230 }
231+
232+ // Mark recovery as attempted after any successful build (whether recovery was needed or not).
233+ // This ensures unaffected users never trigger the retry path on future startups.
234+ if (! staleMonitorRecoveryAttempted) {
235+ staleMonitorRecoveryAttempted = true
236+ }
237+
238+ node
199239 }
200240
201241 private suspend fun Builder.configureGossipSource (customRgsServerUrl : String? ) {
0 commit comments