From 1505a8f2e41c385806bb9d62d6601927ee63e6a3 Mon Sep 17 00:00:00 2001 From: alltheseas Date: Mon, 2 Feb 2026 09:25:22 -0600 Subject: [PATCH] Simplify Swift invoice handling with non-optional return types - Mentions.swift: convert_invoice_description now returns non-optional InvoiceDescription, returning empty description for BOLT11 compliance (both description and description_hash are optional per spec) - Block.swift, NdbBlock.swift, NostrEvent.swift, NoteContent.swift: Updated call sites to use non-optional invoice conversion - InvoiceTests.swift: Added test for specific failing invoice Signed-off-by: alltheseas Co-Authored-By: Claude Opus 4.5 --- damus/Core/Nostr/Mentions.swift | 11 +++-- damus/Core/Nostr/NostrEvent.swift | 4 +- damus/Core/Types/Block.swift | 21 +++------ .../Features/Events/Models/NoteContent.swift | 9 ++-- damusTests/InvoiceTests.swift | 43 ++++++++++++++----- nostrdb/NdbBlock.swift | 31 ++++++------- 6 files changed, 66 insertions(+), 53 deletions(-) diff --git a/damus/Core/Nostr/Mentions.swift b/damus/Core/Nostr/Mentions.swift index 9352ef4c..c44c4bf6 100644 --- a/damus/Core/Nostr/Mentions.swift +++ b/damus/Core/Nostr/Mentions.swift @@ -294,16 +294,19 @@ func format_msats(_ msat: Int64, locale: Locale = Locale.current) -> String { return String(format: format, locale: locale, sats.decimalValue as NSDecimalNumber, formattedSats) } -func convert_invoice_description(b11: ndb_invoice) -> InvoiceDescription? { +/// Extracts the description from a BOLT11 invoice. +/// Returns empty description if invoice has neither description nor description_hash, +/// as both fields are optional per BOLT11 spec. +func convert_invoice_description(b11: ndb_invoice) -> InvoiceDescription { if let desc = b11.description { return .description(String(cString: desc)) } - + if var deschash = maybe_pointee(b11.description_hash) { return .description_hash(Data(bytes: &deschash, count: 32)) } - - return nil + + return .description("") } func find_tag_ref(type: String, id: String, tags: [[String]]) -> Int? { diff --git a/damus/Core/Nostr/NostrEvent.swift b/damus/Core/Nostr/NostrEvent.swift index 9c6d04a9..889c82ed 100644 --- a/damus/Core/Nostr/NostrEvent.swift +++ b/damus/Core/Nostr/NostrEvent.swift @@ -839,9 +839,7 @@ func separate_invoices(ndb: Ndb, ev: NostrEvent, keypair: Keypair) -> [Invoice]? let invoiceBlocks: [Invoice] = (try? blockGroup.reduce(initialResult: [Invoice](), { index, invoices, block in switch block { case .invoice(let invoice): - if let invoice = invoice.as_invoice() { - return .loopReturn(invoices + [invoice]) - } + return .loopReturn(invoices + [invoice.as_invoice()]) default: break } diff --git a/damus/Core/Types/Block.swift b/damus/Core/Types/Block.swift index 77d29548..290991fc 100644 --- a/damus/Core/Types/Block.swift +++ b/damus/Core/Types/Block.swift @@ -79,8 +79,7 @@ extension Block { guard let url = URL(string: block.as_str()) else { return nil } self = .url(url) case BLOCK_INVOICE: - guard let b = Block(invoice: block.block.invoice) else { return nil } - self = b + self = Block(invoice: block.block.invoice) case BLOCK_MENTION_BECH32: guard let b = Block(bech32: block.block.mention_bech32) else { return nil } self = b @@ -113,26 +112,20 @@ fileprivate extension Block { } fileprivate extension Block { - /// Failable initializer for the C-backed type `invoice_block_t`. - init?(invoice: ndb_invoice_block) { - - guard let invoice = invoice_block_as_invoice(invoice) else { return nil } - self = .invoice(invoice) + /// Initializer for the C-backed type `invoice_block_t`. + init(invoice: ndb_invoice_block) { + self = .invoice(invoice_block_as_invoice(invoice)) } } -func invoice_block_as_invoice(_ invoice: ndb_invoice_block) -> Invoice? { +/// Converts a C-backed invoice block to a Swift Invoice. +func invoice_block_as_invoice(_ invoice: ndb_invoice_block) -> Invoice { let invstr = invoice.invstr.as_str() let b11 = invoice.invoice - - guard let description = convert_invoice_description(b11: b11) else { - return nil - } - + let description = convert_invoice_description(b11: b11) let amount: Amount = b11.amount == 0 ? .any : .specific(Int64(b11.amount)) return Invoice(description: description, amount: amount, string: invstr, expiry: b11.expiry, created_at: b11.timestamp) - } fileprivate extension Block { diff --git a/damus/Features/Events/Models/NoteContent.swift b/damus/Features/Events/Models/NoteContent.swift index 71ee273a..51d5ba2f 100644 --- a/damus/Features/Events/Models/NoteContent.swift +++ b/damus/Features/Events/Models/NoteContent.swift @@ -191,8 +191,7 @@ func render_blocks(blocks: borrowing NdbBlockGroup, profiles: Profiles, can_hide let url_type = classify_url(url) urls.append(url_type) case .invoice(let invoice_block): - guard let invoice = invoice_block.as_invoice() else { break } - invoices.append(invoice) + invoices.append(invoice_block.as_invoice()) default: break } @@ -258,9 +257,9 @@ func render_blocks(blocks: borrowing NdbBlockGroup, profiles: Profiles, can_hide return .loopReturn(str + CompatibleText(stringLiteral: reduce_text_block(ind: index, hide_text_index: hide_text_index_argument, txt: txt.as_str()))) case .hashtag(let htag): return .loopReturn(str + hashtag_str(htag.as_str())) - case .invoice(let invoice): - guard let inv = invoice.as_invoice() else { return .loopContinue } - invoices.append(inv) + case .invoice: + // Invoice already added in previewable-collection switch above + break case .url(let url): guard let url = URL(string: url.as_str()) else { return .loopContinue } return .loopReturn(str + url_str(url)) diff --git a/damusTests/InvoiceTests.swift b/damusTests/InvoiceTests.swift index 3b324266..a486f1a6 100644 --- a/damusTests/InvoiceTests.swift +++ b/damusTests/InvoiceTests.swift @@ -33,10 +33,7 @@ final class InvoiceTests: XCTestCase { let success: Bool? = blockList.useItem(at: 0, { block in switch block { case .invoice(let invoiceData): - guard let invoice = invoiceData.as_invoice() else { - XCTFail("Cannot get invoice from invoice block") - return false - } + let invoice = invoiceData.as_invoice() XCTAssertEqual(invoice.amount, .any) XCTAssertEqual(invoice.string, invstr) return true @@ -110,10 +107,7 @@ final class InvoiceTests: XCTestCase { let success: Bool? = blockList.useItem(at: 0, { block in switch block { case .invoice(let invoiceData): - guard let invoice = invoiceData.as_invoice() else { - XCTFail("Cannot get invoice from invoice block") - return false - } + let invoice = invoiceData.as_invoice() XCTAssertEqual(invoice.amount, .specific(10000)) XCTAssertEqual(invoice.expiry, 604800) XCTAssertEqual(invoice.created_at, 1666139119) @@ -127,7 +121,7 @@ final class InvoiceTests: XCTestCase { XCTAssertEqual(success, true) }) } - + func testParseInvoiceWithPrefix() throws { let invstr = "lightning:lnbc100n1p357sl0sp5t9n56wdztun39lgdqlr30xqwksg3k69q4q2rkr52aplujw0esn0qpp5mrqgljk62z20q4nvgr6lzcyn6fhylzccwdvu4k77apg3zmrkujjqdpzw35xjueqd9ejqcfqv3jhxcmjd9c8g6t0dcxqyjw5qcqpjrzjqt56h4gvp5yx36u2uzqa6qwcsk3e2duunfxppzj9vhypc3wfe2wswz607uqq3xqqqsqqqqqqqqqqqlqqyg9qyysgqagx5h20aeulj3gdwx3kxs8u9f4mcakdkwuakasamm9562ffyr9en8yg20lg0ygnr9zpwp68524kmda0t5xp2wytex35pu8hapyjajxqpsql29r" @@ -173,7 +167,36 @@ final class InvoiceTests: XCTestCase { XCTAssertEqual(success, true) }) } - + + /// Test parsing the specific invoice from GitHub issue that wasn't rendering + func testParseSpecificFailingInvoice() throws { + let invstr = "lnbc130n1p5h7alnpp5f83swv5wx9h25ansxsvkw7364c65vxktthy2m9ww5zf3cjrzp0vsdq9tfpygcqzysxqzjcsp5essuf0xnfeu4rpw7nllcggr6e9635xdpnaklr2fadtkwej0vvyfs9qxpqysgqddjjzxa2dwhntx8uvppx3u6pu864ul5dxkayp6jgf7n45ql5x7u9xzrvuav5rzsaz7h8d2gq455je2ezku40a5xrshu0w00ylprk03qq6kvvjd" + + guard let blockGroup: NdbBlockGroup = try? NdbBlockGroup.parse(content: invstr) else { + XCTFail("Parsing threw an error") + return + } + + blockGroup.withList({ blockList in + XCTAssertEqual(blockList.count, 1, "Expected 1 block, got \(blockList.count)") + let success: Bool? = blockList.useItem(at: 0, { block in + switch block { + case .invoice(let invoiceData): + let invoice = invoiceData.as_invoice() + XCTAssertEqual(invoice.amount, .specific(13000)) + return true + case .text(let txt): + XCTFail("Expected invoice block, got text block") + return false + default: + XCTFail("Block is not an invoice") + return false + } + }) + XCTAssertEqual(success, true) + }) + } + /* // gh-3144: It was decided on a standup meeting that we do not need invoices to render, few people use this feature. func testParseInvoice() throws { diff --git a/nostrdb/NdbBlock.swift b/nostrdb/NdbBlock.swift index ca10a305..74e214e6 100644 --- a/nostrdb/NdbBlock.swift +++ b/nostrdb/NdbBlock.swift @@ -37,14 +37,11 @@ enum NdbBech32Type: UInt32 { } extension ndb_invoice_block { - func as_invoice() -> Invoice? { + /// Converts to a Swift Invoice object. + func as_invoice() -> Invoice { let b11 = self.invoice let invstr = self.invstr.as_str() - - guard let description = convert_invoice_description(b11: b11) else { - return nil - } - + let description = convert_invoice_description(b11: b11) let amount: Amount = b11.amount == 0 ? .any : .specific(Int64(b11.amount)) return Invoice(description: description, amount: amount, string: invstr, expiry: b11.expiry, created_at: b11.timestamp) @@ -110,12 +107,12 @@ struct NdbBlockGroup: ~Copyable { var words: Int { return metadata.words } - + init(metadata: consuming BlocksMetadata, rawTextContent: String) { self.metadata = metadata self.rawTextContent = rawTextContent } - + /// Gets the parsed blocks from a specific note. /// /// This function will: @@ -125,17 +122,17 @@ struct NdbBlockGroup: ~Copyable { if event.is_content_encrypted() { return try lendingFunction(parse(event: event, keypair: keypair)) } - else if event.known_kind == .highlight { + if event.known_kind == .highlight { return try lendingFunction(parse(event: event, keypair: keypair)) } - else { - return try ndb.lookup_block_group_by_key(event: event, borrow: { group in - switch group { - case .none: return try lendingFunction(parse(event: event, keypair: keypair)) - case .some(let group): return try lendingFunction(group) - } - }) - } + return try ndb.lookup_block_group_by_key(event: event, borrow: { group in + switch group { + case .none: + return try lendingFunction(parse(event: event, keypair: keypair)) + case .some(let group): + return try lendingFunction(group) + } + }) } /// Parses the note contents on-demand from a specific note.