Updated on 2026-08-14

This commit is contained in:
Tangem 2025-09-24 12:36:51 +03:00
commit 808cd4a301
503 changed files with 12306 additions and 3036 deletions

View file

@ -2,12 +2,15 @@ package com.tangem.datasource.api.common.config
import com.tangem.datasource.BuildConfig
import com.tangem.datasource.api.common.AuthProvider
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
import com.tangem.datasource.utils.RequestHeader
import com.tangem.utils.ProviderSuspend
import com.tangem.utils.info.AppInfoProvider
import com.tangem.utils.version.AppVersionProvider
/** TangemTech [ApiConfig] */
internal class TangemTech(
private val environmentConfigStorage: EnvironmentConfigStorage,
private val appVersionProvider: AppVersionProvider,
private val authProvider: AuthProvider,
private val appInfoProvider: AppInfoProvider,
@ -38,29 +41,41 @@ internal class TangemTech(
private fun createDevEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
environment = ApiEnvironment.DEV,
baseUrl = "[REDACTED_ENV_URL]",
headers = createHeaders(),
headers = createHeaders(ApiEnvironment.DEV),
)
private fun createStageEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
environment = ApiEnvironment.STAGE,
baseUrl = "[REDACTED_ENV_URL]",
headers = createHeaders(),
headers = createHeaders(ApiEnvironment.STAGE),
)
private fun createMockedEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
environment = ApiEnvironment.MOCK,
baseUrl = "[REDACTED_ENV_URL]",
headers = createHeaders(),
headers = createHeaders(ApiEnvironment.MOCK),
)
private fun createProdEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
environment = ApiEnvironment.PROD,
baseUrl = "https://api.tangem.org/",
headers = createHeaders(),
headers = createHeaders(ApiEnvironment.PROD),
)
private fun createHeaders() = buildMap {
private fun createHeaders(apiEnvironment: ApiEnvironment) = buildMap {
put(key = "api-key", value = ProviderSuspend { getApiKey(apiEnvironment) })
putAll(from = RequestHeader.AppVersionPlatformHeaders(appVersionProvider, appInfoProvider).values)
putAll(from = RequestHeader.AuthenticationHeader(authProvider).values)
}
private fun getApiKey(apiEnvironment: ApiEnvironment): String {
return when (apiEnvironment) {
ApiEnvironment.MOCK -> null
ApiEnvironment.DEV,
ApiEnvironment.DEV_2,
-> environmentConfigStorage.getConfigSync().tangemApiKeyDev
ApiEnvironment.STAGE -> environmentConfigStorage.getConfigSync().tangemApiKeyStage
ApiEnvironment.PROD -> environmentConfigStorage.getConfigSync().tangemApiKey
} ?: error("No tangem tech api config provided")
}
}

View file

