From a1ec9a8b9ba76f5e7777216ca558d985bf0578bd Mon Sep 17 00:00:00 2001 From: Kurdistan Tech Ministry Date: Sun, 25 Jan 2026 16:45:13 +0300 Subject: [PATCH] Add graceful error handling for RPC connection failures - StatemineAssetBalance: Handle connection errors in queryAccountBalance, existentialDeposit, subscribeAccountBalanceUpdatePoint, and startSyncingBalance - NativeAssetBalance: Handle connection errors in all balance query and sync methods - Return safe defaults (zero balance, empty flows) instead of crashing - Log errors for debugging without interrupting user experience --- .../statemine/StatemineAssetBalance.kt | 110 +++++++++++------- .../balances/utility/NativeAssetBalance.kt | 80 +++++++++---- 2 files changed, 128 insertions(+), 62 deletions(-) diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/statemine/StatemineAssetBalance.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/statemine/StatemineAssetBalance.kt index af2722f..8735685 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/statemine/StatemineAssetBalance.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/statemine/StatemineAssetBalance.kt @@ -1,6 +1,8 @@ package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.balances.statemine +import android.util.Log import io.novafoundation.nova.common.data.network.runtime.binding.AccountBalance +import io.novafoundation.nova.common.utils.LOG_TAG import io.novafoundation.nova.common.utils.decodeValue import io.novafoundation.nova.core.updater.SharedRequestsBuilder import io.novafoundation.nova.core_db.model.AssetLocal.EDCountingModeLocal @@ -27,6 +29,7 @@ import io.novasama.substrate_sdk_android.runtime.AccountId import io.novasama.substrate_sdk_android.runtime.metadata.storage import io.novasama.substrate_sdk_android.runtime.metadata.storageKey import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.map @@ -51,28 +54,40 @@ class StatemineAssetBalance( } override fun isSelfSufficient(chainAsset: Chain.Asset): Boolean { - return chainAsset.requireStatemine().isSufficient + return runCatching { + chainAsset.requireStatemine().isSufficient + }.getOrDefault(false) } override suspend fun existentialDeposit(chainAsset: Chain.Asset): BigInteger { - return queryAssetDetails(chainAsset).minimumBalance + return runCatching { + queryAssetDetails(chainAsset).minimumBalance + }.getOrElse { error -> + Log.e(LOG_TAG, "Failed to query existential deposit for ${chainAsset.symbol}: ${error.message}") + BigInteger.ZERO + } } override suspend fun queryAccountBalance(chain: Chain, chainAsset: Chain.Asset, accountId: AccountId): ChainAssetBalance { - val statemineType = chainAsset.requireStatemine() + return runCatching { + val statemineType = chainAsset.requireStatemine() - val assetAccount = remoteStorage.query(chain.id) { - val encodableId = statemineType.prepareIdForEncoding(runtime) + val assetAccount = remoteStorage.query(chain.id) { + val encodableId = statemineType.prepareIdForEncoding(runtime) - runtime.metadata.statemineModule(statemineType).storage("Account").query( - encodableId, - accountId, - binding = ::bindAssetAccountOrEmpty - ) + runtime.metadata.statemineModule(statemineType).storage("Account").query( + encodableId, + accountId, + binding = ::bindAssetAccountOrEmpty + ) + } + + val accountBalance = assetAccount.toAccountBalance() + ChainAssetBalance.default(chainAsset, accountBalance) + }.getOrElse { error -> + Log.e(LOG_TAG, "Failed to query balance for ${chainAsset.symbol} on ${chain.name}: ${error.message}") + ChainAssetBalance.fromFree(chainAsset, BigInteger.ZERO) } - - val accountBalance = assetAccount.toAccountBalance() - return ChainAssetBalance.default(chainAsset, accountBalance) } override suspend fun subscribeAccountBalanceUpdatePoint( @@ -80,18 +95,25 @@ class StatemineAssetBalance( chainAsset: Chain.Asset, accountId: AccountId, ): Flow { - val statemineType = chainAsset.requireStatemine() + return runCatching { + val statemineType = chainAsset.requireStatemine() - return remoteStorage.subscribe(chain.id) { - val encodableId = statemineType.prepareIdForEncoding(runtime) + remoteStorage.subscribe(chain.id) { + val encodableId = statemineType.prepareIdForEncoding(runtime) - runtime.metadata.statemineModule(statemineType).storage("Account").observeWithRaw( - encodableId, - accountId, - binding = ::bindAssetAccountOrEmpty - ).map { - TransferableBalanceUpdatePoint(it.at!!) + runtime.metadata.statemineModule(statemineType).storage("Account").observeWithRaw( + encodableId, + accountId, + binding = ::bindAssetAccountOrEmpty + ).map { + TransferableBalanceUpdatePoint(it.at!!) + } + }.catch { error -> + Log.e(LOG_TAG, "Balance subscription failed for ${chainAsset.symbol} on ${chain.name}: ${error.message}") } + }.getOrElse { error -> + Log.e(LOG_TAG, "Failed to setup balance subscription for ${chainAsset.symbol} on ${chain.name}: ${error.message}") + emptyFlow() } } @@ -116,32 +138,40 @@ class StatemineAssetBalance( accountId: AccountId, subscriptionBuilder: SharedRequestsBuilder ): Flow { - val runtime = chainRegistry.getRuntime(chain.id) + return runCatching { + val runtime = chainRegistry.getRuntime(chain.id) - val statemineType = chainAsset.requireStatemine() - val encodableAssetId = statemineType.prepareIdForEncoding(runtime) + val statemineType = chainAsset.requireStatemine() + val encodableAssetId = statemineType.prepareIdForEncoding(runtime) - val module = runtime.metadata.statemineModule(statemineType) + val module = runtime.metadata.statemineModule(statemineType) - val assetAccountStorage = module.storage("Account") - val assetAccountKey = assetAccountStorage.storageKey(runtime, encodableAssetId, accountId) + val assetAccountStorage = module.storage("Account") + val assetAccountKey = assetAccountStorage.storageKey(runtime, encodableAssetId, accountId) - val assetDetailsFlow = statemineAssetsRepository.subscribeAndSyncAssetDetails(chain.id, statemineType, subscriptionBuilder) + val assetDetailsFlow = statemineAssetsRepository.subscribeAndSyncAssetDetails(chain.id, statemineType, subscriptionBuilder) - return combine( - subscriptionBuilder.subscribe(assetAccountKey), - assetDetailsFlow.map { it.status.transfersFrozen } - ) { balanceStorageChange, isAssetFrozen -> - val assetAccountDecoded = assetAccountStorage.decodeValue(balanceStorageChange.value, runtime) - val assetAccount = bindAssetAccountOrEmpty(assetAccountDecoded) + combine( + subscriptionBuilder.subscribe(assetAccountKey), + assetDetailsFlow.map { it.status.transfersFrozen } + ) { balanceStorageChange, isAssetFrozen -> + val assetAccountDecoded = assetAccountStorage.decodeValue(balanceStorageChange.value, runtime) + val assetAccount = bindAssetAccountOrEmpty(assetAccountDecoded) - val assetChanged = updateAssetBalance(metaAccount.id, chainAsset, isAssetFrozen, assetAccount) + val assetChanged = updateAssetBalance(metaAccount.id, chainAsset, isAssetFrozen, assetAccount) - if (assetChanged) { - BalanceSyncUpdate.CauseFetchable(balanceStorageChange.block) - } else { - BalanceSyncUpdate.NoCause + if (assetChanged) { + BalanceSyncUpdate.CauseFetchable(balanceStorageChange.block) + } else { + BalanceSyncUpdate.NoCause + } + }.catch { error -> + Log.e(LOG_TAG, "Balance sync failed for ${chainAsset.symbol} on ${chain.name}: ${error.message}") + emit(BalanceSyncUpdate.NoCause) } + }.getOrElse { error -> + Log.e(LOG_TAG, "Failed to start balance sync for ${chainAsset.symbol} on ${chain.name}: ${error.message}") + emptyFlow() } } diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/utility/NativeAssetBalance.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/utility/NativeAssetBalance.kt index 3e9cdf1..f5f14bb 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/utility/NativeAssetBalance.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/utility/NativeAssetBalance.kt @@ -42,6 +42,7 @@ import io.novasama.substrate_sdk_android.runtime.metadata.storage import io.novasama.substrate_sdk_android.runtime.metadata.storageKey import io.novasama.substrate_sdk_android.runtime.metadata.storageOrNull import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.map @@ -63,15 +64,22 @@ class NativeAssetBalance( accountId: AccountId, subscriptionBuilder: SharedRequestsBuilder ): Flow<*> { - return remoteStorage.subscribe(chain.id, subscriptionBuilder) { - combine( - metadata.balances.locks.observe(accountId), - metadata.balances.freezes.observe(accountId) - ) { locks, freezes -> - val all = locks.orEmpty() + freezes.orEmpty() + return runCatching { + remoteStorage.subscribe(chain.id, subscriptionBuilder) { + combine( + metadata.balances.locks.observe(accountId), + metadata.balances.freezes.observe(accountId) + ) { locks, freezes -> + val all = locks.orEmpty() + freezes.orEmpty() - lockDao.updateLocks(all, metaAccount.id, chain.id, chainAsset.id) + lockDao.updateLocks(all, metaAccount.id, chain.id, chainAsset.id) + } + }.catch { error -> + Log.e(LOG_TAG, "Balance locks sync failed for ${chainAsset.symbol} on ${chain.name}: ${error.message}") } + }.getOrElse { error -> + Log.e(LOG_TAG, "Failed to start balance locks sync for ${chainAsset.symbol} on ${chain.name}: ${error.message}") + emptyFlow() } } @@ -82,15 +90,23 @@ class NativeAssetBalance( accountId: AccountId, subscriptionBuilder: SharedRequestsBuilder ): Flow<*> { - val runtime = chainRegistry.getRuntime(chain.id) - val storage = runtime.metadata.balances().storageOrNull("Holds") ?: return emptyFlow() - val key = storage.storageKey(runtime, accountId) + return runCatching { + val runtime = chainRegistry.getRuntime(chain.id) + val storage = runtime.metadata.balances().storageOrNull("Holds") ?: return emptyFlow() + val key = storage.storageKey(runtime, accountId) - return subscriptionBuilder.subscribe(key) - .map { change -> - val holds = bindBalanceHolds(storage.decodeValue(change.value, runtime)).orEmpty() - holdsDao.updateHolds(holds, metaAccount.id, chain.id, chainAsset.id) - } + subscriptionBuilder.subscribe(key) + .map { change -> + val holds = bindBalanceHolds(storage.decodeValue(change.value, runtime)).orEmpty() + holdsDao.updateHolds(holds, metaAccount.id, chain.id, chainAsset.id) + } + .catch { error -> + Log.e(LOG_TAG, "Balance holds sync failed for ${chainAsset.symbol} on ${chain.name}: ${error.message}") + } + }.getOrElse { error -> + Log.e(LOG_TAG, "Failed to start balance holds sync for ${chainAsset.symbol} on ${chain.name}: ${error.message}") + emptyFlow() + } } override fun isSelfSufficient(chainAsset: Chain.Asset): Boolean { @@ -98,13 +114,22 @@ class NativeAssetBalance( } override suspend fun existentialDeposit(chainAsset: Chain.Asset): BigInteger { - val runtime = chainRegistry.getRuntime(chainAsset.chainId) - - return runtime.metadata.balances().numberConstant("ExistentialDeposit", runtime) + return runCatching { + val runtime = chainRegistry.getRuntime(chainAsset.chainId) + runtime.metadata.balances().numberConstant("ExistentialDeposit", runtime) + }.getOrElse { error -> + Log.e(LOG_TAG, "Failed to query existential deposit for ${chainAsset.symbol}: ${error.message}") + BigInteger.ZERO + } } override suspend fun queryAccountBalance(chain: Chain, chainAsset: Chain.Asset, accountId: AccountId): ChainAssetBalance { - return accountInfoRepository.getAccountInfo(chain.id, accountId).data.toChainAssetBalance(chainAsset) + return runCatching { + accountInfoRepository.getAccountInfo(chain.id, accountId).data.toChainAssetBalance(chainAsset) + }.getOrElse { error -> + Log.e(LOG_TAG, "Failed to query balance for ${chainAsset.symbol} on ${chain.name}: ${error.message}") + ChainAssetBalance.fromFree(chainAsset, BigInteger.ZERO) + } } override suspend fun subscribeAccountBalanceUpdatePoint( @@ -112,10 +137,17 @@ class NativeAssetBalance( chainAsset: Chain.Asset, accountId: AccountId, ): Flow { - return remoteStorage.subscribe(chain.id) { - metadata.system.account.observeWithRaw(accountId).map { - TransferableBalanceUpdatePoint(it.at!!) + return runCatching { + remoteStorage.subscribe(chain.id) { + metadata.system.account.observeWithRaw(accountId).map { + TransferableBalanceUpdatePoint(it.at!!) + } + }.catch { error -> + Log.e(LOG_TAG, "Balance subscription failed for ${chainAsset.symbol} on ${chain.name}: ${error.message}") } + }.getOrElse { error -> + Log.e(LOG_TAG, "Failed to setup balance subscription for ${chainAsset.symbol} on ${chain.name}: ${error.message}") + emptyFlow() } } @@ -147,6 +179,10 @@ class NativeAssetBalance( BalanceSyncUpdate.NoCause } } + .catch { error -> + Log.e(LOG_TAG, "Balance sync failed for ${chainAsset.symbol} on ${chain.name}: ${error.message}") + emit(BalanceSyncUpdate.NoCause) + } } private fun bindBalanceHolds(dynamicInstance: Any?): List? {