Updated on 2026-08-14

This commit is contained in:
Tangem 2025-06-18 18:21:03 +05:00
parent 41648e4ffc
commit 3dc6ff9548
12 changed files with 400 additions and 1 deletions

View file

@ -0,0 +1,36 @@
package com.tangem.data.express
import com.tangem.data.common.api.safeApiCall
import com.tangem.data.express.converter.ExpressProviderConverter
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.express.TangemExpressApi
import com.tangem.datasource.exchangeservice.swap.ExpressUtils
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.domain.express.models.ExpressProvider
import com.tangem.domain.express.ExpressRepository
import com.tangem.domain.wallets.models.UserWallet
import timber.log.Timber
internal class DefaultExpressRepository(
private val tangemExpressApi: TangemExpressApi,
private val appPreferencesStore: AppPreferencesStore,
) : ExpressRepository {
override suspend fun getProviders(userWallet: UserWallet): List<ExpressProvider> {
return safeApiCall(
call = {
tangemExpressApi.getProviders(
userWalletId = userWallet.walletId.stringValue,
refCode = ExpressUtils.getRefCode(
userWallet = userWallet,
appPreferencesStore = appPreferencesStore,
),
).getOrThrow().map(ExpressProviderConverter()::convert)
},
onError = {
Timber.w(it, "Unable to fetch express providers")
throw it
},
)
}
}

View file

@ -0,0 +1,91 @@
package com.tangem.data.express.converter
import com.squareup.moshi.JsonAdapter
import com.tangem.datasource.api.express.models.response.ExpressErrorResponse
import com.tangem.domain.express.models.ExpressError
import com.tangem.utils.converter.Converter
import com.tangem.datasource.api.express.models.response.ExpressError as ExpressErrorDTO
class ExpressErrorConverter(
private val jsonAdapter: JsonAdapter<ExpressErrorResponse>,
) : Converter<String, ExpressError> {
@Suppress("MagicNumber", "CyclomaticComplexMethod")
override fun convert(value: String): ExpressError {
try {
val error: ExpressErrorDTO = jsonAdapter.fromJson(value)?.error ?: return ExpressError.UnknownError
return when (error.code) {
2010 -> ExpressError.BadRequest(code = error.code)
2020 -> ExpressError.Forbidden(code = error.code)
2200 -> ExpressError.InternalError(code = error.code)
2210 -> ExpressError.ProviderNotFoundError(code = error.code)
2220 -> ExpressError.ProviderNotActiveError(code = error.code)
2230 -> ExpressError.ProviderNotAvailableError(code = error.code)
2231 -> ExpressError.ProviderInternalError(code = error.code)
2240 -> ExpressError.ExchangeNotPossibleError(code = error.code)
2250 -> error.toTooSmallAmountError()
2251 -> error.toTooBigAmountError()
2260 -> error.toNotEnoughAllowanceError()
2270 -> ExpressError.NotEnoughBalanceError(code = error.code)
2280 -> ExpressError.InvalidAddressError(code = error.code)
2290 -> error.toInvalidFromDecimalsError()
2320 -> error.toProviderDifferentAmountError()
else -> ExpressError.DataError(error.code, error.description)
}
} catch (e: Exception) {
return ExpressError.UnknownError
}
}
private fun ExpressErrorDTO.toTooSmallAmountError(): ExpressError {
val minAmount = value?.minAmount ?: return ExpressError.DataError(code, description)
val decimals = value?.decimals ?: return ExpressError.DataError(code, description)
val requiredAmount = minAmount.toBigDecimalOrNull()?.movePointLeft(decimals)
?: return ExpressError.DataError(code, description)
return ExpressError.AmountError.TooSmallError(code = code, amount = requiredAmount)
}
private fun ExpressErrorDTO.toTooBigAmountError(): ExpressError {
val maxAmount = value?.maxAmount ?: return ExpressError.DataError(code, description)
val decimals = value?.decimals ?: return ExpressError.DataError(code, description)
val requiredAmount = maxAmount.toBigDecimalOrNull()?.movePointLeft(decimals)
?: return ExpressError.DataError(code, description)
return ExpressError.AmountError.TooBigError(code = code, amount = requiredAmount)
}
private fun ExpressErrorDTO.toNotEnoughAllowanceError(): ExpressError {
val currentAllowance = value?.currentAllowance ?: return ExpressError.DataError(code, description)
return ExpressError.AmountError.NotEnoughAllowanceError(code = code, amount = currentAllowance)
}
private fun ExpressErrorDTO.toInvalidFromDecimalsError(): ExpressError {
val receivedFromDecimals = value?.receivedFromDecimals ?: return ExpressError.DataError(code, description)
val expressFromDecimals = value?.expressFromDecimals ?: return ExpressError.DataError(code, description)
return ExpressError.InvalidFromDecimalsError(
code = code,
receivedFromDecimals = receivedFromDecimals,
expressFromDecimals = expressFromDecimals,
)
}
private fun ExpressErrorDTO.toProviderDifferentAmountError(): ExpressError {
val decimals = value?.decimals ?: return ExpressError.DataError(code, description)
val fromAmount = value?.fromAmount?.toBigDecimalOrNull() ?: return ExpressError.DataError(code, description)
val fromAmountProvider = value?.fromAmountProvider?.toBigDecimalOrNull()
?: return ExpressError.DataError(code, description)
return ExpressError.ProviderDifferentAmountError(
code = code,
decimals = decimals,
fromAmount = fromAmount.movePointLeft(decimals),
fromProviderAmount = fromAmountProvider.movePointLeft(decimals),
)
}
}

