Updated on 2026-08-14

This commit is contained in:
Tangem 2022-12-15 12:45:36 +03:00
parent 78fab3a9de
commit 78f93a8491
18 changed files with 169 additions and 143 deletions

View file

@ -1,8 +0,0 @@
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

@ -58,7 +58,7 @@ interface OneInchApi {
suspend fun approveAllowance(
@Query("tokenAddress") tokenAddress: String,
@Query("walletAddress") walletAddress: String,
): AllowanceResponse
): Response<AllowanceResponse>
//endregion Approve
//region Info

View file

@ -1,25 +1,25 @@
package com.tangem.datasource.api.oneinch
import com.squareup.moshi.Moshi
import com.tangem.datasource.api.oneinch.errors.OneIncResponseException
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) {
class OneInchErrorsHandler @Inject constructor(moshi: Moshi) {
fun <T> handleOneInchResponse(response: Response<T>): BaseOneInchResponse<T> {
val body = response.body()
private val errorMoshiAdapter = moshi.adapter(SwapErrorDto::class.java)
@Throws(OneIncResponseException::class)
fun <T> handleOneInchResponse(response: Response<T>): T {
return if (response.isSuccessful) {
return BaseOneInchResponse(body, null)
response.body() ?: error("response body is 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)
}
val swapErrorDto = errorMoshiAdapter.fromJson(response.errorBody()?.string() ?: "")
throw OneIncResponseException(swapErrorDto ?: throw HttpException(response))
}
else -> {
throw HttpException(response)

View file

@ -0,0 +1,5 @@
package com.tangem.datasource.api.oneinch.errors
import com.tangem.datasource.api.oneinch.models.SwapErrorDto
class OneIncResponseException(val data: SwapErrorDto) : Exception()

View file

@ -2,6 +2,7 @@ package com.tangem.feature.swap
import com.tangem.datasource.api.oneinch.OneInchApi
import com.tangem.datasource.api.oneinch.OneInchErrorsHandler
import com.tangem.datasource.api.oneinch.errors.OneIncResponseException
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.feature.swap.converters.ApproveConverter
import com.tangem.feature.swap.converters.QuotesConverter
@ -11,8 +12,9 @@ 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
import com.tangem.feature.swap.domain.models.data.SwapDataModel
import com.tangem.feature.swap.domain.models.data.SwapState.QuoteModel
import com.tangem.feature.swap.domain.models.data.mapErrors
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext
import javax.inject.Inject
@ -37,14 +39,18 @@ internal class SwapRepositoryImpl @Inject constructor(
override suspend fun findBestQuote(fromTokenAddress: String, toTokenAddress: String, amount: String):
AggregatedSwapDataModel<QuoteModel> {
return withContext(coroutineDispatcher.io) {
val quoteResponse = oneInchErrorsHandler.handleOneInchResponse(
oneInchApi.quote(
fromTokenAddress = fromTokenAddress,
toTokenAddress = toTokenAddress,
amount = amount,
),
)
quotesConverter.convert(quoteResponse)
try {
val response = oneInchErrorsHandler.handleOneInchResponse(
oneInchApi.quote(
fromTokenAddress = fromTokenAddress,
toTokenAddress = toTokenAddress,
amount = amount,
),
)
AggregatedSwapDataModel(dataModel = quotesConverter.convert(response))
} catch (ex: OneIncResponseException) {
AggregatedSwapDataModel(null, mapErrors(ex.data.description))
}
}
}
@ -54,15 +60,23 @@ internal class SwapRepositoryImpl @Inject constructor(
}
}
override suspend fun dataToApprove(tokenAddress: String, amount: String): ApproveModel {
override suspend fun dataToApprove(tokenAddress: String, amount: String?): ApproveModel {
return withContext(coroutineDispatcher.io) {
approveConverter.convert(oneInchApi.approveTransaction(tokenAddress, amount))
}
}
override suspend fun checkTokensSpendAllowance(tokenAddress: String, walletAddress: String): String {
override suspend fun checkTokensSpendAllowance(tokenAddress: String, walletAddress: String):
AggregatedSwapDataModel<String> {
return withContext(coroutineDispatcher.io) {
oneInchApi.approveAllowance(tokenAddress, walletAddress).allowance
try {
val response = oneInchErrorsHandler.handleOneInchResponse(
oneInchApi.approveAllowance(tokenAddress, walletAddress)
)
AggregatedSwapDataModel(response.allowance)
} catch (ex: OneIncResponseException) {
AggregatedSwapDataModel(null, mapErrors(ex.data.description))
}
}
}
@ -74,18 +88,20 @@ internal class SwapRepositoryImpl @Inject constructor(
slippage: Int,
): AggregatedSwapDataModel<SwapDataModel> {
return withContext(coroutineDispatcher.io) {
val swapResponse = oneInchErrorsHandler.handleOneInchResponse(
oneInchApi.swap(
fromTokenAddress = fromTokenAddress,
toTokenAddress = toTokenAddress,
amount = amount,
fromAddress = fromWalletAddress,
slippage = slippage,
),
)
swapConverter.convert(
swapResponse,
)
try {
val swapResponse = oneInchErrorsHandler.handleOneInchResponse(
oneInchApi.swap(
fromTokenAddress = fromTokenAddress,
toTokenAddress = toTokenAddress,
amount = amount,
fromAddress = fromWalletAddress,
slippage = slippage,
),
)
AggregatedSwapDataModel(swapConverter.convert(swapResponse))
} catch (ex: OneIncResponseException) {
AggregatedSwapDataModel(null, mapErrors(ex.data.description))
}
}
}
}

View file

@ -1,30 +1,17 @@
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.feature.swap.domain.models.data.SwapState.QuoteModel
import com.tangem.utils.converter.Converter
import javax.inject.Inject
class QuotesConverter @Inject constructor() : Converter<BaseOneInchResponse<QuoteResponse>, AggregatedSwapDataModel<QuoteModel>> {
class QuotesConverter @Inject constructor() : Converter<QuoteResponse, QuoteModel> {
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),
)
}
override fun convert(value: QuoteResponse): QuoteModel {
return QuoteModel(
fromTokenAmount = value.fromTokenAmount,
toTokenAmount = value.toTokenAmount,
estimatedGas = value.estimatedGas,
)
}
}

View file

@ -1,35 +1,22 @@
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<BaseOneInchResponse<SwapResponse>, AggregatedSwapDataModel<SwapDataModel>> {
class SwapConverter @Inject constructor() : Converter<SwapResponse, SwapDataModel> {
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),
)
}
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),
)
}
private fun convertTransaction(transactionDto: TransactionDto): TransactionModel {

View file

@ -1,8 +1,7 @@
package com.tangem.feature.swap.domain
import com.tangem.feature.swap.domain.models.SwapResultModel
import com.tangem.feature.swap.domain.models.data.Currency
import com.tangem.feature.swap.domain.models.data.QuoteModel
import com.tangem.feature.swap.domain.models.data.SwapState
interface SwapInteractor {
@ -10,11 +9,13 @@ interface SwapInteractor {
suspend fun getTokenBalance(tokenId: String): String
suspend fun givePermissionToSwap(tokenAddress: String)
suspend fun findBestQuote(
fromTokenAddress: String,
toTokenAddress: String,
amount: String,
): QuoteModel
): SwapState
suspend fun onSwap(): SwapResultModel
suspend fun onSwap(): SwapState
}

View file

@ -1,9 +1,9 @@
package com.tangem.feature.swap.domain
import com.tangem.feature.swap.domain.cache.SwapDataCache
import com.tangem.feature.swap.domain.models.SwapResultModel
import com.tangem.feature.swap.domain.models.data.Currency
import com.tangem.feature.swap.domain.models.data.QuoteModel
import com.tangem.feature.swap.domain.models.data.DataError
import com.tangem.feature.swap.domain.models.data.SwapState
import com.tangem.feature.swap.domain.models.data.TransactionModel
import com.tangem.lib.crypto.UserWalletManager
import javax.inject.Inject
@ -28,12 +28,20 @@ internal class SwapInteractorImpl @Inject constructor(
TODO("Not yet implemented")
}
override suspend fun findBestQuote(fromTokenAddress: String, toTokenAddress: String, amount: String): QuoteModel {
override suspend fun givePermissionToSwap(tokenAddress: String) {
val oneInchAddress = repository.addressForTrust()
val transactionData = repository.dataToApprove(tokenAddress)
//todo sign transaction (next tasks)
}
override suspend fun findBestQuote(fromTokenAddress: String, toTokenAddress: String, amount: String): SwapState {
val networkId = cache.getNetworkId()
val isAllowedToSpend = if (networkId.isNullOrEmpty()) {
false
} else {
repository.checkTokensSpendAllowance(fromTokenAddress, userWalletManager.getWalletAddress(networkId)) != "0"
val allowance =
repository.checkTokensSpendAllowance(fromTokenAddress, userWalletManager.getWalletAddress(networkId))
allowance.error == DataError.NO_ERROR && allowance.dataModel != "0"
}
repository.findBestQuote(fromTokenAddress, toTokenAddress, amount).let { quotes ->
val quoteDataModel = quotes.dataModel
@ -41,37 +49,37 @@ internal class SwapInteractorImpl @Inject constructor(
cache.cacheSwapParams(quoteDataModel.copy(isAllowedToSpend = isAllowedToSpend), amount)
return quoteDataModel
} else {
error("") //todo handle error in domain layer(task [REDACTED_TASK_KEY])
return SwapState.SwapError(quotes.error)
}
}
}
override suspend fun onSwap(): SwapResultModel {
override suspend fun onSwap(): SwapState {
val quoteModel = cache.getLastQuote()
val amountToSwap = cache.getAmountToSwap()
val networkId = cache.getNetworkId()
if (quoteModel != null && amountToSwap != null && networkId != null) {
val swapData = repository.prepareSwapTransaction(
repository.prepareSwapTransaction(
fromTokenAddress = quoteModel.fromTokenAmount,
toTokenAddress = quoteModel.toTokenAmount,
amount = amountToSwap,
slippage = DEFAULT_SLIPPAGE,
fromWalletAddress = getWalletAddress(networkId),
).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])
).let {
val swapData = it.dataModel
if (swapData != null) {
signTransactionData(swapData.transaction) //todo implement
return SwapState.SwapSuccess(
quoteModel.fromTokenAmount,
quoteModel.toTokenAmount,
)
} else {
return SwapState.SwapError(it.error)
}
}
} else {
throw IllegalStateException("cache is empty, call 'findBestQuote' first")
}
return SwapResultModel.SwapError(
0,
"",
)
}
private fun getWalletAddress(networkId: String): String {

View file

@ -3,8 +3,8 @@ 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
import com.tangem.feature.swap.domain.models.data.SwapDataModel
import com.tangem.feature.swap.domain.models.data.SwapState
interface SwapRepository {
@ -14,7 +14,7 @@ interface SwapRepository {
fromTokenAddress: String,
toTokenAddress: String,
amount: String,
): AggregatedSwapDataModel<QuoteModel>
): AggregatedSwapDataModel<SwapState.QuoteModel>
/**
* Returns address of 1inch router that must be trusted
@ -31,7 +31,7 @@ interface SwapRepository {
*/
suspend fun dataToApprove(
tokenAddress: String,
amount: String,
amount: String? = null,
): ApproveModel
/**
@ -42,7 +42,7 @@ interface SwapRepository {
*
* @return amount of tokens allowed to spend
*/
suspend fun checkTokensSpendAllowance(tokenAddress: String, walletAddress: String): String
suspend fun checkTokensSpendAllowance(tokenAddress: String, walletAddress: String): AggregatedSwapDataModel<String>
suspend fun prepareSwapTransaction(
fromTokenAddress: String,

View file

@ -1,7 +1,7 @@
package com.tangem.feature.swap.domain.cache
import com.tangem.feature.swap.domain.models.data.Currency
import com.tangem.feature.swap.domain.models.data.QuoteModel
import com.tangem.feature.swap.domain.models.data.SwapState.QuoteModel
interface SwapDataCache {

View file

@ -2,7 +2,7 @@ package com.tangem.feature.swap.domain.cache
import com.tangem.feature.swap.domain.models.SwapDataHolder
import com.tangem.feature.swap.domain.models.data.Currency
import com.tangem.feature.swap.domain.models.data.QuoteModel
import com.tangem.feature.swap.domain.models.data.SwapState.QuoteModel
class SwapDataCacheImpl : SwapDataCache {

View file

@ -1,9 +1,10 @@
package com.tangem.feature.swap.domain.models
import com.tangem.feature.swap.domain.models.data.QuoteModel
import com.tangem.feature.swap.domain.models.data.SwapState.QuoteModel
data class SwapDataHolder(
val quoteModel: QuoteModel? = null,
val quoteModel:
QuoteModel? = null,
val amountToSwap: String? = null,
val networkId: String? = null,
)

View file

@ -1,14 +0,0 @@
package com.tangem.feature.swap.domain.models
sealed interface SwapResultModel {
data class SwapSuccess(
val fromTokenAmount: String,
val toTokenAmount: String,
) : SwapResultModel
data class SwapError(
val errorCode: Int,
val errorMessage: String,
) : SwapResultModel
}

View file

@ -3,16 +3,19 @@ package com.tangem.feature.swap.domain.models.data
enum class DataError {
NO_ERROR,
UNKNOWN_ERROR,
INSUFFICIENT_LIQUIDITY
INSUFFICIENT_LIQUIDITY,
CANNOT_SYNC
}
fun mapErrors(error: String?): DataError {
return if (error == null) {
DataError.UNKNOWN_ERROR
} else when (error) {
INSUFFICIENT_LIQUIDITY_ERROR -> DataError.INSUFFICIENT_LIQUIDITY
} else when {
error == INSUFFICIENT_LIQUIDITY_ERROR -> DataError.INSUFFICIENT_LIQUIDITY
error.startsWith(CANNOT_SYNC_ERROR) -> DataError.CANNOT_SYNC
else -> DataError.UNKNOWN_ERROR
}
}
private const val INSUFFICIENT_LIQUIDITY_ERROR = "insufficient liquidity"
private const val INSUFFICIENT_LIQUIDITY_ERROR = "insufficient liquidity"
private const val CANNOT_SYNC_ERROR = "cannot sync"

View file

@ -1,8 +0,0 @@
package com.tangem.feature.swap.domain.models.data
data class QuoteModel(
val fromTokenAmount: String,
val toTokenAmount: String,
val estimatedGas: Int,
val isAllowedToSpend: Boolean = false,
)

View file

@ -0,0 +1,20 @@
package com.tangem.feature.swap.domain.models.data
sealed interface SwapState {
data class QuoteModel(
val fromTokenAmount: String,
val toTokenAmount: String,
val estimatedGas: Int,
val isAllowedToSpend: Boolean = false,
) : SwapState
data class SwapSuccess(
val fromTokenAmount: String,
val toTokenAmount: String,
) : SwapState
data class SwapError(
val errorType: DataError,
) : SwapState
}

View file

@ -1,9 +1,13 @@
package com.tangem.feature.swap.viewmodels
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.tangem.feature.swap.domain.SwapInteractor
import com.tangem.feature.swap.domain.models.data.Currency
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
@ -12,4 +16,28 @@ internal class SwapViewModel @Inject constructor(
private val dispatchers: CoroutineDispatcherProvider,
) : ViewModel() {
// region temp for test
init {
viewModelScope.launch {
runCatching {
val tokens = referralInteractor.getTokensToSwap("binance-smart-chain")
val token = tokens[1]
val token2 = tokens[2]
if (token is Currency.NonNativeToken && token2 is Currency.NonNativeToken) {
findBestQuotes(token.contractAddress, token2.contractAddress)
}
}.onFailure {
Log.e("SwapViewModel", it.message ?: it.cause.toString())
}
}
}
private suspend fun findBestQuotes(fromTokenAddress: String, toTokenAddress: String) {
referralInteractor.findBestQuote(
fromTokenAddress,
toTokenAddress,
"1",
)
}
// endregion temp for test
}