From c5174d0ccf694c1be98e04527c496be9c42b5320 Mon Sep 17 00:00:00 2001 From: Satoshi Qazi Muhammed Date: Thu, 9 Jul 2026 11:53:35 -0700 Subject: [PATCH] fix: NativeAssetBalance uses typed subscribe DSL, not raw key subscription Root cause found via 4 rounds of CI diagnostics: HEZ (native asset) on Pezkuwi Asset Hub was correctly present+enabled in the Chain domain model, correctly dispatched to NativeAssetBalance, and its System.Account storage key was correctly computed - but the raw subscriptionBuilder.subscribe(key) call never actually reached the RPC connection, with zero exceptions and zero data, while the chain's 5 other assets (all statemine-type, all using the same SharedRequestsBuilder) synced fine. The same NativeAssetBalance code worked correctly for every OTHER native asset on every OTHER chain tested, ruling out a generic bug in the class. Switched startSyncingBalance() to the typed remoteStorage.subscribe { } DSL (metadata.system.account.observeWithRaw) - the same mechanism this class's own subscribeAccountBalanceUpdatePoint() and PooledBalanceUpdater/ BalanceLocksUpdater already use successfully on the same busy chain connection, instead of the raw subscriptionBuilder.subscribe(key) call that appears to silently drop registration in that specific configuration. Also removes the diagnostic logging added during the investigation (BalancesUpdateSystem's per-chain balancesSync trace, FullSyncPaymentUpdater's per-chain asset dump) - kept the one genuinely valuable permanent addition, the "listenForUpdates() threw synchronously" error log for a previously silent failure mode. --- .../PezkuwiFullArchitectureBalancesTest.kt | 10 ++--- .../data/network/BalancesUpdateSystem.kt | 7 +--- .../balances/utility/NativeAssetBalance.kt | 38 +++++++++---------- .../balance/FullSyncPaymentUpdater.kt | 9 +---- 4 files changed, 26 insertions(+), 38 deletions(-) diff --git a/app/src/androidTest/java/io/novafoundation/nova/balances/PezkuwiFullArchitectureBalancesTest.kt b/app/src/androidTest/java/io/novafoundation/nova/balances/PezkuwiFullArchitectureBalancesTest.kt index 14041e6f..08377910 100644 --- a/app/src/androidTest/java/io/novafoundation/nova/balances/PezkuwiFullArchitectureBalancesTest.kt +++ b/app/src/androidTest/java/io/novafoundation/nova/balances/PezkuwiFullArchitectureBalancesTest.kt @@ -29,9 +29,9 @@ import kotlin.time.Duration.Companion.seconds * Activity, so we collect it ourselves here via the same AssetsFeatureApi.updateSystem instance the real app * uses, instead of relying on app UI lifecycle to start it. * - * If this test fails, the failure message + logcat (tag "BalancesDiag", plus the standard per-updater error - * logs already wired into BalancesUpdateSystem/FullSyncPaymentUpdater) shows exactly which decision branch or - * exception is responsible - not another layer of inference from silence. + * If this test fails, the standard per-updater error logs already wired into BalancesUpdateSystem/ + * FullSyncPaymentUpdater/NativeAssetBalance show exactly which decision branch or exception is responsible - + * not another layer of inference from silence. */ class PezkuwiFullArchitectureBalancesTest { @@ -69,8 +69,8 @@ class PezkuwiFullArchitectureBalancesTest { assertNotNull( "No `assets` row was ever written for HEZ on Pezkuwi Asset Hub (metaId=$metaId) within 90s. " + - "The real BalancesUpdateSystem pipeline never completed a sync for this asset - check logcat " + - "tag 'BalancesDiag' and the standard FullSyncPaymentUpdater/StatemineAssetBalance error logs.", + "The real BalancesUpdateSystem pipeline never completed a sync for this asset - check the " + + "standard FullSyncPaymentUpdater/NativeAssetBalance error logs in logcat.", assetRow ) } finally { diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/data/network/BalancesUpdateSystem.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/data/network/BalancesUpdateSystem.kt index a6753875..820ef106 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/data/network/BalancesUpdateSystem.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/data/network/BalancesUpdateSystem.kt @@ -45,11 +45,6 @@ class BalancesUpdateSystem( } private suspend fun balancesSync(chain: Chain, metaAccount: MetaAccount): Flow { - Log.d( - "BalancesDiag", - "balancesSync(${chain.name}): hasAccountIn=${metaAccount.hasAccountIn(chain)} " + - "isDisabled=${chain.connectionState.isDisabled} canPerformFullSync=${chain.canPerformFullSync()}" - ) return when { !metaAccount.hasAccountIn(chain) -> emptyFlow() chain.connectionState.isDisabled -> emptyFlow() @@ -97,7 +92,7 @@ class BalancesUpdateSystem( // Was silently swallowed here with zero logging - listenForUpdates() itself is a suspend // call that can throw synchronously (e.g. FullSyncPaymentUpdater.listenForUpdates() calling // requireAccountIdIn(chain)), before ever returning a flow for the .catch{} above to guard. - Log.e("BalancesDiag", "listenForUpdates() threw synchronously for ${updater.javaClass.simpleName} in ${chain.name}", e) + Log.e(LOG_TAG, "listenForUpdates() threw synchronously for ${updater.javaClass.simpleName} in ${chain.name}", e) 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 c9513444..7f9df0c1 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 @@ -1,6 +1,7 @@ package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.balances.utility import android.util.Log +import io.novafoundation.nova.common.data.network.runtime.binding.AccountInfo import io.novafoundation.nova.common.data.network.runtime.binding.bindList import io.novafoundation.nova.common.data.network.runtime.binding.bindNumber import io.novafoundation.nova.common.data.network.runtime.binding.castToDictEnum @@ -17,7 +18,6 @@ import io.novafoundation.nova.core_db.dao.LockDao import io.novafoundation.nova.core_db.model.BalanceHoldLocal import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount import io.novafoundation.nova.feature_wallet_api.data.cache.AssetCache -import io.novafoundation.nova.feature_wallet_api.data.cache.bindAccountInfoOrDefault import io.novafoundation.nova.feature_wallet_api.data.cache.updateAsset import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.balances.AssetBalance import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.balances.BalanceSyncUpdate @@ -154,6 +154,14 @@ class NativeAssetBalance( // Setup/subscription failures are allowed to propagate rather than being swallowed into emptyFlow()/NoCause: // the caller, FullSyncPaymentUpdater.syncAsset(), wraps this whole call in a single retryWhen boundary meant // to catch and retry exactly these failures. Swallowing here would make that retry boundary never trigger. + // + // Uses the typed remoteStorage.subscribe { metadata.system.account... } DSL (same as this class's own + // subscribeAccountBalanceUpdatePoint() and PooledBalanceUpdater/BalanceLocksUpdater) instead of a raw + // subscriptionBuilder.subscribe(key) call: on chains with several other assets/updaters already sharing + // the same SharedRequestsBuilder (e.g. Pezkuwi Asset Hub's 5 statemine assets + nomination-pools updater), + // the raw form's System.Account subscription was silently never reaching the wire - no exception, no data, + // ever - while every other asset on the same chain synced fine. The typed DSL is what every other caller + // on a busy shared connection already uses successfully. override suspend fun startSyncingBalance( chain: Chain, chainAsset: Chain.Asset, @@ -161,26 +169,18 @@ class NativeAssetBalance( accountId: AccountId, subscriptionBuilder: SharedRequestsBuilder ): Flow { - Log.d("BalancesDiag", "NativeAssetBalance.startSyncingBalance() ENTERED for ${chainAsset.symbol} on ${chain.name}") + return remoteStorage.subscribe(chain.id, subscriptionBuilder) { + metadata.system.account.observeWithRaw(accountId) + }.map { change -> + val accountInfo = change.value ?: AccountInfo.empty() + val assetChanged = assetCache.updateAsset(metaAccount.id, chain.utilityAsset, accountInfo) - val runtime = chainRegistry.getRuntime(chain.id) - Log.d("BalancesDiag", "NativeAssetBalance: got runtime for ${chain.name}") - - val key = runtime.metadata.system().storage("Account").storageKey(runtime, accountId) - Log.d("BalancesDiag", "NativeAssetBalance: computed key for ${chainAsset.symbol} on ${chain.name}: $key") - - return subscriptionBuilder.subscribe(key) - .map { change -> - Log.d("BalancesDiag", "NativeAssetBalance: received change for ${chainAsset.symbol} on ${chain.name}") - val accountInfo = bindAccountInfoOrDefault(change.value, runtime) - val assetChanged = assetCache.updateAsset(metaAccount.id, chain.utilityAsset, accountInfo) - - if (assetChanged) { - BalanceSyncUpdate.CauseFetchable(change.block) - } else { - BalanceSyncUpdate.NoCause - } + if (assetChanged) { + BalanceSyncUpdate.CauseFetchable(change.at!!) + } else { + BalanceSyncUpdate.NoCause } + } } private fun bindBalanceHolds(dynamicInstance: Any?): List? { 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 fae660a6..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 @@ -46,14 +46,7 @@ internal class FullSyncPaymentUpdater( ): Flow { val accountId = scopeValue.requireAccountIdIn(chain) - val enabled = chain.enabledAssets() - Log.d( - "BalancesDiag", - "FullSyncPaymentUpdater(${chain.name}): allAssets=${chain.assets.map { "${it.symbol}(id=${it.id},enabled=${it.enabled},type=${it.type})" }} " + - "enabledAssets=${enabled.map { it.symbol }}" - ) - - return enabled.map { chainAsset -> + return chain.enabledAssets().map { chainAsset -> syncAsset(chainAsset, scopeValue, accountId, storageSubscriptionBuilder) } .mergeIfMultiple()