diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/repository/datasource/AccountDataSourceImpl.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/repository/datasource/AccountDataSourceImpl.kt index d5fc3184..4253256d 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/repository/datasource/AccountDataSourceImpl.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/repository/datasource/AccountDataSourceImpl.kt @@ -80,11 +80,9 @@ class AccountDataSourceImpl( accountDataMigration.migrate(::saveSecuritySource) } - Log.d("AccountDataSourceImpl", "about to check tronAddressBackfillMigration") + Log.d("AccountDataSourceImpl", "about to run tronAddressBackfillMigration") - if (tronAddressBackfillMigration.migrationNeeded()) { - tronAddressBackfillMigration.migrate() - } + tronAddressBackfillMigration.migrate() Log.d("AccountDataSourceImpl", "migrations block done") diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/repository/datasource/migration/TronAddressBackfillMigration.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/repository/datasource/migration/TronAddressBackfillMigration.kt index e7747000..58b3f07a 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/repository/datasource/migration/TronAddressBackfillMigration.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/repository/datasource/migration/TronAddressBackfillMigration.kt @@ -12,7 +12,6 @@ import io.novafoundation.nova.common.data.secrets.v2.seed import io.novafoundation.nova.common.data.secrets.v2.substrateDerivationPath import io.novafoundation.nova.common.data.secrets.v2.substrateKeypair import io.novafoundation.nova.common.data.secrets.v2.tronKeypair -import io.novafoundation.nova.common.data.storage.Preferences import io.novafoundation.nova.common.utils.tronPublicKeyToAccountId import io.novafoundation.nova.core_db.dao.MetaAccountDao import io.novafoundation.nova.core_db.dao.updateMetaAccount @@ -23,19 +22,22 @@ import io.novasama.substrate_sdk_android.encrypt.mnemonic.MnemonicCreator import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -private const val PREFS_TRON_ADDRESS_BACKFILL_DONE = "tron_address_backfill_1_1_3" private const val TAG = "TronAddressBackfill" /** - * One-time backfill for accounts created before Tron support existed. `73_74_AddTronSupport` (the migration - * that added `meta_accounts.tronPublicKey`/`tronAddress`) is, like every other migration in this codebase, pure - * `ALTER TABLE` - it never derives a value for pre-existing rows. `tronAddress` is otherwise only ever set once, - * at fresh-mnemonic-creation time in [io.novafoundation.nova.feature_account_impl.data.secrets.AccountSecretsFactory.metaAccountSecrets], - * so without this backfill `MetaAccount.hasAccountIn(tronChain)` (`tronAddress != null`) permanently returns - * false for every pre-existing seed-derived wallet, which makes `BalancesUpdateSystem` skip Tron entirely for - * that account - no address, no balance, no send, with no error surfaced anywhere. Found via manual testing - * against a real pre-Tron production wallet; no automated test catches this because every automated test - * creates a fresh (post-Tron) account. + * Idempotent, per-account backfill for accounts that don't yet have a Tron keypair - both accounts created + * before Tron support existed, AND accounts whose Tron keypair was lost some other way (e.g. the cloud-backup + * schema round trip that used to silently drop it before every wallet had a `tron` field to serialize into - + * see CloudBackup.kt). `73_74_AddTronSupport` (the migration that added `meta_accounts.tronPublicKey`/ + * `tronAddress`) is, like every other migration in this codebase, pure `ALTER TABLE` - it never derives a value + * for pre-existing rows. + * + * Deliberately has NO "have I already run once" flag: an earlier version of this class gated itself behind a + * one-shot SharedPreferences flag, which meant that once it ran and marked itself done - even for an account + * that legitimately still lacked a Tron keypair afterwards (e.g. because a *different*, since-fixed bug kept + * re-losing it) - it would never run again for that account, ever, on that install. [backfillIfNeeded] is cheap + * to call for an account that doesn't need it (a handful of null-checks, no derivation), so this just runs + * unconditionally on every app start instead: self-healing by construction, no stuck-flag failure mode possible. * * Only touches accounts that are: * - `Type.SECRETS` (mnemonic-derived) - watch-only/Ledger/Json/multisig/proxied accounts never had a @@ -52,18 +54,11 @@ private const val TAG = "TronAddressBackfill" * same Tron address it would have gotten had it been created today, not a separately-reimplemented derivation. */ class TronAddressBackfillMigration( - private val preferences: Preferences, private val secretStoreV2: SecretStoreV2, private val metaAccountDao: MetaAccountDao, private val accountSecretsFactory: AccountSecretsFactory, ) { - suspend fun migrationNeeded(): Boolean = withContext(Dispatchers.Default) { - val needed = !preferences.getBoolean(PREFS_TRON_ADDRESS_BACKFILL_DONE, false) - Log.d(TAG, "migrationNeeded = $needed") - needed - } - suspend fun migrate() = withContext(Dispatchers.Default) { val secretsAccounts = metaAccountDao.getMetaAccounts().filter { it.type == MetaAccountLocal.Type.SECRETS } Log.d(TAG, "migrate() starting - ${secretsAccounts.size} SECRETS-type account(s): ${secretsAccounts.map { it.id }}") @@ -76,8 +71,7 @@ class TronAddressBackfillMigration( } } - preferences.putBoolean(PREFS_TRON_ADDRESS_BACKFILL_DONE, true) - Log.d(TAG, "migrate() done, flag persisted") + Log.d(TAG, "migrate() done") } private suspend fun backfillIfNeeded(account: MetaAccountLocal) { diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/di/AccountFeatureModule.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/di/AccountFeatureModule.kt index 3532aa3e..d6e8edcf 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/di/AccountFeatureModule.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/di/AccountFeatureModule.kt @@ -445,12 +445,11 @@ class AccountFeatureModule { @Provides @FeatureScope fun provideTronAddressBackfillMigration( - preferences: Preferences, secretStoreV2: SecretStoreV2, metaAccountDao: MetaAccountDao, accountSecretsFactory: AccountSecretsFactory, ): TronAddressBackfillMigration { - return TronAddressBackfillMigration(preferences, secretStoreV2, metaAccountDao, accountSecretsFactory) + return TronAddressBackfillMigration(secretStoreV2, metaAccountDao, accountSecretsFactory) } @Provides diff --git a/feature-account-impl/src/test/java/io/novafoundation/nova/feature_account_impl/data/repository/datasource/migration/TronAddressBackfillMigrationTest.kt b/feature-account-impl/src/test/java/io/novafoundation/nova/feature_account_impl/data/repository/datasource/migration/TronAddressBackfillMigrationTest.kt index 3c1f4c50..6e075706 100644 --- a/feature-account-impl/src/test/java/io/novafoundation/nova/feature_account_impl/data/repository/datasource/migration/TronAddressBackfillMigrationTest.kt +++ b/feature-account-impl/src/test/java/io/novafoundation/nova/feature_account_impl/data/repository/datasource/migration/TronAddressBackfillMigrationTest.kt @@ -5,7 +5,6 @@ import io.novafoundation.nova.common.data.secrets.v2.MetaAccountSecrets import io.novafoundation.nova.common.data.secrets.v2.SecretStoreV2 import io.novafoundation.nova.common.data.secrets.v2.mapKeypairStructToKeypair import io.novafoundation.nova.common.data.secrets.v2.tronKeypair -import io.novafoundation.nova.common.data.storage.Preferences import io.novafoundation.nova.common.utils.invoke import io.novafoundation.nova.common.utils.tronAddressToAccountId import io.novafoundation.nova.common.utils.tronPublicKeyToAccountId @@ -17,7 +16,6 @@ import io.novasama.substrate_sdk_android.encrypt.json.JsonDecoder import io.novasama.substrate_sdk_android.encrypt.mnemonic.MnemonicCreator import io.novasama.substrate_sdk_android.scale.EncodableStruct import kotlinx.coroutines.runBlocking -import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -58,9 +56,6 @@ private fun whenever(methodCall: T?) = Mockito.`when`(methodCall) @RunWith(MockitoJUnitRunner::class) class TronAddressBackfillMigrationTest { - @Mock - lateinit var preferences: Preferences - @Mock lateinit var secretStoreV2: SecretStoreV2 @@ -79,19 +74,7 @@ class TronAddressBackfillMigrationTest { fun setup() { // Real factory, not a mock - the whole point of this test is to exercise the actual derivation. val accountSecretsFactory = AccountSecretsFactory(jsonDecoder) - subject = TronAddressBackfillMigration(preferences, secretStoreV2, metaAccountDao, accountSecretsFactory) - } - - @Test - fun `migrationNeeded should reflect the persisted flag`(): Unit = runBlocking { - // The flag's preference key is a private implementation detail of the production class - matched via - // any() here rather than duplicating the literal key string, which would let this test pass even if - // that string silently drifted out of sync with the production code. - whenever(preferences.getBoolean(any(), Mockito.anyBoolean())).thenReturn(false) - assertTrue(subject.migrationNeeded()) - - whenever(preferences.getBoolean(any(), Mockito.anyBoolean())).thenReturn(true) - assertTrue(!subject.migrationNeeded()) + subject = TronAddressBackfillMigration(secretStoreV2, metaAccountDao, accountSecretsFactory) } @Test