Skip to content

Commit 23f55e0

Browse files
authored
Merge pull request #472 from synonymdev/feat/closed-channels
Add closed channels
2 parents 9a03139 + cffcd73 commit 23f55e0

File tree

5 files changed

+221
-44
lines changed

5 files changed

+221
-44
lines changed

app/src/main/java/to/bitkit/services/LightningService.kt

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package to.bitkit.services
22

3+
import com.synonym.bitkitcore.ClosedChannelDetails
4+
import com.synonym.bitkitcore.upsertClosedChannel
35
import kotlinx.coroutines.CoroutineDispatcher
46
import kotlinx.coroutines.currentCoroutineContext
57
import kotlinx.coroutines.delay
@@ -49,6 +51,7 @@ import to.bitkit.utils.LdkError
4951
import to.bitkit.utils.LdkLogWriter
5052
import to.bitkit.utils.Logger
5153
import to.bitkit.utils.ServiceError
54+
import java.util.concurrent.ConcurrentHashMap
5255
import javax.inject.Inject
5356
import javax.inject.Singleton
5457
import kotlin.io.path.Path
@@ -57,6 +60,7 @@ import kotlin.time.Duration.Companion.seconds
5760

5861
typealias NodeEventHandler = suspend (Event) -> Unit
5962

63+
@Suppress("LargeClass")
6064
@Singleton
6165
class LightningService @Inject constructor(
6266
@BgDispatcher private val bgDispatcher: CoroutineDispatcher,
@@ -70,6 +74,8 @@ class LightningService @Inject constructor(
7074

7175
private lateinit var trustedPeers: List<PeerDetails>
7276

77+
private val channelCache = ConcurrentHashMap<String, ChannelDetails>()
78+
7379
suspend fun setup(
7480
walletIndex: Int,
7581
customServerUrl: String? = null,
@@ -190,6 +196,7 @@ class LightningService @Inject constructor(
190196
}
191197
}
192198

199+
refreshChannelCache()
193200
Logger.info("Node started")
194201
}
195202

@@ -225,9 +232,79 @@ class LightningService @Inject constructor(
225232
node.syncWallets()
226233
// launch { setMaxDustHtlcExposureForCurrentChannels() }
227234
}
235+
refreshChannelCache()
236+
228237
Logger.debug("LDK synced")
229238
}
230239

240+
private suspend fun refreshChannelCache() {
241+
val node = this.node ?: return
242+
243+
ServiceQueue.LDK.background {
244+
val channels = node.listChannels()
245+
channels.forEach { channel ->
246+
channelCache[channel.channelId] = channel
247+
}
248+
}
249+
}
250+
251+
private suspend fun registerClosedChannel(channelId: String, reason: String?) {
252+
try {
253+
val channel = ServiceQueue.LDK.background {
254+
channelCache[channelId]
255+
} ?: run {
256+
Logger.error(
257+
"Could not find channel details for closed channel: channelId=$channelId",
258+
context = TAG
259+
)
260+
return@registerClosedChannel
261+
}
262+
263+
val fundingTxo = channel.fundingTxo
264+
if (fundingTxo == null) {
265+
Logger.error(
266+
"Channel has no funding transaction, cannot persist closed channel: channelId=$channelId",
267+
context = TAG
268+
)
269+
return@registerClosedChannel
270+
}
271+
272+
val channelName = channel.inboundScidAlias?.toString()
273+
?: channel.channelId.take(CHANNEL_ID_PREVIEW_LENGTH) + ""
274+
275+
val closedAt = (System.currentTimeMillis() / 1000L).toULong()
276+
277+
val closedChannel = ClosedChannelDetails(
278+
channelId = channel.channelId,
279+
counterpartyNodeId = channel.counterpartyNodeId,
280+
fundingTxoTxid = fundingTxo.txid,
281+
fundingTxoIndex = fundingTxo.vout,
282+
channelValueSats = channel.channelValueSats,
283+
closedAt = closedAt,
284+
outboundCapacityMsat = channel.outboundCapacityMsat,
285+
inboundCapacityMsat = channel.inboundCapacityMsat,
286+
counterpartyUnspendablePunishmentReserve = channel.counterpartyUnspendablePunishmentReserve,
287+
unspendablePunishmentReserve = channel.unspendablePunishmentReserve ?: 0u,
288+
forwardingFeeProportionalMillionths = channel.config.forwardingFeeProportionalMillionths,
289+
forwardingFeeBaseMsat = channel.config.forwardingFeeBaseMsat,
290+
channelName = channelName,
291+
channelClosureReason = reason.orEmpty()
292+
)
293+
294+
ServiceQueue.CORE.background {
295+
upsertClosedChannel(closedChannel)
296+
}
297+
298+
ServiceQueue.LDK.background {
299+
channelCache.remove(channelId)
300+
}
301+
302+
Logger.info("Registered closed channel: ${channel.userChannelId}", context = TAG)
303+
} catch (e: Exception) {
304+
Logger.error("Failed to register closed channel: $e", e, context = TAG)
305+
}
306+
}
307+
231308
// private fun setMaxDustHtlcExposureForCurrentChannels() {
232309
// if (Env.network != Network.REGTEST) {
233310
// Logger.debug("Not updating channel config for non-regtest network")
@@ -738,6 +815,9 @@ class LightningService @Inject constructor(
738815
Logger.info(
739816
"⏳ Channel pending: channelId: $channelId userChannelId: $userChannelId formerTemporaryChannelId: $formerTemporaryChannelId counterpartyNodeId: $counterpartyNodeId fundingTxo: $fundingTxo"
740817
)
818+
launch {
819+
refreshChannelCache()
820+
}
741821
}
742822

