Skip to content
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
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ allprojects {
}

group = "org.onflow.flow"
val defaultVersion = "0.0.16"
val defaultVersion = "0.0.17"
version = System.getenv("GITHUB_REF")?.split('/')?.last() ?: defaultVersion
}

Expand Down
8 changes: 4 additions & 4 deletions flow/src/commonMain/kotlin/org/onflow/flow/evm/EVMManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -84,15 +84,15 @@ class EVMManager(chainId: ChainId) {
val script = scriptLoader.load("create_coa", "common/evm")
val latestBlock = blocksApi.getBlock()
val proposerAccount = accountsApi.getAccount(proposer.base16Value)
val proposerKey = proposerAccount.keys?.firstOrNull()
?: throw IllegalArgumentException("Proposer has no keys")
val proposerKey = proposerAccount.keys?.firstOrNull { !it.revoked }
?: throw IllegalArgumentException("Proposer has no non-revoked keys")

// Fill in keyIndex dynamically if not set
signers.forEach { signer ->
if (signer.keyIndex == -1) {
val account = accountsApi.getAccount(signer.address)
signer.keyIndex = account.keys?.firstOrNull()?.index?.toInt()
?: throw IllegalStateException("No key found for ${signer.address}")
signer.keyIndex = account.keys?.firstOrNull { !it.revoked }?.index?.toInt()
?: throw IllegalStateException("No non-revoked key found for ${signer.address}")
}
}

Expand Down
124 changes: 118 additions & 6 deletions flow/src/commonMain/kotlin/org/onflow/flow/infrastructure/Cadence.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import io.ktor.util.*
import io.ktor.utils.io.core.*
import kotlinx.serialization.*
import kotlinx.serialization.json.*
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.plus
import kotlinx.serialization.modules.polymorphic
Expand Down Expand Up @@ -41,6 +42,7 @@ import org.onflow.flow.infrastructure.Cadence.Value.UInt32Value
import org.onflow.flow.infrastructure.Cadence.Value.UInt64Value
import org.onflow.flow.infrastructure.Cadence.Value.UInt8Value
import org.onflow.flow.infrastructure.Cadence.Value.UIntValue
import org.onflow.flow.infrastructure.Cadence.Value.UnknownValue
import org.onflow.flow.infrastructure.Cadence.Value.VoidValue
import org.onflow.flow.infrastructure.Cadence.Value.Word16Value
import org.onflow.flow.infrastructure.Cadence.Value.Word32Value
Expand Down Expand Up @@ -131,6 +133,7 @@ class Cadence {
subclass(TypeValue::class)
subclass(PathValue::class)
subclass(CapabilityValue::class)
subclass(UnknownValue::class)
}
}

