diff --git a/httpclient/src/test/kotlin/tbdex/sdk/httpclient/E2ETest.kt b/httpclient/src/test/kotlin/tbdex/sdk/httpclient/E2ETest.kt index 9247b9b1..db334b8f 100644 --- a/httpclient/src/test/kotlin/tbdex/sdk/httpclient/E2ETest.kt +++ b/httpclient/src/test/kotlin/tbdex/sdk/httpclient/E2ETest.kt @@ -12,6 +12,9 @@ import tbdex.sdk.protocol.models.Rfq import tbdex.sdk.protocol.models.RfqData import tbdex.sdk.protocol.models.SelectedPayinMethod import tbdex.sdk.protocol.models.SelectedPayoutMethod +import tbdex.sdk.protocol.models.UnhashedRfqData +import tbdex.sdk.protocol.models.UnhashedSelectedPayinMethod +import tbdex.sdk.protocol.models.UnhashedSelectedPayoutMethod import web5.sdk.credentials.VerifiableCredential import web5.sdk.crypto.InMemoryKeyManager import web5.sdk.crypto.JwaCurve @@ -254,14 +257,14 @@ class E2ETest { private fun buildRfqData( firstOfferingId: String, vcJwt: String - ) = RfqData( + ) = UnhashedRfqData( offeringId = firstOfferingId, - payin = SelectedPayinMethod( + payin = UnhashedSelectedPayinMethod( kind = "NGN_ADDRESS", paymentDetails = mapOf("walletAddress" to "ngn-wallet-address"), amount = "1.00" ), - payout = SelectedPayoutMethod( + payout = UnhashedSelectedPayoutMethod( kind = "BANK_Access Bank", paymentDetails = mapOf( "accountNumber" to "1234567890", diff --git a/httpclient/src/test/kotlin/tbdex/sdk/httpclient/TestData.kt b/httpclient/src/test/kotlin/tbdex/sdk/httpclient/TestData.kt index f92503fe..e74d4de9 100644 --- a/httpclient/src/test/kotlin/tbdex/sdk/httpclient/TestData.kt +++ b/httpclient/src/test/kotlin/tbdex/sdk/httpclient/TestData.kt @@ -15,6 +15,9 @@ import tbdex.sdk.protocol.models.Rfq import tbdex.sdk.protocol.models.RfqData import tbdex.sdk.protocol.models.SelectedPayinMethod import tbdex.sdk.protocol.models.SelectedPayoutMethod +import tbdex.sdk.protocol.models.UnhashedRfqData +import tbdex.sdk.protocol.models.UnhashedSelectedPayinMethod +import tbdex.sdk.protocol.models.UnhashedSelectedPayoutMethod import web5.sdk.credentials.VcDataModel import web5.sdk.credentials.VerifiableCredential import web5.sdk.credentials.model.ConstraintsV2 @@ -82,10 +85,10 @@ object TestData { val rfq = Rfq.create( to = to, from = ALICE_DID.uri, - rfqData = RfqData( + unhashedRfqData = UnhashedRfqData( offeringId = offeringId, - payin = SelectedPayinMethod("BTC_ADDRESS", mapOf("address" to 123456), amount = "10.00"), - payout = SelectedPayoutMethod("MOMO", mapOf("phone_number" to 123456)), + payin = UnhashedSelectedPayinMethod("BTC_ADDRESS", mapOf("address" to 123456), amount = "10.00"), + payout = UnhashedSelectedPayoutMethod("MOMO", mapOf("phone_number" to 123456)), claims = claims ) ) diff --git a/httpserver/src/test/kotlin/TestData.kt b/httpserver/src/test/kotlin/TestData.kt index ca01de44..9c6c6be5 100644 --- a/httpserver/src/test/kotlin/TestData.kt +++ b/httpserver/src/test/kotlin/TestData.kt @@ -13,6 +13,9 @@ import tbdex.sdk.protocol.models.Rfq import tbdex.sdk.protocol.models.RfqData import tbdex.sdk.protocol.models.SelectedPayinMethod import tbdex.sdk.protocol.models.SelectedPayoutMethod +import tbdex.sdk.protocol.models.UnhashedRfqData +import tbdex.sdk.protocol.models.UnhashedSelectedPayinMethod +import tbdex.sdk.protocol.models.UnhashedSelectedPayoutMethod import web5.sdk.crypto.InMemoryKeyManager import web5.sdk.dids.methods.dht.DidDht import java.time.OffsetDateTime @@ -26,14 +29,14 @@ object TestData { return Rfq.create( to = pfiDid.uri, from = aliceDid.uri, - rfqData = RfqData( + unhashedRfqData = UnhashedRfqData( offeringId = offering?.metadata?.id ?: TypeId.generate("offering").toString(), - payin = SelectedPayinMethod( + payin = UnhashedSelectedPayinMethod( kind = offering?.data?.payin?.methods?.first()?.kind ?: "DEBIT_CARD", paymentDetails = mapOf("foo" to "bar"), amount = "1.00" ), - payout = SelectedPayoutMethod( + payout = UnhashedSelectedPayoutMethod( kind = offering?.data?.payout?.methods?.first()?.kind ?: "BTC_ADDRESS", paymentDetails = mapOf("foo" to "bar") ), diff --git a/protocol/src/main/kotlin/tbdex/sdk/protocol/models/MessageData.kt b/protocol/src/main/kotlin/tbdex/sdk/protocol/models/MessageData.kt index 12b3dfbc..1a58809f 100644 --- a/protocol/src/main/kotlin/tbdex/sdk/protocol/models/MessageData.kt +++ b/protocol/src/main/kotlin/tbdex/sdk/protocol/models/MessageData.kt @@ -15,15 +15,56 @@ sealed interface MessageData : Data * @property offeringId Offering which Alice would like to get a quote for * @property payin selected payin amount, method, and details * @property payout selected payout method, and details - * @property claims an array of claims that fulfill the requirements declared in the referenced Offering + * @property claimsHashes an array of hashes claims that fulfill the requirements declared in the referenced Offering */ class RfqData( val offeringId: String, val payin: SelectedPayinMethod, val payout: SelectedPayoutMethod, - val claims: List + val claimsHashes: List ) : MessageData +/** + * Private data contained in a RFQ message, including data which will be placed in {@link RfqPrivateData} + * + * @property salt Randomly generated cryptographic salt used to hash privateData fields + * @property payin A container for the unhashed `payin.paymentDetails` + * @property payout A container for the unhashed `payout.paymentDetails` + * @property claims claims that fulfill the requirements declared in an Offering + */ +class RfqPrivateData( + val salt: String, + val payin: PrivatePaymentDetails? = null, + val payout: PrivatePaymentDetails? = null, + val claims: List? = null +) + +/** + * Data contained in a RFQ message, including data which will be placed in Rfq.privateDAta. + * Used for creating an RFQ. + * + * @property offeringId Offering which Alice would like to get a quote for + * @property payin selected payin amount, method, and unhashed payment details + * @property payout selected payout method, and unhashed payment details + * @property claims an array of hashes claims that fulfill the requirements declared in the referenced Offering + */ +class UnhashedRfqData( + val offeringId: String, + val payin: UnhashedSelectedPayinMethod, + val payout: UnhashedSelectedPayoutMethod, + val claims: List +) + +/** + * A container for the unhashed `paymentDetails` + * + * @property paymentDetails An object containing the properties defined in the + * respective Offering's requiredPaymentDetails json schema. + */ +class PrivatePaymentDetails( + val paymentDetails: Map? = null +) + /** * A data class representing the payment method selected. * @@ -33,35 +74,70 @@ class RfqData( */ sealed class SelectedPaymentMethod( val kind: String, - val paymentDetails: Map? = null + val paymentDetailsHash: String? = null ) : MessageData /** - * A data class representing the payin method selected. - * + * A data class representing the payment method selected, including the unhashed payment details. + * Used for creating an RFQ. * @property kind type of payment method * @property paymentDetails An object containing the properties * defined in an Offering's requiredPaymentDetails json schema + */ +sealed class UnhashedSelectedPaymentMethod( + val kind: String, + val paymentDetails: Map? = null +) + +/** + * A data class representing the payin method selected. + * + * @property kind type of payment method + * @property paymentDetailsHash An object containing the properties + * defined in an Offering's requiredPaymentDetails json schema * @property amount Amount of currency Alice wants to pay in exchange for payout currency */ class SelectedPayinMethod( kind: String, - paymentDetails: Map? = null, + paymentDetailsHash: String? = null, val amount: String -) : SelectedPaymentMethod(kind, paymentDetails) +) : SelectedPaymentMethod(kind, paymentDetailsHash) +/** + * A data class representing the payin method selected, including the unhashed payin details. + * @property kind type of payment method + * @property paymentDetails An object containing the properties + * defined in an Offering's requiredPaymentDetails json schema + * @property amount Amount of currency Alice wants to pay in exchange for payout currency + */ +class UnhashedSelectedPayinMethod( + kind: String, + paymentDetails: Map? = null, + val amount: String +) : UnhashedSelectedPaymentMethod(kind, paymentDetails) /** * A data class representing the payout method selected. * * @property kind type of payment method - * @property paymentDetails An object containing the properties + * @property paymentDetailsHash An object containing the properties * defined in an Offering's requiredPaymentDetails json schema */ class SelectedPayoutMethod( kind: String, - paymentDetails: Map? = null -) : SelectedPaymentMethod(kind, paymentDetails) + paymentDetailsHash: String? = null +) : SelectedPaymentMethod(kind, paymentDetailsHash) + +/** + * A data class representing the payin method selected, including the unhashed payout details. + * @property kind type of payment method + * @property paymentDetails An object containing the properties + * defined in an Offering's requiredPaymentDetails json schema + */ +class UnhashedSelectedPayoutMethod( + kind: String, + paymentDetails: Map? = null, +) : UnhashedSelectedPaymentMethod(kind, paymentDetails) /** * A data class implementing [MessageData] that represents the contents of a [Quote]. diff --git a/protocol/src/main/kotlin/tbdex/sdk/protocol/models/Rfq.kt b/protocol/src/main/kotlin/tbdex/sdk/protocol/models/Rfq.kt index 2c407da9..5778d82a 100644 --- a/protocol/src/main/kotlin/tbdex/sdk/protocol/models/Rfq.kt +++ b/protocol/src/main/kotlin/tbdex/sdk/protocol/models/Rfq.kt @@ -2,12 +2,16 @@ package tbdex.sdk.protocol.models import com.fasterxml.jackson.databind.JsonNode import de.fxlae.typeid.TypeId +import org.erdtman.jcs.JsonCanonicalizer import tbdex.sdk.protocol.Validator import tbdex.sdk.protocol.models.Close.Companion.create import tbdex.sdk.protocol.models.Rfq.Companion.create import tbdex.sdk.protocol.serialization.Json +import web5.sdk.common.Convert import web5.sdk.credentials.PresentationExchange import web5.sdk.credentials.model.PresentationDefinitionV2 +import java.security.MessageDigest +import java.security.SecureRandom import java.time.OffsetDateTime /** @@ -29,8 +33,8 @@ import java.time.OffsetDateTime class Rfq private constructor( override val metadata: MessageMetadata, override val data: RfqData, - private: Map? = null, - override var signature: String? = null + val privateData: RfqPrivateData? = null, + override var signature: String? = null, ) : Message() { override val validNext: Set = setOf(MessageKind.quote, MessageKind.close) @@ -39,6 +43,8 @@ class Rfq private constructor( * * @param offering The offering to evaluate this Rfq against. * @throws Exception if the Rfq doesn't satisfy the Offering's requirements + * @throws Exception if Rfq.privateData is necessary to satisfy the Offering's requirements + * and the respective privateData property is not present */ fun verifyOfferingRequirements(offering: Offering) { require(data.offeringId == offering.metadata.id) @@ -49,17 +55,38 @@ class Rfq private constructor( if (offering.data.payin.max != null) check(offering.data.payin.max.toDouble() >= data.payin.amount.toDouble()) - validatePaymentMethod(data.payin, offering.data.payin.methods) - validatePaymentMethod(data.payout, offering.data.payout.methods) + validatePaymentMethod( + data.payin.kind, + data.payin.paymentDetailsHash, + privateData?.payin?.paymentDetails, + offering.data.payin.methods + ) + validatePaymentMethod( + data.payout.kind, + data.payout.paymentDetailsHash, + privateData?.payout?.paymentDetails, + offering.data.payout.methods + ) offering.data.requiredClaims?.let { verifyClaims(it) } } - private fun validatePaymentMethod(selectedMethod: SelectedPaymentMethod, offeredMethods: List) { - val matchedOfferingMethod = offeredMethods.first { it.kind == selectedMethod.kind } + private fun validatePaymentMethod( + selectedMethodKind: String, + selectedMethodDetailsHash: String?, + selectedMethodDetails: Map?, + offeredMethods: List + ) { + val matchedOfferingMethod = offeredMethods.first { it.kind == selectedMethodKind } matchedOfferingMethod.requiredPaymentDetails?.let { val schema = matchedOfferingMethod.getRequiredPaymentDetailsSchema() - val jsonNodePaymentDetails = Json.jsonMapper.valueToTree(selectedMethod.paymentDetails) + + if (schema == null && selectedMethodDetailsHash == null) { + // If requiredPaymentDetails is omitted, and paymentDetails is also omitted, we have a match + return + } + + val jsonNodePaymentDetails = Json.jsonMapper.valueToTree(selectedMethodDetails) schema?.validate(jsonNodePaymentDetails) } } @@ -68,31 +95,119 @@ class Rfq private constructor( // TODO check that VCs satisfying PD are crypto verified try { - PresentationExchange.satisfiesPresentationDefinition(data.claims, requiredClaims) + PresentationExchange.satisfiesPresentationDefinition( + privateData?.claims ?: emptyList(), + requiredClaims + ) } catch (e: Exception) { throw IllegalArgumentException("No matching claim for Offering requirements: ${requiredClaims.id}") } } + /** + * Verify the presence and integrity of all possible properties in Rfq.privateData + * @throws Exception if there are properties missing in Rfq.privateData or which do not match the corresponding + * hashed property in Rfq.data + */ + fun verifyAllPrivateData() { + privateData ?: throw Error("privateData property is missing") + + // Verify payin details + data.payin.paymentDetailsHash?.let { + verifyPayinDetailsHash() + } + + // Verify payout details + data.payout.paymentDetailsHash?.let { + verifyPayoutDetailsHash() + } + + // Verify claims + if (data.claimsHashes.isNotEmpty()) { + verifyClaimsHashes() + } + } + + /** + * Verify the integrity properties that are present in + * @throws Exception if there are properties present in Rfq.privateData which do not match the corresponding + * hashed property in Rfq.data + */ + fun verifyPresentPrivateData() { + privateData ?: throw Error("privateData property is missing") + + // Verify payin details + if (data.payin.paymentDetailsHash != null && privateData.payin?.paymentDetails != null) { + verifyPayinDetailsHash() + } + + // Verify payout details + if (data.payout.paymentDetailsHash != null && privateData.payout?.paymentDetails != null) { + verifyPayoutDetailsHash() + } + + // Verify claims + if (data.claimsHashes.isNotEmpty() && !privateData.claims.isNullOrEmpty()) { + verifyClaimsHashes() + } + } + + private fun verifyPayinDetailsHash() { + val salt = privateData?.salt ?: throw Error("Salt must be present to verify data.payin.paymentDetailsHash") + val digest = privateData.payin?.paymentDetails?.let { digestPrivateData(salt, it) } + + if (digest != data.payin.paymentDetailsHash) { + throw Error( + "Private data integrity check failed: " + + "data.payin.paymentDetailsHash does not match digest of privateData.payin.paymentDetails" + ) + } + } + + private fun verifyPayoutDetailsHash() { + val salt = privateData?.salt ?: throw Error("Salt must be present to verify data.payout.paymentDetailsHash") + val digest = privateData.payout?.paymentDetails?.let { digestPrivateData(salt, it) } + + if (digest != data.payout.paymentDetailsHash) { + throw Error( + "Private data integrity check failed: " + + "data.payout.paymentDetailsHash does not match digest of privateData.payout.paymentDetails" + ) + } + } + + private fun verifyClaimsHashes() { + val salt = privateData?.salt ?: throw Error("Salt must be present to verify data.claimsHashes") + + data.claimsHashes.forEachIndexed { index, claimsHash -> + val claim = privateData.claims?.getOrNull(index) + val digest = digestPrivateData(salt, claim ?: throw Error("Claim at index $index is missing")) + if (digest != claimsHash) { + throw Error( + "Private data integrity check failed: " + + "data.claimsHashes[$index] does not match digest of privateData.claims[$index]" + ) + } + } + } + companion object { /** * Creates a new `Rfq` message, autopopulating the id, creation time, and message kind. * * @param to DID that the message is being sent to. * @param from DID of the sender. - * @param rfqData Specific parameters relevant to a Rfq. + * @param unhashedRfqData Specific parameters relevant to a Rfq. * @param protocol version of the tbdex protocol. * @param externalId external reference for the Rfq. Optional. - * @param private Sensitive information that will be ephemeral. * @return Rfq instance. */ fun create( to: String, from: String, - rfqData: RfqData, + unhashedRfqData: UnhashedRfqData, protocol: String = "1.0", externalId: String? = null, - private: Map? = null ): Rfq { val id = TypeId.generate(MessageKind.rfq.name).toString() val metadata = MessageMetadata( @@ -105,12 +220,91 @@ class Rfq private constructor( protocol = protocol, externalId = externalId ) - Validator.validateData(rfqData, "rfq") - // TODO: hash `data.payinMethod.paymentDetails` and set `private` - // TODO: hash `data.payoutMethod.paymentDetails` and set `private` + val (data, privateData) = hashPrivateData(unhashedRfqData) + + Validator.validateData(data, "rfq") + + return Rfq(metadata, data, privateData) + } + + /** + * Takes an existing Message in the form of a json string and parses it into a Message object. + * Validates object structure and performs an integrity check using the message signature. + * + * @param payload The message as a json string. + * @param requireAllPrivateData If true, validate that all private data properties are present and run integrity + * check. + * Otherwise, only check integrity of private fields which are present. + * If false or omitted, validate only the private data properties that are + * currently present in `privateData` + * @return The json string parsed into a Rfq. + * @throws IllegalArgumentException if the payload is not valid json. + * @throws IllegalArgumentException if the payload does not conform to the expected json schema. + * @throws IllegalArgumentException if the payload signature verification fails. + * @throws Exception if Rfq.privateData does not match Rfq.data + */ + fun parse(payload: String, requireAllPrivateData: Boolean = false): Rfq { + // TODO: Ensure that Message.parse() also validates private data + val rfq = Message.parse(payload) as Rfq + + if (requireAllPrivateData) { + rfq.verifyAllPrivateData() + } else { + rfq.verifyPresentPrivateData() + } - return Rfq(metadata, rfqData, private) + return rfq + } + + private fun hashPrivateData(unhashedRfqData: UnhashedRfqData): Pair { + val salt = generateRandomSalt() + + val payinPaymentDetailsHash = unhashedRfqData.payin.paymentDetails?.let { digestPrivateData(salt, it) } + val payoutPaymentDetailsHash = unhashedRfqData.payout.paymentDetails?.let { digestPrivateData(salt, it) } + val claimsHashes = unhashedRfqData.claims.map { claim -> digestPrivateData(salt, mapOf("claim" to claim)) } + + val hashedRfqData = RfqData( + offeringId = unhashedRfqData.offeringId, + payin = SelectedPayinMethod( + kind = unhashedRfqData.payin.kind, + paymentDetailsHash = payinPaymentDetailsHash, + amount = unhashedRfqData.payin.amount + ), + payout = SelectedPayoutMethod( + kind = unhashedRfqData.payout.kind, + paymentDetailsHash = payoutPaymentDetailsHash + ), + claimsHashes = claimsHashes + ) + + val privateRfqData = RfqPrivateData( + salt = salt, + payin = PrivatePaymentDetails(unhashedRfqData.payin.paymentDetails), + payout = PrivatePaymentDetails(unhashedRfqData.payout.paymentDetails), + claims = unhashedRfqData.claims + ) + + return Pair(hashedRfqData, privateRfqData) + } + + private fun digestPrivateData(salt: String, value: Any): String { + val payload = arrayOf(salt, value) + val canonicalJsonSerializedPayload = JsonCanonicalizer(Json.stringify(payload)) + val sha256 = MessageDigest.getInstance("SHA-256") + val hash = sha256.digest(canonicalJsonSerializedPayload.encodedUTF8) + return Convert(hash).toBase64Url(padding = false) + } + + /** + * Generate random salt, used for salted hashes in RfqPrivateData + */ + fun generateRandomSalt(): String { + val byteArraySize = 16 + val secureRandom = SecureRandom() + val byteArray = ByteArray(byteArraySize) + secureRandom.nextBytes(byteArray) + return Convert(byteArray).toString() } } } \ No newline at end of file diff --git a/protocol/src/test/kotlin/tbdex/sdk/protocol/TbdexTestVectorsProtocol.kt b/protocol/src/test/kotlin/tbdex/sdk/protocol/TbdexTestVectorsProtocol.kt index a670ac97..3bab9f08 100644 --- a/protocol/src/test/kotlin/tbdex/sdk/protocol/TbdexTestVectorsProtocol.kt +++ b/protocol/src/test/kotlin/tbdex/sdk/protocol/TbdexTestVectorsProtocol.kt @@ -56,6 +56,13 @@ class TbdexTestVectorsProtocol { testSuccessMessageTestVector(vector) } +// @Test +// fun parse_rfq_omit_private_data() { +// val vector = TestVectors.getVector("parse-rfq-omit-private-data.json") +// assertNotNull(vector) +// testSuccessMessageTestVector(vector) +// } + /** * Tbdex Test Vectors Resource Tests */ diff --git a/protocol/src/test/kotlin/tbdex/sdk/protocol/TestData.kt b/protocol/src/test/kotlin/tbdex/sdk/protocol/TestData.kt index 317396cb..8c631de0 100644 --- a/protocol/src/test/kotlin/tbdex/sdk/protocol/TestData.kt +++ b/protocol/src/test/kotlin/tbdex/sdk/protocol/TestData.kt @@ -23,6 +23,9 @@ import tbdex.sdk.protocol.models.Rfq import tbdex.sdk.protocol.models.RfqData import tbdex.sdk.protocol.models.SelectedPayinMethod import tbdex.sdk.protocol.models.SelectedPayoutMethod +import tbdex.sdk.protocol.models.UnhashedRfqData +import tbdex.sdk.protocol.models.UnhashedSelectedPayinMethod +import tbdex.sdk.protocol.models.UnhashedSelectedPayoutMethod import web5.sdk.credentials.VcDataModel import web5.sdk.credentials.VerifiableCredential import web5.sdk.credentials.model.ConstraintsV2 @@ -96,10 +99,10 @@ object TestData { ) = Rfq.create( to = PFI_DID.uri, from = ALICE_DID.uri, - rfqData = RfqData( + unhashedRfqData = UnhashedRfqData( offeringId = offeringId, - payin = SelectedPayinMethod("BTC_ADDRESS", mapOf("address" to "123456"), amount = "10.00"), - payout = SelectedPayoutMethod( + payin = UnhashedSelectedPayinMethod("BTC_ADDRESS", mapOf("address" to "123456"), amount = "10.00"), + payout = UnhashedSelectedPayoutMethod( "MOMO", mapOf( "phoneNumber" to "+254712345678", "accountHolderName" to "Alfred Holder" diff --git a/protocol/src/test/kotlin/tbdex/sdk/protocol/TestVectors.kt b/protocol/src/test/kotlin/tbdex/sdk/protocol/TestVectors.kt index eba4ca45..2de32ff8 100644 --- a/protocol/src/test/kotlin/tbdex/sdk/protocol/TestVectors.kt +++ b/protocol/src/test/kotlin/tbdex/sdk/protocol/TestVectors.kt @@ -16,6 +16,7 @@ object TestVectors { "parse-orderstatus.json", "parse-quote.json", "parse-rfq.json", + "parse-rfq-omit-private-data.json", "parse-balance.json" ) for (vectorFile in vectorFiles) { diff --git a/protocol/src/test/kotlin/tbdex/sdk/protocol/ValidatorTest.kt b/protocol/src/test/kotlin/tbdex/sdk/protocol/ValidatorTest.kt index 9a366f10..f2a4403f 100644 --- a/protocol/src/test/kotlin/tbdex/sdk/protocol/ValidatorTest.kt +++ b/protocol/src/test/kotlin/tbdex/sdk/protocol/ValidatorTest.kt @@ -73,18 +73,31 @@ class ValidatorTest { val stringRfqWithoutPayinAmount = """ { "metadata": { - "from": "did:key:z6MkpkvGVrxxTVbo56mvbSiF6iCKNev56wqoMcHHowqUqvKQ", - "to": "did:ex:pfi", + "from": "did:dht:hybk1aqkk1gyf3xf65n4i5i8eejuf1i8z815rdu151iyyas5kgqo", + "to": "did:dht:zg49uprcdsnhcis5upqzz691pni3o7oisnih3eosxhxgngemj73y", + "protocol": "1.0", "kind": "rfq", - "id": "rfq_01hkx53kgafbmrg2xp87n5htfb", - "exchangeId": "rfq_01hkx53kgafbmrg2xp87n5htfb", - "createdAt": "2024-01-11T20:58:34.378Z", - "protocol": "1.0" + "id": "rfq_01ht15gcytfmmvmrsecezv1wtj", + "exchangeId": "rfq_01ht15gcytfmmvmrsecezv1wtj", + "createdAt": "2024-03-27T23:56:42.330Z" }, "data": { - "offeringId": "abcd123", - "payinMethod": { + "offeringId": "offering_01ht15gcytfmmvmrsec8sycmxv", + "payin": { "kind": "DEBIT_CARD", + "paymentDetailsHash": "YhnSs26y0oj3YrzajVQWQoUTwOReENRZgXyVyCs_ON4" + }, + "payout": { + "kind": "BTC_ADDRESS", + "paymentDetailsHash": "wtwAUlYVbMfGHVJ-F6VlAqpD4TwJpM8ep5Rv4JNNRCI" + }, + "claimsHashes": [ + "iaJKmdBSPJ3rz9U1laNbZDgvNNZER3TTy8DqRqSCbdc" + ] + }, + "privateData": { + "salt": "123", + "payin": { "paymentDetails": { "cardNumber": "1234567890123456", "expiryDate": "12/22", @@ -92,14 +105,13 @@ class ValidatorTest { "cvv": "123" } }, - "payoutMethod": { - "kind": "BTC_ADDRESS", + "payout": { "paymentDetails": { "btcAddress": "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa" } }, "claims": [ - "" + "eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpkaHQ6aHliazFhcWtrMWd5ZjN4ZjY1bjRpNWk4ZWVqdWYxaTh6ODE1cmR1MTUxaXl5YXM1a2dxbyMwIn0.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiUHV1cHV1Q3JlZGVudGlhbCJdLCJpZCI6InVybjp1dWlkOjlkM2M1OGQ1LTI5NzMtNDI4Zi04YjNkLWY4ZGI1MmIzNGI1MyIsImlzc3VlciI6ImRpZDpkaHQ6aHliazFhcWtrMWd5ZjN4ZjY1bjRpNWk4ZWVqdWYxaTh6ODE1cmR1MTUxaXl5YXM1a2dxbyIsImlzc3VhbmNlRGF0ZSI6IjIwMjQtMDMtMjdUMjM6NTY6NDJaIiwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJkaWQ6ZGh0Omh5YmsxYXFrazFneWYzeGY2NW40aTVpOGVlanVmMWk4ejgxNXJkdTE1MWl5eWFzNWtncW8iLCJiZWVwIjoiYm9vcCJ9fSwiaXNzIjoiZGlkOmRodDpoeWJrMWFxa2sxZ3lmM3hmNjVuNGk1aThlZWp1ZjFpOHo4MTVyZHUxNTFpeXlhczVrZ3FvIiwic3ViIjoiZGlkOmRodDpoeWJrMWFxa2sxZ3lmM3hmNjVuNGk1aThlZWp1ZjFpOHo4MTVyZHUxNTFpeXlhczVrZ3FvIn0.gQMcjzeKdhcDExx1mtrLnA9g_MahmTuJWumgFOSY9TtY1bEDB77ZykaeuYvc0hotJuEmQSng3uecH-lsMuk0BA" ] }, "signature": "blah" @@ -113,6 +125,6 @@ class ValidatorTest { } assertEquals(1, exception.errors.size) - assertThat(exception.errors).contains("$.payinAmount: is missing but it is required") + assertThat(exception.errors).contains("$.payin.amount: is missing but it is required") } } diff --git a/protocol/src/test/kotlin/tbdex/sdk/protocol/models/RfqTest.kt b/protocol/src/test/kotlin/tbdex/sdk/protocol/models/RfqTest.kt index 69a13079..766acc6c 100644 --- a/protocol/src/test/kotlin/tbdex/sdk/protocol/models/RfqTest.kt +++ b/protocol/src/test/kotlin/tbdex/sdk/protocol/models/RfqTest.kt @@ -19,10 +19,10 @@ class RfqTest { val rfq = Rfq.create( to = TestData.PFI, from = TestData.ALICE, - rfqData = RfqData( + unhashedRfqData = UnhashedRfqData( offeringId = TypeId.generate(ResourceKind.offering.name).toString(), - payin = SelectedPayinMethod("BTC_ADDRESS", mapOf("address" to 123456), amount = "10.00"), - payout = SelectedPayoutMethod("MOMO", mapOf("phone_number" to 123456)), + payin = UnhashedSelectedPayinMethod("BTC_ADDRESS", mapOf("address" to 123456), amount = "10.00"), + payout = UnhashedSelectedPayoutMethod("MOMO", mapOf("phone_number" to 123456)), claims = emptyList() ), externalId = "P_12345" diff --git a/tbdex b/tbdex index 6af20c9d..ac3e3e7d 160000 --- a/tbdex +++ b/tbdex @@ -1 +1 @@ -Subproject commit 6af20c9d52a1497bef72ee88f712496844edd372 +Subproject commit ac3e3e7ddee041fe619ec2084211de9dc98884be