From cc49bef6ef743fe1f425d22883a34b69df431dff Mon Sep 17 00:00:00 2001 From: Jeremy Klein Date: Sat, 21 Oct 2023 00:26:54 -0700 Subject: [PATCH] Add travelRuleFormat field to the payerdata compliance --- .idea/misc.xml | 1 - .../kotlin/me/uma/UmaProtocolHelper.kt | 6 +++ .../kotlin/me/uma/protocol/PayRequest.kt | 41 +++++++++++++++++++ .../src/commonTest/kotlin/me/uma/UmaTests.kt | 35 +++++++++++++++- 4 files changed, 81 insertions(+), 2 deletions(-) diff --git a/.idea/misc.xml b/.idea/misc.xml index 735e1f6..b3c3e1e 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,3 @@ - diff --git a/uma-sdk/src/commonMain/kotlin/me/uma/UmaProtocolHelper.kt b/uma-sdk/src/commonMain/kotlin/me/uma/UmaProtocolHelper.kt index af4d98c..eb815c7 100644 --- a/uma-sdk/src/commonMain/kotlin/me/uma/UmaProtocolHelper.kt +++ b/uma-sdk/src/commonMain/kotlin/me/uma/UmaProtocolHelper.kt @@ -244,6 +244,8 @@ class UmaProtocolHelper @JvmOverloads constructor( * compliance provider, this will be used to pre-screen the sender's UTXOs for compliance purposes. * @param payerName The name of the sender (optional). * @param payerEmail The email of the sender (optional). + * @param travelRuleFormat An optional standardized format of the travel rule information (e.g. IVMS). Null + * indicates raw json or a custom format. * @return The [PayRequest] that should be sent to the receiver. */ @JvmOverloads @@ -260,6 +262,7 @@ class UmaProtocolHelper @JvmOverloads constructor( payerNodePubKey: String? = null, payerName: String? = null, payerEmail: String? = null, + travelRuleFormat: TravelRuleFormat? = null, ): PayRequest { val compliancePayerData = getSignedCompliancePayerData( receiverEncryptionPubKey, @@ -270,6 +273,7 @@ class UmaProtocolHelper @JvmOverloads constructor( payerUtxos, payerNodePubKey, utxoCallback, + travelRuleFormat ) val payerData = PayerData( identifier = payerIdentifier, @@ -293,6 +297,7 @@ class UmaProtocolHelper @JvmOverloads constructor( payerUtxos: List?, payerNodePubKey: String?, utxoCallback: String, + travelRuleFormat: TravelRuleFormat?, ): CompliancePayerData { val nonce = generateNonce() val timestamp = System.currentTimeMillis() / 1000 @@ -305,6 +310,7 @@ class UmaProtocolHelper @JvmOverloads constructor( signatureNonce = nonce, signatureTimestamp = timestamp, utxoCallback = utxoCallback, + travelRuleFormat = travelRuleFormat, ) val signablePayload = "$payerIdentifier|$nonce|$timestamp".encodeToByteArray() val signature = signPayload(signablePayload, sendingVaspPrivateKey) diff --git a/uma-sdk/src/commonMain/kotlin/me/uma/protocol/PayRequest.kt b/uma-sdk/src/commonMain/kotlin/me/uma/protocol/PayRequest.kt index 28ebb7c..0d73b77 100644 --- a/uma-sdk/src/commonMain/kotlin/me/uma/protocol/PayRequest.kt +++ b/uma-sdk/src/commonMain/kotlin/me/uma/protocol/PayRequest.kt @@ -1,8 +1,13 @@ package me.uma.protocol +import kotlinx.serialization.KSerializer import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor import kotlinx.serialization.encodeToString +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.json.Json /** @@ -50,6 +55,8 @@ data class PayerData @JvmOverloads constructor( * @property signature The signature of the sender on the signable payload. * @property signatureNonce The nonce used in the signature. * @property signatureTimestamp The timestamp used in the signature. + * @property travelRuleFormat An optional standardized format of the travel rule information (e.g. IVMS). Null + * indicates raw json or a custom format. */ @Serializable data class CompliancePayerData( @@ -61,6 +68,40 @@ data class CompliancePayerData( val signature: String, val signatureNonce: String, val signatureTimestamp: Long, + val travelRuleFormat: TravelRuleFormat? = null, ) { fun signedWith(signature: String) = copy(signature = signature) } + +/** + * A standardized format of the travel rule information. + */ +@Serializable(with = TravelRuleFormatSerializer::class) +data class TravelRuleFormat( + /** The type of the travel rule format (e.g. IVMS). */ + val type: String, + /** The version of the travel rule format (e.g. 1.0). */ + val version: String?, +) + +/** + * Serializes the TravelRuleFormat to string in the format of "type@version". If there's no version, it will be + * serialized as "type". + */ +class TravelRuleFormatSerializer : KSerializer { + override val descriptor = PrimitiveSerialDescriptor("TravelRuleFormat", PrimitiveKind.STRING) + + override fun deserialize(decoder: Decoder): TravelRuleFormat { + val value = decoder.decodeString() + if (!value.contains("@")) { + return TravelRuleFormat(value, null) + } + val parts = value.split("@") + return TravelRuleFormat(parts[0], parts.getOrNull(1)) + } + + override fun serialize(encoder: Encoder, value: TravelRuleFormat) { + encoder.encodeString("${value.type}${value.version?.let { "@$it" } ?: ""}") + } +} + diff --git a/uma-sdk/src/commonTest/kotlin/me/uma/UmaTests.kt b/uma-sdk/src/commonTest/kotlin/me/uma/UmaTests.kt index 6053fd5..f622f65 100644 --- a/uma-sdk/src/commonTest/kotlin/me/uma/UmaTests.kt +++ b/uma-sdk/src/commonTest/kotlin/me/uma/UmaTests.kt @@ -1,14 +1,20 @@ package me.uma +import me.uma.crypto.Secp256k1 +import me.uma.protocol.KycStatus +import me.uma.protocol.PayerDataOptions +import me.uma.protocol.TravelRuleFormat import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.fail import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import kotlinx.serialization.json.Json -import me.uma.protocol.PayerDataOptions @OptIn(ExperimentalCoroutinesApi::class) class UmaTests { + val keys = Secp256k1.generateKeyPair() + @Test fun `test serialize PayerDataOptions`() = runTest { val payerDataOptions = PayerDataOptions( @@ -22,4 +28,31 @@ class UmaTests { Json.decodeFromString(PayerDataOptions.serializer(), json), ) } + + @OptIn(ExperimentalStdlibApi::class) + @Test + fun `test create and parse payreq`() = runTest { + val travelRuleInfo = "travel rule info" + val payreq = UmaProtocolHelper().getPayRequest( + receiverEncryptionPubKey = keys.publicKey, + sendingVaspPrivateKey = keys.privateKey, + currencyCode = "USD", + amount = 100, + payerIdentifier = "test@test.com", + payerKycStatus = KycStatus.VERIFIED, + utxoCallback = "https://example.com/utxo", + travelRuleInfo = "travel rule info", + travelRuleFormat = TravelRuleFormat("someFormat", "1.0"), + ) + val json = payreq.toJson() + val decodedPayReq = UmaProtocolHelper().parseAsPayRequest(json) + assertEquals(payreq, decodedPayReq) + + val encryptedTravelRuleInfo = + decodedPayReq.payerData.compliance?.travelRuleInfo ?: fail("travel rule info not found") + assertEquals( + travelRuleInfo, + String(Secp256k1.decryptEcies(encryptedTravelRuleInfo.hexToByteArray(), keys.privateKey)), + ) + } }