lnlink

iOS app for connecting to lightning nodes
git clone git://jb55.com/lnlink
Log | Files | Refs | Submodules | README | LICENSE

commit 5b9f38d440859b9527085598b4b2cf984aca5e3e
parent f7e45486455d19268db41f4b12fb45c6fa8c47ad
Author: William Casarin <jb55@jb55.com>
Date:   Mon, 21 Mar 2022 19:46:32 -0700

lnurlp metadata parsing

Signed-off-by: William Casarin <jb55@jb55.com>

Diffstat:
Mlightninglink/LNUrl.swift | 37+++++++++++++++++++++++++++++++++++++
Mlightninglink/RPC.swift | 55++++++++++++++++++++++++++++++++++++++++++++++++++++---
Mlightninglink/Views/PayView.swift | 83+++++++++++++++++++++++++++++++++++++++++++------------------------------------
3 files changed, 134 insertions(+), 41 deletions(-)

diff --git a/lightninglink/LNUrl.swift b/lightninglink/LNUrl.swift @@ -6,6 +6,7 @@ // import Foundation +import SwiftUI public struct LNUrlDecode { let encoded: Bech32 @@ -155,3 +156,39 @@ func handle_lnurl(_ url: URL, completion: @escaping (LNUrl?) -> ()) { task.resume() } + +func decode_lnurlp_metadata(_ lnurlp: LNUrlPay) -> LNUrlPayDecode { + var metadata = Array<Array<String>>() + do { + metadata = try JSONDecoder().decode(Array<Array<String>>.self, from: Data(lnurlp.metadata.utf8)) + } catch { + + } + + var description: String? = nil + var longDescription: String? = nil + var thumbnail: Image? = nil + var vendor: String = lnurlp.callback.host ?? "" + + for entry in metadata { + if entry.count == 2 { + if entry[0] == "text/plain" { + description = entry[1] + } else if entry[0] == "text/identifier" { + vendor = entry[1] + } else if entry[0] == "text/long-desc" { + longDescription = entry[1] + } else if entry[0] == "image/png;base64" || entry[0] == "image/jpg;base64" { + guard let dat = Data(base64Encoded: entry[1]) else { + continue + } + guard let ui_img = UIImage(data: dat) else { + continue + } + thumbnail = Image(uiImage: ui_img) + } + } + } + + return LNUrlPayDecode(description: description, longDescription: longDescription, thumbnail: thumbnail, vendor: vendor) +} diff --git a/lightninglink/RPC.swift b/lightninglink/RPC.swift @@ -6,6 +6,7 @@ // import Foundation +import SwiftUI public typealias RequestRes<T> = Result<T, RequestError<RpcErrorData>> @@ -46,7 +47,55 @@ public struct Channel: Decodable { public var channel_total_sat: Int64 } -public struct Decode: Decodable { +public struct LNUrlPayDecode { + public let description: String? + public let longDescription: String? + public let thumbnail: Image? + public let vendor: String +} + +public enum Decode { + case invoice(InvoiceDecode) + case lnurlp(LNUrlPayDecode) + + func thumbnail() -> Image? { + switch self { + case .lnurlp(let decode): + return decode.thumbnail + case .invoice: + return nil + } + } + + func description() -> String? { + switch self { + case .invoice(let inv): + return inv.description + case .lnurlp(let lnurl): + return lnurl.description + } + } + + func vendor() -> String? { + switch self { + case .invoice(let inv): + return inv.vendor + case .lnurlp(let lnurl): + return lnurl.vendor + } + } + + func amount_msat() -> String? { + switch self { + case .invoice(let inv): + return inv.amount_msat + case .lnurlp: + return nil + } + } +} + +public struct InvoiceDecode: Decodable { public var type: String public var currency: String? public var valid: Bool @@ -62,7 +111,7 @@ public struct Decode: Decodable { public var vendor: String? } -func get_decode_expiry(_ decode: Decode) -> Int64? { +func get_decode_expiry(_ decode: InvoiceDecode) -> Int64? { // bolt11 if decode.expiry != nil { return decode.expiry @@ -316,7 +365,7 @@ public func rpc_listfunds(ln: LNSocket, token: String) -> RequestRes<ListFunds> return performRpc(ln: ln, operation: "listfunds", authToken: token, timeout_ms: default_timeout, params: params) } -public func rpc_decode(ln: LNSocket, token: String, inv: String) -> RequestRes<Decode> +public func rpc_decode(ln: LNSocket, token: String, inv: String) -> RequestRes<InvoiceDecode> { let params = [ inv ]; return performRpc(ln: ln, operation: "decode", authToken: token, timeout_ms: 1000, params: params) diff --git a/lightninglink/Views/PayView.swift b/lightninglink/Views/PayView.swift @@ -11,7 +11,7 @@ import Combine public struct Offer { let offer: String let amount: InvoiceAmount - let decoded: Decode + let decoded: InvoiceDecode } public struct Invoice { @@ -149,19 +149,23 @@ struct PayView: View { if self.invoice != nil { let invoice = self.invoice! - if invoice.description != nil { - Text(invoice.description!) + if invoice.description() != nil { + Text(invoice.description()!) .multilineTextAlignment(.center) .padding() } - if invoice.vendor != nil { - Text(invoice.vendor!) + if invoice.vendor() != nil { + Text(invoice.vendor()!) .font(.callout) .foregroundColor(.gray) } } + if self.invoice != nil && self.invoice!.thumbnail() != nil { + self.invoice!.thumbnail()! + } + Spacer() // Middle area @@ -256,7 +260,7 @@ struct PayView: View { guard let invoice = self.invoice else { return } - guard let amount_msat_str = invoice.amount_msat else { + guard let amount_msat_str = invoice.amount_msat() else { return } guard let amount_msat = parse_msat(amount_msat_str) else { @@ -523,10 +527,10 @@ struct PayView: View { } - func handle_offer(ln: LNSocket, decoded: Decode, inv: String) { + func handle_offer(ln: LNSocket, decoded: InvoiceDecode, inv: String) { switch handle_bolt12_offer(ln: ln, decoded: decoded, inv: inv) { case .right(let state): - self.invoice = decoded + self.invoice = .invoice(decoded) switch_state(state) case .left(let err): self.error = err @@ -534,6 +538,9 @@ struct PayView: View { } func handle_lnurl_payview(ln: LNSocket?, lnurlp: LNUrlPay) { + let decode = decode_lnurlp_metadata(lnurlp) + self.invoice = .lnurlp(decode) + switch_state(.invoice_request(.lnurl(lnurlp))) } @@ -583,7 +590,7 @@ struct PayView: View { } self.state = .ready(Invoice(invstr: inv, amount: amount)) - self.invoice = decoded + self.invoice = .invoice(decoded) update_expiry_percent() } else { self.error = "unknown decoded type: \(decoded.type)" @@ -593,46 +600,45 @@ struct PayView: View { } func update_expiry_percent() { - guard let invoice = self.invoice else { - return - } + if case let .invoice(invoice) = self.invoice { + guard let expiry = get_decode_expiry(invoice) else { + self.expiry_percent = nil + return + } - guard let expiry = get_decode_expiry(invoice) else { - self.expiry_percent = nil - return - } + guard let created_at = invoice.created_at else { + self.expiry_percent = nil + return + } - guard let created_at = invoice.created_at else { - self.expiry_percent = nil - return - } + let now = Int64(Date().timeIntervalSince1970) + let expires_at = created_at + expiry - let now = Int64(Date().timeIntervalSince1970) - let expires_at = created_at + expiry + guard expiry > 0 else { + self.expiry_percent = nil + return + } - guard expiry > 0 else { - self.expiry_percent = nil - return - } + guard now < expires_at else { + self.error = "Invoice expired" + self.expiry_percent = nil + return + } - guard now < expires_at else { - self.error = "Invoice expired" - self.expiry_percent = nil - return - } + guard now >= created_at else { + self.expiry_percent = 1 + return + } - guard now >= created_at else { - self.expiry_percent = 1 - return + let prog = now - created_at + self.expiry_percent = 1.0 - (Double(prog) / Double(expiry)) } - let prog = now - created_at - self.expiry_percent = 1.0 - (Double(prog) / Double(expiry)) } } -func fetchinvoice_req_from_offer(offer: Decode, offer_str: String, pay_amt: PayAmount) -> Either<String, FetchInvoiceReq> { +func fetchinvoice_req_from_offer(offer: InvoiceDecode, offer_str: String, pay_amt: PayAmount) -> Either<String, FetchInvoiceReq> { var qty: Int? = nil if offer.quantity_min != nil { @@ -747,7 +753,7 @@ struct PayView_Previews: PreviewProvider { } */ -func handle_bolt12_offer(ln: LNSocket, decoded: Decode, inv: String) -> Either<String, PayState> { +func handle_bolt12_offer(ln: LNSocket, decoded: InvoiceDecode, inv: String) -> Either<String, PayState> { if decoded.amount_msat != nil { guard let min_amt = parse_msat(decoded.amount_msat!) else { return .left("Error parsing amount_msat: '\(decoded.amount_msat!)'") @@ -844,3 +850,4 @@ func pay_amount_matches(pay_amt: PayAmount, invoice_amount: InvoiceAmount) -> Bo return false } +