lnlink

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

commit 0c1d209642aa17ae11c0e80ba23d8e19b036219f
parent 266c01b1094171b343e96b02c406d1b5f5e321c9
Author: William Casarin <jb55@jb55.com>
Date:   Thu, 31 Mar 2022 15:26:11 -0700

option to generate bolt12 offers

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

Diffstat:
Mlightninglink/RPC.swift | 29+++++++++++++++++++++++++++++
Mlightninglink/Views/ReceiveView.swift | 130++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------
2 files changed, 114 insertions(+), 45 deletions(-)

diff --git a/lightninglink/RPC.swift b/lightninglink/RPC.swift @@ -51,6 +51,10 @@ public struct InvoiceRes: Decodable { public let bolt11: String } +public struct OfferRes: Decodable { + public let bolt12: String +} + public struct LNUrlPayDecode { public let description: String? public let longDescription: String? @@ -354,6 +358,31 @@ public func rpc_getinfo(ln: LNSocket, token: String, timeout: Int32 = default_ti return performRpc(ln: ln, operation: "getinfo", authToken: token, timeout_ms: default_timeout, params: params) } +public func rpc_offer(ln: LNSocket, token: String, amount: InvoiceAmount = .any, description: String? = nil, issuer: String? = nil) -> RequestRes<OfferRes> { + + let now = Date().timeIntervalSince1970 + let label = "lnlink-\(now)" + let desc = description ?? "lnlink offer" + var params: [String: String] = ["description": desc, "label": label] + + if let issuer = issuer { + params["issuer"] = issuer + } + + switch amount { + case .amount(let val): + params["amount"] = "\(val)msat" + case .any: + params["amount"] = "any" + case .min(let val): + params["amount"] = "\(val)msat" + case .range(let min, _): + params["amount"] = "\(min)msat" + } + + return performRpc(ln: ln, operation: "offer", authToken: token, timeout_ms: default_timeout, params: params) +} + public func rpc_invoice(ln: LNSocket, token: String, amount: InvoiceAmount = .any, description: String? = nil, expiry: String? = nil) -> RequestRes<InvoiceRes> { let now = Date().timeIntervalSince1970 diff --git a/lightninglink/Views/ReceiveView.swift b/lightninglink/Views/ReceiveView.swift @@ -19,9 +19,11 @@ struct ReceiveView: View { @State private var loading: Bool = true @State private var qr_data: QRData? = nil @State private var description: String = "" + @State private var issuer: String = "" @State private var amount: Int64? = nil @State private var amount_str: String = "" @State private var making: Bool = false + @State private var is_offer: Bool = false @FocusState private var is_kb_focused: Bool @Binding var rate: ExchangeRate? @@ -34,6 +36,48 @@ struct ReceiveView: View { .progressViewStyle(.circular) } + func invoice_details_form() -> some View { + Group { + Form { + Section(header: Text("Invoice Details")) { + Toggle("Offer (bolt12)", isOn: $is_offer) + .padding() + + TextField("Description", text: $description) + .font(.body) + .focused($is_kb_focused) + .padding() + + if self.is_offer { + TextField("Issuer", text: $issuer) + .font(.body) + .focused($is_kb_focused) + .padding() + } + + AmountInput(text: $amount_str, placeholder: "any") { parsed in + if let str = parsed.msats_str { + self.amount_str = str + } + if let msats = parsed.msats { + self.amount = msats + } + } + .padding() + .focused($is_kb_focused) + } + } + .frame(height: 350) + + if self.amount_str != "", let msats = self.amount { + if let rate = self.rate { + Text("\(msats_to_fiat(msats: msats, xr: rate))") + .foregroundColor(.gray) + } + } + } + } + var body: some View { VStack { Text("Receive payment") @@ -42,40 +86,13 @@ struct ReceiveView: View { Spacer() if let qr = self.qr_data { - qrcode_view(qr) + QRCodeView(qr: qr) } else { if making { ProgressView() .progressViewStyle(.circular) } else { - Form { - TextField("Description", text: $description) - .font(.body) - .focused($is_kb_focused) - - Section { - AmountInput(text: $amount_str, placeholder: "any") { parsed in - if let str = parsed.msats_str { - self.amount_str = str - } - if let msats = parsed.msats { - self.amount = msats - } - } - .focused($is_kb_focused) - - } - - } - .frame(height: 200) - - if self.amount_str != "", let msats = self.amount { - if let rate = self.rate { - Text("\(msats_to_fiat(msats: msats, xr: rate))") - .foregroundColor(.gray) - } - } - + invoice_details_form() } } @@ -97,14 +114,16 @@ struct ReceiveView: View { if !self.making && self.qr_data == nil { Button("Receive") { self.making = true - make_invoice(lnlink: lnlink, expiry: "12h", description: self.description, amount: self.amount) { res in - self.making = false + make_invoice(lnlink: lnlink, expiry: "12h", description: self.description, amount: self.amount, issuer: self.issuer, is_offer: self.is_offer) { res in switch res { case .failure: + self.making = false break case .success(let invres): - let img = generate_qr(from: invres.bolt11) - self.qr_data = QRData(img: img, data: invres.bolt11) + let upper = invres.uppercased() + let img = generate_qr(from: upper) + self.making = false + self.qr_data = QRData(img: img, data: upper) } } } @@ -143,7 +162,7 @@ func generate_qr(from string: String) -> Image { return Image(uiImage: uiimg) } -func make_invoice(lnlink: LNLink, expiry: String, description: String?, amount: Int64?, callback: @escaping (RequestRes<InvoiceRes>) -> ()) { +func make_invoice(lnlink: LNLink, expiry: String, description: String?, amount: Int64?, issuer: String?, is_offer: Bool, callback: @escaping (RequestRes<String>) -> ()) { let ln = LNSocket() ln.genkey() @@ -151,24 +170,45 @@ func make_invoice(lnlink: LNLink, expiry: String, description: String?, amount: return } - DispatchQueue.global(qos: .background).async { var amt: InvoiceAmount = .any if let a = amount { amt = .amount(a) } - let res = rpc_invoice(ln: ln, token: lnlink.token, amount: amt, description: description ?? "lnlink invoice", expiry: "12h") - callback(res) + + let desc = description ?? "lnlink invoice" + let expiry = "12h" + if is_offer { + let res = rpc_offer(ln: ln, token: lnlink.token, amount: amt, description: desc, issuer: issuer) + callback(res.map{ $0.bolt12 }) + } else { + let res = rpc_invoice(ln: ln, token: lnlink.token, amount: amt, description: desc, expiry: expiry) + callback(res.map{ $0.bolt11 }) + } } } -func qrcode_view(_ qrd: QRData) -> some View { - qrd.img - .resizable() - .scaledToFit() - .frame(width: 300, height: 300) - .onTapGesture { - AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate)) - UIPasteboard.general.string = qrd.data +struct QRCodeView: View { + let qr: QRData + @State var copied: Bool = false + + var body: some View { + Group { + qr.img + .resizable() + .scaledToFit() + .frame(width: 300, height: 300) + .onTapGesture { + AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate)) + UIPasteboard.general.string = self.qr.data + copied = true + } + + Text("\(!copied ? "Tap QR to copy invoice" : "Copied!")") + .font(.subheadline) + .foregroundColor(.gray) + } + + } }