mirror of
https://github.com/pezkuwichain/pezkuwi-wallet-android.git
synced 2026-07-22 21:55:53 +00:00
fix: make Tron address backfill self-healing instead of one-shot
TronAddressBackfillMigration was gated behind a global SharedPreferences
flag ("has this migration ever run on this install") rather than checking
per-account whether a Tron keypair is actually missing. Once that flag was
set - even by a run that found nothing to fix, or partially failed - the
migration never ran again on that install, so an account that lost its Tron
keypair some other way (the cloud-backup schema gap fixed in c78b325) could
never be repaired without a fresh reinstall.
backfillIfNeeded() already does a cheap, correct per-account check (skips
instantly if the account already has a keypair, isn't SECRETS-type, has no
entropy, etc.), so there's no need for an outer one-shot gate at all - just
run it unconditionally on every app start.
This commit is contained in:
+2
-4
@@ -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")
|
||||
|
||||
|
||||
+14
-20
@@ -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) {
|
||||
|
||||
+1
-2
@@ -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
|
||||
|
||||
+1
-18
@@ -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 <T> 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
|
||||
|
||||
Reference in New Issue
Block a user