View file

@ -0,0 +1,33 @@
package com.tangem.data.express.converter
import com.tangem.datasource.api.express.models.response.ExchangeProvider
import com.tangem.datasource.api.express.models.response.ExchangeProviderType
import com.tangem.domain.express.models.ExpressProvider
import com.tangem.domain.express.models.ExpressProviderType
import com.tangem.utils.converter.Converter
internal class ExpressProviderConverter : Converter<ExchangeProvider, ExpressProvider> {
override fun convert(value: ExchangeProvider): ExpressProvider {
return ExpressProvider(
providerId = value.id,
rateTypes = emptyList(),
name = value.name,
type = convertExchangeType(value.type),
imageLarge = value.imageLargeUrl,
termsOfUse = value.termsOfUse,
privacyPolicy = value.privacyPolicy,
isRecommended = value.isRecommended,
slippage = value.slippage,
)
}
private fun convertExchangeType(type: ExchangeProviderType): ExpressProviderType {
return when (type) {
ExchangeProviderType.DEX -> ExpressProviderType.DEX
ExchangeProviderType.CEX -> ExpressProviderType.CEX
ExchangeProviderType.DEX_BRIDGE -> ExpressProviderType.DEX_BRIDGE
ExchangeProviderType.ONRAMP -> ExpressProviderType.ONRAMP
}
}
}

View file

@ -0,0 +1,43 @@
package com.tangem.data.express.di
import com.squareup.moshi.Moshi
import com.tangem.data.express.DefaultExpressRepository
import com.tangem.data.express.converter.ExpressErrorConverter
import com.tangem.data.express.error.DefaultExpressErrorResolver
import com.tangem.datasource.api.express.TangemExpressApi
import com.tangem.datasource.api.express.models.response.ExpressErrorResponse
import com.tangem.datasource.di.NetworkMoshi
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.domain.express.ExpressErrorResolver
import com.tangem.domain.express.ExpressRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object ExpressDataModule {
@Provides
@Singleton
fun provideExpressErrorResolver(@NetworkMoshi moshi: Moshi): ExpressErrorResolver {
val jsonAdapter = moshi.adapter(ExpressErrorResponse::class.java)
return DefaultExpressErrorResolver(
ExpressErrorConverter(jsonAdapter),
)
}
@Provides
@Singleton
fun provideExpressRepository(
tangemExpressApi: TangemExpressApi,
appPreferencesStore: AppPreferencesStore,
): ExpressRepository {
return DefaultExpressRepository(
tangemExpressApi = tangemExpressApi,
appPreferencesStore = appPreferencesStore,
)
}
}

View file

@ -0,0 +1,20 @@
package com.tangem.data.express.error
import com.tangem.data.express.converter.ExpressErrorConverter
import com.tangem.datasource.api.common.response.ApiResponseError
import com.tangem.domain.express.ExpressErrorResolver
import com.tangem.domain.express.models.ExpressError
internal class DefaultExpressErrorResolver(
private val expressErrorConverter: ExpressErrorConverter,
) : ExpressErrorResolver {
override fun resolve(throwable: Throwable): ExpressError {
return when (throwable) {
is ApiResponseError.HttpException -> {
expressErrorConverter.convert(throwable.errorBody.orEmpty())
}
else -> ExpressError.UnknownError
}
}
}

View file

