Skip to content
This repository was archived by the owner on Dec 12, 2024. It is now read-only.

Commit 10daa37

Browse files
authored
Implement sendMessage & getExchanges in the tbDEX http-api (#38)
1 parent 8890665 commit 10daa37

File tree

5 files changed

+275
-79
lines changed

5 files changed

+275
-79
lines changed

Sources/tbDEX/Common/JSON/tbDEXJSONEncoder.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ public class tbDEXJSONEncoder: JSONEncoder {
55
public override init() {
66
super.init()
77

8-
outputFormatting = .sortedKeys
8+
outputFormatting = [.sortedKeys, .withoutEscapingSlashes]
99
dateEncodingStrategy = .custom { date, encoder in
1010
var container = encoder.singleValueContainer()
1111
try container.encode(tbDEXDateFormatter.string(from: date))

Sources/tbDEX/HttpClient/HttpClient.swift

Lines changed: 0 additions & 78 deletions
This file was deleted.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import Foundation
2+
import Web5
3+
4+
/// RequestToken struct, used to access protected endpoints in the tbDEX ecosystem
5+
///
6+
/// See [spec](https://github.com/TBD54566975/tbdex/tree/main/specs/http-api#protected-endpoints) for more information.
7+
enum RequestToken {
8+
9+
/// Generate a `RequestToken`
10+
/// - Parameters:
11+
/// - did: The `BearerDID` of the token creator
12+
/// - pfiDIDURI: The DID URI of the PFI that is the token receiver
13+
/// - Returns: Signed request token to be included as Authorization header for sending to PFI endpoints
14+
static func generate(did: BearerDID, pfiDIDURI: String) async throws -> String {
15+
let now = Date()
16+
let exp = now.addingTimeInterval(60)
17+
18+
let claims = JWT.Claims(
19+
issuer: did.uri,
20+
subject: nil,
21+
audience: pfiDIDURI,
22+
expiration: exp,
23+
notBefore: nil,
24+
issuedAt: now,
25+
jwtID: UUID().uuidString
26+
)
27+
28+
return try JWT.sign(did: did, claims: claims)
29+
}
30+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import AnyCodable
2+
import Foundation
3+
4+
/// Representation of an error response from the tbDEX API.
5+
public struct tbDEXErrorResponse: LocalizedError {
6+
7+
/// The error message
8+
public let message: String
9+
10+
/// Additional details about the error
11+
public let errorDetails: [ErrorDetail]?
12+
13+
public struct ErrorDetail: Codable {
14+
let id: String?
15+
let status: String?
16+
let code: String?
17+
let title: String?
18+
let detail: String?
19+
let source: Source?
20+
let meta: [String: AnyCodable]?
21+
22+
public struct Source: Codable {
23+
let pointer: String?
24+
let parameter: String?
25+
let header: String?
26+
}
27+
}
28+
}
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
import AnyCodable
2+
import Foundation
3+
import Web5
4+
5+
public enum tbDEXHttpClient {
6+
7+
static let session = URLSession(configuration: .default)
8+
9+
/// Fetch `Offering`s from a PFI
10+
/// - Parameters:
11+
/// - pfiDIDURI: The DID URI of the PFI
12+
/// - filter: A `GetOfferingFilter` to filter the results
13+
/// - Returns: An array of `Offering`, matching the request
14+
public static func getOfferings(
15+
pfiDIDURI: String,
16+
filter: GetOfferingFilter? = nil
17+
) async throws -> [Offering] {
18+
guard let pfiServiceEndpoint = await getPFIServiceEndpoint(pfiDIDURI: pfiDIDURI) else {
19+
throw Error(reason: "DID does not have service of type PFI")
20+
}
21+
22+
guard var components = URLComponents(string: "\(pfiServiceEndpoint)/offerings") else {
23+
throw Error(reason: "Could not create URLComponents from PFI service endpoint")
24+
}
25+
26+
components.queryItems = filter?.queryItems()
27+
28+
guard let url = components.url else {
29+
throw Error(reason: "Could not create URL from URLComponents")
30+
}
31+
32+
do {
33+
let response = try await URLSession.shared.data(from: url)
34+
let offeringsResponse = try tbDEXJSONDecoder().decode(GetOfferingsResponse.self, from: response.0)
35+
36+
// Return all valid Offerings provided by the PFI, throwing away any that are invalid
37+
return await validOfferings(in: offeringsResponse.data)
38+
} catch {
39+
throw Error(reason: "Error while fetching offerings: \(error)")
40+
}
41+
}
42+
43+
/// Sends a message to a PFI
44+
/// - Parameter message: The message to send
45+
public static func sendMessage<D: MessageData>(
46+
message: Message<D>
47+
) async throws {
48+
guard try await message.verify() else {
49+
throw Error(reason: "Message signature is invalid")
50+
}
51+
52+
let pfiDidUri = message.metadata.to
53+
let exchangeID = message.metadata.exchangeID
54+
let kind = message.metadata.kind
55+
56+
guard let pfiServiceEndpoint = await getPFIServiceEndpoint(pfiDIDURI: pfiDidUri) else {
57+
throw Error(reason: "DID does not have service of type PFI")
58+
}
59+
guard let url = URL(string: "\(pfiServiceEndpoint)/exchanges/\(exchangeID)/\(kind.rawValue)") else {
60+
throw Error(reason: "Could not create URL from PFI service endpoint")
61+
}
62+
63+
var request = URLRequest(url: url)
64+
request.httpMethod = "POST"
65+
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
66+
67+
if case .rfq = message.metadata.kind {
68+
// RFQs are special, and wrap their message in an `rfq` object.
69+
request.httpBody = try tbDEXJSONEncoder().encode(["rfq": message])
70+
} else {
71+
// All other messages encode their messages directly to the http body
72+
request.httpBody = try tbDEXJSONEncoder().encode(message)
73+
}
74+
75+
let (data , response) = try await URLSession.shared.data(for: request)
76+
77+
guard let httpResponse = response as? HTTPURLResponse else {
78+
throw Error(reason: "Invalid response")
79+
}
80+
81+
switch httpResponse.statusCode {
82+
case 200...299:
83+
return
84+
default:
85+
throw buildErrorResponse(data: data, response: httpResponse)
86+
}
87+
}
88+
89+
/// Fetches the exchanges from the PFI based
90+
/// - Parameters:
91+
/// - pfiDIDURI: The PFI's DID URI
92+
/// - requesterDID: The DID of the requester
93+
/// - Returns: 2D array of `AnyMessage` objects, each representing an Exchange between the requester and the PFI
94+
public static func getExchanges(
95+
pfiDIDURI: String,
96+
requesterDID: BearerDID
97+
) async throws -> [[AnyMessage]] {
98+
guard let pfiServiceEndpoint = await getPFIServiceEndpoint(pfiDIDURI: pfiDIDURI) else {
99+
throw Error(reason: "DID does not have service of type PFI")
100+
}
101+
102+
guard let url = URL(string: "\(pfiServiceEndpoint)/exchanges") else {
103+
throw Error(reason: "Could not create URL from PFI service endpoint")
104+
}
105+
106+
let requestToken = try await RequestToken.generate(did: requesterDID, pfiDIDURI: pfiDIDURI)
107+
108+
var request = URLRequest(url: url)
109+
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
110+
request.setValue("Bearer \(requestToken)", forHTTPHeaderField: "Authorization")
111+
112+
let (data, response) = try await URLSession.shared.data(for: request)
113+
114+
guard let httpResponse = response as? HTTPURLResponse else {
115+
throw Error(reason: "Invalid response")
116+
}
117+
118+
switch httpResponse.statusCode {
119+
case 200...299:
120+
do {
121+
let exchangesResponse = try tbDEXJSONDecoder().decode(GetExchangesResponse.self, from: data)
122+
return exchangesResponse.data
123+
} catch {
124+
throw Error(reason: "Error while decoding exchanges: \(error)")
125+
}
126+
default:
127+
throw buildErrorResponse(data: data, response: httpResponse)
128+
}
129+
}
130+
131+
// MARK: - Decodable Response Types
132+
133+
struct GetOfferingsResponse: Decodable {
134+
public let data: [Offering]
135+
}
136+
137+
struct GetExchangesResponse: Decodable {
138+
public let data: [[AnyMessage]]
139+
}
140+
141+
// MARK: - Private
142+
143+
/// Get the PFI service endpoint
144+
/// - Parameter pfiDIDURI: The PFI's DID URI
145+
/// - Returns: The PFI's service endpoint (if it exists)
146+
private static func getPFIServiceEndpoint(pfiDIDURI: String) async -> String? {
147+
let resolutionResult = await DIDResolver.resolve(didURI: pfiDIDURI)
148+
if let service = resolutionResult.didDocument?.service?.first(where: { $0.type == "PFI" }) {
149+
switch service.serviceEndpoint {
150+
case let .one(uri):
151+
return uri
152+
case let .many(uris):
153+
return uris.first
154+
}
155+
} else {
156+
return nil
157+
}
158+
}
159+
160+
/// Returns all the valid `Offering`s contained within the provided array
161+
/// - Parameter offerings: The `Offering`s to verify
162+
/// - Returns: An array of `Offering`s that have been verified and are valid
163+
private static func validOfferings(in offerings: [Offering]) async -> [Offering] {
164+
var validOfferings: [Offering] = []
165+
166+
for offering in offerings {
167+
let isValid = (try? await offering.verify()) ?? false
168+
if isValid {
169+
validOfferings.append(offering)
170+
} else {
171+
print("Invalid offering: \(offering.metadata.id)")
172+
}
173+
}
174+
175+
return validOfferings
176+
}
177+
178+
/// Builds an error response based on the provided HTTP response.
179+
/// - Parameters:
180+
/// - data: The response received in the HTTP response from the PFI.
181+
/// - response: The HTTP response received from the PFI.
182+
/// - Returns: A `tbDEXErrorResponse` containing the errors and related information extraced from
183+
/// the HTTP response.
184+
private static func buildErrorResponse(
185+
data: Data,
186+
response: HTTPURLResponse
187+
) -> tbDEXErrorResponse {
188+
let errorDetails: [tbDEXErrorResponse.ErrorDetail]?
189+
190+
if let responseBody = try? tbDEXJSONDecoder().decode([String: AnyCodable].self, from: data),
191+
let errors = responseBody["errors"],
192+
let errorsData = try? tbDEXJSONEncoder().encode(errors) {
193+
errorDetails = try? tbDEXJSONDecoder().decode([tbDEXErrorResponse.ErrorDetail].self, from: errorsData)
194+
} else {
195+
errorDetails = nil
196+
}
197+
198+
return tbDEXErrorResponse(
199+
message: "response status: \(response.statusCode)",
200+
errorDetails: errorDetails
201+
)
202+
}
203+
}
204+
205+
// MARK: - Errors
206+
207+
extension tbDEXHttpClient {
208+
209+
public struct Error: LocalizedError {
210+
let reason: String
211+
212+
public var errorDescription: String? {
213+
return reason
214+
}
215+
}
216+
}

0 commit comments

Comments
 (0)