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
25 changes: 25 additions & 0 deletions Bitkit/Extensions/ShortChannelId.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 { $0.isASCII && $0.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
}
9 changes: 2 additions & 7 deletions Bitkit/ViewModels/ChannelDetailsViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions BitkitTests/ChannelDetailsViewModelTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 26 additions & 0 deletions BitkitTests/ShortChannelIdTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,30 @@ 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"))
// Non-ASCII numeric glyphs (e.g. superscripts) are not valid scid digits.
XCTAssertNil(resolveDisplayShortChannelId(channelScid: nil, linkedOrderScid: "²x³x¹"))
}
}
Loading