commit 2b8806932db23c6a2d86794832d7b634c0e89032
parent 44038ffb7a81672fabc08620680e3eafc3edc6eb
Author: William Casarin <jb55@jb55.com>
Date: Sat, 5 Mar 2022 14:18:12 -0800
custom amounts + offers workingish
Signed-off-by: William Casarin <jb55@jb55.com>
Diffstat:
3 files changed, 211 insertions(+), 53 deletions(-)
diff --git a/lightninglink/Invoice.swift b/lightninglink/Invoice.swift
@@ -15,6 +15,7 @@ public enum DecodeType {
public enum InvoiceAmount {
case amount(Int64)
+ case min(Int64)
case any
}
diff --git a/lightninglink/RPC.swift b/lightninglink/RPC.swift
@@ -316,11 +316,9 @@ public func rpc_fetchinvoice(ln: LNSocket, token: String, req: FetchInvoiceReq)
{
var params: [String: String] = [ "offer": req.offer ]
- switch req.amount {
- case .amount(let amt):
+ if req.pay_amt != nil {
+ let amt = req.pay_amt!.amount + (req.pay_amt!.tip ?? 0)
params["msatoshi"] = "\(amt)msat"
- case .any:
- break
}
if req.quantity != nil {
diff --git a/lightninglink/Views/PayView.swift b/lightninglink/Views/PayView.swift
@@ -13,8 +13,14 @@ public struct ReadyInvoice {
let amount: InvoiceAmount
}
+public struct PayAmount {
+ let tip: Int64?
+ let amount: Int64
+}
+
public struct FetchInvoiceReq {
let offer: String
+ let pay_amt: PayAmount?
let amount: InvoiceAmount
let quantity: Int?
}
@@ -25,6 +31,7 @@ public enum PayState {
case decoded(DecodeType)
case fetch_invoice(LNSocket, FetchInvoiceReq)
case ready(ReadyInvoice)
+ case offer_input(ReadyInvoice, Decode)
}
struct PayView: View {
@@ -39,6 +46,7 @@ struct PayView: View {
@State var invoice: Decode?
@State var error: String?
@State var expiry_percent: Double?
+ @State var custom_amount: String = ""
@Environment(\.presentationMode) var presentationMode
@@ -113,8 +121,10 @@ struct PayView: View {
.foregroundColor(.gray)
}
}
+
Spacer()
+ // Middle area
let ready_invoice = is_ready(state)
if ready_invoice != nil {
amount_view_inv(ready_invoice!.amount)
@@ -122,7 +132,10 @@ struct PayView: View {
Text("\(self.error ?? "")")
.foregroundColor(Color.red)
+
Spacer()
+
+ // Bottom area
HStack {
Button("Cancel") {
self.dismiss()
@@ -132,7 +145,14 @@ struct PayView: View {
Spacer()
- confirm_button(ready_invoice)
+ if should_show_confirm(self.state) {
+ Button("Confirm") {
+ handle_confirm(ln: nil)
+ }
+ .foregroundColor(Color.green)
+ .font(.title)
+ }
+
}
}
.padding()
@@ -144,25 +164,126 @@ struct PayView: View {
}
}
- func confirm_button(_ ready_invoice: ReadyInvoice?) -> some View {
+ func handle_custom_receive(_ new_val: String) {
+ let filtered = new_val.filter { "0123456789".contains($0) }
+ if filtered != new_val {
+ self.custom_amount = filtered
+ }
+ }
+
+ func amount_view_inv(_ amt: InvoiceAmount) -> some View {
Group {
- if ready_invoice != nil {
- Button("Confirm") {
- let res = confirm_payment(bolt11: ready_invoice!.invoice, lnlink: self.lnlink)
+ Text("Pay")
+ switch amt {
+ case .min(let min_amt):
+ Text("\(render_amount_msats(min_amt))")
+ .font(.title)
+ Text("Tip?")
+ TextField("Amount", text: $custom_amount)
+ .keyboardType(.numberPad)
+ .onReceive(Just(self.custom_amount)) {
+ handle_custom_receive($0)
+ }
- switch res {
- case .left(let err):
- self.error = "Error: \(err)"
+ case .any:
+ TextField("Amount", text: $custom_amount)
+ .keyboardType(.numberPad)
+ .onReceive(Just(self.custom_amount)) {
+ handle_custom_receive($0)
+ }
+ case .amount(let amt):
+ Text("\(render_amount_msats(amt))")
+ .font(.title)
+ }
+ }
+ }
+
+ 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 {
+ case .left(let err):
+ self.error = "Error: \(err)"
+
+ case .right(let pay):
+ print(pay)
+ self.dismiss()
+ NotificationCenter.default.post(name: .sentPayment, object: pay)
+ }
+ }
- case .right(let pay):
- print(pay)
- self.dismiss()
- NotificationCenter.default.post(name: .sentPayment, object: pay)
+ func get_pay_amount(_ amt: InvoiceAmount) -> Either<String, PayAmount> {
+ let m_pay_amount = Int64(self.custom_amount)
+
+ switch amt {
+ case .min(let min_amt):
+ // TODO: get tip from tip percent buttons
+ let tip = m_pay_amount ?? 0
+ return .right(PayAmount(tip: tip, amount: min_amt))
+ case .any:
+ guard let custom_amount = Int64(self.custom_amount) else {
+ return .left("Invalid amount: '\(self.custom_amount)'")
+ }
+ return .right(PayAmount(tip: 0, amount: custom_amount))
+ case .amount(let amt):
+ return .right(PayAmount(tip: 0, amount: amt))
+ }
+ }
+
+ func handle_confirm(ln mln: LNSocket?) {
+ switch self.state {
+ case .offer_input(let inv, let decoded):
+ switch get_pay_amount(inv.amount) {
+ case .left(let err):
+ self.error = err
+ return
+ case .right(let pay_amt):
+ let req = fetchinvoice_req_from_offer(
+ offer: decoded,
+ offer_str: inv.invoice,
+ pay_amt: pay_amt)
+ switch req {
+ case .left(let err):
+ self.error = err
+ case .right(let req):
+ let token = self.lnlink.token
+ DispatchQueue.global(qos: .background).async {
+ let ln = mln ?? LNSocket()
+ if mln == nil {
+ guard ln.connect_and_init(node_id: self.lnlink.node_id, host: self.lnlink.host) else {
+ self.error = "Connection failed when fetching invoice"
+ return
+ }
+ }
+ switch rpc_fetchinvoice(ln: ln, token: token, req: req) {
+ case .failure(let err):
+ self.error = err.description
+ case .success(let fetch_invoice):
+ confirm_pay(ln: ln, inv: fetch_invoice.invoice, pay_amt: nil)
+ }
}
}
- .foregroundColor(Color.green)
- .font(.title)
}
+
+
+ case .ready(let ready_invoice):
+ switch get_pay_amount(ready_invoice.amount) {
+ case .left(let err):
+ self.error = err
+ case .right(let pay_amt):
+ confirm_pay(ln: mln, inv: ready_invoice.invoice, pay_amt: pay_amt)
+ }
+
+ case .initial: fallthrough
+ case .decoding: fallthrough
+ case .decoded: fallthrough
+ case .fetch_invoice:
+ self.error = "Invalid state: \(self.state)"
+ }
+ }
+
+ func confirm_button() -> some View {
+ Group {
}
}
@@ -175,6 +296,8 @@ struct PayView: View {
switch self.state {
case .ready:
break
+ case .offer_input:
+ break
case .initial:
switch_state(.decoding(nil, self.init_invoice_str))
case .decoding(let ln, let inv):
@@ -196,7 +319,17 @@ struct PayView: View {
case .failure(let err):
self.error = err.description
case .success(let fetch_invoice):
- switch_state(.decoding(ln, fetch_invoice.invoice))
+ confirm_pay(ln: ln, inv: fetch_invoice.invoice, pay_amt: req.pay_amt)
+ }
+ }
+
+ func handle_offer(ln: LNSocket, decoded: Decode, inv: String) {
+ switch handle_bolt12_offer(ln: ln, decoded: decoded, inv: inv) {
+ case .right(let state):
+ self.invoice = decoded
+ switch_state(state)
+ case .left(let err):
+ self.error = err
}
}
@@ -213,15 +346,8 @@ struct PayView: View {
self.error = fail.description
case .success(let decoded):
if decoded.type == "bolt12 offer" {
- // TODO: handle custom amounts for offers
- let amt: Int64? = 10000
- let req = fetchinvoice_req_from_offer(offer: decoded, offer_str: inv, amount: amt)
- switch req {
- case .left(let err):
- self.error = err
- case .right(let req):
- switch_state(.fetch_invoice(ln, req))
- }
+ self.handle_offer(ln: ln, decoded: decoded, inv: inv)
+
} else if decoded.type == "bolt11 invoice" || decoded.type == "bolt12 invoice" {
var amount: InvoiceAmount = .any
if decoded.amount_msat != nil {
@@ -282,7 +408,7 @@ struct PayView: View {
}
}
-func fetchinvoice_req_from_offer(offer: Decode, offer_str: String, amount: Int64?) -> Either<String, FetchInvoiceReq> {
+func fetchinvoice_req_from_offer(offer: Decode, offer_str: String, pay_amt: PayAmount) -> Either<String, FetchInvoiceReq> {
var qty: Int? = nil
if offer.quantity_min != nil {
@@ -290,13 +416,10 @@ func fetchinvoice_req_from_offer(offer: Decode, offer_str: String, amount: Int64
}
if offer.amount_msat != nil {
- return .right(FetchInvoiceReq(offer: offer_str, amount: .any, quantity: qty))
+ return .right(.init(offer: offer_str, pay_amt: pay_amt, amount: .any, quantity: qty))
} else {
- guard let amt = amount else {
- return .left("Amount required for offer")
- }
-
- return .right(FetchInvoiceReq(offer: offer_str, amount: .amount(amt), quantity: qty))
+ let amount: InvoiceAmount = .amount(pay_amt.amount)
+ return .right(.init(offer: offer_str, pay_amt: pay_amt, amount: amount, quantity: qty))
}
}
@@ -310,19 +433,25 @@ public enum Either<L, R> {
case right(R)
}
-func confirm_payment(bolt11: String, lnlink: LNLink) -> Either<String, Pay> {
- // do a fresh connection for each payment
- let ln = LNSocket()
+func confirm_payment(ln mln: LNSocket?, lnlink: LNLink, bolt11: String, pay_amt: PayAmount?) -> Either<String, Pay> {
+ let ln = mln ?? LNSocket()
+
+ if mln == nil {
+ guard ln.connect_and_init(node_id: lnlink.node_id, host: lnlink.host) else {
+ return .left("Failed to connect, please try again!")
+ }
+ }
- guard ln.connect_and_init(node_id: lnlink.node_id, host: lnlink.host) else {
- return .left("Failed to connect, please try again!")
+ var amount_msat: Int64? = nil
+ if pay_amt != nil {
+ amount_msat = pay_amt!.amount + (pay_amt!.tip ?? 0)
}
let res = rpc_pay(
ln: ln,
token: lnlink.token,
bolt11: bolt11,
- amount_msat: nil)
+ amount_msat: amount_msat)
switch res {
case .failure(let req_err):
@@ -344,6 +473,8 @@ func is_ready(_ state: PayState) -> ReadyInvoice? {
switch state {
case .ready(let ready_invoice):
return ready_invoice
+ case .offer_input(let ready_invoice, _):
+ return ready_invoice
case .fetch_invoice: fallthrough
case .initial: fallthrough
case .decoding: fallthrough
@@ -352,18 +483,6 @@ func is_ready(_ state: PayState) -> ReadyInvoice? {
}
}
-func amount_view_inv(_ amt: InvoiceAmount) -> some View {
- Group {
- switch amt {
- case .any:
- Text("Custom amounts not supported yet :(")
- case .amount(let amt):
- Text("Pay")
- Text("\(render_amount_msats(amt))")
- .font(.title)
- }
- }
-}
func render_amount(_ amt: InvoiceAmount) -> String {
switch amt {
@@ -371,6 +490,8 @@ func render_amount(_ amt: InvoiceAmount) -> String {
return "Enter amount"
case .amount(let amt):
return "\(render_amount_msats(amt))?"
+ case .min(let min):
+ return "\(render_amount_msats(min))?"
}
}
@@ -391,3 +512,41 @@ struct PayView_Previews: PreviewProvider {
}
}
*/
+
+func handle_bolt12_offer(ln: LNSocket, decoded: Decode, inv: String) -> Either<String, PayState> {
+ if decoded.amount_msat != nil {
+ guard let min_amt = parse_msat(decoded.amount_msat!) else {
+ return .left("Error parsing amount_msat: '\(decoded.amount_msat!)'")
+ }
+ let ready = ReadyInvoice(invoice: inv, amount: .min(min_amt))
+ return .right(.offer_input(ready, decoded))
+ } else {
+ let ready = ReadyInvoice(invoice: inv, amount: .any)
+ return .right(.offer_input(ready, decoded))
+ }
+}
+
+
+func confirm_offer(ln: LNSocket, bolt12: String, decoded: Decode, pay_amt: PayAmount) -> Either<String, PayState> {
+ let req = fetchinvoice_req_from_offer(offer: decoded, offer_str: bolt12, pay_amt: pay_amt)
+ switch req {
+ case .left(let err):
+ return .left(err)
+ case .right(let req):
+ return .right(.fetch_invoice(ln, req))
+ }
+}
+
+func should_show_confirm(_ state: PayState) -> Bool {
+ switch state {
+ case .ready: fallthrough
+ case .offer_input:
+ return true
+
+ case .decoded: fallthrough
+ case .initial: fallthrough
+ case .fetch_invoice: fallthrough
+ case .decoding:
+ return false
+ }
+}