Updated on 2026-08-14
This commit is contained in:
parent
0ecd5e8da1
commit
b87f415e29
13 changed files with 175 additions and 46 deletions
|
|
@ -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?,
|
||||
)
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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"
|
||||
Loading…
Add table
Add a link
Reference in a new issue