diff --git a/Package.swift b/Package.swift index 5c6725d..79e7621 100644 --- a/Package.swift +++ b/Package.swift @@ -16,7 +16,7 @@ let package = Package( dependencies: [ .package(url: "https://github.com/Frizlab/swift-typeid.git", from: "0.3.0"), .package(url: "https://github.com/flight-school/anycodable.git", from: "0.6.7"), - .package(url: "https://github.com/TBD54566975/web5-swift", exact: "0.0.4"), + .package(url: "https://github.com/TBD54566975/web5-swift", branch: "main"), .package(url: "https://github.com/allegro/swift-junit.git", from: "2.1.0"), .package(url: "https://github.com/pointfreeco/swift-custom-dump.git", from: "1.1.2"), ], diff --git a/Sources/tbDEX/Protocol/Models/Messages/OrderStatus.swift b/Sources/tbDEX/Protocol/Models/Messages/OrderStatus.swift index a1e926d..56d8d67 100644 --- a/Sources/tbDEX/Protocol/Models/Messages/OrderStatus.swift +++ b/Sources/tbDEX/Protocol/Models/Messages/OrderStatus.swift @@ -14,11 +14,4 @@ public struct OrderStatusData: MessageData { public func kind() -> MessageKind { return .orderStatus } - - /// Default Initializer - public init( - orderStatus: String - ) { - self.orderStatus = orderStatus - } } diff --git a/Sources/tbDEX/Protocol/Models/Messages/Quote.swift b/Sources/tbDEX/Protocol/Models/Messages/Quote.swift index 4879a05..8ab3808 100644 --- a/Sources/tbDEX/Protocol/Models/Messages/Quote.swift +++ b/Sources/tbDEX/Protocol/Models/Messages/Quote.swift @@ -20,17 +20,6 @@ public struct QuoteData: MessageData { public func kind() -> MessageKind { return .quote } - - /// Default Initializer - public init( - expiresAt: Date, - payin: QuoteDetails, - payout: QuoteDetails - ) { - self.expiresAt = expiresAt - self.payin = payin - self.payout = payout - } } /// Details about a quoted amount @@ -50,18 +39,6 @@ public struct QuoteDetails: Codable, Equatable { /// 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 - ) { - self.currencyCode = currencyCode - self.amount = amount - self.fee = fee - self.paymentInstruction = paymentInstruction - } } /// Instruction about how to pay or be paid by the PFI @@ -75,12 +52,4 @@ public struct PaymentInstruction: Codable, Equatable { /// 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/OrderStatusTests.swift b/Tests/tbDEXTests/Protocol/Models/Messages/OrderStatusTests.swift index f2a3a89..5be076a 100644 --- a/Tests/tbDEXTests/Protocol/Models/Messages/OrderStatusTests.swift +++ b/Tests/tbDEXTests/Protocol/Models/Messages/OrderStatusTests.swift @@ -5,69 +5,60 @@ import XCTest final class OrderStatusTests: XCTestCase { - let did = try! DIDJWK.create(keyManager: InMemoryKeyManager()) - let pfi = try! DIDJWK.create(keyManager: InMemoryKeyManager()) - - func test_init() { - let orderStatus = createOrderStatus(from: did.uri, to: pfi.uri) - - XCTAssertEqual(orderStatus.metadata.id.prefix, "orderstatus") - XCTAssertEqual(orderStatus.metadata.from, did.uri) - XCTAssertEqual(orderStatus.metadata.to, pfi.uri) - XCTAssertEqual(orderStatus.metadata.exchangeID, "exchange_123") - XCTAssertEqual(orderStatus.data.orderStatus, "test status") + func test_parseOrderStatusFromStringified() throws { + if let orderStatus = try parsedOrderStatus(orderStatus: orderStatusStringJSON) { + XCTAssertEqual(orderStatus.metadata.kind, MessageKind.orderStatus) + } else { + XCTFail("Order status is not a parsed orderStatus") + } } - func test_overrideProtocolVersion() { - let orderstatus = OrderStatus( - from: did.uri, - to: pfi.uri, - exchangeID: "exchange_123", - data: .init( - orderStatus: "test status" - ), - externalID: nil, - protocol: "2.0" - ) - - XCTAssertEqual(orderstatus.metadata.id.prefix, "orderstatus") - XCTAssertEqual(orderstatus.metadata.from, did.uri) - XCTAssertEqual(orderstatus.metadata.to, pfi.uri) - XCTAssertEqual(orderstatus.metadata.exchangeID, "exchange_123") - XCTAssertEqual(orderstatus.metadata.protocol, "2.0") + func test_parseOrderStatusFromPrettified() throws { + if let orderStatus = try parsedOrderStatus(orderStatus: orderStatusPrettyJSON) { + XCTAssertEqual(orderStatus.metadata.kind, MessageKind.orderStatus) + } else { + XCTFail("Order status is not a parsed orderStatus") + } } - func test_signAndVerify() async throws { - let did = try DIDJWK.create(keyManager: InMemoryKeyManager()) - let pfi = try DIDJWK.create(keyManager: InMemoryKeyManager()) - var orderStatus = createOrderStatus(from: did.uri, to: pfi.uri) - - XCTAssertNil(orderStatus.signature) - try orderStatus.sign(did: did) - XCTAssertNotNil(orderStatus.signature) - let isValid = try await orderStatus.verify() - XCTAssertTrue(isValid) + func test_verifyOrderStatusIsValid() async throws { + if let orderStatus = try parsedOrderStatus(orderStatus: orderStatusPrettyJSON) { + XCTAssertNotNil(orderStatus.signature) + XCTAssertNotNil(orderStatus.data) + XCTAssertNotNil(orderStatus.metadata) + let isValid = try await orderStatus.verify() + XCTAssertTrue(isValid) + } else { + XCTFail("Order status is not a parsed orderStatus") + } } - - func test_verifyWithoutSigningFailure() async throws { - let orderStatus = createOrderStatus(from: did.uri, to: pfi.uri) - - await XCTAssertThrowsErrorAsync(try await orderStatus.verify()) - } - - private func createOrderStatus( - from: String, - to: String - ) -> OrderStatus { - OrderStatus( - from: from, - to: to, - exchangeID: "exchange_123", - data: .init( - orderStatus: "test status" - ), - externalID: nil, - protocol: nil - ) + + private func parsedOrderStatus(orderStatus: String) throws -> OrderStatus? { + let parsedMessage = try AnyMessage.parse(orderStatus) + guard case let .orderStatus(parsedOrderStatus) = parsedMessage else { + return nil + } + return parsedOrderStatus } + + let orderStatusPrettyJSON = """ + { + "metadata": { + "from": "did:dht:geiro75xjbn81snmangwc35wkfsra8mt3awbga8drrjde5z9r9jo", + "to": "did:dht:n46hom5afi6xrsxmddx5rjecyyx1faz4ocs4ie43tfkyo4darh9y", + "exchangeId": "rfq_01hrqn6pp1e48a3meq95dzmkzs", + "protocol": "1.0", + "kind": "orderstatus", + "id": "orderstatus_01hrqn6pp1e48a3meq9b3brgta", + "createdAt": "2024-03-11T21:02:55.681Z" + }, + "data": { + "orderStatus": "wee" + }, + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpkaHQ6Z2Vpcm83NXhqYm44MXNubWFuZ3djMzV3a2ZzcmE4bXQzYXdiZ2E4ZHJyamRlNXo5cjlqbyMwIn0..aHifNdyzwVZ-bvqyp8H6WHE_K_24y1-sdPIohXPvdBZXIxjqMb2tDaeJLKbtz1mcoYDau_N-_5kVqVSeGtUYCA" + } + """ + + let orderStatusStringJSON = + "{\"metadata\":{\"from\":\"did:dht:geiro75xjbn81snmangwc35wkfsra8mt3awbga8drrjde5z9r9jo\",\"to\":\"did:dht:n46hom5afi6xrsxmddx5rjecyyx1faz4ocs4ie43tfkyo4darh9y\",\"exchangeId\":\"rfq_01hrqn6pp1e48a3meq95dzmkzs\",\"protocol\":\"1.0\",\"kind\":\"orderstatus\",\"id\":\"orderstatus_01hrqn6pp1e48a3meq9b3brgta\",\"createdAt\":\"2024-03-11T21:02:55.681Z\"},\"data\":{\"orderStatus\":\"wee\"},\"signature\":\"eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpkaHQ6Z2Vpcm83NXhqYm44MXNubWFuZ3djMzV3a2ZzcmE4bXQzYXdiZ2E4ZHJyamRlNXo5cjlqbyMwIn0..aHifNdyzwVZ-bvqyp8H6WHE_K_24y1-sdPIohXPvdBZXIxjqMb2tDaeJLKbtz1mcoYDau_N-_5kVqVSeGtUYCA\"}" } diff --git a/Tests/tbDEXTests/Protocol/Models/Messages/QuoteTests.swift b/Tests/tbDEXTests/Protocol/Models/Messages/QuoteTests.swift index feda499..ad369f2 100644 --- a/Tests/tbDEXTests/Protocol/Models/Messages/QuoteTests.swift +++ b/Tests/tbDEXTests/Protocol/Models/Messages/QuoteTests.swift @@ -5,106 +5,77 @@ import XCTest final class QuoteTests: XCTestCase { - let did = try! DIDJWK.create(keyManager: InMemoryKeyManager()) - let pfi = try! DIDJWK.create(keyManager: InMemoryKeyManager()) - - func test_init() { - let quote = createQuote(from: did.uri, to: pfi.uri) - - XCTAssertEqual(quote.metadata.id.prefix, "quote") - XCTAssertEqual(quote.metadata.from, did.uri) - XCTAssertEqual(quote.metadata.to, pfi.uri) - XCTAssertEqual(quote.metadata.exchangeID, "exchange_123") - - 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_parseQuoteFromStringified() throws { + if let quote = try parsedQuote(quote: quoteStringJSON) { + XCTAssertEqual(quote.metadata.kind, MessageKind.quote) + } else { + XCTFail("Quote is not a parsed quote") + } } - func test_overrideProtocolVersion() { - let quote = Quote( - from: did.uri, - to: pfi.uri, - exchangeID: "exchange_123", - data: .init( - expiresAt: Date().addingTimeInterval(60), - payin: .init( - currencyCode: "USD", - amount: "1.00", - paymentInstruction: .init( - link: "https://example.com", - instruction: "test instruction" - ) - ), - payout: .init( - currencyCode: "AUD", - amount: "2.00", - fee: "0.50" - ) - ), - externalID: nil, - protocol: "2.0" - ) - - XCTAssertEqual(quote.metadata.id.prefix, "quote") - XCTAssertEqual(quote.metadata.from, did.uri) - XCTAssertEqual(quote.metadata.to, pfi.uri) - XCTAssertEqual(quote.metadata.exchangeID, "exchange_123") - XCTAssertEqual(quote.metadata.protocol, "2.0") - } - - func test_signAndVerify() async throws { - var quote = createQuote(from: did.uri, to: pfi.uri) - - XCTAssertNil(quote.signature) - try quote.sign(did: did) - XCTAssertNotNil(quote.signature) - let isValid = try await quote.verify() - XCTAssertTrue(isValid) + func test_parseQuoteFromPrettified() throws { + if let quote = try parsedQuote(quote: quotePrettyJSON) { + XCTAssertEqual(quote.metadata.kind, MessageKind.quote) + } else { + XCTFail("Quote is not a parsed quote") + } } - func test_verifyWithoutSigningFailure() async throws { - let quote = createQuote(from: did.uri, to: pfi.uri) - - await XCTAssertThrowsErrorAsync(try await quote.verify()) + func test_verifyQuoteIsValid() async throws { + if let quote = try parsedQuote(quote: quotePrettyJSON) { + XCTAssertNotNil(quote.signature) + XCTAssertNotNil(quote.data) + XCTAssertNotNil(quote.metadata) + let isValid = try await quote.verify() + XCTAssertTrue(isValid) + } else { + XCTFail("Quote is not a parsed quote") + } } - - private func createQuote( - from: String, - to: String - ) -> Quote { - let now = Date() - let expiresAt = now.addingTimeInterval(60) - - return Quote( - from: from, - to: to, - exchangeID: "exchange_123", - data: .init( - expiresAt: expiresAt, - payin: .init( - currencyCode: "USD", - amount: "1.00", - paymentInstruction: .init( - link: "https://example.com", - instruction: "test instruction" - ) - ), - payout: .init( - currencyCode: "AUD", - amount: "2.00", - fee: "0.50" - ) - ), - externalID: nil, - protocol: nil - ) + + private func parsedQuote(quote: String) throws -> Quote? { + let parsedMessage = try AnyMessage.parse(quote) + guard case let .quote(parsedQuote) = parsedMessage else { + return nil + } + return parsedQuote } + + let quotePrettyJSON = """ + { + "metadata": { + "exchangeId": "rfq_01hrqn6pj7e3k8yt8wb6bvgjq2", + "from": "did:dht:ukqgxyzjmt8h7brwqrrfes8if5f11hun888kbaj899i1gjuz4ogo", + "to": "did:dht:n46hom5afi6xrsxmddx5rjecyyx1faz4ocs4ie43tfkyo4darh9y", + "protocol": "1.0", + "kind": "quote", + "id": "quote_01hrqn6pj7e3k8yt8wb8f6h76n", + "createdAt": "2024-03-11T21:02:55.559Z" + }, + "data": { + "expiresAt": "2024-03-11T21:02:55.559Z", + "payin": { + "currencyCode": "BTC", + "amount": "0.01", + "fee": "0.0001", + "paymentInstruction": { + "link": "tbdex.io/example", + "instruction": "Fake instruction" + } + }, + "payout": { + "currencyCode": "USD", + "amount": "1000.00", + "paymentInstruction": { + "link": "tbdex.io/example", + "instruction": "Fake instruction" + } + } + }, + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpkaHQ6dWtxZ3h5emptdDhoN2Jyd3FycmZlczhpZjVmMTFodW44ODhrYmFqODk5aTFnanV6NG9nbyMwIn0..L3HbRNTeyY8bgaAwkWOGEpwXnGxhs0Hk2bzT5GSaZRMoA0mVvj9x27sVxn5B1PMq-1UekKLSdlQWi65uSQ04Dg" + } + """ + + let quoteStringJSON = + "{\"metadata\":{\"exchangeId\":\"rfq_01hrqn6pj7e3k8yt8wb6bvgjq2\",\"from\":\"did:dht:ukqgxyzjmt8h7brwqrrfes8if5f11hun888kbaj899i1gjuz4ogo\",\"to\":\"did:dht:n46hom5afi6xrsxmddx5rjecyyx1faz4ocs4ie43tfkyo4darh9y\",\"protocol\":\"1.0\",\"kind\":\"quote\",\"id\":\"quote_01hrqn6pj7e3k8yt8wb8f6h76n\",\"createdAt\":\"2024-03-11T21:02:55.559Z\"},\"data\":{\"expiresAt\":\"2024-03-11T21:02:55.559Z\",\"payin\":{\"currencyCode\":\"BTC\",\"amount\":\"0.01\",\"fee\":\"0.0001\",\"paymentInstruction\":{\"link\":\"tbdex.io/example\",\"instruction\":\"Fake instruction\"}},\"payout\":{\"currencyCode\":\"USD\",\"amount\":\"1000.00\",\"paymentInstruction\":{\"link\":\"tbdex.io/example\",\"instruction\":\"Fake instruction\"}}},\"signature\":\"eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpkaHQ6dWtxZ3h5emptdDhoN2Jyd3FycmZlczhpZjVmMTFodW44ODhrYmFqODk5aTFnanV6NG9nbyMwIn0..L3HbRNTeyY8bgaAwkWOGEpwXnGxhs0Hk2bzT5GSaZRMoA0mVvj9x27sVxn5B1PMq-1UekKLSdlQWi65uSQ04Dg\"}" } diff --git a/Tests/tbDEXTests/Protocol/Models/Resources/OfferingTests.swift b/Tests/tbDEXTests/Protocol/Models/Resources/OfferingTests.swift index e78aca4..9d45e38 100644 --- a/Tests/tbDEXTests/Protocol/Models/Resources/OfferingTests.swift +++ b/Tests/tbDEXTests/Protocol/Models/Resources/OfferingTests.swift @@ -5,17 +5,24 @@ import XCTest final class OfferingTests: XCTestCase { - func test_parseOffering() throws { - if let offering = try parsedOffering() { + func test_parseOfferingFromStringified() throws { + if let offering = try parsedOffering(offering: offeringStringJSON) { + XCTAssertEqual(offering.metadata.kind, ResourceKind.offering) + } else { + XCTFail("Offering is not a parsed offering") + } + } + + func test_parseOfferingFromPrettified() throws { + if let offering = try parsedOffering(offering: offeringPrettyJSON) { XCTAssertEqual(offering.metadata.kind, ResourceKind.offering) } else { XCTFail("Offering is not a parsed offering") } - } func test_verifyOfferingIsValid() async throws { - if let offering = try parsedOffering() { + if let offering = try parsedOffering(offering: offeringPrettyJSON) { XCTAssertNotNil(offering.signature) XCTAssertNotNil(offering.data) XCTAssertNotNil(offering.metadata) @@ -25,14 +32,111 @@ final class OfferingTests: XCTestCase { XCTFail("Offering is not a parsed offering") } } - - private func parsedOffering() throws -> Offering? { - let offeringJson = "{\"metadata\":{\"from\":\"did:dht:77em1f968c1gzwrrb15cgkzjxg8rft67ebxj6gjkocnz5p8sdniy\",\"protocol\":\"1.0\",\"kind\":\"offering\",\"id\":\"offering_01hrqn6ph3f00asxqvx46capbw\",\"createdAt\":\"2024-03-11T21:02:55.523Z\"},\"data\":{\"description\":\"Selling BTC for USD\",\"payinCurrency\":{\"currencyCode\":\"USD\",\"minAmount\":\"0.0\",\"maxAmount\":\"999999.99\"},\"payoutCurrency\":{\"currencyCode\":\"BTC\",\"maxAmount\":\"999526.11\"},\"payoutUnitsPerPayinUnit\":\"0.00003826\",\"payinMethods\":[{\"kind\":\"DEBIT_CARD\",\"requiredPaymentDetails\":{\"$schema\":\"http://json-schema.org/draft-07/schema\",\"type\":\"object\",\"properties\":{\"cardNumber\":{\"type\":\"string\",\"description\":\"The 16-digit debit card number\",\"minLength\":16,\"maxLength\":16},\"expiryDate\":{\"type\":\"string\",\"description\":\"The expiry date of the card in MM/YY format\",\"pattern\":\"^(0[1-9]|1[0-2])\\\\/([0-9]{2})$\"},\"cardHolderName\":{\"type\":\"string\",\"description\":\"Name of the cardholder as it appears on the card\"},\"cvv\":{\"type\":\"string\",\"description\":\"The 3-digit CVV code\",\"minLength\":3,\"maxLength\":3}},\"required\":[\"cardNumber\",\"expiryDate\",\"cardHolderName\",\"cvv\"],\"additionalProperties\":false}}],\"payoutMethods\":[{\"kind\":\"BTC_ADDRESS\",\"requiredPaymentDetails\":{\"$schema\":\"http://json-schema.org/draft-07/schema\",\"type\":\"object\",\"properties\":{\"btcAddress\":{\"type\":\"string\",\"description\":\"your Bitcoin wallet address\"}},\"required\":[\"btcAddress\"],\"additionalProperties\":false}}],\"requiredClaims\":{\"id\":\"7ce4004c-3c38-4853-968b-e411bafcd945\",\"input_descriptors\":[{\"id\":\"bbdb9b7c-5754-4f46-b63b-590bada959e0\",\"constraints\":{\"fields\":[{\"path\":[\"$.type\"],\"filter\":{\"type\":\"string\",\"const\":\"YoloCredential\"}}]}}]}},\"signature\":\"eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpkaHQ6NzdlbTFmOTY4YzFnendycmIxNWNna3pqeGc4cmZ0NjdlYnhqNmdqa29jbno1cDhzZG5peSMwIn0..puQwdTvi4KTfKedA6CXdHHldztoQ8udUrQrGmw1wvWfYW3ilMB8myoD3ATw7NGlt1NuizJ80i4ufZArgGrTiAA\"}" - let parsedResource = try AnyResource.parse(offeringJson) + + private func parsedOffering(offering: String) throws -> Offering? { + let parsedResource = try AnyResource.parse(offering) guard case let .offering(parsedOffering) = parsedResource else { return nil } return parsedOffering } + + let offeringPrettyJSON = """ + { + "metadata": { + "from": "did:dht:77em1f968c1gzwrrb15cgkzjxg8rft67ebxj6gjkocnz5p8sdniy", + "protocol": "1.0", + "kind": "offering", + "id": "offering_01hrqn6ph3f00asxqvx46capbw", + "createdAt": "2024-03-11T21:02:55.523Z" + }, + "data": { + "description": "Selling BTC for USD", + "payinCurrency": { + "currencyCode": "USD", + "minAmount": "0.0", + "maxAmount": "999999.99" + }, + "payoutCurrency": { + "currencyCode": "BTC", + "maxAmount": "999526.11" + }, + "payoutUnitsPerPayinUnit": "0.00003826", + "payinMethods": [ + { + "kind": "DEBIT_CARD", + "requiredPaymentDetails": { + "$schema": "http://json-schema.org/draft-07/schema", + "type": "object", + "properties": { + "cardNumber": { + "type": "string", + "description": "The 16-digit debit card number", + "minLength": 16, + "maxLength": 16 + }, + "expiryDate": { + "type": "string", + "description": "The expiry date of the card in MM/YY format", + "pattern": "^(0[1-9]|1[0-2])\\\\/([0-9]{2})$" + }, + "cardHolderName": { + "type": "string", + "description": "Name of the cardholder as it appears on the card" + }, + "cvv": { + "type": "string", + "description": "The 3-digit CVV code", + "minLength": 3, + "maxLength": 3 + } + }, + "required": ["cardNumber", "expiryDate", "cardHolderName", "cvv"], + "additionalProperties": false + } + } + ], + "payoutMethods": [ + { + "kind": "BTC_ADDRESS", + "requiredPaymentDetails": { + "$schema": "http://json-schema.org/draft-07/schema", + "type": "object", + "properties": { + "btcAddress": { + "type": "string", + "description": "your Bitcoin wallet address" + } + }, + "required": ["btcAddress"], + "additionalProperties": false + } + } + ], + "requiredClaims": { + "id": "7ce4004c-3c38-4853-968b-e411bafcd945", + "input_descriptors": [ + { + "id": "bbdb9b7c-5754-4f46-b63b-590bada959e0", + "constraints": { + "fields": [ + { + "path": ["$.type"], + "filter": { + "type": "string", + "const": "YoloCredential" + } + } + ] + } + } + ] + } + }, + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpkaHQ6NzdlbTFmOTY4YzFnendycmIxNWNna3pqeGc4cmZ0NjdlYnhqNmdqa29jbno1cDhzZG5peSMwIn0..puQwdTvi4KTfKedA6CXdHHldztoQ8udUrQrGmw1wvWfYW3ilMB8myoD3ATw7NGlt1NuizJ80i4ufZArgGrTiAA" + } + """ + + let offeringStringJSON = "{\"metadata\":{\"from\":\"did:dht:77em1f968c1gzwrrb15cgkzjxg8rft67ebxj6gjkocnz5p8sdniy\",\"protocol\":\"1.0\",\"kind\":\"offering\",\"id\":\"offering_01hrqn6ph3f00asxqvx46capbw\",\"createdAt\":\"2024-03-11T21:02:55.523Z\"},\"data\":{\"description\":\"Selling BTC for USD\",\"payinCurrency\":{\"currencyCode\":\"USD\",\"minAmount\":\"0.0\",\"maxAmount\":\"999999.99\"},\"payoutCurrency\":{\"currencyCode\":\"BTC\",\"maxAmount\":\"999526.11\"},\"payoutUnitsPerPayinUnit\":\"0.00003826\",\"payinMethods\":[{\"kind\":\"DEBIT_CARD\",\"requiredPaymentDetails\":{\"$schema\":\"http://json-schema.org/draft-07/schema\",\"type\":\"object\",\"properties\":{\"cardNumber\":{\"type\":\"string\",\"description\":\"The 16-digit debit card number\",\"minLength\":16,\"maxLength\":16},\"expiryDate\":{\"type\":\"string\",\"description\":\"The expiry date of the card in MM/YY format\",\"pattern\":\"^(0[1-9]|1[0-2])\\\\/([0-9]{2})$\"},\"cardHolderName\":{\"type\":\"string\",\"description\":\"Name of the cardholder as it appears on the card\"},\"cvv\":{\"type\":\"string\",\"description\":\"The 3-digit CVV code\",\"minLength\":3,\"maxLength\":3}},\"required\":[\"cardNumber\",\"expiryDate\",\"cardHolderName\",\"cvv\"],\"additionalProperties\":false}}],\"payoutMethods\":[{\"kind\":\"BTC_ADDRESS\",\"requiredPaymentDetails\":{\"$schema\":\"http://json-schema.org/draft-07/schema\",\"type\":\"object\",\"properties\":{\"btcAddress\":{\"type\":\"string\",\"description\":\"your Bitcoin wallet address\"}},\"required\":[\"btcAddress\"],\"additionalProperties\":false}}],\"requiredClaims\":{\"id\":\"7ce4004c-3c38-4853-968b-e411bafcd945\",\"input_descriptors\":[{\"id\":\"bbdb9b7c-5754-4f46-b63b-590bada959e0\",\"constraints\":{\"fields\":[{\"path\":[\"$.type\"],\"filter\":{\"type\":\"string\",\"const\":\"YoloCredential\"}}]}}]}},\"signature\":\"eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpkaHQ6NzdlbTFmOTY4YzFnendycmIxNWNna3pqeGc4cmZ0NjdlYnhqNmdqa29jbno1cDhzZG5peSMwIn0..puQwdTvi4KTfKedA6CXdHHldztoQ8udUrQrGmw1wvWfYW3ilMB8myoD3ATw7NGlt1NuizJ80i4ufZArgGrTiAA\"}" }