Updated on 2026-08-14

This commit is contained in:
Tangem 2025-07-21 18:34:38 +03:00
commit 438126dc82
665 changed files with 16174 additions and 6245 deletions

View file

@ -25,7 +25,6 @@ sealed class ApiConfig {
TangemTech,
StakeKit,
TangemPay,
Attestation,
BlockAid,
}
@ -35,7 +34,6 @@ sealed class ApiConfig {
is TangemTech -> ID.TangemTech
is StakeKit -> ID.StakeKit
is TangemPay -> ID.TangemPay
is Attestation -> ID.Attestation
is BlockAid -> ID.BlockAid
}
}

View file

@ -13,9 +13,15 @@ enum class ApiEnvironment {
@Json(name = "DEV")
DEV,
@Json(name = "DEV_2")
DEV_2,
@Json(name = "STAGE")
STAGE,
@Json(name = "MOCK")
MOCK,
@Json(name = "PROD")
PROD,
}

View file

@ -1,26 +0,0 @@
package com.tangem.datasource.api.common.config
/**
* Attestation [ApiConfig] that used in CardSDK
*
[REDACTED_AUTHOR]
*/
internal class Attestation : ApiConfig() {
override val defaultEnvironment: ApiEnvironment = ApiEnvironment.PROD
override val environmentConfigs: List<ApiEnvironmentConfig> = listOf(
createDevEnvironment(),
createProdEnvironment(),
)
private fun createDevEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
environment = ApiEnvironment.DEV,
baseUrl = "[REDACTED_ENV_URL]",
)
private fun createProdEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
environment = ApiEnvironment.PROD,
baseUrl = "https://api.tangem-tech.com/",
)
}

View file

@ -14,6 +14,7 @@ import com.tangem.utils.version.AppVersionProvider
* @property environmentConfigStorage environment config storage
* @property expressAuthProvider express auth provider
* @property appVersionProvider app version provider
* @property appInfoProvider app info provider
*/
internal class Express(
private val environmentConfigStorage: EnvironmentConfigStorage,
@ -26,22 +27,51 @@ internal class Express(
override val environmentConfigs: List<ApiEnvironmentConfig> = listOf(
createDevEnvironment(),
createDev2Environment(),
createStageEnvironment(),
createMockedEnvironment(),
createProdEnvironment(),
)
private fun getInitialEnvironment(): ApiEnvironment {
return when (BuildConfig.BUILD_TYPE) {
DEBUG_BUILD_TYPE,
-> ApiEnvironment.DEV
INTERNAL_BUILD_TYPE,
-> ApiEnvironment.STAGE
MOCKED_BUILD_TYPE,
-> ApiEnvironment.MOCK
EXTERNAL_BUILD_TYPE,
RELEASE_BUILD_TYPE,
-> ApiEnvironment.PROD
else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]")
}
}
private fun createDevEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
environment = ApiEnvironment.DEV,
baseUrl = "[REDACTED_ENV_URL]",
headers = createHeaders(isProd = false),
)
private fun createDev2Environment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
environment = ApiEnvironment.DEV_2,
baseUrl = "[REDACTED_ENV_URL]",
headers = createHeaders(isProd = false),
)
private fun createStageEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
environment = ApiEnvironment.STAGE,
baseUrl = "[REDACTED_ENV_URL]",
headers = createHeaders(isProd = false),
)
private fun createMockedEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
environment = ApiEnvironment.MOCK,
baseUrl = "[REDACTED_ENV_URL]",
headers = createHeaders(isProd = false),
)
private fun createProdEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
environment = ApiEnvironment.PROD,
baseUrl = "https://express.tangem.com/v1/",
@ -63,21 +93,4 @@ internal class Express(
?.apiKey
?: error("No express config provided")
}
private companion object {
fun getInitialEnvironment(): ApiEnvironment {
return when (BuildConfig.BUILD_TYPE) {
DEBUG_BUILD_TYPE,
-> ApiEnvironment.DEV
INTERNAL_BUILD_TYPE,
MOCKED_BUILD_TYPE,
-> ApiEnvironment.STAGE
EXTERNAL_BUILD_TYPE,
RELEASE_BUILD_TYPE,
-> ApiEnvironment.PROD
else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]")
}
}
}
}

View file

