From 4aebac305f9e4b384bcf9641fa1b14d1435508cd Mon Sep 17 00:00:00 2001 From: Satoshi Qazi Muhammed Date: Wed, 8 Jul 2026 11:57:26 -0700 Subject: [PATCH] fix: retry balance sync on failure for ALL chains, not just Tron Generalizes today's Tron balance-polling fix - the same silent-death bug exists in the shared Substrate sync path used by every chain (Interlay, Kintsugi, Karura, Acala, Hydration, Polkadex, and any other orml/statemine asset), not just Tron-specific code. FullSyncPaymentUpdater.syncAsset() previously used runCatching around the one-time startSyncingBalance() call and a separate .catch on the resulting flow - either path failing (a transient WSS hiccup during setup, an orml currencyId-decode edge case, a node not supporting a specific RPC method during round-robin, a dropped connection later) permanently ended that asset's sync for the Updater's lifetime: no retry, only a Log.e line, and mapNotNull silently dropped the null result. Since no balance update flow ever emits, AssetCache never creates a DB row for that asset, and every UI surface (main list, multi-chain picker) reads via an INNER JOIN that requires that row - so the asset stays invisible with zero user-visible error until app restart, which has the same odds of failing again. Live-tested: Interlay/Kintsugi's node URLs are all reachable (manual WSS handshake test, 101 Switching Protocols), and none of these chains are blacklisted - so this wasn't a connectivity or config issue, it was this retry gap. Wraps both the initial subscription call and the ongoing flow in one flow{} builder with retryWhen (30s fixed interval, matching Tron's polling interval) instead of runCatching + .catch, so a transient failure at either point just gets retried instead of permanently killing sync for that asset. --- .../balance/FullSyncPaymentUpdater.kt | 44 +++++++++++++------ 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/updaters/balance/FullSyncPaymentUpdater.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/updaters/balance/FullSyncPaymentUpdater.kt index 56a18dd3..4b318e93 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/updaters/balance/FullSyncPaymentUpdater.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/updaters/balance/FullSyncPaymentUpdater.kt @@ -22,9 +22,14 @@ import io.novafoundation.nova.runtime.ext.enabledAssets import io.novafoundation.nova.runtime.ext.localId import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import io.novasama.substrate_sdk_android.runtime.AccountId +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.retryWhen + +private const val SYNC_RETRY_DELAY_MS = 30_000L internal class FullSyncPaymentUpdater( private val operationDao: OperationDao, @@ -41,32 +46,43 @@ internal class FullSyncPaymentUpdater( ): Flow { val accountId = scopeValue.requireAccountIdIn(chain) - return chain.enabledAssets().mapNotNull { chainAsset -> + return chain.enabledAssets().map { chainAsset -> syncAsset(chainAsset, scopeValue, accountId, storageSubscriptionBuilder) } .mergeIfMultiple() .noSideAffects() } - private suspend fun syncAsset( + /** + * Wraps both the initial `startSyncingBalance()` call and the resulting flow in a single retry + * boundary. Without this, any transient failure - during initial subscription setup (a WSS + * hiccup, an orml currencyId-decode edge case, a node not supporting a specific RPC method + * during round-robin) or later in the flow (a dropped connection) - would permanently and + * silently kill sync for that one asset: no DB row ever gets created/updated for it, so it + * vanishes from every UI screen with nothing but a logcat line as evidence, until the app is + * restarted (and even then, with the same odds of failing again). Retrying indefinitely on a + * fixed interval matches the same fix already applied to Tron's balance polling. + */ + private fun syncAsset( chainAsset: Chain.Asset, metaAccount: MetaAccount, accountId: AccountId, storageSubscriptionBuilder: SharedRequestsBuilder - ): Flow? { + ): Flow { val assetSource = assetSourceRegistry.sourceFor(chainAsset) - val assetUpdateFlow = runCatching { - assetSource.balance.startSyncingBalance(chain, chainAsset, metaAccount, accountId, storageSubscriptionBuilder) + return flow { + val assetUpdateFlow = assetSource.balance.startSyncingBalance(chain, chainAsset, metaAccount, accountId, storageSubscriptionBuilder) + emitAll(assetUpdateFlow) } - .onFailure { logSyncError(chain, chainAsset, error = it) } - .getOrNull() - ?: return null - - return assetUpdateFlow.onEach { balanceUpdate -> - assetSource.history.syncOperationsForBalanceChange(chainAsset, balanceUpdate, accountId) - } - .catch { logSyncError(chain, chainAsset, error = it) } + .onEach { balanceUpdate -> + assetSource.history.syncOperationsForBalanceChange(chainAsset, balanceUpdate, accountId) + } + .retryWhen { cause, _ -> + logSyncError(chain, chainAsset, error = cause) + delay(SYNC_RETRY_DELAY_MS) + true + } } private fun logSyncError(chain: Chain, chainAsset: Chain.Asset, error: Throwable) {