Expand All @@ -139,6 +142,12 @@ class Cadence {
ignoreUnknownKeys = true
isLenient = true
classDiscriminator = "type"
allowStructuredMapKeys = true
explicitNulls = false
encodeDefaults = false
allowSpecialFloatingPointValues = true
useArrayPolymorphism = false
allowTrailingComma = true
}
}
}
Expand All @@ -160,19 +169,99 @@ class Cadence {
companion object {

fun decodeFromJsonElement(jsonElement: JsonElement): Value {
return jsonSerializer.decodeFromJsonElement(jsonElement)
return tryDecodeFromJsonElement(jsonElement) ?: VoidValue()
}

/**
* Robust JSON element decoding that handles various edge cases from Flow network responses
*/
private fun tryDecodeFromJsonElement(jsonElement: JsonElement): Value? {
return try {
// First, try the standard approach
jsonSerializer.decodeFromJsonElement<Value>(jsonElement)
} catch (e: Exception) {
// If standard decoding fails, try to handle specific cases
when {
jsonElement is JsonObject -> handleComplexJsonObject(jsonElement)
else -> null
}
}
}

/**
* Handle complex JSON objects that might have non-standard type structures
*/
private fun handleComplexJsonObject(jsonObject: JsonObject): Value? {
return try {
val typeElement = jsonObject["type"]

when {
// Handle empty type field
typeElement == null || typeElement.jsonPrimitive.content.isEmpty() -> {
// Try to infer type from structure or default to VoidValue
VoidValue()
}

// Handle complex type objects (like Kind structures)
typeElement is JsonObject -> {
// For complex type objects, try to extract the kind or default to VoidValue
VoidValue()
}

else -> {
// Try to decode with the type as a string
val typeString = typeElement.jsonPrimitive.content
decodeValueWithType(typeString, jsonObject)
}
}
} catch (e: Exception) {
// Final fallback - return VoidValue for any complex structure we can't parse
VoidValue()
}
}

/**
* Decode a value given a specific type string and JSON object
*/
private fun decodeValueWithType(typeString: String, jsonObject: JsonObject): Value? {
return try {
// Create a simplified JSON object with just the type and value
val simplifiedJson = buildJsonObject {
put("type", typeString)
jsonObject["value"]?.let { put("value", it) }
}
jsonSerializer.decodeFromJsonElement<Value>(simplifiedJson)
} catch (e: Exception) {
null
}
}

fun decodeFromJson(jsonString: String): Value {
return jsonSerializer.decodeFromString(jsonString)
return try {
jsonSerializer.decodeFromString(jsonString)
} catch (e: Exception) {
// Try to parse as JsonElement first and then use robust decoding
try {
val jsonElement = Json.parseToJsonElement(jsonString)
decodeFromJsonElement(jsonElement)
} catch (e2: Exception) {
// Final fallback
VoidValue()
}
}
}

fun encodeToJsonString(Value: Value): String {
return Value.encode()
}

fun decodeFromBase64(base64String: String): Value {
return decodeFromJson(base64String.decodeBase64Bytes().decodeToString())
return try {
decodeFromJson(base64String.decodeBase64Bytes().decodeToString())
} catch (e: Exception) {
// Graceful handling of base64 decoding failures
VoidValue()
}
}
}

Expand All @@ -187,6 +276,7 @@ class Cadence {
fun decodeToAny(): Any? {
return when (this) {
is VoidValue -> { null }
is UnknownValue -> { value }
is OptionalValue -> { value?.decodeToAny() }
is ArrayValue -> { value.map { it.decodeToAny() } }

Expand Down Expand Up @@ -400,6 +490,14 @@ class Cadence {
@Serializable
@SerialName(TYPE_CAPABILITY)
open class CapabilityValue(override val value: Capability) : Value()

/**
* Fallback value class for unknown or malformed Cadence types
* Used when the type field is empty, missing, or contains complex objects
*/
@Serializable
@SerialName("")
data class UnknownValue(override val value: String? = null) : Value()
}

companion object {
Expand Down Expand Up @@ -478,7 +576,7 @@ class Cadence {
fun path(domain: PathDomain, identifier: String) = Value.PathValue(Path(domain, identifier))

fun type(value: TypeEntry) = Value.TypeValue(value)
fun type(value: Cadence.Kind) = type(TypeEntry(value))
fun type(kind: String, typeID: String? = null) = type(TypeEntry(Kind(kind = kind, typeID = typeID)))

fun capability(value: Capability) = Value.CapabilityValue(value)
fun capability(path: String, address: String, borrowType: Type) = capability(Capability(path, address, borrowType))
Expand All @@ -505,8 +603,22 @@ class Cadence {

//TODO: Handle more types
@Serializable
data class Kind (val kind: Type, val typeID : String?, val type: String?)

data class Kind (
val kind: String? = null,
val typeID: String? = null,
val type: String? = null,
val fields: List<JsonElement>? = null,
val initializers: List<JsonElement>? = null,
val elementType: JsonElement? = null,
val key: JsonElement? = null,
val value: JsonElement? = null,
val size: Int? = null,
val staticType: JsonElement? = null,
val authorization: JsonElement? = null,
val borrowType: JsonElement? = null,
val path: String? = null
)

@Serializable
data class Capability(val path: String, val address: String, val borrowType: Type)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.*
import org.onflow.flow.models.TransactionExecution

object ByteCadenceSerializer : KSerializer<Byte> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("value", PrimitiveKind.STRING)
Expand Down Expand Up @@ -374,6 +375,54 @@ object CadenceTypeSerializer : KSerializer<Cadence.Type> {
}
}

/**
* A safe TransactionExecution serializer that can handle both simple enum strings and complex objects
* This prevents parsing failures when the Flow network returns complex objects instead of simple strings
*/
object SafeTransactionExecutionSerializer : KSerializer<TransactionExecution?> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("SafeTransactionExecution", PrimitiveKind.STRING)

override fun serialize(encoder: Encoder, value: TransactionExecution?) {
if (value != null) {
encoder.encodeString(value.value)
} else {
encoder.encodeNull()
}
}

override fun deserialize(decoder: Decoder): TransactionExecution? {
// 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 -> {
// Normal case - simple string value
TransactionExecution.decode(element.content)
}
is JsonObject -> {
// Complex object case - try to extract meaningful info or default to null
// For now, we'll just return null for complex objects
null
}
else -> null
}
} catch (e: Exception) {
null
}
} else {
// For non-JSON decoders, try string decoding
return try {
val stringValue = decoder.decodeString()
TransactionExecution.decode(stringValue)
} catch (e: Exception) {
null
}
}
}
}

//open class NewNumberSerializer<T : Any>(val type: KClass<T> ): KSerializer<T> {
// override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("value", PrimitiveKind.STRING)
// override fun serialize(encoder: Encoder, value: T) = encoder.encodeString(value.toString())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,14 @@ object Base64HexSerializer : KSerializer<String> {
object CadenceBase64Serializer : KSerializer<Cadence.Value> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("CadenceBase64", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: Cadence.Value) = encoder.encodeString(value.encodeBase64())
override fun deserialize(decoder: Decoder): Cadence.Value = Cadence.Value.decodeFromBase64(decoder.decodeString())
override fun deserialize(decoder: Decoder): Cadence.Value {
return try {
Cadence.Value.decodeFromBase64(decoder.decodeString())
} catch (e: Exception) {
// Graceful fallback for problematic Cadence values
Cadence.void()
}
}
}

class CadenceBase64ListSerializer : KSerializer<List<Cadence.Value>> {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.onflow.flow.models
import kotlinx.serialization.*
import org.onflow.flow.infrastructure.SafeStringSerializer
import org.onflow.flow.infrastructure.SafeTransactionExecutionSerializer

/**
*
Expand Down Expand Up @@ -33,7 +34,9 @@ data class TransactionResult (

@SerialName(value = "events") @Required val events: List<Event>,

@SerialName(value = "execution") val execution: TransactionExecution? = null,
@SerialName(value = "execution")
@Serializable(with = SafeTransactionExecutionSerializer::class)
val execution: TransactionExecution? = null,

@SerialName(value = "_links") val links: Links? = null

Expand Down
Loading
Loading