@ -0,0 +1,64 @@
package com.tangem.domain.express.models
import java.math.BigDecimal
sealed class ExpressError {
abstract val code: Int
data class DataError(
override val code: Int,
val description: String?,
) : ExpressError()
data class BadRequest(override val code: Int) : ExpressError()
data class Forbidden(override val code: Int) : ExpressError()
data class InternalError(override val code: Int) : ExpressError()
data class ProviderNotFoundError(override val code: Int) : ExpressError()
data class ProviderNotActiveError(override val code: Int) : ExpressError()
data class ProviderNotAvailableError(override val code: Int) : ExpressError()
data class ProviderInternalError(override val code: Int) : ExpressError()
data class ExchangeNotPossibleError(override val code: Int) : ExpressError()
data class NotEnoughBalanceError(override val code: Int) : ExpressError()
data class InvalidAddressError(override val code: Int) : ExpressError()
sealed class AmountError : ExpressError() {
abstract val amount: BigDecimal
data class TooSmallError(override val code: Int, override val amount: BigDecimal) : AmountError()
data class TooBigError(override val code: Int, override val amount: BigDecimal) : AmountError()
data class NotEnoughAllowanceError(override val code: Int, override val amount: BigDecimal) : AmountError()
}
data class ProviderDifferentAmountError(
override val code: Int,
val fromAmount: BigDecimal,
val fromProviderAmount: BigDecimal,
val decimals: Int,
) : ExpressError()
data class InvalidFromDecimalsError(
override val code: Int,
val receivedFromDecimals: Int,
val expressFromDecimals: Int,
) : ExpressError()
data class InvalidSignatureError(override val code: Int = 990) : ExpressError()
data class InvalidRequestIdError(override val code: Int = 991) : ExpressError()
data class InvalidPayoutAddressError(override val code: Int = 992) : ExpressError()
data object UnknownError : ExpressError() {
override val code: Int = -1
}
}

View file

@ -0,0 +1,43 @@
package com.tangem.domain.express.models
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import java.math.BigDecimal
/**
* Express provider data model
*
* @property providerId provider id
* @property rateTypes supported rate types
* @property name provider name
* @property type provider type
* @property imageLarge provider large logo image
* @property termsOfUse terms of use link
* @property privacyPolicy privacy policy link
* @property isRecommended flag that indicates if this provider is recommended
* @property slippage provider slippage
*
* Uses to store transaction data in datastore, when extends - should always add default value
* to support backward compatibility
*/
@JsonClass(generateAdapter = true)
data class ExpressProvider(
@Json(name = "providerId")
val providerId: String,
@Json(name = "rateTypes")
val rateTypes: List<ExpressRateType> = emptyList(),
@Json(name = "name")
val name: String,
@Json(name = "type")
val type: ExpressProviderType,
@Json(name = "imageLarge")
val imageLarge: String,
@Json(name = "termsOfUse")
val termsOfUse: String?,
@Json(name = "privacyPolicy")
val privacyPolicy: String?,
@Json(name = "isRecommended")
val isRecommended: Boolean = false,
@Json(name = "slippage")
val slippage: BigDecimal?,
)

View file

@ -0,0 +1,24 @@
package com.tangem.domain.express.models
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/**
* Express provider type
*
* @property typeName provider type name
*/
@JsonClass(generateAdapter = false)
enum class ExpressProviderType(val typeName: String) {
@Json(name = "DEX")
DEX(typeName = "DEX"),
@Json(name = "CEX")
CEX(typeName = "CEX"),
@Json(name = "DEX_BRIDGE")
DEX_BRIDGE(typeName = "DEX/Bridge"),
@Json(name = "onramp")
ONRAMP(typeName = "ONRAMP"),
}

View file

@ -0,0 +1,18 @@
package com.tangem.domain.express.models
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/**
* Rate type.
*
* Current implementation contains only float type, fixed will be supported later.
*/
@JsonClass(generateAdapter = false)
enum class ExpressRateType {
@Json(name = "FLOAT")
Float,
@Json(name = "FIXED")
Fixed,
}

View file

@ -0,0 +1,11 @@
package com.tangem.domain.express
import com.tangem.domain.express.models.ExpressError
/**
* Common express error resolver
*/
interface ExpressErrorResolver {
fun resolve(throwable: Throwable): ExpressError
}

View file

@ -0,0 +1,17 @@
package com.tangem.domain.express
import com.tangem.domain.express.models.ExpressProvider
import com.tangem.domain.wallets.models.UserWallet
/**
* Express repository
*/
interface ExpressRepository {
/**
* Returns list of express providers
*
* @param userWallet selected user wallet info
*/
suspend fun getProviders(userWallet: UserWallet): List<ExpressProvider>
}

View file

@ -23,7 +23,6 @@ dependencies {
implementation(projects.features.swapV2.api)
implementation(projects.features.manageTokens.api)
implementation(projects.features.sendV2.api)
implementation(projects.features.feeSelector.api)
/** Core */
implementation(projects.core.decompose)