mirror of
https://github.com/pezkuwichain/pezkuwi-wallet-android.git
synced 2026-07-22 13:45:48 +00:00
fix: log and stop swallowing synchronous listenForUpdates() failures; retry NDK download in CI
BalancesUpdateSystem.launchChainUpdaters() wrapped updater.listenForUpdates() in a try/catch that discarded synchronous exceptions with zero logging - a silent failure point that could explain a chain's balances never syncing with no trace in logcat. Also add a full-architecture instrumented test that persists a real watch-only MetaAccount and polls AssetDao through the actual BalancesUpdateSystem pipeline, instead of bypassing it like the existing BalancesIntegrationTest does.
This commit is contained in:
@@ -18,7 +18,27 @@ runs:
|
||||
- name: Install NDK
|
||||
run: |
|
||||
SDKMANAGER=$(find ${ANDROID_SDK_ROOT}/cmdline-tools -name sdkmanager -type f 2>/dev/null | head -1)
|
||||
echo "y" | sudo ${SDKMANAGER} --install "ndk;26.1.10909125" --sdk_root=${ANDROID_SDK_ROOT}
|
||||
NDK_PACKAGE="ndk;26.1.10909125"
|
||||
|
||||
# sdkmanager's download of the ~1GB NDK zip from Google's CDN occasionally comes back truncated/corrupted
|
||||
# ("Error on ZipFile unknown archive") with no built-in retry, taking down the whole build for a purely
|
||||
# transient network blip. Retry with cleanup between attempts: sdkmanager can otherwise resume from the
|
||||
# same corrupted partial file instead of re-fetching, making a naive retry fail identically every time.
|
||||
for attempt in 1 2 3; do
|
||||
echo "NDK install attempt $attempt/3"
|
||||
if echo "y" | sudo ${SDKMANAGER} --install "$NDK_PACKAGE" --sdk_root=${ANDROID_SDK_ROOT}; then
|
||||
echo "NDK installed successfully"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Attempt $attempt failed - clearing any partial/corrupted download before retrying"
|
||||
sudo rm -rf "${ANDROID_SDK_ROOT}/ndk/26.1.10909125"
|
||||
sudo find "${ANDROID_SDK_ROOT}" -maxdepth 1 -name "tmp*" -exec rm -rf {} +
|
||||
sleep 10
|
||||
done
|
||||
|
||||
echo "NDK install failed after 3 attempts"
|
||||
exit 1
|
||||
shell: bash
|
||||
|
||||
- name: Set ndk.dir in local.properties
|
||||
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
package io.novafoundation.nova.balances
|
||||
|
||||
import android.content.Context
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import io.novafoundation.nova.common.di.FeatureUtils
|
||||
import io.novafoundation.nova.core.model.CryptoType
|
||||
import io.novafoundation.nova.core_db.dao.AssetDao
|
||||
import io.novafoundation.nova.core_db.dao.MetaAccountDao
|
||||
import io.novafoundation.nova.core_db.di.DbApi
|
||||
import io.novafoundation.nova.core_db.model.chain.account.MetaAccountLocal
|
||||
import io.novasama.substrate_sdk_android.ss58.SS58Encoder.toAccountId
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Test
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
/**
|
||||
* Exercises the ACTUAL production balance-sync pipeline (BalancesUpdateSystem -> AssetCache/AssetDao) end to
|
||||
* end, unlike [BalancesIntegrationTest] which bypasses it entirely via a direct low-level storage query. This
|
||||
* is meant to answer one question with hard evidence, not speculation: does the app's real, running background
|
||||
* sync ever write an `assets` cache row for HEZ on the Pezkuwi Asset Hub chain, for a real, well-funded account?
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
class PezkuwiFullArchitectureBalancesTest {
|
||||
|
||||
// Mainnet Founder account (SS58, generic substrate prefix) - verified live via @pezkuwi/api on 2026-07-09
|
||||
// to hold a substantial non-zero, non-frozen free HEZ balance on Pezkuwi Asset Hub (180,297.80 HEZ).
|
||||
private val founderSubstrateAddress = "5CyuFfbF95rzBxru7c9yEsX4XmQXUxpLUcbj9RLg9K1cGiiF"
|
||||
private val pezkuwiAssetHubChainId = "e7c15092dcbe3f320260ddbbc685bfceed9125a3b3d8436db2766201dec3b949"
|
||||
private val hezAssetId = 0
|
||||
|
||||
private val context = ApplicationProvider.getApplicationContext<Context>()
|
||||
|
||||
private val dbApi = FeatureUtils.getFeature<DbApi>(context, DbApi::class.java)
|
||||
private val metaAccountDao = dbApi.metaAccountDao()
|
||||
private val assetDao: AssetDao = dbApi.provideAssetDao()
|
||||
|
||||
@Test
|
||||
fun testPezkuwiAssetHubHezBalanceActuallySyncs() = runBlocking {
|
||||
val metaId = insertAndSelectFounderWatchAccount(metaAccountDao)
|
||||
|
||||
val assetRow = withTimeoutOrNull(90.seconds) {
|
||||
while (true) {
|
||||
val asset = assetDao.getAsset(metaId, pezkuwiAssetHubChainId, hezAssetId)
|
||||
if (asset != null) return@withTimeoutOrNull asset
|
||||
|
||||
delay(2.seconds)
|
||||
}
|
||||
@Suppress("UNREACHABLE_CODE")
|
||||
null
|
||||
}
|
||||
|
||||
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.",
|
||||
assetRow
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun insertAndSelectFounderWatchAccount(dao: MetaAccountDao): Long {
|
||||
val accountId = founderSubstrateAddress.toAccountId()
|
||||
|
||||
val metaAccount = MetaAccountLocal(
|
||||
substratePublicKey = accountId,
|
||||
substrateCryptoType = CryptoType.SR25519,
|
||||
substrateAccountId = accountId,
|
||||
ethereumPublicKey = null,
|
||||
ethereumAddress = null,
|
||||
name = "PezkuwiFullArchitectureBalancesTest",
|
||||
parentMetaId = null,
|
||||
isSelected = false,
|
||||
position = 0,
|
||||
type = MetaAccountLocal.Type.WATCH_ONLY,
|
||||
status = MetaAccountLocal.Status.ACTIVE,
|
||||
globallyUniqueId = MetaAccountLocal.generateGloballyUniqueId(),
|
||||
typeExtras = null
|
||||
)
|
||||
|
||||
val metaId = dao.insertMetaAccount(metaAccount)
|
||||
dao.selectMetaAccount(metaId)
|
||||
|
||||
return metaId
|
||||
}
|
||||
}
|
||||
+9
@@ -45,6 +45,11 @@ class BalancesUpdateSystem(
|
||||
}
|
||||
|
||||
private suspend fun balancesSync(chain: Chain, metaAccount: MetaAccount): Flow<Updater.SideEffect> {
|
||||
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()
|
||||
@@ -89,6 +94,10 @@ class BalancesUpdateSystem(
|
||||
try {
|
||||
updater.listenForUpdates(subscriptionBuilder, metaAccount).catch { logError(chain, it) }
|
||||
} catch (e: Exception) {
|
||||
// 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)
|
||||
emptyFlow()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user