From 46f8abc04ca0f69d0dc9250595e3f1eff8f91ffd Mon Sep 17 00:00:00 2001 From: Satoshi Qazi Muhammed Date: Fri, 10 Jul 2026 12:45:50 -0700 Subject: [PATCH] fix: backfill Tron address for pre-existing wallets - TRX was invisible for every account created before Tron support shipped Found via manual device testing against a real, pre-Tron production wallet: TRX/USDT-TRC20 didn't appear anywhere in the Assets list, not even as a zero balance - unlike every other configured chain (KSM, USDC, ETH, BNB, AVAX, LINK all show up at 0). Root cause: MetaAccount.hasAccountIn() gates Tron balance sync on tronAddress != null, but tronAddress is only ever populated once, at fresh-mnemonic-creation time in AccountSecretsFactory.metaAccountSecrets(). The migration that added the tronPublicKey/tronAddress columns (73_74_AddTronSupport) is pure ALTER TABLE like every other migration in this codebase - it never derived a value for pre-existing rows. Net effect: BalancesUpdateSystem silently skips Tron entirely for every wallet that existed before this feature shipped, with zero error surfaced anywhere. No automated test catches this because every automated test creates a fresh (post-Tron) account - this is exactly the class of bug that only manual testing against a real, aged wallet can find. Adds TronAddressBackfillMigration: a one-time, flag-gated pass (mirroring the existing AccountDataMigration idiom) over SECRETS-type accounts that still hold their mnemonic entropy in SecretStoreV2 but have no TronKeypair yet. Derives the Tron keypair via AccountSecretsFactory.chainAccountSecrets (isEthereum=true, same BIP32/secp256k1 path Tron already uses everywhere else) at TRON_DEFAULT_DERIVATION_PATH - the same call fresh-account creation already makes - so a backfilled wallet ends up with byte-for-byte the same Tron address it would have gotten had it been created today, not a separately-reimplemented derivation. Watch-only/Ledger/Json/multisig/ proxied accounts (never had a Tron-capable mnemonic) and raw-seed imports (no entropy) are correctly left untouched, same as they already are for Ethereum. Includes a dedicated unit test asserting the backfill derives the exact same reference Tron address (TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH) that TronDerivationTest already cross-validated against live TronGrid data for the standard BIP39 test mnemonic - this touches real wallets' key material, so it's pinned to a known-good vector rather than just asserting against its own output. --- .../model/chain/account/MetaAccountLocal.kt | 26 +++ .../datasource/AccountDataSourceImpl.kt | 18 +- .../migration/TronAddressBackfillMigration.kt | 105 +++++++++ .../di/AccountFeatureModule.kt | 16 +- .../TronAddressBackfillMigrationTest.kt | 199 ++++++++++++++++++ 5 files changed, 358 insertions(+), 6 deletions(-) create mode 100644 feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/repository/datasource/migration/TronAddressBackfillMigration.kt create mode 100644 feature-account-impl/src/test/java/io/novafoundation/nova/feature_account_impl/data/repository/datasource/migration/TronAddressBackfillMigrationTest.kt diff --git a/core-db/src/main/java/io/novafoundation/nova/core_db/model/chain/account/MetaAccountLocal.kt b/core-db/src/main/java/io/novafoundation/nova/core_db/model/chain/account/MetaAccountLocal.kt index 4886b762..edc1c3e3 100644 --- a/core-db/src/main/java/io/novafoundation/nova/core_db/model/chain/account/MetaAccountLocal.kt +++ b/core-db/src/main/java/io/novafoundation/nova/core_db/model/chain/account/MetaAccountLocal.kt @@ -96,6 +96,32 @@ class MetaAccountLocal( } } + // We do not use copy as we need explicitly set id + fun addTronAccount( + tronPublicKey: ByteArray, + tronAddress: ByteArray, + ): MetaAccountLocal { + return MetaAccountLocal( + substratePublicKey = substratePublicKey, + substrateCryptoType = substrateCryptoType, + substrateAccountId = substrateAccountId, + ethereumPublicKey = ethereumPublicKey, + ethereumAddress = ethereumAddress, + name = name, + parentMetaId = parentMetaId, + isSelected = isSelected, + position = position, + type = type, + status = status, + globallyUniqueId = globallyUniqueId, + typeExtras = typeExtras, + tronPublicKey = tronPublicKey, + tronAddress = tronAddress + ).also { + it.id = id + } + } + override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is MetaAccountLocal) return false 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 b4021b68..580998af 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 @@ -30,6 +30,7 @@ import io.novafoundation.nova.feature_account_impl.data.mappers.AccountMappers import io.novafoundation.nova.feature_account_impl.data.mappers.mapMetaAccountTypeToLocal import io.novafoundation.nova.feature_account_impl.data.mappers.mapMetaAccountWithBalanceFromLocal import io.novafoundation.nova.feature_account_impl.data.repository.datasource.migration.AccountDataMigration +import io.novafoundation.nova.feature_account_impl.data.repository.datasource.migration.TronAddressBackfillMigration import io.novafoundation.nova.feature_account_impl.data.repository.datasource.migration.model.ChainAccountInsertionData import io.novafoundation.nova.feature_account_impl.data.repository.datasource.migration.model.MetaAccountInsertionData import io.novafoundation.nova.runtime.ext.accountIdOf @@ -64,15 +65,22 @@ class AccountDataSourceImpl( private val secretsMetaAccountLocalFactory: SecretsMetaAccountLocalFactory, secretStoreV1: SecretStoreV1, accountDataMigration: AccountDataMigration, + tronAddressBackfillMigration: TronAddressBackfillMigration, ) : AccountDataSource, SecretStoreV1 by secretStoreV1 { init { - migrateIfNeeded(accountDataMigration) - } + // Run sequentially in one coroutine, not as two independent migrateIfNeeded() launches - the Tron + // backfill reads accounts/secrets that the legacy migration may still be in the middle of writing for + // very old (pre-MetaAccount) installs, and two separate GlobalScope.launch calls give no ordering + // guarantee relative to each other. + async { + if (accountDataMigration.migrationNeeded()) { + accountDataMigration.migrate(::saveSecuritySource) + } - private fun migrateIfNeeded(migration: AccountDataMigration) = async { - if (migration.migrationNeeded()) { - migration.migrate(::saveSecuritySource) + if (tronAddressBackfillMigration.migrationNeeded()) { + tronAddressBackfillMigration.migrate() + } } } 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 new file mode 100644 index 00000000..c0723cf6 --- /dev/null +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/repository/datasource/migration/TronAddressBackfillMigration.kt @@ -0,0 +1,105 @@ +package io.novafoundation.nova.feature_account_impl.data.repository.datasource.migration + +import io.novafoundation.nova.common.data.secrets.v2.ChainAccountSecrets +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.entropy +import io.novafoundation.nova.common.data.secrets.v2.ethereumDerivationPath +import io.novafoundation.nova.common.data.secrets.v2.ethereumKeypair +import io.novafoundation.nova.common.data.secrets.v2.mapKeypairStructToKeypair +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 +import io.novafoundation.nova.core_db.model.chain.account.MetaAccountLocal +import io.novafoundation.nova.feature_account_impl.data.secrets.AccountSecretsFactory +import io.novafoundation.nova.feature_account_impl.data.secrets.TRON_DEFAULT_DERIVATION_PATH +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" + +/** + * 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. + * + * Only touches accounts that are: + * - `Type.SECRETS` (mnemonic-derived) - watch-only/Ledger/Json/multisig/proxied accounts never had a + * Tron-capable mnemonic and are correctly left with `tronAddress == null` forever, same as they already are + * for Ethereum. + * - still holding their `Entropy` in [SecretStoreV2] - an account imported from a raw seed/keypair rather than + * a mnemonic has no entropy either and is likewise correctly left alone. + * - missing a `TronKeypair` - i.e. not already backfilled and not created after Tron support shipped. + * + * The Tron keypair is derived via [AccountSecretsFactory.chainAccountSecrets] with `isEthereum = true` (Tron + * reuses the exact same secp256k1/BIP32 derivation as Ethereum, just under its own SLIP-44 coin-type-195 path - + * see that class's own doc comment) at [TRON_DEFAULT_DERIVATION_PATH], the same call this codebase's own + * `metaAccountSecrets()` makes for a fresh account - so a backfilled account ends up with byte-for-byte the + * 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) { + !preferences.getBoolean(PREFS_TRON_ADDRESS_BACKFILL_DONE, false) + } + + suspend fun migrate() = withContext(Dispatchers.Default) { + val secretsAccounts = metaAccountDao.getMetaAccounts().filter { it.type == MetaAccountLocal.Type.SECRETS } + + secretsAccounts.forEach { account -> + backfillIfNeeded(account) + } + + preferences.putBoolean(PREFS_TRON_ADDRESS_BACKFILL_DONE, true) + } + + private suspend fun backfillIfNeeded(account: MetaAccountLocal) { + val secrets = secretStoreV2.getMetaAccountSecrets(account.id) ?: return + val entropy = secrets.entropy ?: return + if (secrets.tronKeypair != null) return + val substrateCryptoType = account.substrateCryptoType ?: return + + val mnemonic = MnemonicCreator.fromEntropy(entropy).words + + val tronChainSecrets = accountSecretsFactory.chainAccountSecrets( + derivationPath = TRON_DEFAULT_DERIVATION_PATH, + accountSource = AccountSecretsFactory.AccountSource.Mnemonic(substrateCryptoType, mnemonic), + isEthereum = true + ).secrets + + val tronKeypair = mapKeypairStructToKeypair(tronChainSecrets[ChainAccountSecrets.Keypair]) + + val updatedSecrets = MetaAccountSecrets( + substrateKeyPair = mapKeypairStructToKeypair(secrets.substrateKeypair), + entropy = secrets.entropy, + substrateSeed = secrets.seed, + substrateDerivationPath = secrets.substrateDerivationPath, + ethereumKeypair = secrets.ethereumKeypair?.let(::mapKeypairStructToKeypair), + ethereumDerivationPath = secrets.ethereumDerivationPath, + tronKeypair = tronKeypair, + tronDerivationPath = TRON_DEFAULT_DERIVATION_PATH + ) + + secretStoreV2.putMetaAccountSecrets(account.id, updatedSecrets) + + val tronAccountId = tronKeypair.publicKey.tronPublicKeyToAccountId() + metaAccountDao.updateMetaAccount(account.id) { it.addTronAccount(tronKeypair.publicKey, tronAccountId) } + } +} 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 42a1baec..3532aa3e 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 @@ -104,6 +104,7 @@ import io.novafoundation.nova.feature_account_impl.data.repository.datasource.Ac import io.novafoundation.nova.feature_account_impl.data.repository.datasource.RealSecretsMetaAccountLocalFactory import io.novafoundation.nova.feature_account_impl.data.repository.datasource.SecretsMetaAccountLocalFactory import io.novafoundation.nova.feature_account_impl.data.repository.datasource.migration.AccountDataMigration +import io.novafoundation.nova.feature_account_impl.data.repository.datasource.migration.TronAddressBackfillMigration import io.novafoundation.nova.feature_account_impl.data.secrets.AccountSecretsFactory import io.novafoundation.nova.feature_account_impl.data.signer.signingContext.SigningContextFactory import io.novafoundation.nova.feature_account_impl.di.AccountFeatureModule.BindsModule @@ -402,6 +403,7 @@ class AccountFeatureModule { nodeDao: NodeDao, secretStoreV1: SecretStoreV1, accountDataMigration: AccountDataMigration, + tronAddressBackfillMigration: TronAddressBackfillMigration, metaAccountDao: MetaAccountDao, secretsMetaAccountLocalFactory: SecretsMetaAccountLocalFactory, secretStoreV2: SecretStoreV2, @@ -416,7 +418,8 @@ class AccountFeatureModule { secretStoreV2, secretsMetaAccountLocalFactory, secretStoreV1, - accountDataMigration + accountDataMigration, + tronAddressBackfillMigration ) } @@ -439,6 +442,17 @@ class AccountFeatureModule { return AccountDataMigration(preferences, encryptedPreferences, accountDao) } + @Provides + @FeatureScope + fun provideTronAddressBackfillMigration( + preferences: Preferences, + secretStoreV2: SecretStoreV2, + metaAccountDao: MetaAccountDao, + accountSecretsFactory: AccountSecretsFactory, + ): TronAddressBackfillMigration { + return TronAddressBackfillMigration(preferences, secretStoreV2, metaAccountDao, accountSecretsFactory) + } + @Provides @FeatureScope fun provideExternalAccountActions( 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 new file mode 100644 index 00000000..3cfc195d --- /dev/null +++ b/feature-account-impl/src/test/java/io/novafoundation/nova/feature_account_impl/data/repository/datasource/migration/TronAddressBackfillMigrationTest.kt @@ -0,0 +1,199 @@ +package io.novafoundation.nova.feature_account_impl.data.repository.datasource.migration + +import io.novafoundation.nova.common.data.secrets.v2.KeyPairSchema +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.tronAddressToAccountId +import io.novafoundation.nova.common.utils.tronPublicKeyToAccountId +import io.novafoundation.nova.core.model.CryptoType +import io.novafoundation.nova.core_db.dao.MetaAccountDao +import io.novafoundation.nova.core_db.model.chain.account.MetaAccountLocal +import io.novafoundation.nova.feature_account_impl.data.secrets.AccountSecretsFactory +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.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatcher +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import org.mockito.junit.MockitoJUnitRunner + +// Same guaranteed-non-null-return wrappers as RealTronTransactionServiceTest, and for the same reason: the +// shared test_shared eq()/any() helpers crash with "eq(...) must not be null" once Mockito's genuinely-null +// runtime return flows into a Kotlin non-null-typed parameter - see that test's class doc for the full writeup. +private fun eq(value: T): T = Mockito.eq(value) ?: value + +@Suppress("UNCHECKED_CAST") +private fun any(): T { + Mockito.any() + return null as T +} + +@Suppress("UNCHECKED_CAST") +private fun argThat(matcher: (T) -> Boolean): T { + Mockito.argThat(ArgumentMatcher { matcher(it) }) + return null as T +} + +private fun whenever(methodCall: T?) = Mockito.`when`(methodCall) + +/** + * This is the money-safety-critical half of the Tron backfill fix (see TronAddressBackfillMigration's class + * doc for the full context): a wrong derivation here would silently give a pre-existing wallet a Tron address + * it does NOT actually control, or fail to skip an account it shouldn't touch. Uses a real (non-mocked) + * AccountSecretsFactory so the actual BIP32/secp256k1 derivation math runs for real, and asserts against the + * same live-TronGrid-cross-validated reference address TronDerivationTest already established for the standard + * BIP39 test mnemonic, so this test and that one are pinned to the same known-good vector. + */ +@RunWith(MockitoJUnitRunner::class) +class TronAddressBackfillMigrationTest { + + @Mock + lateinit var preferences: Preferences + + @Mock + lateinit var secretStoreV2: SecretStoreV2 + + @Mock + lateinit var metaAccountDao: MetaAccountDao + + @Mock + lateinit var jsonDecoder: JsonDecoder + + private lateinit var subject: TronAddressBackfillMigration + + private val testMnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" + private val expectedTronAccountId = "TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH".tronAddressToAccountId() + + @Before + 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) + + // any() would try to unbox a null placeholder into the primitive Boolean parameter and crash - Mockito's + // own anyBoolean() returns a real `false` default instead, avoiding that entirely. + whenever(preferences.getBoolean(any(), Mockito.anyBoolean())).thenReturn(false) + } + + @Test + fun `migrate should derive and persist the well-known reference Tron address for a pre-existing mnemonic account`(): Unit = runBlocking { + val account = secretsAccount(id = 42, substrateCryptoType = CryptoType.SR25519) + val secrets = accountSecrets(entropy = MnemonicCreator.fromWords(testMnemonic).entropy, tronKeypair = null) + + whenever(metaAccountDao.getMetaAccounts()).thenReturn(listOf(account)) + whenever(metaAccountDao.getMetaAccount(eq(42L))).thenReturn(account) + whenever(secretStoreV2.getMetaAccountSecrets(eq(42L))).thenReturn(secrets) + + subject.migrate() + + verify(metaAccountDao).updateMetaAccount( + argThat { updated -> + updated.id == 42L && updated.tronAddress.contentEquals(expectedTronAccountId) + } + ) + + verify(secretStoreV2).putMetaAccountSecrets( + eq(42L), + argThat> { updatedSecrets -> + val tronKeypairStruct = updatedSecrets.tronKeypair + tronKeypairStruct != null && + mapKeypairStructToKeypair(tronKeypairStruct).publicKey.tronPublicKeyToAccountId().contentEquals(expectedTronAccountId) + } + ) + } + + @Test + fun `migrate should skip an account that already has a Tron keypair`(): Unit = runBlocking { + val account = secretsAccount(id = 7, substrateCryptoType = CryptoType.SR25519) + val existingTronKeypair = KeyPairSchema { keypair -> + keypair[KeyPairSchema.PublicKey] = ByteArray(33) { 9 } + keypair[KeyPairSchema.PrivateKey] = ByteArray(32) { 8 } + keypair[KeyPairSchema.Nonce] = null + } + val secrets = accountSecrets(entropy = MnemonicCreator.fromWords(testMnemonic).entropy, tronKeypair = existingTronKeypair) + + whenever(metaAccountDao.getMetaAccounts()).thenReturn(listOf(account)) + whenever(secretStoreV2.getMetaAccountSecrets(eq(7L))).thenReturn(secrets) + + subject.migrate() + + verify(metaAccountDao, never()).updateMetaAccount(any()) + // metaId is a primitive Long parameter - same anyBoolean() reasoning applies, use anyLong(). + verify(secretStoreV2, never()).putMetaAccountSecrets(Mockito.anyLong(), any()) + } + + @Test + fun `migrate should skip an account with no entropy (raw-seed import, not a mnemonic)`(): Unit = runBlocking { + val account = secretsAccount(id = 11, substrateCryptoType = CryptoType.SR25519) + val secrets = accountSecrets(entropy = null, tronKeypair = null) + + whenever(metaAccountDao.getMetaAccounts()).thenReturn(listOf(account)) + whenever(secretStoreV2.getMetaAccountSecrets(eq(11L))).thenReturn(secrets) + + subject.migrate() + + verify(metaAccountDao, never()).updateMetaAccount(any()) + // metaId is a primitive Long parameter - same anyBoolean() reasoning applies, use anyLong(). + verify(secretStoreV2, never()).putMetaAccountSecrets(Mockito.anyLong(), any()) + } + + @Test + fun `migrate should skip an account with no stored secrets at all (watch-only, Ledger, etc)`(): Unit = runBlocking { + val account = secretsAccount(id = 13, substrateCryptoType = CryptoType.SR25519) + + whenever(metaAccountDao.getMetaAccounts()).thenReturn(listOf(account)) + whenever(secretStoreV2.getMetaAccountSecrets(eq(13L))).thenReturn(null) + + subject.migrate() + + verify(metaAccountDao, never()).updateMetaAccount(any()) + // metaId is a primitive Long parameter - same anyBoolean() reasoning applies, use anyLong(). + verify(secretStoreV2, never()).putMetaAccountSecrets(Mockito.anyLong(), any()) + } + + private fun secretsAccount(id: Long, substrateCryptoType: CryptoType): MetaAccountLocal { + return MetaAccountLocal( + substratePublicKey = ByteArray(32) { 1 }, + substrateCryptoType = substrateCryptoType, + substrateAccountId = ByteArray(32) { 2 }, + ethereumPublicKey = null, + ethereumAddress = null, + name = "Test account", + parentMetaId = null, + isSelected = true, + position = 0, + type = MetaAccountLocal.Type.SECRETS, + status = MetaAccountLocal.Status.ACTIVE, + globallyUniqueId = "test-guid-$id", + typeExtras = null + ).also { it.id = id } + } + + private fun accountSecrets(entropy: ByteArray?, tronKeypair: EncodableStruct?): EncodableStruct { + val dummySubstrateKeypair = KeyPairSchema { keypair -> + keypair[KeyPairSchema.PublicKey] = ByteArray(32) { 5 } + keypair[KeyPairSchema.PrivateKey] = ByteArray(32) { 6 } + keypair[KeyPairSchema.Nonce] = ByteArray(8) { 7 } + } + + return MetaAccountSecrets( + substrateKeyPair = mapKeypairStructToKeypair(dummySubstrateKeypair), + entropy = entropy, + substrateSeed = null, + substrateDerivationPath = null, + ethereumKeypair = null, + ethereumDerivationPath = null, + tronKeypair = tronKeypair?.let(::mapKeypairStructToKeypair), + tronDerivationPath = null + ) + } +}