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 }