Updated on 2026-08-14

This commit is contained in:
Tangem 2025-08-22 10:09:11 +02:00
commit e4754fed3b
483 changed files with 9315 additions and 4084 deletions

View file

@ -8,12 +8,12 @@ interface AuthProvider {
/**
* Returns authToken for tangem tech api
*/
fun getCardPublicKey(): String
suspend fun getCardPublicKey(): String
fun getCardId(): String
suspend fun getCardId(): String
/**
* Returns map where keys(cardId) associated with cardPublicKey
*/
fun getCardsPublicKeys(): Map<String, String>
suspend fun getCardsPublicKeys(): Map<String, String>
}

View file

@ -15,12 +15,14 @@ import okio.IOException
* Switch api environment [Interceptor]
*
* @property id api config id [ApiConfig.ID]
* @property baseUrls base urls for all api config environments
* @property apiConfigsManager api configs manager
*
[REDACTED_AUTHOR]
*/
internal class SwitchEnvironmentInterceptor(
private val id: ApiConfig.ID,
private val baseUrls: Set<String>,
private val apiConfigsManager: ApiConfigsManager,
) : Interceptor {
@ -39,10 +41,13 @@ internal class SwitchEnvironmentInterceptor(
return chain.proceed(request)
}
private fun HttpUrl.adjustBaseUrl(url: String): HttpUrl {
return this.newBuilder()
.host(host = url.toHttpUrl().host)
.build()
private fun HttpUrl.adjustBaseUrl(newBaseUrl: String): HttpUrl {
val currentUrl = this.toString()
val currentBaseUrl = baseUrls.first { currentUrl.contains(it) }
return currentUrl
.replace(oldValue = currentBaseUrl, newValue = newBaseUrl)
.toHttpUrl()
}
private fun Request.Builder.addHeaders(headers: Map<String, ProviderSuspend<String>>): Request.Builder {

View file

@ -4,6 +4,7 @@ import com.tangem.datasource.api.common.blockaid.models.request.DomainScanReques
import com.tangem.datasource.api.common.blockaid.models.request.EvmTransactionScanRequest
import com.tangem.datasource.api.common.blockaid.models.request.SolanaTransactionScanRequest
import com.tangem.datasource.api.common.blockaid.models.response.DomainScanResponse
import com.tangem.datasource.api.common.blockaid.models.response.SolanaTransactionResponse
import com.tangem.datasource.api.common.blockaid.models.response.TransactionScanResponse
import retrofit2.http.Body
import retrofit2.http.POST
@ -17,5 +18,5 @@ interface BlockAidApi {
suspend fun scanJsonRpc(@Body request: EvmTransactionScanRequest): TransactionScanResponse
@POST("solana/message/scan")
suspend fun scanSolanaMessage(@Body request: SolanaTransactionScanRequest): TransactionScanResponse
suspend fun scanSolanaMessage(@Body request: SolanaTransactionScanRequest): SolanaTransactionResponse
}

View file

@ -7,7 +7,7 @@ import com.tangem.datasource.api.common.blockaid.models.response.TransactionMeta
@JsonClass(generateAdapter = true)
data class SolanaTransactionScanRequest(
@Json(name = "encoding") val encoding: String = "base64",
@Json(name = "chain") val chain: String,
@Json(name = "blockchain") val blockchain: String,
@Json(name = "method") val method: String,
@Json(name = "options") val options: List<String> = listOf("simulation", "validation"),
@Json(name = "metadata") val metadata: TransactionMetadata,

View file

@ -16,6 +16,7 @@ data class Asset(
@Json(name = "chain_id") val chainId: Int? = null,
@Json(name = "logo_url") val logoUrl: String? = null,
@Json(name = "symbol") val symbol: String? = null,
@Json(name = "name") val name: String? = null,
@Json(name = "decimals") val decimals: Int? = null,
)

View file

@ -5,6 +5,7 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class Exposure(
@Json(name = "asset_type") val assetType: String,
@Json(name = "asset") val asset: Asset,
@Json(name = "spenders") val spenders: Map<String, SpenderDetails>,
)

View file

@ -0,0 +1,58 @@
package com.tangem.datasource.api.common.blockaid.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class SolanaTransactionResponse(
@Json(name = "result") val result: SolanaTransactionResult,
)
@JsonClass(generateAdapter = true)
data class SolanaTransactionResult(
@Json(name = "validation") val validation: SolanaTransactionValidation,
@Json(name = "simulation") val simulation: SolanaTransactionSimulation? = null,
)
@JsonClass(generateAdapter = true)
data class SolanaTransactionValidation(
@Json(name = "result_type") val resultType: String,
@Json(name = "description") val description: String?,
)
@JsonClass(generateAdapter = true)
data class SolanaTransactionSimulation(
@Json(name = "account_summary") val accountSummary: SolanaTransactionAccountSummary,
@Json(name = "error") val error: String? = null,
@Json(name = "error_details") val errorDetails: String? = null,
)
@JsonClass(generateAdapter = true)
data class SolanaTransactionAccountSummary(
@Json(name = "account_assets_diff")
val accountAssetsDiff: List<SolanaTransactionAssetDiff>,
)
@JsonClass(generateAdapter = true)
data class SolanaTransactionAssetDiff(
@Json(name = "asset_type") val assetType: String,
@Json(name = "asset") val asset: SolanaTransactionAsset,
@Json(name = "in") val inTransfer: SolanaTransferDetail? = null,
@Json(name = "out") val outTransfer: SolanaTransferDetail? = null,
)
@JsonClass(generateAdapter = true)
data class SolanaTransactionAsset(
@Json(name = "address") val address: String? = null,
@Json(name = "symbol") val symbol: String? = null,
@Json(name = "name") val name: String? = null,
@Json(name = "decimals") val decimals: Int? = null,
@Json(name = "type") val type: String? = null,
@Json(name = "logo") val logoUrl: String? = null,
)
@JsonClass(generateAdapter = true)
data class SolanaTransferDetail(
@Json(name = "value") val amount: String? = null,
@Json(name = "summary") val summary: String? = null,
)

View file

@ -1,5 +1,6 @@
package com.tangem.datasource.api.common.config
import com.tangem.datasource.BuildConfig
import com.tangem.lib.auth.StakeKitAuthProvider
import com.tangem.utils.ProviderSuspend
@ -14,20 +15,44 @@ internal class StakeKit(
private val stakeKitAuthProvider: StakeKitAuthProvider,
) : ApiConfig() {
override val defaultEnvironment: ApiEnvironment = ApiEnvironment.PROD
override val defaultEnvironment: ApiEnvironment = getInitialEnvironment()
override val environmentConfigs: List<ApiEnvironmentConfig> = listOf(
createProdEnvironment(),
createMockEnvironment(),
)
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 createProdEnvironment(): ApiEnvironmentConfig {
return ApiEnvironmentConfig(
environment = ApiEnvironment.PROD,
baseUrl = "https://api.stakek.it/v1/",
headers = mapOf(
"X-API-KEY" to ProviderSuspend(stakeKitAuthProvider::getApiKey),
"accept" to ProviderSuspend { "application/json" },
),
headers = createHeaders(),
)
}
private fun createMockEnvironment(): ApiEnvironmentConfig {
return ApiEnvironmentConfig(
environment = ApiEnvironment.MOCK,
baseUrl = "[REDACTED_ENV_URL]",
headers = createHeaders(),
)
}
private fun createHeaders() = buildMap {
put(key = "X-API-KEY", value = ProviderSuspend(stakeKitAuthProvider::getApiKey))
put(key = "accept", value = ProviderSuspend { "application/json" })
}
}

View file

@ -1,19 +1,7 @@
package com.tangem.datasource.api.pay
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.pay.models.request.ActivationByCardWalletRequest
import com.tangem.datasource.api.pay.models.request.ActivationByCustomerWalletRequest
import com.tangem.datasource.api.pay.models.request.ActivationStatusRequest
import com.tangem.datasource.api.pay.models.request.ExchangeAccessTokenRequest
import com.tangem.datasource.api.pay.models.request.GenerateNoneByCardIdRequest
import com.tangem.datasource.api.pay.models.request.GenerateNoneByCardWalletRequest
import com.tangem.datasource.api.pay.models.request.GetAccessTokenByCardIdRequest
import com.tangem.datasource.api.pay.models.request.GetAccessTokenByCardWalletRequest
import com.tangem.datasource.api.pay.models.request.GetCardWalletAcceptanceRequest
import com.tangem.datasource.api.pay.models.request.GetCustomerWalletAcceptanceRequest
import com.tangem.datasource.api.pay.models.request.RefreshTokenByCardIdRequest
import com.tangem.datasource.api.pay.models.request.RefreshTokenByCardWalletRequest
import com.tangem.datasource.api.pay.models.request.SetPinCodeRequest
import com.tangem.datasource.api.pay.models.request.*
import com.tangem.datasource.api.pay.models.response.*
import retrofit2.http.Body
import retrofit2.http.GET
@ -33,9 +21,17 @@ interface TangemPayApi {
@Body request: GenerateNoneByCardWalletRequest,
): ApiResponse<GenerateNonceResponse>
@POST("v1/auth/challenge")
suspend fun generateNonceByCustomerWallet(
@Body request: GenerateNonceByCustomerWalletRequest,
): ApiResponse<GenerateNonceResponse>
@POST("v1/auth/token")
suspend fun getAccessTokenByCardId(@Body request: GetAccessTokenByCardIdRequest): ApiResponse<JWTResponse>
@POST("v1/auth/token")
suspend fun getTokenByCustomerWallet(@Body request: GetTokenByCustomerWalletRequest): ApiResponse<JWTResponse>
@POST("v1/auth/token")
suspend fun getAccessTokenByCardWallet(@Body request: GetAccessTokenByCardWalletRequest): ApiResponse<JWTResponse>

View file

@ -0,0 +1,10 @@
package com.tangem.datasource.api.pay.models.request
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class GenerateNonceByCustomerWalletRequest(
@Json(name = "auth_type") val authType: String = "customer_wallet",
@Json(name = "customer_wallet_address") val customerWalletAddress: String,
)

View file

@ -0,0 +1,12 @@
package com.tangem.datasource.api.pay.models.request
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class GetTokenByCustomerWalletRequest(
@Json(name = "auth_type") val authType: String = "customer_wallet",
@Json(name = "session_id") val sessionId: String,
@Json(name = "signature") val signature: String,
@Json(name = "message_format") val messageFormat: String,
)

View file

@ -1,9 +1,11 @@
package com.tangem.datasource.api.tangemTech
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.promotion.models.PromotionInfoResponse
import com.tangem.datasource.api.promotion.models.StoryContentResponse
import com.tangem.datasource.api.tangemTech.models.*
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
import com.tangem.datasource.api.tangemTech.models.account.GetWalletArchivedAccountsResponse
import com.tangem.datasource.api.tangemTech.models.account.SaveWalletAccountsResponse
import com.tangem.datasource.api.utils.ReadTimeout
import com.tangem.datasource.local.config.providers.models.ProviderModel
import retrofit2.http.*
@ -30,9 +32,6 @@ interface TangemTechApi {
@Query("limit") limit: Int? = null,
): ApiResponse<CoinsResponse>
@GET("v1/rates")
suspend fun getRates(@Query("currencyId") currencyId: String, @Query("coinIds") coinIds: String): RatesResponse
@GET("v1/currencies")
suspend fun getCurrencyList(
@Header("Cache-Control") cacheControl: String = "max-age=600",
@ -68,46 +67,11 @@ interface TangemTechApi {
@Query("fields") fields: String,
): ApiResponse<QuotesResponse>
@GET("v1/promotion")
suspend fun getPromotionInfo(
@Query("programName") name: String,
@Header("Cache-Control") cacheControl: String = "max-age=600",
): ApiResponse<PromotionInfoResponse>
@GET("v1/settings/{wallet_id}")
suspend fun getUserTokensSettings(@Path("wallet_id") walletId: String): ApiResponse<UserTokensSettingsResponse>
@PUT("v1/settings/{wallet_id}")
suspend fun saveUserTokensSettings(
@Path("wallet_id") walletId: String,
@Body userTokensSettings: UserTokensSettingsResponse,
): ApiResponse<Unit>
@POST("v1/user-network-account")
suspend fun createUserNetworkAccount(
@Body body: CreateUserNetworkAccountBody,
): ApiResponse<CreateUserNetworkAccountResponse>
@POST("v1/account")
suspend fun createUserTokensAccount(
@Body body: CreateUserTokensAccountBody,
): ApiResponse<UserTokensAccountResponse>
@PUT("v1/account/{account_id}")
suspend fun updateUserTokensAccount(
@Path("account_id") accountId: Int,
@Body body: UpdateUserTokensAccountBody,
): ApiResponse<UserTokensAccountResponse>
@PUT("v1/account/{account_id}/archive")
suspend fun archiveUserTokensAccount(@Path("account_id") accountId: Int): ApiResponse<UserTokensAccountResponse>
@PUT("v1/account/{account_id}/unarchive")
suspend fun restoreUserTokensAccount(@Path("account_id") accountId: Int): ApiResponse<UserTokensAccountResponse>
@GET("v1/features")
suspend fun getFeatures(): ApiResponse<FeaturesResponse>
@ReadTimeout(duration = 5, unit = TimeUnit.SECONDS)
@GET("v1/networks/providers")
suspend fun getBlockchainProviders(): Map<String, List<ProviderModel>>
@ -160,7 +124,7 @@ interface TangemTechApi {
suspend fun setNotificationsEnabled(@Path("wallet_id") walletId: String, @Body body: WalletBody): ApiResponse<Unit>
// endregion
// region wallets
// region user-wallets
@PATCH("v1/user-wallets/wallets/{wallet_id}")
suspend fun updateWallet(@Path("wallet_id") walletId: String, @Body body: WalletBody): ApiResponse<Unit>
@ -176,4 +140,20 @@ interface TangemTechApi {
@GET("v1/user-wallets/wallets/by-app/{app_id}")
suspend fun getWallets(@Path("app_id") appId: String): ApiResponse<List<WalletResponse>>
// endregion
// region account
@GET("/v1/wallets/{walletId}/accounts")
suspend fun getWalletAccounts(@Path("walletId") walletId: String): ApiResponse<GetWalletAccountsResponse>
@PUT("/v1/wallets/{walletId}/accounts")
suspend fun saveWalletAccounts(
@Path("walletId") walletId: String,
@Header("If-Match") ifMatch: String,
): ApiResponse<SaveWalletAccountsResponse>
@GET("/v1/wallets/{walletId}/accounts/archived")
suspend fun getWalletArchivedAccounts(
@Path("walletId") walletId: String,
): ApiResponse<GetWalletArchivedAccountsResponse>
// endregion
}

View file

@ -16,6 +16,7 @@ data class UserTokensResponse(
@JsonClass(generateAdapter = true)
data class Token(
@Json(name = "id") val id: String? = null,
@Json(name = "accountId") val accountId: String? = null,
@Json(name = "networkId") val networkId: String,
@Json(name = "derivationPath") val derivationPath: String? = null,
@Json(name = "name") val name: String,

View file

@ -0,0 +1,23 @@
package com.tangem.datasource.api.tangemTech.models.account
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse.GroupType
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse.SortType
@JsonClass(generateAdapter = true)
data class GetWalletAccountsResponse(
@Json(name = "wallet") val wallet: Wallet,
@Json(name = "accounts") val accounts: List<WalletAccountDTO>,
@Json(name = "unassignedTokens") val unassignedTokens: List<UserTokensResponse.Token>,
) {
@JsonClass(generateAdapter = true)
data class Wallet(
@Json(name = "version") val version: Int,
@Json(name = "group") val group: GroupType,
@Json(name = "sort") val sort: SortType,
@Json(name = "totalAccounts") val totalAccounts: Int,
)
}

View file

@ -0,0 +1,9 @@
package com.tangem.datasource.api.tangemTech.models.account
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class GetWalletArchivedAccountsResponse(
@Json(name = "archivedAccounts") val accounts: List<WalletAccountDTO>,
)

View file

@ -0,0 +1,9 @@
package com.tangem.datasource.api.tangemTech.models.account
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class SaveWalletAccountsResponse(
@Json(name = "accounts") val accounts: List<WalletAccountDTO>,
)

View file

@ -0,0 +1,17 @@
package com.tangem.datasource.api.tangemTech.models.account
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
@JsonClass(generateAdapter = true)
data class WalletAccountDTO(
@Json(name = "id") val id: String,
@Json(name = "name") val name: String,
@Json(name = "derivation") val derivationIndex: Int,
@Json(name = "icon") val icon: String,
@Json(name = "iconColor") val iconColor: String,
@Json(name = "tokens") val tokens: List<UserTokensResponse.Token>? = null,
@Json(name = "totalTokens") val totalTokens: Int? = null,
@Json(name = "totalNetworks") val totalNetworks: Int? = null,
)

View file

@ -7,7 +7,7 @@ import com.tangem.core.analytics.api.AnalyticsErrorHandler
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.ApiConfigs
import com.tangem.datasource.api.common.config.ApiEnvironmentConfig
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
import com.tangem.datasource.api.common.createNetworkLoggingInterceptor
@ -42,6 +42,7 @@ import javax.inject.Singleton
*/
@Singleton
internal class RetrofitApiBuilder @Inject constructor(
private val apiConfigs: ApiConfigs,
private val apiConfigsManager: ApiConfigsManager,
@NetworkMoshi private val moshi: Moshi,
private val analyticsErrorHandler: AnalyticsErrorHandler,
@ -49,6 +50,8 @@ internal class RetrofitApiBuilder @Inject constructor(
private val appLogsStore: AppLogsStore,
) {
private val configsBaseUrls: Map<ApiConfig.ID, Set<String>> = getConfigsBaseUrls()
/**
* Builds a Retrofit API instance for the specified API configuration ID
*
@ -95,13 +98,26 @@ internal class RetrofitApiBuilder @Inject constructor(
val writeTimeoutSeconds: Long? = null,
)
private fun getConfigsBaseUrls(): Map<ApiConfig.ID, Set<String>> {
return apiConfigs.associate { config ->
val allBaseUrls = config.environmentConfigs.mapTo(hashSetOf(), ApiEnvironmentConfig::baseUrl)
config.id to allBaseUrls
}
}
private fun OkHttpClient.Builder.applyApiConfig(
apiConfigId: ApiConfig.ID,
environmentConfig: ApiEnvironmentConfig,
): OkHttpClient.Builder {
return if (BuildConfig.TESTER_MENU_ENABLED || BuildConfig.BUILD_TYPE == MOCKED_BUILD_TYPE) {
return if (BuildConfig.TESTER_MENU_ENABLED) {
addInterceptor(
interceptor = SwitchEnvironmentInterceptor(id = apiConfigId, apiConfigsManager = apiConfigsManager),
interceptor = SwitchEnvironmentInterceptor(
id = apiConfigId,
baseUrls = configsBaseUrls[apiConfigId]
?: error("Base URLs for ApiConfig with id [$apiConfigId] not found"),
apiConfigsManager = apiConfigsManager,
),
)
} else {
val headers = environmentConfig.headers

View file

@ -84,6 +84,10 @@ object PreferencesKeys {
val SHOULD_SAVE_ACCESS_CODES_KEY by lazy { booleanPreferencesKey(name = "saveAccessCodes") }
val REQUIRE_ACCESS_CODE_KEY by lazy { booleanPreferencesKey(name = "requireAccessCode") }
val USE_BIOMETRIC_AUTHENTICATION_KEY by lazy { booleanPreferencesKey(name = "useBiometricAuthentication") }
val SHOULD_SHOW_MARKETS_TOOLTIP_KEY by lazy { booleanPreferencesKey(name = "shouldShowMarketsTooltip") }
val MARKETS_STAKING_NOTIFICATION_HIDE_CLICKED_KEY by lazy {
@ -165,6 +169,18 @@ object PreferencesKeys {
fun getShouldShowInitialPermissionScreen(permission: String) =
booleanPreferencesKey("shouldShowInitialPushPermissionScreen_$permission")
// endregion
// region Hot Wallet unlock attempts
fun getHotWalletUnlockAttemptsKey(attemptId: String) =
intPreferencesKey(name = "hotWalletUnlockAttempts_$attemptId")
fun getHotWalletUnlockBootKey(attemptId: String) = intPreferencesKey(name = "hotWalletUnlockBootCount_$attemptId")
fun getHotWalletUnlockDeadlineKey(attemptId: String) =
longPreferencesKey(name = "hotWalletUnlockDeadline_$attemptId")
// endregion
}
/** Preferences keys set that should be migrated from "PreferencesDataSource" to a new DataStore<Preferences> */

View file

@ -5,6 +5,10 @@ import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import kotlinx.coroutines.flow.Flow
@Deprecated(
message = "Use UserWalletsListRepository instead",
replaceWith = ReplaceWith("UserWalletsListRepository"),
)
interface UserWalletsStore {
val selectedUserWalletOrNull: UserWallet?
@ -15,8 +19,6 @@ interface UserWalletsStore {
fun getSyncStrict(key: UserWalletId): UserWallet
suspend fun getAllSyncOrNull(): List<UserWallet>?
suspend fun update(
userWalletId: UserWalletId,
update: suspend (UserWallet) -> UserWallet,

View file

@ -18,6 +18,7 @@ import com.tangem.utils.ProviderSuspend
import com.tangem.utils.info.AppInfoProvider
import com.tangem.utils.version.AppVersionProvider
import io.mockk.clearMocks
import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.runBlocking
@ -55,8 +56,8 @@ internal class ProdApiConfigsManagerTest {
every { appVersionProvider.versionName } returns VERSION_NAME
every { expressAuthProvider.getSessionId() } returns EXPRESS_SESSION_ID
every { stakeKitAuthProvider.getApiKey() } returns STAKE_KIT_API_KEY
every { appAuthProvider.getCardId() } returns APP_CARD_ID
every { appAuthProvider.getCardPublicKey() } returns APP_CARD_PUBLIC_KEY
coEvery { appAuthProvider.getCardId() } returns APP_CARD_ID
coEvery { appAuthProvider.getCardPublicKey() } returns APP_CARD_PUBLIC_KEY
every { appInfoProvider.osVersion } returns "Android 16"
}