From 12341020b4218419b9ca6d23ea762ac394c05231 Mon Sep 17 00:00:00 2001 From: Ethan Lee <125412902+ethan-tbd@users.noreply.github.com> Date: Thu, 1 Aug 2024 11:11:14 -0700 Subject: [PATCH] feat: add `OrderInstructions` (#99) * add `OrderInstructions` message * remove `PaymentInstruction` from `Quote` * add `createOrderInstructions` in `DevTools` * remove `PaymentInstruction` checks in `QuoteTests` * add `OrderInstructionsTests` --- Sources/tbDEX/Protocol/DevTools.swift | 50 +++++++++++++++++-- Sources/tbDEX/Protocol/Models/Message.swift | 1 + .../Protocol/Models/Messages/AnyMessage.swift | 3 ++ .../Models/Messages/OrderInstructions.swift | 50 +++++++++++++++++++ .../Protocol/Models/Messages/Quote.swift | 28 +---------- .../Messages/OrderInstructionsTests.swift | 39 +++++++++++++++ .../Protocol/Models/Messages/QuoteTests.swift | 3 -- 7 files changed, 139 insertions(+), 35 deletions(-) create mode 100644 Sources/tbDEX/Protocol/Models/Messages/OrderInstructions.swift create mode 100644 Tests/tbDEXTests/Protocol/Models/Messages/OrderInstructionsTests.swift diff --git a/Sources/tbDEX/Protocol/DevTools.swift b/Sources/tbDEX/Protocol/DevTools.swift index 9dc6880..2e931e5 100644 --- a/Sources/tbDEX/Protocol/DevTools.swift +++ b/Sources/tbDEX/Protocol/DevTools.swift @@ -133,11 +133,8 @@ enum DevTools { expiresAt: expiresAt, payin: .init( currencyCode: "USD", - amount: "1.00", - paymentInstruction: .init( - link: "https://example.com", - instruction: "test instruction" - ) + amount: "1.00" + ), payout: .init( currencyCode: "AUD", @@ -195,6 +192,49 @@ enum DevTools { } } + /// Creates a mock `OrderInstructions`. Optionally override the `OrderInstructionsData` + /// - Parameters: + /// - from: The DID the `OrderInstructions` should be from. Included in the metadata. + /// - to: The DID the `OrderInstructions` should be sent to. Included in the metadata. + /// - exchangeID: The exchangeID of the associated exchange. Included in the metadata. + /// - protocol: Optional. The protocol version to use if different from the default. Included in the metadata. + /// - Returns: The `OrderInstructions` + static func createOrderInstructions( + from: String, + to: String, + exchangeID: String = "exchange_123", + data: OrderInstructionsData? = nil, + protocol: String? = nil + ) -> OrderInstructions { + let orderInstructionsData = data ?? OrderInstructionsData( + payin: .init( + link: "https://example.com", + instruction: "test instruction" + ), + payout: .init( + link: "https://example.com", + instruction: "test instruction" + ) + ) + + if let `protocol` = `protocol` { + return OrderInstructions( + from: from, + to: to, + exchangeID: exchangeID, + data: orderInstructionsData, + protocol: `protocol` + ) + } else { + return OrderInstructions( + from: from, + to: to, + exchangeID: exchangeID, + data: orderInstructionsData + ) + } + } + /// Creates a mock `OrderStatus`. Optionally override the `OrderStatusData` /// - Parameters: /// - from: The DID the `OrderStatus` should be from. Included in the metadata. diff --git a/Sources/tbDEX/Protocol/Models/Message.swift b/Sources/tbDEX/Protocol/Models/Message.swift index b2ab0c4..96c1dc7 100644 --- a/Sources/tbDEX/Protocol/Models/Message.swift +++ b/Sources/tbDEX/Protocol/Models/Message.swift @@ -82,6 +82,7 @@ public enum MessageKind: String, Codable { case close case quote case order + case orderInstructions = "orderinstructions" case orderStatus = "orderstatus" } diff --git a/Sources/tbDEX/Protocol/Models/Messages/AnyMessage.swift b/Sources/tbDEX/Protocol/Models/Messages/AnyMessage.swift index 18bc81c..02e3412 100644 --- a/Sources/tbDEX/Protocol/Models/Messages/AnyMessage.swift +++ b/Sources/tbDEX/Protocol/Models/Messages/AnyMessage.swift @@ -11,6 +11,7 @@ import Foundation public enum AnyMessage { case close(Close) case order(Order) + case orderInstructions(OrderInstructions) case orderStatus(OrderStatus) case quote(Quote) case rfq(RFQ) @@ -58,6 +59,8 @@ extension AnyMessage: Decodable { self = .close(try container.decode(Close.self)) case .order: self = .order(try container.decode(Order.self)) + case .orderInstructions: + self = .orderInstructions(try container.decode(OrderInstructions.self)) case .orderStatus: self = .orderStatus(try container.decode(OrderStatus.self)) case .quote: diff --git a/Sources/tbDEX/Protocol/Models/Messages/OrderInstructions.swift b/Sources/tbDEX/Protocol/Models/Messages/OrderInstructions.swift new file mode 100644 index 0000000..b1c5581 --- /dev/null +++ b/Sources/tbDEX/Protocol/Models/Messages/OrderInstructions.swift @@ -0,0 +1,50 @@ +import Foundation + +public typealias OrderInstructions = Message + +/// Data that makes up a OrderInstructions Message. +/// +/// [Specification Reference](https://github.com/TBD54566975/tbdex/blob/main/specs/protocol/README.md#orderinstructions) +public struct OrderInstructionsData: MessageData { + + /// The amount of payin currency that the PFI will receive + public let payin: PaymentInstruction + + /// The amount of payout currency that Alice will receive + public let payout: PaymentInstruction + + /// Returns the MessageKind of quote + public func kind() -> MessageKind { + return .orderInstructions + } + + /// Default Initializer + public init( + payin: PaymentInstruction, + payout: PaymentInstruction + ) { + self.payin = payin + self.payout = payout + } +} + +/// Instruction about how to pay or be paid by the PFI +/// +/// [Specification Reference](https://github.com/TBD54566975/tbdex/tree/main/specs/protocol#paymentinstruction) +public struct PaymentInstruction: Codable, Equatable { + + /// Link to allow Alice to pay PFI, or be paid by the PFI + public let link: String? + + /// Instruction on how Alice can pay PFI, or how Alice can be paid by the PFI + public let instruction: String? + + /// Default Initializer + public init( + link: String? = nil, + instruction: String? = nil + ) { + self.link = link + self.instruction = instruction + } +} diff --git a/Sources/tbDEX/Protocol/Models/Messages/Quote.swift b/Sources/tbDEX/Protocol/Models/Messages/Quote.swift index 4879a05..1aa0bf6 100644 --- a/Sources/tbDEX/Protocol/Models/Messages/Quote.swift +++ b/Sources/tbDEX/Protocol/Models/Messages/Quote.swift @@ -47,40 +47,14 @@ public struct QuoteDetails: Codable, Equatable { /// The amount paid in fees public let fee: String? - /// Object that describes how to pay the PFI, and how to get paid by the PFI (e.g. BTC address, payment link) - public let paymentInstruction: PaymentInstruction? - /// Default Initializer public init( currencyCode: String, amount: String, - fee: String? = nil, - paymentInstruction: PaymentInstruction? = nil + fee: String? = nil ) { self.currencyCode = currencyCode self.amount = amount self.fee = fee - self.paymentInstruction = paymentInstruction - } -} - -/// Instruction about how to pay or be paid by the PFI -/// -/// [Specification Reference](https://github.com/TBD54566975/tbdex/tree/main/specs/protocol#paymentinstruction) -public struct PaymentInstruction: Codable, Equatable { - - /// Link to allow Alice to pay PFI, or be paid by the PFI - public let link: String? - - /// Instruction on how Alice can pay PFI, or how Alice can be paid by the PFI - public let instruction: String? - - /// Default Initializer - public init( - link: String? = nil, - instruction: String? = nil - ) { - self.link = link - self.instruction = instruction } } diff --git a/Tests/tbDEXTests/Protocol/Models/Messages/OrderInstructionsTests.swift b/Tests/tbDEXTests/Protocol/Models/Messages/OrderInstructionsTests.swift new file mode 100644 index 0000000..2753c97 --- /dev/null +++ b/Tests/tbDEXTests/Protocol/Models/Messages/OrderInstructionsTests.swift @@ -0,0 +1,39 @@ +import Web5 +import XCTest + +@testable import tbDEX + +final class OrderInstructionsTests: XCTestCase { + + let did = try! DIDJWK.create(keyManager: InMemoryKeyManager()) + let pfi = try! DIDJWK.create(keyManager: InMemoryKeyManager()) + + func test_init() { + let orderInstructions = DevTools.createOrderInstructions(from: pfi.uri, to: did.uri) + + XCTAssertEqual(orderInstructions.metadata.id.prefix, "orderinstructions") + XCTAssertEqual(orderInstructions.metadata.from, pfi.uri) + XCTAssertEqual(orderInstructions.metadata.to, did.uri) + XCTAssertEqual(orderInstructions.metadata.exchangeID, "exchange_123") + + XCTAssertEqual(orderInstructions.data.payin.link, "https://example.com") + XCTAssertEqual(orderInstructions.data.payin.instruction, "test instruction") + + XCTAssertEqual(orderInstructions.data.payout.link, "https://example.com") + XCTAssertEqual(orderInstructions.data.payout.instruction, "test instruction") + } + + func test_verifySuccess() async throws { + var orderInstructions = DevTools.createOrderInstructions(from: pfi.uri, to: did.uri) + try orderInstructions.sign(did: pfi) + + let isValid = try await orderInstructions.verify() + XCTAssertTrue(isValid) + } + + func test_verifyWithoutSigningFailure() async throws { + let orderInstructions = DevTools.createOrderInstructions(from: pfi.uri, to: did.uri) + + await XCTAssertThrowsErrorAsync(try await orderInstructions.verify()) + } +} diff --git a/Tests/tbDEXTests/Protocol/Models/Messages/QuoteTests.swift b/Tests/tbDEXTests/Protocol/Models/Messages/QuoteTests.swift index 5ffc074..a7c5a59 100644 --- a/Tests/tbDEXTests/Protocol/Models/Messages/QuoteTests.swift +++ b/Tests/tbDEXTests/Protocol/Models/Messages/QuoteTests.swift @@ -19,13 +19,10 @@ final class QuoteTests: XCTestCase { XCTAssertEqual(quote.data.payin.currencyCode, "USD") XCTAssertEqual(quote.data.payin.amount, "1.00") XCTAssertNil(quote.data.payin.fee) - XCTAssertEqual(quote.data.payin.paymentInstruction?.link, "https://example.com") - XCTAssertEqual(quote.data.payin.paymentInstruction?.instruction, "test instruction") XCTAssertEqual(quote.data.payout.currencyCode, "AUD") XCTAssertEqual(quote.data.payout.amount, "2.00") XCTAssertEqual(quote.data.payout.fee, "0.50") - XCTAssertNil(quote.data.payout.paymentInstruction) } func test_verifySuccess() async throws {