Updated on 2026-08-14

This commit is contained in:
Tangem 2025-11-07 13:54:47 +04:00
parent 3088258c74
commit 50c633e873
11 changed files with 119 additions and 33 deletions

View file

@ -182,7 +182,7 @@ internal class DefaultWalletAccountsFetcher @Inject constructor(
store(userWalletId = userWalletId, response = accountsResponseWithTokens)
userTokensSaver.push(
userTokensSaver.pushWithRetryer(
userWalletId = userWalletId,
response = accountsResponseWithTokens.toUserTokensResponse(),
)

View file

@ -84,7 +84,7 @@ internal class FetchWalletAccountsErrorHandler @Inject constructor(
eTagsStore.store(userWalletId = userWalletId, key = ETagsStore.Key.WalletAccounts, value = eTag)
pushWalletAccounts(userWalletId, accountDTOs)
userTokensSaver.push(userWalletId, userTokensResponse)
userTokensSaver.pushWithRetryer(userWalletId, userTokensResponse)
}
}

View file

@ -181,7 +181,7 @@ internal class DefaultAccountsCRUDRepository(
return
}
userTokensSaver.push(userWalletId = userWalletId, response = response.toUserTokensResponse())
userTokensSaver.pushWithRetryer(userWalletId = userWalletId, response = response.toUserTokensResponse())
}
override suspend fun getTotalAccountsCountSync(userWalletId: UserWalletId): Option<Int> = option {

View file

@ -80,12 +80,11 @@ internal class DefaultMainAccountTokensMigration(
store.updateData { updatedResponse }
userTokensSaver.push(
val userTokensResponse = updatedResponse.toUserTokensResponse()
userTokensSaver.pushWithRetryer(
userWalletId = userWalletId,
response = updatedResponse.toUserTokensResponse(),
response = userTokensResponse,
onFailSend = {
// TODO: save failed state to retry later
// [REDACTED_JIRA]
val exception = IllegalStateException("Failed to push updated tokens after migration")
Timber.e(exception)
raise(exception)

View file

@ -86,7 +86,7 @@ class FetchWalletAccountsErrorHandlerTest {
userTokensResponseStore.getSyncOrNull(userWalletId = any())
defaultWalletAccountsResponseFactory.create(userWalletId = any(), userTokensResponse = any())
pushWalletAccounts(any(), any())
userTokensSaver.push(userWalletId = any(), response = any())
userTokensSaver.pushWithRetryer(userWalletId = any(), response = any())
storeWalletAccounts(any(), any())
}
}
@ -127,7 +127,7 @@ class FetchWalletAccountsErrorHandlerTest {
// Assert
coVerify {
userTokensSaver.push(userWalletId, response = savedAccountsResponse.toUserTokensResponse())
userTokensSaver.pushWithRetryer(userWalletId, response = savedAccountsResponse.toUserTokensResponse())
tangemTechApi.createWallet(OnlyWalletIdBody(userWalletId.stringValue))
eTagsStore.store(userWalletId, ETagsStore.Key.WalletAccounts, eTagValue)
pushWalletAccounts(userWalletId, listOf(accountDTO))
@ -191,7 +191,7 @@ class FetchWalletAccountsErrorHandlerTest {
coVerify(inverse = true) {
pushWalletAccounts(any(), any())
userTokensSaver.push(userWalletId = any(), response = any())
userTokensSaver.pushWithRetryer(userWalletId = any(), response = any())
}
}

View file

@ -30,7 +30,8 @@ class DefaultMainAccountTokensMigrationTest {
private val accountsResponseStore = mockk<AccountsResponseStore>()
private val accountsResponseStoreFlow = MutableStateFlow<GetWalletAccountsResponse?>(value = null)
private val userTokensSaver = mockk<UserTokensSaver>(relaxed = true)
private val userTokensSaver = mockk<UserTokensSaver>(relaxUnitFun = true)
private val migration = DefaultMainAccountTokensMigration(
accountsResponseStoreFactory = accountsResponseStoreFactory,
userTokensSaver = userTokensSaver,
@ -62,7 +63,7 @@ class DefaultMainAccountTokensMigrationTest {
coVerify(inverse = true) {
accountsResponseStoreFactory.create(any())
accountsResponseStore.data
userTokensSaver.push(userWalletId = any(), response = any(), onFailSend = any())
userTokensSaver.pushWithRetryer(userWalletId = any(), response = any(), onFailSend = any())
}
}
@ -81,7 +82,7 @@ class DefaultMainAccountTokensMigrationTest {
}
coVerify(inverse = true) {
userTokensSaver.push(userWalletId = any(), response = any(), onFailSend = any())
userTokensSaver.pushWithRetryer(userWalletId = any(), response = any(), onFailSend = any())
}
}
@ -110,7 +111,7 @@ class DefaultMainAccountTokensMigrationTest {
}
coVerify(inverse = true) {
userTokensSaver.push(userWalletId = any(), response = any(), onFailSend = any())
userTokensSaver.pushWithRetryer(userWalletId = any(), response = any(), onFailSend = any())
}
}
@ -145,7 +146,7 @@ class DefaultMainAccountTokensMigrationTest {
}
coVerify(inverse = true) {
userTokensSaver.push(userWalletId = any(), response = any(), onFailSend = any())
userTokensSaver.pushWithRetryer(userWalletId = any(), response = any(), onFailSend = any())
}
}
@ -202,7 +203,7 @@ class DefaultMainAccountTokensMigrationTest {
accountsResponseStoreFactory.create(userWalletId)
accountsResponseStore.data
accountsResponseStore.updateData(any())
userTokensSaver.push(
userTokensSaver.pushWithRetryer(
userWalletId = userWalletId,
response = migratedResponse.toUserTokensResponse(),
onFailSend = any(),

View file

@ -8,7 +8,10 @@ import com.tangem.datasource.local.token.UserTokensResponseStore
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.retryer.Retryer
import com.tangem.utils.retryer.RetryerPool
import kotlinx.coroutines.withContext
import timber.log.Timber
class UserTokensSaver(
private val tangemTechApi: TangemTechApi,
@ -16,6 +19,7 @@ class UserTokensSaver(
private val dispatchers: CoroutineDispatcherProvider,
private val addressesEnricher: UserTokensResponseAddressesEnricher,
private val accountsFeatureToggles: AccountsFeatureToggles,
private val pushTokensRetryerPool: RetryerPool,
) {
private val userTokensBackwardCompatibility = UserTokensBackwardCompatibility()
@ -58,6 +62,23 @@ class UserTokensSaver(
}
}
suspend fun pushWithRetryer(
userWalletId: UserWalletId,
response: UserTokensResponse,
useEnricher: Boolean = true,
onFailSend: () -> Unit = {},
) {
push(
userWalletId = userWalletId,
response = response,
useEnricher = useEnricher,
onFailSend = {
pushTokensRetryerPool + createPushTokensRetryer(userWalletId, response)
onFailSend()
},
)
}
private fun UserTokensResponse.applyCompatibility(): UserTokensResponse {
return userTokensBackwardCompatibility.applyCompatibilityAndGetUpdated(userTokensResponse = this)
}
@ -86,4 +107,24 @@ class UserTokensSaver(
private fun UserTokensResponse.enrichByAccountId(userWalletId: UserWalletId): UserTokensResponse {
return UserTokensResponseAccountIdEnricher(userWalletId = userWalletId, response = this)
}
private fun createPushTokensRetryer(userWalletId: UserWalletId, response: UserTokensResponse): Retryer {
return Retryer(attempt = 3) { iteration ->
var isSuccess = true
push(
userWalletId = userWalletId,
response = response,
onFailSend = {
Timber.e(
"Retryer: Failed to push updated tokens on attempt ${iteration + 1} for $userWalletId",
)
isSuccess = false
},
)
isSuccess
}
}
}

View file

@ -16,10 +16,13 @@ import com.tangem.domain.demo.models.DemoConfig
import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.retryer.RetryerPool
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import javax.inject.Singleton
@Module
@ -76,6 +79,9 @@ internal object DataCommonModule {
dispatchers = dispatchers,
addressesEnricher = addressesEnricher,
accountsFeatureToggles = accountsFeatureToggles,
pushTokensRetryerPool = RetryerPool(
coroutineScope = CoroutineScope(SupervisorJob() + dispatchers.default),
),
)
}

View file

@ -30,6 +30,7 @@ class UserTokensSaverTest {
dispatchers = TestingCoroutineDispatcherProvider(),
addressesEnricher = enricher,
accountsFeatureToggles = accountsFeatureToggles,
pushTokensRetryerPool = mockk(),
)
@BeforeEach