lnlink

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

ContentView.swift (5932B)


      1 //
      2 //  ContentView.swift
      3 //  lightninglink
      4 //
      5 //  Created by William Casarin on 2022-01-07.
      6 //
      7 
      8 import SwiftUI
      9 import AVFoundation
     10 
     11 extension Notification.Name {
     12     static var sentPayment: Notification.Name {
     13         return Notification.Name("did send payment")
     14     }
     15 }
     16 
     17 enum ActiveAlert: Identifiable {
     18     var id: String {
     19         switch self {
     20         case .pay:
     21             return "pay"
     22         }
     23     }
     24 
     25     case pay(InvoiceAmount, String)
     26 }
     27 
     28 enum ActiveSheet: Identifiable {
     29     var id: String {
     30         switch self {
     31         case .qr:
     32             return "qrcode"
     33         case .pay:
     34             return "paysheet"
     35         }
     36     }
     37 
     38     case qr
     39     case pay(InvoiceAmount, String)
     40 }
     41 
     42 struct Funds {
     43     public var onchain_sats: Int64
     44     public var channel_sats: Int64
     45 
     46     public static var empty = Funds(onchain_sats: 0, channel_sats: 0)
     47 
     48     public static func from_listfunds(fs: ListFunds) -> Funds {
     49         var onchain_sats: Int64 = 0
     50         var channel_sats: Int64 = 0
     51 
     52         for channel in fs.channels {
     53             channel_sats += channel.channel_sat
     54         }
     55 
     56         for output in fs.outputs {
     57             onchain_sats += output.value
     58         }
     59 
     60         return Funds(onchain_sats: onchain_sats, channel_sats: channel_sats)
     61     }
     62 }
     63 
     64 let SCAN_TYPES: [AVMetadataObject.ObjectType] = [.qr]
     65 
     66 struct ContentView: View {
     67     @State private var info: GetInfo
     68     @State private var active_sheet: ActiveSheet?
     69     @State private var active_alert: ActiveAlert?
     70     @State private var has_alert: Bool
     71     @State private var last_pay: Pay?
     72     @State private var funds: Funds
     73 
     74     private var lnlink: LNLink
     75 
     76     init(info: GetInfo, lnlink: LNLink, funds: ListFunds) {
     77         self.info = info
     78         self.lnlink = lnlink
     79         self.has_alert = false
     80         self.funds = Funds.from_listfunds(fs: funds)
     81     }
     82 
     83     func refresh_funds() {
     84         let ln = LNSocket()
     85         guard ln.connect_and_init(node_id: self.lnlink.node_id, host: self.lnlink.host) else {
     86             return
     87         }
     88         let funds = fetch_funds(ln: ln, token: lnlink.token)
     89         self.funds = Funds.from_listfunds(fs: funds)
     90     }
     91 
     92     func format_last_pay() -> String {
     93         guard let pay = last_pay else {
     94             return ""
     95         }
     96 
     97         if (pay.msatoshi >= 1000) {
     98             let sats = pay.msatoshi / 1000
     99             let fee = (pay.msatoshi_sent - pay.msatoshi) / 1000
    100             return "-\(sats) sats (\(fee) sats fee)"
    101         }
    102 
    103         return "-\(pay.msatoshi) msats (\(pay.msatoshi_sent) msats sent)"
    104     }
    105 
    106     func check_pay() {
    107         guard let (amt, inv) = get_clipboard_invoice() else {
    108             self.active_sheet = .qr
    109             self.has_alert = false
    110             return
    111         }
    112 
    113         self.active_sheet = nil
    114         self.active_alert = .pay(amt, inv)
    115         self.has_alert = true
    116     }
    117 
    118     var body: some View {
    119         VStack {
    120             Group {
    121                 Text(self.info.alias)
    122                     .font(.largeTitle)
    123                     .padding()
    124                 Text("\(self.info.num_active_channels) active channels")
    125                 Text("\(self.info.msatoshi_fees_collected / 1000) sats collected in fees")
    126                 }
    127             Spacer()
    128             Text("\(format_last_pay())")
    129                 .foregroundColor(Color.red)
    130 
    131             Text("\(self.funds.channel_sats) sats")
    132                 .font(.title)
    133                 .padding()
    134             Text("\(self.funds.onchain_sats) onchain")
    135             Spacer()
    136             HStack {
    137                 Spacer()
    138                 Button("Pay", action: check_pay)
    139                 .font(.title)
    140                 .padding()
    141             }
    142         }
    143         .alert("Use invoice in clipboard?", isPresented: $has_alert, presenting: active_alert) { alert in
    144             Button("Use QR") {
    145                 self.has_alert = false
    146                 self.active_sheet = .qr
    147             }
    148             Button("Yes") {
    149                 self.has_alert = false
    150                 self.active_alert = nil
    151                 switch alert {
    152                 case .pay(let amt, let inv):
    153                     self.active_sheet = .pay(amt, inv)
    154                 }
    155             }
    156         }
    157         .sheet(item: $active_sheet) { sheet in
    158             switch sheet {
    159             case .qr:
    160                 CodeScannerView(codeTypes: SCAN_TYPES) { res in
    161                     switch res {
    162                     case .success(let scan_res):
    163                         let code = scan_res.string
    164                         var invstr: String = code
    165                         if code.starts(with: "lightning:") {
    166                             let index = code.index(code.startIndex, offsetBy: 10)
    167                             invstr = String(code[index...])
    168                         }
    169                         let m_parsed = parseInvoiceAmount(invstr)
    170                         guard let parsed = m_parsed else {
    171                             return
    172                         }
    173                         self.active_sheet = .pay(parsed, invstr)
    174 
    175                     case .failure:
    176                         self.active_sheet = nil
    177                         return
    178                     }
    179 
    180                 }
    181 
    182             case .pay(let amt, let raw):
    183                 PayView(invoice_str: raw, amount: amt, lnlink: self.lnlink)
    184             }
    185         }
    186         .onReceive(NotificationCenter.default.publisher(for: .sentPayment)) { payment in
    187             last_pay = payment.object as! Pay
    188             self.active_sheet = nil
    189             refresh_funds()
    190         }
    191     }
    192 }
    193 
    194 /*
    195 struct ContentView_Previews: PreviewProvider {
    196     static var previews: some View {
    197         Group {
    198             ContentView(info: .empty, lnlink: ln, token: "", funds: .empty)
    199         }
    200     }
    201 }
    202  */
    203 
    204 
    205 func get_clipboard_invoice() -> (InvoiceAmount, String)? {
    206     guard let inv = UIPasteboard.general.string else {
    207         return nil
    208     }
    209 
    210     guard let amt = parseInvoiceAmount(inv) else {
    211         return nil
    212     }
    213 
    214     return (amt, inv)
    215 }