lnlink

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

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 }