From 428c714efdfbdb93d2f7f169d8c3e7deeeac1bba Mon Sep 17 00:00:00 2001 From: Diane Huxley Date: Fri, 29 Mar 2024 10:32:05 -0700 Subject: [PATCH] WIP hash Rfq privateData --- .../kotlin/tbdex/sdk/httpclient/E2ETest.kt | 9 +- .../kotlin/tbdex/sdk/httpclient/TestData.kt | 9 +- httpserver/src/test/kotlin/TestData.kt | 9 +- .../tbdex/sdk/protocol/models/MessageData.kt | 100 +++++++- .../kotlin/tbdex/sdk/protocol/models/Rfq.kt | 227 ++++++++++++++++-- .../sdk/protocol/TbdexTestVectorsProtocol.kt | 7 + .../kotlin/tbdex/sdk/protocol/TestData.kt | 9 +- .../kotlin/tbdex/sdk/protocol/TestVectors.kt | 1 + .../tbdex/sdk/protocol/ValidatorTest.kt | 29 ++- .../tbdex/sdk/protocol/models/RfqTest.kt | 6 +- tbdex | 2 +- 11 files changed, 355 insertions(+), 53 deletions(-) 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 c9ad7fc5..f35e3ef1 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 claimsHash hash of 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 claimsHash: String? = null ) : 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. - * - * @property kind type of payin method + * 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 A hash of the 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 payout method - * @property paymentDetails An object containing the properties + * @property kind type of payment method + * @property paymentDetailsHash A hash of the 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..2f60dceb 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,116 @@ 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.claimsHash.isNullOrEmpty()) { + verifyClaimsHash() + } + } + + /** + * 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.claimsHash.isNullOrEmpty() && !privateData.claims.isNullOrEmpty()) { + verifyClaimsHash() + } + } + + 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 verifyClaimsHash() { + val salt = privateData?.salt ?: throw Error("Salt must be present to verify data.claimsHash") + + val digest = digestPrivateData(salt, privateData.claims ?: throw Error("privateData.claims is missing")) + if (digest != data.claimsHash) { + throw Error( + "Private data integrity check failed: " + + "data.claimsHash does not match digest of privateData.claims" + ) + } + } + 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 +217,95 @@ 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 + } + + 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 claimsHash = if (unhashedRfqData.claims.isEmpty()) { + null + } else { + digestPrivateData(salt, unhashedRfqData.claims) + } + + 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 + ), + claimsHash = claimsHash + ) + + val privateRfqData = RfqPrivateData( + salt = salt, + payin = PrivatePaymentDetails(unhashedRfqData.payin.paymentDetails), + payout = PrivatePaymentDetails(unhashedRfqData.payout.paymentDetails), + claims = unhashedRfqData.claims + ) + + return Pair(hashedRfqData, privateRfqData) + } - return Rfq(metadata, rfqData, private) + 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..dee1ad72 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 2bb51f81..022376c6 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 95f5db81..4c3366ce 100644 --- a/protocol/src/test/kotlin/tbdex/sdk/protocol/ValidatorTest.kt +++ b/protocol/src/test/kotlin/tbdex/sdk/protocol/ValidatorTest.kt @@ -73,18 +73,30 @@ class ValidatorTest { val stringRfqWithoutPayinAmount = """ { "metadata": { - "from": "did:key:z6MkpkvGVrxxTVbo56mvbSiF6iCKNev56wqoMcHHowqUqvKQ", - "to": "did:ex:pfi", + "from": "did:dht:9kkuh34q7nkd4tphbcg7py9h1g16iftbtskesi9courdwj96q3sy", + "to": "did:dht:8rqqxczxhdugndj5mykiahy7y4zg4zkk3jajr6qyo5owdrgqqx3y", + "protocol": "1.0", "kind": "rfq", - "id": "rfq_01hkx53kgafbmrg2xp87n5htfb", - "exchangeId": "rfq_01hkx53kgafbmrg2xp87n5htfb", - "createdAt": "2024-01-11T20:58:34.378Z", - "protocol": "1.0" + "id": "rfq_01ht6fh3fsf9z8ekh5tyjbsmwk", + "exchangeId": "rfq_01ht6fh3fsf9z8ekh5tyjbsmwk", + "createdAt": "2024-03-30T01:28:03.321Z" }, "data": { - "offeringId": "abcd123", + "offeringId": "offering_01ht6fh3fre7t8zfc6j97t2pe1", "payin": { "kind": "DEBIT_CARD", + "amount": "20000.00", + "paymentDetailsHash": "k8nU7MKYNb140u3pN8FO5ndAK3WDJ8lQDEsYA0H9fWA" + }, + "payout": { + "kind": "BTC_ADDRESS", + "paymentDetailsHash": "xwyJqXOcLsYBBojNlVHZGZzlhZvswFPUh3xsLxus_EU" + }, + "claimsHash": "o16anc251kJb3uyYKzPdlS5SdhGomylHZ8_YZ7CBbgU" + }, + "privateData": { + "salt": "�>�����\u0014�]�NɈ", + "payin": { "paymentDetails": { "cardNumber": "1234567890123456", "expiryDate": "12/22", @@ -93,13 +105,12 @@ class ValidatorTest { } }, "payout": { - "kind": "BTC_ADDRESS", "paymentDetails": { "btcAddress": "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa" } }, "claims": [ - "" + "eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpkaHQ6OWtrdWgzNHE3bmtkNHRwaGJjZzdweTloMWcxNmlmdGJ0c2tlc2k5Y291cmR3ajk2cTNzeSMwIn0.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiUHV1cHV1Q3JlZGVudGlhbCJdLCJpZCI6InVybjp1dWlkOmY5YWVlNjdjLTQ1NmUtNGU1Yy05NDg3LWM3MjM2ZmQyMWUxNiIsImlzc3VlciI6ImRpZDpkaHQ6OWtrdWgzNHE3bmtkNHRwaGJjZzdweTloMWcxNmlmdGJ0c2tlc2k5Y291cmR3ajk2cTNzeSIsImlzc3VhbmNlRGF0ZSI6IjIwMjQtMDMtMzBUMDE6Mjg6MDNaIiwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJkaWQ6ZGh0Ojlra3VoMzRxN25rZDR0cGhiY2c3cHk5aDFnMTZpZnRidHNrZXNpOWNvdXJkd2o5NnEzc3kiLCJiZWVwIjoiYm9vcCJ9fSwiaXNzIjoiZGlkOmRodDo5a2t1aDM0cTdua2Q0dHBoYmNnN3B5OWgxZzE2aWZ0YnRza2VzaTljb3VyZHdqOTZxM3N5Iiwic3ViIjoiZGlkOmRodDo5a2t1aDM0cTdua2Q0dHBoYmNnN3B5OWgxZzE2aWZ0YnRza2VzaTljb3VyZHdqOTZxM3N5In0.kxFwk-eCpCrO1m7lYAlqEioGjVZ1vlM-DctE52atVKa6Egn-DS0nqmqDcXo_yRmAUylU5E-7lOPsP7N90DbAAA" ] }, "signature": "blah" 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..146ae27b 160000 --- a/tbdex +++ b/tbdex @@ -1 +1 @@ -Subproject commit 6af20c9d52a1497bef72ee88f712496844edd372 +Subproject commit 146ae27bea652cff1cc45bb33d773f6b64c7cf57