Updated on 2026-08-14
This commit is contained in:
parent
7bbab0a40b
commit
46dd1333ff
5 changed files with 306 additions and 2 deletions
|
|
@ -108,10 +108,16 @@ internal object AccountDataModule {
|
|||
@Provides
|
||||
@Singleton
|
||||
fun provideMainAccountTokensMigration(
|
||||
defaultMainAccountTokensMigration: DefaultMainAccountTokensMigration,
|
||||
): MainAccountTokensMigration = defaultMainAccountTokensMigration
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideDefaultMainAccountTokensMigration(
|
||||
accountsResponseStoreFactory: AccountsResponseStoreFactory,
|
||||
userTokensSaver: UserTokensSaver,
|
||||
accountTokenMigrationStore: AccountTokenMigrationStore,
|
||||
): MainAccountTokensMigration {
|
||||
): DefaultMainAccountTokensMigration {
|
||||
return DefaultMainAccountTokensMigration(
|
||||
accountsResponseStoreFactory = accountsResponseStoreFactory,
|
||||
accountTokenMigrationStore = accountTokenMigrationStore,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.tangem.data.account.fetcher
|
|||
|
||||
import com.tangem.data.account.store.AccountsResponseStore
|
||||
import com.tangem.data.account.store.AccountsResponseStoreFactory
|
||||
import com.tangem.data.account.tokens.DefaultMainAccountTokensMigration
|
||||
import com.tangem.data.account.utils.DefaultWalletAccountsResponseFactory
|
||||
import com.tangem.data.account.utils.assignTokens
|
||||
import com.tangem.data.common.account.WalletAccountsFetcher
|
||||
|
|
@ -50,6 +51,7 @@ internal class DefaultWalletAccountsFetcher @Inject constructor(
|
|||
private val defaultWalletAccountsResponseFactory: DefaultWalletAccountsResponseFactory,
|
||||
private val eTagsStore: ETagsStore,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val mainAccountTokensMigration: DefaultMainAccountTokensMigration,
|
||||
) : WalletAccountsFetcher, WalletAccountsSaver {
|
||||
|
||||
override suspend fun fetch(userWalletId: UserWalletId): GetWalletAccountsResponse {
|
||||
|
|
@ -71,7 +73,8 @@ internal class DefaultWalletAccountsFetcher @Inject constructor(
|
|||
throw fetchResult.error
|
||||
}
|
||||
|
||||
return updatedResponse
|
||||
val migratedResponse = mainAccountTokensMigration.migrate(userWalletId).getOrNull() ?: updatedResponse
|
||||
return migratedResponse
|
||||
}
|
||||
|
||||
override suspend fun getSaved(userWalletId: UserWalletId): GetWalletAccountsResponse? {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import arrow.core.raise.ensureNotNull
|
|||
import arrow.core.toNonEmptyListOrNull
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchainsdk.utils.fromNetworkId
|
||||
import com.tangem.data.account.converter.toDerivationIndex
|
||||
import com.tangem.data.account.store.AccountsResponseStoreFactory
|
||||
import com.tangem.data.account.utils.assignTokens
|
||||
import com.tangem.data.common.currency.UserTokensSaver
|
||||
|
|
@ -37,6 +38,67 @@ internal class DefaultMainAccountTokensMigration(
|
|||
private val userTokensSaver: UserTokensSaver,
|
||||
) : MainAccountTokensMigration {
|
||||
|
||||
internal suspend fun migrate(userWalletId: UserWalletId): Either<Throwable, GetWalletAccountsResponse> = either {
|
||||
val store = accountsResponseStoreFactory.create(userWalletId)
|
||||
val response = store.getSyncOrNull()
|
||||
ensureNotNull(response) {
|
||||
val exception = IllegalStateException("No cached accounts response found")
|
||||
Timber.e(exception)
|
||||
exception
|
||||
}
|
||||
val mainAccount = findAccount(response = response, derivationIndex = DerivationIndex.Main)
|
||||
val notMainAccounts = response.accounts
|
||||
.filterNot { accountDTO -> accountDTO.derivationIndex.toDerivationIndex().isMain }
|
||||
|
||||
if (notMainAccounts.isEmpty()) {
|
||||
Timber.i("There is only the Main account. Nothing to migrate")
|
||||
return@either response
|
||||
}
|
||||
|
||||
val unassignedTokens = mainAccount.groupUnassignedTokens()
|
||||
|
||||
if (unassignedTokens.isEmpty()) {
|
||||
Timber.i("No unassigned tokens found for migration")
|
||||
return@either response
|
||||
}
|
||||
|
||||
var updatedMainAccount = mainAccount
|
||||
val assignedTokensAccounts = notMainAccounts.mapNotNull { accountDTO ->
|
||||
val derivationIndex = accountDTO.derivationIndex.toDerivationIndex()
|
||||
val tokensForAccount = unassignedTokens[derivationIndex]
|
||||
if (tokensForAccount.isNullOrEmpty()) return@mapNotNull null
|
||||
updatedMainAccount = updatedMainAccount.copy(
|
||||
tokens = updatedMainAccount.tokens.orEmpty() - tokensForAccount,
|
||||
)
|
||||
accountDTO.assignTokens(userWalletId, tokensForAccount)
|
||||
}
|
||||
|
||||
val updatedResponse = response.copy(
|
||||
accounts = response.accounts.map { account ->
|
||||
val assignAccount = assignedTokensAccounts.find { it.id == account.id }
|
||||
when {
|
||||
account.id == mainAccount.id -> updatedMainAccount
|
||||
assignAccount != null -> assignAccount
|
||||
else -> account
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
store.updateData { updatedResponse }
|
||||
|
||||
val userTokensResponse = updatedResponse.toUserTokensResponse()
|
||||
userTokensSaver.pushWithRetryer(
|
||||
userWalletId = userWalletId,
|
||||
response = userTokensResponse,
|
||||
onFailSend = {
|
||||
val exception = IllegalStateException("Failed to push updated tokens after migration")
|
||||
Timber.e(exception)
|
||||
raise(exception)
|
||||
},
|
||||
)
|
||||
return@either updatedResponse
|
||||
}
|
||||
|
||||
override suspend fun migrate(
|
||||
userWalletId: UserWalletId,
|
||||
derivationIndex: DerivationIndex,
|
||||
|
|
@ -113,6 +175,22 @@ internal class DefaultMainAccountTokensMigration(
|
|||
}
|
||||
}
|
||||
|
||||
private fun WalletAccountDTO.groupUnassignedTokens(): Map<DerivationIndex, List<UserTokensResponse.Token>> =
|
||||
this.tokens
|
||||
.orEmpty()
|
||||
.mapNotNull { token ->
|
||||
val blockchain = Blockchain.fromNetworkId(token.networkId) ?: return@mapNotNull null
|
||||
val derivationPath = token.derivationPath ?: return@mapNotNull null
|
||||
val accountNode = AccountNodeRecognizer(blockchain)
|
||||
.recognize(derivationPath)
|
||||
?: return@mapNotNull null
|
||||
|
||||
if (accountNode == DerivationIndex.Main.value.toLong()) return@mapNotNull null
|
||||
val derivationIndex = DerivationIndex(accountNode.toInt()).getOrNull() ?: return@mapNotNull null
|
||||
derivationIndex to token.copy(accountId = null)
|
||||
}
|
||||
.groupBy({ it.first }, { it.second })
|
||||
|
||||
private fun WalletAccountDTO.findUnassignedTokens(
|
||||
derivationIndex: DerivationIndex,
|
||||
): List<UserTokensResponse.Token>? {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
package com.tangem.data.account.fetcher
|
||||
|
||||
import arrow.core.right
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.data.account.converter.createGetWalletAccountsResponse
|
||||
import com.tangem.data.account.converter.createWalletAccountDTO
|
||||
import com.tangem.data.account.fetcher.DefaultWalletAccountsFetcher.FetchResult
|
||||
import com.tangem.data.account.store.AccountsResponseStore
|
||||
import com.tangem.data.account.store.AccountsResponseStoreFactory
|
||||
import com.tangem.data.account.tokens.DefaultMainAccountTokensMigration
|
||||
import com.tangem.data.account.utils.DefaultWalletAccountsResponseFactory
|
||||
import com.tangem.data.common.cache.etag.ETagsStore
|
||||
import com.tangem.data.common.currency.UserTokensSaver
|
||||
|
|
@ -35,6 +37,7 @@ class DefaultWalletAccountsFetcherTest {
|
|||
private val accountsResponseStoreFactory: AccountsResponseStoreFactory = mockk()
|
||||
private val accountsResponseStore: AccountsResponseStore = mockk()
|
||||
private val accountsResponseStoreFlow = MutableStateFlow<GetWalletAccountsResponse?>(value = null)
|
||||
private val tokensMigration: DefaultMainAccountTokensMigration = mockk(relaxed = true)
|
||||
|
||||
private val userTokensSaver: UserTokensSaver = mockk(relaxUnitFun = true)
|
||||
private val fetchWalletAccountsErrorHandler: FetchWalletAccountsErrorHandler = mockk(relaxUnitFun = true)
|
||||
|
|
@ -49,16 +52,19 @@ class DefaultWalletAccountsFetcherTest {
|
|||
defaultWalletAccountsResponseFactory = defaultWalletAccountsResponseFactory,
|
||||
eTagsStore = eTagsStore,
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
mainAccountTokensMigration = tokensMigration,
|
||||
)
|
||||
|
||||
private val userWalletId = UserWalletId("011")
|
||||
private val eTag = "etag"
|
||||
private val migratedAccountsResponse = createGetWalletAccountsResponse(userWalletId)
|
||||
|
||||
@BeforeAll
|
||||
fun setUp() {
|
||||
every { accountsResponseStoreFactory.create(userWalletId) } returns accountsResponseStore
|
||||
every { accountsResponseStore.data } returns accountsResponseStoreFlow
|
||||
|
||||
coEvery { tokensMigration.migrate(userWalletId) } returns migratedAccountsResponse.right()
|
||||
coEvery { eTagsStore.getSyncOrNull(userWalletId, ETagsStore.Key.WalletAccounts) } returns eTag
|
||||
}
|
||||
|
||||
|
|
@ -134,6 +140,7 @@ class DefaultWalletAccountsFetcherTest {
|
|||
accountsResponseStore.updateData(any())
|
||||
accountsResponseStoreFactory.create(userWalletId = userWalletId)
|
||||
accountsResponseStore.updateData(any())
|
||||
tokensMigration.migrate(userWalletId)
|
||||
}
|
||||
|
||||
coVerify(inverse = true) {
|
||||
|
|
@ -181,6 +188,7 @@ class DefaultWalletAccountsFetcherTest {
|
|||
eTagsStore.store(userWalletId = userWalletId, key = ETagsStore.Key.WalletAccounts, value = newETag)
|
||||
accountsResponseStoreFactory.create(userWalletId = userWalletId)
|
||||
accountsResponseStore.updateData(any())
|
||||
tokensMigration.migrate(userWalletId)
|
||||
}
|
||||
|
||||
coVerify(inverse = true) {
|
||||
|
|
@ -236,6 +244,7 @@ class DefaultWalletAccountsFetcherTest {
|
|||
pushWalletAccounts = any(),
|
||||
storeWalletAccounts = any(),
|
||||
)
|
||||
tokensMigration.migrate(userWalletId)
|
||||
}
|
||||
|
||||
coVerify(inverse = true) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.data.account.token
|
||||
|
||||
import arrow.core.right
|
||||
import com.tangem.data.account.converter.createGetWalletAccountsResponse
|
||||
import com.tangem.data.account.converter.createWalletAccountDTO
|
||||
import com.tangem.data.account.store.AccountsResponseStore
|
||||
|
|
@ -13,6 +14,7 @@ import com.tangem.datasource.local.accounts.AccountTokenMigrationStore
|
|||
import com.tangem.domain.models.account.AccountId
|
||||
import com.tangem.domain.models.account.DerivationIndex
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.test.core.assertEither
|
||||
import com.tangem.test.core.assertEitherLeft
|
||||
import com.tangem.test.core.assertEitherRight
|
||||
import io.mockk.*
|
||||
|
|
@ -214,6 +216,212 @@ class DefaultMainAccountTokensMigrationTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `migrate updates tokens for all accounts`() = runTest {
|
||||
// Arrange
|
||||
val unassignedToken1 = createBitcoin(accountIndex = 1)
|
||||
val unassignedToken2 = createBitcoin(accountIndex = 2)
|
||||
|
||||
val derivationIndex1 = DerivationIndex(1).getOrNull()!!
|
||||
val derivationIndex2 = DerivationIndex(2).getOrNull()!!
|
||||
|
||||
val mainAccount = createWalletAccountDTO(
|
||||
userWalletId = userWalletId,
|
||||
accountId = AccountId.forCryptoPortfolio(userWalletId, DerivationIndex.Main).value,
|
||||
derivationIndex = DerivationIndex.Main.value,
|
||||
tokens = listOf(
|
||||
createBitcoin(accountIndex = 0),
|
||||
unassignedToken1,
|
||||
unassignedToken2,
|
||||
),
|
||||
)
|
||||
|
||||
val account1 = createWalletAccountDTO(
|
||||
userWalletId = userWalletId,
|
||||
accountId = AccountId.forCryptoPortfolio(userWalletId, derivationIndex1).value,
|
||||
derivationIndex = derivationIndex1.value,
|
||||
tokens = emptyList(),
|
||||
)
|
||||
|
||||
val account2 = createWalletAccountDTO(
|
||||
userWalletId = userWalletId,
|
||||
accountId = AccountId.forCryptoPortfolio(userWalletId, derivationIndex2).value,
|
||||
derivationIndex = derivationIndex2.value,
|
||||
tokens = emptyList(),
|
||||
)
|
||||
|
||||
val response = GetWalletAccountsResponse(
|
||||
wallet = GetWalletAccountsResponse.Wallet(
|
||||
group = UserTokensResponse.GroupType.NONE,
|
||||
sort = UserTokensResponse.SortType.MANUAL,
|
||||
totalAccounts = 3,
|
||||
totalArchivedAccounts = 0,
|
||||
),
|
||||
accounts = listOf(mainAccount, account1, account2),
|
||||
unassignedTokens = emptyList(),
|
||||
)
|
||||
|
||||
accountsResponseStoreFlow.value = response
|
||||
|
||||
coEvery { accountsResponseStore.updateData(any()) } returns mockk()
|
||||
|
||||
// Act
|
||||
val actual = migration.migrate(userWalletId)
|
||||
|
||||
val migratedResponse = response.copy(
|
||||
accounts = listOf(
|
||||
mainAccount.copy(tokens = mainAccount.tokens!! - unassignedToken1 - unassignedToken2),
|
||||
account1.copy(tokens = listOf(unassignedToken1)),
|
||||
account2.copy(tokens = listOf(unassignedToken2)),
|
||||
),
|
||||
)
|
||||
|
||||
// Assert
|
||||
assertEither(actual, migratedResponse.right())
|
||||
|
||||
coVerifySequence {
|
||||
accountsResponseStoreFactory.create(userWalletId)
|
||||
accountsResponseStore.data
|
||||
accountsResponseStore.updateData(any())
|
||||
userTokensSaver.pushWithRetryer(
|
||||
userWalletId = userWalletId,
|
||||
response = migratedResponse.toUserTokensResponse(),
|
||||
onFailSend = any(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `migrate updates tokens only for one account`() = runTest {
|
||||
// Arrange
|
||||
val unassignedToken1 = createBitcoin(accountIndex = 1)
|
||||
|
||||
val derivationIndex1 = DerivationIndex(1).getOrNull()!!
|
||||
val derivationIndex2 = DerivationIndex(2).getOrNull()!!
|
||||
|
||||
val mainAccount = createWalletAccountDTO(
|
||||
userWalletId = userWalletId,
|
||||
accountId = AccountId.forCryptoPortfolio(userWalletId, DerivationIndex.Main).value,
|
||||
derivationIndex = DerivationIndex.Main.value,
|
||||
tokens = listOf(
|
||||
createBitcoin(accountIndex = 0),
|
||||
unassignedToken1,
|
||||
),
|
||||
)
|
||||
|
||||
val account1 = createWalletAccountDTO(
|
||||
userWalletId = userWalletId,
|
||||
accountId = AccountId.forCryptoPortfolio(userWalletId, derivationIndex1).value,
|
||||
derivationIndex = derivationIndex1.value,
|
||||
tokens = emptyList(),
|
||||
)
|
||||
|
||||
val account2 = createWalletAccountDTO(
|
||||
userWalletId = userWalletId,
|
||||
accountId = AccountId.forCryptoPortfolio(userWalletId, derivationIndex2).value,
|
||||
derivationIndex = derivationIndex2.value,
|
||||
tokens = emptyList(),
|
||||
)
|
||||
|
||||
val response = GetWalletAccountsResponse(
|
||||
wallet = GetWalletAccountsResponse.Wallet(
|
||||
group = UserTokensResponse.GroupType.NONE,
|
||||
sort = UserTokensResponse.SortType.MANUAL,
|
||||
totalAccounts = 3,
|
||||
totalArchivedAccounts = 0,
|
||||
),
|
||||
accounts = listOf(mainAccount, account1, account2),
|
||||
unassignedTokens = emptyList(),
|
||||
)
|
||||
|
||||
accountsResponseStoreFlow.value = response
|
||||
|
||||
coEvery { accountsResponseStore.updateData(any()) } returns mockk()
|
||||
|
||||
// Act
|
||||
val actual = migration.migrate(userWalletId)
|
||||
|
||||
val migratedResponse = response.copy(
|
||||
accounts = listOf(
|
||||
mainAccount.copy(tokens = mainAccount.tokens!! - unassignedToken1),
|
||||
account1.copy(tokens = listOf(unassignedToken1)),
|
||||
account2,
|
||||
),
|
||||
)
|
||||
|
||||
// Assert
|
||||
assertEither(actual, migratedResponse.right())
|
||||
|
||||
coVerifySequence {
|
||||
accountsResponseStoreFactory.create(userWalletId)
|
||||
accountsResponseStore.data
|
||||
accountsResponseStore.updateData(any())
|
||||
userTokensSaver.pushWithRetryer(
|
||||
userWalletId = userWalletId,
|
||||
response = migratedResponse.toUserTokensResponse(),
|
||||
onFailSend = any(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `migrate all skips when no unassigned tokens`() = runTest {
|
||||
// Arrange
|
||||
val derivationIndex1 = DerivationIndex(1).getOrNull()!!
|
||||
val derivationIndex2 = DerivationIndex(2).getOrNull()!!
|
||||
|
||||
val mainAccount = createWalletAccountDTO(
|
||||
userWalletId = userWalletId,
|
||||
accountId = AccountId.forCryptoPortfolio(userWalletId, DerivationIndex.Main).value,
|
||||
derivationIndex = DerivationIndex.Main.value,
|
||||
tokens = listOf(
|
||||
createBitcoin(accountIndex = 0),
|
||||
),
|
||||
)
|
||||
|
||||
val account1 = createWalletAccountDTO(
|
||||
userWalletId = userWalletId,
|
||||
accountId = AccountId.forCryptoPortfolio(userWalletId, derivationIndex1).value,
|
||||
derivationIndex = derivationIndex1.value,
|
||||
tokens = emptyList(),
|
||||
)
|
||||
|
||||
val account2 = createWalletAccountDTO(
|
||||
userWalletId = userWalletId,
|
||||
accountId = AccountId.forCryptoPortfolio(userWalletId, derivationIndex2).value,
|
||||
derivationIndex = derivationIndex2.value,
|
||||
tokens = emptyList(),
|
||||
)
|
||||
|
||||
val response = GetWalletAccountsResponse(
|
||||
wallet = GetWalletAccountsResponse.Wallet(
|
||||
group = UserTokensResponse.GroupType.NONE,
|
||||
sort = UserTokensResponse.SortType.MANUAL,
|
||||
totalAccounts = 3,
|
||||
totalArchivedAccounts = 0,
|
||||
),
|
||||
accounts = listOf(mainAccount, account1, account2),
|
||||
unassignedTokens = emptyList(),
|
||||
)
|
||||
|
||||
accountsResponseStoreFlow.value = response
|
||||
|
||||
// Act
|
||||
val actual = migration.migrate(userWalletId)
|
||||
|
||||
// Assert
|
||||
assertEither(actual, response.right())
|
||||
|
||||
coVerifySequence {
|
||||
accountsResponseStoreFactory.create(userWalletId)
|
||||
accountsResponseStore.data
|
||||
}
|
||||
|
||||
coVerify(inverse = true) {
|
||||
userTokensSaver.pushWithRetryer(userWalletId = any(), response = any(), onFailSend = any())
|
||||
}
|
||||
}
|
||||
|
||||
private fun createBitcoin(accountIndex: Int): UserTokensResponse.Token {
|
||||
return UserTokensResponse.Token(
|
||||
id = "ne",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue