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
This commit is contained in:
2026-01-25 16:45:13 +03:00
parent fa41ffb4d4
commit a1ec9a8b9b
2 changed files with 128 additions and 62 deletions
@@ -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<TransferableBalanceUpdatePoint> {
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<BalanceSyncUpdate> {
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()
}
}
@@ -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<Nothing>()
val key = storage.storageKey(runtime, accountId)
return runCatching {
val runtime = chainRegistry.getRuntime(chain.id)
val storage = runtime.metadata.balances().storageOrNull("Holds") ?: return emptyFlow<Nothing>()
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<TransferableBalanceUpdatePoint> {
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<BlockchainHold>? {