From 54bea1234a5140e0a7ef9814a7e4baa307f8019c Mon Sep 17 00:00:00 2001 From: Satoshi Qazi Muhammed Date: Thu, 9 Jul 2026 14:44:30 -0700 Subject: [PATCH] fix: revert FullSyncPaymentUpdater's retryWhen wrapper - root cause found Diffed the whole branch against main (the live, working Play Store source) instead of continuing to guess from logs. Found the actual regression: syncAsset() was changed from a suspend fun that EAGERLY calls startSyncingBalance() (registering each asset's storage key synchronously, during listenForUpdates()) into a plain fun returning a lazy flow { } that only calls startSyncingBalance() once collected - to support a retryWhen wrapper added earlier this session. BalancesUpdateSystem.launchChainUpdaters() calls subscriptionBuilder.subscribe(coroutineContext) - which seals the shared per-chain subscription multiplexer - immediately after listenForUpdates() returns, then only starts collecting the merged result flow (which is what triggers the lazy startSyncingBalance() calls) afterward. So every asset's key registration now races against the seal instead of reliably happening before it, matching main's original guaranteed ordering. Some assets win the race often enough to look like they work; this is why the full ecosystem test - which creates one account and races ALL of it at once - saw every asset fail, while the older single-asset test mostly saw only HEZ fail. Reverted to main's version: suspend syncAsset(), eager startSyncingBalance() call, no retryWhen. Loses automatic retry of transient full-sync failures (which the retryWhen wrapper was meant to add), but that capability isn't worth reintroducing an ordering bug that can silently break sync for an unpredictable subset of assets on every chain, not just Pezkuwi's. --- .../balance/FullSyncPaymentUpdater.kt | 44 ++++++------------- 1 file changed, 14 insertions(+), 30 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 4b318e93..56a18dd3 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,14 +22,9 @@ 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.emitAll -import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.catch 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, @@ -46,43 +41,32 @@ internal class FullSyncPaymentUpdater( ): Flow { val accountId = scopeValue.requireAccountIdIn(chain) - return chain.enabledAssets().map { chainAsset -> + return chain.enabledAssets().mapNotNull { chainAsset -> syncAsset(chainAsset, scopeValue, accountId, storageSubscriptionBuilder) } .mergeIfMultiple() .noSideAffects() } - /** - * 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( + private suspend fun syncAsset( chainAsset: Chain.Asset, metaAccount: MetaAccount, accountId: AccountId, storageSubscriptionBuilder: SharedRequestsBuilder - ): Flow { + ): Flow? { val assetSource = assetSourceRegistry.sourceFor(chainAsset) - return flow { - val assetUpdateFlow = assetSource.balance.startSyncingBalance(chain, chainAsset, metaAccount, accountId, storageSubscriptionBuilder) - emitAll(assetUpdateFlow) + val assetUpdateFlow = runCatching { + assetSource.balance.startSyncingBalance(chain, chainAsset, metaAccount, accountId, storageSubscriptionBuilder) } - .onEach { balanceUpdate -> - assetSource.history.syncOperationsForBalanceChange(chainAsset, balanceUpdate, accountId) - } - .retryWhen { cause, _ -> - logSyncError(chain, chainAsset, error = cause) - delay(SYNC_RETRY_DELAY_MS) - true - } + .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) } } private fun logSyncError(chain: Chain, chainAsset: Chain.Asset, error: Throwable) {