From 037359e26b672049258a1bb1da0e1d34366a4bc8 Mon Sep 17 00:00:00 2001 From: Matthew Date: Fri, 19 Jul 2024 11:10:31 -0500 Subject: [PATCH 01/10] bip21: recieve --- LDKNodeMonday.xcodeproj/project.pbxproj | 42 +++-- .../xcshareddata/swiftpm/Package.resolved | 11 +- LDKNodeMonday/Model/ReceiveOption.swift | 1 + .../LightningNodeService.swift | 16 ++ .../LightningServiceError.swift | 6 + .../Home/Receive/BIP21ViewModel.swift | 55 ++++++ .../View/Home/Receive/BIP21View.swift | 175 ++++++++++++++++++ .../View/Home/Receive/ReceiveView.swift | 2 + 8 files changed, 280 insertions(+), 28 deletions(-) create mode 100644 LDKNodeMonday/View Model/Home/Receive/BIP21ViewModel.swift create mode 100644 LDKNodeMonday/View/Home/Receive/BIP21View.swift diff --git a/LDKNodeMonday.xcodeproj/project.pbxproj b/LDKNodeMonday.xcodeproj/project.pbxproj index 8efac00..6ed98bf 100644 --- a/LDKNodeMonday.xcodeproj/project.pbxproj +++ b/LDKNodeMonday.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 56; + objectVersion = 60; objects = { /* Begin PBXBuildFile section */ @@ -32,7 +32,6 @@ AE17E8E129A402E40058C9C9 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AE17E8E029A402E40058C9C9 /* Preview Assets.xcassets */; }; AE17E90D29A42D430058C9C9 /* LightningNodeService.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE17E90C29A42D430058C9C9 /* LightningNodeService.swift */; }; AE186B8E2A1540B700338463 /* StartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE186B8D2A1540B700338463 /* StartView.swift */; }; - AE1AED452C25C94D00B467EF /* LDKNode in Frameworks */ = {isa = PBXBuildFile; productRef = AE1AED442C25C94D00B467EF /* LDKNode */; }; AE1D9BEC2B2A1FFD00620748 /* BitcoinUI in Frameworks */ = {isa = PBXBuildFile; productRef = AE1D9BEB2B2A1FFD00620748 /* BitcoinUI */; }; AE1D9C0B2B2A251500620748 /* ChannelDetails+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE1D9C0A2B2A251500620748 /* ChannelDetails+Extensions.swift */; }; AE3815362B6A9705006B2952 /* LightningNodesService.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE3815352B6A9705006B2952 /* LightningNodesService.swift */; }; @@ -70,6 +69,9 @@ AE7D3FAD2A4263C100EAE730 /* PaymentsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE7D3FAC2A4263C100EAE730 /* PaymentsViewModel.swift */; }; AE80116B29A59976009B9967 /* NodeIDView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE80116A29A59976009B9967 /* NodeIDView.swift */; }; AE80116D29A59AF4009B9967 /* ChannelAddView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE80116C29A59AF4009B9967 /* ChannelAddView.swift */; }; + AE80C2002C4AB360006E7193 /* BIP21View.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE80C1FF2C4AB360006E7193 /* BIP21View.swift */; }; + AE80C2022C4AB38D006E7193 /* BIP21ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE80C2012C4AB38D006E7193 /* BIP21ViewModel.swift */; }; + AE80C2052C4AB5E4006E7193 /* LDKNode in Frameworks */ = {isa = PBXBuildFile; productRef = AE80C2042C4AB5E4006E7193 /* LDKNode */; }; AE94226A2A007D6C007E4F12 /* ChannelsListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE9422692A007D6C007E4F12 /* ChannelsListView.swift */; }; AEA057E92B912C3C00DB1096 /* ZeroInvoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEA057E82B912C3C00DB1096 /* ZeroInvoiceView.swift */; }; AEA057EB2B912E1800DB1096 /* AmountInvoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEA057EA2B912E1800DB1096 /* AmountInvoiceView.swift */; }; @@ -154,6 +156,8 @@ AE7D3FAC2A4263C100EAE730 /* PaymentsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentsViewModel.swift; sourceTree = ""; }; AE80116A29A59976009B9967 /* NodeIDView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeIDView.swift; sourceTree = ""; }; AE80116C29A59AF4009B9967 /* ChannelAddView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelAddView.swift; sourceTree = ""; }; + AE80C1FF2C4AB360006E7193 /* BIP21View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BIP21View.swift; sourceTree = ""; }; + AE80C2012C4AB38D006E7193 /* BIP21ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BIP21ViewModel.swift; sourceTree = ""; }; AE8D877429B145F100F2B918 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; AE9422692A007D6C007E4F12 /* ChannelsListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelsListView.swift; sourceTree = ""; }; AEA057E82B912C3C00DB1096 /* ZeroInvoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZeroInvoiceView.swift; sourceTree = ""; }; @@ -189,7 +193,7 @@ AEE5B7652A09B1FC001E5E59 /* CodeScanner in Frameworks */, AE51BEFD2B37A5DC00BAE452 /* SimpleToast in Frameworks */, AE7C4A082B406D590061189D /* SimpleToast in Frameworks */, - AE1AED452C25C94D00B467EF /* LDKNode in Frameworks */, + AE80C2052C4AB5E4006E7193 /* LDKNode in Frameworks */, AE01C5B02AB3BEED00F28C7E /* KeychainAccess in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -283,6 +287,7 @@ AEAAD9D22C23399700765F5B /* Bolt12ZeroInvoiceViewModel.swift */, AE5BFE852C0B9D7B003B467C /* Bolt12InvoiceViewModel.swift */, AE028A272B96325700B336E7 /* AmountInvoiceViewModel.swift */, + AE80C2012C4AB38D006E7193 /* BIP21ViewModel.swift */, AE028A292B96328600B336E7 /* JITInvoiceViewModel.swift */, AE49E8552A253674002623E8 /* AddressViewModel.swift */, ); @@ -378,6 +383,7 @@ AE5BFE832C0B9D06003B467C /* Bolt12InvoiceView.swift */, AEAAD9D02C23394D00765F5B /* Bolt12ZeroInvoiceView.swift */, AEA057EA2B912E1800DB1096 /* AmountInvoiceView.swift */, + AE80C1FF2C4AB360006E7193 /* BIP21View.swift */, AEA057EC2B912FEA00DB1096 /* JITInvoiceView.swift */, AE17E8DB29A402E30058C9C9 /* AddressView.swift */, ); @@ -555,7 +561,7 @@ AE51BEFC2B37A5DC00BAE452 /* SimpleToast */, AE7C4A072B406D590061189D /* SimpleToast */, AE060C372C051B59006724F1 /* LDKNode */, - AE1AED442C25C94D00B467EF /* LDKNode */, + AE80C2042C4AB5E4006E7193 /* LDKNode */, ); productName = LDKNodeMonday; productReference = AE17E8D629A402E30058C9C9 /* LDKNodeMonday.app */; @@ -590,7 +596,7 @@ AE01C5AE2AB3BEED00F28C7E /* XCRemoteSwiftPackageReference "KeychainAccess" */, AE1D9BEA2B2A1FFD00620748 /* XCRemoteSwiftPackageReference "BitcoinUI" */, AE7C4A062B406D590061189D /* XCRemoteSwiftPackageReference "SimpleToast" */, - AE1AED432C25C94D00B467EF /* XCRemoteSwiftPackageReference "ldk-node" */, + AE80C2032C4AB5E4006E7193 /* XCLocalSwiftPackageReference "../ldk-node/bindings/swift" */, ); productRefGroup = AE17E8D729A402E30058C9C9 /* Products */; projectDirPath = ""; @@ -626,6 +632,7 @@ AE0055162B4A0E0100100797 /* Logger+Extensions.swift in Sources */, AEA057EB2B912E1800DB1096 /* AmountInvoiceView.swift in Sources */, AE70963C2B5C22270038BE56 /* CurrencyCode.swift in Sources */, + AE80C2002C4AB360006E7193 /* BIP21View.swift in Sources */, AE01C5B22AB3BF3C00F28C7E /* KeyService.swift in Sources */, AE49E8642A2537B3002623E8 /* PeersListViewModel.swift in Sources */, AE49E8542A253647002623E8 /* BitcoinViewModel.swift in Sources */, @@ -685,6 +692,7 @@ AE6BB56F2A008CBA009E16E3 /* UInt64+Extensions.swift in Sources */, AE49E85A2A2536D4002623E8 /* ChannelAddViewModel.swift in Sources */, AE0055142B4895B500100797 /* SeedViewModel.swift in Sources */, + AE80C2022C4AB38D006E7193 /* BIP21ViewModel.swift in Sources */, AE7D3FAB2A4263AE00EAE730 /* PaymentsView.swift in Sources */, AE80116D29A59AF4009B9967 /* ChannelAddView.swift in Sources */, AE551D472B8ECE7D0034B61E /* Payment.swift in Sources */, @@ -906,6 +914,13 @@ }; /* End XCConfigurationList section */ +/* Begin XCLocalSwiftPackageReference section */ + AE80C2032C4AB5E4006E7193 /* XCLocalSwiftPackageReference "../ldk-node/bindings/swift" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = "../ldk-node/bindings/swift"; + }; +/* End XCLocalSwiftPackageReference section */ + /* Begin XCRemoteSwiftPackageReference section */ AE01C5AE2AB3BEED00F28C7E /* XCRemoteSwiftPackageReference "KeychainAccess" */ = { isa = XCRemoteSwiftPackageReference; @@ -915,14 +930,6 @@ version = 4.2.2; }; }; - AE1AED432C25C94D00B467EF /* XCRemoteSwiftPackageReference "ldk-node" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/lightningdevkit/ldk-node.git"; - requirement = { - kind = exactVersion; - version = 0.3.0; - }; - }; AE1D9BEA2B2A1FFD00620748 /* XCRemoteSwiftPackageReference "BitcoinUI" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/reez/BitcoinUI.git"; @@ -959,11 +966,6 @@ isa = XCSwiftPackageProductDependency; productName = LDKNode; }; - AE1AED442C25C94D00B467EF /* LDKNode */ = { - isa = XCSwiftPackageProductDependency; - package = AE1AED432C25C94D00B467EF /* XCRemoteSwiftPackageReference "ldk-node" */; - productName = LDKNode; - }; AE1D9BEB2B2A1FFD00620748 /* BitcoinUI */ = { isa = XCSwiftPackageProductDependency; package = AE1D9BEA2B2A1FFD00620748 /* XCRemoteSwiftPackageReference "BitcoinUI" */; @@ -978,6 +980,10 @@ package = AE7C4A062B406D590061189D /* XCRemoteSwiftPackageReference "SimpleToast" */; productName = SimpleToast; }; + AE80C2042C4AB5E4006E7193 /* LDKNode */ = { + isa = XCSwiftPackageProductDependency; + productName = LDKNode; + }; AEE5B7642A09B1FC001E5E59 /* CodeScanner */ = { isa = XCSwiftPackageProductDependency; package = AEE5B7632A09B1FC001E5E59 /* XCRemoteSwiftPackageReference "CodeScanner" */; diff --git a/LDKNodeMonday.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/LDKNodeMonday.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index c0a4157..89d3b4b 100644 --- a/LDKNodeMonday.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/LDKNodeMonday.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "3c4f831e3e2d84e3d749572be35ef446cf04910008de67451a51af6ca58e22b3", + "originHash" : "2d457a0d145c5ec561d10ae3f13d805b262d5bfda9ebf0868311848416a25b6e", "pins" : [ { "identity" : "bitcoinui", @@ -28,15 +28,6 @@ "version" : "4.2.2" } }, - { - "identity" : "ldk-node", - "kind" : "remoteSourceControl", - "location" : "https://github.com/lightningdevkit/ldk-node.git", - "state" : { - "revision" : "bd9bd683201c1597d8930ac4118fa950cedfd56c", - "version" : "0.3.0" - } - }, { "identity" : "simpletoast", "kind" : "remoteSourceControl", diff --git a/LDKNodeMonday/Model/ReceiveOption.swift b/LDKNodeMonday/Model/ReceiveOption.swift index ba8ab4c..04d0395 100644 --- a/LDKNodeMonday/Model/ReceiveOption.swift +++ b/LDKNodeMonday/Model/ReceiveOption.swift @@ -14,6 +14,7 @@ enum ReceiveOption: String, CaseIterable, Identifiable { // case bolt12Zero = "Bolt12 0" case bolt12 = "Bolt12" case bitcoin = "Address" + case bip21 = "BIP21" var id: Self { self } } diff --git a/LDKNodeMonday/Service/Lightning Service/LightningNodeService.swift b/LDKNodeMonday/Service/Lightning Service/LightningNodeService.swift index 3838887..b0abfce 100644 --- a/LDKNodeMonday/Service/Lightning Service/LightningNodeService.swift +++ b/LDKNodeMonday/Service/Lightning Service/LightningNodeService.swift @@ -186,6 +186,12 @@ class LightningNodeService { return paymentId } + // Parses a URI string, attempts to pay a BOLT12 offer, BOLT11 invoice, then falls back to the on-chain address if the offer and invoice fail. + func send(uriStr: String) async throws -> QrPaymentResult { + let qrPaymentResult = try ldkNode.unifiedQrPayment().send(uriStr: uriStr) + return qrPaymentResult + } + func sendUsingAmount(bolt11Invoice: Bolt11Invoice, amountMsat: UInt64) async throws -> PaymentHash { @@ -227,6 +233,16 @@ class LightningNodeService { return offer } + // Generates a BIP21 URI string with an on the address and BOLT11 invoice. + func receive(amountSat: UInt64, message: String, expirySec: UInt32) async throws -> String { + let bip21UriString = try ldkNode.unifiedQrPayment().receive( + amountSats: amountSat, + message: message, + expirySec: expirySec + ) + return bip21UriString + } + func receiveVariableAmount(description: String, expirySecs: UInt32) async throws -> Bolt11Invoice { diff --git a/LDKNodeMonday/Service/Lightning Service/LightningServiceError.swift b/LDKNodeMonday/Service/Lightning Service/LightningServiceError.swift index 1cc85a8..323b624 100644 --- a/LDKNodeMonday/Service/Lightning Service/LightningServiceError.swift +++ b/LDKNodeMonday/Service/Lightning Service/LightningServiceError.swift @@ -155,6 +155,12 @@ func handleNodeError(_ error: NodeError) -> MondayError { case .GossipUpdateTimeout(let message): return .init(title: "GossipUpdateTimeout", detail: message) + case .UriParameterParsingFailed(let message): + return .init(title: "UriParameterParsingFailed", detail: message) + + case .InvalidUri(let message): + return .init(title: "InvalidUri", detail: message) + } } diff --git a/LDKNodeMonday/View Model/Home/Receive/BIP21ViewModel.swift b/LDKNodeMonday/View Model/Home/Receive/BIP21ViewModel.swift new file mode 100644 index 0000000..b3f0173 --- /dev/null +++ b/LDKNodeMonday/View Model/Home/Receive/BIP21ViewModel.swift @@ -0,0 +1,55 @@ +// +// BIP21ViewModel.swift +// LDKNodeMonday +// +// Created by Matthew Ramsden on 7/19/24. +// + +import BitcoinUI +import Foundation +import LDKNode +import SwiftUI + +class BIP21ViewModel: ObservableObject { + @Published var unified: String = "" + @Published var receiveViewError: MondayError? + @Published var networkColor = Color.gray + @Published var amountSat: String = "" + + func receivePayment(amountSat: UInt64, message: String, expirySecs: UInt32) async { + do { + let unified = try await LightningNodeService.shared.receive( + amountSat: amountSat, + message: message, + expirySec: expirySecs + ) + DispatchQueue.main.async { + self.unified = unified + } + } catch let error as NodeError { + let errorString = handleNodeError(error) + DispatchQueue.main.async { + self.receiveViewError = .init(title: errorString.title, detail: errorString.detail) + } + } catch { + DispatchQueue.main.async { + self.receiveViewError = .init( + title: "Unexpected error", + detail: error.localizedDescription + ) + } + } + } + + func clearInvoice() { + self.unified = "" + } + + func getColor() { + let color = LightningNodeService.shared.networkColor + DispatchQueue.main.async { + self.networkColor = color + } + } + +} diff --git a/LDKNodeMonday/View/Home/Receive/BIP21View.swift b/LDKNodeMonday/View/Home/Receive/BIP21View.swift new file mode 100644 index 0000000..e3db035 --- /dev/null +++ b/LDKNodeMonday/View/Home/Receive/BIP21View.swift @@ -0,0 +1,175 @@ +// +// BIP21View.swift +// LDKNodeMonday +// +// Created by Matthew Ramsden on 7/19/24. +// + +import BitcoinUI +import LDKNode +import SwiftUI + +struct BIP21View: View { + @ObservedObject var viewModel: BIP21ViewModel + @State private var isCopied = false + @State private var showCheckmark = false + @State private var showingReceiveViewErrorAlert = false + @State private var isKeyboardVisible = false + + var body: some View { + + VStack { + + if viewModel.unified == "" { + + VStack(alignment: .leading) { + + Text("Sats") + .bold() + .padding(.horizontal) + + ZStack { + TextField( + "0", + text: $viewModel.amountSat + ) + .keyboardType(.numberPad) + .submitLabel(.done) + .padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 32)) + + if !viewModel.amountSat.isEmpty { + HStack { + Spacer() + Button { + self.viewModel.amountSat = "" + } label: { + Image(systemName: "xmark.circle.fill") + .foregroundColor(.secondary) + } + .padding(.trailing, 8) + } + } + } + .padding(.horizontal) + + } + .padding() + + Button { + Task { + let amountSat = (UInt64(viewModel.amountSat) ?? 0) // * 1000 + await viewModel.receivePayment( + amountSat: amountSat, + message: "Monday Wallet", + expirySecs: UInt32(3600) + ) + } + } label: { + Text("Create Amount Invoice") + } + + } else { + + // QRCodeView(qrCodeType: .lightning(viewModel.invoice)) + + VStack { + + HStack(alignment: .center) { + + VStack(alignment: .leading, spacing: 5.0) { + HStack { + Text("Lightning Network") + .font(.caption) + .bold() + } + Text(viewModel.unified) + .font(.caption) + .truncationMode(.middle) + .lineLimit(1) + .foregroundColor(.secondary) + .redacted( + reason: viewModel.unified.isEmpty ? .placeholder : [] + ) + } + + Spacer() + + Button { + UIPasteboard.general.string = viewModel.unified + isCopied = true + showCheckmark = true + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + isCopied = false + showCheckmark = false + } + } label: { + HStack { + withAnimation { + Image( + systemName: showCheckmark + ? "checkmark" : "doc.on.doc" + ) + .font(.title2) + .minimumScaleFactor(0.5) + } + } + .bold() + .foregroundColor(viewModel.networkColor) + } + + } + .padding() + + Button("Clear Invoice") { + viewModel.clearInvoice() + } + .buttonBorderShape(.capsule) + .buttonStyle(.bordered) + .tint(viewModel.networkColor) + .padding() + + } + + } + + } + .onAppear { + viewModel.getColor() + print("unified: \n \(viewModel.unified)") + } + .onReceive(viewModel.$receiveViewError) { errorMessage in + if errorMessage != nil { + showingReceiveViewErrorAlert = true + } + } + .onReceive( + NotificationCenter.default.publisher( + for: UIResponder.keyboardWillShowNotification + ) + ) { _ in + isKeyboardVisible = true + } + .onReceive( + NotificationCenter.default.publisher( + for: UIResponder.keyboardWillHideNotification + ) + ) { _ in + isKeyboardVisible = false + } + .alert(isPresented: $showingReceiveViewErrorAlert) { + Alert( + title: Text(viewModel.receiveViewError?.title ?? "Unknown"), + message: Text(viewModel.receiveViewError?.detail ?? ""), + dismissButton: .default(Text("OK")) { + viewModel.receiveViewError = nil + } + ) + } + + } + +} + +#Preview { + BIP21View(viewModel: .init()) +} diff --git a/LDKNodeMonday/View/Home/Receive/ReceiveView.swift b/LDKNodeMonday/View/Home/Receive/ReceiveView.swift index 2d9e3d9..501bf40 100644 --- a/LDKNodeMonday/View/Home/Receive/ReceiveView.swift +++ b/LDKNodeMonday/View/Home/Receive/ReceiveView.swift @@ -31,6 +31,8 @@ struct ReceiveView: View { Bolt12InvoiceView(viewModel: .init()) case .bitcoin: AddressView(viewModel: .init()) + case .bip21: + BIP21View(viewModel: .init()) } } From 375a7a8fee1774ffdc96e0e79f70c9f223e2fba3 Mon Sep 17 00:00:00 2001 From: Matthew Date: Mon, 5 Aug 2024 16:18:07 -0500 Subject: [PATCH 02/10] ui: three items in bip21 copyable --- .../View/Home/Receive/BIP21View.swift | 201 +++++++++++++++--- 1 file changed, 172 insertions(+), 29 deletions(-) diff --git a/LDKNodeMonday/View/Home/Receive/BIP21View.swift b/LDKNodeMonday/View/Home/Receive/BIP21View.swift index e3db035..61324aa 100644 --- a/LDKNodeMonday/View/Home/Receive/BIP21View.swift +++ b/LDKNodeMonday/View/Home/Receive/BIP21View.swift @@ -11,8 +11,13 @@ import SwiftUI struct BIP21View: View { @ObservedObject var viewModel: BIP21ViewModel - @State private var isCopied = false - @State private var showCheckmark = false + @State private var onchainIsCopied = false + @State private var onchainShowCheckmark = false + @State private var bolt11IsCopied = false + @State private var bolt11ShowCheckmark = false + @State private var bolt12IsCopied = false + @State private var bolt12ShowCheckmark = false + @State private var showingReceiveViewErrorAlert = false @State private var isKeyboardVisible = false @@ -77,44 +82,142 @@ struct BIP21View: View { HStack(alignment: .center) { VStack(alignment: .leading, spacing: 5.0) { - HStack { - Text("Lightning Network") + Text("On Chain") + .font(.caption) + .bold() + if let components = parseUnifiedQR(viewModel.unified) { + Text(components.onchain) .font(.caption) - .bold() + .truncationMode(.middle) + .lineLimit(1) + .foregroundColor(.secondary) + .redacted( + reason: viewModel.unified.isEmpty ? .placeholder : [] + ) + } + } + + Spacer() + + if let components = parseUnifiedQR(viewModel.unified) { + Button { + UIPasteboard.general.string = components.onchain + onchainIsCopied = true + onchainShowCheckmark = true + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + onchainIsCopied = false + onchainShowCheckmark = false + } + } label: { + HStack { + withAnimation { + Image( + systemName: onchainShowCheckmark + ? "checkmark" : "doc.on.doc" + ) + .font(.title2) + .minimumScaleFactor(0.5) + } + } + .bold() + .foregroundColor(viewModel.networkColor) } - Text(viewModel.unified) + } + + } + .padding() + + HStack(alignment: .center) { + + VStack(alignment: .leading, spacing: 5.0) { + Text("Bolt 11") .font(.caption) - .truncationMode(.middle) - .lineLimit(1) - .foregroundColor(.secondary) - .redacted( - reason: viewModel.unified.isEmpty ? .placeholder : [] - ) + .bold() + if let components = parseUnifiedQR(viewModel.unified) { + Text(components.bolt11) + .font(.caption) + .truncationMode(.middle) + .lineLimit(1) + .foregroundColor(.secondary) + .redacted( + reason: viewModel.unified.isEmpty ? .placeholder : [] + ) + } } Spacer() - Button { - UIPasteboard.general.string = viewModel.unified - isCopied = true - showCheckmark = true - DispatchQueue.main.asyncAfter(deadline: .now() + 2) { - isCopied = false - showCheckmark = false + if let components = parseUnifiedQR(viewModel.unified) { + Button { + UIPasteboard.general.string = components.bolt11 + bolt11IsCopied = true + bolt11ShowCheckmark = true + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + bolt11IsCopied = false + bolt11ShowCheckmark = false + } + } label: { + HStack { + withAnimation { + Image( + systemName: bolt11ShowCheckmark + ? "checkmark" : "doc.on.doc" + ) + .font(.title2) + .minimumScaleFactor(0.5) + } + } + .bold() + .foregroundColor(viewModel.networkColor) } - } label: { - HStack { - withAnimation { - Image( - systemName: showCheckmark - ? "checkmark" : "doc.on.doc" + } + + } + .padding() + + HStack(alignment: .center) { + + VStack(alignment: .leading, spacing: 5.0) { + Text("Bolt 12") + .font(.caption) + .bold() + if let components = parseUnifiedQR(viewModel.unified) { + Text(components.bolt12) + .font(.caption) + .truncationMode(.middle) + .lineLimit(1) + .foregroundColor(.secondary) + .redacted( + reason: viewModel.unified.isEmpty ? .placeholder : [] ) - .font(.title2) - .minimumScaleFactor(0.5) + } + } + + Spacer() + + if let components = parseUnifiedQR(viewModel.unified) { + Button { + UIPasteboard.general.string = components.bolt12 + bolt12IsCopied = true + bolt12ShowCheckmark = true + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + bolt12IsCopied = false + bolt12ShowCheckmark = false + } + } label: { + HStack { + withAnimation { + Image( + systemName: bolt12ShowCheckmark + ? "checkmark" : "doc.on.doc" + ) + .font(.title2) + .minimumScaleFactor(0.5) + } } + .bold() + .foregroundColor(viewModel.networkColor) } - .bold() - .foregroundColor(viewModel.networkColor) } } @@ -170,6 +273,46 @@ struct BIP21View: View { } +struct UnifiedQRComponents { + let onchain: String + let bolt11: String + let bolt12: String +} + +func parseUnifiedQR(_ unifiedQR: String) -> UnifiedQRComponents? { + // Split the string by '?' + let components = unifiedQR.components(separatedBy: "?") + + guard components.count > 1 else { return nil } + + // Extract onchain (everything before the first '?') and remove the "BITCOIN:" prefix + var onchain = components[0] + if onchain.lowercased().hasPrefix("bitcoin:") { + onchain = String(onchain.dropFirst(8)) // Remove "BITCOIN:" + } + + // Join the rest of the components back together + let remainingString = components.dropFirst().joined(separator: "?") + + // Split the remaining string by '&' + let params = remainingString.components(separatedBy: "&") + + var bolt11: String? + var bolt12: String? + + for param in params { + if param.starts(with: "lightning=") { + bolt11 = String(param.dropFirst("lightning=".count)) + } else if param.starts(with: "lno=") { + bolt12 = String(param.dropFirst("lno=".count)) + } + } + + guard let bolt11 = bolt11, let bolt12 = bolt12 else { return nil } + + return UnifiedQRComponents(onchain: onchain, bolt11: bolt11, bolt12: bolt12) +} + #Preview { BIP21View(viewModel: .init()) } From 4356edfd9694e5e8001a2f60c2b94f7fe506d6cc Mon Sep 17 00:00:00 2001 From: Matthew Date: Mon, 5 Aug 2024 16:24:12 -0500 Subject: [PATCH 03/10] ui: remove bolt11 bolt12 onchain --- LDKNodeMonday/Model/ReceiveOption.swift | 14 +++++----- .../View/Home/Receive/ReceiveView.swift | 26 +++++++++---------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/LDKNodeMonday/Model/ReceiveOption.swift b/LDKNodeMonday/Model/ReceiveOption.swift index 04d0395..5a2b1cc 100644 --- a/LDKNodeMonday/Model/ReceiveOption.swift +++ b/LDKNodeMonday/Model/ReceiveOption.swift @@ -8,12 +8,12 @@ import Foundation enum ReceiveOption: String, CaseIterable, Identifiable { - case bolt11Zero = "Bolt11 0" - case bolt11 = "Bolt11" +// case bolt11Zero = "Bolt11 0" +// case bolt11 = "Bolt11" case bolt11JIT = "Bolt11 JIT" // case bolt12Zero = "Bolt12 0" - case bolt12 = "Bolt12" - case bitcoin = "Address" +// case bolt12 = "Bolt12" +// case bitcoin = "Address" case bip21 = "BIP21" var id: Self { self } @@ -22,10 +22,10 @@ enum ReceiveOption: String, CaseIterable, Identifiable { extension ReceiveOption { var systemImageName: String { switch self { - case .bitcoin: - return "bitcoinsign" - default: + case .bolt11JIT: return "bolt" + default: + return "qrcode" } } } diff --git a/LDKNodeMonday/View/Home/Receive/ReceiveView.swift b/LDKNodeMonday/View/Home/Receive/ReceiveView.swift index 501bf40..6b42982 100644 --- a/LDKNodeMonday/View/Home/Receive/ReceiveView.swift +++ b/LDKNodeMonday/View/Home/Receive/ReceiveView.swift @@ -8,7 +8,7 @@ import SwiftUI struct ReceiveView: View { - @State private var selectedOption: ReceiveOption = .bolt11Zero + @State private var selectedOption: ReceiveOption = .bip21 var body: some View { @@ -19,18 +19,18 @@ struct ReceiveView: View { Spacer() switch selectedOption { - case .bolt11Zero: - ZeroInvoiceView(viewModel: .init()) - case .bolt11: - AmountInvoiceView(viewModel: .init()) +// case .bolt11Zero: +// ZeroInvoiceView(viewModel: .init()) +// case .bolt11: +// AmountInvoiceView(viewModel: .init()) case .bolt11JIT: JITInvoiceView(viewModel: .init()) // case .bolt12Zero: // Bolt12ZeroInvoiceView(viewModel: .init()) - case .bolt12: - Bolt12InvoiceView(viewModel: .init()) - case .bitcoin: - AddressView(viewModel: .init()) +// case .bolt12: +// Bolt12InvoiceView(viewModel: .init()) +// case .bitcoin: +// AddressView(viewModel: .init()) case .bip21: BIP21View(viewModel: .init()) } @@ -54,12 +54,12 @@ struct CustomSegmentedPicker: View { }) { VStack { Image(systemName: option.systemImageName) - .font(.system(size: 6)) + //.font(.system(size: 6)) Text(option.rawValue) - .font(.system(size: 6)) + //.font(.system(size: 6)) } .padding() - .font(.caption2) + .font(.body) .foregroundColor( self.selectedOption == option ? Color.primary : Color.secondary ) @@ -76,5 +76,5 @@ struct CustomSegmentedPicker: View { } #Preview { - CustomSegmentedPicker(options: ReceiveOption.allCases, selectedOption: .constant(.bolt11Zero)) + CustomSegmentedPicker(options: ReceiveOption.allCases, selectedOption: .constant(.bip21)) } From 46fcf58aa7b08719ec1374dec9c0efc39a533161 Mon Sep 17 00:00:00 2001 From: Matthew Date: Fri, 23 Aug 2024 20:01:01 -0500 Subject: [PATCH 04/10] bip21: send --- .../Extensions/String+Extensions.swift | 13 +++++++----- LDKNodeMonday/Model/ReceiveOption.swift | 8 ++++---- .../View/Home/Receive/ReceiveView.swift | 20 +++++++++---------- 3 files changed, 22 insertions(+), 19 deletions(-) diff --git a/LDKNodeMonday/Extensions/String+Extensions.swift b/LDKNodeMonday/Extensions/String+Extensions.swift index 2ebc5dd..fd555eb 100644 --- a/LDKNodeMonday/Extensions/String+Extensions.swift +++ b/LDKNodeMonday/Extensions/String+Extensions.swift @@ -165,10 +165,8 @@ extension String { if let lightningAddress = queryParams["lightning"], !lightningAddress.isEmpty { return processLightningAddress(lightningAddress) - } else if self.isLightningAddress && !self.starts(with: "lnurl") { - return processLightningAddress(self) } else if self.isBitcoinAddress { - return processBitcoinAddress(spendableBalance) + return processBitcoinAddress(spendableBalance) // Modified to handle BIP21 } else if self.starts(with: "lnurl") { return ("LNURL not supported yet", "0", .isLightningURL) } else { @@ -177,10 +175,11 @@ extension String { } private func processBitcoinAddress(_ spendableBalance: UInt64) -> (String, String, Payment) { - let address = self.extractBitcoinAddress() + let address = self.extractBitcoinAddress() // Modified for BIP21 extraction let queryParams = self.queryParameters() - let amount = queryParams["amount"] ?? "0" + let amount = queryParams["amount"] ?? "0" // Modified: Handling BIP21 amount only + // Validate the amount against the spendable balance if let amountValue = UInt64(amount), amountValue <= spendableBalance { return (address, amount, .isBitcoin) } else { @@ -201,7 +200,11 @@ extension String { private func extractBitcoinAddress() -> String { if self.lowercased().hasPrefix("bitcoin:") { + // Extract the address from the "bitcoin:" URI, ignoring any query parameters let address = self.replacingOccurrences(of: "bitcoin:", with: "") + if let addressEnd = address.range(of: "?")?.lowerBound { // New: Handles query parameters in BIP21 + return String(address[.. Date: Fri, 23 Aug 2024 20:09:01 -0500 Subject: [PATCH 05/10] bitcoinui main brings support for bip21 in qrcodeview --- LDKNodeMonday.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 6 +++--- LDKNodeMonday/View/Home/Receive/BIP21View.swift | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/LDKNodeMonday.xcodeproj/project.pbxproj b/LDKNodeMonday.xcodeproj/project.pbxproj index 6ed98bf..64e71c3 100644 --- a/LDKNodeMonday.xcodeproj/project.pbxproj +++ b/LDKNodeMonday.xcodeproj/project.pbxproj @@ -934,7 +934,7 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/reez/BitcoinUI.git"; requirement = { - branch = 1.0.6; + branch = main; kind = branch; }; }; diff --git a/LDKNodeMonday.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/LDKNodeMonday.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 89d3b4b..2522a59 100644 --- a/LDKNodeMonday.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/LDKNodeMonday.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,13 +1,13 @@ { - "originHash" : "2d457a0d145c5ec561d10ae3f13d805b262d5bfda9ebf0868311848416a25b6e", + "originHash" : "205ed5286a50854d994301c2ba51d3a38173557dcfa21e1c3da1ccccb0730a15", "pins" : [ { "identity" : "bitcoinui", "kind" : "remoteSourceControl", "location" : "https://github.com/reez/BitcoinUI.git", "state" : { - "branch" : "1.0.6", - "revision" : "2f4f1a49a869ebd19c08718ade319ffdf59e6da1" + "branch" : "main", + "revision" : "22ea27e2495aac9035c39a1b09165a802d90bdde" } }, { diff --git a/LDKNodeMonday/View/Home/Receive/BIP21View.swift b/LDKNodeMonday/View/Home/Receive/BIP21View.swift index 61324aa..f4892d7 100644 --- a/LDKNodeMonday/View/Home/Receive/BIP21View.swift +++ b/LDKNodeMonday/View/Home/Receive/BIP21View.swift @@ -75,7 +75,7 @@ struct BIP21View: View { } else { - // QRCodeView(qrCodeType: .lightning(viewModel.invoice)) + QRCodeView(qrCodeType: .bip21(viewModel.unified)) VStack { From 0c6b97f689a3240ce02bc1dd6c6f5e0eaf6e2ae8 Mon Sep 17 00:00:00 2001 From: Matthew Date: Sat, 24 Aug 2024 18:47:07 -0500 Subject: [PATCH 06/10] fix: bolt11amount support for signet --- .../Extensions/String+Extensions.swift | 86 ++++++++++++------- 1 file changed, 54 insertions(+), 32 deletions(-) diff --git a/LDKNodeMonday/Extensions/String+Extensions.swift b/LDKNodeMonday/Extensions/String+Extensions.swift index fd555eb..8ba270a 100644 --- a/LDKNodeMonday/Extensions/String+Extensions.swift +++ b/LDKNodeMonday/Extensions/String+Extensions.swift @@ -10,46 +10,68 @@ import Foundation extension String { func bolt11amount() -> String? { - let regex = try! NSRegularExpression(pattern: "ln.*?(\\d+)([munp]?)", options: []) + print("Entering bolt11amount() with input: \(self)") + + // Updated regex pattern to be more flexible + let regex = try! NSRegularExpression( + pattern: "ln(?:bc|tb|tbs)(?\\d+)(?[munp]?)", + options: [.caseInsensitive] + ) + print("Regex pattern: \(regex.pattern)") + if let match = regex.firstMatch( in: self, options: [], range: NSRange(location: 0, length: self.utf16.count) ) { - let amountRange = match.range(at: 1) - let multiplierRange = match.range(at: 2) - - if let amountSwiftRange = Range(amountRange, in: self), - let multiplierSwiftRange = Range(multiplierRange, in: self) - { - - let amountString = self[amountSwiftRange] - let multiplierString = self[multiplierSwiftRange] - let numberFormatter = NumberFormatter() - - if let amount = numberFormatter.number(from: String(amountString))?.doubleValue { - var conversion = amount - - switch multiplierString { - case "m": - conversion *= 0.001 - case "u": - conversion *= 0.000001 - case "n": - conversion *= 0.000000001 - case "p": - conversion *= 0.000000000001 - default: - break - } - - let convertedAmount = conversion * 100_000_000 - let formattedAmount = String(format: "%.0f", convertedAmount) - return formattedAmount - } + print("Regex match found") + + guard let amountRange = Range(match.range(withName: "amount"), in: self), + let multiplierRange = Range(match.range(withName: "multiplier"), in: self) + else { + print("Failed to extract amount or multiplier ranges") + return nil + } + + let amountString = String(self[amountRange]) + let multiplierString = String(self[multiplierRange]) + + print("Extracted amount: \(amountString), multiplier: \(multiplierString)") + + guard let amount = Int(amountString) else { + print("Failed to convert amount to Int") + return nil + } + + var conversion = Double(amount) + print("Initial conversion: \(conversion)") + + switch multiplierString.lowercased() { + case "m": + conversion *= 0.001 + print("Applied 'm' multiplier") + case "u": + conversion *= 0.000001 + print("Applied 'u' multiplier") + case "n": + conversion *= 0.000000001 + print("Applied 'n' multiplier") + case "p": + conversion *= 0.000000000001 + print("Applied 'p' multiplier") + default: + print("No multiplier applied") } + + let convertedAmount = conversion * 100_000_000 + let formattedAmount = String(format: "%.0f", convertedAmount) + print("Final converted amount: \(formattedAmount) satoshis") + return formattedAmount + } else { + print("No regex match found") } + print("Returning nil from bolt11amount()") return nil } From e622eff96ec5256dfa88a73d475eaa9343c32913 Mon Sep 17 00:00:00 2001 From: Matthew Date: Sat, 24 Aug 2024 19:01:55 -0500 Subject: [PATCH 07/10] ui: bip21 qr size --- .../View/Home/Receive/BIP21View.swift | 30 +++++++++++-------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/LDKNodeMonday/View/Home/Receive/BIP21View.swift b/LDKNodeMonday/View/Home/Receive/BIP21View.swift index f4892d7..690c375 100644 --- a/LDKNodeMonday/View/Home/Receive/BIP21View.swift +++ b/LDKNodeMonday/View/Home/Receive/BIP21View.swift @@ -76,18 +76,17 @@ struct BIP21View: View { } else { QRCodeView(qrCodeType: .bip21(viewModel.unified)) + // .padding(-30.0) - VStack { + VStack(spacing: 5.0) { HStack(alignment: .center) { VStack(alignment: .leading, spacing: 5.0) { Text("On Chain") - .font(.caption) .bold() if let components = parseUnifiedQR(viewModel.unified) { Text(components.onchain) - .font(.caption) .truncationMode(.middle) .lineLimit(1) .foregroundColor(.secondary) @@ -96,6 +95,7 @@ struct BIP21View: View { ) } } + .font(.caption2) Spacer() @@ -115,27 +115,27 @@ struct BIP21View: View { systemName: onchainShowCheckmark ? "checkmark" : "doc.on.doc" ) - .font(.title2) + .font(.title3) .minimumScaleFactor(0.5) } } .bold() .foregroundColor(viewModel.networkColor) } + .font(.caption2) + } } - .padding() + .padding(.horizontal) HStack(alignment: .center) { VStack(alignment: .leading, spacing: 5.0) { Text("Bolt 11") - .font(.caption) .bold() if let components = parseUnifiedQR(viewModel.unified) { Text(components.bolt11) - .font(.caption) .truncationMode(.middle) .lineLimit(1) .foregroundColor(.secondary) @@ -144,6 +144,7 @@ struct BIP21View: View { ) } } + .font(.caption2) Spacer() @@ -163,27 +164,27 @@ struct BIP21View: View { systemName: bolt11ShowCheckmark ? "checkmark" : "doc.on.doc" ) - .font(.title2) + .font(.title3) .minimumScaleFactor(0.5) } } .bold() .foregroundColor(viewModel.networkColor) } + .font(.caption2) + } } - .padding() + .padding(.horizontal) HStack(alignment: .center) { VStack(alignment: .leading, spacing: 5.0) { Text("Bolt 12") - .font(.caption) .bold() if let components = parseUnifiedQR(viewModel.unified) { Text(components.bolt12) - .font(.caption) .truncationMode(.middle) .lineLimit(1) .foregroundColor(.secondary) @@ -192,6 +193,7 @@ struct BIP21View: View { ) } } + .font(.caption2) Spacer() @@ -211,17 +213,19 @@ struct BIP21View: View { systemName: bolt12ShowCheckmark ? "checkmark" : "doc.on.doc" ) - .font(.title2) + .font(.title3) .minimumScaleFactor(0.5) } } .bold() .foregroundColor(viewModel.networkColor) } + .font(.caption2) + } } - .padding() + .padding(.horizontal) Button("Clear Invoice") { viewModel.clearInvoice() From 6be02aa871ea3e8306b03b1d1fd07b5cd21c376a Mon Sep 17 00:00:00 2001 From: Matthew Date: Sat, 24 Aug 2024 19:53:52 -0500 Subject: [PATCH 08/10] extension: process bip21 --- .../Extensions/String+Extensions.swift | 75 +++++++++++++++++-- .../View Model/Home/AmountViewModel.swift | 7 +- 2 files changed, 72 insertions(+), 10 deletions(-) diff --git a/LDKNodeMonday/Extensions/String+Extensions.swift b/LDKNodeMonday/Extensions/String+Extensions.swift index 8ba270a..101b28c 100644 --- a/LDKNodeMonday/Extensions/String+Extensions.swift +++ b/LDKNodeMonday/Extensions/String+Extensions.swift @@ -180,15 +180,74 @@ extension String { return params } + func processBIP21(_ input: String, spendableBalance: UInt64) -> (String, String, Payment) { + print("Processing BIP21 URI") + guard let url = URL(string: input), + let components = URLComponents(url: url, resolvingAgainstBaseURL: false) + else { + print("Failed to parse BIP21 URI") + return ("", "0", .isNone) + } + + let bitcoinAddress = url.path + var amount = "0" + var bolt12Offer: String? + var bolt11Invoice: String? + + for item in components.queryItems ?? [] { + switch item.name.lowercased() { + case "amount": + if let value = item.value, let btcAmount = Double(value) { + amount = String(format: "%.0f", btcAmount * 100_000_000) // Convert BTC to satoshis + } + case "lightning": + bolt11Invoice = item.value + case "lno": + bolt12Offer = item.value + default: + break + } + } + + // Priority: Bolt 12 > Bolt 11 > On-chain + if let offer = bolt12Offer { + print("Bolt 12 offer found in BIP21 URI") + return processLightningAddress(offer) + } + + if let invoice = bolt11Invoice { + print("Bolt 11 invoice found in BIP21 URI") + return processLightningAddress(invoice) + } + + print("Using on-chain Bitcoin address from BIP21 URI") + return (bitcoinAddress, amount, .isBitcoin) + } + func extractPaymentInfo(spendableBalance: UInt64) -> ( address: String, amount: String, payment: Payment ) { - let queryParams = self.queryParameters() + // BIP 21 + if self.lowercased().starts(with: "bitcoin:") && self.contains("?") { + return processBIP21(self, spendableBalance: spendableBalance) + } + + // Bolt 11 JIT + // Check for BOLT11 invoice, including those prefixed with "lightning:" + if self.lowercased().starts(with: "lightning:") { + let invoice = String(self.dropFirst(10)) // Remove "lightning:" prefix + return processLightningAddress(invoice) + } else if self.lowercased().starts(with: "lnbc") || self.lowercased().starts(with: "lntb") { + return processLightningAddress(self) + } - if let lightningAddress = queryParams["lightning"], !lightningAddress.isEmpty { - return processLightningAddress(lightningAddress) - } else if self.isBitcoinAddress { - return processBitcoinAddress(spendableBalance) // Modified to handle BIP21 + // let queryParams = self.queryParameters() + // + // if let lightningAddress = queryParams["lightning"], !lightningAddress.isEmpty { + // return processLightningAddress(lightningAddress) + // } + else if self.isBitcoinAddress { + return processBitcoinAddress(spendableBalance) } else if self.starts(with: "lnurl") { return ("LNURL not supported yet", "0", .isLightningURL) } else { @@ -197,9 +256,9 @@ extension String { } private func processBitcoinAddress(_ spendableBalance: UInt64) -> (String, String, Payment) { - let address = self.extractBitcoinAddress() // Modified for BIP21 extraction + let address = self.extractBitcoinAddress() let queryParams = self.queryParameters() - let amount = queryParams["amount"] ?? "0" // Modified: Handling BIP21 amount only + let amount = queryParams["amount"] ?? "0" // Validate the amount against the spendable balance if let amountValue = UInt64(amount), amountValue <= spendableBalance { @@ -212,7 +271,7 @@ extension String { private func processLightningAddress(_ address: String) -> (String, String, Payment) { let sanitizedAddress = address.replacingOccurrences(of: "lightning:", with: "") - if sanitizedAddress.starts(with: "lno") { + if sanitizedAddress.lowercased().starts(with: "lno") { return (sanitizedAddress, "0", .isLightning) } else { let amount = sanitizedAddress.bolt11amount() ?? "0" diff --git a/LDKNodeMonday/View Model/Home/AmountViewModel.swift b/LDKNodeMonday/View Model/Home/AmountViewModel.swift index 1bfcc9a..661dee5 100644 --- a/LDKNodeMonday/View Model/Home/AmountViewModel.swift +++ b/LDKNodeMonday/View Model/Home/AmountViewModel.swift @@ -90,8 +90,10 @@ class AmountViewModel { func sendPaymentBolt12(invoice: Bolt12Invoice) async { do { - try await LightningNodeService.shared.send(bolt12Invoice: invoice) + let paymentId = try await LightningNodeService.shared.send(bolt12Invoice: invoice) + print("sendPaymentBolt12 success w paymentId: \(paymentId)") } catch let error as NodeError { + print("sendPaymentBolt12 NodeError: \(error.localizedDescription)") NotificationCenter.default.post(name: .ldkErrorReceived, object: error) let errorString = handleNodeError(error) DispatchQueue.main.async { @@ -102,6 +104,7 @@ class AmountViewModel { } } catch { DispatchQueue.main.async { + print("sendPaymentBolt12 error: \(error.localizedDescription)") self.amountConfirmationViewError = .init( title: "Unexpected error", detail: error.localizedDescription @@ -118,7 +121,7 @@ class AmountViewModel { } func handleLightningPayment(address: String, numpadAmount: String) async { - if address.starts(with: "lno") { + if address.lowercased().starts(with: "lno") { await sendPaymentBolt12(invoice: address) } else if address.bolt11amount() == "0" { if let amountSats = UInt64(numpadAmount) { From aa9973ac32765609a52b4d1dea42cf40ce7f5c1c Mon Sep 17 00:00:00 2001 From: Matthew Date: Sat, 24 Aug 2024 20:03:09 -0500 Subject: [PATCH 09/10] format --- .../Extensions/String+Extensions.swift | 51 ++----------------- LDKNodeMonday/Model/ReceiveOption.swift | 9 +--- .../View Model/Home/AmountViewModel.swift | 5 +- .../View/Home/Receive/BIP21View.swift | 4 +- .../View/Home/Receive/ReceiveView.swift | 12 ----- 5 files changed, 9 insertions(+), 72 deletions(-) diff --git a/LDKNodeMonday/Extensions/String+Extensions.swift b/LDKNodeMonday/Extensions/String+Extensions.swift index 101b28c..d8161b9 100644 --- a/LDKNodeMonday/Extensions/String+Extensions.swift +++ b/LDKNodeMonday/Extensions/String+Extensions.swift @@ -10,68 +10,49 @@ import Foundation extension String { func bolt11amount() -> String? { - print("Entering bolt11amount() with input: \(self)") - - // Updated regex pattern to be more flexible let regex = try! NSRegularExpression( pattern: "ln(?:bc|tb|tbs)(?\\d+)(?[munp]?)", options: [.caseInsensitive] ) - print("Regex pattern: \(regex.pattern)") if let match = regex.firstMatch( in: self, options: [], range: NSRange(location: 0, length: self.utf16.count) ) { - print("Regex match found") guard let amountRange = Range(match.range(withName: "amount"), in: self), let multiplierRange = Range(match.range(withName: "multiplier"), in: self) else { - print("Failed to extract amount or multiplier ranges") return nil } let amountString = String(self[amountRange]) let multiplierString = String(self[multiplierRange]) - print("Extracted amount: \(amountString), multiplier: \(multiplierString)") - guard let amount = Int(amountString) else { - print("Failed to convert amount to Int") return nil } var conversion = Double(amount) - print("Initial conversion: \(conversion)") - switch multiplierString.lowercased() { case "m": conversion *= 0.001 - print("Applied 'm' multiplier") case "u": conversion *= 0.000001 - print("Applied 'u' multiplier") case "n": conversion *= 0.000000001 - print("Applied 'n' multiplier") case "p": conversion *= 0.000000000001 - print("Applied 'p' multiplier") default: - print("No multiplier applied") + break } let convertedAmount = conversion * 100_000_000 let formattedAmount = String(format: "%.0f", convertedAmount) - print("Final converted amount: \(formattedAmount) satoshis") return formattedAmount - } else { - print("No regex match found") } - print("Returning nil from bolt11amount()") return nil } @@ -181,11 +162,9 @@ extension String { } func processBIP21(_ input: String, spendableBalance: UInt64) -> (String, String, Payment) { - print("Processing BIP21 URI") guard let url = URL(string: input), let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { - print("Failed to parse BIP21 URI") return ("", "0", .isNone) } @@ -198,7 +177,7 @@ extension String { switch item.name.lowercased() { case "amount": if let value = item.value, let btcAmount = Double(value) { - amount = String(format: "%.0f", btcAmount * 100_000_000) // Convert BTC to satoshis + amount = String(format: "%.0f", btcAmount * 100_000_000) } case "lightning": bolt11Invoice = item.value @@ -209,44 +188,26 @@ extension String { } } - // Priority: Bolt 12 > Bolt 11 > On-chain if let offer = bolt12Offer { - print("Bolt 12 offer found in BIP21 URI") return processLightningAddress(offer) } - if let invoice = bolt11Invoice { - print("Bolt 11 invoice found in BIP21 URI") return processLightningAddress(invoice) } - - print("Using on-chain Bitcoin address from BIP21 URI") return (bitcoinAddress, amount, .isBitcoin) } func extractPaymentInfo(spendableBalance: UInt64) -> ( address: String, amount: String, payment: Payment ) { - // BIP 21 if self.lowercased().starts(with: "bitcoin:") && self.contains("?") { return processBIP21(self, spendableBalance: spendableBalance) - } - - // Bolt 11 JIT - // Check for BOLT11 invoice, including those prefixed with "lightning:" - if self.lowercased().starts(with: "lightning:") { + } else if self.lowercased().starts(with: "lightning:") { let invoice = String(self.dropFirst(10)) // Remove "lightning:" prefix return processLightningAddress(invoice) } else if self.lowercased().starts(with: "lnbc") || self.lowercased().starts(with: "lntb") { return processLightningAddress(self) - } - - // let queryParams = self.queryParameters() - // - // if let lightningAddress = queryParams["lightning"], !lightningAddress.isEmpty { - // return processLightningAddress(lightningAddress) - // } - else if self.isBitcoinAddress { + } else if self.isBitcoinAddress { return processBitcoinAddress(spendableBalance) } else if self.starts(with: "lnurl") { return ("LNURL not supported yet", "0", .isLightningURL) @@ -260,7 +221,6 @@ extension String { let queryParams = self.queryParameters() let amount = queryParams["amount"] ?? "0" - // Validate the amount against the spendable balance if let amountValue = UInt64(amount), amountValue <= spendableBalance { return (address, amount, .isBitcoin) } else { @@ -281,9 +241,8 @@ extension String { private func extractBitcoinAddress() -> String { if self.lowercased().hasPrefix("bitcoin:") { - // Extract the address from the "bitcoin:" URI, ignoring any query parameters let address = self.replacingOccurrences(of: "bitcoin:", with: "") - if let addressEnd = address.range(of: "?")?.lowerBound { // New: Handles query parameters in BIP21 + if let addressEnd = address.range(of: "?")?.lowerBound { return String(address[.. Date: Sat, 24 Aug 2024 20:09:56 -0500 Subject: [PATCH 10/10] readme: add local build link --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 8fbc46a..9df1f60 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ A lightning node on your iPhone with the ability to send & receive on-chain & li ## Swift Packages - LDK Node via [ldk-node](https://github.com/lightningdevkit/ldk-node) + *Note: Sometimes, a [local build of LDK Node](https://github.com/lightningdevkit/ldk-node/blob/main/scripts/uniffi_bindgen_generate_swift.sh) is used instead of the remote Swift package.* - Bitcoin UI Kit via [BitcoinUI](https://github.com/reez/BitcoinUI)