RPC.swift (14185B)
1 // 2 // RPC.swift 3 // lightninglink 4 // 5 // Created by William Casarin on 2022-01-30. 6 // 7 8 import Foundation 9 import SwiftUI 10 11 12 public typealias RequestRes<T> = Result<T, RequestError<RpcErrorData>> 13 14 public struct ResultWrapper<T: Decodable>: Decodable { 15 public var result: T 16 } 17 18 public struct ErrorWrapper<T: Decodable>: Decodable { 19 public var error: T 20 } 21 22 public struct RpcErrorData: Decodable, CustomStringConvertible { 23 public var message: String 24 25 public var description: String { 26 return message 27 } 28 } 29 30 public struct Output: Decodable { 31 //public var txid: String 32 //public var output: Int 33 //public var value: Int64 34 public var amount_msat: MSat 35 //public var scriptpubkey: String 36 //public var address: String 37 //public var status: String 38 //public var blockheight: Int 39 //public var reserved: Bool 40 } 41 42 public struct Channel: Decodable { 43 //public var peer_id: String 44 //public var connected: Bool 45 //public var state: String 46 public var our_amount_msat: MSat 47 public var amount_msat: MSat 48 } 49 50 public struct InvoiceRes: Decodable { 51 public let bolt11: String 52 } 53 54 public struct OfferRes: Decodable { 55 public let bolt12: String 56 } 57 58 public struct LNUrlPayDecode { 59 public let description: String? 60 public let longDescription: String? 61 public let thumbnail: Image? 62 public let vendor: String 63 } 64 65 public enum Decode { 66 case invoice(InvoiceDecode) 67 case lnurlp(LNUrlPayDecode) 68 69 func thumbnail() -> Image? { 70 switch self { 71 case .lnurlp(let decode): 72 return decode.thumbnail 73 case .invoice: 74 return nil 75 } 76 } 77 78 func description() -> String? { 79 switch self { 80 case .invoice(let inv): 81 return inv.description 82 case .lnurlp(let lnurl): 83 return lnurl.description 84 } 85 } 86 87 func vendor() -> String? { 88 switch self { 89 case .invoice(let inv): 90 return inv.vendor 91 case .lnurlp(let lnurl): 92 return lnurl.vendor 93 } 94 } 95 96 func amount_msat() -> Int64? { 97 switch self { 98 case .invoice(let inv): 99 return inv.amount_msat?.msat 100 case .lnurlp: 101 return nil 102 } 103 } 104 } 105 106 public struct InvoiceDecode: Decodable { 107 public var type: String 108 public var currency: String? 109 public var valid: Bool 110 public var created_at: Int64? 111 public var expiry: Int64? 112 public var relative_expiry: Int64? 113 public var payee: String? 114 public var quantity_min: Int? 115 public var description: String? 116 public var node_id: String? 117 public var amount_msat: MSat? 118 public var vendor: String? 119 } 120 121 func get_decode_expiry(_ decode: InvoiceDecode) -> Int64? { 122 // bolt11 123 if decode.expiry != nil { 124 return decode.expiry 125 } 126 127 // bolt12 128 return decode.relative_expiry 129 } 130 131 public struct FetchInvoice: Decodable { 132 public var invoice: String 133 } 134 135 public struct ListFunds: Decodable { 136 public var outputs: [Output]? 137 public var channels: [Channel]? 138 139 public static var empty = ListFunds(outputs: [], channels: []) 140 } 141 142 public struct MakeSecret: Decodable { 143 public let secret: String 144 } 145 146 public struct Pay: Decodable { 147 public var destination: String 148 public var payment_hash: String 149 public var created_at: Float 150 public var parts: Int 151 public var amount_msat: MSat 152 public var amount_sent_msat: MSat 153 public var payment_preimage: String 154 public var status: String 155 } 156 157 public struct MSat: Decodable { 158 public var msat: Int64 159 160 public init(msats: Int64) { 161 self.msat = msats 162 } 163 164 public init(from decoder: Decoder) throws { 165 if let int = try? decoder.singleValueContainer().decode(Int64.self) { 166 self.msat = int 167 return 168 } 169 if let string = try? decoder.singleValueContainer().decode(String.self) { 170 if let msat = parse_msat(string) { 171 self.msat = msat 172 return 173 } 174 } 175 throw MSatError.missingValue 176 } 177 178 public enum MSatError: Error { 179 case missingValue 180 } 181 } 182 183 public struct GetInfo: Decodable { 184 public var alias: String 185 public var id: String 186 public var color: String 187 public var network: String 188 public var num_peers: Int 189 public var fees_collected_msat: MSat 190 public var num_active_channels: Int 191 public var blockheight: Int 192 193 public static var empty = GetInfo(alias: "", id: "", color: "", network: "", num_peers: 0, fees_collected_msat: .init(msats: 0), num_active_channels: 0, blockheight: 0) 194 } 195 196 public enum RequestErrorType: Error { 197 case decoding(DecodingError) 198 case connectionFailed 199 case initFailed 200 case writeFailed 201 case timeout 202 case selectFailed 203 case recvFailed 204 case badCommandoMsgType(Int) 205 case badConnectionString 206 case outOfMemory 207 case encoding(EncodingError) 208 case status(Int) 209 case unknown(String) 210 } 211 212 public struct RequestError<E: CustomStringConvertible & Decodable>: Error, CustomStringConvertible { 213 public var response: HTTPURLResponse? 214 public var respData: Data = Data() 215 public var decoded: Either<String, E>? 216 public var errorType: RequestErrorType 217 218 init(errorType: RequestErrorType) { 219 self.errorType = errorType 220 } 221 222 init(respData: Data, errorType: RequestErrorType) { 223 self.respData = respData 224 self.errorType = errorType 225 self.decoded = maybe_decode_error_json(respData) 226 } 227 228 public var description: String { 229 if let decoded = self.decoded { 230 switch decoded { 231 case .left(let err): 232 return err 233 case .right(let err): 234 return err.description 235 } 236 } 237 238 let strData = String(decoding: respData, as: UTF8.self) 239 240 guard let resp = response else { 241 return "respData: \(strData)\nerrorType: \(errorType)\n" 242 } 243 244 return "response: \(resp)\nrespData: \(strData)\nerrorType: \(errorType)\n" 245 } 246 } 247 248 249 func parse_connection_string(_ cs: String) -> (String, String)? { 250 let arr = cs.components(separatedBy: "@") 251 if arr.count != 2 { 252 return nil 253 } 254 return (arr[0], arr[1]) 255 } 256 257 public func performRpcOnce<IN: Encodable, OUT: Decodable>( 258 connectionString: String, operation: String, authToken: String, 259 timeout_ms: Int32, 260 params: IN 261 ) -> RequestRes<OUT> { 262 guard let parts = parse_connection_string(connectionString) else { 263 return .failure(RequestError(errorType: .badConnectionString)) 264 } 265 266 let node_id = parts.0 267 let host = parts.1 268 269 let ln = LNSocket() 270 ln.genkey() 271 272 guard ln.connect_and_init(node_id: node_id, host: host) else { 273 return .failure(RequestError(errorType: .connectionFailed)) 274 } 275 276 return performRpc(ln: ln, operation: operation, authToken: authToken, timeout_ms: timeout_ms, params: params) 277 } 278 279 public func performRpc<IN: Encodable, OUT: Decodable>( 280 ln: LNSocket, operation: String, authToken: String, timeout_ms: Int32, params: IN) -> RequestRes<OUT> 281 { 282 283 guard let msg = make_commando_msg(authToken: authToken, operation: operation, params: params) else { 284 return .failure(RequestError(errorType: .outOfMemory)) 285 } 286 287 guard ln.write(msg) else { 288 return .failure(RequestError(errorType: .writeFailed)) 289 } 290 291 switch commando_read_all(ln: ln, timeout_ms: timeout_ms) { 292 case .failure(let req_err): 293 return .failure(req_err) 294 295 case .success(let data): 296 return decodeJSON(data) 297 } 298 } 299 300 func decodeJSON<T: Decodable>(_ dat: Data) -> RequestRes<T> { 301 do { 302 let dat = try JSONDecoder().decode(ResultWrapper<T>.self, from: dat) 303 return .success(dat.result) 304 } 305 catch let decode_err as DecodingError { 306 return .failure(RequestError(respData: dat, errorType: .decoding(decode_err))) 307 } 308 catch let err { 309 return .failure(RequestError(respData: dat, errorType: .unknown("\(err)"))) 310 } 311 } 312 313 314 func make_commando_msg<IN: Encodable>(authToken: String, operation: String, params: IN) -> Data? { 315 let encoder = JSONEncoder() 316 let json_data = try! encoder.encode(params) 317 guard let params_json = String(data: json_data, encoding: String.Encoding.utf8) else { 318 return nil 319 } 320 var buf = [UInt8](repeating: 0, count: 65536) 321 var outlen: Int32 = 0 322 323 authToken.withCString { token in 324 operation.withCString { op in 325 params_json.withCString { ps in 326 outlen = commando_make_rpc_msg(op, ps, token, 1, &buf, Int32(buf.capacity)) 327 }}} 328 329 guard outlen != 0 else { 330 return nil 331 } 332 333 return Data(buf[..<Int(outlen)]) 334 } 335 336 337 func commando_read_all(ln: LNSocket, timeout_ms: Int32 = 2000) -> RequestRes<Data> { 338 var rv: Int32 = 0 339 var set = fd_set() 340 var timeout = timeval() 341 342 timeout.tv_sec = __darwin_time_t(timeout_ms / 1000); 343 timeout.tv_usec = (timeout_ms % 1000) * 1000; 344 345 fd_do_zero(&set) 346 let fd = ln.fd() 347 fd_do_set(fd, &set) 348 349 var all_data = Data() 350 351 while(true) { 352 rv = select(fd + 1, &set, nil, nil, &timeout) 353 354 if rv == -1 { 355 return .failure(RequestError(errorType: .selectFailed)) 356 } else if rv == 0 { 357 return .failure(RequestError(errorType: .timeout)) 358 } 359 360 guard let (msgtype, data) = ln.recv() else { 361 return .failure(RequestError(errorType: .recvFailed)) 362 } 363 364 if msgtype == COMMANDO_REPLY_TERM { 365 all_data.append(data[8...]) 366 break 367 } else if msgtype == COMMANDO_REPLY_CONTINUES { 368 all_data.append(data[8...]) 369 continue 370 } else if msgtype == WIRE_PING.rawValue { 371 // respond to pings for long requests like waitinvoice, etc 372 ln.pong(ping: data) 373 } else { 374 //return .failure(RequestError(errorType: .badCommandoMsgType(Int(msgtype)))) 375 // we could get random messages like channel update! just ignore them 376 continue 377 } 378 } 379 380 return .success(all_data) 381 } 382 383 public let default_timeout: Int32 = 8000 384 385 public func rpc_getinfo(ln: LNSocket, token: String, timeout: Int32 = default_timeout) -> RequestRes<GetInfo> 386 { 387 let params: Array<String> = [] 388 return performRpc(ln: ln, operation: "getinfo", authToken: token, timeout_ms: default_timeout, params: params) 389 } 390 391 public func rpc_offer(ln: LNSocket, token: String, amount: InvoiceAmount = .any, description: String? = nil, issuer: String? = nil) -> RequestRes<OfferRes> { 392 393 let now = Date().timeIntervalSince1970 394 let label = "lnlink-\(now)" 395 let desc = description ?? "lnlink offer" 396 var params: [String: String] = ["description": desc, "label": label] 397 398 if let issuer = issuer { 399 params["issuer"] = issuer 400 } 401 402 switch amount { 403 case .amount(let val): 404 params["amount"] = "\(val)msat" 405 case .any: 406 params["amount"] = "any" 407 case .min(let val): 408 params["amount"] = "\(val)msat" 409 case .range(let min, _): 410 params["amount"] = "\(min)msat" 411 } 412 413 return performRpc(ln: ln, operation: "offer", authToken: token, timeout_ms: default_timeout, params: params) 414 } 415 416 public func rpc_invoice(ln: LNSocket, token: String, amount: InvoiceAmount = .any, description: String? = nil, expiry: UInt64? = nil) -> RequestRes<InvoiceRes> { 417 418 let now = Date().timeIntervalSince1970 419 let label = "lnlink-\(now)" 420 let desc = description ?? "lnlink invoice" 421 var params: [String: String] = ["description": desc, "label": label] 422 423 if let exp = expiry { 424 params["expiry"] = "\(exp)" 425 } 426 427 switch amount { 428 case .amount(let val): 429 params["amount_msat"] = "\(val)msat" 430 case .any: 431 params["amount_msat"] = "any" 432 case .min(let val): 433 params["amount_msat"] = "\(val)msat" 434 case .range(let min, _): 435 params["amount_msat"] = "\(min)msat" 436 } 437 438 return performRpc(ln: ln, operation: "invoice", authToken: token, timeout_ms: default_timeout, params: params) 439 } 440 441 public func rpc_pay(ln: LNSocket, token: String, bolt11: String, amount_msat: Int64?, timeout_ms: Int32, description: String?) -> RequestRes<Pay> 442 { 443 var params: [String: String] = ["bolt11": bolt11] 444 if amount_msat != nil { 445 params["amount_msat"] = "\(amount_msat!)" 446 } 447 448 if let desc = description { 449 params["description"] = desc 450 } 451 452 return performRpc(ln: ln, operation: "pay", authToken: token, timeout_ms: timeout_ms, params: params) 453 } 454 455 public func rpc_listfunds(ln: LNSocket, token: String) -> RequestRes<ListFunds> 456 { 457 let params: Array<String> = [] 458 return performRpc(ln: ln, operation: "listfunds", authToken: token, timeout_ms: default_timeout, params: params) 459 } 460 461 public func rpc_makesecret(ln: LNSocket, token: String, hex: String) -> RequestRes<MakeSecret> 462 { 463 let params: Array<String> = [hex] 464 return performRpc(ln: ln, operation: "makesecret", authToken: token, timeout_ms: 1000, params: params) 465 } 466 467 public func rpc_decode(ln: LNSocket, token: String, inv: String) -> RequestRes<InvoiceDecode> 468 { 469 let params = [ inv ]; 470 return performRpc(ln: ln, operation: "decode", authToken: token, timeout_ms: 1000, params: params) 471 } 472 473 public func rpc_fetchinvoice(ln: LNSocket, token: String, req: FetchInvoiceReq) -> RequestRes<FetchInvoice> 474 { 475 var params: [String: String] = [ "offer": req.offer ] 476 477 if let pay_amt = req.pay_amt { 478 let amt = pay_amt.amount + (pay_amt.tip ?? 0) 479 params["amount_msat"] = "\(amt)msat" 480 } 481 482 let timeout = req.timeout ?? 15 483 params["timeout"] = "\(timeout)" 484 485 if let qty = req.quantity { 486 params["quantity"] = "\(qty)" 487 } 488 489 return performRpc(ln: ln, operation: "fetchinvoice", authToken: token, timeout_ms: Int32((timeout + 2) * 1000), params: params) 490 } 491 492 public func maybe_decode_error_json<T: Decodable>(_ dat: Data) -> Either<String, T>? { 493 do { 494 return .right(try JSONDecoder().decode(ErrorWrapper<T>.self, from: dat).error) 495 } catch { 496 do { 497 return .left(try JSONDecoder().decode(ErrorWrapper<String>.self, from: dat).error) 498 } catch { 499 return nil 500 } 501 } 502 }