From cbbb9c872a1046ff854fa64e57257778cbd1e218 Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Wed, 4 Sep 2024 19:19:57 -0700 Subject: [PATCH 1/5] Fixing convert number functions for Longs --- .../kotlin/me/uma/UmaProtocolHelper.kt | 5 +--- .../kotlin/me/uma/protocol/Invoice.kt | 14 +++++------ .../kotlin/me/uma/utils/TLVUtils.kt | 25 ++++++++++++++++--- .../src/commonTest/kotlin/me/uma/UmaTests.kt | 15 ++++++++--- 4 files changed, 42 insertions(+), 17 deletions(-) diff --git a/uma-sdk/src/commonMain/kotlin/me/uma/UmaProtocolHelper.kt b/uma-sdk/src/commonMain/kotlin/me/uma/UmaProtocolHelper.kt index 02f1929..a4086fd 100644 --- a/uma-sdk/src/commonMain/kotlin/me/uma/UmaProtocolHelper.kt +++ b/uma-sdk/src/commonMain/kotlin/me/uma/UmaProtocolHelper.kt @@ -893,10 +893,7 @@ class UmaProtocolHelper @JvmOverloads constructor( return identifier.substring(atIndex + 1) } - fun verifyUmaInvoice( - invoice: Invoice, - pubKeyResponse: PubKeyResponse, - ): Boolean { + fun verifyUmaInvoice(invoice: Invoice, pubKeyResponse: PubKeyResponse): Boolean { return invoice.signature?.let { signature -> verifySignature( invoice.toSignablePayload(), diff --git a/uma-sdk/src/commonMain/kotlin/me/uma/protocol/Invoice.kt b/uma-sdk/src/commonMain/kotlin/me/uma/protocol/Invoice.kt index 5af91ed..8e8a29b 100644 --- a/uma-sdk/src/commonMain/kotlin/me/uma/protocol/Invoice.kt +++ b/uma-sdk/src/commonMain/kotlin/me/uma/protocol/Invoice.kt @@ -34,7 +34,7 @@ data class InvoiceCurrency( 0 -> code = bytes.getString(offset.valueOffset(), length) 1 -> name = bytes.getString(offset.valueOffset(), length) 2 -> symbol = bytes.getString(offset.valueOffset(), length) - 3 -> decimals = bytes.getNumber(offset.valueOffset(), length) + 3 -> decimals = bytes.getNumber(offset.valueOffset(), length) as Int } offset = offset.valueOffset() + length } @@ -77,7 +77,7 @@ data class Invoice( /** The currency of the invoice */ val receivingCurrency: InvoiceCurrency, /** The unix timestamp the UMA invoice expires */ - val expiration: Int, + val expiration: Number, /** Indicates whether the VASP is a financial institution that requires travel rule information. */ val isSubjectToTravelRule: Boolean, /** RequiredPayerData the data about the payer that the sending VASP must provide in order to send a payment. */ @@ -141,12 +141,12 @@ data class Invoice( when (bytes[offset].toInt()) { 0 -> ib.receiverUma = bytes.getString(offset.valueOffset(), length) 1 -> ib.invoiceUUID = bytes.getString(offset.valueOffset(), length) - 2 -> ib.amount = bytes.getNumber(offset.valueOffset(), length) + 2 -> ib.amount = bytes.getNumber(offset.valueOffset(), length).toInt() 3 -> ib.receivingCurrency = bytes.getTLV(offset.valueOffset(), length, InvoiceCurrency::fromTLV) as InvoiceCurrency - 4 -> ib.expiration = bytes.getNumber(offset.valueOffset(), length) + 4 -> ib.expiration = bytes.getNumber(offset.valueOffset(), length).toLong() 5 -> ib.isSubjectToTravelRule = bytes.getBoolean(offset.valueOffset()) 6 -> ib.requiredPayerData = @@ -159,9 +159,9 @@ data class Invoice( ).options 7 -> ib.umaVersion = bytes.getString(offset.valueOffset(), length) - 8 -> ib.commentCharsAllowed = bytes.getNumber(offset.valueOffset(), length) + 8 -> ib.commentCharsAllowed = bytes.getNumber(offset.valueOffset(), length).toInt() 9 -> ib.senderUma = bytes.getString(offset.valueOffset(), length) - 10 -> ib.invoiceLimit = bytes.getNumber(offset.valueOffset(), length) + 10 -> ib.invoiceLimit = bytes.getNumber(offset.valueOffset(), length).toInt() 11 -> ib.kycStatus = ( bytes.getByteCodeable( @@ -195,7 +195,7 @@ class InvoiceBuilder { var invoiceUUID: String? = null var amount: Int? = null var receivingCurrency: InvoiceCurrency? = null - var expiration: Int? = null + var expiration: Number? = null var isSubjectToTravelRule: Boolean? = null var requiredPayerData: CounterPartyDataOptions? = null var umaVersion: String? = null diff --git a/uma-sdk/src/commonMain/kotlin/me/uma/utils/TLVUtils.kt b/uma-sdk/src/commonMain/kotlin/me/uma/utils/TLVUtils.kt index 4de5f1d..0fdafb6 100644 --- a/uma-sdk/src/commonMain/kotlin/me/uma/utils/TLVUtils.kt +++ b/uma-sdk/src/commonMain/kotlin/me/uma/utils/TLVUtils.kt @@ -41,6 +41,23 @@ fun MutableList.putNumber(tag: Int, value: Number?): MutableList { + when (value) { + in Byte.MIN_VALUE.toInt()..Byte.MAX_VALUE.toInt() -> { + tlvBuffer(Byte.SIZE_BYTES).put(value.toByte()) + } + in Short.MIN_VALUE.toInt()..Short.MAX_VALUE.toInt() -> { + tlvBuffer(Short.SIZE_BYTES).putShort(value.toShort()) + } + in Int.MIN_VALUE..Int.MAX_VALUE -> { + tlvBuffer(Int.SIZE_BYTES).putInt(value.toInt()) + } + else -> { + tlvBuffer(Long.SIZE_BYTES).putLong(value) + } + } + } + is Int -> { when (value) { in Byte.MIN_VALUE.toInt()..Byte.MAX_VALUE.toInt() -> { @@ -54,6 +71,7 @@ fun MutableList.putNumber(tag: Int, value: Number?): MutableList { when (value) { in Byte.MIN_VALUE..Byte.MAX_VALUE -> { @@ -62,12 +80,12 @@ fun MutableList.putNumber(tag: Int, value: Number?): MutableList tlvBuffer(Short.SIZE_BYTES).putShort(value.toShort()) } } + is Byte -> tlvBuffer(Byte.SIZE_BYTES).put(value.toByte()) is Float -> tlvBuffer(Float.SIZE_BYTES).putFloat(value) is Double -> tlvBuffer(Double.SIZE_BYTES).putDouble(value) - is Long -> tlvBuffer(Long.SIZE_BYTES).putLong(value) else -> throw IllegalArgumentException("Unsupported type: ${value::class.simpleName}") - }.array() + }.array(), ) return this } @@ -128,12 +146,13 @@ fun MutableList.array(): ByteArray { return buffer.array() } -fun ByteArray.getNumber(offset: Int, length: Int): Int { +fun ByteArray.getNumber(offset: Int, length: Int): Number { val buffer = ByteBuffer.wrap(slice(offset.. this[offset].toInt() 2 -> buffer.getShort().toInt() 4 -> buffer.getInt() + 8 -> buffer.getLong() else -> this[offset].toInt() } } diff --git a/uma-sdk/src/commonTest/kotlin/me/uma/UmaTests.kt b/uma-sdk/src/commonTest/kotlin/me/uma/UmaTests.kt index 7cfe47a..561a9c5 100644 --- a/uma-sdk/src/commonTest/kotlin/me/uma/UmaTests.kt +++ b/uma-sdk/src/commonTest/kotlin/me/uma/UmaTests.kt @@ -48,6 +48,15 @@ class UmaTests { validateInvoice(invoice, result) } + @Test + fun `correctly serialized timestamps in invoices`() = runTest { + val timestamp = System.currentTimeMillis() + val invoice = createInvoice(timestamp) + val serializedInvoice = serialFormat.encodeToString(invoice) + val result = serialFormat.decodeFromString(serializedInvoice) + assertEquals(result.expiration, timestamp) + } + @Test fun `deserializing an Invoice with missing required fields triggers error`() = runTest { val exception = @@ -88,7 +97,7 @@ class UmaTests { assertEquals("\$foo@bar.com", decodedInvoice.receiverUma) assertEquals("c7c07fec-cf00-431c-916f-6c13fc4b69f9", decodedInvoice.invoiceUUID) assertEquals(1000, decodedInvoice.amount) - assertEquals(1000000, decodedInvoice.expiration) + assertEquals(1000000L, decodedInvoice.expiration) assertEquals(true, decodedInvoice.isSubjectToTravelRule) assertEquals("0.3", decodedInvoice.umaVersion) assertEquals(KycStatus.VERIFIED, decodedInvoice.kycStatus) @@ -251,7 +260,7 @@ class UmaTests { ) } - private fun createInvoice(): Invoice { + private fun createInvoice(timestamp: Long? = null): Invoice { val requiredPayerData = mapOf( "name" to CounterPartyDataOption(false), @@ -271,7 +280,7 @@ class UmaTests { invoiceUUID = "c7c07fec-cf00-431c-916f-6c13fc4b69f9", amount = 1000, receivingCurrency = invoiceCurrency, - expiration = 1000000, + expiration = timestamp ?: 1000000L, isSubjectToTravelRule = true, requiredPayerData = requiredPayerData, commentCharsAllowed = null, From 2249aa0023348e3c622d1f8426a101335c7f0a6a Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Wed, 4 Sep 2024 19:26:22 -0700 Subject: [PATCH 2/5] as int -> toInt --- uma-sdk/src/commonMain/kotlin/me/uma/protocol/Invoice.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uma-sdk/src/commonMain/kotlin/me/uma/protocol/Invoice.kt b/uma-sdk/src/commonMain/kotlin/me/uma/protocol/Invoice.kt index 8e8a29b..52e5022 100644 --- a/uma-sdk/src/commonMain/kotlin/me/uma/protocol/Invoice.kt +++ b/uma-sdk/src/commonMain/kotlin/me/uma/protocol/Invoice.kt @@ -34,7 +34,7 @@ data class InvoiceCurrency( 0 -> code = bytes.getString(offset.valueOffset(), length) 1 -> name = bytes.getString(offset.valueOffset(), length) 2 -> symbol = bytes.getString(offset.valueOffset(), length) - 3 -> decimals = bytes.getNumber(offset.valueOffset(), length) as Int + 3 -> decimals = bytes.getNumber(offset.valueOffset(), length).toInt() } offset = offset.valueOffset() + length } From 02cf83b5c46f46217afb026949572e4dca088804 Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Wed, 4 Sep 2024 20:30:10 -0700 Subject: [PATCH 3/5] clarify return type of getNumber into getLong and getInt --- .../kotlin/me/uma/protocol/Invoice.kt | 10 +++++----- .../commonMain/kotlin/me/uma/utils/TLVUtils.kt | 17 ++++++++++++++++- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/uma-sdk/src/commonMain/kotlin/me/uma/protocol/Invoice.kt b/uma-sdk/src/commonMain/kotlin/me/uma/protocol/Invoice.kt index 52e5022..2734d74 100644 --- a/uma-sdk/src/commonMain/kotlin/me/uma/protocol/Invoice.kt +++ b/uma-sdk/src/commonMain/kotlin/me/uma/protocol/Invoice.kt @@ -34,7 +34,7 @@ data class InvoiceCurrency( 0 -> code = bytes.getString(offset.valueOffset(), length) 1 -> name = bytes.getString(offset.valueOffset(), length) 2 -> symbol = bytes.getString(offset.valueOffset(), length) - 3 -> decimals = bytes.getNumber(offset.valueOffset(), length).toInt() + 3 -> decimals = bytes.getInt(offset.valueOffset(), length) } offset = offset.valueOffset() + length } @@ -141,12 +141,12 @@ data class Invoice( when (bytes[offset].toInt()) { 0 -> ib.receiverUma = bytes.getString(offset.valueOffset(), length) 1 -> ib.invoiceUUID = bytes.getString(offset.valueOffset(), length) - 2 -> ib.amount = bytes.getNumber(offset.valueOffset(), length).toInt() + 2 -> ib.amount = bytes.getInt(offset.valueOffset(), length) 3 -> ib.receivingCurrency = bytes.getTLV(offset.valueOffset(), length, InvoiceCurrency::fromTLV) as InvoiceCurrency - 4 -> ib.expiration = bytes.getNumber(offset.valueOffset(), length).toLong() + 4 -> ib.expiration = bytes.getLong(offset.valueOffset(), length) 5 -> ib.isSubjectToTravelRule = bytes.getBoolean(offset.valueOffset()) 6 -> ib.requiredPayerData = @@ -159,9 +159,9 @@ data class Invoice( ).options 7 -> ib.umaVersion = bytes.getString(offset.valueOffset(), length) - 8 -> ib.commentCharsAllowed = bytes.getNumber(offset.valueOffset(), length).toInt() + 8 -> ib.commentCharsAllowed = bytes.getInt(offset.valueOffset(), length) 9 -> ib.senderUma = bytes.getString(offset.valueOffset(), length) - 10 -> ib.invoiceLimit = bytes.getNumber(offset.valueOffset(), length).toInt() + 10 -> ib.invoiceLimit = bytes.getInt(offset.valueOffset(), length) 11 -> ib.kycStatus = ( bytes.getByteCodeable( diff --git a/uma-sdk/src/commonMain/kotlin/me/uma/utils/TLVUtils.kt b/uma-sdk/src/commonMain/kotlin/me/uma/utils/TLVUtils.kt index 0fdafb6..43abf7a 100644 --- a/uma-sdk/src/commonMain/kotlin/me/uma/utils/TLVUtils.kt +++ b/uma-sdk/src/commonMain/kotlin/me/uma/utils/TLVUtils.kt @@ -146,7 +146,22 @@ fun MutableList.array(): ByteArray { return buffer.array() } -fun ByteArray.getNumber(offset: Int, length: Int): Number { +fun ByteArray.getInt(offset: Int, length: Int): Int { + return getNumber(offset, length).toInt() +} + +fun ByteArray.getLong(offset: Int, length: Int): Long { + return getNumber(offset, length).toLong() +} + +/** + * in Invoice's TLV, the numeric fields are stored in their smallest possible representation (ie, 9L would + * be stored as a single Byte) + * what this means is that when deserializing, we can't simply call buffer.getLong() for Long fields, + * as the encoded field may be as little as 1 byte, triggering a Buffer Underflow Exception. + * Instead, we read the value based on its byte length, and then case it to a Long or Int in a wrapper function + */ +private fun ByteArray.getNumber(offset: Int, length: Int): Number { val buffer = ByteBuffer.wrap(slice(offset.. this[offset].toInt() From cbf8e314bf43252217ea21d208a7b791012c9fef Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Thu, 5 Sep 2024 09:48:55 -0700 Subject: [PATCH 4/5] remove uma version parameter, this will be determined internally --- .../commonMain/kotlin/me/uma/UmaProtocolHelper.kt | 3 +-- uma-sdk/src/commonTest/kotlin/me/uma/UmaTests.kt | 14 ++++++-------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/uma-sdk/src/commonMain/kotlin/me/uma/UmaProtocolHelper.kt b/uma-sdk/src/commonMain/kotlin/me/uma/UmaProtocolHelper.kt index a4086fd..34b7315 100644 --- a/uma-sdk/src/commonMain/kotlin/me/uma/UmaProtocolHelper.kt +++ b/uma-sdk/src/commonMain/kotlin/me/uma/UmaProtocolHelper.kt @@ -910,7 +910,6 @@ class UmaProtocolHelper @JvmOverloads constructor( receivingCurrency: InvoiceCurrency, expiration: Int, isSubjectToTravelRule: Boolean, - umaVersion: String, commentCharsAllowed: Int? = null, senderUma: String? = null, invoiceLimit: Int? = null, @@ -926,7 +925,7 @@ class UmaProtocolHelper @JvmOverloads constructor( receivingCurrency = receivingCurrency, expiration = expiration, isSubjectToTravelRule = isSubjectToTravelRule, - umaVersion = umaVersion, + umaVersion = UMA_VERSION_STRING, commentCharsAllowed = commentCharsAllowed, senderUma = senderUma, invoiceLimit = invoiceLimit, diff --git a/uma-sdk/src/commonTest/kotlin/me/uma/UmaTests.kt b/uma-sdk/src/commonTest/kotlin/me/uma/UmaTests.kt index 561a9c5..1553a89 100644 --- a/uma-sdk/src/commonTest/kotlin/me/uma/UmaTests.kt +++ b/uma-sdk/src/commonTest/kotlin/me/uma/UmaTests.kt @@ -122,10 +122,9 @@ class UmaTests { commentCharsAllowed = null, senderUma = null, invoiceLimit = null, - umaVersion = "0.3", kycStatus = KycStatus.VERIFIED, callback = "https://example.com/callback", - privateSigningKey = keys.privateKey + privateSigningKey = keys.privateKey, ) assertTrue(UmaProtocolHelper().verifyUmaInvoice(invoice, PubKeyResponse(keys.publicKey, keys.publicKey))) } @@ -145,12 +144,11 @@ class UmaTests { utxoCallback = "https://example.com/utxo", travelRuleInfo = "travel rule info", travelRuleFormat = TravelRuleFormat("someFormat", "1.0"), - requestedPayeeData = - createCounterPartyDataOptions( - "email" to true, - "name" to false, - "compliance" to true, - ), + requestedPayeeData = createCounterPartyDataOptions( + "email" to true, + "name" to false, + "compliance" to true, + ), receiverUmaVersion = "1.0", ) assertTrue(payreq is PayRequestV1) From bcaa59069a8e3982546c1b9d6c4d6fd9bf1e23c5 Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Thu, 5 Sep 2024 09:57:06 -0700 Subject: [PATCH 5/5] changing amount to long --- .../src/commonMain/kotlin/me/uma/UmaProtocolHelper.kt | 4 ++-- .../src/commonMain/kotlin/me/uma/protocol/Invoice.kt | 10 +++++----- uma-sdk/src/commonTest/kotlin/me/uma/UmaTests.kt | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/uma-sdk/src/commonMain/kotlin/me/uma/UmaProtocolHelper.kt b/uma-sdk/src/commonMain/kotlin/me/uma/UmaProtocolHelper.kt index 34b7315..8bc1ece 100644 --- a/uma-sdk/src/commonMain/kotlin/me/uma/UmaProtocolHelper.kt +++ b/uma-sdk/src/commonMain/kotlin/me/uma/UmaProtocolHelper.kt @@ -906,9 +906,9 @@ class UmaProtocolHelper @JvmOverloads constructor( fun getInvoice( receiverUma: String, invoiceUUID: String, - amount: Int, + amount: Long, receivingCurrency: InvoiceCurrency, - expiration: Int, + expiration: Long, isSubjectToTravelRule: Boolean, commentCharsAllowed: Int? = null, senderUma: String? = null, diff --git a/uma-sdk/src/commonMain/kotlin/me/uma/protocol/Invoice.kt b/uma-sdk/src/commonMain/kotlin/me/uma/protocol/Invoice.kt index 2734d74..e81c61d 100644 --- a/uma-sdk/src/commonMain/kotlin/me/uma/protocol/Invoice.kt +++ b/uma-sdk/src/commonMain/kotlin/me/uma/protocol/Invoice.kt @@ -73,11 +73,11 @@ data class Invoice( /** Invoice UUID Served as both the identifier of the UMA invoice, and the validation of proof of payment.*/ val invoiceUUID: String, /** The amount of invoice to be paid in the smallest unit of the ReceivingCurrency. */ - val amount: Int, + val amount: Long, /** The currency of the invoice */ val receivingCurrency: InvoiceCurrency, /** The unix timestamp the UMA invoice expires */ - val expiration: Number, + val expiration: Long, /** Indicates whether the VASP is a financial institution that requires travel rule information. */ val isSubjectToTravelRule: Boolean, /** RequiredPayerData the data about the payer that the sending VASP must provide in order to send a payment. */ @@ -141,7 +141,7 @@ data class Invoice( when (bytes[offset].toInt()) { 0 -> ib.receiverUma = bytes.getString(offset.valueOffset(), length) 1 -> ib.invoiceUUID = bytes.getString(offset.valueOffset(), length) - 2 -> ib.amount = bytes.getInt(offset.valueOffset(), length) + 2 -> ib.amount = bytes.getLong(offset.valueOffset(), length) 3 -> ib.receivingCurrency = bytes.getTLV(offset.valueOffset(), length, InvoiceCurrency::fromTLV) as InvoiceCurrency @@ -193,9 +193,9 @@ data class Invoice( class InvoiceBuilder { var receiverUma: String? = null var invoiceUUID: String? = null - var amount: Int? = null + var amount: Long? = null var receivingCurrency: InvoiceCurrency? = null - var expiration: Number? = null + var expiration: Long? = null var isSubjectToTravelRule: Boolean? = null var requiredPayerData: CounterPartyDataOptions? = null var umaVersion: String? = null diff --git a/uma-sdk/src/commonTest/kotlin/me/uma/UmaTests.kt b/uma-sdk/src/commonTest/kotlin/me/uma/UmaTests.kt index 1553a89..37d0484 100644 --- a/uma-sdk/src/commonTest/kotlin/me/uma/UmaTests.kt +++ b/uma-sdk/src/commonTest/kotlin/me/uma/UmaTests.kt @@ -276,7 +276,7 @@ class UmaTests { return Invoice( receiverUma = "\$foo@bar.com", invoiceUUID = "c7c07fec-cf00-431c-916f-6c13fc4b69f9", - amount = 1000, + amount = 1000L, receivingCurrency = invoiceCurrency, expiration = timestamp ?: 1000000L, isSubjectToTravelRule = true,