@ -1,5 +1,6 @@
package com.tangem.datasource.api.common.config
import com.tangem.datasource.BuildConfig
import com.tangem.utils.ProviderSuspend
import com.tangem.utils.version.AppVersionProvider
@ -7,11 +8,37 @@ internal class TangemPay(
private val appVersionProvider: AppVersionProvider,
) : ApiConfig() {
override val defaultEnvironment: ApiEnvironment = ApiEnvironment.DEV
override val defaultEnvironment: ApiEnvironment = getInitialEnvironment()
override val environmentConfigs = listOf(
createProdEnvironment(),
createDevEnvironment(),
createMockedEnvironment(),
createProdEnvironment(),
)
private fun getInitialEnvironment(): ApiEnvironment {
return when (BuildConfig.BUILD_TYPE) {
MOCKED_BUILD_TYPE -> ApiEnvironment.MOCK
DEBUG_BUILD_TYPE,
INTERNAL_BUILD_TYPE,
-> ApiEnvironment.DEV
EXTERNAL_BUILD_TYPE,
RELEASE_BUILD_TYPE,
-> ApiEnvironment.PROD
else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]")
}
}
private fun createDevEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
environment = ApiEnvironment.DEV,
baseUrl = "[REDACTED_ENV_URL]",
headers = createHeaders(),
)
private fun createMockedEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
environment = ApiEnvironment.MOCK,
baseUrl = "[REDACTED_ENV_URL]",
headers = createHeaders(),
)
private fun createProdEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
@ -20,12 +47,6 @@ internal class TangemPay(
headers = createHeaders(),
)
private fun createDevEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
environment = ApiEnvironment.DEV,
baseUrl = "[REDACTED_ENV_URL]",
headers = createHeaders(),
)
private fun createHeaders() = mapOf(
"version" to ProviderSuspend { appVersionProvider.versionName },
"platform" to ProviderSuspend { "Android" },

View file

@ -1,5 +1,6 @@
package com.tangem.datasource.api.common.config
import com.tangem.datasource.BuildConfig
import com.tangem.datasource.api.common.AuthProvider
import com.tangem.datasource.utils.RequestHeader
import com.tangem.utils.info.AppInfoProvider
@ -12,18 +13,27 @@ internal class TangemTech(
private val appInfoProvider: AppInfoProvider,
) : ApiConfig() {
override val defaultEnvironment: ApiEnvironment = ApiEnvironment.PROD
override val defaultEnvironment: ApiEnvironment = getInitialEnvironment()
override val environmentConfigs = listOf(
createDevEnvironment(),
createStageEnvironment(),
createMockedEnvironment(),
createProdEnvironment(),
)
private fun createProdEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
environment = ApiEnvironment.PROD,
baseUrl = "https://api.tangem.org/v1/",
headers = createHeaders(),
)
private fun getInitialEnvironment(): ApiEnvironment {
return when (BuildConfig.BUILD_TYPE) {
MOCKED_BUILD_TYPE,
-> ApiEnvironment.MOCK
DEBUG_BUILD_TYPE,
INTERNAL_BUILD_TYPE,
EXTERNAL_BUILD_TYPE,
RELEASE_BUILD_TYPE,
-> ApiEnvironment.PROD
else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]")
}
}
private fun createDevEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
environment = ApiEnvironment.DEV,
@ -31,6 +41,24 @@ internal class TangemTech(
headers = createHeaders(),
)
private fun createStageEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
environment = ApiEnvironment.STAGE,
baseUrl = "[REDACTED_ENV_URL]",
headers = createHeaders(),
)
private fun createMockedEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
environment = ApiEnvironment.MOCK,
baseUrl = "[REDACTED_ENV_URL]",
headers = createHeaders(),
)
private fun createProdEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
environment = ApiEnvironment.PROD,
baseUrl = "https://api.tangem.org/",
headers = createHeaders(),
)
private fun createHeaders() = buildMap {
putAll(from = RequestHeader.AppVersionPlatformHeaders(appVersionProvider, appInfoProvider).values)
putAll(from = RequestHeader.AuthenticationHeader(authProvider).values)

View file

@ -20,44 +20,42 @@ import kotlinx.coroutines.flow.*
* @property dispatchers coroutine dispatcher provider
*/
internal class DevApiConfigsManager(
apiConfigs: ApiConfigs,
private val apiConfigs: ApiConfigs,
private val appPreferencesStore: AppPreferencesStore,
private val dispatchers: CoroutineDispatcherProvider,
) : MutableApiConfigsManager {
) : MutableApiConfigsManager() {
override val configs: Flow<Map<ApiConfig, ApiEnvironment>> get() = _apiConfigs
override val configs: StateFlow<Map<ApiConfig, ApiEnvironment>>
field = MutableStateFlow(value = getInitialConfigs())
private val _apiConfigs = MutableStateFlow(value = apiConfigs.associateWith { it.defaultEnvironment })
override val isInitialized: StateFlow<Boolean> get() = _isInitialized.asStateFlow()
private val _isInitialized = MutableStateFlow(value = false)
override val isInitialized: StateFlow<Boolean>
field = MutableStateFlow(value = false)
override fun initialize() {
_isInitialized.value = false
isInitialized.value = false
// We can't use appPreferencesStore.getObjectMap as base flow,
// because we should keep possibility to work with configs synchronous.
// See [getBaseUrl]
appPreferencesStore.getObjectMap<ApiEnvironment>(PreferencesKeys.apiConfigsEnvironmentKey)
.distinctUntilChanged()
.onEach { savedEnvironments ->
_apiConfigs.update { apiConfigs ->
apiConfigs.mapValues {
val (config, currentEnvironment) = it
val apiConfigs = configs.value
savedEnvironments[config.id.name] ?: currentEnvironment
}
configs.value = apiConfigs.mapValues {
val (config, currentEnvironment) = it
savedEnvironments[config.id.name] ?: currentEnvironment
}
if (!_isInitialized.value) {
_isInitialized.value = true
if (!isInitialized.value) {
isInitialized.value = true
}
notifyListeners(apiConfigs = apiConfigs, savedEnvironments = savedEnvironments)
}
.launchIn(CoroutineScope(SupervisorJob() + dispatchers.default))
}
override fun getEnvironmentConfig(id: ApiConfig.ID): ApiEnvironmentConfig {
val apiConfigs = _apiConfigs.value
val apiConfigs = configs.value
val config = apiConfigs.map { it }.firstOrNull { it.key.id == id }?.key
?: error("Api config with id [$id] not found")
@ -78,4 +76,56 @@ internal class DevApiConfigsManager(
mutablePreferences.setObjectMap(PreferencesKeys.apiConfigsEnvironmentKey, updatedMap)
}
}
override suspend fun changeEnvironment(environment: ApiEnvironment) {
val supportedConfigs = apiConfigs
.filter { config ->
config.environmentConfigs.any { it.environment == environment }
}
.map { it.id.name }
appPreferencesStore.editData { mutablePreferences ->
val updatedMap = mutablePreferences.getObjectMap<ApiEnvironment>(PreferencesKeys.apiConfigsEnvironmentKey)
.mapValues { (configName, currentEnvironment) ->
val isEnvSupported = supportedConfigs.contains(configName)
if (isEnvSupported) environment else currentEnvironment
}
mutablePreferences.setObjectMap(key = PreferencesKeys.apiConfigsEnvironmentKey, value = updatedMap)
}
}
private fun notifyListeners(
apiConfigs: Map<ApiConfig, ApiEnvironment>,
savedEnvironments: Map<String, ApiEnvironment>,
) {
if (registerListeners.isNotEmpty()) {
val changedConfigs = apiConfigs.mapNotNull { (config, prevEnvironment) ->
val newEnvironment = savedEnvironments[config.id.name] ?: config.defaultEnvironment
if (prevEnvironment == newEnvironment) return@mapNotNull null
val environmentConfig = config.environmentConfigs
.firstOrNull { it.environment == newEnvironment }
?: return@mapNotNull null
config.id to environmentConfig
}
if (changedConfigs.isNotEmpty()) {
registerListeners.forEach { listener ->
val changedConfig = changedConfigs.firstOrNull { it.first == listener.id }?.second
if (changedConfig != null) {
listener.onChange(changedConfig)
}
}
}
}
}
private fun getInitialConfigs(): Map<ApiConfig, ApiEnvironment> {
return apiConfigs.associateWith(ApiConfig::defaultEnvironment)
}
}

View file

@ -0,0 +1,79 @@
package com.tangem.datasource.api.common.config.managers
import com.tangem.datasource.api.common.config.ApiConfig
import com.tangem.datasource.api.common.config.ApiConfigs
import com.tangem.datasource.api.common.config.ApiEnvironment
import com.tangem.datasource.api.common.config.ApiEnvironmentConfig
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.*
/**
* Implementation of [ApiConfigsManager] in MOCK environment
*
* @property apiConfigs api configs
*
[REDACTED_AUTHOR]
*/
internal class MockApiConfigsManager(
private val apiConfigs: ApiConfigs,
dispatchers: CoroutineDispatcherProvider,
) : MutableApiConfigsManager() {
override val configs: StateFlow<Map<ApiConfig, ApiEnvironment>>
field = MutableStateFlow(value = getInitialConfigs())
override val isInitialized: StateFlow<Boolean> = MutableStateFlow(value = true)
private val coroutineScope = CoroutineScope(SupervisorJob() + dispatchers.default)
override fun initialize() = Unit
override fun getEnvironmentConfig(id: ApiConfig.ID): ApiEnvironmentConfig {
val apiConfigs = configs.value
val (config, currentEnvironment) = apiConfigs.entries.firstOrNull { it.key.id == id }
?: error("Api config with id [$id] not found")
return config.environmentConfigs.firstOrNull { it.environment == currentEnvironment }
?: error("Api config with id [$id] doesn't contain environment [$currentEnvironment]")
}
override suspend fun changeEnvironment(id: String, environment: ApiEnvironment) {
configs.update { apiConfigs ->
val apiConfig = apiConfigs.keys.firstOrNull { it.id.name == id }
?: error("Api config with id [$id] not found. Check that ApiConfig with id [$id] was provided into DI")
apiConfigs + (apiConfig to environment)
}
}
override suspend fun changeEnvironment(environment: ApiEnvironment) {
configs.update { apiConfigs ->
apiConfigs.mapValues { (config, currentEnvironment) ->
val isEnvSupported = config.environmentConfigs.any { it.environment == environment }
if (isEnvSupported) environment else currentEnvironment
}
}
}
override fun addListener(listener: ApiConfigEnvChangeListener) {
super.addListener(listener)
configs
.map { it.entries.firstOrNull { it.key.id == listener.id } }
.filterNotNull()
.onEach { (apiConfig, currentEnvironment) ->
listener.onChange(
environmentConfig = apiConfig.environmentConfigs.first { it.environment == currentEnvironment },
)
}
.launchIn(coroutineScope)
}
private fun getInitialConfigs(): Map<ApiConfig, ApiEnvironment> {
return apiConfigs.associateWith { it.defaultEnvironment }
}
}

View file

@ -2,6 +2,7 @@ package com.tangem.datasource.api.common.config.managers
import com.tangem.datasource.api.common.config.ApiConfig
import com.tangem.datasource.api.common.config.ApiEnvironment
import com.tangem.datasource.api.common.config.ApiEnvironmentConfig
import kotlinx.coroutines.flow.Flow
/**
@ -9,11 +10,41 @@ import kotlinx.coroutines.flow.Flow
*
[REDACTED_AUTHOR]
*/
interface MutableApiConfigsManager : ApiConfigsManager {
abstract class MutableApiConfigsManager : ApiConfigsManager {
/** Api configs with current [ApiEnvironment] */
val configs: Flow<Map<ApiConfig, ApiEnvironment>>
abstract val configs: Flow<Map<ApiConfig, ApiEnvironment>>
/**
* A set of listeners registered to observe changes in API environment configurations.
* These listeners are notified whenever an environment change occurs.
*/
protected val registerListeners: Set<ApiConfigEnvChangeListener>
field = mutableSetOf<ApiConfigEnvChangeListener>()
/** Change api environment [environment] by [id] */
suspend fun changeEnvironment(id: String, environment: ApiEnvironment)
abstract suspend fun changeEnvironment(id: String, environment: ApiEnvironment)
/** Change api environment [environment] for all configs */
abstract suspend fun changeEnvironment(environment: ApiEnvironment)
/** Adds a [listener] to observe changes in API environment configurations */
open fun addListener(listener: ApiConfigEnvChangeListener) {
registerListeners += listener
}
/**
* Listener for observing changes in API environment configurations
*
* @property id the identifier of the API configuration this listener is associated with
*/
abstract class ApiConfigEnvChangeListener(val id: ApiConfig.ID) {
/**
* Called when the environment configuration changes
*
* @param environmentConfig the updated [ApiEnvironmentConfig] for the associated API configuration
*/
abstract fun onChange(environmentConfig: ApiEnvironmentConfig)
}
}

View file

@ -9,7 +9,7 @@ import retrofit2.http.Query
interface TangemTechMarketsApi {
@Suppress("LongParameterList")
@GET("coins/list")
@GET("v1/coins/list")
suspend fun getCoinsList(
@Query("currency") currency: String,
@Query("interval") interval: String,
@ -20,24 +20,24 @@ interface TangemTechMarketsApi {
@Query("timestamp") timestamp: Long?,
): ApiResponse<TokenMarketListResponse>
@GET("coins/{coin_id}")
@GET("v1/coins/{coin_id}")
suspend fun getCoinMarketData(
@Path("coin_id") coinId: String,
@Query("currency") currency: String,
@Query("language") language: String,
): ApiResponse<TokenMarketInfoResponse>
@GET("coins/{coin_id}/history")
@GET("v1/coins/{coin_id}/history")
suspend fun getCoinChart(
@Path("coin_id") coinId: String,
@Query("currency") currency: String,
@Query("interval") interval: String,
): ApiResponse<TokenMarketChartResponse>
@GET("coins/{coin_id}/exchanges")
@GET("v1/coins/{coin_id}/exchanges")
suspend fun getCoinExchanges(@Path("coin_id") coinId: String): ApiResponse<TokenMarketExchangesResponse>
@GET("coins/history_preview")
@GET("v1/coins/history_preview")
suspend fun getCoinsListCharts(
@Query("coin_ids") coinIds: String,
@Query("currency") currency: String,

View file

@ -17,7 +17,7 @@ import java.util.concurrent.TimeUnit
@Suppress("TooManyFunctions")
interface TangemTechApi {
@GET("coins")
@GET("v1/coins")
suspend fun getCoins(
@Header("Cache-Control") cacheControl: String = "max-age=600",
@Query("contractAddress") contractAddress: String? = null,
@ -30,150 +30,150 @@ interface TangemTechApi {
@Query("limit") limit: Int? = null,
): ApiResponse<CoinsResponse>
@GET("rates")
@GET("v1/rates")
suspend fun getRates(@Query("currencyId") currencyId: String, @Query("coinIds") coinIds: String): RatesResponse
@GET("currencies")
@GET("v1/currencies")
suspend fun getCurrencyList(
@Header("Cache-Control") cacheControl: String = "max-age=600",
): ApiResponse<CurrenciesResponse>
@GET("geo")
@GET("v1/geo")
suspend fun getUserCountryCode(): GeoResponse
@GET("user-tokens/{user-id}")
@GET("v1/user-tokens/{user-id}")
suspend fun getUserTokens(@Path(value = "user-id") userId: String): ApiResponse<UserTokensResponse>
@PUT("user-tokens/{user-id}")
@PUT("v1/user-tokens/{user-id}")
suspend fun saveUserTokens(
@Path(value = "user-id") userId: String,
@Body userTokens: UserTokensResponse,
): ApiResponse<Unit>
@POST("user-tokens")
@POST("v1/user-tokens")
suspend fun markUserWallerWasCreated(@Body body: MarkUserWalletWasCreatedBody): ApiResponse<Unit>
/** Returns referral status by [walletId] */
@GET("referral/{walletId}")
@GET("v1/referral/{walletId}")
suspend fun getReferralStatus(@Path("walletId") walletId: String): ApiResponse<ReferralResponse>
/** Make user referral, requires [StartReferralBody] */
@POST("referral")
@POST("v1/referral")
suspend fun startReferral(@Body startReferralBody: StartReferralBody): ApiResponse<ReferralResponse>
@GET("quotes")
@GET("v1/quotes")
suspend fun getQuotes(
@Query("currencyId") currencyId: String,
@Query("coinIds") coinIds: String,
@Query("fields") fields: String,
): ApiResponse<QuotesResponse>
@GET("promotion")
@GET("v1/promotion")
suspend fun getPromotionInfo(
@Query("programName") name: String,
@Header("Cache-Control") cacheControl: String = "max-age=600",
): ApiResponse<PromotionInfoResponse>
@GET("settings/{wallet_id}")
@GET("v1/settings/{wallet_id}")
suspend fun getUserTokensSettings(@Path("wallet_id") walletId: String): ApiResponse<UserTokensSettingsResponse>
@PUT("settings/{wallet_id}")
@PUT("v1/settings/{wallet_id}")
suspend fun saveUserTokensSettings(
@Path("wallet_id") walletId: String,
@Body userTokensSettings: UserTokensSettingsResponse,
): ApiResponse<Unit>
@POST("user-network-account")
@POST("v1/user-network-account")
suspend fun createUserNetworkAccount(
@Body body: CreateUserNetworkAccountBody,
): ApiResponse<CreateUserNetworkAccountResponse>
@POST("account")
@POST("v1/account")
suspend fun createUserTokensAccount(
@Body body: CreateUserTokensAccountBody,
): ApiResponse<UserTokensAccountResponse>
@PUT("account/{account_id}")
@PUT("v1/account/{account_id}")
suspend fun updateUserTokensAccount(
@Path("account_id") accountId: Int,
@Body body: UpdateUserTokensAccountBody,
): ApiResponse<UserTokensAccountResponse>
@PUT("account/{account_id}/archive")
@PUT("v1/account/{account_id}/archive")
suspend fun archiveUserTokensAccount(@Path("account_id") accountId: Int): ApiResponse<UserTokensAccountResponse>
@PUT("account/{account_id}/unarchive")
@PUT("v1/account/{account_id}/unarchive")
suspend fun restoreUserTokensAccount(@Path("account_id") accountId: Int): ApiResponse<UserTokensAccountResponse>
@GET("features")
@GET("v1/features")
suspend fun getFeatures(): ApiResponse<FeaturesResponse>
@ReadTimeout(duration = 5, unit = TimeUnit.SECONDS)
@GET("networks/providers")
@GET("v1/networks/providers")
suspend fun getBlockchainProviders(): Map<String, List<ProviderModel>>
@GET("seedphrase-notification/{wallet_id}")
@GET("v1/seedphrase-notification/{wallet_id}")
suspend fun getSeedPhraseNotificationStatus(
@Path("wallet_id") walletId: String,
): ApiResponse<SeedPhraseNotificationDTO>
@PUT("seedphrase-notification/{wallet_id}")
@PUT("v1/seedphrase-notification/{wallet_id}")
suspend fun updateSeedPhraseNotificationStatus(
@Path("wallet_id") walletId: String,
@Body body: SeedPhraseNotificationDTO,
): ApiResponse<Unit>
@GET("seedphrase-notification/{wallet_id}/confirmed")
@GET("v1/seedphrase-notification/{wallet_id}/confirmed")
suspend fun getSeedPhraseSecondNotificationStatus(
@Path("wallet_id") walletId: String,
): ApiResponse<SeedPhraseNotificationDTO>
@PUT("seedphrase-notification/{wallet_id}/confirmed")
@PUT("v1/seedphrase-notification/{wallet_id}/confirmed")
suspend fun updateSeedPhraseSecondNotificationStatus(
@Path("wallet_id") walletId: String,
@Body body: SeedPhraseNotificationDTO,
): ApiResponse<Unit>
@GET("hot_crypto")
@GET("v1/hot_crypto")
suspend fun getHotCrypto(@Query("currency") currencyId: String): ApiResponse<HotCryptoResponse>
@GET("stories/{story_id}")
@GET("v1/stories/{story_id}")
suspend fun getStoryById(@Path("story_id") storyId: String): ApiResponse<StoryContentResponse>
// region push notifications
@GET("notification/push_notifications_eligible_networks")
@GET("v1/notification/push_notifications_eligible_networks")
suspend fun getEligibleNetworksForPushNotifications(): ApiResponse<List<CryptoNetworkResponse>>
@POST("user-wallets/applications/")
@POST("v1/user-wallets/applications/")
suspend fun createApplicationId(
@Body
body: NotificationApplicationCreateBody,
): ApiResponse<NotificationApplicationIdResponse>
@PATCH("user-wallets/applications/{application_id}")
@PATCH("v1/user-wallets/applications/{application_id}")
suspend fun updatePushTokenForApplicationId(
@Path("application_id") applicationId: String,
@Body body: NotificationApplicationCreateBody,
): ApiResponse<Unit>
@PATCH("user-wallets/wallets/{wallet_id}/notify")
@PATCH("v1/user-wallets/wallets/{wallet_id}/notify")
suspend fun setNotificationsEnabled(@Path("wallet_id") walletId: String, @Body body: WalletBody): ApiResponse<Unit>
// endregion
// region wallets
@PATCH("user-wallets/wallets/{wallet_id}")
@PATCH("v1/user-wallets/wallets/{wallet_id}")
suspend fun updateWallet(@Path("wallet_id") walletId: String, @Body body: WalletBody): ApiResponse<Unit>
@POST("user-wallets/wallets/create-and-connect-by-appuid/{application_id}")
@POST("v1/user-wallets/wallets/create-and-connect-by-appuid/{application_id}")
suspend fun associateApplicationIdWithWallets(
@Path("application_id") applicationId: String,
@Body body: List<WalletIdBody>,
): ApiResponse<Unit>
@GET("user-wallets/wallets/{wallet_id}")
@GET("v1/user-wallets/wallets/{wallet_id}")
suspend fun getWalletById(@Path("wallet_id") walletId: String): ApiResponse<WalletResponse>
@GET("user-wallets/wallets/by-app/{app_id}")
@GET("v1/user-wallets/wallets/by-app/{app_id}")
suspend fun getWallets(@Path("app_id") appId: String): ApiResponse<List<WalletResponse>>
// endregion
}

View file

@ -60,8 +60,4 @@ internal object ApiConfigsModule {
fun provideBlockAidConfig(environmentConfigStorage: EnvironmentConfigStorage): ApiConfig {
return BlockAid(environmentConfigStorage)
}
@Provides
@IntoSet
fun provideAttestationConfig(): ApiConfig = Attestation()
}

View file

@ -6,18 +6,20 @@ import com.tangem.core.analytics.api.AnalyticsErrorHandler
import com.tangem.datasource.BuildConfig
import com.tangem.datasource.api.common.blockaid.BlockAidApi
import com.tangem.datasource.api.common.config.ApiConfig
import com.tangem.datasource.api.common.config.ApiConfig.Companion.MOCKED_BUILD_TYPE
import com.tangem.datasource.api.common.config.ApiConfigs
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
import com.tangem.datasource.api.common.config.managers.DevApiConfigsManager
import com.tangem.datasource.api.common.config.managers.MockApiConfigsManager
import com.tangem.datasource.api.common.config.managers.ProdApiConfigsManager
import com.tangem.datasource.api.common.response.ApiResponseCallAdapterFactory
import com.tangem.datasource.api.express.TangemExpressApi
import com.tangem.datasource.api.markets.TangemTechMarketsApi
import com.tangem.datasource.api.onramp.OnrampApi
import com.tangem.datasource.api.pay.TangemPayApi
import com.tangem.datasource.api.stakekit.StakeKitApi
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.TangemTechApiV2
import com.tangem.datasource.api.pay.TangemPayApi
import com.tangem.datasource.local.logs.AppLogsStore
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.utils.*
@ -45,7 +47,7 @@ internal object NetworkModule {
private const val STAKE_KIT_API_TIMEOUT_SECONDS = 60L
private val excludedApiForLogging: Set<ApiConfig.ID> = setOf(
// ApiConfig.ID.StakeKit,
ApiConfig.ID.StakeKit,
)
@Provides
@ -55,10 +57,10 @@ internal object NetworkModule {
appPreferencesStore: AppPreferencesStore,
dispatchers: CoroutineDispatcherProvider,
): ApiConfigsManager {
return if (BuildConfig.TESTER_MENU_ENABLED) {
DevApiConfigsManager(apiConfigs, appPreferencesStore, dispatchers)
} else {
ProdApiConfigsManager(apiConfigs)
return when {
BuildConfig.BUILD_TYPE == MOCKED_BUILD_TYPE -> MockApiConfigsManager(apiConfigs, dispatchers)
BuildConfig.TESTER_MENU_ENABLED -> DevApiConfigsManager(apiConfigs, appPreferencesStore, dispatchers)
else -> ProdApiConfigsManager(apiConfigs)
}
}

View file

@ -8,8 +8,10 @@ import com.squareup.moshi.Moshi
import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO
import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO
import com.tangem.datasource.local.datastore.RuntimeDataStore
import com.tangem.datasource.local.datastore.RuntimeSharedStore
import com.tangem.datasource.local.token.*
import com.tangem.datasource.local.token.DefaultStakingActionsStore
import com.tangem.datasource.local.token.DefaultStakingYieldsStore
import com.tangem.datasource.local.token.StakingActionsStore
import com.tangem.datasource.local.token.StakingYieldsStore
import com.tangem.datasource.utils.MoshiDataStoreSerializer
import com.tangem.datasource.utils.listTypes
import com.tangem.datasource.utils.mapWithStringKeyTypes
@ -66,17 +68,6 @@ internal object StakingStoreModule {
)
}
@Provides
@Singleton
fun provideStakingBalanceStore(
persistenceStore: DataStore<Map<String, Set<YieldBalanceWrapperDTO>>>,
): StakingBalanceStore {
return DefaultStakingBalanceStore(
persistenceStore = persistenceStore,
runtimeStore = RuntimeSharedStore(),
)
}
@Provides
@Singleton
fun provideStakingActionsStore(): StakingActionsStore {

View file

@ -1,269 +0,0 @@
package com.tangem.datasource.local.token
import androidx.datastore.core.DataStore
import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO
import com.tangem.datasource.local.datastore.RuntimeSharedStore
import com.tangem.datasource.local.token.StakingBalanceStore.StakingID
import com.tangem.datasource.local.token.converter.YieldBalanceConverter
import com.tangem.domain.models.StatusSource
import com.tangem.domain.staking.model.stakekit.YieldBalance
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.extensions.addOrReplace
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
internal typealias YieldBalanceWrappersDTO = Map<String, Set<YieldBalanceWrapperDTO>>
internal typealias YieldBalanceListByWalletId = Map<UserWalletId, Set<YieldBalance>>
/**
* Default implementation of [StakingBalanceStore]
*
* @property persistenceStore persistence store
* @property runtimeStore runtime store
*/
internal class DefaultStakingBalanceStore(
private val persistenceStore: DataStore<YieldBalanceWrappersDTO>,
private val runtimeStore: RuntimeSharedStore<YieldBalanceListByWalletId>,
) : StakingBalanceStore {
override fun get(userWalletId: UserWalletId, stakingIds: List<StakingID>): Flow<Set<YieldBalance>> = channelFlow {
val cachedBalances = persistenceStore.data
.map {
val wrappers = it[userWalletId.stringValue].orEmpty()
.filter { wrapper ->
stakingIds.any { id ->
id.address == wrapper.addresses.address && id.integrationId == wrapper.integrationId
}
}
YieldBalanceConverter(isCached = true).convertSet(input = wrappers)
}
.firstOrNull()
.orEmpty()
if (cachedBalances.isNotEmpty()) {
send(cachedBalances)
}
runtimeStore.get()
.map {
it[userWalletId].orEmpty().filter { balance ->
stakingIds.any { id ->
id.address == balance.address && id.integrationId == balance.integrationId
}
}
.toSet()
}
.onEach {
val mergedBalances = mergeYieldBalances(
stakingIds = stakingIds,
cachedBalances = cachedBalances,
runtimeBalances = it,
)
send(mergedBalances)
}
.launchIn(scope = this)
}
override fun get(userWalletId: UserWalletId, stakingID: StakingID): Flow<YieldBalance?> {
return get(userWalletId = userWalletId, stakingIds = listOf(stakingID)).map { balances ->
balances.getBalance(stakingID = stakingID)
}
}
override suspend fun getSyncOrNull(userWalletId: UserWalletId): Set<YieldBalance>? {
val runtimeBalances = runtimeStore.getSyncOrNull()?.getValue(userWalletId).orEmpty()
val cachedBalances = persistenceStore.data.firstOrNull()?.get(userWalletId.stringValue).orEmpty()
if (runtimeBalances.isEmpty() && cachedBalances.isEmpty()) return null
return cachedBalances.mapTo(hashSetOf()) {
val cached = YieldBalanceConverter(source = StatusSource.ONLY_CACHE).convert(value = it)
val runtime = runtimeBalances.getBalance(address = cached.address, integrationId = cached.integrationId)
if (runtime == null || runtime is YieldBalance.Error) cached else runtime
}
}
override suspend fun getSyncOrNull(userWalletId: UserWalletId, stakingIds: List<StakingID>): Set<YieldBalance>? {
val runtime = runtimeStore.getSyncOrNull()?.getValue(userWalletId)
val cached = persistenceStore.data.firstOrNull()?.get(userWalletId.stringValue)
if (runtime.isNullOrEmpty() && cached.isNullOrEmpty()) return null
return mergeYieldBalances(
cachedBalances = YieldBalanceConverter(source = StatusSource.ONLY_CACHE)
.convertSet(input = cached.orEmpty()),
runtimeBalances = runtime.orEmpty(),
stakingIds = stakingIds,
)
}
override suspend fun getSyncOrNull(userWalletId: UserWalletId, stakingID: StakingID): YieldBalance? {
val balances = getSyncOrNull(userWalletId = userWalletId, stakingIds = listOf(stakingID)) ?: return null
return balances.getBalance(stakingID = stakingID)
}
override suspend fun store(userWalletId: UserWalletId, items: Set<YieldBalanceWrapperDTO>) {
coroutineScope {
launch {
val newBalances = YieldBalanceConverter(isCached = false).convertSet(input = items)
runtimeStore.update(default = emptyMap()) { saved ->
saved.toMutableMap().apply {
this[userWalletId] = saved[userWalletId]
?.addOrReplace(newBalances) { old, new ->
old.integrationId == new.integrationId && old.address == new.address
}
?: newBalances
}
}
}
launch { storeInPersistenceStore(userWalletId = userWalletId, items = items) }
}
}
override suspend fun refresh(userWalletId: UserWalletId, stakingIds: List<StakingID>) {
updateRuntimeStore(userWalletId = userWalletId) { saved ->
saved.mapTo(hashSetOf()) {
val yieldBalance = it.takeIf { balance ->
stakingIds.any { id -> balance.integrationId == id.integrationId && balance.address == id.address }
}
yieldBalance?.copySealed(source = StatusSource.CACHE) ?: it
}
}
}
override suspend fun store(userWalletId: UserWalletId, stakingID: StakingID, item: YieldBalanceWrapperDTO) {
coroutineScope {
launch {
storeInRuntimeStore(
userWalletId = userWalletId,
integrationId = stakingID.integrationId,
address = stakingID.address,
item = item,
)
storeInPersistenceStore(
userWalletId = userWalletId,
integrationId = stakingID.integrationId,
address = stakingID.address,
item = item,
)
}
}
}
override suspend fun storeSingleYieldBalance(userWalletId: UserWalletId, item: YieldBalance) {
runtimeStore.update(default = emptyMap()) { saved ->
saved.toMutableMap().apply {
this[userWalletId] = saved[userWalletId]
?.addOrReplace(item) { it.integrationId == item.integrationId && it.address == item.address }
?: setOf(item)
}
}
}
private suspend fun storeInRuntimeStore(
userWalletId: UserWalletId,
integrationId: String,
address: String,
item: YieldBalanceWrapperDTO,
) {
val newBalance = YieldBalanceConverter(isCached = false).convert(value = item)
runtimeStore.update(default = emptyMap()) { saved ->
saved.toMutableMap().apply {
this[userWalletId] = saved[userWalletId]
?.addOrReplace(newBalance) { it.integrationId == integrationId && it.address == address }
?: setOf(newBalance)
}
}
}
private suspend fun updateRuntimeStore(
userWalletId: UserWalletId,
function: (Set<YieldBalance>) -> Set<YieldBalance>,
) {
runtimeStore.update(default = emptyMap()) { saved ->
saved.toMutableMap().apply {
this[userWalletId] = function(this[userWalletId].orEmpty())
}
}
}
private suspend fun storeInPersistenceStore(userWalletId: UserWalletId, items: Set<YieldBalanceWrapperDTO>) {
persistenceStore.updateData { current ->
current.toMutableMap().apply {
this[userWalletId.stringValue] = current[userWalletId.stringValue]
?.addOrReplace(items = items) { old, new ->
old.integrationId == new.integrationId && old.addresses.address == new.addresses.address
}
?: items
}
}
}
private suspend fun storeInPersistenceStore(
userWalletId: UserWalletId,
integrationId: String,
address: String,
item: YieldBalanceWrapperDTO,
) {
persistenceStore.updateData { current ->
current.toMutableMap().apply {
this[userWalletId.stringValue] = current[userWalletId.stringValue]
?.addOrReplace(item) { it.integrationId == integrationId && it.addresses.address == address }
?: setOf(item)
}
}
}
private fun mergeYieldBalances(
cachedBalances: Set<YieldBalance>,
runtimeBalances: Set<YieldBalance>,
stakingIds: List<StakingID>,
): Set<YieldBalance> {
return stakingIds.mapTo(hashSetOf()) { id ->
val runtime = runtimeBalances.getBalance(stakingID = id)
if (runtime == null || runtime is YieldBalance.Error) {
getCachedBalanceIfPossible(cachedBalances = cachedBalances, stakingID = id)
} else {
runtime
}
}
}
private fun getCachedBalanceIfPossible(cachedBalances: Set<YieldBalance>, stakingID: StakingID): YieldBalance {
val cached = cachedBalances.getBalance(stakingID)
?: return YieldBalance.Error(integrationId = stakingID.address, address = stakingID.integrationId)
val updatedCached = when (cached) {
is YieldBalance.Data -> cached.copy(source = StatusSource.ONLY_CACHE)
is YieldBalance.Empty -> cached.copy(source = StatusSource.ONLY_CACHE)
is YieldBalance.Error,
is YieldBalance.Unsupported,
-> null
}
return updatedCached ?: YieldBalance.Error(integrationId = stakingID.address, address = stakingID.integrationId)
}
private fun Set<YieldBalance>.getBalance(stakingID: StakingID): YieldBalance? {
return getBalance(address = stakingID.address, integrationId = stakingID.integrationId)
}
private fun Set<YieldBalance>.getBalance(address: String?, integrationId: String?): YieldBalance? {
return firstOrNull { yieldBalance ->
val isCorrectAddress = address != null && address == yieldBalance.address
val isCorrectIntegration = integrationId != null && yieldBalance.integrationId == integrationId
isCorrectIntegration && isCorrectAddress
}
}
}

View file

@ -1,40 +0,0 @@
package com.tangem.datasource.local.token
import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO
import com.tangem.domain.staking.model.stakekit.YieldBalance
import com.tangem.domain.staking.model.stakekit.YieldBalanceList
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.Flow
/** Staking balance store */
interface StakingBalanceStore {
/** Get flow of [YieldBalanceList] by [userWalletId] and [stakingIds] */
fun get(userWalletId: UserWalletId, stakingIds: List<StakingID>): Flow<Set<YieldBalance>>
/** Get flow of [YieldBalance] by [userWalletId] and [stakingID] */
fun get(userWalletId: UserWalletId, stakingID: StakingID): Flow<YieldBalance?>
/** Get all [YieldBalance] synchronously or null by [userWalletId] */
suspend fun getSyncOrNull(userWalletId: UserWalletId): Set<YieldBalance>?
/** Get [YieldBalanceList] synchronously or null by [userWalletId] and [stakingIds] */
suspend fun getSyncOrNull(userWalletId: UserWalletId, stakingIds: List<StakingID>): Set<YieldBalance>?
/** Get [YieldBalance] synchronously or null by [userWalletId] and [stakingID] */
suspend fun getSyncOrNull(userWalletId: UserWalletId, stakingID: StakingID): YieldBalance?
/** Store [items] by [userWalletId] */
suspend fun store(userWalletId: UserWalletId, items: Set<YieldBalanceWrapperDTO>)
/** Store [item] by [userWalletId] and [stakingID] */
suspend fun store(userWalletId: UserWalletId, stakingID: StakingID, item: YieldBalanceWrapperDTO)
/** Store [item] by [userWalletId] */
suspend fun storeSingleYieldBalance(userWalletId: UserWalletId, item: YieldBalance)
/** Refresh balances of [stakingIds] by [userWalletId] */
suspend fun refresh(userWalletId: UserWalletId, stakingIds: List<StakingID>)
data class StakingID(val integrationId: String, val address: String)
}

View file

@ -5,6 +5,7 @@ import com.chuckerteam.chucker.api.ChuckerInterceptor
import com.tangem.datasource.BuildConfig
import com.tangem.datasource.api.common.SwitchEnvironmentInterceptor
import com.tangem.datasource.api.common.config.ApiConfig
import com.tangem.datasource.api.common.config.ApiConfig.Companion.MOCKED_BUILD_TYPE
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
import com.tangem.datasource.api.common.createNetworkLoggingInterceptor
import com.tangem.datasource.api.utils.ConnectTimeout
@ -93,7 +94,7 @@ internal fun OkHttpClient.Builder.applyApiConfig(
id: ApiConfig.ID,
apiConfigsManager: ApiConfigsManager,
): OkHttpClient.Builder {
return if (BuildConfig.TESTER_MENU_ENABLED) {
return if (BuildConfig.TESTER_MENU_ENABLED || BuildConfig.BUILD_TYPE == MOCKED_BUILD_TYPE) {
addInterceptor(
interceptor = SwitchEnvironmentInterceptor(
id = id,