lnlink

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

commit 88285079cd37d807ec35a0cbbc3254422fb9d67e
parent 88288bca30013b60b769abf6ef10f133f6416354
Author: William Casarin <jb55@jb55.com>
Date:   Mon, 28 Mar 2022 17:01:09 -0700

exchange rates in payview!

Signed-off-by: William Casarin <jb55@jb55.com>

Diffstat:
Mlightninglink/RPC.swift | 2+-
Mlightninglink/Rates.swift | 14++++----------
Mlightninglink/Views/AmountInputView.swift | 78+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
Mlightninglink/Views/ContentView.swift | 6+++++-
Mlightninglink/Views/PayView.swift | 21+++++++++++++++------
5 files changed, 90 insertions(+), 31 deletions(-)

diff --git a/lightninglink/RPC.swift b/lightninglink/RPC.swift @@ -372,7 +372,7 @@ public func rpc_invoice(ln: LNSocket, token: String, amount: InvoiceAmount = .an params["msatoshi"] = "any" case .min(let val): params["msatoshi"] = "\(val)msat" - case .range(let min, let max): + case .range(let min, _): params["msatoshi"] = "\(min)msat" } diff --git a/lightninglink/Rates.swift b/lightninglink/Rates.swift @@ -7,16 +7,9 @@ import Foundation -enum Currency: CustomStringConvertible { +enum Currency: String { case USD case CAD - - var description: String { - switch self { - case .USD: return "USD" - case .CAD: return "CAD" - } - } } enum StringNum: Decodable { @@ -27,6 +20,7 @@ enum StringNum: Decodable { let value = try decoder.singleValueContainer() if let str = try? value.decode(String.self) { self = .string(str) + return } self = .number(try value.decode(Double.self)) @@ -39,7 +33,7 @@ struct ExchangeRate { } func get_exchange_rate(for_cur: Currency, cb: @escaping (ExchangeRate?) -> ()) { - let url = URL(string: "https://api-pub.bitfinex.com/v2/tickers?=symbols=tBTC\(for_cur)")! + let url = URL(string: "https://api-pub.bitfinex.com/v2/tickers?symbols=tBTC\(for_cur)")! let task = URLSession.shared.dataTask(with: url) { (mdata, response, error) in guard let data = mdata else { cb(nil) @@ -70,7 +64,7 @@ func decode_bitfinex_exchange_rate(_ data: Data) -> Double? { case .string: return nil case .number(let xr): - return xr + return Double(xr) } } diff --git a/lightninglink/Views/AmountInputView.swift b/lightninglink/Views/AmountInputView.swift @@ -8,27 +8,79 @@ import SwiftUI import Combine +enum BTCDenomination: String { + case sats + case bits + case mbtc + case btc +} + +enum Denomination: CustomStringConvertible, Identifiable, Hashable { + case fiat(Currency) + case bitcoin(BTCDenomination) + + var description: String { + switch self { + case .fiat(let cur): + return cur.rawValue + case .bitcoin(let btc): + return btc.rawValue + } + } + + var id: String { + return self.description + } +} + +func get_preferred_denominations() -> [Denomination] { + let fiat_pref_str = UserDefaults.standard.string(forKey: "fiat_denomination") ?? "USD" + let btc_pref_str = UserDefaults.standard.string(forKey: "btc_denomination") ?? "sats" + + let btc_pref = BTCDenomination(rawValue: btc_pref_str) ?? .sats + let fiat_pref = Currency(rawValue: fiat_pref_str) ?? .USD + + return [.bitcoin(btc_pref), .fiat(fiat_pref)] +} + struct AmountInput: View { + @State var amount_msat: Int64? = nil let text: Binding<String> - let onReceive: (String) -> () + let rate: ExchangeRate? + let onReceive: (String) -> Int64? var body: some View { - Form { - Section { - HStack(alignment: .lastTextBaseline) { - TextField("10,000", text: self.text) - .font(.title) - .keyboardType(.numberPad) - .multilineTextAlignment(.trailing) - .onReceive(Just(self.text)) { - onReceive($0.wrappedValue) - } - Text("sats") + VStack { + Form { + Section { + HStack(alignment: .lastTextBaseline) { + TextField("10,000", text: self.text) + .font(.title) + .keyboardType(.numberPad) + .multilineTextAlignment(.trailing) + .onReceive(Just(self.text)) { + amount_msat = onReceive($0.wrappedValue) + } + Text("sats") + } + } + } + .frame(height: 100) + + if let msats = amount_msat { + if let rate = self.rate { + Text("about \(sats_to_fiat(msats: msats, xr: rate))") + .foregroundColor(.gray) } } } - .frame(height: 100) } } +func sats_to_fiat(msats: Int64, xr: ExchangeRate) -> String { + let btc = Double(msats) / Double(100_000_000_000) + let rate = xr.rate * btc + return String(format: "%.2f \(xr.currency)", rate) +} + diff --git a/lightninglink/Views/ContentView.swift b/lightninglink/Views/ContentView.swift @@ -85,6 +85,7 @@ struct ContentView: View { @State private var funds: Funds = .empty @State private var is_reset: Bool = false @State private var scan_invoice: String? = nil + @State private var rate: ExchangeRate? private let dashboard: Dashboard private let lnlink: LNLink @@ -221,7 +222,7 @@ struct ContentView: View { ReceiveView(lnlink: lnlink) case .pay(let decode): - PayView(decode: decode, lnlink: self.lnlink) + PayView(decode: decode, lnlink: self.lnlink, rate: self.rate) } } .onReceive(NotificationCenter.default.publisher(for: .sentPayment)) { payment in @@ -240,6 +241,9 @@ struct ContentView: View { handle_scan(url.absoluteString) } .onAppear() { + get_exchange_rate(for_cur: .USD) { + self.rate = $0 + } refresh_funds() if init_scan_invoice != nil { handle_scan(init_scan_invoice!) diff --git a/lightninglink/Views/PayView.swift b/lightninglink/Views/PayView.swift @@ -82,6 +82,7 @@ public enum PayState { struct PayView: View { let init_decode_type: DecodeType let lnlink: LNLink + let rate: ExchangeRate? let expiry_timer = Timer.publish(every: 1, on: .main, in: .default).autoconnect() @@ -97,9 +98,10 @@ struct PayView: View { @Environment(\.presentationMode) var presentationMode - init(decode: DecodeType, lnlink: LNLink) { + init(decode: DecodeType, lnlink: LNLink, rate: ExchangeRate?) { self.init_decode_type = decode self.lnlink = lnlink + self.rate = rate } var successView: some View { @@ -230,10 +232,10 @@ struct PayView: View { return self.paying || (self.error == nil && is_ready(self.state) == nil) } - func handle_custom_receive(_ new_val: String) { + func handle_custom_receive(_ new_val: String) -> Int64? { if new_val == "" { self.custom_amount_input = "" - return + return nil } let ok = new_val.allSatisfy { $0 == "," || ($0 >= "0" && $0 <= "9") } @@ -246,7 +248,10 @@ struct PayView: View { let msats = sats * 1000 self.custom_amount_input = num_fmt.string(from: NSNumber(value: sats)) ?? new_val self.custom_amount_msats = msats + return msats } + + return nil } func tip_percent(_ tip: TipSelection) { @@ -331,8 +336,8 @@ struct PayView: View { Text("\(render_amount_msats(amt))") .font(.title) } else { - AmountInput(text: $custom_amount_input) { - handle_custom_receive($0) + AmountInput(text: $custom_amount_input, rate: rate) { + let msats = handle_custom_receive($0) if self.custom_amount_input != "" { if self.custom_amount_msats < min_amt { self.error = "Amount not allowed, must be higher than \(render_amount_msats(min_amt))" @@ -344,6 +349,7 @@ struct PayView: View { } } } + return msats } } @@ -353,7 +359,7 @@ struct PayView: View { Text("\(render_amount_msats(amt))") .font(.title) } else { - AmountInput(text: $custom_amount_input) { + AmountInput(text: $custom_amount_input, rate: rate) { handle_custom_receive($0) } } @@ -464,6 +470,9 @@ struct PayView: View { } func handle_confirm(ln mln: LNSocket?) { + // clear last error on confirm + self.error = nil + switch self.state { case .invoice_request(let reqinv): switch reqinv {