commit 88288bca30013b60b769abf6ef10f133f6416354
parent 64849054e9bb5d96a9ab57023ce4bea2d9198f97
Author: William Casarin <jb55@jb55.com>
Date: Fri, 25 Mar 2022 17:06:37 -0700
Receive view
Signed-off-by: William Casarin <jb55@jb55.com>
Diffstat:
6 files changed, 198 insertions(+), 26 deletions(-)
diff --git a/lightninglink.xcodeproj/project.pbxproj b/lightninglink.xcodeproj/project.pbxproj
@@ -11,6 +11,9 @@
4C035A0427AEFD2F00FF92CE /* Invoice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C035A0327AEFD2F00FF92CE /* Invoice.swift */; };
4C3DC97127D4829A00773021 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3DC97027D4829A00773021 /* SettingsView.swift */; };
4C4D7A5627DD84BA005A4F7F /* LNUrl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4D7A5527DD84BA005A4F7F /* LNUrl.swift */; };
+ 4C5EAC5427ED30A8002A6FE5 /* Rates.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C5EAC5327ED30A8002A6FE5 /* Rates.swift */; };
+ 4C5EAC5627EE8544002A6FE5 /* ReceiveView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C5EAC5527EE8544002A6FE5 /* ReceiveView.swift */; };
+ 4C5EAC5827EE8942002A6FE5 /* AmountInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C5EAC5727EE8942002A6FE5 /* AmountInputView.swift */; };
4C641D192788FF2F002A36C9 /* lightninglinkApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C641D182788FF2F002A36C9 /* lightninglinkApp.swift */; };
4C641D1B2788FF2F002A36C9 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C641D1A2788FF2F002A36C9 /* ContentView.swift */; };
4C641D1D2788FF30002A36C9 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4C641D1C2788FF30002A36C9 /* Assets.xcassets */; };
@@ -55,6 +58,9 @@
4C035A0327AEFD2F00FF92CE /* Invoice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Invoice.swift; sourceTree = "<group>"; };
4C3DC97027D4829A00773021 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
4C4D7A5527DD84BA005A4F7F /* LNUrl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LNUrl.swift; sourceTree = "<group>"; };
+ 4C5EAC5327ED30A8002A6FE5 /* Rates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Rates.swift; sourceTree = "<group>"; };
+ 4C5EAC5527EE8544002A6FE5 /* ReceiveView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiveView.swift; sourceTree = "<group>"; };
+ 4C5EAC5727EE8942002A6FE5 /* AmountInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AmountInputView.swift; sourceTree = "<group>"; };
4C641D152788FF2F002A36C9 /* lightninglink.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = lightninglink.app; sourceTree = BUILT_PRODUCTS_DIR; };
4C641D182788FF2F002A36C9 /* lightninglinkApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = lightninglinkApp.swift; sourceTree = "<group>"; };
4C641D1A2788FF2F002A36C9 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
@@ -149,6 +155,7 @@
4C8B289227B44EAF00DF3372 /* LNLink.swift */,
4C4D7A5527DD84BA005A4F7F /* LNUrl.swift */,
4C72DB2427DD9D9B00834545 /* QR.swift */,
+ 4C5EAC5327ED30A8002A6FE5 /* Rates.swift */,
);
path = lightninglink;
sourceTree = "<group>";
@@ -216,6 +223,8 @@
4C641D1A2788FF2F002A36C9 /* ContentView.swift */,
4CCB0E2A27CA71CA0026461C /* SetupView.swift */,
4C3DC97027D4829A00773021 /* SettingsView.swift */,
+ 4C5EAC5527EE8544002A6FE5 /* ReceiveView.swift */,
+ 4C5EAC5727EE8942002A6FE5 /* AmountInputView.swift */,
);
path = Views;
sourceTree = "<group>";
@@ -351,11 +360,13 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
+ 4C5EAC5827EE8942002A6FE5 /* AmountInputView.swift in Sources */,
4C035A0027AEF90000FF92CE /* PayView.swift in Sources */,
4C873FD527A6EF3F008C972C /* LNSocket.swift in Sources */,
4C641D1B2788FF2F002A36C9 /* ContentView.swift in Sources */,
4C8B289327B44EAF00DF3372 /* LNLink.swift in Sources */,
4CCB0E2727C979F30026461C /* ScannerCoordinator.swift in Sources */,
+ 4C5EAC5427ED30A8002A6FE5 /* Rates.swift in Sources */,
4C641D492789083E002A36C9 /* lightninglink.c in Sources */,
4CCB0E2627C979F30026461C /* CodeScanner.swift in Sources */,
4CCB0E2827C979F30026461C /* ScannerViewController.swift in Sources */,
@@ -364,6 +375,7 @@
4C641D192788FF2F002A36C9 /* lightninglinkApp.swift in Sources */,
4CCB0E2B27CA71CA0026461C /* SetupView.swift in Sources */,
4C873FD727A6F1F5008C972C /* RPC.swift in Sources */,
+ 4C5EAC5627EE8544002A6FE5 /* ReceiveView.swift in Sources */,
4C3DC97127D4829A00773021 /* SettingsView.swift in Sources */,
4C4D7A5627DD84BA005A4F7F /* LNUrl.swift in Sources */,
);
diff --git a/lightninglink/RPC.swift b/lightninglink/RPC.swift
@@ -47,6 +47,10 @@ public struct Channel: Decodable {
public var channel_total_sat: Int64
}
+public struct InvoiceRes: Decodable {
+ public let bolt11: String
+}
+
public struct LNUrlPayDecode {
public let description: String?
public let longDescription: String?
@@ -350,6 +354,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_invoice(ln: LNSocket, token: String, amount: InvoiceAmount = .any, description: String? = nil, expiry: String? = nil) -> RequestRes<InvoiceRes> {
+
+ let now = Date().timeIntervalSince1970
+ let label = "lnlink-\(now)"
+ let desc = description ?? "lnlink invoice"
+ var params: [String: String] = ["description": desc, "label": label]
+
+ if let exp = expiry {
+ params["expiry"] = exp
+ }
+
+ switch amount {
+ case .amount(let val):
+ params["msatoshi"] = "\(val)msat"
+ case .any:
+ params["msatoshi"] = "any"
+ case .min(let val):
+ params["msatoshi"] = "\(val)msat"
+ case .range(let min, let max):
+ params["msatoshi"] = "\(min)msat"
+ }
+
+ return performRpc(ln: ln, operation: "invoice", authToken: token, timeout_ms: default_timeout, params: params)
+}
+
public func rpc_pay(ln: LNSocket, token: String, bolt11: String, amount_msat: Int64?) -> RequestRes<Pay>
{
var params: Array<String> = [ bolt11 ]
diff --git a/lightninglink/Views/AmountInputView.swift b/lightninglink/Views/AmountInputView.swift
@@ -0,0 +1,34 @@
+//
+// AmountInput.swift
+// lightninglink
+//
+// Created by William Casarin on 2022-03-25.
+//
+
+import SwiftUI
+import Combine
+
+struct AmountInput: View {
+ let text: Binding<String>
+ let onReceive: (String) -> ()
+
+ 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")
+ }
+ }
+ }
+ .frame(height: 100)
+ }
+}
+
+
diff --git a/lightninglink/Views/ContentView.swift b/lightninglink/Views/ContentView.swift
@@ -36,6 +36,8 @@ enum ActiveAlert: Identifiable {
public enum ActiveSheet: Identifiable {
public var id: String {
switch self {
+ case .receive:
+ return "receive"
case .qr:
return "qrcode"
case .pay:
@@ -44,6 +46,7 @@ public enum ActiveSheet: Identifiable {
}
case qr
+ case receive
case pay(DecodeType)
}
@@ -111,6 +114,10 @@ struct ContentView: View {
return "-\(render_amount_msats(pay.msatoshi)) (\(render_amount_msats(fee)) fee)"
}
+ func receive_pay() {
+ self.active_sheet = .receive
+ }
+
func check_pay() {
guard let decode = get_clipboard_invoice() else {
self.active_sheet = .qr
@@ -150,7 +157,6 @@ struct ContentView: View {
Spacer()
}
}
- .padding()
Spacer()
Text("\(format_last_pay())")
@@ -168,13 +174,20 @@ struct ContentView: View {
Spacer()
HStack {
+ Button(action: receive_pay) {
+ Label("", systemImage: "arrow.down.circle")
+ }
+ .font(.largeTitle)
+
Spacer()
- Button("Pay", action: check_pay)
- .font(.title)
- .buttonStyle(.bordered)
- .padding()
+
+ Button(action: check_pay) {
+ Label("", systemImage: "qrcode.viewfinder")
+ }
+ .font(.largeTitle)
}
}
+ .padding()
.alert("Use invoice in clipboard?", isPresented: $has_alert, presenting: active_alert) { alert in
Button("Use QR") {
self.has_alert = false
@@ -204,6 +217,9 @@ struct ContentView: View {
}
+ case .receive:
+ ReceiveView(lnlink: lnlink)
+
case .pay(let decode):
PayView(decode: decode, lnlink: self.lnlink)
}
diff --git a/lightninglink/Views/PayView.swift b/lightninglink/Views/PayView.swift
@@ -331,7 +331,7 @@ struct PayView: View {
Text("\(render_amount_msats(amt))")
.font(.title)
} else {
- InputView {
+ AmountInput(text: $custom_amount_input) {
handle_custom_receive($0)
if self.custom_amount_input != "" {
if self.custom_amount_msats < min_amt {
@@ -353,7 +353,7 @@ struct PayView: View {
Text("\(render_amount_msats(amt))")
.font(.title)
} else {
- InputView {
+ AmountInput(text: $custom_amount_input) {
handle_custom_receive($0)
}
}
@@ -365,25 +365,6 @@ struct PayView: View {
}
}
- func InputView(onReceive: @escaping (String) -> ()) -> some View {
- // TODO remove from class, pass input binding?
- return Form {
- Section {
- HStack(alignment: .lastTextBaseline) {
- TextField("10,000", text: $custom_amount_input)
- .font(.title)
- .keyboardType(.numberPad)
- .multilineTextAlignment(.trailing)
- .onReceive(Just(self.custom_amount_input)) {
- onReceive($0)
- }
- Text("sats")
- }
- }
- }
- .frame(height: 100)
- }
-
func confirm_pay(ln: LNSocket?, inv: String, pay_amt: PayAmount?) {
let res = confirm_payment(ln: ln, lnlink: self.lnlink, bolt11: inv, pay_amt: pay_amt)
switch res {
diff --git a/lightninglink/Views/ReceiveView.swift b/lightninglink/Views/ReceiveView.swift
@@ -0,0 +1,100 @@
+//
+// ReceiveView.swift
+// lightninglink
+//
+// Created by William Casarin on 2022-03-25.
+//
+
+import SwiftUI
+import AVFoundation
+import CoreImage.CIFilterBuiltins
+
+struct ReceiveView: View {
+ @State private var loading: Bool = true
+ @State private var qr: Image? = nil
+ @State private var qr_data: String? = nil
+
+ let lnlink: LNLink
+
+ @Environment(\.presentationMode) var presentationMode
+
+ var body: some View {
+ VStack {
+ Text("Receive payment")
+ .font(.title)
+
+ Spacer()
+
+ if let qr = self.qr {
+ qr
+ .resizable()
+ .scaledToFit()
+ .frame(width: 300, height: 300)
+ .onTapGesture {
+ AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate))
+ UIPasteboard.general.string = self.qr_data
+ }
+ } else {
+ ProgressView()
+ .progressViewStyle(.circular)
+ }
+
+ Spacer()
+
+ HStack {
+ Button("Back") {
+ dismiss()
+ }
+ Spacer()
+ }
+ }
+ .padding()
+ .onAppear() {
+ make_invoice(lnlink: lnlink, expiry: "12h") { res in
+ switch res {
+ case .failure:
+ break
+ case .success(let invres):
+ self.qr = generate_qr(from: invres.bolt11)
+ self.qr_data = invres.bolt11
+ }
+ }
+ }
+ }
+
+ private func dismiss() {
+ self.presentationMode.wrappedValue.dismiss()
+ }
+
+}
+
+
+func generate_qr(from string: String) -> Image {
+ let context = CIContext()
+ let filter = CIFilter.qrCodeGenerator()
+
+ filter.message = Data(string.uppercased().utf8)
+ if let output_img = filter.outputImage {
+ if let cgimg = context.createCGImage(output_img, from: output_img.extent) {
+ let uiimg = UIImage(cgImage: cgimg)
+ return Image(uiImage: uiimg).interpolation(.none)
+ }
+ }
+
+ let uiimg = UIImage(systemName: "xmark.circle") ?? UIImage()
+ return Image(uiImage: uiimg)
+}
+
+func make_invoice(lnlink: LNLink, expiry: String, callback: @escaping (RequestRes<InvoiceRes>) -> ()) {
+ let ln = LNSocket()
+
+ ln.genkey()
+ guard ln.connect_and_init(node_id: lnlink.node_id, host: lnlink.host) else {
+ return
+ }
+
+ DispatchQueue.global(qos: .background).async {
+ let res = rpc_invoice(ln: ln, token: lnlink.token, amount: .any, description: "lnlink invoice", expiry: "12h")
+ callback(res)
+ }
+}