Updated on 2026-08-14

This commit is contained in:
Tangem 2022-12-13 18:45:25 +03:00
commit ad914c9a67
13 changed files with 175 additions and 46 deletions

View file

@ -0,0 +1,8 @@
package com.tangem.datasource.api.oneinch
import com.tangem.datasource.api.oneinch.models.SwapErrorDto
data class BaseOneInchResponse<T>(
val body: T?,
val errorDto: SwapErrorDto?,
)

View file

@ -8,6 +8,7 @@ import com.tangem.datasource.api.oneinch.models.QuoteResponse
import com.tangem.datasource.api.oneinch.models.StatusResponse
import com.tangem.datasource.api.oneinch.models.SwapResponse
import com.tangem.datasource.api.oneinch.models.TokensResponse
import retrofit2.Response
import retrofit2.http.GET
import retrofit2.http.Query
@ -127,7 +128,7 @@ interface OneInchApi {
@Query("mainRouteParts") mainRouteParts: String? = null,
@Query("parts") parts: String? = null,
@Query("gasPrice") gasPrice: String? = null,
): QuoteResponse
): Response<QuoteResponse>
/**
* Generate data for calling the 1inch router for exchange
@ -197,6 +198,6 @@ interface OneInchApi {
@Query("complexityLevel") complexityLevel: String? = null,
@Query("gasLimit") gasLimit: String? = null,
@Query("gasPrice") gasPrice: String? = null,
): SwapResponse
): Response<SwapResponse>
//endregion Swap
}

View file

@ -0,0 +1,34 @@
package com.tangem.datasource.api.oneinch
import com.squareup.moshi.Moshi
import com.tangem.datasource.api.oneinch.models.SwapErrorDto
import retrofit2.HttpException
import retrofit2.Response
import javax.inject.Inject
class OneInchErrorsHandler @Inject constructor(private val moshi: Moshi) {
fun <T> handleOneInchResponse(response: Response<T>): BaseOneInchResponse<T> {
val body = response.body()
return if (response.isSuccessful) {
return BaseOneInchResponse(body, null)
} else {
when (response.code()) {
HTTP_CODE_400 -> {
if (body != null) {
BaseOneInchResponse(null, moshi.adapter(SwapErrorDto::class.java).fromJson(body.toString()))
} else {
throw HttpException(response)
}
}
else -> {
throw HttpException(response)
}
}
}
}
companion object {
private const val HTTP_CODE_400 = 400
}
}

View file

@ -15,7 +15,6 @@ import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Converter
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import javax.inject.Singleton
@ -28,10 +27,12 @@ class NetworkModule {
@Singleton
fun provideTangemTechApi(
okHttpClient: OkHttpClient,
converter: Converter.Factory,
moshi: Moshi,
): TangemTechApi {
return Retrofit.Builder()
.addConverterFactory(converter)
.addConverterFactory(
MoshiConverterFactory.create(moshi),
)
.baseUrl(DEV_TANGEM_TECH_BASE_URL)
.client(okHttpClient)
.build()
@ -41,9 +42,11 @@ class NetworkModule {
@Provides
@Singleton
@OneInchEthereum
fun provideOneInchEthereumApi(converter: Converter.Factory): OneInchApi {
fun provideOneInchEthereumApi(moshi: Moshi): OneInchApi {
return Retrofit.Builder()
.addConverterFactory(converter)
.addConverterFactory(
MoshiConverterFactory.create(moshi),
)
.baseUrl(ONE_INCH_ETHER_BASE_URL)
.client(
OkHttpClient.Builder()
@ -58,9 +61,11 @@ class NetworkModule {
@Provides
@Singleton
fun provideReferralApi(okHttpClient: OkHttpClient, converter: Converter.Factory): ReferralApi {
fun provideReferralApi(okHttpClient: OkHttpClient, moshi: Moshi): ReferralApi {
return Retrofit.Builder()
.addConverterFactory(converter)
.addConverterFactory(
MoshiConverterFactory.create(moshi),
)
.baseUrl(DEV_TANGEM_TECH_BASE_URL)
.client(okHttpClient)
.build()
@ -80,13 +85,11 @@ class NetworkModule {
@Provides
@Singleton
fun provideMoshiConverter(): Converter.Factory {
return MoshiConverterFactory.create(
Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.add(BigDecimalAdapter())
.build(),
)
fun provideMoshi(): Moshi {
return Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.add(BigDecimalAdapter())
.build()
}
private companion object {

View file

@ -45,6 +45,9 @@ dependencies {
implementation(project(":core:utils"))
implementation(project(":features:swap:domain"))
/** Network */
implementation(Library.retrofit)
/** DI */
implementation(Library.hilt)
kapt(Library.hiltKapt)

View file

