From 932484ccb846d551099f786488c04024c1bd17a0 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Wed, 20 May 2026 12:26:39 -0400 Subject: [PATCH] fix: catch network exceptions in Solana RPC calls to prevent unhandled crashes Wrap all makeRequest calls in runCatching so SSLHandshakeException and other transient network errors become Result.failure instead of crashing. Add SSLException and SocketException to ignored/network error lists in ErrorUtils so they are silently handled. Signed-off-by: Brandon McAnsh --- .../kotlin/com/getcode/solana/rpc/Calls.kt | 60 +++++++++++-------- .../kotlin/com/getcode/utils/ErrorUtils.kt | 14 ++++- 2 files changed, 48 insertions(+), 26 deletions(-) diff --git a/libs/crypto/solana/src/main/kotlin/com/getcode/solana/rpc/Calls.kt b/libs/crypto/solana/src/main/kotlin/com/getcode/solana/rpc/Calls.kt index fea601a3f..5d58322bb 100644 --- a/libs/crypto/solana/src/main/kotlin/com/getcode/solana/rpc/Calls.kt +++ b/libs/crypto/solana/src/main/kotlin/com/getcode/solana/rpc/Calls.kt @@ -20,10 +20,12 @@ class SolanaConnection(rpcUrl: String,) { * Returns the SOL balance (in lamports) for the given public key. */ suspend fun Rpc20Driver.getBalance(publicKey: PublicKey): Result { - val response = makeRequest( - request = GetBalance(publicKey), - resultSerializer = JsonElement.serializer() - ) + val response = runCatching { + makeRequest( + request = GetBalance(publicKey), + resultSerializer = JsonElement.serializer() + ) + }.getOrElse { return Result.failure(it) } val error = response.error if (error != null) { return Result.failure(RpcException(error.code, error.message)) @@ -42,10 +44,12 @@ suspend fun Rpc20Driver.getBalance(publicKey: PublicKey): Result { * Returns 0 if the account does not exist. */ suspend fun Rpc20Driver.getTokenAccountBalance(tokenAccount: PublicKey): Result { - val response = makeRequest( - request = GetTokenAccountBalance(tokenAccount), - resultSerializer = JsonElement.serializer() - ) + val response = runCatching { + makeRequest( + request = GetTokenAccountBalance(tokenAccount), + resultSerializer = JsonElement.serializer() + ) + }.getOrElse { return Result.failure(it) } val error = response.error if (error != null) { // Account not found — treat as zero balance @@ -77,10 +81,12 @@ suspend fun Rpc20Driver.getTokenAccountBalance(tokenAccount: PublicKey): Result< * (e.g., network issue, account not found, or RPC error). */ suspend fun Rpc20Driver.doesAccountExist(publicKey: PublicKey): Result { - val response = makeRequest( - request = GetAccountInfo(publicKey), - resultSerializer = JsonElement.serializer() - ) + val response = runCatching { + makeRequest( + request = GetAccountInfo(publicKey), + resultSerializer = JsonElement.serializer() + ) + }.getOrElse { return Result.failure(it) } val error = response.error if (error != null) { return Result.failure(RpcException(error.code, error.message)) @@ -98,10 +104,12 @@ suspend fun Rpc20Driver.doesAccountExist(publicKey: PublicKey): Result { * Returns the raw account data for the given public key, base64-decoded. */ suspend fun Rpc20Driver.getAccountData(publicKey: PublicKey): Result { - val response = makeRequest( - request = GetAccountInfo(publicKey), - resultSerializer = JsonElement.serializer() - ) + val response = runCatching { + makeRequest( + request = GetAccountInfo(publicKey), + resultSerializer = JsonElement.serializer() + ) + }.getOrElse { return Result.failure(it) } val error = response.error if (error != null) { return Result.failure(RpcException(error.code, error.message)) @@ -127,10 +135,12 @@ suspend fun Rpc20Driver.getAccountData(publicKey: PublicKey): Result */ suspend fun Rpc20Driver.sendTransaction(encodedTransaction: String): Result { println("sending transaction on the blockchain => $encodedTransaction") - val response = makeRequest( - request = SendTransaction(encodedTransaction), - resultSerializer = JsonElement.serializer() - ) + val response = runCatching { + makeRequest( + request = SendTransaction(encodedTransaction), + resultSerializer = JsonElement.serializer() + ) + }.getOrElse { return Result.failure(it) } val error = response.error if (error != null) { return Result.failure(RpcException(error.code, error.message)) @@ -163,10 +173,12 @@ suspend fun Rpc20Driver.simulateTransaction( commitment: String = "confirmed", ): Result { println("simulating transaction on the blockchain => $encodedTransaction") - val response = makeRequest( - request = SimulateTransaction(encodedTransaction, commitment), - resultSerializer = JsonElement.serializer() - ) + val response = runCatching { + makeRequest( + request = SimulateTransaction(encodedTransaction, commitment), + resultSerializer = JsonElement.serializer() + ) + }.getOrElse { return Result.failure(it) } val error = response.error val errorDetails = response.result?.jsonObject?.get("err")?.toString() if (error != null || errorDetails != null) { diff --git a/libs/logging/src/main/kotlin/com/getcode/utils/ErrorUtils.kt b/libs/logging/src/main/kotlin/com/getcode/utils/ErrorUtils.kt index 230fa199a..28e94faab 100644 --- a/libs/logging/src/main/kotlin/com/getcode/utils/ErrorUtils.kt +++ b/libs/logging/src/main/kotlin/com/getcode/utils/ErrorUtils.kt @@ -10,8 +10,10 @@ import kotlinx.coroutines.CancellationException import kotlinx.coroutines.TimeoutCancellationException import timber.log.Timber import java.net.ConnectException +import java.net.SocketException import java.net.UnknownHostException import java.util.concurrent.TimeoutException +import javax.net.ssl.SSLException object ErrorUtils { private var isDisplayErrors = false @@ -30,6 +32,8 @@ object ErrorUtils { TimeoutCancellationException::class, CancellationException::class, ConnectException::class, + SSLException::class, + SocketException::class, ) fun handleError(throwable: Throwable) { @@ -76,7 +80,11 @@ object ErrorUtils { throwable is TimeoutException || throwable.cause is TimeoutException || throwable is UnknownHostException || - throwable.cause is UnknownHostException + throwable.cause is UnknownHostException || + throwable is SSLException || + throwable.cause is SSLException || + throwable is SocketException || + throwable.cause is SocketException private val gmsTransientMessages = setOf("SERVICE_NOT_AVAILABLE", "FIS_AUTH_ERROR") @@ -122,7 +130,9 @@ object ErrorUtils { fun Throwable.isNetworkError(): Boolean = this is UnknownHostException || cause is UnknownHostException || this is ConnectException || cause is ConnectException || - this is TimeoutException || cause is TimeoutException + this is TimeoutException || cause is TimeoutException || + this is SSLException || cause is SSLException || + this is SocketException || cause is SocketException data class SuppressibleException(override val message: String, override val cause: Throwable? = null) : Throwable(message, cause) { constructor(cause: Throwable) : this(cause.message.orEmpty(), cause)