diff --git a/build.gradle.kts b/build.gradle.kts index c3d9a7a..188d49c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -22,7 +22,7 @@ allprojects { } group = "org.onflow.flow" - val defaultVersion = "0.0.14" + val defaultVersion = "0.0.15" version = System.getenv("GITHUB_REF")?.split('/')?.last() ?: defaultVersion } diff --git a/flow/src/commonMain/kotlin/org/onflow/flow/infrastructure/Cadence.kt b/flow/src/commonMain/kotlin/org/onflow/flow/infrastructure/Cadence.kt index 0e0a647..0475a8d 100644 --- a/flow/src/commonMain/kotlin/org/onflow/flow/infrastructure/Cadence.kt +++ b/flow/src/commonMain/kotlin/org/onflow/flow/infrastructure/Cadence.kt @@ -248,7 +248,7 @@ class Cadence { @Serializable @SerialName(TYPE_UINT) data class UIntValue( - @Serializable(UIntCadenceSerializer::class) + @Serializable(SafeUIntCadenceSerializer::class) override val value: UInt): Value() @Serializable @@ -296,7 +296,7 @@ class Cadence { @Serializable @SerialName(TYPE_UINT64) data class UInt64Value( - @Serializable(ULongCadenceSerializer::class) + @Serializable(SafeULongCadenceSerializer::class) override val value: ULong): Value() @Serializable @@ -351,13 +351,13 @@ class Cadence { @Serializable @SerialName(TYPE_WORD32) data class Word32Value( - @Serializable(UIntCadenceSerializer::class) + @Serializable(SafeUIntCadenceSerializer::class) override val value: UInt): Value() @Serializable @SerialName(TYPE_WORD64) data class Word64Value( - @Serializable(ULongCadenceSerializer::class) + @Serializable(SafeULongCadenceSerializer::class) override val value: ULong): Value() @Serializable diff --git a/flow/src/commonMain/kotlin/org/onflow/flow/infrastructure/Serializer.kt b/flow/src/commonMain/kotlin/org/onflow/flow/infrastructure/Serializer.kt index ba689b2..2be9c7e 100644 --- a/flow/src/commonMain/kotlin/org/onflow/flow/infrastructure/Serializer.kt +++ b/flow/src/commonMain/kotlin/org/onflow/flow/infrastructure/Serializer.kt @@ -37,6 +37,91 @@ object UIntCadenceSerializer : KSerializer { override fun deserialize(decoder: Decoder): UInt = decoder.decodeString().toUInt() } +/** + * A safe UInt serializer that can handle both string and numeric values in JSON. + * This prevents parsing failures when large numbers that exceed UInt range are encountered. + * + * The serializer will: + * - Accept both quoted strings and unquoted numbers from JSON + * - Handle very large numbers by clamping to UInt.MAX_VALUE + * - Always parse safely without throwing exceptions + */ +object SafeUIntCadenceSerializer : KSerializer { + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("SafeUInt", PrimitiveKind.STRING) + + override fun serialize(encoder: Encoder, value: UInt) { + encoder.encodeString(value.toString()) + } + + override fun deserialize(decoder: Decoder): UInt { + // First, check if we have a JsonDecoder to work with + val jsonDecoder = decoder as? JsonDecoder + if (jsonDecoder != null) { + return try { + val element = jsonDecoder.decodeJsonElement() + when (element) { + is JsonPrimitive -> { + // Get the raw content as string regardless of whether it's quoted or not + val numberString = element.content + // Try to parse as ULong first, then clamp to UInt range + val longValue = numberString.toULongOrNull() ?: 0UL + if (longValue > UInt.MAX_VALUE.toULong()) { + UInt.MAX_VALUE // Clamp to UInt.MAX_VALUE + } else { + longValue.toUInt() + } + } + else -> { + // Fallback - try to convert to string and parse + val numberString = element.toString().removeSurrounding("\"") + val longValue = numberString.toULongOrNull() ?: 0UL + if (longValue > UInt.MAX_VALUE.toULong()) { + UInt.MAX_VALUE + } else { + longValue.toUInt() + } + } + } + } catch (e: Exception) { + // If JSON element parsing fails, try string decoding + try { + val numberString = decoder.decodeString() + val longValue = numberString.toULongOrNull() ?: 0UL + if (longValue > UInt.MAX_VALUE.toULong()) { + UInt.MAX_VALUE + } else { + longValue.toUInt() + } + } catch (e2: Exception) { + 0u // Safe fallback + } + } + } else { + // For non-JSON decoders, try multiple approaches + return try { + val numberString = decoder.decodeString() + val longValue = numberString.toULongOrNull() ?: 0UL + if (longValue > UInt.MAX_VALUE.toULong()) { + UInt.MAX_VALUE + } else { + longValue.toUInt() + } + } catch (e: Exception) { + try { + val longValue = decoder.decodeLong().toULong() + if (longValue > UInt.MAX_VALUE.toULong()) { + UInt.MAX_VALUE + } else { + longValue.toUInt() + } + } catch (e2: Exception) { + 0u // Safe fallback + } + } + } + } +} + object ShortCadenceSerializer : KSerializer { override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("value", PrimitiveKind.STRING) override fun serialize(encoder: Encoder, value: Short) = encoder.encodeString(value.toString()) @@ -61,6 +146,61 @@ object ULongCadenceSerializer : KSerializer { override fun deserialize(decoder: Decoder): ULong = decoder.decodeString().toULong() } +/** + * A safe ULong serializer that can handle both string and numeric values in JSON. + * This prevents parsing failures when large ULong numbers are encountered. + * + * The serializer will: + * - Accept both quoted strings and unquoted numbers from JSON + * - Handle very large numbers that might cause parsing issues + * - Always parse as ULong safely + */ +object SafeULongCadenceSerializer : KSerializer { + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("SafeULong", PrimitiveKind.STRING) + + override fun serialize(encoder: Encoder, value: ULong) { + encoder.encodeString(value.toString()) + } + + override fun deserialize(decoder: Decoder): ULong { + // First, check if we have a JsonDecoder to work with + val jsonDecoder = decoder as? JsonDecoder + if (jsonDecoder != null) { + return try { + val element = jsonDecoder.decodeJsonElement() + when (element) { + is JsonPrimitive -> { + // Get the raw content as string regardless of whether it's quoted or not + element.content.toULong() + } + else -> { + // Fallback - try to convert to string and parse + element.toString().removeSurrounding("\"").toULong() + } + } + } catch (e: Exception) { + // If JSON element parsing fails, try string decoding + try { + decoder.decodeString().toULong() + } catch (e2: Exception) { + 0UL // Safe fallback + } + } + } else { + // For non-JSON decoders, try multiple approaches + return try { + decoder.decodeString().toULong() + } catch (e: Exception) { + try { + decoder.decodeLong().toULong() + } catch (e2: Exception) { + 0UL // Safe fallback + } + } + } + } +} + object BigIntegerCadenceSerializer : KSerializer { override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("value", PrimitiveKind.STRING) override fun serialize(encoder: Encoder, value: BigInteger) = encoder.encodeString(value.toString()) diff --git a/flow/src/commonMain/kotlin/org/onflow/flow/models/Event.kt b/flow/src/commonMain/kotlin/org/onflow/flow/models/Event.kt index 5382433..5828888 100644 --- a/flow/src/commonMain/kotlin/org/onflow/flow/models/Event.kt +++ b/flow/src/commonMain/kotlin/org/onflow/flow/models/Event.kt @@ -1,6 +1,7 @@ package org.onflow.flow.models import org.onflow.flow.infrastructure.Cadence import kotlinx.serialization.* +import org.onflow.flow.infrastructure.SafeStringSerializer /** * @@ -20,9 +21,13 @@ data class Event ( /* A 32-byte unique identifier for an entity. */ @SerialName(value = "transaction_id") @Required val transactionId: kotlin.String, - @SerialName(value = "transaction_index") @Required val transactionIndex: kotlin.String, + @SerialName(value = "transaction_index") + @Serializable(with = SafeStringSerializer::class) + @Required val transactionIndex: kotlin.String, - @SerialName(value = "event_index") @Required val eventIndex: kotlin.String, + @SerialName(value = "event_index") + @Serializable(with = SafeStringSerializer::class) + @Required val eventIndex: kotlin.String, @Serializable(CadenceBase64Serializer::class) @SerialName(value = "payload") @Required val payload: Cadence.Value diff --git a/flow/src/commonTest/kotlin/org/onflow/flow/FlowMainnetApiTests.kt b/flow/src/commonTest/kotlin/org/onflow/flow/FlowMainnetApiTests.kt index 0c13068..e058db5 100644 --- a/flow/src/commonTest/kotlin/org/onflow/flow/FlowMainnetApiTests.kt +++ b/flow/src/commonTest/kotlin/org/onflow/flow/FlowMainnetApiTests.kt @@ -32,6 +32,34 @@ class FlowMainnetApiTests { } } + @Test + fun testLargeNumberParsingIssue() { + runBlocking { + try { + // This is the specific transaction ID from the user's logs that's causing the parsing error + val result = api.getTransactionResult("41741c38f870842971808eaa93599211e241036b87727c87713e2329a767d4d7") + println("Transaction result retrieved successfully") + println("Events count: ${result.events.size}") + println("Computation used: ${result.computationUsed}") + println("Status: ${result.status}") + + // Log event details to see where the large number appears + result.events.forEachIndexed { index, event -> + println("Event $index:") + println(" Type: ${event.type}") + println(" Transaction Index: ${event.transactionIndex}") + println(" Event Index: ${event.eventIndex}") + println(" Payload: ${event.payload}") + } + } catch (e: Exception) { + println("Failed to parse transaction result: ${e.message}") + println("Exception type: ${e::class.simpleName}") + e.printStackTrace() + throw e + } + } + } + @Test fun decodeStruct() { runBlocking { diff --git a/flow/src/commonTest/kotlin/org/onflow/flow/infrastructure/SafeStringSerializerTest.kt b/flow/src/commonTest/kotlin/org/onflow/flow/infrastructure/SafeStringSerializerTest.kt index 3c4960a..59b7755 100644 --- a/flow/src/commonTest/kotlin/org/onflow/flow/infrastructure/SafeStringSerializerTest.kt +++ b/flow/src/commonTest/kotlin/org/onflow/flow/infrastructure/SafeStringSerializerTest.kt @@ -39,6 +39,13 @@ class SafeStringSerializerTest { assertEquals("1544440348859079121", result.value) } + @Test + fun `test specific error number from logs`() { + val jsonString = """{"value":1604440348859079121}""" + val result = json.decodeFromString(jsonString) + assertEquals("1604440348859079121", result.value) + } + @Test fun `test large number as quoted string`() { val jsonString = """{"value":"1544440348859079121"}"""