Updated on 2026-08-14

This commit is contained in:
Tangem 2026-01-15 11:56:24 +03:00
parent d77875be51
commit e45d91b15e
15 changed files with 389 additions and 67 deletions

View file

@ -4,10 +4,15 @@ import com.tangem.datasource.api.tangemTech.models.CardInfoBody
import com.tangem.datasource.api.tangemTech.models.WalletIdBody import com.tangem.datasource.api.tangemTech.models.WalletIdBody
import com.tangem.datasource.api.tangemTech.models.WalletType import com.tangem.datasource.api.tangemTech.models.WalletType
import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.wallets.models.AppsFlyerConversionData
object WalletIdBodyConverter { object WalletIdBodyConverter {
fun convert(userWallet: UserWallet, publicKeys: Map<String, String>? = null): WalletIdBody { fun convert(
userWallet: UserWallet,
conversionData: AppsFlyerConversionData? = null,
publicKeys: Map<String, String>? = null,
): WalletIdBody {
return WalletIdBody( return WalletIdBody(
walletId = userWallet.walletId.stringValue, walletId = userWallet.walletId.stringValue,
name = userWallet.name, name = userWallet.name,

View file

@ -17,6 +17,8 @@ data class UserTokensResponse(
@Json(name = "notifyStatus") val notifyStatus: Boolean? = null, @Json(name = "notifyStatus") val notifyStatus: Boolean? = null,
@Json(name = "name") val walletName: String? = null, @Json(name = "name") val walletName: String? = null,
@Json(name = "type") val walletType: WalletType? = null, @Json(name = "type") val walletType: WalletType? = null,
@Json(name = "ref") val refcode: String? = null,
@Json(name = "campaign") val campaign: String? = null,
) { ) {
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)

View file

@ -9,4 +9,6 @@ data class WalletIdBody(
@Json(name = "name") val name: String, @Json(name = "name") val name: String,
@Json(name = "type") val walletType: WalletType? = null, @Json(name = "type") val walletType: WalletType? = null,
@Json(name = "cards") val cards: List<CardInfoBody>? = null, @Json(name = "cards") val cards: List<CardInfoBody>? = null,
@Json(name = "ref") val refcode: String? = null,
@Json(name = "campaign") val campaign: String? = null,
) )

View file

@ -30,7 +30,11 @@ class WalletIdBodyConverterTest {
) )
// WHEN // WHEN
val result = WalletIdBodyConverter.convert(userWallet, publicKeys) val result = WalletIdBodyConverter.convert(
userWallet = userWallet,
conversionData = null,
publicKeys = publicKeys,
)
// THEN // THEN
assertThat(result).isEqualTo( assertThat(result).isEqualTo(
@ -68,7 +72,11 @@ class WalletIdBodyConverterTest {
val publicKeys = emptyMap<String, String>() val publicKeys = emptyMap<String, String>()
// WHEN // WHEN
val result = WalletIdBodyConverter.convert(userWallet, publicKeys) val result = WalletIdBodyConverter.convert(
userWallet = userWallet,
conversionData = null,
publicKeys = publicKeys,
)
// THEN // THEN
assertThat(result).isEqualTo( assertThat(result).isEqualTo(

View file

@ -4,22 +4,18 @@ import com.tangem.data.account.fetcher.DefaultWalletAccountsFetcher.FetchResult
import com.tangem.data.account.utils.DefaultWalletAccountsResponseFactory import com.tangem.data.account.utils.DefaultWalletAccountsResponseFactory
import com.tangem.data.common.currency.UserTokensResponseAccountIdEnricher import com.tangem.data.common.currency.UserTokensResponseAccountIdEnricher
import com.tangem.data.common.currency.UserTokensSaver import com.tangem.data.common.currency.UserTokensSaver
import com.tangem.data.common.wallet.WalletServerBinder
import com.tangem.datasource.api.common.response.ApiResponse import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.common.response.ApiResponseError import com.tangem.datasource.api.common.response.ApiResponseError
import com.tangem.datasource.api.common.response.ApiResponseError.HttpException.Code import com.tangem.datasource.api.common.response.ApiResponseError.HttpException.Code
import com.tangem.datasource.api.common.response.ETAG_HEADER import com.tangem.datasource.api.common.response.ETAG_HEADER
import com.tangem.datasource.api.common.response.isNetworkError 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.UserTokensResponse
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO
import com.tangem.datasource.api.tangemTech.models.account.toUserTokensResponse import com.tangem.datasource.api.tangemTech.models.account.toUserTokensResponse
import com.tangem.datasource.local.token.UserTokensResponseStore import com.tangem.datasource.local.token.UserTokensResponseStore
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -27,26 +23,21 @@ import javax.inject.Singleton
/** /**
* Handles errors that occur during the fetching of wallet accounts * Handles errors that occur during the fetching of wallet accounts
* *
* @property tangemTechApi API for network requests
* @property userWalletsStore provides access to user wallets storage
* @property userTokensSaver saves user tokens to the storage * @property userTokensSaver saves user tokens to the storage
* @property userTokensResponseStore provides access to user token responses. * @property userTokensResponseStore provides access to user token responses.
* @property defaultWalletAccountsResponseFactory creates [GetWalletAccountsResponse] from [UserTokensResponse] * @property defaultWalletAccountsResponseFactory creates [GetWalletAccountsResponse] from [UserTokensResponse]
* @property dispatchers dispatchers * @property walletServerBinder binds user wallets on the server
* *
* @see DefaultWalletAccountsFetcher * @see DefaultWalletAccountsFetcher
* *
[REDACTED_AUTHOR] [REDACTED_AUTHOR]
*/ */
@Suppress("LongParameterList")
@Singleton @Singleton
internal class FetchWalletAccountsErrorHandler @Inject constructor( internal class FetchWalletAccountsErrorHandler @Inject constructor(
private val tangemTechApi: TangemTechApi,
private val userWalletsStore: UserWalletsStore,
private val userTokensSaver: UserTokensSaver, private val userTokensSaver: UserTokensSaver,
private val userTokensResponseStore: UserTokensResponseStore, private val userTokensResponseStore: UserTokensResponseStore,
private val defaultWalletAccountsResponseFactory: DefaultWalletAccountsResponseFactory, private val defaultWalletAccountsResponseFactory: DefaultWalletAccountsResponseFactory,
private val dispatchers: CoroutineDispatcherProvider, private val walletServerBinder: WalletServerBinder,
) { ) {
/** /**
@ -119,13 +110,7 @@ internal class FetchWalletAccountsErrorHandler @Inject constructor(
*/ */
private suspend fun createWallet(userWalletId: UserWalletId): String? { private suspend fun createWallet(userWalletId: UserWalletId): String? {
val userWallet = userWalletsStore.getSyncOrNull(key = userWalletId) ?: return null val creationResponse = walletServerBinder.bind(userWalletId)
val creationResponse = withContext(dispatchers.io) {
tangemTechApi.createWallet(
body = WalletIdBodyConverter.convert(userWallet),
)
}
return if (creationResponse is ApiResponse.Success && creationResponse.code == Code.CREATED) { return if (creationResponse is ApiResponse.Success && creationResponse.code == Code.CREATED) {
creationResponse.headers[ETAG_HEADER]?.firstOrNull() creationResponse.headers[ETAG_HEADER]?.firstOrNull()

View file

@ -4,23 +4,22 @@ import com.tangem.data.account.converter.createGetWalletAccountsResponse
import com.tangem.data.account.converter.createWalletAccountDTO import com.tangem.data.account.converter.createWalletAccountDTO
import com.tangem.data.account.utils.DefaultWalletAccountsResponseFactory import com.tangem.data.account.utils.DefaultWalletAccountsResponseFactory
import com.tangem.data.common.currency.UserTokensSaver import com.tangem.data.common.currency.UserTokensSaver
import com.tangem.data.common.wallet.WalletServerBinder
import com.tangem.datasource.api.common.response.ApiResponse import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.common.response.ApiResponseError import com.tangem.datasource.api.common.response.ApiResponseError
import com.tangem.datasource.api.common.response.ApiResponseError.HttpException.Code import com.tangem.datasource.api.common.response.ApiResponseError.HttpException.Code
import com.tangem.datasource.api.common.response.ETAG_HEADER import com.tangem.datasource.api.common.response.ETAG_HEADER
import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.api.tangemTech.models.WalletIdBody
import com.tangem.datasource.api.tangemTech.models.WalletType
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO
import com.tangem.datasource.api.tangemTech.models.account.toUserTokensResponse import com.tangem.datasource.api.tangemTech.models.account.toUserTokensResponse
import com.tangem.datasource.local.token.UserTokensResponseStore import com.tangem.datasource.local.token.UserTokensResponseStore
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import io.mockk.clearMocks
import io.mockk.* import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.mockk
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
@ -33,18 +32,16 @@ import org.junit.jupiter.api.TestInstance
class FetchWalletAccountsErrorHandlerTest { class FetchWalletAccountsErrorHandlerTest {
private val tangemTechApi: TangemTechApi = mockk() private val tangemTechApi: TangemTechApi = mockk()
private val userWalletsStore: UserWalletsStore = mockk() private val walletServerBinder: WalletServerBinder = mockk()
private val userTokensSaver: UserTokensSaver = mockk(relaxUnitFun = true) private val userTokensSaver: UserTokensSaver = mockk(relaxUnitFun = true)
private val userTokensResponseStore: UserTokensResponseStore = mockk(relaxUnitFun = true) private val userTokensResponseStore: UserTokensResponseStore = mockk(relaxUnitFun = true)
private val defaultWalletAccountsResponseFactory: DefaultWalletAccountsResponseFactory = mockk() private val defaultWalletAccountsResponseFactory: DefaultWalletAccountsResponseFactory = mockk()
private val handler = FetchWalletAccountsErrorHandler( private val handler = FetchWalletAccountsErrorHandler(
tangemTechApi = tangemTechApi, walletServerBinder = walletServerBinder,
userWalletsStore = userWalletsStore,
userTokensSaver = userTokensSaver, userTokensSaver = userTokensSaver,
userTokensResponseStore = userTokensResponseStore, userTokensResponseStore = userTokensResponseStore,
defaultWalletAccountsResponseFactory = defaultWalletAccountsResponseFactory, defaultWalletAccountsResponseFactory = defaultWalletAccountsResponseFactory,
dispatchers = TestingCoroutineDispatcherProvider(),
) )
private val pushWalletAccounts: suspend (List<WalletAccountDTO>, String) -> GetWalletAccountsResponse = private val pushWalletAccounts: suspend (List<WalletAccountDTO>, String) -> GetWalletAccountsResponse =
@ -55,7 +52,7 @@ class FetchWalletAccountsErrorHandlerTest {
fun setupEach() { fun setupEach() {
clearMocks( clearMocks(
tangemTechApi, tangemTechApi,
userWalletsStore, walletServerBinder,
userTokensSaver, userTokensSaver,
userTokensResponseStore, userTokensResponseStore,
defaultWalletAccountsResponseFactory, defaultWalletAccountsResponseFactory,
@ -112,22 +109,7 @@ class FetchWalletAccountsErrorHandlerTest {
code = Code.CREATED, code = Code.CREATED,
) )
val walletName = "Wallet" coEvery { walletServerBinder.bind(userWalletId) } returns apiResponse
val userWallet = mockk<UserWallet.Cold> {
every { this@mockk.walletId } returns userWalletId
every { this@mockk.name } returns walletName
}
coEvery { userWalletsStore.getSyncOrNull(userWalletId) } returns userWallet
coEvery {
tangemTechApi.createWallet(
WalletIdBody(
walletId = userWalletId.stringValue,
name = walletName,
walletType = WalletType.COLD,
),
)
} returns apiResponse
coEvery { pushWalletAccounts(listOf(accountDTO), eTagValue) } returns savedAccountsResponse coEvery { pushWalletAccounts(listOf(accountDTO), eTagValue) } returns savedAccountsResponse
// Act // Act
@ -142,13 +124,7 @@ class FetchWalletAccountsErrorHandlerTest {
// Assert // Assert
coVerify { coVerify {
userTokensSaver.pushWithRetryer(userWalletId, response = savedAccountsResponse.toUserTokensResponse()) userTokensSaver.pushWithRetryer(userWalletId, response = savedAccountsResponse.toUserTokensResponse())
tangemTechApi.createWallet( walletServerBinder.bind(userWalletId)
WalletIdBody(
walletId = userWalletId.stringValue,
name = walletName,
walletType = WalletType.COLD,
),
)
pushWalletAccounts(listOf(accountDTO), eTagValue) pushWalletAccounts(listOf(accountDTO), eTagValue)
storeWalletAccounts(userWalletId, savedAccountsResponse) storeWalletAccounts(userWalletId, savedAccountsResponse)
} }

View file

@ -2,13 +2,14 @@ package com.tangem.data.common.currency
import com.tangem.data.common.api.safeApiCall import com.tangem.data.common.api.safeApiCall
import com.tangem.data.common.tokens.UserTokensBackwardCompatibility import com.tangem.data.common.tokens.UserTokensBackwardCompatibility
import com.tangem.data.common.wallet.WalletServerBinder
import com.tangem.datasource.api.common.response.ApiResponse import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.common.response.ApiResponseError import com.tangem.datasource.api.common.response.ApiResponseError
import com.tangem.datasource.api.common.response.isNetworkError import com.tangem.datasource.api.common.response.isNetworkError
import com.tangem.datasource.api.tangemTech.TangemTechApi 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.UserTokensResponse
import com.tangem.datasource.api.tangemTech.models.WalletType import com.tangem.datasource.api.tangemTech.models.WalletType
import com.tangem.datasource.local.appsflyer.AppsFlyerConversionStore
import com.tangem.datasource.local.token.UserTokensResponseStore import com.tangem.datasource.local.token.UserTokensResponseStore
import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
@ -27,6 +28,8 @@ class UserTokensSaver(
private val userTokensResponseStore: UserTokensResponseStore, private val userTokensResponseStore: UserTokensResponseStore,
private val dispatchers: CoroutineDispatcherProvider, private val dispatchers: CoroutineDispatcherProvider,
private val addressesEnricher: UserTokensResponseAddressesEnricher, private val addressesEnricher: UserTokensResponseAddressesEnricher,
private val walletServerBinder: WalletServerBinder,
private val appsFlyerConversionStore: AppsFlyerConversionStore,
private val accountsFeatureToggles: AccountsFeatureToggles, private val accountsFeatureToggles: AccountsFeatureToggles,
private val pushTokensRetryerPool: RetryerPool, private val pushTokensRetryerPool: RetryerPool,
) { ) {
@ -69,6 +72,8 @@ class UserTokensSaver(
pushNew(userWallet = userWallet, response = enrichedResponse, onFailSend = onFailSend) pushNew(userWallet = userWallet, response = enrichedResponse, onFailSend = onFailSend)
} else { } else {
val conversionData = appsFlyerConversionStore.get()
val enrichedResponse = response.enrichIf(userWalletId = userWalletId, condition = useEnricher).copy( val enrichedResponse = response.enrichIf(userWalletId = userWalletId, condition = useEnricher).copy(
walletName = userWallet.name, walletName = userWallet.name,
walletType = WalletType.from(userWallet), walletType = WalletType.from(userWallet),
@ -116,7 +121,7 @@ class UserTokensSaver(
apiResponse.cause.isNetworkError(ApiResponseError.HttpException.Code.NOT_FOUND) apiResponse.cause.isNetworkError(ApiResponseError.HttpException.Code.NOT_FOUND)
if (isWalletNotFound) { if (isWalletNotFound) {
tangemTechApi.createWallet(body = WalletIdBodyConverter.convert(userWallet)).bind() walletServerBinder.bind(userWallet).bind()
tangemTechApi.saveTokens( tangemTechApi.saveTokens(
userId = userWallet.walletId.stringValue, userId = userWallet.walletId.stringValue,

View file

@ -7,7 +7,10 @@ import com.tangem.data.common.cache.etag.ETagsStore
import com.tangem.data.common.currency.* import com.tangem.data.common.currency.*
import com.tangem.data.common.quote.DefaultQuotesFetcher import com.tangem.data.common.quote.DefaultQuotesFetcher
import com.tangem.data.common.quote.QuotesFetcher import com.tangem.data.common.quote.QuotesFetcher
import com.tangem.data.common.wallet.DefaultWalletServerBinder
import com.tangem.data.common.wallet.WalletServerBinder
import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.local.appsflyer.AppsFlyerConversionStore
import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.token.UserTokensResponseStore import com.tangem.datasource.local.token.UserTokensResponseStore
import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.datasource.local.userwallet.UserWalletsStore
@ -72,6 +75,8 @@ internal object DataCommonModule {
userTokensResponseStore: UserTokensResponseStore, userTokensResponseStore: UserTokensResponseStore,
dispatchers: CoroutineDispatcherProvider, dispatchers: CoroutineDispatcherProvider,
addressesEnricher: UserTokensResponseAddressesEnricher, addressesEnricher: UserTokensResponseAddressesEnricher,
walletServerBinder: WalletServerBinder,
appsFlyerConversionStore: AppsFlyerConversionStore,
accountsFeatureToggles: AccountsFeatureToggles, accountsFeatureToggles: AccountsFeatureToggles,
): UserTokensSaver { ): UserTokensSaver {
return UserTokensSaver( return UserTokensSaver(
@ -84,6 +89,8 @@ internal object DataCommonModule {
pushTokensRetryerPool = RetryerPool( pushTokensRetryerPool = RetryerPool(
coroutineScope = CoroutineScope(SupervisorJob() + dispatchers.default), coroutineScope = CoroutineScope(SupervisorJob() + dispatchers.default),
), ),
walletServerBinder = walletServerBinder,
appsFlyerConversionStore = appsFlyerConversionStore,
) )
} }
@ -98,4 +105,20 @@ internal object DataCommonModule {
fun provideETagsStore(appPreferencesStore: AppPreferencesStore): ETagsStore { fun provideETagsStore(appPreferencesStore: AppPreferencesStore): ETagsStore {
return DefaultETagsStore(appPreferencesStore = appPreferencesStore) return DefaultETagsStore(appPreferencesStore = appPreferencesStore)
} }
@Provides
@Singleton
fun provideWalletServerBinder(
userWalletsStore: UserWalletsStore,
appsFlyerConversionStore: AppsFlyerConversionStore,
tangemTechApi: TangemTechApi,
dispatchers: CoroutineDispatcherProvider,
): WalletServerBinder {
return DefaultWalletServerBinder(
userWalletsStore = userWalletsStore,
appsFlyerConversionStore = appsFlyerConversionStore,
tangemTechApi = tangemTechApi,
dispatchers = dispatchers,
)
}
} }

View file

@ -0,0 +1,35 @@
package com.tangem.data.common.wallet
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.converters.WalletIdBodyConverter
import com.tangem.datasource.local.appsflyer.AppsFlyerConversionStore
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext
internal class DefaultWalletServerBinder(
private val userWalletsStore: UserWalletsStore,
private val appsFlyerConversionStore: AppsFlyerConversionStore,
private val tangemTechApi: TangemTechApi,
private val dispatchers: CoroutineDispatcherProvider,
) : WalletServerBinder {
override suspend fun bind(userWalletId: UserWalletId): ApiResponse<Unit>? {
val userWallet = userWalletsStore.getSyncOrNull(key = userWalletId) ?: return null
return bind(userWallet)
}
override suspend fun bind(userWallet: UserWallet): ApiResponse<Unit> {
val conversionData = appsFlyerConversionStore.get()
return withContext(dispatchers.io) {
tangemTechApi.createWallet(
body = WalletIdBodyConverter.convert(userWallet, conversionData),
)
}
}
}

View file

@ -0,0 +1,26 @@
package com.tangem.data.common.wallet
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
/**
*
[REDACTED_AUTHOR]
*/
interface WalletServerBinder {
/**
* Binds a wallet to the server using its [userWalletId].
* Returns an [ApiResponse] indicating the result of the binding operation,
* or null if the binding was not performed.
*/
suspend fun bind(userWalletId: UserWalletId): ApiResponse<Unit>?
/**
* Binds a wallet to the server using the provided [userWallet].
* Returns an [ApiResponse] indicating the result of the binding operation.
*/
suspend fun bind(userWallet: UserWallet): ApiResponse<Unit>
}

View file

@ -1,10 +1,12 @@
package com.tangem.data.common.currency package com.tangem.data.common.currency
import com.tangem.data.common.wallet.WalletServerBinder
import com.tangem.datasource.api.common.response.ApiResponse import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.common.response.ApiResponseError import com.tangem.datasource.api.common.response.ApiResponseError
import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.api.tangemTech.models.WalletType import com.tangem.datasource.api.tangemTech.models.WalletType
import com.tangem.datasource.local.appsflyer.AppsFlyerConversionStore
import com.tangem.datasource.local.token.UserTokensResponseStore import com.tangem.datasource.local.token.UserTokensResponseStore
import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
@ -27,6 +29,8 @@ class UserTokensSaverTest {
private val accountsFeatureToggles = mockk<AccountsFeatureToggles> { private val accountsFeatureToggles = mockk<AccountsFeatureToggles> {
every { this@mockk.isFeatureEnabled } returns true every { this@mockk.isFeatureEnabled } returns true
} }
private val walletServerBinder: WalletServerBinder = mockk()
private val appsFlyerConversionStore: AppsFlyerConversionStore = mockk()
private val userTokensSaver: UserTokensSaver = UserTokensSaver( private val userTokensSaver: UserTokensSaver = UserTokensSaver(
tangemTechApi = tangemTechApi, tangemTechApi = tangemTechApi,
@ -34,13 +38,21 @@ class UserTokensSaverTest {
userTokensResponseStore = userTokensResponseStore, userTokensResponseStore = userTokensResponseStore,
dispatchers = TestingCoroutineDispatcherProvider(), dispatchers = TestingCoroutineDispatcherProvider(),
addressesEnricher = enricher, addressesEnricher = enricher,
walletServerBinder = walletServerBinder,
appsFlyerConversionStore = appsFlyerConversionStore,
accountsFeatureToggles = accountsFeatureToggles, accountsFeatureToggles = accountsFeatureToggles,
pushTokensRetryerPool = mockk(), pushTokensRetryerPool = mockk(),
) )
@BeforeEach @BeforeEach
fun resetMocks() { fun resetMocks() {
clearMocks(tangemTechApi, userWalletsStore, userTokensResponseStore, enricher) clearMocks(
tangemTechApi,
userWalletsStore,
userTokensResponseStore,
enricher,
walletServerBinder,
)
} }
@Test @Test
@ -109,7 +121,6 @@ class UserTokensSaverTest {
coEvery { userWalletsStore.getSyncOrNull(userWalletId) } returns userWallet coEvery { userWalletsStore.getSyncOrNull(userWalletId) } returns userWallet
coEvery { enricher(userWalletId, response) } returns enrichedResponse coEvery { enricher(userWalletId, response) } returns enrichedResponse
coEvery { tangemTechApi.saveTokens(any(), any()) } returns ApiResponse.Error(error) as ApiResponse<Unit> coEvery { tangemTechApi.saveTokens(any(), any()) } returns ApiResponse.Error(error) as ApiResponse<Unit>
coEvery { tangemTechApi.createWallet(body = any()) } returns ApiResponse.Error(error) as ApiResponse<Unit>
// WHEN // WHEN
userTokensSaver.push( userTokensSaver.push(

View file

@ -0,0 +1,226 @@
package com.tangem.data.common.wallet
import com.google.common.truth.Truth
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.common.response.ApiResponseError
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.models.WalletIdBody
import com.tangem.datasource.api.tangemTech.models.WalletType
import com.tangem.datasource.local.appsflyer.AppsFlyerConversionStore
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.wallets.models.AppsFlyerConversionData
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.*
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
/**
[REDACTED_AUTHOR]
*/
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
internal class DefaultWalletServerBinderTest {
private val userWalletsStore: UserWalletsStore = mockk()
private val appsFlyerConversionStore: AppsFlyerConversionStore = mockk()
private val tangemTechApi: TangemTechApi = mockk()
private val dispatchers: CoroutineDispatcherProvider = TestingCoroutineDispatcherProvider()
private val binder = DefaultWalletServerBinder(
userWalletsStore = userWalletsStore,
appsFlyerConversionStore = appsFlyerConversionStore,
tangemTechApi = tangemTechApi,
dispatchers = dispatchers
)
private val userWalletId = UserWalletId("011")
private val userWallet = mockk<UserWallet.Cold> {
every { this@mockk.walletId } returns userWalletId
every { this@mockk.name } returns "Wallet"
}
private val conversionData = AppsFlyerConversionData(refcode = "refcode", campaign = "campaign")
@AfterEach
fun tearDown() {
clearMocks(userWalletsStore, appsFlyerConversionStore, tangemTechApi)
}
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class BindUserWalletId {
@Test
fun `bind successfully`() = runTest {
val requestBody = WalletIdBody(
walletId = userWalletId.stringValue,
name = "Wallet",
walletType = WalletType.COLD,
refcode = "refcode",
campaign = "campaign",
)
val apiResponse = ApiResponse.Success(Unit)
coEvery { userWalletsStore.getSyncOrNull(userWalletId) } returns userWallet
coEvery { appsFlyerConversionStore.get() } returns conversionData
coEvery { tangemTechApi.createWallet(requestBody) } returns apiResponse
val actual = binder.bind(userWalletId = userWalletId)
Truth.assertThat(actual).isEqualTo(apiResponse)
coVerifyOrder {
userWalletsStore.getSyncOrNull(userWalletId)
appsFlyerConversionStore.get()
tangemTechApi.createWallet(requestBody)
}
}
@Test
fun `bind will skipped if userWalletsStore returns null`() = runTest {
coEvery { userWalletsStore.getSyncOrNull(userWalletId) } returns null
val actual = binder.bind(userWalletId = userWalletId)
Truth.assertThat(actual).isEqualTo(null)
coVerifyOrder {
userWalletsStore.getSyncOrNull(userWalletId)
}
coVerify(inverse = true) {
appsFlyerConversionStore.get()
tangemTechApi.createWallet(any())
}
}
@Test
fun `bind successfully if appsFlyerConversionStore returns null`() = runTest {
val requestBody = WalletIdBody(
walletId = userWalletId.stringValue,
name = "Wallet",
walletType = WalletType.COLD,
)
val apiResponse = ApiResponse.Success(Unit)
coEvery { userWalletsStore.getSyncOrNull(userWalletId) } returns userWallet
coEvery { appsFlyerConversionStore.get() } returns null
coEvery { tangemTechApi.createWallet(requestBody) } returns apiResponse
val actual = binder.bind(userWalletId = userWalletId)
Truth.assertThat(actual).isEqualTo(apiResponse)
coVerifyOrder {
userWalletsStore.getSyncOrNull(userWalletId)
appsFlyerConversionStore.get()
tangemTechApi.createWallet(requestBody)
}
}
@Test
fun `bind returns error if request is failed`() = runTest {
val requestBody = WalletIdBody(
walletId = userWalletId.stringValue,
name = "Wallet",
walletType = WalletType.COLD,
refcode = "refcode",
campaign = "campaign",
)
val apiResponse = ApiResponse.Error(ApiResponseError.TimeoutException()) as ApiResponse<Unit>
coEvery { userWalletsStore.getSyncOrNull(userWalletId) } returns userWallet
coEvery { appsFlyerConversionStore.get() } returns conversionData
coEvery { tangemTechApi.createWallet(requestBody) } returns apiResponse
val actual = binder.bind(userWalletId = userWalletId)
Truth.assertThat(actual).isEqualTo(apiResponse)
coVerifyOrder {
userWalletsStore.getSyncOrNull(userWalletId)
appsFlyerConversionStore.get()
tangemTechApi.createWallet(requestBody)
}
}
}
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class BindUserWallet {
@Test
fun `bind successfully`() = runTest {
val requestBody = WalletIdBody(
walletId = userWalletId.stringValue,
name = "Wallet",
walletType = WalletType.COLD,
refcode = "refcode",
campaign = "campaign",
)
val apiResponse = ApiResponse.Success(Unit)
coEvery { appsFlyerConversionStore.get() } returns conversionData
coEvery { tangemTechApi.createWallet(requestBody) } returns apiResponse
val actual = binder.bind(userWallet = userWallet)
Truth.assertThat(actual).isEqualTo(apiResponse)
coVerifyOrder {
appsFlyerConversionStore.get()
tangemTechApi.createWallet(requestBody)
}
}
@Test
fun `bind successfully if appsFlyerConversionStore returns null`() = runTest {
val requestBody = WalletIdBody(
walletId = userWalletId.stringValue,
name = "Wallet",
walletType = WalletType.COLD,
)
val apiResponse = ApiResponse.Success(Unit)
coEvery { appsFlyerConversionStore.get() } returns null
coEvery { tangemTechApi.createWallet(requestBody) } returns apiResponse
val actual = binder.bind(userWallet = userWallet)
Truth.assertThat(actual).isEqualTo(apiResponse)
coVerifyOrder {
appsFlyerConversionStore.get()
tangemTechApi.createWallet(requestBody)
}
}
@Test
fun `bind returns error if request is failed`() = runTest {
val requestBody = WalletIdBody(
walletId = userWalletId.stringValue,
name = "Wallet",
walletType = WalletType.COLD,
refcode = "refcode",
campaign = "campaign",
)
val apiResponse = ApiResponse.Error(ApiResponseError.TimeoutException()) as ApiResponse<Unit>
coEvery { appsFlyerConversionStore.get() } returns conversionData
coEvery { tangemTechApi.createWallet(requestBody) } returns apiResponse
val actual = binder.bind(userWallet = userWallet)
Truth.assertThat(actual).isEqualTo(apiResponse)
coVerifyOrder {
appsFlyerConversionStore.get()
tangemTechApi.createWallet(requestBody)
}
}
}
}

View file

@ -3,6 +3,7 @@ package com.tangem.data.wallets
import arrow.core.Either import arrow.core.Either
import arrow.core.left import arrow.core.left
import arrow.core.right import arrow.core.right
import com.tangem.data.common.wallet.WalletServerBinder
import com.tangem.data.wallets.converters.UserWalletRemoteInfoConverter import com.tangem.data.wallets.converters.UserWalletRemoteInfoConverter
import com.tangem.datasource.api.common.AuthProvider import com.tangem.datasource.api.common.AuthProvider
import com.tangem.datasource.api.common.response.ApiResponse import com.tangem.datasource.api.common.response.ApiResponse
@ -14,6 +15,7 @@ import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.converters.WalletIdBodyConverter import com.tangem.datasource.api.tangemTech.converters.WalletIdBodyConverter
import com.tangem.datasource.api.tangemTech.models.* import com.tangem.datasource.api.tangemTech.models.*
import com.tangem.datasource.api.tangemTech.models.SeedPhraseNotificationDTO.Status import com.tangem.datasource.api.tangemTech.models.SeedPhraseNotificationDTO.Status
import com.tangem.datasource.local.appsflyer.AppsFlyerConversionStore
import com.tangem.datasource.local.datastore.RuntimeStateStore import com.tangem.datasource.local.datastore.RuntimeStateStore
import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.PreferencesKeys
@ -46,6 +48,8 @@ internal class DefaultWalletsRepository(
private val seedPhraseNotificationVisibilityStore: RuntimeStateStore<SeedPhraseNotificationsStatuses>, private val seedPhraseNotificationVisibilityStore: RuntimeStateStore<SeedPhraseNotificationsStatuses>,
private val dispatchers: CoroutineDispatcherProvider, private val dispatchers: CoroutineDispatcherProvider,
private val authProvider: AuthProvider, private val authProvider: AuthProvider,
private val walletServerBinder: WalletServerBinder,
private val appsFlyerConversionStore: AppsFlyerConversionStore,
private val accountsFeatureToggles: AccountsFeatureToggles, private val accountsFeatureToggles: AccountsFeatureToggles,
private val moshi: com.squareup.moshi.Moshi, private val moshi: com.squareup.moshi.Moshi,
) : WalletsRepository { ) : WalletsRepository {
@ -250,13 +254,7 @@ internal class DefaultWalletsRepository(
} }
override suspend fun createWallet(userWalletId: UserWalletId) { override suspend fun createWallet(userWalletId: UserWalletId) {
runCatching(dispatchers.io) { walletServerBinder.bind(userWalletId)
val userWallet = userWalletsStore.getSyncStrict(key = userWalletId)
tangemTechApi.createWallet(
body = WalletIdBodyConverter.convert(userWallet = userWallet),
)
}
} }
override suspend fun rejectSeedPhraseSecondNotification(userWalletId: UserWalletId) { override suspend fun rejectSeedPhraseSecondNotification(userWalletId: UserWalletId) {
@ -413,10 +411,12 @@ internal class DefaultWalletsRepository(
associateApplicationIdWithWallets().getOrThrow() associateApplicationIdWithWallets().getOrThrow()
} }
} else { } else {
val conversionData = appsFlyerConversionStore.get()
val publicKeys = authProvider.getCardsPublicKeys() val publicKeys = authProvider.getCardsPublicKeys()
val walletsBody = wallets.map { userWallet -> val walletsBody = wallets.map { userWallet ->
WalletIdBodyConverter.convert( WalletIdBodyConverter.convert(
userWallet = userWallet, userWallet = userWallet,
conversionData = conversionData,
publicKeys = if (userWallet is UserWallet.Cold) { publicKeys = if (userWallet is UserWallet.Cold) {
publicKeys.filterKeys { publicKeys.filterKeys {
userWallet.cardsInWallet.contains(it) userWallet.cardsInWallet.contains(it)

View file

@ -1,6 +1,7 @@
package com.tangem.data.wallets.di package com.tangem.data.wallets.di
import com.squareup.moshi.Moshi import com.squareup.moshi.Moshi
import com.tangem.data.common.wallet.WalletServerBinder
import com.tangem.data.wallets.DefaultWalletNamesMigrationRepository import com.tangem.data.wallets.DefaultWalletNamesMigrationRepository
import com.tangem.data.wallets.DefaultWalletsRepository import com.tangem.data.wallets.DefaultWalletsRepository
import com.tangem.data.wallets.cold.DefaultColdMapDerivationsRepository import com.tangem.data.wallets.cold.DefaultColdMapDerivationsRepository
@ -10,6 +11,7 @@ import com.tangem.data.wallets.hot.DefaultHotWalletAccessCodeAttemptsRepository
import com.tangem.datasource.api.common.AuthProvider import com.tangem.datasource.api.common.AuthProvider
import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.di.NetworkMoshi import com.tangem.datasource.di.NetworkMoshi
import com.tangem.datasource.local.appsflyer.AppsFlyerConversionStore
import com.tangem.datasource.local.datastore.RuntimeStateStore import com.tangem.datasource.local.datastore.RuntimeStateStore
import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.datasource.local.userwallet.UserWalletsStore
@ -40,6 +42,8 @@ internal object WalletsDataModule {
userWalletsStore: UserWalletsStore, userWalletsStore: UserWalletsStore,
dispatchers: CoroutineDispatcherProvider, dispatchers: CoroutineDispatcherProvider,
authProvider: AuthProvider, authProvider: AuthProvider,
walletServerBinder: WalletServerBinder,
appsFlyerConversionStore: AppsFlyerConversionStore,
accountsFeatureToggles: AccountsFeatureToggles, accountsFeatureToggles: AccountsFeatureToggles,
@NetworkMoshi moshi: Moshi, @NetworkMoshi moshi: Moshi,
): WalletsRepository { ): WalletsRepository {
@ -50,6 +54,8 @@ internal object WalletsDataModule {
seedPhraseNotificationVisibilityStore = RuntimeStateStore(defaultValue = emptyMap()), seedPhraseNotificationVisibilityStore = RuntimeStateStore(defaultValue = emptyMap()),
dispatchers = dispatchers, dispatchers = dispatchers,
authProvider = authProvider, authProvider = authProvider,
walletServerBinder = walletServerBinder,
appsFlyerConversionStore = appsFlyerConversionStore,
accountsFeatureToggles = accountsFeatureToggles, accountsFeatureToggles = accountsFeatureToggles,
moshi = moshi, moshi = moshi,
) )

View file

@ -4,6 +4,7 @@ import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.Preferences
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import com.squareup.moshi.Moshi import com.squareup.moshi.Moshi
import com.tangem.data.common.wallet.WalletServerBinder
import com.tangem.datasource.api.common.AuthProvider import com.tangem.datasource.api.common.AuthProvider
import com.tangem.datasource.api.common.response.ApiResponse import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.common.response.ApiResponseError.HttpException import com.tangem.datasource.api.common.response.ApiResponseError.HttpException
@ -11,6 +12,7 @@ import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.models.PromocodeActivationBody import com.tangem.datasource.api.tangemTech.models.PromocodeActivationBody
import com.tangem.datasource.api.tangemTech.models.PromocodeActivationResponse import com.tangem.datasource.api.tangemTech.models.PromocodeActivationResponse
import com.tangem.datasource.api.tangemTech.models.WalletResponse import com.tangem.datasource.api.tangemTech.models.WalletResponse
import com.tangem.datasource.local.appsflyer.AppsFlyerConversionStore
import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
@ -38,12 +40,16 @@ class DefaultWalletsRepositoryTest {
) )
private lateinit var tangemTechApi: TangemTechApi private lateinit var tangemTechApi: TangemTechApi
private lateinit var dispatchers: CoroutineDispatcherProvider private lateinit var dispatchers: CoroutineDispatcherProvider
private lateinit var walletServerBinder: WalletServerBinder
private lateinit var appsFlyerConversionStore: AppsFlyerConversionStore
private val testWalletId = UserWalletId("1234567890abcdef") private val testWalletId = UserWalletId("1234567890abcdef")
@Before @Before
fun setup() { fun setup() {
tangemTechApi = mockk() tangemTechApi = mockk()
walletServerBinder = mockk()
appsFlyerConversionStore = mockk()
dispatchers = TestingCoroutineDispatcherProvider() dispatchers = TestingCoroutineDispatcherProvider()
repository = DefaultWalletsRepository( repository = DefaultWalletsRepository(
appPreferencesStore = appPreferenceStore, appPreferencesStore = appPreferenceStore,
@ -52,6 +58,8 @@ class DefaultWalletsRepositoryTest {
seedPhraseNotificationVisibilityStore = mockk(), seedPhraseNotificationVisibilityStore = mockk(),
dispatchers = dispatchers, dispatchers = dispatchers,
authProvider = mockk(), authProvider = mockk(),
walletServerBinder = walletServerBinder,
appsFlyerConversionStore = appsFlyerConversionStore,
accountsFeatureToggles = mockk(), accountsFeatureToggles = mockk(),
moshi = mockk(), moshi = mockk(),
) )
@ -218,10 +226,14 @@ class DefaultWalletsRepositoryTest {
seedPhraseNotificationVisibilityStore = mockk(), seedPhraseNotificationVisibilityStore = mockk(),
dispatchers = dispatchers, dispatchers = dispatchers,
authProvider = authProvider, authProvider = authProvider,
walletServerBinder = walletServerBinder,
appsFlyerConversionStore = appsFlyerConversionStore,
accountsFeatureToggles = accountsFeatureToggles, accountsFeatureToggles = accountsFeatureToggles,
moshi = mockk(), moshi = mockk(),
) )
coEvery { appsFlyerConversionStore.get() } returns null
coEvery { coEvery {
tangemTechApi.associateApplicationIdWithWallets( tangemTechApi.associateApplicationIdWithWallets(
eq(applicationId), eq(applicationId),