Updated on 2026-08-14

This commit is contained in:
Tangem 2025-04-23 13:28:11 +05:00
parent e87d946e40
commit a3a18854c8
48 changed files with 544 additions and 328 deletions

View file

@ -48,10 +48,8 @@ internal class Express(
private fun createHeaders(isProd: Boolean) = buildMap {
put(key = "api-key", value = ProviderSuspend { getApiKey(isProd) })
put(key = "user-id", value = ProviderSuspend(expressAuthProvider::getUserId))
put(key = "session-id", value = ProviderSuspend(expressAuthProvider::getSessionId))
putAll(from = RequestHeader.AppVersionPlatformHeaders(appVersionProvider).values)
put(key = "refcode", value = ProviderSuspend(expressAuthProvider::getRefCode))
}
private fun getApiKey(isProd: Boolean): String {

View file

@ -5,10 +5,7 @@ import com.tangem.datasource.api.express.models.request.AssetsRequestBody
import com.tangem.datasource.api.express.models.request.ExchangeSentRequestBody
import com.tangem.datasource.api.express.models.request.PairsRequestBody
import com.tangem.datasource.api.express.models.response.*
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.POST
import retrofit2.http.Query
import retrofit2.http.*
/**
* Interface of Tangem Express API (new swap mechanism)
@ -17,16 +14,29 @@ import retrofit2.http.Query
interface TangemExpressApi {
@POST("assets")
suspend fun getAssets(@Body body: AssetsRequestBody): ApiResponse<List<Asset>>
suspend fun getAssets(
@Header("user-id") userWalletId: String,
@Header("refcode") refCode: String,
@Body body: AssetsRequestBody,
): ApiResponse<List<Asset>>
@POST("pairs")
suspend fun getPairs(@Body body: PairsRequestBody): ApiResponse<List<SwapPair>>
suspend fun getPairs(
@Header("user-id") userWalletId: String,
@Header("refcode") refCode: String,
@Body body: PairsRequestBody,
): ApiResponse<List<SwapPair>>
@GET("providers")
suspend fun getProviders(): ApiResponse<List<ExchangeProvider>>
suspend fun getProviders(
@Header("user-id") userWalletId: String,
@Header("refcode") refCode: String,
): ApiResponse<List<ExchangeProvider>>
@GET("exchange-quote")
suspend fun getExchangeQuote(
@Header("user-id") userWalletId: String,
@Header("refcode") refCode: String,
@Query("fromContractAddress") fromContractAddress: String,
@Query("fromNetwork") fromNetwork: String,
@Query("toContractAddress") toContractAddress: String,
@ -40,6 +50,8 @@ interface TangemExpressApi {
@GET("exchange-data")
suspend fun getExchangeData(
@Header("user-id") userWalletId: String,
@Header("refcode") refCode: String,
@Query("fromContractAddress") fromContractAddress: String,
@Query("fromNetwork") fromNetwork: String,
@Query("toContractAddress") toContractAddress: String,
@ -57,8 +69,16 @@ interface TangemExpressApi {
): ApiResponse<ExchangeDataResponse>
@GET("exchange-status")
suspend fun getExchangeStatus(@Query("txId") txId: String): ApiResponse<ExchangeStatusResponse>
suspend fun getExchangeStatus(
@Header("user-id") userWalletId: String,
@Header("refcode") refCode: String,
@Query("txId") txId: String,
): ApiResponse<ExchangeStatusResponse>
@POST("exchange-sent")
suspend fun exchangeSent(@Body body: ExchangeSentRequestBody): ApiResponse<ExchangeSentResponseBody>
suspend fun exchangeSent(
@Header("user-id") userWalletId: String,
@Header("refcode") refCode: String,
@Body body: ExchangeSentRequestBody,
): ApiResponse<ExchangeSentResponseBody>
}

View file

@ -3,37 +3,52 @@ package com.tangem.datasource.api.onramp
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.onramp.models.request.OnrampPairsRequest
import com.tangem.datasource.api.onramp.models.response.OnrampDataResponse
import com.tangem.datasource.api.onramp.models.response.model.OnrampPairDTO
import com.tangem.datasource.api.onramp.models.response.OnrampQuoteResponse
import com.tangem.datasource.api.onramp.models.response.OnrampStatusResponse
import com.tangem.datasource.api.onramp.models.response.model.OnrampCountryDTO
import com.tangem.datasource.api.onramp.models.response.model.OnrampCurrencyDTO
import com.tangem.datasource.api.onramp.models.response.model.OnrampPairDTO
import com.tangem.datasource.api.onramp.models.response.model.PaymentMethodDTO
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.POST
import retrofit2.http.Query
import retrofit2.http.*
@Suppress("LongParameterList", "LargeClass", "TooManyFunctions")
interface OnrampApi {
@GET("currencies")
suspend fun getCurrencies(): ApiResponse<List<OnrampCurrencyDTO>>
suspend fun getCurrencies(
@Header("user-id") userWalletId: String,
@Header("refcode") refCode: String,
): ApiResponse<List<OnrampCurrencyDTO>>
@GET("countries")
suspend fun getCountries(): ApiResponse<List<OnrampCountryDTO>>
suspend fun getCountries(
@Header("user-id") userWalletId: String,
@Header("refcode") refCode: String,
): ApiResponse<List<OnrampCountryDTO>>
@GET("country-by-ip")
suspend fun getCountryByIp(): ApiResponse<OnrampCountryDTO>
suspend fun getCountryByIp(
@Header("user-id") userWalletId: String,
@Header("refcode") refCode: String,
): ApiResponse<OnrampCountryDTO>
@GET("payment-methods")
suspend fun getPaymentMethods(): ApiResponse<List<PaymentMethodDTO>>
suspend fun getPaymentMethods(
@Header("user-id") userWalletId: String,
@Header("refcode") refCode: String,
): ApiResponse<List<PaymentMethodDTO>>
@POST("onramp-pairs")
suspend fun getPairs(@Body body: OnrampPairsRequest): ApiResponse<List<OnrampPairDTO>>
suspend fun getPairs(
@Header("user-id") userWalletId: String,
@Header("refcode") refCode: String,
@Body body: OnrampPairsRequest,
): ApiResponse<List<OnrampPairDTO>>
@GET("onramp-quote")
suspend fun getQuote(
@Header("user-id") userWalletId: String,
@Header("refcode") refCode: String,
@Query("fromCurrencyCode") fromCurrencyCode: String,
@Query("fromPrecision") fromPrecision: Int,
@Query("toContractAddress") toContractAddress: String,
@ -47,6 +62,8 @@ interface OnrampApi {
@GET("onramp-data")
suspend fun getData(
@Header("user-id") userWalletId: String,
@Header("refcode") refCode: String,
@Query("fromCurrencyCode") fromCurrencyCode: String,
@Query("fromPrecision") fromPrecision: Int,
@Query("toContractAddress") toContractAddress: String,
@ -64,5 +81,9 @@ interface OnrampApi {
): ApiResponse<OnrampDataResponse>
@GET("onramp-status")
suspend fun getStatus(@Query("txId") txId: String): ApiResponse<OnrampStatusResponse>
suspend fun getStatus(
@Header("user-id") userWalletId: String,
@Header("refcode") refCode: String,
@Query("txId") txId: String,
): ApiResponse<OnrampStatusResponse>
}

View file

@ -5,11 +5,14 @@ import com.tangem.datasource.api.express.TangemExpressApi
import com.tangem.datasource.api.express.models.request.AssetsRequestBody
import com.tangem.datasource.api.express.models.request.LeastTokenInfo
import com.tangem.datasource.api.express.models.response.Asset
import com.tangem.datasource.exchangeservice.swap.ExpressUtils.getRefCode
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.token.ExpressAssetsStore
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.core.utils.lceContent
import com.tangem.domain.core.utils.lceError
import com.tangem.domain.core.utils.lceLoading
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.Flow
@ -33,31 +36,34 @@ typealias InitializationStatusFlow = MutableStateFlow<Lce<Throwable, List<Asset>
internal class DefaultExpressServiceLoader @Inject constructor(
private val tangemExpressApi: TangemExpressApi,
private val expressAssetsStore: ExpressAssetsStore,
private val appPreferencesStore: AppPreferencesStore,
private val dispatchers: CoroutineDispatcherProvider,
) : ExpressServiceLoader {
private val initializationStatuses =
MutableStateFlow<Map<UserWalletId, InitializationStatusFlow>>(value = emptyMap())
override suspend fun update(userWalletId: UserWalletId, userTokens: List<LeastTokenInfo>) {
override suspend fun update(userWallet: UserWallet, userTokens: List<LeastTokenInfo>) {
withContext(dispatchers.io) {
val initializationStatus = getInitializationStatusInternal(userWalletId)
val initializationStatus = getInitializationStatusInternal(userWallet.walletId)
try {
if (userTokens.isNotEmpty()) {
val response = tangemExpressApi.getAssets(
userWalletId = userWallet.walletId.stringValue,
refCode = getRefCode(userWallet, appPreferencesStore),
body = AssetsRequestBody(tokensList = userTokens),
).getOrThrow()
expressAssetsStore.store(userWalletId, response)
expressAssetsStore.store(userWallet.walletId, response)
initializationStatus.update { response.lceContent() }
}
} catch (e: Throwable) {
if (expressAssetsStore.getSyncOrNull(userWalletId) == null) {
if (expressAssetsStore.getSyncOrNull(userWallet.walletId) == null) {
initializationStatus.update { e.lceError() }
}
Timber.e(e, "Unable to fetch assets for: ${userWalletId.stringValue}")
Timber.e(e, "Unable to fetch assets for: ${userWallet.walletId.stringValue}")
}
}
}

View file

@ -3,6 +3,7 @@ package com.tangem.datasource.exchangeservice.swap
import com.tangem.datasource.api.express.models.request.LeastTokenInfo
import com.tangem.datasource.api.express.models.response.Asset
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.Flow
@ -13,8 +14,8 @@ import kotlinx.coroutines.flow.Flow
*/
interface ExpressServiceLoader {
/** Update service using [userWalletId] and [userTokens] */
suspend fun update(userWalletId: UserWalletId, userTokens: List<LeastTokenInfo>)
/** Update service using [userWallet] and [userTokens] */
suspend fun update(userWallet: UserWallet, userTokens: List<LeastTokenInfo>)
/** Get initialization status by [userWalletId] */
fun getInitializationStatus(userWalletId: UserWalletId): Flow<Lce<Throwable, List<Asset>>>

View file

@ -0,0 +1,46 @@
package com.tangem.datasource.exchangeservice.swap
import com.tangem.common.CardIdRangeDec
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getSyncOrDefault
import com.tangem.domain.wallets.models.UserWallet
import kotlinx.coroutines.runBlocking
object ExpressUtils {
private const val BATCH_ID_CHANGENOW = "BB000013"
private const val BATCH_ID_PARTNER = "AF990015"
fun getRefCode(userWallet: UserWallet, appPreferencesStore: AppPreferencesStore): String {
return when {
isRing(userWallet, appPreferencesStore) -> "ring"
isChangeNow(userWallet) -> "ChangeNow"
isPartner(userWallet) -> "partner"
else -> ""
}
}
private fun isRing(selectedUserWallet: UserWallet, appPreferencesStore: AppPreferencesStore): Boolean {
val addedWalletsWithRings = runBlocking {
appPreferencesStore.getSyncOrDefault(
key = PreferencesKeys.ADDED_WALLETS_WITH_RING_KEY,
default = emptySet(),
)
}
return addedWalletsWithRings.contains(selectedUserWallet.walletId.stringValue)
}
private fun isChangeNow(selectedUserWallet: UserWallet): Boolean {
val changeNowRange = CardIdRangeDec(
start = "AF99001800554008",
end = "AF99001800559994",
)
val card = selectedUserWallet.scanResponse.card
return card.batchId == BATCH_ID_CHANGENOW || changeNowRange?.contains(card.cardId) == true
}
private fun isPartner(selectedUserWallet: UserWallet): Boolean {
return selectedUserWallet.scanResponse.card.batchId == BATCH_ID_PARTNER
}
}

View file

@ -50,9 +50,7 @@ internal class ProdApiConfigsManagerTest(private val model: Model) {
@Before
fun setup() {
every { appVersionProvider.versionName } returns VERSION_NAME
every { expressAuthProvider.getUserId() } returns EXPRESS_USER_ID
every { expressAuthProvider.getSessionId() } returns EXPRESS_SESSION_ID
every { expressAuthProvider.getRefCode() } returns EXPRESS_REF_CODE
every { stakeKitAuthProvider.getApiKey() } returns STAKE_KIT_API_KEY
every { appAuthProvider.getCardId() } returns APP_CARD_ID
every { appAuthProvider.getCardPublicKey() } returns APP_CARD_PUBLIC_KEY
@ -131,9 +129,7 @@ internal class ProdApiConfigsManagerTest(private val model: Model) {
MockEnvironmentConfigStorage.EXPRESS_DEV_API_KEY
}
},
"user-id" to ProviderSuspend { EXPRESS_USER_ID },
"session-id" to ProviderSuspend { EXPRESS_SESSION_ID },
"refcode" to ProviderSuspend { EXPRESS_REF_CODE },
"version" to ProviderSuspend { VERSION_NAME },
"platform" to ProviderSuspend { "android" },
"language" to ProviderSuspend { Locale.getDefault().language.checkHeaderValueOrEmpty() },