743823
is Event.ChannelReady -> {
@@ -747,16 +827,22 @@ class LightningService @Inject constructor(
747827
Logger.info(
748828
"👐 Channel ready: channelId: $channelId userChannelId: $userChannelId counterpartyNodeId: $counterpartyNodeId"
749829
)
830+
launch {
831+
refreshChannelCache()
832+
}
750833
}
751834

752835
is Event.ChannelClosed -> {
753836
val channelId = event.channelId
754837
val userChannelId = event.userChannelId
755838
val counterpartyNodeId = event.counterpartyNodeId ?: "?"
756-
val reason = event.reason
839+
val reason = event.reason?.toString() ?: ""
757840
Logger.info(
758841
"⛔ Channel closed: channelId: $channelId userChannelId: $userChannelId counterpartyNodeId: $counterpartyNodeId reason: $reason"
759842
)
843+
launch {
844+
registerClosedChannel(channelId, reason)
845+
}
760846
}
761847
}
762848
}
@@ -781,6 +867,7 @@ class LightningService @Inject constructor(
781867

782868
companion object {
783869
private const val TAG = "LightningService"
870+
private const val CHANNEL_ID_PREVIEW_LENGTH = 10
784871
}
785872
}
786873

app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,8 @@ fun ChannelDetailScreen(
9797

9898
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
9999
val paidOrders by viewModel.blocktankRepo.blocktankState.collectAsStateWithLifecycle()
100+
101+
val isClosedChannel = uiState.closedChannels.any { it.details.channelId == channel.details.channelId }
100102
val txDetails by viewModel.txDetails.collectAsStateWithLifecycle()
101103
val walletState by wallet.uiState.collectAsStateWithLifecycle()
102104

@@ -113,6 +115,7 @@ fun ChannelDetailScreen(
113115
cjitEntries = paidOrders.cjitEntries,
114116
txDetails = txDetails,
115117
isRefreshing = uiState.isRefreshing,
118+
isClosedChannel = isClosedChannel,
116119
onBack = { navController.popBackStack() },
117120
onClose = { navController.navigateToHome() },
118121
onRefresh = {
@@ -137,13 +140,15 @@ fun ChannelDetailScreen(
137140
}
138141

139142
@OptIn(ExperimentalMaterial3Api::class)
143+
@Suppress("CyclomaticComplexMethod")
140144
@Composable
141145
private fun Content(
142146
channel: ChannelUi,
143147
blocktankOrders: List<IBtOrder> = emptyList(),
144148
cjitEntries: List<IcJitEntry> = emptyList(),
145149
txDetails: TxDetails? = null,
146150
isRefreshing: Boolean = false,
151+
isClosedChannel: Boolean = false,
147152
onBack: () -> Unit = {},
148153
onClose: () -> Unit = {},
149154
onRefresh: () -> Unit = {},
@@ -201,7 +206,7 @@ private fun Content(
201206
capacity = capacity,
202207
localBalance = localBalance,
203208
remoteBalance = remoteBalance,
204-
status = getChannelStatus(channel, blocktankOrder),
209+
status = getChannelStatus(channel, blocktankOrder, isClosedChannel),
205210
)
206211
VerticalSpacer(32.dp)
207212
HorizontalDivider()
@@ -211,6 +216,7 @@ private fun Content(
211216
ChannelStatusView(
212217
channel = channel,
213218
blocktankOrder = blocktankOrder,
219+
isClosedChannel = isClosedChannel,
214220
)
215221
VerticalSpacer(16.dp)
216222
HorizontalDivider()
@@ -447,16 +453,15 @@ private fun Content(
447453
onClick = { onCopyText(channel.details.counterpartyNodeId) }
448454
)
449455

450-
// TODO add closure reason when tracking closed channels
451-
// val channelClosureReason: String? = null
452-
// if (channelClosureReason != null) {
453-
// SectionRow(
454-
// name = stringResource(R.string.lightning__closure_reason),
455-
// valueContent = {
456-
// CaptionB(text = channelClosureReason)
457-
// },
458-
// )
459-
// }
456+
// Closure reason for closed channels
457+
channel.closureReason?.let { closureReason ->
458+
SectionRow(
459+
name = stringResource(R.string.lightning__closure_reason),
460+
valueContent = {
461+
CaptionB(text = closureReason)
462+
},
463+
)
464+
}
460465

461466
// Action Buttons
462467
FillHeight()
@@ -526,7 +531,12 @@ private fun SectionRow(
526531
private fun getChannelStatus(
527532
channel: ChannelUi,
528533
blocktankOrder: IBtOrder?,
534+
isClosedChannel: Boolean = false,
529535
): ChannelStatusUi {
536+
if (isClosedChannel) {
537+
return ChannelStatusUi.CLOSED
538+
}
539+
530540
blocktankOrder?.let { order ->
531541
when {
532542
order.state2 == BtOrderState2.EXPIRED ||

app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsScreen.kt

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import androidx.compose.runtime.remember
2828
import androidx.compose.runtime.setValue
2929
import androidx.compose.ui.Alignment
3030
import androidx.compose.ui.Modifier
31+
import androidx.compose.ui.draw.alpha
3132
import androidx.compose.ui.graphics.Color
3233
import androidx.compose.ui.graphics.vector.ImageVector
3334
import androidx.compose.ui.platform.LocalContext
@@ -63,6 +64,8 @@ import to.bitkit.ui.shared.util.shareZipFile
6364
import to.bitkit.ui.theme.AppThemeSurface
6465
import to.bitkit.ui.theme.Colors
6566

67+
private const val CLOSED_CHANNEL_ALPHA = 0.64f
68+
6669
object LightningConnectionsTestTags {
6770
const val SCREEN = "lightning_connections_screen"
6871
const val ADD_CONNECTION_BUTTON = "add_connection_button"
@@ -188,8 +191,21 @@ private fun Content(
188191
}
189192
}
190193

194+
// Closed Channels Section
195+
AnimatedVisibility(visible = showClosed && uiState.closedChannels.isNotEmpty()) {
196+
Column {
197+
VerticalSpacer(16.dp)
198+
Caption13Up(stringResource(R.string.lightning__conn_closed), color = Colors.White64)
199+
ChannelList(
200+
status = ChannelStatusUi.CLOSED,
201+
channels = uiState.closedChannels,
202+
onClickChannel = onClickChannel,
203+
)
204+
}
205+
}
206+
191207
// Show/Hide Closed Channels Button
192-
if (uiState.failedOrders.isNotEmpty()) {
208+
if (uiState.failedOrders.isNotEmpty() || uiState.closedChannels.isNotEmpty()) {
193209
VerticalSpacer(16.dp)
194210
TertiaryButton(
195211
text = stringResource(
@@ -295,6 +311,13 @@ private fun ChannelItem(
295311
.fillMaxWidth()
296312
.clickableAlpha { onClick() }
297313
.testTag("Channel")
314+
.then(
315+
if (status == ChannelStatusUi.CLOSED) {
316+
Modifier.alpha(CLOSED_CHANNEL_ALPHA)
317+
} else {
318+
Modifier
319+
}
320+
)
298321
) {
299322
VerticalSpacer(16.dp)
300323
Row(

0 commit comments

Comments
 (0)