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.18"
version = System.getenv("GITHUB_REF")?.split('/')?.last() ?: defaultVersion
}

Expand Down
67 changes: 49 additions & 18 deletions flow/src/commonMain/kotlin/org/onflow/flow/apis/TransactionsApi.kt
Original file line number Diff line number Diff line change
Expand Up @@ -93,33 +93,64 @@ internal class TransactionsApi(val baseUrl: String) : ApiBase() {

internal suspend fun waitForSeal(transactionId: String): TransactionResult {
var attempts = 0
val maxAttempts = 30 // 30 seconds timeout
val maxAttempts = 60 // Increased to 60 attempts (up to 2 minutes)
var lastError: Exception? = null

while (attempts < maxAttempts) {
val result = getTransactionResult(transactionId)
when (result.status ?: TransactionStatus.EMPTY) {
TransactionStatus.SEALED -> {
if (result.errorMessage.isNotBlank() || result.execution == TransactionExecution.failure) {
throw RuntimeException("Transaction failed: ${result.errorMessage}")
try {
val result = getTransactionResult(transactionId)
when (result.status ?: TransactionStatus.EMPTY) {
TransactionStatus.SEALED -> {
if (result.errorMessage.isNotBlank() || result.execution == TransactionExecution.failure) {
throw RuntimeException("Transaction failed: ${result.errorMessage}")
}
return result
}
return result
}

TransactionStatus.EXPIRED -> throw RuntimeException("Transaction expired")
TransactionStatus.EMPTY, TransactionStatus.UNKNOWN -> {
// Treat empty/unknown status as pending
attempts++
delay(1000)
}
TransactionStatus.EXPIRED -> throw RuntimeException("Transaction expired")
TransactionStatus.EMPTY, TransactionStatus.UNKNOWN -> {
// Treat empty/unknown status as pending
attempts++
// Use exponential backoff with jitter for first few attempts, then stabilize
val delayMs = when {
attempts <= 5 -> 1000L // First 5 attempts: 1 second
attempts <= 15 -> 2000L // Next 10 attempts: 2 seconds
else -> 3000L // Remaining attempts: 3 seconds
}
delay(delayMs)
}

else -> {
attempts++
delay(1000)
else -> {
attempts++
val delayMs = when {
attempts <= 5 -> 1000L
attempts <= 15 -> 2000L
else -> 3000L
}
delay(delayMs)
}
}
} catch (e: Exception) {
lastError = e
attempts++


// If we're getting consistent errors, increase delay to avoid hammering the server
val errorDelayMs = when {
attempts <= 5 -> 2000L // 2 seconds for early failures
attempts <= 15 -> 5000L // 5 seconds for persistent failures
else -> 10000L // 10 seconds for repeated failures
}
delay(errorDelayMs)
}
}

throw RuntimeException("Transaction not sealed after $maxAttempts seconds")
val timeoutMessage = if (lastError != null) {
"Transaction not sealed after $maxAttempts attempts. Last error: ${lastError.message}"
} else {
"Transaction not sealed after $maxAttempts attempts"
}
throw RuntimeException(timeoutMessage)
}

private suspend fun resolveKeyIndex(address: FlowAddress, accountsApi: AccountsApi): Int {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package org.onflow.flow.infrastructure

import io.ktor.client.HttpClient
import io.ktor.client.plugins.HttpRequestRetry
import io.ktor.client.plugins.HttpTimeout
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.plugins.logging.DEFAULT
import io.ktor.client.plugins.logging.Logger
Expand All @@ -14,9 +15,29 @@ open class ApiBase {

companion object {
val client = HttpClient {
install(HttpTimeout) {
requestTimeoutMillis = 45000L // Increased to 45 seconds
connectTimeoutMillis = 20000L // Increased to 20 seconds
socketTimeoutMillis = 45000L // Increased to 45 seconds
}

install(HttpRequestRetry) {
retryOnServerErrors(maxRetries = 5)
exponentialDelay()
retryOnException(maxRetries = 3) { _, cause ->
// Retry on connection-related exceptions
cause.message?.contains("Connection reset by peer", ignoreCase = true) == true ||
cause.message?.contains("IOException", ignoreCase = true) == true ||
cause.message?.contains("ConnectException", ignoreCase = true) == true ||
cause.message?.contains("SocketTimeoutException", ignoreCase = true) == true ||
cause.message?.contains("Channel was closed", ignoreCase = true) == true ||
cause.message?.contains("ClosedReceiveChannelException", ignoreCase = true) == true ||
cause.message?.contains("ClosedSendChannelException", ignoreCase = true) == true ||
cause.message?.contains("TLS", ignoreCase = true) == true ||
// Check class names for Kotlin exceptions
cause.javaClass.simpleName.contains("ClosedReceiveChannelException", ignoreCase = true) ||
cause.javaClass.simpleName.contains("ClosedSendChannelException", ignoreCase = true)
}
exponentialDelay(base = 2.0, maxDelayMs = 10000L)
}

install(Logging) {
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) {
// Minimal fallback for problematic Cadence values (e.g., empty type fields)
Cadence.void()
}
}
}

class CadenceBase64ListSerializer : KSerializer<List<Cadence.Value>> {
Expand Down
Loading
Loading