lnlink

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

LNUrl.swift (6213B)


      1 //
      2 //  LNUrl.swift
      3 //  lightninglink
      4 //
      5 //  Created by William Casarin on 2022-03-12.
      6 //
      7 
      8 import Foundation
      9 import SwiftUI
     10 
     11 public struct LNUrlDecode {
     12     let encoded: Bech32
     13 }
     14 
     15 public enum Bech32Type {
     16     case bech32
     17     case bech32m
     18 }
     19 
     20 public struct LNUrlError: Decodable {
     21     let status: String?
     22     let reason: String?
     23 
     24     public init (reason: String) {
     25         self.status = "ERROR"
     26         self.reason = reason
     27     }
     28 }
     29 
     30 public struct LNUrlPay: Decodable {
     31     let callback: URL
     32     let maxSendable: UInt64?
     33     let minSendable: UInt64?
     34     let metadata: String
     35     let tag: String
     36 }
     37 
     38 public struct LNUrlPayInvoice: Decodable {
     39     let pr: String
     40 }
     41 
     42 public enum LNUrl {
     43     case payRequest(LNUrlPay)
     44 }
     45 
     46 public struct Bech32 {
     47     let hrp: String
     48     let dat: Data
     49     let type: Bech32Type
     50 }
     51 
     52 func decode_bech32(_ str: String) -> Bech32? {
     53     let hrp_buf = UnsafeMutableBufferPointer<CChar>.allocate(capacity: str.count)
     54     let bits_buf = UnsafeMutableBufferPointer<UInt8>.allocate(capacity: str.count)
     55     let data_buf = UnsafeMutableBufferPointer<UInt8>.allocate(capacity: str.count)
     56     var bitslen: Int = 0
     57     var datalen: Int = 0
     58     var m_hrp_str: String? = nil
     59     var m_data: Data? = nil
     60     var typ: bech32_encoding = BECH32_ENCODING_NONE
     61 
     62     hrp_buf.withMemoryRebound(to: CChar.self) { hrp_ptr in
     63     str.withCString { input in
     64         typ = bech32_decode(hrp_ptr.baseAddress, bits_buf.baseAddress, &bitslen, input, str.count)
     65         bech32_convert_bits(data_buf.baseAddress, &datalen, 8, bits_buf.baseAddress, bitslen, 5, 0)
     66         if datalen == 0 {
     67             return
     68         }
     69         m_data = Data(buffer: data_buf)[...(datalen-1)]
     70         m_hrp_str = String(cString: hrp_ptr.baseAddress!)
     71     }
     72     }
     73 
     74     guard let hrp = m_hrp_str else {
     75         return nil
     76     }
     77 
     78     guard let data = m_data else {
     79         return nil
     80     }
     81 
     82     var m_type: Bech32Type? = nil
     83     if typ == BECH32_ENCODING_BECH32 {
     84         m_type = .bech32
     85     } else if typ == BECH32_ENCODING_BECH32M {
     86         m_type = .bech32m
     87     }
     88 
     89     guard let type = m_type else {
     90         return nil
     91     }
     92 
     93     return Bech32(hrp: hrp, dat: data, type: type)
     94 }
     95 
     96 func decode_lnurl(_ data: Data) -> LNUrl? {
     97     let lnurlp: LNUrlPay? = decode_data(data)
     98     return lnurlp.map { .payRequest($0) }
     99 }
    100 
    101 func decode_lnurl_pay(_ data: Data) -> LNUrlPayInvoice? {
    102     return decode_data(data)
    103 }
    104 
    105 func decode_data<T: Decodable>(_ data: Data) -> T? {
    106     let decoder = JSONDecoder()
    107     do {
    108         return try decoder.decode(T.self, from: data)
    109     } catch {
    110         print("decode_data failed for \(T.self): \(error)")
    111     }
    112 
    113     return nil
    114 }
    115 
    116 func lnurl_fetchinvoice(lnurlp: LNUrlPay, amount: Int64, completion: @escaping (Either<LNUrlError, LNUrlPayInvoice>) -> ()) {
    117     let c = lnurlp.callback.absoluteString.contains("?") ? "&" : "?"
    118     guard let url = URL(string: lnurlp.callback.absoluteString + "\(c)amount=\(amount)") else {
    119         completion(.left(LNUrlError(reason: "Invalid lnurl callback")))
    120         return
    121     }
    122     handle_lnurl_request(url, completion: completion)
    123 }
    124 
    125 func handle_lnurl_request<T: Decodable>(_ url: URL, completion: @escaping (Either<LNUrlError, T>) -> ()) {
    126     let task = URLSession.shared.dataTask(with: url) { (mdata, response, error) in
    127         guard let data = mdata else {
    128             completion(.left(LNUrlError(reason: "Request failed: \(error.debugDescription)")))
    129             return
    130         }
    131 
    132         if let merr: LNUrlError = decode_data(data) {
    133             if merr.status == "ERROR" {
    134                 completion(.left(merr))
    135             }
    136         }
    137 
    138         guard let t: T = decode_data(data) else {
    139             completion(.left(LNUrlError(reason: "Failed when decoding \(T.self)")))
    140             return
    141         }
    142 
    143         completion(.right(t))
    144     }
    145 
    146     task.resume()
    147 }
    148 
    149 public struct LNUrlAuth {
    150     let k1: String
    151     let tag: String
    152     let url: URL
    153     let host: String
    154 }
    155 
    156 func is_lnurl_auth(_ url: URL) -> LNUrlAuth? {
    157     var components = URLComponents()
    158     components.query = url.query
    159     
    160     guard let items = components.queryItems else {
    161         return nil
    162     }
    163     
    164     guard let host = url.host else {
    165         return nil
    166     }
    167     
    168     var k1: String? = nil
    169     var tag: String? = nil
    170     
    171     for item in items {
    172         if item.name == "k1" {
    173             k1 = item.value
    174         } else if item.name == "tag" && item.value == "login" {
    175             tag = item.value
    176         }
    177     }
    178     
    179     guard let k1 = k1 else {
    180         return nil
    181     }
    182 
    183     guard let tag = tag else {
    184         return nil
    185     }
    186     
    187     return LNUrlAuth(k1: k1, tag: tag, url: url, host: host)
    188 }
    189 
    190 func handle_lnurl(_ url: URL, completion: @escaping (LNUrl?) -> ()) {
    191     let task = URLSession.shared.dataTask(with: url) { (mdata, response, error) in
    192         guard let data = mdata else {
    193             let lnurl: LNUrl? = nil
    194             completion(lnurl)
    195             return
    196         }
    197 
    198         completion(decode_lnurl(data))
    199     }
    200 
    201     task.resume()
    202 }
    203 
    204 
    205 func decode_lnurlp_metadata(_ lnurlp: LNUrlPay) -> LNUrlPayDecode {
    206     var metadata = Array<Array<String>>()
    207     do {
    208         metadata = try JSONDecoder().decode(Array<Array<String>>.self, from: Data(lnurlp.metadata.utf8))
    209     } catch {
    210 
    211     }
    212 
    213     var description: String? = nil
    214     var longDescription: String? = nil
    215     var thumbnail: Image? = nil
    216     var vendor: String = lnurlp.callback.host ?? ""
    217 
    218     for entry in metadata {
    219         if entry.count == 2 {
    220             if entry[0] == "text/plain" {
    221                 description = entry[1]
    222             } else if entry[0] == "text/identifier" {
    223                 vendor = entry[1]
    224             } else if entry[0] == "text/long-desc" {
    225                 longDescription = entry[1]
    226             } else if entry[0] == "image/png;base64" || entry[0] == "image/jpg;base64" {
    227                 guard let dat = Data(base64Encoded: entry[1]) else {
    228                     continue
    229                 }
    230                 guard let ui_img = UIImage(data: dat) else {
    231                     continue
    232                 }
    233                 thumbnail = Image(uiImage: ui_img)
    234             }
    235         }
    236     }
    237 
    238     return LNUrlPayDecode(description: description, longDescription: longDescription, thumbnail:    thumbnail, vendor: vendor)
    239 }