@ -26,10 +26,10 @@ internal class DevApiConfigsManager(
) : MutableApiConfigsManager() {
override val configs: StateFlow<Map<ApiConfig, ApiEnvironment>>
field = MutableStateFlow(value = getInitialConfigs())
field = MutableStateFlow(value = getInitialConfigs())
override val isInitialized: StateFlow<Boolean>
field = MutableStateFlow(value = false)
field = MutableStateFlow(value = false)
override fun initialize() {
isInitialized.value = false

View file

@ -22,7 +22,7 @@ internal class MockApiConfigsManager(
) : MutableApiConfigsManager() {
override val configs: StateFlow<Map<ApiConfig, ApiEnvironment>>
field = MutableStateFlow(value = getInitialConfigs())
field = MutableStateFlow(value = getInitialConfigs())
override val isInitialized: StateFlow<Boolean> = MutableStateFlow(value = true)

View file

@ -20,7 +20,7 @@ abstract class MutableApiConfigsManager : ApiConfigsManager {
* These listeners are notified whenever an environment change occurs.
*/
protected val registerListeners: Set<ApiConfigEnvChangeListener>
field = mutableSetOf<ApiConfigEnvChangeListener>()
field = mutableSetOf<ApiConfigEnvChangeListener>()
/** Change api environment [environment] by [id] */
abstract suspend fun changeEnvironment(id: String, environment: ApiEnvironment)

View file

@ -9,6 +9,8 @@ import retrofit2.http.Header
import retrofit2.http.POST
import retrofit2.http.Query
private const val TX_HISTORY_PAGING_DEFAULT_LIMIT = 20
@Suppress("TooManyFunctions")
interface TangemPayApi {
@ -107,6 +109,13 @@ interface TangemPayApi {
@Query("offset") offset: Int,
): ApiResponse<VisaTxHistoryResponse>
@GET("v1/customer/transactions")
suspend fun getTangemPayTxHistory(
@Header("Authorization") authHeader: String,
@Query("cursor") cursor: String?,
@Query("limit") limit: Int = TX_HISTORY_PAGING_DEFAULT_LIMIT,
): ApiResponse<TangemPayTxHistoryResponse>
@GET("v1/customer/kyc")
suspend fun getKycAccess(@Header("Authorization") authHeader: String): ApiResponse<KycAccessInfoResponse>

View file

@ -2,6 +2,7 @@ package com.tangem.datasource.api.pay.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import java.math.BigDecimal
@JsonClass(generateAdapter = true)
data class CustomerMeResponse(
@ -16,6 +17,8 @@ data class CustomerMeResponse(
@Json(name = "product_instance") val productInstance: ProductInstance?,
@Json(name = "payment_account") val paymentAccount: PaymentAccount?,
@Json(name = "kyc") val kyc: Kyc?,
@Json(name = "card") val card: Card?,
@Json(name = "balance") val balance: Balance?,
)
@JsonClass(generateAdapter = true)
@ -45,4 +48,25 @@ data class CustomerMeResponse(
@Json(name = "review_answer") val reviewAnswer: String,
@Json(name = "created_at") val createdAt: String,
)
@JsonClass(generateAdapter = true)
data class Card(
@Json(name = "token") val token: String,
@Json(name = "expiration_month") val expirationMonth: Int,
@Json(name = "expiration_year") val expirationYear: Int,
@Json(name = "emboss_name") val embossName: String,
@Json(name = "card_type") val cardType: String,
@Json(name = "card_status") val cardStatus: String,
@Json(name = "card_number_end") val cardNumberEnd: String,
)
@JsonClass(generateAdapter = true)
data class Balance(
@Json(name = "currency") val currency: String,
@Json(name = "available_balance") val availableBalance: BigDecimal,
@Json(name = "credit_limit") val creditLimit: BigDecimal,
@Json(name = "pending_charges") val pendingCharges: BigDecimal,
@Json(name = "posted_charges") val postedCharges: BigDecimal,
@Json(name = "balance_due") val balanceDue: BigDecimal,
)
}

View file

@ -0,0 +1,83 @@
package com.tangem.datasource.api.pay.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import org.joda.time.DateTime
import java.math.BigDecimal
@JsonClass(generateAdapter = true)
data class TangemPayTxHistoryResponse(
@Json(name = "error") val error: String?,
@Json(name = "result") val result: Result,
) {
@JsonClass(generateAdapter = true)
data class Result(
@Json(name = "transactions") val transactions: List<Transaction>,
)
@JsonClass(generateAdapter = true)
data class Transaction(
@Json(name = "id") val id: String, // UUID (used as cursor for pagination)
@Json(name = "type") val type: String, // "SPEND", "COLLATERAL", "PAYMENT", "FEE"
@Json(name = "spend") val spend: Spend? = null,
@Json(name = "collateral") val collateral: Collateral? = null,
@Json(name = "payment") val payment: Payment? = null,
@Json(name = "fee") val fee: Fee? = null,
)
@JsonClass(generateAdapter = true)
data class Spend(
@Json(name = "amount") val amount: BigDecimal,
@Json(name = "currency") val currency: String,
@Json(name = "local_amount") val localAmount: BigDecimal? = null,
@Json(name = "local_currency") val localCurrency: String? = null,
@Json(name = "authorized_amount") val authorizedAmount: BigDecimal? = null,
@Json(name = "authorization_method") val authorizationMethod: String? = null,
@Json(name = "memo") val memo: String? = null,
@Json(name = "receipt") val receipt: Boolean? = null,
@Json(name = "merchant_name") val merchantName: String? = null,
@Json(name = "merchant_category") val merchantCategory: String? = null,
@Json(name = "merchant_category_code") val merchantCategoryCode: String? = null,
@Json(name = "merchant_id") val merchantId: String? = null,
@Json(name = "enriched_merchant_icon") val enrichedMerchantIcon: String? = null,
@Json(name = "enriched_merchant_name") val enrichedMerchantName: String? = null,
@Json(name = "enriched_merchant_category") val enrichedMerchantCategory: String? = null,
@Json(name = "card_id") val cardId: String? = null,
@Json(name = "card_type") val cardType: String? = null,
@Json(name = "status") val status: String? = null,
@Json(name = "declined_reason") val declinedReason: String? = null,
@Json(name = "authorized_at") val authorizedAt: DateTime? = null,
@Json(name = "posted_at") val postedAt: DateTime? = null,
)
@JsonClass(generateAdapter = true)
data class Collateral(
@Json(name = "amount") val amount: BigDecimal,
@Json(name = "currency") val currency: String,
@Json(name = "memo") val memo: String? = null,
@Json(name = "chain_id") val chainId: Long? = null,
@Json(name = "wallet_address") val walletAddress: String? = null,
@Json(name = "transaction_hash") val transactionHash: String? = null,
@Json(name = "posted_at") val postedAt: DateTime? = null,
)
@JsonClass(generateAdapter = true)
data class Payment(
@Json(name = "amount") val amount: BigDecimal,
@Json(name = "currency") val currency: String,
@Json(name = "memo") val memo: String? = null,
@Json(name = "chain_id") val chainId: Long? = null,
@Json(name = "wallet_address") val walletAddress: String? = null,
@Json(name = "transaction_hash") val transactionHash: String? = null,
@Json(name = "status") val status: String? = null,
@Json(name = "posted_at") val postedAt: DateTime? = null,
)
@JsonClass(generateAdapter = true)
data class Fee(
@Json(name = "amount") val amount: BigDecimal,
@Json(name = "currency") val currency: String,
@Json(name = "description") val description: String? = null,
@Json(name = "posted_at") val postedAt: DateTime? = null,
)
}

View file

@ -198,6 +198,7 @@ data class YieldDTO(
enum class RewardTypeDTO {
@Json(name = "apy")
APY, // compound rate
@Json(name = "apr")
APR, // simple rate,

View file

@ -24,7 +24,5 @@ data class SeedPhraseNotificationDTO(val status: Status) {
@Json(name = "accepted")
ACCEPTED,
;
}
}

View file

@ -42,10 +42,12 @@ internal object ApiConfigsModule {
@Provides
@IntoSet
fun provideTangemTechConfig(
environmentConfigStorage: EnvironmentConfigStorage,
appVersionProvider: AppVersionProvider,
authProvider: AuthProvider,
appInfoProvider: AppInfoProvider,
): ApiConfig = TangemTech(
environmentConfigStorage = environmentConfigStorage,
appVersionProvider = appVersionProvider,
authProvider = authProvider,
appInfoProvider = appInfoProvider,

View file

@ -3,6 +3,8 @@ package com.tangem.datasource.di
import com.tangem.datasource.local.datastore.RuntimeDataStore
import com.tangem.datasource.local.txhistory.DefaultTxHistoryItemsStore
import com.tangem.datasource.local.txhistory.TxHistoryItemsStore
import com.tangem.datasource.local.visa.DefaultTangemPayTxHistoryItemsStore
import com.tangem.datasource.local.visa.TangemPayTxHistoryItemsStore
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -20,4 +22,12 @@ internal object TxHistoryItemsStoreModule {
dataStore = RuntimeDataStore(),
)
}
@Provides
@Singleton
fun provideTangemPayTxHistoryItemsStore(): TangemPayTxHistoryItemsStore {
return DefaultTangemPayTxHistoryItemsStore(
dataStore = RuntimeDataStore(),
)
}
}

View file

@ -17,4 +17,7 @@ data class EnvironmentConfig(
val devExpress: ExpressModel? = null,
val stakeKitApiKey: String? = null,
val blockAidApiKey: String? = null,
val tangemApiKey: String? = null,
val tangemApiKeyDev: String? = null,
val tangemApiKeyStage: String? = null,
)

View file

@ -26,6 +26,9 @@ internal object EnvironmentConfigConverter : Converter<EnvironmentConfigModel, E
devExpress = value.devExpress,
stakeKitApiKey = value.stakeKitApiKey,
blockAidApiKey = value.blockaidApiKey,
tangemApiKey = value.tangemApiKey,
tangemApiKeyDev = value.tangemApiKeyDev,
tangemApiKeyStage = value.tangemApiKeyStage,
)
}
}

View file

@ -40,6 +40,9 @@ class EnvironmentConfigModel(
@Json(name = "moralisApiKey") val moralisApiKey: String?,
@Json(name = "nftScanApiKey") val nftScanApiKey: String?,
@Json(name = "blockaidApiKey") val blockaidApiKey: String?,
@Json(name = "tangemApiKey") val tangemApiKey: String?,
@Json(name = "tangemApiKeyDev") val tangemApiKeyDev: String?,
@Json(name = "tangemApiKeyStage") val tangemApiKeyStage: String?,
@Json(name = "etherscanApiKey") val etherScanApiKey: String?,
)

View file

@ -129,6 +129,8 @@ object PreferencesKeys {
val WALLETS_NFT_ENABLED_STATES_KEY by lazy { stringPreferencesKey(name = "walletsNftEnabledStates") }
val YIELD_SUPPLY_WARNINGS_STATES_KEY by lazy { stringPreferencesKey(name = "yieldSupplyWarningsStates") }
// region Notifications
val NOTIFICATIONS_APPLICATION_ID_KEY by lazy { stringPreferencesKey(name = "notificationsApplicationId") }

View file

@ -0,0 +1,24 @@
package com.tangem.datasource.local.visa
import com.tangem.datasource.local.datastore.core.StringKeyDataStore
import com.tangem.datasource.local.datastore.core.StringKeyDataStoreDecorator
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.visa.model.TangemPayTxHistoryItem
internal class DefaultTangemPayTxHistoryItemsStore(
dataStore: StringKeyDataStore<Map<String, List<TangemPayTxHistoryItem>>>,
) : TangemPayTxHistoryItemsStore,
StringKeyDataStoreDecorator<UserWalletId, Map<String, List<TangemPayTxHistoryItem>>>(dataStore) {
override fun provideStringKey(key: UserWalletId): String = key.stringValue
override suspend fun getSyncOrNull(key: UserWalletId, cursor: String): List<TangemPayTxHistoryItem>? {
val storedValue = getSyncOrNull(key)
return storedValue?.get(cursor)
}
override suspend fun store(key: UserWalletId, cursor: String, value: List<TangemPayTxHistoryItem>) {
val oldValue = getSyncOrNull(key).orEmpty()
val newValue = oldValue.toMutableMap().apply { put(cursor, value) }
store(key, newValue)
}
}

View file

@ -8,9 +8,5 @@ interface TangemPayStorage {
suspend fun getAuthTokens(customerWalletAddress: String): VisaAuthTokens?
suspend fun storeCustomerWalletAddress(customerWalletAddress: String)
suspend fun getCustomerWalletAddress(): String?
suspend fun clear(customerWalletAddress: String)
}

View file

@ -0,0 +1,13 @@
package com.tangem.datasource.local.visa
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.visa.model.TangemPayTxHistoryItem
interface TangemPayTxHistoryItemsStore {
suspend fun getSyncOrNull(key: UserWalletId, cursor: String): List<TangemPayTxHistoryItem>?
suspend fun remove(key: UserWalletId)
suspend fun store(key: UserWalletId, cursor: String, value: List<TangemPayTxHistoryItem>)
}

View file

@ -39,6 +39,7 @@ class ApiConfigTest {
}
ApiConfig.ID.TangemTech -> {
TangemTech(
environmentConfigStorage = mockk(),
appVersionProvider = mockk(),
authProvider = mockk(),
appInfoProvider = mockk(),

View file

@ -86,6 +86,7 @@ internal class ProdApiConfigsManagerTest {
}
ApiConfig.ID.TangemTech -> {
TangemTech(
environmentConfigStorage = environmentConfigStorage,
appVersionProvider = appVersionProvider,
authProvider = appAuthProvider,
appInfoProvider = appInfoProvider,