From 164cff8576e97b3f42b491fb8e69557e0ade8674 Mon Sep 17 00:00:00 2001 From: Lea Lobanov Date: Thu, 19 Jun 2025 02:07:08 +0900 Subject: [PATCH 01/10] Bump version --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index 1f6d80d..18fd739 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -22,7 +22,7 @@ allprojects { } group = "org.onflow.flow" - val defaultVersion = "0.0.18" + val defaultVersion = "0.0.19" version = System.getenv("GITHUB_REF")?.split('/')?.last() ?: defaultVersion } From 5e230243d61b131ef550fa3e6a499014441040f2 Mon Sep 17 00:00:00 2001 From: Lea Lobanov Date: Thu, 19 Jun 2025 02:22:31 +0900 Subject: [PATCH 02/10] Bump version --- build.gradle.kts | 2 +- .../org/onflow/flow/infrastructure/ApiBase.kt | 21 ++++--------------- 2 files changed, 5 insertions(+), 18 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 18fd739..dff683f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -22,7 +22,7 @@ allprojects { } group = "org.onflow.flow" - val defaultVersion = "0.0.19" + val defaultVersion = "0.0.20" version = System.getenv("GITHUB_REF")?.split('/')?.last() ?: defaultVersion } diff --git a/flow/src/commonMain/kotlin/org/onflow/flow/infrastructure/ApiBase.kt b/flow/src/commonMain/kotlin/org/onflow/flow/infrastructure/ApiBase.kt index 6d1822a..4d958f8 100644 --- a/flow/src/commonMain/kotlin/org/onflow/flow/infrastructure/ApiBase.kt +++ b/flow/src/commonMain/kotlin/org/onflow/flow/infrastructure/ApiBase.kt @@ -20,24 +20,11 @@ open class ApiBase { connectTimeoutMillis = 20000L // Increased to 20 seconds socketTimeoutMillis = 45000L // Increased to 45 seconds } - + install(HttpRequestRetry) { - retryOnServerErrors(maxRetries = 5) - 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) + retryOnServerErrors(maxRetries = 3) + retryOnException(maxRetries = 3, retryOnTimeout = true) + exponentialDelay() // Use exponential backoff } install(Logging) { From 7f2f12e161bcd45cb68230781bd8a3b627b97698 Mon Sep 17 00:00:00 2001 From: Lea Lobanov Date: Thu, 19 Jun 2025 02:27:49 +0900 Subject: [PATCH 03/10] Bump version --- .../org/onflow/flow/infrastructure/ApiBase.kt | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/flow/src/commonMain/kotlin/org/onflow/flow/infrastructure/ApiBase.kt b/flow/src/commonMain/kotlin/org/onflow/flow/infrastructure/ApiBase.kt index 4d958f8..613d5cd 100644 --- a/flow/src/commonMain/kotlin/org/onflow/flow/infrastructure/ApiBase.kt +++ b/flow/src/commonMain/kotlin/org/onflow/flow/infrastructure/ApiBase.kt @@ -2,7 +2,6 @@ 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 @@ -15,16 +14,9 @@ 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 = 3) - retryOnException(maxRetries = 3, retryOnTimeout = true) - exponentialDelay() // Use exponential backoff + retryOnServerErrors(maxRetries = 5) + exponentialDelay() } install(Logging) { @@ -42,4 +34,4 @@ open class ApiBase { } } -} +} \ No newline at end of file From a2165a0652dbb53dc27681f036294017204fafe5 Mon Sep 17 00:00:00 2001 From: Lea Lobanov Date: Thu, 19 Jun 2025 02:31:09 +0900 Subject: [PATCH 04/10] Bump version --- .../org/onflow/flow/FlowTransactionTests.kt | 154 ------------------ 1 file changed, 154 deletions(-) diff --git a/flow/src/commonTest/kotlin/org/onflow/flow/FlowTransactionTests.kt b/flow/src/commonTest/kotlin/org/onflow/flow/FlowTransactionTests.kt index d5b1dc2..ac69d12 100644 --- a/flow/src/commonTest/kotlin/org/onflow/flow/FlowTransactionTests.kt +++ b/flow/src/commonTest/kotlin/org/onflow/flow/FlowTransactionTests.kt @@ -498,160 +498,6 @@ class FlowTransactionTests { println("✅ Signature debug test completed") } - /** - * Debug test to verify multi-signer signing process - */ - @OptIn(ExperimentalStdlibApi::class) - @Test - fun testMultiSignerDebug() = runBlocking { - // ===== SETUP ===== - val proposerAddress = "c6de0d94160377cd" - val proposerPrivateKey = "c9c0f04adddf7674d265c395de300a65a777d3ec412bba5bfdfd12cffbbb78d9" - val payerAddress = "10711015c370a95c" - val payerPrivateKey = "38ebd09b83e221e406b176044a65350333b3a5280ed3f67227bd80d55ac91a0f" - - // Get on-chain account information - val proposerAccount = api.getAccount(proposerAddress) - val payerAccount = api.getAccount(payerAddress) - val proposerKey = proposerAccount.keys!!.first { it.index.toInt() == 0 } - val payerKey = payerAccount.keys!!.first { it.index.toInt() == 0 } - - // Create signers with on-chain algorithms - val proposerSigner = Crypto.getSigner( - Crypto.decodePrivateKey(proposerPrivateKey, proposerKey.signingAlgorithm), - proposerKey.hashingAlgorithm - ).apply { - address = proposerAddress - keyIndex = 0 - } - - val payerSigner = Crypto.getSigner( - Crypto.decodePrivateKey(payerPrivateKey, payerKey.signingAlgorithm), - payerKey.hashingAlgorithm - ).apply { - address = payerAddress - keyIndex = 0 - } - - val latestBlock = api.getBlockHeader() - - // ===== TRANSACTION BUILDING ===== - val transaction = TransactionBuilder( - script = """ - transaction { - prepare(signer: auth(Storage) &Account) { - log("Multi-signer debug test") - } - } - """.trimIndent() - ) - .withProposalKey(proposerAddress, 0, proposerKey.sequenceNumber.toBigInteger()) - .withPayer(payerAddress) - .withAuthorizers(listOf(proposerAddress)) - .withReferenceBlockId(latestBlock.id) - .build() - - println("MULTI-SIGNER DEBUG:") - println("Initial transaction payload signatures: ${transaction.payloadSignatures.size}") - println("Initial transaction envelope signatures: ${transaction.envelopeSignatures.size}") - - // Step 1: Test payload signing manually (corrected approach) - val payloadMessageToSign = transaction.payloadMessage() - val proposerPayloadSig = proposerSigner.sign(payloadMessageToSign) // Fix: Use corrected approach - - println("Payload message to sign: ${payloadMessageToSign.toHexString()}") - println("Proposer payload signature: ${proposerPayloadSig.toHexString()}") - - // Add the payload signature - val txWithPayloadSig = transaction.copy( - payloadSignatures = listOf( - TransactionSignature( - address = proposerAddress, - keyIndex = 0, - signature = proposerPayloadSig.toHexString() - ) - ) - ) - - println("Transaction after adding payload sig - payloadSignatures: ${txWithPayloadSig.payloadSignatures.size}") - - // Step 2: Test envelope signing manually (corrected approach) - val envelopeMessageToSign = txWithPayloadSig.envelopeMessage() - val payerEnvelopeSig = payerSigner.sign(envelopeMessageToSign) // Fix: Use corrected approach - - println("Envelope message to sign: ${envelopeMessageToSign.toHexString()}") - println("Payer envelope signature: ${payerEnvelopeSig.toHexString()}") - - // Test automatic signing vs manual signing - val autoSignedTransaction = transaction.sign(listOf(proposerSigner, payerSigner)) - - println("Auto-signed transaction - payloadSignatures: ${autoSignedTransaction.payloadSignatures.size}") - println("Auto-signed transaction - envelopeSignatures: ${autoSignedTransaction.envelopeSignatures.size}") - - if (autoSignedTransaction.payloadSignatures.isNotEmpty()) { - println("Auto payload signature: ${autoSignedTransaction.payloadSignatures[0].signature}") - } - if (autoSignedTransaction.envelopeSignatures.isNotEmpty()) { - println("Auto envelope signature: ${autoSignedTransaction.envelopeSignatures[0].signature}") - } - - // Compare signatures - val manualPayloadSig = proposerPayloadSig.toHexString() - val autoPayloadSig = if (autoSignedTransaction.payloadSignatures.isNotEmpty()) - autoSignedTransaction.payloadSignatures[0].signature else "NONE" - - val manualEnvelopeSig = payerEnvelopeSig.toHexString() - val autoEnvelopeSig = if (autoSignedTransaction.envelopeSignatures.isNotEmpty()) - autoSignedTransaction.envelopeSignatures[0].signature else "NONE" - - println("Manual vs Auto payload signature match: ${manualPayloadSig == autoPayloadSig}") - println("Manual vs Auto envelope signature match: ${manualEnvelopeSig == autoEnvelopeSig}") - - // The key test: Do both payload messages for signing match? - val manualPayloadMessage = transaction.payloadMessage() - val autoPayloadMessage = transaction.payloadMessage() // These should be identical - - println("Manual vs Auto payload message match: ${manualPayloadMessage.contentEquals(autoPayloadMessage)}") - - // CRITICAL TEST: Try submitting the manual transaction - println("Testing manual transaction submission...") - val manualTransaction = txWithPayloadSig.copy( - envelopeSignatures = listOf( - TransactionSignature( - address = payerAddress, - keyIndex = 0, - signature = payerEnvelopeSig.toHexString() - ) - ) - ) - - try { - val manualResult = api.sendTransaction(manualTransaction) - println("Manual transaction submitted successfully: ${manualResult.id}") - - api.waitForSeal(manualResult.id!!) - println("Manual transaction sealed successfully!") - - } catch (e: Exception) { - println("Manual transaction failed: ${e.message}") - } - - // TEST: Try submitting the automatic transaction - println("Testing automatic transaction submission...") - try { - val autoResult = api.sendTransaction(autoSignedTransaction) - println("Auto transaction submitted successfully: ${autoResult.id}") - - api.waitForSeal(autoResult.id!!) - println("Auto transaction sealed successfully!") - - } catch (e: Exception) { - println("Auto transaction failed: ${e.message}") - } - - println("✅ Multi-signer debug test completed") - } - /** * Test UFix64 scientific notation handling in real transactions * This test verifies that values like 1.0E-8 are properly encoded and don't cause transaction failures From e2f831191a00d9b2c61d12bb9d1803be43f32a78 Mon Sep 17 00:00:00 2001 From: Lea Lobanov Date: Thu, 19 Jun 2025 09:16:34 +0900 Subject: [PATCH 05/10] Wait for FINAL state --- flow/src/commonMain/kotlin/org/onflow/flow/FlowApi.kt | 8 ++++---- .../kotlin/org/onflow/flow/apis/AccountsApi.kt | 2 +- .../kotlin/org/onflow/flow/apis/BlocksApi.kt | 4 ++-- .../kotlin/org/onflow/flow/apis/ScriptsApi.kt | 2 +- .../kotlin/org/onflow/flow/apis/TransactionsApi.kt | 11 +++++------ 5 files changed, 13 insertions(+), 14 deletions(-) diff --git a/flow/src/commonMain/kotlin/org/onflow/flow/FlowApi.kt b/flow/src/commonMain/kotlin/org/onflow/flow/FlowApi.kt index fac1442..b3d0b54 100644 --- a/flow/src/commonMain/kotlin/org/onflow/flow/FlowApi.kt +++ b/flow/src/commonMain/kotlin/org/onflow/flow/FlowApi.kt @@ -29,18 +29,18 @@ class FlowApi(val chainId: ChainIdProvider) { private val scriptsApi = ScriptsApi(baseUrl) private val transactionsApi = TransactionsApi(baseUrl) - suspend fun getAccount(address: String, blockHeight: String? = null, blockStatus: BlockStatus = BlockStatus.SEALED): Account { + suspend fun getAccount(address: String, blockHeight: String? = null, blockStatus: BlockStatus = BlockStatus.FINAL): Account { return accountsApi.getAccount(address, blockHeight, blockStatus) } - suspend fun getBlock(id: String? = null, blockHeight: String? = null, blockStatus: BlockStatus = BlockStatus.SEALED): Block { + suspend fun getBlock(id: String? = null, blockHeight: String? = null, blockStatus: BlockStatus = BlockStatus.FINAL): Block { return blocksApi.getBlock(id, blockHeight, blockStatus) } suspend fun getBlockHeader( id: String? = null, blockHeight: String? = null, - blockStatus: BlockStatus = BlockStatus.SEALED + blockStatus: BlockStatus = BlockStatus.FINAL ): BlockHeader { return blocksApi.getBlockHeader(id, blockHeight, blockStatus) } @@ -71,7 +71,7 @@ class FlowApi(val chainId: ChainIdProvider) { arguments: List? = null, blockId: String? = null, blockHeight: String? = null, - blockStatus: BlockStatus = BlockStatus.SEALED + blockStatus: BlockStatus = BlockStatus.FINAL ): Cadence.Value { return scriptsApi.executeScript(script, arguments, blockId, blockHeight, blockStatus) } diff --git a/flow/src/commonMain/kotlin/org/onflow/flow/apis/AccountsApi.kt b/flow/src/commonMain/kotlin/org/onflow/flow/apis/AccountsApi.kt index 3e18f64..0a474fb 100644 --- a/flow/src/commonMain/kotlin/org/onflow/flow/apis/AccountsApi.kt +++ b/flow/src/commonMain/kotlin/org/onflow/flow/apis/AccountsApi.kt @@ -37,7 +37,7 @@ internal class AccountsApi(val baseUrl: String) : ApiBase() { * @param blockStatus The status of the block to query (FINAL or SEALED). Defaults to FINAL. * @return Account */ - internal suspend fun getAccount(address: String, blockHeight: String? = null, blockStatus: BlockStatus = BlockStatus.SEALED): Account { + internal suspend fun getAccount(address: String, blockHeight: String? = null, blockStatus: BlockStatus = BlockStatus.FINAL): Account { val expand = setOf("contracts", "keys") return if (blockHeight != null) { request(address, blockHeight, expand) diff --git a/flow/src/commonMain/kotlin/org/onflow/flow/apis/BlocksApi.kt b/flow/src/commonMain/kotlin/org/onflow/flow/apis/BlocksApi.kt index 1945f93..26e32a4 100644 --- a/flow/src/commonMain/kotlin/org/onflow/flow/apis/BlocksApi.kt +++ b/flow/src/commonMain/kotlin/org/onflow/flow/apis/BlocksApi.kt @@ -54,7 +54,7 @@ internal class BlocksApi(val baseUrl: String) : ApiBase() { }.body() } - internal suspend fun getBlock(id: String? = null, blockHeight: String? = null, blockStatus: BlockStatus = BlockStatus.SEALED): Block { + internal suspend fun getBlock(id: String? = null, blockHeight: String? = null, blockStatus: BlockStatus = BlockStatus.FINAL): Block { val expand = setOf("payload") return if (id != null) { requestBlocksById(id, expand).first() @@ -68,7 +68,7 @@ internal class BlocksApi(val baseUrl: String) : ApiBase() { internal suspend fun getBlockHeader( id: String? = null, blockHeight: String? = null, - blockStatus: BlockStatus = BlockStatus.SEALED + blockStatus: BlockStatus = BlockStatus.FINAL ): BlockHeader { return getBlock(id, blockHeight, blockStatus).header } diff --git a/flow/src/commonMain/kotlin/org/onflow/flow/apis/ScriptsApi.kt b/flow/src/commonMain/kotlin/org/onflow/flow/apis/ScriptsApi.kt index 7a35d0c..d2e0271 100644 --- a/flow/src/commonMain/kotlin/org/onflow/flow/apis/ScriptsApi.kt +++ b/flow/src/commonMain/kotlin/org/onflow/flow/apis/ScriptsApi.kt @@ -56,7 +56,7 @@ class ScriptsApi(val baseUrl: String) : ApiBase() { arguments: List? = null, blockId: String? = null, blockHeight: String? = null, - blockStatus: BlockStatus = BlockStatus.SEALED + blockStatus: BlockStatus = BlockStatus.FINAL ): Cadence.Value { val request = ScriptsPostRequest( script.encodeBase64(), diff --git a/flow/src/commonMain/kotlin/org/onflow/flow/apis/TransactionsApi.kt b/flow/src/commonMain/kotlin/org/onflow/flow/apis/TransactionsApi.kt index def7aed..cb94e45 100644 --- a/flow/src/commonMain/kotlin/org/onflow/flow/apis/TransactionsApi.kt +++ b/flow/src/commonMain/kotlin/org/onflow/flow/apis/TransactionsApi.kt @@ -100,10 +100,9 @@ internal class TransactionsApi(val baseUrl: String) : ApiBase() { 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}") - } + TransactionStatus.FINALIZED -> { + // Transaction is finalized, return immediately without checking for errors + // since finalized transactions haven't been executed yet return result } @@ -146,9 +145,9 @@ internal class TransactionsApi(val baseUrl: String) : ApiBase() { } val timeoutMessage = if (lastError != null) { - "Transaction not sealed after $maxAttempts attempts. Last error: ${lastError.message}" + "Transaction not finalized after $maxAttempts attempts. Last error: ${lastError.message}" } else { - "Transaction not sealed after $maxAttempts attempts" + "Transaction not finalized after $maxAttempts attempts" } throw RuntimeException(timeoutMessage) } From 377bc4b53955fdd35273112a7fee76acb45eba82 Mon Sep 17 00:00:00 2001 From: Lea Lobanov Date: Thu, 19 Jun 2025 09:16:50 +0900 Subject: [PATCH 06/10] Wait for FINAL state --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index dff683f..6f65ce5 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -22,7 +22,7 @@ allprojects { } group = "org.onflow.flow" - val defaultVersion = "0.0.20" + val defaultVersion = "0.0.21" version = System.getenv("GITHUB_REF")?.split('/')?.last() ?: defaultVersion } From 1e6c8d4b6007d5fb10c0b19e53ae1ffc271b5359 Mon Sep 17 00:00:00 2001 From: Lea Lobanov Date: Thu, 19 Jun 2025 09:53:54 +0900 Subject: [PATCH 07/10] Removed sealed classes --- build.gradle.kts | 2 +- .../onflow/flow/websocket/FlowWebSocketMessage.kt | 12 +++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 6f65ce5..1c8da1d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -22,7 +22,7 @@ allprojects { } group = "org.onflow.flow" - val defaultVersion = "0.0.21" + val defaultVersion = "0.0.22" version = System.getenv("GITHUB_REF")?.split('/')?.last() ?: defaultVersion } diff --git a/flow/src/commonMain/kotlin/org/onflow/flow/websocket/FlowWebSocketMessage.kt b/flow/src/commonMain/kotlin/org/onflow/flow/websocket/FlowWebSocketMessage.kt index 09b0237..fbfa897 100644 --- a/flow/src/commonMain/kotlin/org/onflow/flow/websocket/FlowWebSocketMessage.kt +++ b/flow/src/commonMain/kotlin/org/onflow/flow/websocket/FlowWebSocketMessage.kt @@ -6,9 +6,11 @@ import kotlinx.serialization.json.JsonElement import org.onflow.flow.models.Event import org.onflow.flow.models.TransactionResult -@Serializable -sealed class FlowWebSocketMessage { - abstract val subscriptionId: String? +/** + * Base interface for WebSocket messages to avoid sealed class ASM transformation issues + */ +interface FlowWebSocketMessage { + val subscriptionId: String? } @Serializable @@ -18,7 +20,7 @@ data class FlowWebSocketRequest( val action: String, val topic: String? = null, val arguments: Map? = null -) : FlowWebSocketMessage() +) : FlowWebSocketMessage @Serializable data class FlowWebSocketResponse( @@ -28,7 +30,7 @@ data class FlowWebSocketResponse( val topic: String? = null, val payload: JsonElement? = null, val error: FlowWebSocketError? = null -) : FlowWebSocketMessage() +) : FlowWebSocketMessage @Serializable data class BlockEventPayload( From b6cc6dc65a2778d2087560454c82bb5924f3f755 Mon Sep 17 00:00:00 2001 From: Lea Lobanov Date: Thu, 19 Jun 2025 10:13:06 +0900 Subject: [PATCH 08/10] Removed sealed classes --- build.gradle.kts | 2 +- flow/build.gradle.kts | 1 - .../kotlin/org/onflow/flow/websocket/FlowWebSocketClient.kt | 6 +++++- .../kotlin/org/onflow/flow/FlowTransactionTests.kt | 2 -- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 1c8da1d..6c4d707 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -22,7 +22,7 @@ allprojects { } group = "org.onflow.flow" - val defaultVersion = "0.0.22" + val defaultVersion = "0.0.23" version = System.getenv("GITHUB_REF")?.split('/')?.last() ?: defaultVersion } diff --git a/flow/build.gradle.kts b/flow/build.gradle.kts index 99825b1..9e55a1c 100644 --- a/flow/build.gradle.kts +++ b/flow/build.gradle.kts @@ -142,4 +142,3 @@ tasks.withType().configureEach { tasks.named("build") { finalizedBy("createXCFramework") } tasks.named("clean") { doFirst { delete("swiftpackage") } } - diff --git a/flow/src/commonMain/kotlin/org/onflow/flow/websocket/FlowWebSocketClient.kt b/flow/src/commonMain/kotlin/org/onflow/flow/websocket/FlowWebSocketClient.kt index 648a055..4f6f81c 100644 --- a/flow/src/commonMain/kotlin/org/onflow/flow/websocket/FlowWebSocketClient.kt +++ b/flow/src/commonMain/kotlin/org/onflow/flow/websocket/FlowWebSocketClient.kt @@ -230,7 +230,11 @@ class FlowWebSocketClient( private suspend fun sendMessage(message: FlowWebSocketMessage) { val session = webSocketSession ?: throw IllegalStateException("Not connected") - val text = json.encodeToString(FlowWebSocketMessage.serializer(), message) + val text = when (message) { + is FlowWebSocketRequest -> json.encodeToString(FlowWebSocketRequest.serializer(), message) + is FlowWebSocketResponse -> json.encodeToString(FlowWebSocketResponse.serializer(), message) + else -> throw IllegalArgumentException("Unknown message type: ${message::class}") + } session.send(Frame.Text(text)) } diff --git a/flow/src/commonTest/kotlin/org/onflow/flow/FlowTransactionTests.kt b/flow/src/commonTest/kotlin/org/onflow/flow/FlowTransactionTests.kt index ac69d12..941d1ff 100644 --- a/flow/src/commonTest/kotlin/org/onflow/flow/FlowTransactionTests.kt +++ b/flow/src/commonTest/kotlin/org/onflow/flow/FlowTransactionTests.kt @@ -7,10 +7,8 @@ import org.onflow.flow.infrastructure.Cadence import org.onflow.flow.models.CadenceBase64Serializer import org.onflow.flow.models.TransactionBuilder import org.onflow.flow.models.TransactionStatus -import org.onflow.flow.models.TransactionSignature import org.onflow.flow.models.createSigningRLP import org.onflow.flow.models.createSigningRLPJVMStyle -import org.onflow.flow.models.envelopeMessage import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue From fe40174a1c6eead4b2f770bb8db657d1307041e9 Mon Sep 17 00:00:00 2001 From: Lea Lobanov Date: Thu, 19 Jun 2025 10:39:11 +0900 Subject: [PATCH 09/10] Update waitForSeal --- build.gradle.kts | 2 +- .../org/onflow/flow/apis/TransactionsApi.kt | 23 ++++++++----------- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 6c4d707..2293a4f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -22,7 +22,7 @@ allprojects { } group = "org.onflow.flow" - val defaultVersion = "0.0.23" + val defaultVersion = "0.0.24" version = System.getenv("GITHUB_REF")?.split('/')?.last() ?: defaultVersion } diff --git a/flow/src/commonMain/kotlin/org/onflow/flow/apis/TransactionsApi.kt b/flow/src/commonMain/kotlin/org/onflow/flow/apis/TransactionsApi.kt index cb94e45..567b320 100644 --- a/flow/src/commonMain/kotlin/org/onflow/flow/apis/TransactionsApi.kt +++ b/flow/src/commonMain/kotlin/org/onflow/flow/apis/TransactionsApi.kt @@ -105,10 +105,17 @@ internal class TransactionsApi(val baseUrl: String) : ApiBase() { // since finalized transactions haven't been executed yet return result } - + TransactionStatus.EXECUTED -> { + // Transaction is executed, return result + return result + } + TransactionStatus.SEALED -> { + // Transaction is sealed, return result + return result + } TransactionStatus.EXPIRED -> throw RuntimeException("Transaction expired") - TransactionStatus.EMPTY, TransactionStatus.UNKNOWN -> { - // Treat empty/unknown status as pending + TransactionStatus.EMPTY, TransactionStatus.UNKNOWN, TransactionStatus.PENDING -> { + // Treat empty/unknown/pending status as still processing, continue polling attempts++ // Use exponential backoff with jitter for first few attempts, then stabilize val delayMs = when { @@ -118,16 +125,6 @@ internal class TransactionsApi(val baseUrl: String) : ApiBase() { } delay(delayMs) } - - else -> { - attempts++ - val delayMs = when { - attempts <= 5 -> 1000L - attempts <= 15 -> 2000L - else -> 3000L - } - delay(delayMs) - } } } catch (e: Exception) { lastError = e From 97b15eb08ae8368984838874fea444842222cc4c Mon Sep 17 00:00:00 2001 From: Lea Lobanov Date: Mon, 7 Jul 2025 12:37:37 +0800 Subject: [PATCH 10/10] Update fixed point formatter --- build.gradle.kts | 2 +- .../infrastructure/FixedPointFormatter.kt | 8 +- .../org/onflow/flow/FlowTransactionTests.kt | 175 +++++++++++++++++- .../DoubleCadenceSerializerTest.kt | 54 +++++- .../infrastructure/FixedPointFormatterTest.kt | 99 ++++++++++ 5 files changed, 327 insertions(+), 11 deletions(-) create mode 100644 flow/src/commonTest/kotlin/org/onflow/flow/infrastructure/FixedPointFormatterTest.kt diff --git a/build.gradle.kts b/build.gradle.kts index 2293a4f..c48637e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -22,7 +22,7 @@ allprojects { } group = "org.onflow.flow" - val defaultVersion = "0.0.24" + val defaultVersion = "0.0.25" version = System.getenv("GITHUB_REF")?.split('/')?.last() ?: defaultVersion } diff --git a/flow/src/commonMain/kotlin/org/onflow/flow/infrastructure/FixedPointFormatter.kt b/flow/src/commonMain/kotlin/org/onflow/flow/infrastructure/FixedPointFormatter.kt index ba38af7..0d93b41 100644 --- a/flow/src/commonMain/kotlin/org/onflow/flow/infrastructure/FixedPointFormatter.kt +++ b/flow/src/commonMain/kotlin/org/onflow/flow/infrastructure/FixedPointFormatter.kt @@ -3,9 +3,15 @@ package org.onflow.flow.infrastructure object FixedPointFormatter { fun format(num: String, precision: ULong): String? { return try { + // Handle empty or invalid input + if (num.isEmpty() || num.isBlank()) { + return null + } + val parts = num.split(".") if (parts.size == 1) { - num + // For whole numbers (including "0"), add decimal point with appropriate precision + "$num.0" } else { val integerPart = parts[0] val decimalPart = parts[1].take(precision.toInt()) diff --git a/flow/src/commonTest/kotlin/org/onflow/flow/FlowTransactionTests.kt b/flow/src/commonTest/kotlin/org/onflow/flow/FlowTransactionTests.kt index 941d1ff..157451e 100644 --- a/flow/src/commonTest/kotlin/org/onflow/flow/FlowTransactionTests.kt +++ b/flow/src/commonTest/kotlin/org/onflow/flow/FlowTransactionTests.kt @@ -84,7 +84,7 @@ class FlowTransactionTests { api.waitForSeal(result.id!!) val finalResult = api.getTransactionResult(result.id!!) - assertEquals(TransactionStatus.SEALED, finalResult.status) + assertEquals(TransactionStatus.EXECUTED, finalResult.status) println("✅ Gas sponsored transaction sealed successfully") } @@ -139,7 +139,7 @@ class FlowTransactionTests { api.waitForSeal(result.id!!) val finalResult = api.getTransactionResult(result.id!!) - assertEquals(TransactionStatus.SEALED, finalResult.status) + assertEquals(TransactionStatus.EXECUTED, finalResult.status) println("✅ Single signer transaction sealed successfully") } @@ -193,7 +193,7 @@ class FlowTransactionTests { api.waitForSeal(result.id!!) val finalResult = api.getTransactionResult(result.id!!) - assertEquals(TransactionStatus.SEALED, finalResult.status) + assertEquals(TransactionStatus.EXECUTED, finalResult.status) println("✅ Proposer solo transaction sealed successfully") } @@ -247,7 +247,7 @@ class FlowTransactionTests { api.waitForSeal(result.id!!) val finalResult = api.getTransactionResult(result.id!!) - assertEquals(TransactionStatus.SEALED, finalResult.status) + assertEquals(TransactionStatus.EXECUTED, finalResult.status) println("✅ Payer solo transaction sealed successfully") } @@ -324,7 +324,7 @@ class FlowTransactionTests { api.waitForSeal(result.id!!) val finalResult = api.getTransactionResult(result.id!!) - assertEquals(TransactionStatus.SEALED, finalResult.status) + assertEquals(TransactionStatus.EXECUTED, finalResult.status) println("✅ Manual step-by-step transaction sealed successfully") } @@ -557,7 +557,7 @@ class FlowTransactionTests { api.waitForSeal(result.id!!) val finalResult = api.getTransactionResult(result.id!!) - assertEquals(TransactionStatus.SEALED, finalResult.status) + assertEquals(TransactionStatus.EXECUTED, finalResult.status) // Verify no errors occurred - this would previously fail with UFix64 encoding error assertTrue(finalResult.errorMessage.isNullOrEmpty(), "Transaction should not have any errors") @@ -567,6 +567,167 @@ class FlowTransactionTests { println(" Transaction ID: ${result.id}") } + /** + * Test UFix64 zero value handling to prevent "missing decimal point" error + * This test specifically addresses the issue where NFT transfers with zero amounts + * were failing with "invalid UFix64: missing decimal point" errors. + */ + @Test + fun testUFix64ZeroValueTransaction() = runBlocking { + // ===== SETUP ===== + val accountAddress = "c6de0d94160377cd" + val privateKey = "c9c0f04adddf7674d265c395de300a65a777d3ec412bba5bfdfd12cffbbb78d9" + + // Get on-chain account information + val account = api.getAccount(accountAddress) + assertNotNull(account, "Account should exist") + val accountKey = account.keys!!.first { it.index.toInt() == 0 } + + // Create signer with on-chain algorithms + val signer = Crypto.getSigner( + Crypto.decodePrivateKey(privateKey, accountKey.signingAlgorithm), + accountKey.hashingAlgorithm + ).apply { + address = accountAddress + keyIndex = 0 + } + + val latestBlock = api.getBlockHeader() + + // ===== TEST ZERO VALUE ===== + val zeroValue = 0.0 // This is the problematic case for NFT transfers + + val transaction = TransactionBuilder( + script = """ + transaction(amount: UFix64) { + prepare(signer: auth(Storage) &Account) { + log("Testing UFix64 zero value: ".concat(amount.toString())) + + // Verify the amount is exactly zero + assert(amount == 0.0, message: "Amount should be 0.0") + + log("UFix64 zero value test passed!") + } + } + """.trimIndent(), + arguments = listOf(Cadence.ufix64(zeroValue)) + ) + .withProposalKey(accountAddress, 0, accountKey.sequenceNumber.toBigInteger()) + .withPayer(accountAddress) + .withAuthorizers(listOf(accountAddress)) + .withReferenceBlockId(latestBlock.id) + .build() + + // ===== SIGNING ===== + val signedTransaction = transaction.sign(listOf(signer)) + + // ===== SUBMISSION AND VERIFICATION ===== + val result = api.sendTransaction(signedTransaction) + assertNotNull(result.id, "Transaction ID should not be null") + + api.waitForSeal(result.id!!) + val finalResult = api.getTransactionResult(result.id!!) + + assertEquals(TransactionStatus.EXECUTED, finalResult.status) + + // This is the key test: verify no "missing decimal point" error occurs + assertTrue(finalResult.errorMessage.isNullOrEmpty(), + "Transaction should not have 'missing decimal point' error. Error: ${finalResult.errorMessage}") + + println("✅ UFix64 zero value transaction sealed successfully") + println(" Zero value: $zeroValue") + println(" Transaction ID: ${result.id}") + println(" This confirms zero values no longer cause 'missing decimal point' errors") + } + + /** + * Test callContract scenario with zero amount (simulating NFT transfer) + * This test mimics the exact scenario that was failing: callContract cadence + * with amount=0 for NFT transfers from COA to COA/EOA. + */ + @Test + fun testCallContractWithZeroAmountTransaction() = runBlocking { + // ===== SETUP ===== + val accountAddress = "c6de0d94160377cd" + val privateKey = "c9c0f04adddf7674d265c395de300a65a777d3ec412bba5bfdfd12cffbbb78d9" + + // Get on-chain account information + val account = api.getAccount(accountAddress) + assertNotNull(account, "Account should exist") + val accountKey = account.keys!!.first { it.index.toInt() == 0 } + + // Create signer with on-chain algorithms + val signer = Crypto.getSigner( + Crypto.decodePrivateKey(privateKey, accountKey.signingAlgorithm), + accountKey.hashingAlgorithm + ).apply { + address = accountAddress + keyIndex = 0 + } + + val latestBlock = api.getBlockHeader() + + // ===== TEST CALL CONTRACT WITH ZERO AMOUNT ===== + // This simulates the exact callContract scenario for NFT transfers + val zeroAmount = 0.0 // Amount assigned when sending NFT (no Flow tokens) + val contractAddress = "0x1234567890abcdef" // Mock contract address + val callData = "0x00" // Mock call data + + val transaction = TransactionBuilder( + script = """ + transaction(contractAddress: String, amount: UFix64, callData: String) { + prepare(signer: auth(Storage) &Account) { + log("Testing callContract with zero amount") + log("Contract: ".concat(contractAddress)) + log("Amount: ".concat(amount.toString())) + log("CallData: ".concat(callData)) + + // Verify the amount is exactly zero (NFT transfer case) + assert(amount == 0.0, message: "Amount should be 0.0 for NFT transfers") + + // Verify other parameters are present + assert(contractAddress != "", message: "Contract address should not be empty") + assert(callData != "", message: "Call data should not be empty") + + log("CallContract with zero amount test passed!") + } + } + """.trimIndent(), + arguments = listOf( + Cadence.string(contractAddress), + Cadence.ufix64(zeroAmount), // This was causing the "missing decimal point" error + Cadence.string(callData) + ) + ) + .withProposalKey(accountAddress, 0, accountKey.sequenceNumber.toBigInteger()) + .withPayer(accountAddress) + .withAuthorizers(listOf(accountAddress)) + .withReferenceBlockId(latestBlock.id) + .build() + + // ===== SIGNING ===== + val signedTransaction = transaction.sign(listOf(signer)) + + // ===== SUBMISSION AND VERIFICATION ===== + val result = api.sendTransaction(signedTransaction) + assertNotNull(result.id, "Transaction ID should not be null") + + api.waitForSeal(result.id!!) + val finalResult = api.getTransactionResult(result.id!!) + + assertEquals(TransactionStatus.EXECUTED, finalResult.status) + + // This is the critical test: verify no UFix64 decimal point error occurs + assertTrue(finalResult.errorMessage.isNullOrEmpty(), + "CallContract with zero amount should not fail with UFix64 error. Error: ${finalResult.errorMessage}") + + println("✅ CallContract with zero amount transaction sealed successfully") + println(" Zero amount: $zeroAmount") + println(" Contract: $contractAddress") + println(" Transaction ID: ${result.id}") + println(" This confirms NFT transfers with zero amounts no longer fail") + } + /** * Test multiple UFix64 values including edge cases */ @@ -636,7 +797,7 @@ class FlowTransactionTests { api.waitForSeal(result.id!!) val finalResult = api.getTransactionResult(result.id!!) - assertEquals(TransactionStatus.SEALED, finalResult.status) + assertEquals(TransactionStatus.EXECUTED, finalResult.status) assertTrue(finalResult.errorMessage.isNullOrEmpty(), "Transaction should not have any errors") println("✅ Multiple UFix64 values transaction sealed successfully") diff --git a/flow/src/commonTest/kotlin/org/onflow/flow/infrastructure/DoubleCadenceSerializerTest.kt b/flow/src/commonTest/kotlin/org/onflow/flow/infrastructure/DoubleCadenceSerializerTest.kt index 0ddbaf4..87a6377 100644 --- a/flow/src/commonTest/kotlin/org/onflow/flow/infrastructure/DoubleCadenceSerializerTest.kt +++ b/flow/src/commonTest/kotlin/org/onflow/flow/infrastructure/DoubleCadenceSerializerTest.kt @@ -68,8 +68,8 @@ class DoubleCadenceSerializerTest { val json = Json.encodeToString(ufixValue) - assertTrue("Should contain zero") { - json.contains("\"0\"") + assertTrue("Should contain zero with decimal point") { + json.contains("\"0.0\"") } } @@ -94,6 +94,56 @@ class DoubleCadenceSerializerTest { } } + @Test + fun testUFix64ZeroValueSerialization() { + // Test the exact scenario that was failing: UFix64 with zero value + val zeroValue = 0.0 + val ufixValue = Cadence.ufix64(zeroValue) + + // Serialize to JSON + val json = Json.encodeToString(ufixValue) + println("Zero value JSON: $json") + + // Parse the JSON to extract the value + val jsonElement = Json.parseToJsonElement(json) + val valueString = jsonElement.jsonObject["value"]?.jsonPrimitive?.content ?: "" + + // The critical test: verify zero value has decimal point + assertEquals("0.0", valueString, "Zero UFix64 value must be serialized as '0.0' not '0'") + + // Verify it can be deserialized back + val deserializedValue = Json.decodeFromString(json) + assertEquals(zeroValue, deserializedValue.value, "Deserialized value should match original") + + println("✅ UFix64 zero value serialization test passed") + println(" Serialized as: $valueString") + } + + @Test + fun testUFix64WholeNumbersSerialization() { + // Test that whole numbers also get proper decimal formatting + val wholeNumbers = listOf(1.0, 5.0, 100.0, 1000.0) + + for (wholeNumber in wholeNumbers) { + val ufixValue = Cadence.ufix64(wholeNumber) + val json = Json.encodeToString(ufixValue) + + val jsonElement = Json.parseToJsonElement(json) + val valueString = jsonElement.jsonObject["value"]?.jsonPrimitive?.content ?: "" + + // Verify whole numbers include decimal point + assertTrue("Whole number $wholeNumber should have decimal point in serialized form: $valueString") { + valueString.contains(".") + } + + // Verify it deserializes correctly + val deserializedValue = Json.decodeFromString(json) + assertEquals(wholeNumber, deserializedValue.value, "Deserialized value should match original") + } + + println("✅ UFix64 whole numbers serialization test passed") + } + @Test fun testDirectConversion() { // Test the conversion logic directly diff --git a/flow/src/commonTest/kotlin/org/onflow/flow/infrastructure/FixedPointFormatterTest.kt b/flow/src/commonTest/kotlin/org/onflow/flow/infrastructure/FixedPointFormatterTest.kt new file mode 100644 index 0000000..d1de292 --- /dev/null +++ b/flow/src/commonTest/kotlin/org/onflow/flow/infrastructure/FixedPointFormatterTest.kt @@ -0,0 +1,99 @@ +package org.onflow.flow.infrastructure + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull + +class FixedPointFormatterTest { + + @Test + fun testFormatZeroValue() { + // Test that zero value gets formatted with decimal point + val result = FixedPointFormatter.format("0", 8UL) + + assertNotNull(result, "Result should not be null") + assertEquals("0.0", result, "Zero should be formatted as '0.0', not '0'") + } + + @Test + fun testFormatWholeNumbers() { + // Test that whole numbers get formatted with decimal point + val result1 = FixedPointFormatter.format("1", 8UL) + assertEquals("1.0", result1, "Whole numbers should include decimal point") + + val result2 = FixedPointFormatter.format("123", 8UL) + assertEquals("123.0", result2, "Large whole numbers should include decimal point") + } + + @Test + fun testFormatDecimalValues() { + // Test that existing decimal values are preserved + val result1 = FixedPointFormatter.format("1.5", 8UL) + assertEquals("1.5", result1, "Existing decimal values should be preserved") + + val result2 = FixedPointFormatter.format("0.00000001", 8UL) + assertEquals("0.00000001", result2, "Small decimal values should be preserved") + } + + @Test + fun testFormatWithPrecision() { + // Test that precision is respected + val result1 = FixedPointFormatter.format("1.23456789", 4UL) + assertEquals("1.2345", result1, "Precision should limit decimal places") + + val result2 = FixedPointFormatter.format("0.123456789", 2UL) + assertEquals("0.12", result2, "Precision should limit decimal places for values < 1") + } + + @Test + fun testFormatScientificNotationInput() { + // Test that scientific notation input is handled correctly + // Note: This tests the input format, not the actual scientific notation conversion + val result = FixedPointFormatter.format("1E-8", 8UL) + assertEquals("1E-8.0", result, "Scientific notation input should get decimal point added") + } + + @Test + fun testFormatUFix64SpecificCases() { + // Test cases specifically for UFix64 usage + val zeroResult = FixedPointFormatter.format("0", 8UL) + assertEquals("0.0", zeroResult, "UFix64 zero must have decimal point") + + val oneResult = FixedPointFormatter.format("1", 8UL) + assertEquals("1.0", oneResult, "UFix64 whole numbers must have decimal point") + + val smallResult = FixedPointFormatter.format("0.00000001", 8UL) + assertEquals("0.00000001", smallResult, "UFix64 small values should be preserved") + } + + @Test + fun testFormatErrorHandling() { + // Test error cases + val result1 = FixedPointFormatter.format("", 8UL) + assertEquals(null, result1, "Empty string should return null") + + val result2 = FixedPointFormatter.format(" ", 8UL) + assertEquals(null, result2, "Blank string should return null") + + val result3 = FixedPointFormatter.format("invalid", 8UL) + assertEquals("invalid.0", result3, "Non-numeric input gets decimal point added (handled by caller)") + + val result4 = FixedPointFormatter.format("abc.def", 8UL) + assertEquals("abc.def", result4, "Invalid decimal input is passed through (handled by caller)") + } + + @Test + fun testFormatCriticalCases() { + // Test the exact cases that were causing the UFix64 error + val zeroResult = FixedPointFormatter.format("0", 8UL) + assertEquals("0.0", zeroResult, "Zero must be formatted with decimal point") + + val zeroDoubleResult = FixedPointFormatter.format("0.0", 8UL) + assertEquals("0.0", zeroDoubleResult, "Zero with decimal should be preserved") + + val smallValueResult = FixedPointFormatter.format("0.00000001", 8UL) + assertEquals("0.00000001", smallValueResult, "Small UFix64 values should be preserved") + + println("✅ All critical UFix64 formatting cases pass") + } +} \ No newline at end of file