Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions app/src/main/java/to/bitkit/ext/ShortChannelId.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
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"
}

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, 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?.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) }
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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}"
Expand Down
66 changes: 66 additions & 0 deletions app/src/test/java/to/bitkit/ext/ShortChannelIdTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
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 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 malformed order scid`() {
assertNull(resolveDisplayShortChannelId(channelScid = null, linkedOrderScid = "not-a-scid"))
}
}
1 change: 1 addition & 0 deletions changelog.d/next/1002.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Connection Details now shows the short channel ID for Lightning connections instead of the long internal channel ID.
Loading