Skip to content
This repository has been archived by the owner on Dec 12, 2024. It is now read-only.

Update currency representation #129

Merged
merged 4 commits into from
Jan 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import tbdex.sdk.httpclient.models.Exchange
import tbdex.sdk.httpclient.models.GetExchangesFilter
import tbdex.sdk.httpclient.models.GetOfferingsFilter
import tbdex.sdk.httpclient.models.TbdexResponseException
import tbdex.sdk.protocol.Validator
import tbdex.sdk.protocol.models.Message
import tbdex.sdk.protocol.models.Offering
import tbdex.sdk.protocol.serialization.Json
Expand Down Expand Up @@ -73,6 +74,9 @@ object TbdexHttpClient {
* @throws TbdexResponseException for request or response errors.
*/
fun sendMessage(message: Message) {
Validator.validateMessage(message)
message.verify()

val pfiDid = message.metadata.to
val exchangeId = message.metadata.exchangeId
val kind = message.metadata.kind
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ class E2ETest {
vcJwt: String
) = RfqData(
offeringId = firstOfferingId,
payinSubunits = "100",
payinAmount = "1.00",
payinMethod = SelectedPaymentMethod(
kind = "NGN_ADDRESS",
paymentDetails = mapOf("walletAddress" to "ngn-wallet-address")
Expand Down
51 changes: 30 additions & 21 deletions httpclient/src/test/kotlin/tbdex/sdk/httpclient/TestData.kt
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@ object TestData {
return VerifiableCredential.create("test type", ALICE_DID.uri, ALICE_DID.uri, vc)
}

fun getOffering(requiredClaims: PresentationDefinitionV2 = getPresentationDefinition()) =
Offering.create(
fun getOffering(requiredClaims: PresentationDefinitionV2 = getPresentationDefinition()): Offering {
val offering = Offering.create(
from = PFI_DID.uri,
OfferingData(
description = "my fake offering",
Expand All @@ -70,31 +70,40 @@ object TestData {
requiredClaims = requiredClaims
)
)

offering.sign(PFI_DID)
return offering
}
fun getRfq(
to: String = PFI_DID.uri,
offeringId: TypeId = TypeId.generate(ResourceKind.offering.name),
claims: List<String> = emptyList()
) = Rfq.create(
to = to,
from = ALICE_DID.uri,
rfqData = RfqData(
offeringId = offeringId,
payinSubunits = "1000",
payinMethod = SelectedPaymentMethod("BTC_ADDRESS", mapOf("address" to 123456)),
payoutMethod = SelectedPaymentMethod("MOMO", mapOf("phone_number" to 123456)),
claims = claims
): Rfq {
val rfq = Rfq.create(
to = to,
from = ALICE_DID.uri,
rfqData = RfqData(
offeringId = offeringId,
payinAmount = "10.00",
payinMethod = SelectedPaymentMethod("BTC_ADDRESS", mapOf("address" to 123456)),
payoutMethod = SelectedPaymentMethod("MOMO", mapOf("phone_number" to 123456)),
claims = claims
)
)
)

fun getQuote() = Quote.create(
ALICE_DID.uri, PFI_DID.uri, TypeId.generate(MessageKind.rfq.name),
QuoteData(
expiresAt = OffsetDateTime.now().plusDays(1),
payin = QuoteDetails("AUD", "1000", "0"),
payout = QuoteDetails("BTC", "12", "0")
rfq.sign(ALICE_DID)
return rfq
}
fun getQuote(): Quote {
val quote = Quote.create(
ALICE_DID.uri, PFI_DID.uri, TypeId.generate(MessageKind.rfq.name),
QuoteData(
expiresAt = OffsetDateTime.now().plusDays(1),
payin = QuoteDetails("AUD", "10.00", "0.0"),
payout = QuoteDetails("BTC", "0.12", "0.0")
)
)
)
quote.sign(PFI_DID)
return quote
}

private fun buildField(id: String? = null, vararg paths: String): FieldV2 {
return FieldV2(id = id, path = paths.toList())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import tbdex.sdk.protocol.models.CurrencyDetails
import tbdex.sdk.protocol.models.Offering
import tbdex.sdk.protocol.models.OfferingData
import tbdex.sdk.protocol.models.PaymentMethod
import web5.sdk.credentials.model.InputDescriptorV2
import web5.sdk.credentials.model.PresentationDefinitionV2

/**
* A fake implementation of the [OfferingsApi] interface for testing purposes.
Expand All @@ -20,10 +22,24 @@ class FakeOfferingsApi : OfferingsApi {
payoutUnitsPerPayinUnit = "0.000038",
payinMethods = listOf(PaymentMethod(kind = "DEBIT_CARD")),
payoutMethods = listOf(PaymentMethod(kind = "BTC_ADDRESS")),
requiredClaims = null
requiredClaims = buildPresentationDefinition()
)
)

private fun buildPresentationDefinition(
id: String = "test-pd-id",
name: String = "simple PD",
purpose: String = "pd for testing",
inputDescriptors: List<InputDescriptorV2> = listOf()
): PresentationDefinitionV2 {
return PresentationDefinitionV2(
id = id,
name = name,
purpose = purpose,
inputDescriptors = inputDescriptors
)
}

/**
* Retrieves the offering with the specified ID.
*
Expand Down
6 changes: 3 additions & 3 deletions httpserver/src/test/kotlin/TestData.kt
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ object TestData {
from = aliceDid.uri,
rfqData = RfqData(
offeringId = offering?.metadata?.id ?: TypeId.generate("offering"),
payinSubunits = "100",
payinAmount = "1.00",
payinMethod = SelectedPaymentMethod(
kind = offering?.data?.payinMethods?.first()?.kind ?: "USD",
paymentDetails = mapOf("foo" to "bar")
Expand Down Expand Up @@ -64,8 +64,8 @@ object TestData {
exchangeId = exchangeId,
quoteData = QuoteData(
expiresAt = expiresAt,
payin = QuoteDetails("AUD", "1000", "1"),
payout = QuoteDetails("BTC", "12", "2"),
payin = QuoteDetails("AUD", "10.00", "0.1"),
payout = QuoteDetails("BTC", "0.12", "0.02"),
paymentInstructions = PaymentInstructions(
payin = PaymentInstruction(
link = "https://block.xyz",
Expand Down
30 changes: 30 additions & 0 deletions protocol/src/main/kotlin/tbdex/sdk/protocol/Validator.kt
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
package tbdex.sdk.protocol

import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.ObjectMapper
import com.networknt.schema.JsonSchema
import com.networknt.schema.JsonSchemaFactory
import com.networknt.schema.SchemaValidatorsConfig
import com.networknt.schema.SpecVersion
import tbdex.sdk.protocol.models.Data
import tbdex.sdk.protocol.models.Message
import tbdex.sdk.protocol.models.MessageKind
import tbdex.sdk.protocol.models.ResourceKind
import tbdex.sdk.protocol.serialization.Json
import tbdex.sdk.protocol.serialization.Json.jsonMapper
import java.net.URI

/**
Expand Down Expand Up @@ -70,4 +75,29 @@
throw ValidatorException(message = "invalid payload", errors = validationMessages.map { it.message })
}
}

/**
* Validate a message against a predefined schema
* @param message The message data to validate.
* @throws Exception if validation fails, including a list of validation errors.
*/
fun validateMessage(message: Message) {
val messageJsonNode = jsonMapper.readTree(message.toString())

Check warning on line 85 in protocol/src/main/kotlin/tbdex/sdk/protocol/Validator.kt

View check run for this annotation

Codecov / codecov/patch

protocol/src/main/kotlin/tbdex/sdk/protocol/Validator.kt#L85

Added line #L85 was not covered by tests

validate(messageJsonNode, "message")
validateData(message.data, message.metadata.kind.toString())

Check warning on line 88 in protocol/src/main/kotlin/tbdex/sdk/protocol/Validator.kt

View check run for this annotation

Codecov / codecov/patch

protocol/src/main/kotlin/tbdex/sdk/protocol/Validator.kt#L87-L88

Added lines #L87 - L88 were not covered by tests
}

/**
* Validate message data or resource data against a predefined schema
* @param data The message or resource data to validate.
* @param messageKind The message or resource kind of the data.
* @throws Exception if validation fails, including a list of validation errors.
*/
fun validateData(data: Data, messageKind: String) {
val json = Json.stringify(data)
val jsonNode = jsonMapper.readTree(json)

this.validate(jsonNode, messageKind)
}
}
2 changes: 2 additions & 0 deletions protocol/src/main/kotlin/tbdex/sdk/protocol/models/Close.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package tbdex.sdk.protocol.models

import de.fxlae.typeid.TypeId
import tbdex.sdk.protocol.Validator
import tbdex.sdk.protocol.models.Close.Companion.create
import java.time.OffsetDateTime

Expand Down Expand Up @@ -41,6 +42,7 @@ class Close private constructor(
exchangeId = exchangeId,
createdAt = OffsetDateTime.now()
)
Validator.validateData(closeData, "close")
return Close(metadata, closeData)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ sealed interface MessageData : Data
*/
class RfqData(
val offeringId: TypeId,
val payinSubunits: String,
val payinAmount: String,
val payinMethod: SelectedPaymentMethod,
val payoutMethod: SelectedPaymentMethod,
val claims: List<String>
Expand Down Expand Up @@ -45,8 +45,8 @@ class QuoteData(
*/
class QuoteDetails(
val currencyCode: String,
val amountSubunits: String,
val feeSubunits: String? = null
val amount: String,
val fee: String? = null
)

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package tbdex.sdk.protocol.models

import de.fxlae.typeid.TypeId
import tbdex.sdk.protocol.Validator
import tbdex.sdk.protocol.models.Close.Companion.create
import tbdex.sdk.protocol.models.Offering.Companion.create
import tbdex.sdk.protocol.models.Order.Companion.create
Expand Down Expand Up @@ -47,6 +48,7 @@ class Offering private constructor(
createdAt = now,
updatedAt = now
)
Validator.validateData(data, "offering")

return Offering(metadata, data)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package tbdex.sdk.protocol.models

import de.fxlae.typeid.TypeId
import tbdex.sdk.protocol.Validator
import tbdex.sdk.protocol.models.Close.Companion.create
import tbdex.sdk.protocol.models.Order.Companion.create
import java.time.OffsetDateTime
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package tbdex.sdk.protocol.models

import de.fxlae.typeid.TypeId
import tbdex.sdk.protocol.Validator
import tbdex.sdk.protocol.models.Close.Companion.create
import tbdex.sdk.protocol.models.OrderStatus.Companion.create
import java.time.OffsetDateTime
Expand Down Expand Up @@ -41,6 +42,8 @@ class OrderStatus private constructor(
exchangeId = exchangeId,
createdAt = OffsetDateTime.now()
)
Validator.validateData(orderStatusData, "orderstatus")

return OrderStatus(metadata, orderStatusData)
}
}
Expand Down
2 changes: 2 additions & 0 deletions protocol/src/main/kotlin/tbdex/sdk/protocol/models/Quote.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package tbdex.sdk.protocol.models

import de.fxlae.typeid.TypeId
import tbdex.sdk.protocol.Validator
import tbdex.sdk.protocol.models.Close.Companion.create
import tbdex.sdk.protocol.models.Quote.Companion.create
import java.time.OffsetDateTime
Expand Down Expand Up @@ -46,6 +47,7 @@ class Quote private constructor(
exchangeId = exchangeId,
createdAt = OffsetDateTime.now()
)
Validator.validateData(quoteData, "quote")

return Quote(metadata, quoteData)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ class OfferingData(
*/
class CurrencyDetails(
val currencyCode: String,
val minSubunits: String? = null,
val maxSubunits: String? = null
val minAmount: String? = null,
val maxAmount: String? = null
)

/**
Expand All @@ -45,7 +45,7 @@ class CurrencyDetails(
class PaymentMethod(
val kind: String,
val requiredPaymentDetails: JsonNode? = null,
val feeSubunits: String? = null
val fee: String? = null
) {
/**
* Parse the contents of [requiredPaymentDetails] into a [JsonSchema] that can do validation.
Expand Down
10 changes: 6 additions & 4 deletions protocol/src/main/kotlin/tbdex/sdk/protocol/models/Rfq.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package tbdex.sdk.protocol.models

import com.fasterxml.jackson.databind.JsonNode
import de.fxlae.typeid.TypeId
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
Expand Down Expand Up @@ -38,11 +39,11 @@ class Rfq private constructor(
fun verifyOfferingRequirements(offering: Offering) {
require(data.offeringId == offering.metadata.id)

if (offering.data.payinCurrency.minSubunits != null)
check(offering.data.payinCurrency.minSubunits <= this.data.payinSubunits)
if (offering.data.payinCurrency.minAmount != null)
check(offering.data.payinCurrency.minAmount <= this.data.payinAmount)

if (offering.data.payinCurrency.maxSubunits != null)
check(this.data.payinSubunits <= offering.data.payinCurrency.maxSubunits)
if (offering.data.payinCurrency.maxAmount != null)
check(this.data.payinAmount <= offering.data.payinCurrency.maxAmount)

validatePaymentMethod(data.payinMethod, offering.data.payinMethods)
validatePaymentMethod(data.payoutMethod, offering.data.payoutMethods)
Expand Down Expand Up @@ -94,6 +95,7 @@ class Rfq private constructor(
exchangeId = id,
createdAt = OffsetDateTime.now()
)
Validator.validateData(rfqData, "rfq")

// TODO: hash `data.payinMethod.paymentDetails` and set `private`
// TODO: hash `data.payoutMethod.paymentDetails` and set `private`
Expand Down
4 changes: 4 additions & 0 deletions protocol/src/main/resources/definitions.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
"did": {
"type": "string",
"pattern": "^did:([a-z0-9]+):((?:(?:[a-zA-Z0-9._-]|(?:%[0-9a-fA-F]{2}))*:)*((?:[a-zA-Z0-9._-]|(?:%[0-9a-fA-F]{2}))+))((;[a-zA-Z0-9_.:%-]+=[a-zA-Z0-9_.:%-]*)*)(\/[^#?]*)?([?][^#]*)?(#.*)?$"
},
"decimalString": {
"type": "string",
"pattern": "^([0-9]+[.][0-9]+)$"
diehuxx marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
1 change: 1 addition & 0 deletions protocol/src/main/resources/message.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"definitions": {
"MessageMetadata": {
"type": "object",
"additionalProperties": false,
"properties": {
"from": {
"$ref": "definitions.json#/definitions/did",
Expand Down
Loading
Loading