From eb14d336448bf0009cac04f48219efd66d8c3fb0 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 10 Jun 2026 07:08:26 -0300 Subject: [PATCH 1/2] fix: handle blocktank order short channel id formmat --- Bitkit/Extensions/ShortChannelId.swift | 25 +++++++++++++++++++ .../ViewModels/ChannelDetailsViewModel.swift | 9 ++----- .../ChannelDetailsViewModelTests.swift | 14 +++++++++++ BitkitTests/ShortChannelIdTests.swift | 24 ++++++++++++++++++ 4 files changed, 65 insertions(+), 7 deletions(-) diff --git a/Bitkit/Extensions/ShortChannelId.swift b/Bitkit/Extensions/ShortChannelId.swift index d8ae5fca3..598f78741 100644 --- a/Bitkit/Extensions/ShortChannelId.swift +++ b/Bitkit/Extensions/ShortChannelId.swift @@ -11,3 +11,28 @@ extension UInt64 { return "\(blockHeight)x\(txIndex)x\(outputIndex)" } } + +/// Whether the string is a Core Lightning `block x tx x output` short channel id (e.g. `792906x599x1`): +/// exactly three non-empty, all-digit components separated by `x`. +private func isClnShortChannelId(_ value: String) -> Bool { + let parts = value.split(separator: "x", omittingEmptySubsequences: false) + return parts.count == 3 && parts.allSatisfy { !$0.isEmpty && $0.allSatisfy(\.isNumber) } +} + +/// Resolves the short channel id to display, formatted as `block x tx x output`. 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. Nil when unavailable. +func resolveDisplayShortChannelId(channelScid: UInt64?, linkedOrderScid: String?) -> String? { + if let channelScid { + return channelScid.formattedAsShortChannelId + } + + guard let orderScid = linkedOrderScid, !orderScid.isEmpty else { return nil } + + if let numeric = UInt64(orderScid) { + return numeric.formattedAsShortChannelId + } + + return isClnShortChannelId(orderScid) ? orderScid : nil +} diff --git a/Bitkit/ViewModels/ChannelDetailsViewModel.swift b/Bitkit/ViewModels/ChannelDetailsViewModel.swift index 0ed319b82..f04c13a35 100644 --- a/Bitkit/ViewModels/ChannelDetailsViewModel.swift +++ b/Bitkit/ViewModels/ChannelDetailsViewModel.swift @@ -76,13 +76,8 @@ class ChannelDetailsViewModel: ObservableObject { /// scid (open channels) and, for closed channels (which are not stored with one), the scid from /// a confidently linked Blocktank order. Hidden otherwise rather than showing a guessed value. var displayShortChannelId: String? { - if let scid = foundChannel?.shortChannelIdValue { - return scid.formattedAsShortChannelId - } - if isLinkedOrderConfident, let scidString = linkedOrder?.channel?.shortChannelId, let scid = UInt64(scidString) { - return scid.formattedAsShortChannelId - } - return nil + let linkedOrderScid = isLinkedOrderConfident ? linkedOrder?.channel?.shortChannelId : nil + return resolveDisplayShortChannelId(channelScid: foundChannel?.shortChannelIdValue, linkedOrderScid: linkedOrderScid) } /// Find a channel by ID, checking open channels, pending channels, pending orders, then closed channels diff --git a/BitkitTests/ChannelDetailsViewModelTests.swift b/BitkitTests/ChannelDetailsViewModelTests.swift index 3e23b70ab..305a0ad4c 100644 --- a/BitkitTests/ChannelDetailsViewModelTests.swift +++ b/BitkitTests/ChannelDetailsViewModelTests.swift @@ -48,6 +48,20 @@ final class ChannelDetailsViewModelTests: XCTestCase { XCTAssertEqual(vm.displayShortChannelId, "777477x916x0") } + @MainActor + func testDisplayShortChannelIdKeepsClnFormLinkedOrderScidForClosedChannel() { + let vm = ChannelDetailsViewModel.shared + // Blocktank delivers the order scid already in `block x tx x output` form; it is kept as-is. + vm.foundChannel = makeClosedChannel(fundingTxoTxid: "fundingtxid") + var channel = IBtChannel.mock() + channel.fundingTx = FundingTx(id: "fundingtxid", vout: 0) + channel.shortChannelId = "792906x599x1" + vm.linkedOrder = IBtOrder.mock(channel: channel) + defer { resetDisplayState(vm) } + + XCTAssertEqual(vm.displayShortChannelId, "792906x599x1") + } + @MainActor func testDisplayIgnoresWeaklyLinkedOrder() { let vm = ChannelDetailsViewModel.shared diff --git a/BitkitTests/ShortChannelIdTests.swift b/BitkitTests/ShortChannelIdTests.swift index 9ec9cb785..e818e515b 100644 --- a/BitkitTests/ShortChannelIdTests.swift +++ b/BitkitTests/ShortChannelIdTests.swift @@ -21,4 +21,28 @@ final class ShortChannelIdTests: XCTestCase { let scid = (UInt64(0xFFFFFF) << 40) | (UInt64(0xFFFFFF) << 16) | UInt64(0xFFFF) XCTAssertEqual(scid.formattedAsShortChannelId, "16777215x16777215x65535") } + + func testResolveDisplayShortChannelIdDecodesOpenChannelScid() { + XCTAssertEqual(resolveDisplayShortChannelId(channelScid: 854_845_001_888_432_128, linkedOrderScid: nil), "777477x916x0") + } + + func testResolveDisplayShortChannelIdKeepsClnFormLinkedOrderScidForClosedChannel() { + // Blocktank delivers the order scid already in `block x tx x output` form. + XCTAssertEqual(resolveDisplayShortChannelId(channelScid: nil, linkedOrderScid: "792906x599x1"), "792906x599x1") + } + + func testResolveDisplayShortChannelIdDecodesNumericLinkedOrderScid() { + XCTAssertEqual(resolveDisplayShortChannelId(channelScid: nil, linkedOrderScid: "854845001888432128"), "777477x916x0") + } + + func testResolveDisplayShortChannelIdReturnsNilWhenBothUnavailable() { + XCTAssertNil(resolveDisplayShortChannelId(channelScid: nil, linkedOrderScid: nil)) + XCTAssertNil(resolveDisplayShortChannelId(channelScid: nil, linkedOrderScid: "")) + } + + func testResolveDisplayShortChannelIdReturnsNilForMalformedOrderScid() { + XCTAssertNil(resolveDisplayShortChannelId(channelScid: nil, linkedOrderScid: "not-a-scid")) + XCTAssertNil(resolveDisplayShortChannelId(channelScid: nil, linkedOrderScid: "792906x599")) + XCTAssertNil(resolveDisplayShortChannelId(channelScid: nil, linkedOrderScid: "792906xx1")) + } } From 378cce2a7dc82b76be37ad32201fc54b69e1f0cf Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 10 Jun 2026 07:31:23 -0300 Subject: [PATCH 2/2] fix: validate filter non ASCII chars --- Bitkit/Extensions/ShortChannelId.swift | 2 +- BitkitTests/ShortChannelIdTests.swift | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Bitkit/Extensions/ShortChannelId.swift b/Bitkit/Extensions/ShortChannelId.swift index 598f78741..e18562662 100644 --- a/Bitkit/Extensions/ShortChannelId.swift +++ b/Bitkit/Extensions/ShortChannelId.swift @@ -16,7 +16,7 @@ extension UInt64 { /// exactly three non-empty, all-digit components separated by `x`. private func isClnShortChannelId(_ value: String) -> Bool { let parts = value.split(separator: "x", omittingEmptySubsequences: false) - return parts.count == 3 && parts.allSatisfy { !$0.isEmpty && $0.allSatisfy(\.isNumber) } + return parts.count == 3 && parts.allSatisfy { !$0.isEmpty && $0.allSatisfy { $0.isASCII && $0.isNumber } } } /// Resolves the short channel id to display, formatted as `block x tx x output`. Uses the channel's diff --git a/BitkitTests/ShortChannelIdTests.swift b/BitkitTests/ShortChannelIdTests.swift index e818e515b..821b7b134 100644 --- a/BitkitTests/ShortChannelIdTests.swift +++ b/BitkitTests/ShortChannelIdTests.swift @@ -44,5 +44,7 @@ final class ShortChannelIdTests: XCTestCase { XCTAssertNil(resolveDisplayShortChannelId(channelScid: nil, linkedOrderScid: "not-a-scid")) XCTAssertNil(resolveDisplayShortChannelId(channelScid: nil, linkedOrderScid: "792906x599")) XCTAssertNil(resolveDisplayShortChannelId(channelScid: nil, linkedOrderScid: "792906xx1")) + // Non-ASCII numeric glyphs (e.g. superscripts) are not valid scid digits. + XCTAssertNil(resolveDisplayShortChannelId(channelScid: nil, linkedOrderScid: "²x³x¹")) } }