Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixing convert number functions for Longs #56

Merged
merged 5 commits into from
Sep 5, 2024
Merged
Show file tree
Hide file tree
Changes from 3 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
5 changes: 1 addition & 4 deletions uma-sdk/src/commonMain/kotlin/me/uma/UmaProtocolHelper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
14 changes: 7 additions & 7 deletions uma-sdk/src/commonMain/kotlin/me/uma/protocol/Invoice.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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.getInt(offset.valueOffset(), length)
}
offset = offset.valueOffset() + length
}
Expand Down Expand Up @@ -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,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one should be a Long then, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good catch, it should. I'll switch amount to long as well, to cover msat

/** 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. */
Expand Down Expand Up @@ -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.getInt(offset.valueOffset(), length)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be a long too? Could just see for units like msats, this could get pretty big. Not sure if @zhenlu was thinking of this as an int or long, though. I know it's a Long in UMA generally.

3 ->
ib.receivingCurrency =
bytes.getTLV(offset.valueOffset(), length, InvoiceCurrency::fromTLV) as InvoiceCurrency

4 -> ib.expiration = bytes.getNumber(offset.valueOffset(), length)
4 -> ib.expiration = bytes.getLong(offset.valueOffset(), length)
5 -> ib.isSubjectToTravelRule = bytes.getBoolean(offset.valueOffset())
6 ->
ib.requiredPayerData =
Expand All @@ -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.getInt(offset.valueOffset(), length)
9 -> ib.senderUma = bytes.getString(offset.valueOffset(), length)
10 -> ib.invoiceLimit = bytes.getNumber(offset.valueOffset(), length)
10 -> ib.invoiceLimit = bytes.getInt(offset.valueOffset(), length)
11 ->
ib.kycStatus = (
bytes.getByteCodeable(
Expand Down Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why use Number instead of Long directly? I think we're going to need to be explicit about 64-bit ints to be consistent, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

converted all back to Int / Long, it's a bit clearer. during serialization, the values will be converted to their smallest possible representation, so Long fields might actually be serialized as small as a byte, but this detail is now better hidden inside of the getLong/getInt functions

var isSubjectToTravelRule: Boolean? = null
var requiredPayerData: CounterPartyDataOptions? = null
var umaVersion: String? = null
Expand Down
40 changes: 37 additions & 3 deletions uma-sdk/src/commonMain/kotlin/me/uma/utils/TLVUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,23 @@ fun MutableList<ByteArray>.putNumber(tag: Int, value: Number?): MutableList<Byte
}
add(
when (value) {
is Long -> {
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() -> {
Expand All @@ -54,6 +71,7 @@ fun MutableList<ByteArray>.putNumber(tag: Int, value: Number?): MutableList<Byte
}
}
}

is Short -> {
when (value) {
in Byte.MIN_VALUE..Byte.MAX_VALUE -> {
Expand All @@ -62,12 +80,12 @@ fun MutableList<ByteArray>.putNumber(tag: Int, value: Number?): MutableList<Byte
else -> 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
}
Expand Down Expand Up @@ -128,12 +146,28 @@ fun MutableList<ByteArray>.array(): ByteArray {
return buffer.array()
}

fun ByteArray.getNumber(offset: Int, length: Int): Int {
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..<offset + length).toByteArray())
return when (length) {
1 -> this[offset].toInt()
2 -> buffer.getShort().toInt()
4 -> buffer.getInt()
8 -> buffer.getLong()
else -> this[offset].toInt()
}
}
Expand Down
15 changes: 12 additions & 3 deletions uma-sdk/src/commonTest/kotlin/me/uma/UmaTests.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<Invoice>(serializedInvoice)
assertEquals(result.expiration, timestamp)
}

@Test
fun `deserializing an Invoice with missing required fields triggers error`() = runTest {
val exception =
Expand Down Expand Up @@ -88,7 +97,7 @@ class UmaTests {
assertEquals("\[email protected]", 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)
Expand Down Expand Up @@ -251,7 +260,7 @@ class UmaTests {
)
}

private fun createInvoice(): Invoice {
private fun createInvoice(timestamp: Long? = null): Invoice {
val requiredPayerData =
mapOf(
"name" to CounterPartyDataOption(false),
Expand All @@ -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,
Expand Down
Loading