Skip to content

Commit e7547cd

Browse files
authored
Merge pull request #1002 from synonymdev/fix/connection-detail-ids
fix: short channel id in connection details
2 parents 1fe7f46 + 4d95348 commit e7547cd

4 files changed

Lines changed: 120 additions & 12 deletions

File tree

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package to.bitkit.ext
2+
3+
/**
4+
* Decodes a BOLT short channel id into Core Lightning `block x tx x output` form
5+
* (e.g. `777477x916x0`): block height in the high 24 bits, transaction index in the next 24,
6+
* funding output index in the low 16.
7+
*/
8+
fun ULong.formattedAsShortChannelId(): String {
9+
val blockHeight = this shr 40
10+
val txIndex = (this shr 16) and 0xFFFFFFu
11+
val outputIndex = this and 0xFFFFu
12+
return "${blockHeight}x${txIndex}x$outputIndex"
13+
}
14+
15+
private val CLN_SHORT_CHANNEL_ID = Regex("""\d+x\d+x\d+""")
16+
17+
/**
18+
* Short channel id for display. Uses the channel's own scid (open channels, a numeric BOLT scid)
19+
* and, for closed channels which carry none, the scid from the confidently-linked Blocktank order.
20+
* Blocktank delivers it already in `block x tx x output` form, so an `x`-formatted value is kept
21+
* as-is and only a numeric value is decoded. Null when unavailable.
22+
*/
23+
internal fun resolveDisplayShortChannelId(channelScid: ULong?, linkedOrderScid: String?): String? {
24+
channelScid?.let { return it.formattedAsShortChannelId() }
25+
26+
val orderScid = linkedOrderScid?.takeIf { it.isNotBlank() } ?: return null
27+
orderScid.toULongOrNull()?.let { return it.formattedAsShortChannelId() }
28+
return orderScid.takeIf { CLN_SHORT_CHANNEL_ID.matches(it) }
29+
}

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

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ import to.bitkit.env.Env
5959
import to.bitkit.ext.DatePattern
6060
import to.bitkit.ext.amountOnClose
6161
import to.bitkit.ext.createChannelDetails
62+
import to.bitkit.ext.resolveDisplayShortChannelId
6263
import to.bitkit.ext.setClipboardText
6364
import to.bitkit.models.Toast
6465
import to.bitkit.models.msatFloorOf
@@ -220,6 +221,15 @@ private fun ChannelDetailContent(
220221

221222
val order = blocktankOrder ?: cjitEntry
222223

224+
val linkedOrderScid = when (order) {
225+
is IBtOrder -> order.channel?.shortChannelId
226+
is IcJitEntry -> order.channel?.shortChannelId
227+
else -> null
228+
}
229+
val displayShortChannelId = remember(channel, linkedOrderScid) {
230+
resolveDisplayShortChannelId(channel.details.shortChannelId, linkedOrderScid)
231+
}
232+
223233
val capacity = channel.details.channelValueSats.toLong()
224234
val localBalance = channel.details.amountOnClose.toLong()
225235
val remoteBalance = msatFloorOf(channel.details.inboundCapacityMsat).toLong()
@@ -429,18 +439,20 @@ private fun ChannelDetailContent(
429439
)
430440
}
431441

432-
SectionRow(
433-
name = stringResource(R.string.lightning__channel_id),
434-
valueContent = {
435-
CaptionB(
436-
text = channel.details.channelId,
437-
maxLines = 1,
438-
overflow = TextOverflow.MiddleEllipsis,
439-
textAlign = TextAlign.End,
440-
)
441-
},
442-
onClick = { onCopyText(channel.details.channelId) }
443-
)
442+
displayShortChannelId?.let { scid ->
443+
SectionRow(
444+
name = stringResource(R.string.lightning__channel_id),
445+
valueContent = {
446+
CaptionB(
447+
text = scid,
448+
maxLines = 1,
449+
overflow = TextOverflow.MiddleEllipsis,
450+
textAlign = TextAlign.End,
451+
)
452+
},
453+
onClick = { onCopyText(scid) }
454+
)
455+
}
444456

445457
channel.details.fundingTxo?.let { fundingTxo ->
446458
val channelPoint = "${fundingTxo.txid}:${fundingTxo.vout}"
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package to.bitkit.ext
2+
3+
import org.junit.Test
4+
import to.bitkit.test.BaseUnitTest
5+
import kotlin.test.assertEquals
6+
import kotlin.test.assertNull
7+
8+
class ShortChannelIdTest : BaseUnitTest() {
9+
10+
@Test
11+
fun `formattedAsShortChannelId formats lnd scid into cln form`() {
12+
// The two formats the issue calls out (LND uint64 and CLN block x tx x output)
13+
// are two encodings of the same channel.
14+
assertEquals("777477x916x0", 854_845_001_888_432_128uL.formattedAsShortChannelId())
15+
}
16+
17+
@Test
18+
fun `formattedAsShortChannelId formats from components`() {
19+
val scid = (700_000uL shl 40) or (1uL shl 16) or 2uL
20+
assertEquals("700000x1x2", scid.formattedAsShortChannelId())
21+
}
22+
23+
@Test
24+
fun `formattedAsShortChannelId formats zero as all zeroes`() {
25+
assertEquals("0x0x0", 0uL.formattedAsShortChannelId())
26+
}
27+
28+
@Test
29+
fun `formattedAsShortChannelId keeps max components in their fields`() {
30+
val scid = (0xFFFFFFuL shl 40) or (0xFFFFFFuL shl 16) or 0xFFFFuL
31+
assertEquals("16777215x16777215x65535", scid.formattedAsShortChannelId())
32+
}
33+
34+
@Test
35+
fun `resolveDisplayShortChannelId prefers channel scid for open channel`() {
36+
val result = resolveDisplayShortChannelId(
37+
channelScid = 854_845_001_888_432_128uL,
38+
linkedOrderScid = "0",
39+
)
40+
assertEquals("777477x916x0", result)
41+
}
42+
43+
@Test
44+
fun `resolveDisplayShortChannelId keeps cln-form linked order scid for closed channel`() {
45+
// Blocktank delivers the order scid already in `block x tx x output` form.
46+
val result = resolveDisplayShortChannelId(channelScid = null, linkedOrderScid = "792906x599x1")
47+
assertEquals("792906x599x1", result)
48+
}
49+
50+
@Test
51+
fun `resolveDisplayShortChannelId decodes numeric linked order scid`() {
52+
val result = resolveDisplayShortChannelId(channelScid = null, linkedOrderScid = "854845001888432128")
53+
assertEquals("777477x916x0", result)
54+
}
55+
56+
@Test
57+
fun `resolveDisplayShortChannelId returns null when both unavailable`() {
58+
assertNull(resolveDisplayShortChannelId(channelScid = null, linkedOrderScid = null))
59+
assertNull(resolveDisplayShortChannelId(channelScid = null, linkedOrderScid = ""))
60+
}
61+
62+
@Test
63+
fun `resolveDisplayShortChannelId returns null for malformed order scid`() {
64+
assertNull(resolveDisplayShortChannelId(channelScid = null, linkedOrderScid = "not-a-scid"))
65+
}
66+
}

changelog.d/next/1002.fixed.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Connection Details now shows the short channel ID for Lightning connections instead of the long internal channel ID.

0 commit comments

Comments
 (0)