From 9e99b0729ededec42d989fc5840f37179dfe3a4f Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Tue, 9 Jun 2026 14:26:01 -0300 Subject: [PATCH 1/3] fix: show short channel id in connection details --- .../main/java/to/bitkit/ext/ShortChannelId.kt | 21 +++++++ .../settings/lightning/ChannelDetailScreen.kt | 36 +++++++---- .../java/to/bitkit/ext/ShortChannelIdTest.kt | 61 +++++++++++++++++++ changelog.d/next/702.fixed.md | 1 + 4 files changed, 107 insertions(+), 12 deletions(-) create mode 100644 app/src/main/java/to/bitkit/ext/ShortChannelId.kt create mode 100644 app/src/test/java/to/bitkit/ext/ShortChannelIdTest.kt create mode 100644 changelog.d/next/702.fixed.md diff --git a/app/src/main/java/to/bitkit/ext/ShortChannelId.kt b/app/src/main/java/to/bitkit/ext/ShortChannelId.kt new file mode 100644 index 0000000000..b2595add64 --- /dev/null +++ b/app/src/main/java/to/bitkit/ext/ShortChannelId.kt @@ -0,0 +1,21 @@ +package to.bitkit.ext + +/** + * Decodes a BOLT short channel id into Core Lightning `block x tx x output` form + * (e.g. `777477x916x0`): block height in the high 24 bits, transaction index in the next 24, + * funding output index in the low 16. + */ +fun ULong.formattedAsShortChannelId(): String { + val blockHeight = this shr 40 + val txIndex = (this shr 16) and 0xFFFFFFu + val outputIndex = this and 0xFFFFu + return "${blockHeight}x${txIndex}x$outputIndex" +} + +/** + * Short channel id for display. Uses the channel's own scid (open channels) and, for closed + * channels which carry none, the scid from the confidently-linked order. Null when unavailable. + */ +internal fun resolveDisplayShortChannelId(channelScid: ULong?, linkedOrderScid: String?): String? = + channelScid?.formattedAsShortChannelId() + ?: linkedOrderScid?.toULongOrNull()?.formattedAsShortChannelId() diff --git a/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt b/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt index a41243741d..ae3bea0b6e 100644 --- a/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt @@ -59,6 +59,7 @@ import to.bitkit.env.Env import to.bitkit.ext.DatePattern import to.bitkit.ext.amountOnClose import to.bitkit.ext.createChannelDetails +import to.bitkit.ext.resolveDisplayShortChannelId import to.bitkit.ext.setClipboardText import to.bitkit.models.Toast import to.bitkit.models.msatFloorOf @@ -220,6 +221,15 @@ private fun ChannelDetailContent( val order = blocktankOrder ?: cjitEntry + val linkedOrderScid = when (order) { + is IBtOrder -> order.channel?.shortChannelId + is IcJitEntry -> order.channel?.shortChannelId + else -> null + } + val displayShortChannelId = remember(channel, linkedOrderScid) { + resolveDisplayShortChannelId(channel.details.shortChannelId, linkedOrderScid) + } + val capacity = channel.details.channelValueSats.toLong() val localBalance = channel.details.amountOnClose.toLong() val remoteBalance = msatFloorOf(channel.details.inboundCapacityMsat).toLong() @@ -429,18 +439,20 @@ private fun ChannelDetailContent( ) } - SectionRow( - name = stringResource(R.string.lightning__channel_id), - valueContent = { - CaptionB( - text = channel.details.channelId, - maxLines = 1, - overflow = TextOverflow.MiddleEllipsis, - textAlign = TextAlign.End, - ) - }, - onClick = { onCopyText(channel.details.channelId) } - ) + displayShortChannelId?.let { scid -> + SectionRow( + name = stringResource(R.string.lightning__channel_id), + valueContent = { + CaptionB( + text = scid, + maxLines = 1, + overflow = TextOverflow.MiddleEllipsis, + textAlign = TextAlign.End, + ) + }, + onClick = { onCopyText(scid) } + ) + } channel.details.fundingTxo?.let { fundingTxo -> val channelPoint = "${fundingTxo.txid}:${fundingTxo.vout}" diff --git a/app/src/test/java/to/bitkit/ext/ShortChannelIdTest.kt b/app/src/test/java/to/bitkit/ext/ShortChannelIdTest.kt new file mode 100644 index 0000000000..2d86f1f853 --- /dev/null +++ b/app/src/test/java/to/bitkit/ext/ShortChannelIdTest.kt @@ -0,0 +1,61 @@ +package to.bitkit.ext + +import org.junit.Test +import to.bitkit.test.BaseUnitTest +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class ShortChannelIdTest : BaseUnitTest() { + + @Test + fun `formattedAsShortChannelId formats lnd scid into cln form`() { + // The two formats the issue calls out (LND uint64 and CLN block x tx x output) + // are two encodings of the same channel. + assertEquals("777477x916x0", 854_845_001_888_432_128uL.formattedAsShortChannelId()) + } + + @Test + fun `formattedAsShortChannelId formats from components`() { + val scid = (700_000uL shl 40) or (1uL shl 16) or 2uL + assertEquals("700000x1x2", scid.formattedAsShortChannelId()) + } + + @Test + fun `formattedAsShortChannelId formats zero as all zeroes`() { + assertEquals("0x0x0", 0uL.formattedAsShortChannelId()) + } + + @Test + fun `formattedAsShortChannelId keeps max components in their fields`() { + val scid = (0xFFFFFFuL shl 40) or (0xFFFFFFuL shl 16) or 0xFFFFuL + assertEquals("16777215x16777215x65535", scid.formattedAsShortChannelId()) + } + + @Test + fun `resolveDisplayShortChannelId prefers channel scid for open channel`() { + val result = resolveDisplayShortChannelId( + channelScid = 854_845_001_888_432_128uL, + linkedOrderScid = "0", + ) + assertEquals("777477x916x0", result) + } + + @Test + fun `resolveDisplayShortChannelId falls back to linked order scid for closed channel`() { + val result = resolveDisplayShortChannelId( + channelScid = null, + linkedOrderScid = "854845001888432128", + ) + assertEquals("777477x916x0", result) + } + + @Test + fun `resolveDisplayShortChannelId returns null when both unavailable`() { + assertNull(resolveDisplayShortChannelId(channelScid = null, linkedOrderScid = null)) + } + + @Test + fun `resolveDisplayShortChannelId returns null for non-numeric order scid`() { + assertNull(resolveDisplayShortChannelId(channelScid = null, linkedOrderScid = "not-a-number")) + } +} diff --git a/changelog.d/next/702.fixed.md b/changelog.d/next/702.fixed.md new file mode 100644 index 0000000000..13938d0ea9 --- /dev/null +++ b/changelog.d/next/702.fixed.md @@ -0,0 +1 @@ +Connection Details now shows the short channel ID for Lightning connections instead of the long internal channel ID. From 37ee77d4b8a2f6ff16fcbaada2b6024e932b8bea Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Tue, 9 Jun 2026 14:28:35 -0300 Subject: [PATCH 2/3] chore: rename changelog fragment --- changelog.d/next/{702.fixed.md => 1002.fixed.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename changelog.d/next/{702.fixed.md => 1002.fixed.md} (100%) diff --git a/changelog.d/next/702.fixed.md b/changelog.d/next/1002.fixed.md similarity index 100% rename from changelog.d/next/702.fixed.md rename to changelog.d/next/1002.fixed.md From 4d95348bad3eb56593ea233b4e1338d8daa2b3c0 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Tue, 9 Jun 2026 15:11:04 -0300 Subject: [PATCH 3/3] fix: accept cln-form order scid for closed channels --- .../main/java/to/bitkit/ext/ShortChannelId.kt | 18 +++++++++++++----- .../java/to/bitkit/ext/ShortChannelIdTest.kt | 19 ++++++++++++------- 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/to/bitkit/ext/ShortChannelId.kt b/app/src/main/java/to/bitkit/ext/ShortChannelId.kt index b2595add64..a6972b55d7 100644 --- a/app/src/main/java/to/bitkit/ext/ShortChannelId.kt +++ b/app/src/main/java/to/bitkit/ext/ShortChannelId.kt @@ -12,10 +12,18 @@ fun ULong.formattedAsShortChannelId(): String { return "${blockHeight}x${txIndex}x$outputIndex" } +private val CLN_SHORT_CHANNEL_ID = Regex("""\d+x\d+x\d+""") + /** - * Short channel id for display. Uses the channel's own scid (open channels) and, for closed - * channels which carry none, the scid from the confidently-linked order. Null when unavailable. + * Short channel id for display. Uses the channel's own scid (open channels, a numeric BOLT scid) + * and, for closed channels which carry none, the scid from the confidently-linked Blocktank order. + * Blocktank delivers it already in `block x tx x output` form, so an `x`-formatted value is kept + * as-is and only a numeric value is decoded. Null when unavailable. */ -internal fun resolveDisplayShortChannelId(channelScid: ULong?, linkedOrderScid: String?): String? = - channelScid?.formattedAsShortChannelId() - ?: linkedOrderScid?.toULongOrNull()?.formattedAsShortChannelId() +internal fun resolveDisplayShortChannelId(channelScid: ULong?, linkedOrderScid: String?): String? { + channelScid?.let { return it.formattedAsShortChannelId() } + + val orderScid = linkedOrderScid?.takeIf { it.isNotBlank() } ?: return null + orderScid.toULongOrNull()?.let { return it.formattedAsShortChannelId() } + return orderScid.takeIf { CLN_SHORT_CHANNEL_ID.matches(it) } +} diff --git a/app/src/test/java/to/bitkit/ext/ShortChannelIdTest.kt b/app/src/test/java/to/bitkit/ext/ShortChannelIdTest.kt index 2d86f1f853..e01a20a9a7 100644 --- a/app/src/test/java/to/bitkit/ext/ShortChannelIdTest.kt +++ b/app/src/test/java/to/bitkit/ext/ShortChannelIdTest.kt @@ -41,21 +41,26 @@ class ShortChannelIdTest : BaseUnitTest() { } @Test - fun `resolveDisplayShortChannelId falls back to linked order scid for closed channel`() { - val result = resolveDisplayShortChannelId( - channelScid = null, - linkedOrderScid = "854845001888432128", - ) + fun `resolveDisplayShortChannelId keeps cln-form linked order scid for closed channel`() { + // Blocktank delivers the order scid already in `block x tx x output` form. + val result = resolveDisplayShortChannelId(channelScid = null, linkedOrderScid = "792906x599x1") + assertEquals("792906x599x1", result) + } + + @Test + fun `resolveDisplayShortChannelId decodes numeric linked order scid`() { + val result = resolveDisplayShortChannelId(channelScid = null, linkedOrderScid = "854845001888432128") assertEquals("777477x916x0", result) } @Test fun `resolveDisplayShortChannelId returns null when both unavailable`() { assertNull(resolveDisplayShortChannelId(channelScid = null, linkedOrderScid = null)) + assertNull(resolveDisplayShortChannelId(channelScid = null, linkedOrderScid = "")) } @Test - fun `resolveDisplayShortChannelId returns null for non-numeric order scid`() { - assertNull(resolveDisplayShortChannelId(channelScid = null, linkedOrderScid = "not-a-number")) + fun `resolveDisplayShortChannelId returns null for malformed order scid`() { + assertNull(resolveDisplayShortChannelId(channelScid = null, linkedOrderScid = "not-a-scid")) } }