11package to.bitkit.repositories
22
33import com.google.firebase.messaging.FirebaseMessaging
4+ import com.synonym.bitkitcore.ClosedChannelDetails
45import com.synonym.bitkitcore.FeeRates
56import com.synonym.bitkitcore.LightningInvoice
67import com.synonym.bitkitcore.Scanner
@@ -27,6 +28,7 @@ import org.lightningdevkit.ldknode.BalanceDetails
2728import org.lightningdevkit.ldknode.BestBlock
2829import org.lightningdevkit.ldknode.ChannelConfig
2930import org.lightningdevkit.ldknode.ChannelDetails
31+ import org.lightningdevkit.ldknode.Event
3032import org.lightningdevkit.ldknode.NodeStatus
3133import org.lightningdevkit.ldknode.PaymentDetails
3234import org.lightningdevkit.ldknode.PaymentId
@@ -57,6 +59,7 @@ import to.bitkit.services.NodeEventHandler
5759import to.bitkit.utils.AppError
5860import to.bitkit.utils.Logger
5961import to.bitkit.utils.ServiceError
62+ import java.util.concurrent.ConcurrentHashMap
6063import javax.inject.Inject
6164import javax.inject.Singleton
6265import kotlin.time.Duration
@@ -85,6 +88,8 @@ class LightningRepo @Inject constructor(
8588 private val _isRecoveryMode = MutableStateFlow (false )
8689 val isRecoveryMode = _isRecoveryMode .asStateFlow()
8790
91+ private val channelCache = ConcurrentHashMap <String , ChannelDetails >()
92+
8893 /* *
8994 * Executes the provided operation only if the node is running.
9095 * If the node is not running, waits for it to be running for a specified timeout.
@@ -203,12 +208,16 @@ class LightningRepo @Inject constructor(
203208 if (getStatus()?.isRunning == true ) {
204209 Logger .info(" LDK node already running" , context = TAG )
205210 _lightningState .update { it.copy(nodeLifecycleState = NodeLifecycleState .Running ) }
206- lightningService.listenForEvents(onEvent = eventHandler)
211+ lightningService.listenForEvents(onEvent = { event ->
212+ handleLdkEvent(event)
213+ eventHandler?.invoke(event)
214+ })
207215 return @withContext Result .success(Unit )
208216 }
209217
210218 // Start the node service
211219 lightningService.start(timeout) { event ->
220+ handleLdkEvent(event)
212221 eventHandler?.invoke(event)
213222 ldkNodeEventBus.emit(event)
214223 }
@@ -220,6 +229,7 @@ class LightningRepo @Inject constructor(
220229 // Initial state sync
221230 syncState()
222231 updateGeoBlockState()
232+ refreshChannelCache()
223233
224234 // Perform post-startup tasks
225235 connectToTrustedPeers().onFailure { e ->
@@ -296,12 +306,96 @@ class LightningRepo @Inject constructor(
296306
297307 _lightningState .update { it.copy(isSyncingWallet = true ) }
298308 lightningService.sync()
309+ refreshChannelCache()
299310 syncState()
300311 _lightningState .update { it.copy(isSyncingWallet = false ) }
301312
302313 Result .success(Unit )
303314 }
304315
316+ private suspend fun refreshChannelCache () = withContext(bgDispatcher) {
317+ val channels = lightningService.channels ? : return @withContext
318+ channels.forEach { channel ->
319+ channelCache[channel.channelId] = channel
320+ }
321+ }
322+
323+ private fun handleLdkEvent (event : Event ) {
324+ when (event) {
325+ is Event .ChannelPending -> {
326+ scope.launch {
327+ refreshChannelCache()
328+ }
329+ }
330+ is Event .ChannelReady -> {
331+ scope.launch {
332+ refreshChannelCache()
333+ }
334+ }
335+ is Event .ChannelClosed -> {
336+ val channelId = event.channelId
337+ val reason = event.reason?.toString() ? : " "
338+ scope.launch {
339+ registerClosedChannel(channelId, reason)
340+ }
341+ }
342+ else -> {
343+ // Other events don't need special handling
344+ }
345+ }
346+ }
347+
348+ private suspend fun registerClosedChannel (channelId : String , reason : String? ) = withContext(bgDispatcher) {
349+ try {
350+ val channel = channelCache[channelId] ? : run {
351+ Logger .error(
352+ " Could not find channel details for closed channel: channelId=$channelId " ,
353+ context = TAG
354+ )
355+ return @withContext
356+ }
357+
358+ val fundingTxo = channel.fundingTxo
359+ if (fundingTxo == null ) {
360+ Logger .error(
361+ " Channel has no funding transaction, cannot persist closed channel: channelId=$channelId " ,
362+ context = TAG
363+ )
364+ return @withContext
365+ }
366+
367+ val channelName = channel.inboundScidAlias?.toString()
368+ ? : channel.channelId.take(CHANNEL_ID_PREVIEW_LENGTH ) + " …"
369+
370+ val closedAt = (System .currentTimeMillis() / 1000L ).toULong()
371+
372+ val closedChannel = ClosedChannelDetails (
373+ channelId = channel.channelId,
374+ counterpartyNodeId = channel.counterpartyNodeId,
375+ fundingTxoTxid = fundingTxo.txid,
376+ fundingTxoIndex = fundingTxo.vout,
377+ channelValueSats = channel.channelValueSats,
378+ closedAt = closedAt,
379+ outboundCapacityMsat = channel.outboundCapacityMsat,
380+ inboundCapacityMsat = channel.inboundCapacityMsat,
381+ counterpartyUnspendablePunishmentReserve = channel.counterpartyUnspendablePunishmentReserve,
382+ unspendablePunishmentReserve = channel.unspendablePunishmentReserve ? : 0u ,
383+ forwardingFeeProportionalMillionths = channel.config.forwardingFeeProportionalMillionths,
384+ forwardingFeeBaseMsat = channel.config.forwardingFeeBaseMsat,
385+ channelName = channelName,
386+ channelClosureReason = reason.orEmpty()
387+ )
388+
389+ coreService.activity.upsertClosedChannelList(listOf (closedChannel))
390+
391+ channelCache.remove(channelId)
392+
393+ Logger .info(" Registered closed channel: ${channel.userChannelId} " , context = TAG )
394+ } catch (e: Throwable ) {
395+ Logger .error(" Failed to register closed channel: $e " , e, context = TAG )
396+ }
397+ }
398+
305399 suspend fun wipeStorage (walletIndex : Int ): Result <Unit > = withContext(bgDispatcher) {
306400 Logger .debug(" wipeStorage called, stopping node first" , context = TAG )
307401 stop().onSuccess {
@@ -867,6 +961,7 @@ class LightningRepo @Inject constructor(
867961 companion object {
868962 private const val TAG = " LightningRepo"
869963 private const val SYNC_TIMEOUT_MS = 20_000L
964+ private const val CHANNEL_ID_PREVIEW_LENGTH = 10
870965 }
871966}
872967
0 commit comments