@ -1,12 +1,14 @@
package com.tangem.feature.swap
import com.tangem.datasource.api.oneinch.OneInchApi
import com.tangem.datasource.api.oneinch.OneInchErrorsHandler
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.feature.swap.converters.ApproveConverter
import com.tangem.feature.swap.converters.QuotesConverter
import com.tangem.feature.swap.converters.SwapConverter
import com.tangem.feature.swap.converters.TokensConverter
import com.tangem.feature.swap.domain.SwapRepository
import com.tangem.feature.swap.domain.models.AggregatedSwapDataModel
import com.tangem.feature.swap.domain.models.data.ApproveModel
import com.tangem.feature.swap.domain.models.data.Currency
import com.tangem.feature.swap.domain.models.data.QuoteModel
@ -22,6 +24,7 @@ internal class SwapRepositoryImpl @Inject constructor(
private val quotesConverter: QuotesConverter,
private val swapConverter: SwapConverter,
private val approveConverter: ApproveConverter,
private val oneInchErrorsHandler: OneInchErrorsHandler,
private val coroutineDispatcher: CoroutineDispatcherProvider,
) : SwapRepository {
@ -31,15 +34,17 @@ internal class SwapRepositoryImpl @Inject constructor(
}
}
override suspend fun findBestQuote(fromTokenAddress: String, toTokenAddress: String, amount: String): QuoteModel {
override suspend fun findBestQuote(fromTokenAddress: String, toTokenAddress: String, amount: String):
AggregatedSwapDataModel<QuoteModel> {
return withContext(coroutineDispatcher.io) {
quotesConverter.convert(
val quoteResponse = oneInchErrorsHandler.handleOneInchResponse(
oneInchApi.quote(
fromTokenAddress = fromTokenAddress,
toTokenAddress = toTokenAddress,
amount = amount,
),
)
quotesConverter.convert(quoteResponse)
}
}
@ -67,9 +72,9 @@ internal class SwapRepositoryImpl @Inject constructor(
amount: String,
fromWalletAddress: String,
slippage: Int,
): SwapDataModel {
): AggregatedSwapDataModel<SwapDataModel> {
return withContext(coroutineDispatcher.io) {
swapConverter.convert(
val swapResponse = oneInchErrorsHandler.handleOneInchResponse(
oneInchApi.swap(
fromTokenAddress = fromTokenAddress,
toTokenAddress = toTokenAddress,
@ -78,6 +83,9 @@ internal class SwapRepositoryImpl @Inject constructor(
slippage = slippage,
),
)
swapConverter.convert(
swapResponse,
)
}
}
}

View file

@ -1,17 +1,30 @@
package com.tangem.feature.swap.converters
import com.tangem.datasource.api.oneinch.BaseOneInchResponse
import com.tangem.datasource.api.oneinch.models.QuoteResponse
import com.tangem.feature.swap.domain.models.AggregatedSwapDataModel
import com.tangem.feature.swap.domain.models.data.QuoteModel
import com.tangem.feature.swap.domain.models.data.mapErrors
import com.tangem.utils.converter.Converter
import javax.inject.Inject
class QuotesConverter @Inject constructor(): Converter<QuoteResponse, QuoteModel> {
class QuotesConverter @Inject constructor() : Converter<BaseOneInchResponse<QuoteResponse>, AggregatedSwapDataModel<QuoteModel>> {
override fun convert(value: QuoteResponse): QuoteModel {
return QuoteModel(
fromTokenAmount = value.fromTokenAmount,
toTokenAmount = value.toTokenAmount,
estimatedGas = value.estimatedGas,
)
override fun convert(value: BaseOneInchResponse<QuoteResponse>): AggregatedSwapDataModel<QuoteModel> {
val body = value.body
return if (body != null) {
AggregatedSwapDataModel(
QuoteModel(
fromTokenAmount = body.fromTokenAmount,
toTokenAmount = body.toTokenAmount,
estimatedGas = body.estimatedGas,
),
)
} else {
AggregatedSwapDataModel(
null,
mapErrors(value.errorDto?.description),
)
}
}
}

View file

@ -1,22 +1,35 @@
package com.tangem.feature.swap.converters
import com.tangem.datasource.api.oneinch.BaseOneInchResponse
import com.tangem.datasource.api.oneinch.models.SwapResponse
import com.tangem.datasource.api.oneinch.models.TransactionDto
import com.tangem.feature.swap.domain.models.AggregatedSwapDataModel
import com.tangem.feature.swap.domain.models.data.SwapDataModel
import com.tangem.feature.swap.domain.models.data.TransactionModel
import com.tangem.feature.swap.domain.models.data.mapErrors
import com.tangem.utils.converter.Converter
import javax.inject.Inject
class SwapConverter @Inject constructor() : Converter<SwapResponse, SwapDataModel> {
class SwapConverter @Inject constructor() : Converter<BaseOneInchResponse<SwapResponse>, AggregatedSwapDataModel<SwapDataModel>> {
override fun convert(value: SwapResponse): SwapDataModel {
return SwapDataModel(
fromTokenAddress = value.fromToken.address,
toTokenAddress = value.toToken.address,
toTokenAmount = value.fromTokenAmount,
fromTokenAmount = value.toTokenAmount,
transaction = convertTransaction(value.transaction),
)
override fun convert(value: BaseOneInchResponse<SwapResponse>): AggregatedSwapDataModel<SwapDataModel> {
val body = value.body
return if (body != null) {
AggregatedSwapDataModel(
SwapDataModel(
fromTokenAddress = body.fromToken.address,
toTokenAddress = body.toToken.address,
toTokenAmount = body.fromTokenAmount,
fromTokenAmount = body.toTokenAmount,
transaction = convertTransaction(body.transaction),
),
)
} else {
AggregatedSwapDataModel(
null,
mapErrors(value.errorDto?.description),
)
}
}
private fun convertTransaction(transactionDto: TransactionDto): TransactionModel {

View file

@ -1,6 +1,7 @@
package com.tangem.feature.swap.di
import com.tangem.datasource.api.oneinch.OneInchApi
import com.tangem.datasource.api.oneinch.OneInchErrorsHandler
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.di.qualifiers.OneInchEthereum
import com.tangem.feature.swap.SwapRepositoryImpl
@ -29,6 +30,7 @@ class SwapDataModule {
quotesConverter: QuotesConverter,
swapConverter: SwapConverter,
approveConverter: ApproveConverter,
oneInchErrorsHandler: OneInchErrorsHandler,
coroutineDispatcher: CoroutineDispatcherProvider,
): SwapRepository {
return SwapRepositoryImpl(
@ -38,6 +40,7 @@ class SwapDataModule {
quotesConverter = quotesConverter,
swapConverter = swapConverter,
approveConverter = approveConverter,
oneInchErrorsHandler = oneInchErrorsHandler,
coroutineDispatcher = coroutineDispatcher,
)
}

View file

@ -36,8 +36,13 @@ internal class SwapInteractorImpl @Inject constructor(
repository.checkTokensSpendAllowance(fromTokenAddress, userWalletManager.getWalletAddress(networkId)) != "0"
}
repository.findBestQuote(fromTokenAddress, toTokenAddress, amount).let { quotes ->
cache.cacheSwapParams(quotes.copy(isAllowedToSpend = isAllowedToSpend), amount)
return quotes
val quoteDataModel = quotes.dataModel
if (quoteDataModel != null) {
cache.cacheSwapParams(quoteDataModel.copy(isAllowedToSpend = isAllowedToSpend), amount)
return quoteDataModel
} else {
error("") //todo handle error in domain layer(task [REDACTED_TASK_KEY])
}
}
}
@ -52,12 +57,16 @@ internal class SwapInteractorImpl @Inject constructor(
amount = amountToSwap,
slippage = DEFAULT_SLIPPAGE,
fromWalletAddress = getWalletAddress(networkId),
)
signTransactionData(swapData.transaction) //todo implement
return SwapResultModel.SwapSuccess(
quoteModel.fromTokenAmount,
quoteModel.toTokenAmount,
)
).dataModel
if (swapData != null) {
signTransactionData(swapData.transaction) //todo implement
return SwapResultModel.SwapSuccess(
quoteModel.fromTokenAmount,
quoteModel.toTokenAmount,
)
} else {
error("") //todo handle error in domain layer(task [REDACTED_TASK_KEY])
}
}
return SwapResultModel.SwapError(
0,

View file

@ -1,5 +1,6 @@
package com.tangem.feature.swap.domain
import com.tangem.feature.swap.domain.models.AggregatedSwapDataModel
import com.tangem.feature.swap.domain.models.data.ApproveModel
import com.tangem.feature.swap.domain.models.data.Currency
import com.tangem.feature.swap.domain.models.data.QuoteModel
@ -13,7 +14,7 @@ interface SwapRepository {
fromTokenAddress: String,
toTokenAddress: String,
amount: String,
): QuoteModel
): AggregatedSwapDataModel<QuoteModel>
/**
* Returns address of 1inch router that must be trusted
@ -49,5 +50,5 @@ interface SwapRepository {
amount: String,
fromWalletAddress: String,
slippage: Int,
): SwapDataModel
): AggregatedSwapDataModel<SwapDataModel>
}

View file

@ -0,0 +1,15 @@
package com.tangem.feature.swap.domain.models
import com.tangem.feature.swap.domain.models.data.DataError
/**
* Model that aggregate data model from repository return with error [DataError] if it exists
*
* @param T model type
* @property dataModel
* @property error possible from repository [DataError]
*/
data class AggregatedSwapDataModel<T>(
val dataModel: T?,
val error: DataError = DataError.NO_ERROR,
)

View file

@ -0,0 +1,18 @@
package com.tangem.feature.swap.domain.models.data
enum class DataError {
NO_ERROR,
UNKNOWN_ERROR,
INSUFFICIENT_LIQUIDITY
}
fun mapErrors(error: String?): DataError {
return if (error == null) {
DataError.UNKNOWN_ERROR
} else when (error) {
INSUFFICIENT_LIQUIDITY_ERROR -> DataError.INSUFFICIENT_LIQUIDITY
else -> DataError.UNKNOWN_ERROR
}
}
private const val INSUFFICIENT_LIQUIDITY_ERROR = "insufficient liquidity"