-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathLnurl.swift
More file actions
201 lines (168 loc) · 7.62 KB
/
Lnurl.swift
File metadata and controls
201 lines (168 loc) · 7.62 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
import BitkitCore
import Foundation
// MARK: - Response Models
private struct LnurlPayResponse: Codable {
let pr: String
let routes: [String]
}
private struct LnurlWithdrawResponse: Codable {
let status: String
let reason: String?
}
private struct LnurlChannelResponse: Codable {
let status: String
let reason: String?
}
// MARK: - HTTP Helper
private extension LnurlHelper {
/// Makes an HTTP GET request and returns the response data
/// - Parameter url: The URL to request
/// - Returns: The response data as a string
/// - Throws: Network or parsing errors
static func makeHttpGetRequest(url: URL) async throws -> String {
Logger.debug("Making GET request to: \(url.absoluteString)")
let (data, response) = try await URLSession.shared.data(from: url)
guard let httpResponse = response as? HTTPURLResponse else {
throw NSError(domain: "LNURL", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid HTTP response"])
}
Logger.debug("HTTP response status: \(httpResponse.statusCode)")
guard let responseString = String(data: data, encoding: .utf8) else {
throw NSError(domain: "LNURL", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid response data"])
}
guard httpResponse.statusCode == 200 else {
Logger.error("HTTP request failed with status \(httpResponse.statusCode), response: \(responseString)")
throw NSError(
domain: "LNURL", code: httpResponse.statusCode,
userInfo: [NSLocalizedDescriptionKey: "HTTP request failed with status \(httpResponse.statusCode)"]
)
}
Logger.debug("HTTP response: \(responseString)")
return responseString
}
/// Parses JSON response and handles common error patterns
/// - Parameters:
/// - responseString: The JSON response string
/// - responseType: The type to decode to
/// - Returns: The decoded response
/// - Throws: Parsing or business logic errors
static func parseJsonResponse<T: Codable>(_ responseString: String, as responseType: T.Type) throws -> T {
guard let jsonData = responseString.data(using: .utf8) else {
throw NSError(domain: "LNURL", code: -1, userInfo: [NSLocalizedDescriptionKey: "Failed to convert response to data"])
}
return try JSONDecoder().decode(responseType, from: jsonData)
}
/// Builds a URL with query parameters
/// - Parameters:
/// - baseUrl: The base URL string
/// - queryItems: The query parameters to add/override
/// - Returns: The constructed URL
/// - Throws: URL construction errors
static func buildUrl(baseUrl: String, queryItems: [URLQueryItem]) throws -> URL {
guard var urlComponents = URLComponents(string: baseUrl) else {
throw NSError(domain: "LNURL", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid callback URL"])
}
// Get existing query items from the base URL
var existingItems = urlComponents.queryItems ?? []
// Remove any existing items that will be overridden by the new ones
let newItemKeys = Set(queryItems.map(\.name))
existingItems.removeAll { newItemKeys.contains($0.name) }
// Append new query items
existingItems.append(contentsOf: queryItems)
urlComponents.queryItems = existingItems.isEmpty ? nil : existingItems
guard let url = urlComponents.url else {
throw NSError(domain: "LNURL", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid callback URL"])
}
return url
}
/// Handles LNURL error responses
/// - Parameter response: The response containing status and optional reason
/// - Throws: Error if status is "ERROR"
static func handleLnurlError(_ response: some Codable) throws {
// Check if the response has a status field that indicates an error
if let errorResponse = response as? LnurlWithdrawResponse,
errorResponse.status == "ERROR"
{
let errorMessage = errorResponse.reason ?? "Unknown error"
Logger.error("LNURL request failed: \(errorMessage)")
throw NSError(domain: "LNURL", code: -1, userInfo: [NSLocalizedDescriptionKey: errorMessage])
}
if let errorResponse = response as? LnurlChannelResponse,
errorResponse.status == "ERROR"
{
let errorMessage = errorResponse.reason ?? "Unknown error"
Logger.error("LNURL request failed: \(errorMessage)")
throw NSError(domain: "LNURL", code: -1, userInfo: [NSLocalizedDescriptionKey: errorMessage])
}
}
}
@MainActor
struct LnurlHelper {
/// Fetches a Lightning invoice from an LNURL pay callback
/// - Parameters:
/// - callbackUrl: The LNURL callback URL
/// - amountMsats: The amount in millisatoshis to pay
/// - comment: Optional comment to include with the payment
/// - Returns: The bolt11 invoice string
/// - Throws: Network or parsing errors
static func fetchLnurlInvoice(
callbackUrl: String,
amountMsats: UInt64,
comment: String? = nil
) async throws -> String {
var queryItems = [
URLQueryItem(name: "amount", value: String(amountMsats)),
]
// Add comment if provided
if let comment, !comment.isEmpty {
queryItems.append(URLQueryItem(name: "comment", value: comment))
}
let callbackURL = try buildUrl(baseUrl: callbackUrl, queryItems: queryItems)
let responseString = try await makeHttpGetRequest(url: callbackURL)
let lnurlResponse = try parseJsonResponse(responseString, as: LnurlPayResponse.self)
Logger.debug("Extracted bolt11 invoice: \(lnurlResponse.pr)")
return lnurlResponse.pr
}
/// Handles LNURL Withdraw Requests
/// - Parameters:
/// - params: The LNURL withdraw parameters
/// - invoice: The Lightning invoice to withdraw to
/// - Throws: Network or parsing errors
static func handleLnurlWithdraw(
params: LnurlWithdrawData,
invoice: String
) async throws {
let queryItems = [
URLQueryItem(name: "k1", value: params.k1),
URLQueryItem(name: "pr", value: invoice),
]
let callbackURL = try buildUrl(baseUrl: params.callback, queryItems: queryItems)
let responseString = try await makeHttpGetRequest(url: callbackURL)
let withdrawResponse = try parseJsonResponse(responseString, as: LnurlWithdrawResponse.self)
try handleLnurlError(withdrawResponse)
Logger.debug("LNURL withdraw successful")
}
/// Handles LNURL Channel Requests
/// - Parameters:
/// - params: The LNURL channel parameters
/// - nodeId: The node ID to send with the request
/// - Throws: Network or parsing errors
static func handleLnurlChannel(
params: LnurlChannelData,
nodeId: String
) async throws {
let callbackUrlString = try createChannelRequestUrl(
k1: params.k1,
callback: params.callback,
localNodeId: nodeId,
isPrivate: true,
cancel: false
)
guard let callbackURL = URL(string: callbackUrlString) else {
throw NSError(domain: "LNURL", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid callback URL"])
}
let responseString = try await makeHttpGetRequest(url: callbackURL)
let channelResponse = try parseJsonResponse(responseString, as: LnurlChannelResponse.self)
try handleLnurlError(channelResponse)
Logger.debug("LNURL channel request successful")
}
}