Skip to content

Commit aadc31e

Browse files
committed
Add closed channels
1 parent d7ca7ad commit aadc31e

File tree

5 files changed

+213
-44
lines changed

5 files changed

+213
-44
lines changed

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

Lines changed: 80 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
@@ -57,6 +59,7 @@ import kotlin.time.Duration.Companion.seconds
5759

5860
typealias NodeEventHandler = suspend (Event) -> Unit
5961

62+
@Suppress("LargeClass")
6063
@Singleton
6164
class LightningService @Inject constructor(
6265
@BgDispatcher private val bgDispatcher: CoroutineDispatcher,
@@ -70,6 +73,8 @@ class LightningService @Inject constructor(
7073

7174
private lateinit var trustedPeers: List<PeerDetails>
7275

76+
private val channelCache = mutableMapOf<String, ChannelDetails>()
77+
7378
suspend fun setup(
7479
walletIndex: Int,
7580
customServerUrl: String? = null,
@@ -190,6 +195,7 @@ class LightningService @Inject constructor(
190195
}
191196
}
192197

198+
refreshChannelCache()
193199
Logger.info("Node started")
194200
}
195201

@@ -225,9 +231,73 @@ class LightningService @Inject constructor(
225231
node.syncWallets()
226232
// launch { setMaxDustHtlcExposureForCurrentChannels() }
227233
}
234+
refreshChannelCache()
235+
228236
Logger.debug("LDK synced")
229237
}
230238

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

743816
is Event.ChannelReady -> {
@@ -747,16 +820,22 @@ class LightningService @Inject constructor(
747820
Logger.info(
748821
"👐 Channel ready: channelId: $channelId userChannelId: $userChannelId counterpartyNodeId: $counterpartyNodeId"
749822
)
823+
launch {
824+
refreshChannelCache()
825+
}
750826
}
751827

752828
is Event.ChannelClosed -> {
753829
val channelId = event.channelId
754830
val userChannelId = event.userChannelId
755831
val counterpartyNodeId = event.counterpartyNodeId ?: "?"
756-
val reason = event.reason
832+
val reason = event.reason?.toString() ?: ""
757833
Logger.info(
758834
"⛔ Channel closed: channelId: $channelId userChannelId: $userChannelId counterpartyNodeId: $counterpartyNodeId reason: $reason"
759835
)
836+
launch {
837+
registerClosedChannel(channelId, reason)
838+
}
760839
}
761840
}
762841
}

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.reversed(),
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)