From 7d7e6d552d3c078913c833434ac35ad2020caf3d Mon Sep 17 00:00:00 2001 From: Ethan Lee <125412902+ethan-tbd@users.noreply.github.com> Date: Wed, 28 Aug 2024 10:53:34 -0700 Subject: [PATCH] feat: update tbDEX test vectors (#102) * update tbdex test vectors * add `payoutUnitsPerPayinUnit`, `subtotal`, and `total` to `Quote` * add `details` and `status` enums to `OrderStatus` * add fallback date formatter to handle decoding dates without fractional seconds * use updated `QuoteData` and `OrderStatusData` constructors * update `OrderStatusTests` * update `QuoteTests` * add test vectors for `Cancel` and `OrderInstructions` --- .../Common/JSON/tbDEXDateFormatter.swift | 9 ++++++ .../tbDEX/Common/JSON/tbDEXJSONDecoder.swift | 14 ++++----- Sources/tbDEX/Protocol/DevTools.swift | 12 ++++---- .../Models/Messages/OrderStatus.swift | 30 +++++++++++++++++-- .../Protocol/Models/Messages/Quote.swift | 26 +++++++++++----- .../tbDEXTestVectorsProtocol.swift | 28 +++++++++++++++++ Tests/tbDEXTestVectors/tbdex-spec | 2 +- .../Models/Messages/OrderStatusTests.swift | 2 +- .../Protocol/Models/Messages/QuoteTests.swift | 6 ++-- 9 files changed, 103 insertions(+), 26 deletions(-) diff --git a/Sources/tbDEX/Common/JSON/tbDEXDateFormatter.swift b/Sources/tbDEX/Common/JSON/tbDEXDateFormatter.swift index a80f169..c5f20db 100644 --- a/Sources/tbDEX/Common/JSON/tbDEXDateFormatter.swift +++ b/Sources/tbDEX/Common/JSON/tbDEXDateFormatter.swift @@ -7,3 +7,12 @@ let tbDEXDateFormatter: ISO8601DateFormatter = { dateFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] return dateFormatter }() + + +/// A date formatter that can be used to decode dates in the ISO8601 format +/// without fractional seconds. +let tbDEXFallbackDateFormatter: ISO8601DateFormatter = { + let dateFormatter = ISO8601DateFormatter() + dateFormatter.formatOptions = [.withInternetDateTime] + return dateFormatter +}() diff --git a/Sources/tbDEX/Common/JSON/tbDEXJSONDecoder.swift b/Sources/tbDEX/Common/JSON/tbDEXJSONDecoder.swift index 8e94736..0eb70e1 100644 --- a/Sources/tbDEX/Common/JSON/tbDEXJSONDecoder.swift +++ b/Sources/tbDEX/Common/JSON/tbDEXJSONDecoder.swift @@ -8,15 +8,15 @@ public class tbDEXJSONDecoder: JSONDecoder { dateDecodingStrategy = .custom { decoder in let container = try decoder.singleValueContainer() let dateString = try container.decode(String.self) - - if let date = tbDEXDateFormatter.date(from: dateString) { + + if let date = tbDEXDateFormatter.date(from: dateString) ?? tbDEXFallbackDateFormatter.date(from: dateString) { return date - } else { - throw DecodingError.dataCorruptedError( - in: container, - debugDescription: "Invalid date: \(dateString)" - ) } + + throw DecodingError.dataCorruptedError( + in: container, + debugDescription: "Invalid date: \(dateString)" + ) } } } diff --git a/Sources/tbDEX/Protocol/DevTools.swift b/Sources/tbDEX/Protocol/DevTools.swift index 27654cc..bbcc1af 100644 --- a/Sources/tbDEX/Protocol/DevTools.swift +++ b/Sources/tbDEX/Protocol/DevTools.swift @@ -131,15 +131,17 @@ enum DevTools { let quoteData = data ?? QuoteData( expiresAt: expiresAt, + payoutUnitsPerPayinUnit: "1.0", payin: .init( currencyCode: "USD", - amount: "1.00" - + subtotal: "1.00", + total: "1.00" ), payout: .init( currencyCode: "AUD", - amount: "2.00", - fee: "0.50" + subtotal: "2.00", + fee: "0.50", + total: "2.50" ) ) @@ -250,7 +252,7 @@ enum DevTools { protocol: String? = nil ) -> OrderStatus { let orderStatusData = data ?? OrderStatusData( - orderStatus: "test status" + status: Status.payinInitiated ) if let `protocol` = `protocol` { diff --git a/Sources/tbDEX/Protocol/Models/Messages/OrderStatus.swift b/Sources/tbDEX/Protocol/Models/Messages/OrderStatus.swift index a1e926d..3d8f589 100644 --- a/Sources/tbDEX/Protocol/Models/Messages/OrderStatus.swift +++ b/Sources/tbDEX/Protocol/Models/Messages/OrderStatus.swift @@ -8,7 +8,10 @@ public typealias OrderStatus = Message public struct OrderStatusData: MessageData { /// Current status of Order that's being executed - public let orderStatus: String + public let status: Status + + /// Additional details about the status + public let details: String? /// Returns the MessageKind of orderstatus public func kind() -> MessageKind { @@ -17,8 +20,29 @@ public struct OrderStatusData: MessageData { /// Default Initializer public init( - orderStatus: String + status: Status, + details: String? = nil ) { - self.orderStatus = orderStatus + self.status = status + self.details = details } } + +/// Enum representing the various statuses in the OrderStatus Message. +/// +/// [Specification Reference](https://github.com/TBD54566975/tbdex/tree/main/specs/protocol#status) +public enum Status: String, Codable { + case payinPending = "PAYIN_PENDING" + case payinInitiated = "PAYIN_INITIATED" + case payinSettled = "PAYIN_SETTLED" + case payinFailed = "PAYIN_FAILED" + case payinExpired = "PAYIN_EXPIRED" + case payoutPending = "PAYOUT_PENDING" + case payoutInitiated = "PAYOUT_INITIATED" + case payoutSettled = "PAYOUT_SETTLED" + case payoutFailed = "PAYOUT_FAILED" + case refundPending = "REFUND_PENDING" + case refundInitiated = "REFUND_INITIATED" + case refundFailed = "REFUND_FAILED" + case refundSettled = "REFUND_SETTLED" +} diff --git a/Sources/tbDEX/Protocol/Models/Messages/Quote.swift b/Sources/tbDEX/Protocol/Models/Messages/Quote.swift index 1aa0bf6..57be3e6 100644 --- a/Sources/tbDEX/Protocol/Models/Messages/Quote.swift +++ b/Sources/tbDEX/Protocol/Models/Messages/Quote.swift @@ -7,8 +7,11 @@ public typealias Quote = Message /// [Specification Reference](https://github.com/TBD54566975/tbdex/tree/main/specs/protocol#quote) public struct QuoteData: MessageData { - /// When this quote expires. + /// When this quote expires public let expiresAt: Date + + /// The exchange rate to convert from payin currency to payout currency + public let payoutUnitsPerPayinUnit: String /// The amount of payin currency that the PFI will receive public let payin: QuoteDetails @@ -24,10 +27,12 @@ public struct QuoteData: MessageData { /// Default Initializer public init( expiresAt: Date, + payoutUnitsPerPayinUnit: String, payin: QuoteDetails, payout: QuoteDetails ) { self.expiresAt = expiresAt + self.payoutUnitsPerPayinUnit = payoutUnitsPerPayinUnit self.payin = payin self.payout = payout } @@ -41,20 +46,27 @@ public struct QuoteDetails: Codable, Equatable { /// ISO 3166 currency code string public let currencyCode: String - /// The amount of currency expressed in the smallest respective unit - public let amount: String - + /// The amount of currency paid for the exchange, excluding fees + public let subtotal: String + /// The amount paid in fees public let fee: String? + + /// The total amount of currency to be paid in or paid out + public let total: String + + /// Default Initializer public init( currencyCode: String, - amount: String, - fee: String? = nil + subtotal: String, + fee: String? = nil, + total: String ) { self.currencyCode = currencyCode - self.amount = amount + self.subtotal = subtotal self.fee = fee + self.total = total } } diff --git a/Tests/tbDEXTestVectors/tbDEXTestVectorsProtocol.swift b/Tests/tbDEXTestVectors/tbDEXTestVectorsProtocol.swift index 74a67c4..1b48084 100644 --- a/Tests/tbDEXTestVectors/tbDEXTestVectorsProtocol.swift +++ b/Tests/tbDEXTestVectors/tbDEXTestVectorsProtocol.swift @@ -52,6 +52,20 @@ final class tbDEXTestVectorsProtocol: XCTestCase { XCTAssertNoDifference(parsedClose, vector.output) } + + func test_parse_cancel() throws { + let vector = try TestVector( + fileName: "parse-cancel", + subdirectory: vectorSubdirectory + ) + + let parsedMessage = try AnyMessage.parse(vector.input) + guard case let .cancel(parsedCancel) = parsedMessage else { + return XCTFail("Parsed message is not a Cancel") + } + + XCTAssertNoDifference(parsedCancel, vector.output) + } func test_parse_order() throws { let vector = try TestVector( @@ -66,6 +80,20 @@ final class tbDEXTestVectorsProtocol: XCTestCase { XCTAssertNoDifference(parsedOrder, vector.output) } + + func test_parse_orderinstructions() throws { + let vector = try TestVector( + fileName: "parse-orderinstructions", + subdirectory: vectorSubdirectory + ) + + let parsedMessage = try AnyMessage.parse(vector.input) + guard case let .orderInstructions(parsedOrderInstructions) = parsedMessage else { + return XCTFail("Parsed message is not an OrderInstructions") + } + + XCTAssertNoDifference(parsedOrderInstructions, vector.output) + } func test_parse_orderstatus() throws { let vector = try TestVector( diff --git a/Tests/tbDEXTestVectors/tbdex-spec b/Tests/tbDEXTestVectors/tbdex-spec index ad7610a..725e21c 160000 --- a/Tests/tbDEXTestVectors/tbdex-spec +++ b/Tests/tbDEXTestVectors/tbdex-spec @@ -1 +1 @@ -Subproject commit ad7610a0e6d65b8bd879ef9f4dec56a1eab9f2a2 +Subproject commit 725e21c485ca6e17a0e3d3a729c0f056a634cb32 diff --git a/Tests/tbDEXTests/Protocol/Models/Messages/OrderStatusTests.swift b/Tests/tbDEXTests/Protocol/Models/Messages/OrderStatusTests.swift index 3f8b4c0..db59990 100644 --- a/Tests/tbDEXTests/Protocol/Models/Messages/OrderStatusTests.swift +++ b/Tests/tbDEXTests/Protocol/Models/Messages/OrderStatusTests.swift @@ -15,7 +15,7 @@ final class OrderStatusTests: XCTestCase { XCTAssertEqual(orderStatus.metadata.from, pfi.uri) XCTAssertEqual(orderStatus.metadata.to, did.uri) XCTAssertEqual(orderStatus.metadata.exchangeID, "exchange_123") - XCTAssertEqual(orderStatus.data.orderStatus, "test status") + XCTAssertEqual(orderStatus.data.status, Status.payinInitiated) } func test_verifySuccess() async throws { diff --git a/Tests/tbDEXTests/Protocol/Models/Messages/QuoteTests.swift b/Tests/tbDEXTests/Protocol/Models/Messages/QuoteTests.swift index a7c5a59..51da717 100644 --- a/Tests/tbDEXTests/Protocol/Models/Messages/QuoteTests.swift +++ b/Tests/tbDEXTests/Protocol/Models/Messages/QuoteTests.swift @@ -17,12 +17,14 @@ final class QuoteTests: XCTestCase { XCTAssertEqual(quote.metadata.exchangeID, "exchange_123") XCTAssertEqual(quote.data.payin.currencyCode, "USD") - XCTAssertEqual(quote.data.payin.amount, "1.00") + XCTAssertEqual(quote.data.payin.total, "1.00") XCTAssertNil(quote.data.payin.fee) XCTAssertEqual(quote.data.payout.currencyCode, "AUD") - XCTAssertEqual(quote.data.payout.amount, "2.00") + XCTAssertEqual(quote.data.payout.subtotal, "2.00") XCTAssertEqual(quote.data.payout.fee, "0.50") + XCTAssertEqual(quote.data.payout.total, "2.50") + } func test_verifySuccess() async throws {