diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt index d3e09a5f52..aec86d12d2 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt @@ -50,6 +50,12 @@ interface TangemTechApi { @Body userTokens: UserTokensResponse, ): ApiResponse + @PUT("/v1/wallets/{walletId}/tokens") + suspend fun saveTokens( + @Path(value = "walletId") userId: String, + @Body userTokens: UserTokensResponse, + ): ApiResponse + /** Returns referral status by [walletId] */ @GET("v1/referral/{walletId}") suspend fun getReferralStatus(@Path("walletId") walletId: String): ApiResponse @@ -129,6 +135,12 @@ interface TangemTechApi { @Body body: List, ): ApiResponse + @PUT("/v1/user-wallets/applications/{application_id}/wallets") + suspend fun associateApplicationIdWithWalletsV2( + @Path("application_id") applicationId: String, + @Body body: AssociateApplicationIdWithWalletsBody, + ): ApiResponse + @GET("v1/user-wallets/wallets/{wallet_id}") suspend fun getWalletById(@Path("wallet_id") walletId: String): ApiResponse diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/AssociateAppWithWalletsErrorResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/AssociateAppWithWalletsErrorResponse.kt new file mode 100644 index 0000000000..14696568f4 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/AssociateAppWithWalletsErrorResponse.kt @@ -0,0 +1,9 @@ +package com.tangem.datasource.api.tangemTech.models + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class AssociateAppWithWalletsErrorResponse( + @Json(name = "missingWalletIds") val missingWalletIds: List, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/AssociateApplicationIdWithWalletsBody.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/AssociateApplicationIdWithWalletsBody.kt new file mode 100644 index 0000000000..59dfa30b85 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/AssociateApplicationIdWithWalletsBody.kt @@ -0,0 +1,9 @@ +package com.tangem.datasource.api.tangemTech.models + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class AssociateApplicationIdWithWalletsBody( + @Json(name = "walletIds") val walletIds: List, +) \ No newline at end of file diff --git a/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensSaver.kt b/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensSaver.kt index 61f43d9ad7..2b4e65a095 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensSaver.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensSaver.kt @@ -2,12 +2,17 @@ package com.tangem.data.common.currency import com.tangem.data.common.api.safeApiCall import com.tangem.data.common.tokens.UserTokensBackwardCompatibility +import com.tangem.datasource.api.common.response.ApiResponse +import com.tangem.datasource.api.common.response.ApiResponseError +import com.tangem.datasource.api.common.response.isNetworkError import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.datasource.api.tangemTech.converters.WalletIdBodyConverter import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.api.tangemTech.models.WalletType import com.tangem.datasource.local.token.UserTokensResponseStore import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.retryer.Retryer @@ -50,24 +55,26 @@ class UserTokensSaver( response: UserTokensResponse, useEnricher: Boolean = true, onFailSend: () -> Unit = {}, - ) { - withContext(dispatchers.default) { - val userWallet = userWalletsStore.getSyncOrNull(key = userWalletId) + ) = withContext(dispatchers.io) { + val userWallet = userWalletsStore.getSyncOrNull(key = userWalletId) + if (userWallet == null) { + Timber.e("UserWallet with id $userWalletId not found. Cannot push tokens.") + onFailSend() + return@withContext + } + + if (accountsFeatureToggles.isFeatureEnabled) { + val enrichedResponse = response.enrichIf(userWalletId = userWalletId, condition = useEnricher) + + pushNew(userWallet = userWallet, response = enrichedResponse, onFailSend = onFailSend) + } else { val enrichedResponse = response.enrichIf(userWalletId = userWalletId, condition = useEnricher).copy( - walletName = userWallet?.name, + walletName = userWallet.name, walletType = WalletType.from(userWallet), ) - safeApiCall( - call = { - withContext(dispatchers.io) { - tangemTechApi.saveUserTokens(userId = userWalletId.stringValue, userTokens = enrichedResponse) - .bind() - } - }, - onError = { onFailSend() }, - ) + pushLegacy(userWalletId = userWalletId, response = enrichedResponse, onFailSend = onFailSend) } } @@ -88,6 +95,39 @@ class UserTokensSaver( ) } + private suspend fun pushLegacy(userWalletId: UserWalletId, response: UserTokensResponse, onFailSend: () -> Unit) { + safeApiCall( + call = { tangemTechApi.saveUserTokens(userId = userWalletId.stringValue, userTokens = response).bind() }, + onError = { onFailSend() }, + ) + } + + private suspend fun pushNew(userWallet: UserWallet, response: UserTokensResponse, onFailSend: () -> Unit) { + safeApiCall( + call = { + val apiResponse = tangemTechApi.saveTokens( + userId = userWallet.walletId.stringValue, + userTokens = response, + ) + + val isWalletNotFound = apiResponse is ApiResponse.Error && + apiResponse.cause.isNetworkError(ApiResponseError.HttpException.Code.NOT_FOUND) + + if (isWalletNotFound) { + tangemTechApi.createWallet(body = WalletIdBodyConverter.convert(userWallet)).bind() + + tangemTechApi.saveTokens( + userId = userWallet.walletId.stringValue, + userTokens = response, + ).bind() + } else { + apiResponse.bind() + } + }, + onError = { onFailSend() }, + ) + } + private fun UserTokensResponse.applyCompatibility(): UserTokensResponse { return userTokensBackwardCompatibility.applyCompatibilityAndGetUpdated(userTokensResponse = this) } diff --git a/data/common/src/test/kotlin/com/tangem/data/common/currency/UserTokensSaverTest.kt b/data/common/src/test/kotlin/com/tangem/data/common/currency/UserTokensSaverTest.kt index 24c2d207e7..bc51068bff 100644 --- a/data/common/src/test/kotlin/com/tangem/data/common/currency/UserTokensSaverTest.kt +++ b/data/common/src/test/kotlin/com/tangem/data/common/currency/UserTokensSaverTest.kt @@ -73,7 +73,7 @@ class UserTokensSaverTest { } coVerify(inverse = true) { - tangemTechApi.saveUserTokens(any(), any()) + tangemTechApi.saveTokens(any(), any()) } } @@ -108,7 +108,8 @@ class UserTokensSaverTest { coEvery { userWalletsStore.getSyncOrNull(userWalletId) } returns userWallet coEvery { enricher(userWalletId, response) } returns enrichedResponse - coEvery { tangemTechApi.saveUserTokens(any(), any()) } returns ApiResponse.Error(error) as ApiResponse + coEvery { tangemTechApi.saveTokens(any(), any()) } returns ApiResponse.Error(error) as ApiResponse + coEvery { tangemTechApi.createWallet(body = any()) } returns ApiResponse.Error(error) as ApiResponse // WHEN userTokensSaver.push( @@ -120,7 +121,7 @@ class UserTokensSaverTest { // THEN coVerifyOrder { enricher(userWalletId, response) - tangemTechApi.saveUserTokens(userWalletId.stringValue, enrichedResponse) + tangemTechApi.saveTokens(userWalletId.stringValue, enrichedResponse) } assert(onFailSendCalled) { "onFailSend callback should be called when API call fails" } @@ -155,7 +156,7 @@ class UserTokensSaverTest { coEvery { userWalletsStore.getSyncOrNull(userWalletId) } returns userWallet coEvery { enricher(userWalletId, response) } returns enrichedResponse coEvery { - tangemTechApi.saveUserTokens(userWalletId.stringValue, enrichedResponse) + tangemTechApi.saveTokens(userWalletId.stringValue, enrichedResponse) } returns ApiResponse.Success(Unit) // WHEN @@ -164,7 +165,7 @@ class UserTokensSaverTest { // THEN coVerifyOrder { enricher(userWalletId, response) - tangemTechApi.saveUserTokens(userWalletId.stringValue, enrichedResponse) + tangemTechApi.saveTokens(userWalletId.stringValue, enrichedResponse) } } } \ No newline at end of file diff --git a/data/wallets/build.gradle.kts b/data/wallets/build.gradle.kts index f7867f6b5d..5949dd0174 100644 --- a/data/wallets/build.gradle.kts +++ b/data/wallets/build.gradle.kts @@ -25,13 +25,12 @@ dependencies { implementation(projects.core.utils) /** Domain */ - implementation(projects.domain.wallets) + implementation(projects.domain.account) implementation(projects.domain.card) - api(projects.domain.models) - - /** Domain models */ - implementation(projects.domain.wallets.models) + implementation(projects.domain.models) implementation(projects.domain.tokens.models) + implementation(projects.domain.wallets) + implementation(projects.domain.wallets.models) /** DI */ implementation(deps.hilt.android) @@ -41,15 +40,15 @@ dependencies { implementation(deps.androidx.datastore) implementation(deps.arrow.core) implementation(deps.kotlin.coroutines) + implementation(deps.moshi) + implementation(deps.moshi.kotlin) + implementation(deps.retrofit) implementation(deps.timber) /** tests */ - testImplementation(projects.domain.models) testImplementation(projects.common.test) testImplementation(deps.test.junit) testImplementation(deps.test.coroutine) testImplementation(deps.test.truth) testImplementation(deps.test.mockk) - testImplementation(deps.moshi) - testImplementation(deps.moshi.kotlin) } \ No newline at end of file diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt b/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt index 982812c9b8..797d06f372 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt @@ -5,22 +5,22 @@ import arrow.core.left import arrow.core.right import com.tangem.data.wallets.converters.UserWalletRemoteInfoConverter import com.tangem.datasource.api.common.AuthProvider +import com.tangem.datasource.api.common.response.ApiResponse import com.tangem.datasource.api.common.response.ApiResponseError.HttpException import com.tangem.datasource.api.common.response.fold import com.tangem.datasource.api.common.response.getOrThrow +import com.tangem.datasource.api.common.response.isNetworkError import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.converters.WalletIdBodyConverter -import com.tangem.datasource.api.tangemTech.models.PromocodeActivationBody -import com.tangem.datasource.api.tangemTech.models.SeedPhraseNotificationDTO +import com.tangem.datasource.api.tangemTech.models.* import com.tangem.datasource.api.tangemTech.models.SeedPhraseNotificationDTO.Status -import com.tangem.datasource.api.tangemTech.models.WalletBody -import com.tangem.datasource.api.tangemTech.models.WalletType import com.tangem.datasource.local.datastore.RuntimeStateStore import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.PreferencesKeys.SEED_FIRST_NOTIFICATION_SHOW_TIME import com.tangem.datasource.local.preferences.utils.* import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.wallets.models.SeedPhraseNotificationsStatus @@ -30,13 +30,15 @@ import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.utils.WEEK_MILLIS import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.runCatching +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import kotlinx.coroutines.withContext typealias SeedPhraseNotificationsStatuses = Map -@Suppress("TooManyFunctions", "LargeClass") +@Suppress("TooManyFunctions", "LargeClass", "LongParameterList") internal class DefaultWalletsRepository( private val appPreferencesStore: AppPreferencesStore, private val tangemTechApi: TangemTechApi, @@ -44,6 +46,8 @@ internal class DefaultWalletsRepository( private val seedPhraseNotificationVisibilityStore: RuntimeStateStore, private val dispatchers: CoroutineDispatcherProvider, private val authProvider: AuthProvider, + private val accountsFeatureToggles: AccountsFeatureToggles, + private val moshi: com.squareup.moshi.Moshi, ) : WalletsRepository { private val upgradeWalletNotificationDisabled: MutableStateFlow> = @@ -388,24 +392,58 @@ internal class DefaultWalletsRepository( override suspend fun associateWallets(applicationId: String, wallets: List) = withContext(dispatchers.io) { - val publicKeys = authProvider.getCardsPublicKeys() - val walletsBody = wallets.map { userWallet -> - WalletIdBodyConverter.convert( - userWallet = userWallet, - publicKeys = if (userWallet is UserWallet.Cold) { - publicKeys.filterKeys { - userWallet.cardsInWallet.contains(it) - } - } else { - emptyMap() - }, - ) - } + if (accountsFeatureToggles.isFeatureEnabled) { + val associateApplicationIdWithWallets: suspend () -> ApiResponse = { + tangemTechApi.associateApplicationIdWithWalletsV2( + applicationId = applicationId, + body = AssociateApplicationIdWithWalletsBody( + walletIds = wallets.map { it.walletId.stringValue }.distinct(), + ), + ) + } - tangemTechApi.associateApplicationIdWithWallets( - applicationId = applicationId, - body = walletsBody, - ).getOrThrow() + val apiResponse = associateApplicationIdWithWallets() + + if (apiResponse is ApiResponse.Success) return@withContext + + if (apiResponse is ApiResponse.Error && + apiResponse.cause.isNetworkError(HttpException.Code.BAD_REQUEST) + ) { + val errorBody = (apiResponse.cause as? HttpException)?.errorBody + ?: error("Bad Request must have error body") + + val adapter = moshi.adapter(AssociateAppWithWalletsErrorResponse::class.java) + val errorResponse = adapter.fromJson(errorBody) + ?: error("Cannot parse error body: $errorBody") + + errorResponse.missingWalletIds + .map { + async { createWallet(userWalletId = UserWalletId(it)) } + } + .awaitAll() + + associateApplicationIdWithWallets().getOrThrow() + } + } else { + val publicKeys = authProvider.getCardsPublicKeys() + val walletsBody = wallets.map { userWallet -> + WalletIdBodyConverter.convert( + userWallet = userWallet, + publicKeys = if (userWallet is UserWallet.Cold) { + publicKeys.filterKeys { + userWallet.cardsInWallet.contains(it) + } + } else { + emptyMap() + }, + ) + } + + tangemTechApi.associateApplicationIdWithWallets( + applicationId = applicationId, + body = walletsBody, + ).getOrThrow() + } } override suspend fun activatePromoCode( diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/di/WalletsDataModule.kt b/data/wallets/src/main/java/com/tangem/data/wallets/di/WalletsDataModule.kt index bcaff10b6f..a1e870a3e1 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/di/WalletsDataModule.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/di/WalletsDataModule.kt @@ -1,5 +1,6 @@ package com.tangem.data.wallets.di +import com.squareup.moshi.Moshi import com.tangem.data.wallets.DefaultWalletNamesMigrationRepository import com.tangem.data.wallets.DefaultWalletsRepository import com.tangem.data.wallets.cold.DefaultColdMapDerivationsRepository @@ -8,9 +9,11 @@ import com.tangem.data.wallets.hot.DefaultHotMapDerivationsRepository import com.tangem.data.wallets.hot.DefaultHotWalletAccessCodeAttemptsRepository import com.tangem.datasource.api.common.AuthProvider import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.datasource.di.NetworkMoshi import com.tangem.datasource.local.datastore.RuntimeStateStore import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.wallets.derivations.ColdMapDerivationsRepository import com.tangem.domain.wallets.derivations.DerivationsRepository import com.tangem.domain.wallets.derivations.HotMapDerivationsRepository @@ -37,6 +40,8 @@ internal object WalletsDataModule { userWalletsStore: UserWalletsStore, dispatchers: CoroutineDispatcherProvider, authProvider: AuthProvider, + accountsFeatureToggles: AccountsFeatureToggles, + @NetworkMoshi moshi: Moshi, ): WalletsRepository { return DefaultWalletsRepository( appPreferencesStore = appPreferencesStore, @@ -45,6 +50,8 @@ internal object WalletsDataModule { seedPhraseNotificationVisibilityStore = RuntimeStateStore(defaultValue = emptyMap()), dispatchers = dispatchers, authProvider = authProvider, + accountsFeatureToggles = accountsFeatureToggles, + moshi = moshi, ) } diff --git a/data/wallets/src/test/java/com/tangem/data/wallets/DefaultWalletsRepositoryTest.kt b/data/wallets/src/test/java/com/tangem/data/wallets/DefaultWalletsRepositoryTest.kt index e2a9090de4..80b3e8d659 100644 --- a/data/wallets/src/test/java/com/tangem/data/wallets/DefaultWalletsRepositoryTest.kt +++ b/data/wallets/src/test/java/com/tangem/data/wallets/DefaultWalletsRepositoryTest.kt @@ -13,6 +13,7 @@ import com.tangem.datasource.api.tangemTech.models.PromocodeActivationResponse import com.tangem.datasource.api.tangemTech.models.WalletResponse import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys +import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.wallets.models.errors.ActivatePromoCodeError @@ -51,6 +52,8 @@ class DefaultWalletsRepositoryTest { seedPhraseNotificationVisibilityStore = mockk(), dispatchers = dispatchers, authProvider = mockk(), + accountsFeatureToggles = mockk(), + moshi = mockk(), ) } @@ -204,6 +207,10 @@ class DefaultWalletsRepositoryTest { coEvery { getCardsPublicKeys() } returns publicKeys } + val accountsFeatureToggles = mockk { + every { isFeatureEnabled } returns false + } + repository = DefaultWalletsRepository( appPreferencesStore = appPreferenceStore, tangemTechApi = tangemTechApi, @@ -211,6 +218,8 @@ class DefaultWalletsRepositoryTest { seedPhraseNotificationVisibilityStore = mockk(), dispatchers = dispatchers, authProvider = authProvider, + accountsFeatureToggles = accountsFeatureToggles, + moshi = mockk(), ) coEvery {