generated from TBD54566975/tbd-project-template
-
Notifications
You must be signed in to change notification settings - Fork 3
/
tbDEXHttpClient.swift
346 lines (289 loc) · 13.1 KB
/
tbDEXHttpClient.swift
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
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
import AnyCodable
import Foundation
import Web5
public enum tbDEXHttpClient {
static let session = URLSession(configuration: .default)
/// Fetch `Offering`s from a PFI
/// - Parameters:
/// - pfiDIDURI: The DID URI of the PFI
/// - filter: A `GetOfferingFilter` to filter the results
/// - Returns: An array of `Offering`, matching the request
public static func getOfferings(
pfiDIDURI: String,
filter: GetOfferingFilter? = nil
) async throws -> [Offering] {
guard let pfiServiceEndpoint = await getPFIServiceEndpoint(pfiDIDURI: pfiDIDURI) else {
throw Error(reason: "DID does not have service of type PFI")
}
guard var components = URLComponents(string: "\(pfiServiceEndpoint)/offerings") else {
throw Error(reason: "Could not create URLComponents from PFI service endpoint")
}
components.queryItems = filter?.queryItems()
guard let url = components.url else {
throw Error(reason: "Could not create URL from URLComponents")
}
do {
let response = try await URLSession.shared.data(from: url)
let offeringsResponse = try tbDEXJSONDecoder().decode(GetOfferingsResponse.self, from: response.0)
// Return all valid Offerings provided by the PFI, throwing away any that are invalid
return await validOfferings(in: offeringsResponse.data)
} catch {
throw Error(reason: "Error while fetching offerings: \(error)")
}
}
/// Fetch `Balances` from a PFI
/// - Parameters:
/// - pfiDIDURI: The DID URI of the PFI
/// - filter: A `GetOfferingFilter` to filter the results
/// - Returns: An array of `Balances` matching the request
public static func getBalances(
pfiDIDURI: String,
requesterDID: BearerDID
) async throws -> [Balance] {
guard let pfiServiceEndpoint = await getPFIServiceEndpoint(pfiDIDURI: pfiDIDURI) else {
throw Error(reason: "DID does not have service of type PFI")
}
guard let url = URL(string: "\(pfiServiceEndpoint)/balances") else {
throw Error(reason: "Could not create URL from PFI service endpoint")
}
let requestToken = try await RequestToken.generate(did: requesterDID, pfiDIDURI: pfiDIDURI)
var request = URLRequest(url: url)
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("Bearer \(requestToken)", forHTTPHeaderField: "Authorization")
let (data, response) = try await URLSession.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse else {
throw Error(reason: "Invalid response")
}
switch httpResponse.statusCode {
case 200...299:
do {
let balancesResponse = try tbDEXJSONDecoder().decode(GetBalancesResponse.self, from: data)
return balancesResponse.data
} catch {
throw Error(reason: "Error while getting balances: \(error)")
}
default:
throw buildErrorResponse(data: data, response: httpResponse)
}
}
/// Sends an RFQ and options to the PFI to initiate an exchange
/// - Parameters:
/// - rfq: The RFQ message that will be sent to the PFI
/// - Throws: if message verification fails
/// - Throws: if recipient DID resolution fails
/// - Throws: if recipient DID does not have a PFI service entry
public static func createExchange(rfq: RFQ) async throws {
try await sendMessage(message: rfq, messageEndpoint: "/exchanges")
}
/// Sends the Order message to the PFI
/// - Parameters:
/// - order: The Order message that will be sent to the PFI
/// - Throws: if message verification fails
/// - Throws: if recipient DID resolution fails
/// - Throws: if recipient DID does not have a PFI service entry
public static func submitOrder(order: Order) async throws {
let exchangeID = order.metadata.exchangeID
try await sendMessage(message: order, messageEndpoint: "/exchanges/\(exchangeID)")
}
/// Sends the Cancel message to the PFI
/// - Parameters:
/// - cancel: The Cancel message that will be sent to the PFI
/// - Throws: if message verification fails
/// - Throws: if recipient DID resolution fails
/// - Throws: if recipient DID does not have a PFI service entry
public static func submitCancel(cancel: Cancel) async throws {
let exchangeID = cancel.metadata.exchangeID
try await sendMessage(message: cancel, messageEndpoint: "/exchanges/\(exchangeID)")
}
/// Sends a message to a PFI
/// - Parameters:
/// - message: The message to send
/// - messageEndpoint: The endpoint for the message with a leading slash. eg. "/exchanges"
private static func sendMessage<D: MessageData>(
message: Message<D>,
messageEndpoint: String
) async throws {
guard try await message.verify() else {
throw Error(reason: "Message signature is invalid")
}
let pfiDidUri = message.metadata.to
guard let pfiServiceEndpoint = await getPFIServiceEndpoint(pfiDIDURI: pfiDidUri) else {
throw Error(reason: "DID does not have service of type PFI")
}
guard let url = URL(string: "\(pfiServiceEndpoint)\(messageEndpoint)") else {
throw Error(reason: "Could not create URL from PFI service endpoint")
}
var request = URLRequest(url: url)
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
if case .rfq = message.metadata.kind {
// Sending an RFQ means creating an exchange, so we POST
request.httpMethod = "POST"
// RFQs are special, and wrap their message in an `rfq` object.
request.httpBody = try tbDEXJSONEncoder().encode(["rfq": message])
} else {
// Adding messages to an exchange requires a PUT
request.httpMethod = "PUT"
// All other messages encode their messages directly to the http body
request.httpBody = try tbDEXJSONEncoder().encode(message)
}
let (data , response) = try await URLSession.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse else {
throw Error(reason: "Invalid response")
}
switch httpResponse.statusCode {
case 200...299:
return
default:
throw buildErrorResponse(data: data, response: httpResponse)
}
}
/// Fetches the exchanges from the PFI based
/// - Parameters:
/// - pfiDIDURI: The PFI's DID URI
/// - requesterDID: The DID of the requester
/// - Returns: 2D array of `AnyMessage` objects, each representing an Exchange between the requester and the PFI
public static func getExchanges(
pfiDIDURI: String,
requesterDID: BearerDID
) async throws -> [[AnyMessage]] {
guard let pfiServiceEndpoint = await getPFIServiceEndpoint(pfiDIDURI: pfiDIDURI) else {
throw Error(reason: "DID does not have service of type PFI")
}
guard let url = URL(string: "\(pfiServiceEndpoint)/exchanges") else {
throw Error(reason: "Could not create URL from PFI service endpoint")
}
let requestToken = try await RequestToken.generate(did: requesterDID, pfiDIDURI: pfiDIDURI)
var request = URLRequest(url: url)
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("Bearer \(requestToken)", forHTTPHeaderField: "Authorization")
let (data, response) = try await URLSession.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse else {
throw Error(reason: "Invalid response")
}
switch httpResponse.statusCode {
case 200...299:
do {
let exchangesResponse = try tbDEXJSONDecoder().decode(GetExchangesResponse.self, from: data)
return exchangesResponse.data
} catch {
throw Error(reason: "Error while decoding exchanges: \(error)")
}
default:
throw buildErrorResponse(data: data, response: httpResponse)
}
}
/// Fetches a specific exchange between the requester and the PFI
/// - Parameters:
/// - pfiDIDURI: The DID URI of the PFI
/// - requesterDID: The DID of the requester
/// - exchangeId: The ID of the exchange to fetch
/// - Returns: Array of `AnyMessage` objects, representing an Exchange between the requester and the PFI
public static func getExchange(
pfiDIDURI: String,
requesterDID: BearerDID,
exchangeId: String
) async throws -> [AnyMessage] {
guard let pfiServiceEndpoint = await getPFIServiceEndpoint(pfiDIDURI: pfiDIDURI) else {
throw Error(reason: "DID does not have service of type PFI")
}
guard let url = URL(string: "\(pfiServiceEndpoint)/exchanges/\(exchangeId)") else {
throw Error(reason: "Could not create URL from PFI service endpoint")
}
let requestToken = try await RequestToken.generate(did: requesterDID, pfiDIDURI: pfiDIDURI)
var request = URLRequest(url: url)
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("Bearer \(requestToken)", forHTTPHeaderField: "Authorization")
let (data, response) = try await URLSession.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse else {
throw Error(reason: "Invalid response")
}
switch httpResponse.statusCode {
case 200...299:
do {
let exchangesResponse = try tbDEXJSONDecoder().decode(GetExchangeResponse.self, from: data)
return exchangesResponse.data
} catch {
throw Error(reason: "Error while decoding exchange: \(error)")
}
default:
throw buildErrorResponse(data: data, response: httpResponse)
}
}
// MARK: - Decodable Response Types
struct GetOfferingsResponse: Decodable {
public let data: [Offering]
}
struct GetBalancesResponse: Decodable {
public let data: [Balance]
}
struct GetExchangesResponse: Decodable {
public let data: [[AnyMessage]]
}
struct GetExchangeResponse: Decodable {
public let data: [AnyMessage]
}
// MARK: - Private
/// Get the PFI service endpoint
/// - Parameter pfiDIDURI: The PFI's DID URI
/// - Returns: The PFI's service endpoint (if it exists)
private static func getPFIServiceEndpoint(pfiDIDURI: String) async -> String? {
let resolutionResult = await DIDResolver.resolve(didURI: pfiDIDURI)
if let service = resolutionResult.didDocument?.service?.first(where: { $0.type == "PFI" }) {
switch service.serviceEndpoint {
case let .one(uri):
return uri
case let .many(uris):
return uris.first
}
} else {
return nil
}
}
/// Returns all the valid `Offering`s contained within the provided array
/// - Parameter offerings: The `Offering`s to verify
/// - Returns: An array of `Offering`s that have been verified and are valid
private static func validOfferings(in offerings: [Offering]) async -> [Offering] {
var validOfferings: [Offering] = []
for offering in offerings {
let isValid = (try? await offering.verify()) ?? false
if isValid {
validOfferings.append(offering)
} else {
print("Invalid offering: \(offering.metadata.id)")
}
}
return validOfferings
}
/// Builds an error response based on the provided HTTP response.
/// - Parameters:
/// - data: The response received in the HTTP response from the PFI.
/// - response: The HTTP response received from the PFI.
/// - Returns: A `tbDEXErrorResponse` containing the errors and related information extraced from
/// the HTTP response.
private static func buildErrorResponse(
data: Data,
response: HTTPURLResponse
) -> tbDEXErrorResponse {
let errorDetails: [tbDEXErrorResponse.ErrorDetail]?
if let responseBody = try? tbDEXJSONDecoder().decode([String: AnyCodable].self, from: data),
let errors = responseBody["errors"],
let errorsData = try? tbDEXJSONEncoder().encode(errors) {
errorDetails = try? tbDEXJSONDecoder().decode([tbDEXErrorResponse.ErrorDetail].self, from: errorsData)
} else {
errorDetails = nil
}
return tbDEXErrorResponse(
message: "response status: \(response.statusCode)",
errorDetails: errorDetails
)
}
}
// MARK: - Errors
extension tbDEXHttpClient {
public struct Error: LocalizedError {
let reason: String
public var errorDescription: String? {
return reason
}
}
}