Updated on 2026-08-14

This commit is contained in:
Tangem 2023-12-21 19:58:56 +03:00
parent a4beb2cafc
commit bd62089c12
39 changed files with 149 additions and 664 deletions

@ -1 +1 @@
Subproject commit 6ef8ce45d183905b5752e2d33c1d8bf2f4bcace6
Subproject commit 0be5650f8fbcdff22c23fe2e2c7d754c4275a6f6

View file

@ -15,7 +15,7 @@ internal class DefaultExpressAuthProvider(
private var uuid = AtomicReference(UUID.randomUUID())
override fun getApiKey(): String {
return configManager.config.tangemExpressApiKey
return configManager.config.express?.apiKey ?: ""
}
override fun getUserId(): String {

View file

@ -1,13 +0,0 @@
package com.tangem.tap.network.auth
import com.tangem.datasource.config.ConfigManager
import com.tangem.lib.auth.AuthBearerProvider
internal class DefaultOneInchProvider(
private val configManager: ConfigManager,
) : AuthBearerProvider {
override fun getApiKey(): String {
return configManager.config.oneInchApiKey
}
}

View file

@ -2,12 +2,10 @@ package com.tangem.tap.network.auth.di
import com.tangem.datasource.config.ConfigManager
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.lib.auth.AuthBearerProvider
import com.tangem.lib.auth.AuthProvider
import com.tangem.lib.auth.ExpressAuthProvider
import com.tangem.tap.network.auth.DefaultAuthProvider
import com.tangem.tap.network.auth.DefaultExpressAuthProvider
import com.tangem.tap.network.auth.DefaultOneInchProvider
import com.tangem.tap.proxy.AppStateHolder
import dagger.Module
import dagger.Provides
@ -36,12 +34,4 @@ class AuthModule {
configManager = configManager,
)
}
@Provides
@Singleton
fun provideOneInchAuthProvider(configManager: ConfigManager): AuthBearerProvider {
return DefaultOneInchProvider(
configManager = configManager,
)
}
}

View file

@ -49,6 +49,7 @@ interface TangemExpressApi {
@Query("providerId") providerId: String,
@Query("rateType") rateType: String,
@Query("toAddress") toAddress: String,
@Query("requestId") requestId: String,
): ApiResponse<ExchangeDataResponse>
@GET("exchange-status")

View file

@ -3,6 +3,10 @@ package com.tangem.datasource.api.express.models.response
import com.squareup.moshi.Json
import java.math.BigDecimal
data class ExchangeDataResponseWithTxDetails(
val dataResponse: ExchangeDataResponse,
val txDetails: TxDetails,
)
data class ExchangeDataResponse(
@Json(name = "fromAmount")
val fromAmount: String,
@ -16,12 +20,23 @@ data class ExchangeDataResponse(
@Json(name = "toDecimals")
val toDecimals: Int,
@Json(name = "txType")
val txType: TxType,
@Json(name = "txId")
val txId: String, // inner tangem-express transaction id
@Json(name = "txDetailsJson")
val txDetailsJson: String,
@Json(name = "signature")
val signature: String,
)
data class TxDetails(
@Json(name = "requestId")
val requestId: String,
@Json(name = "txType")
val txType: TxType,
@Json(name = "txFrom")
val txFrom: String?, // account for debiting tokens (same as toAddress) if DEX, null if CEX

View file

@ -1,205 +0,0 @@
package com.tangem.datasource.api.oneinch
import com.tangem.datasource.api.oneinch.models.AllowanceResponse
import com.tangem.datasource.api.oneinch.models.ApproveCalldataResponse
import com.tangem.datasource.api.oneinch.models.ApproveSpenderResponse
import com.tangem.datasource.api.oneinch.models.ProtocolsResponse
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
interface OneInchApi {
/**
* Healthcheck return 200 if service is available
*
* @return [StatusResponse]
*/
@GET("healthcheck")
suspend fun healthcheck(): StatusResponse
//region Approve
/**
* Address of the 1inch router that must be trusted to spend funds for the exchange
*
* @return [ApproveSpenderResponse]
*/
@GET("approve/spender")
suspend fun approveSpender(): ApproveSpenderResponse
/**
* Generate data for calling the contract in order to allow the 1inch router to spend funds
*
* @param tokenAddress Token address you want to exchange
* @param amount The number of tokens that the 1inch router is allowed to spend.
* If not specified, it will be allowed to spend an infinite amount of tokens.
*
* @return [ApproveCalldataResponse] Transaction body to allow the exchange with the 1inch router
*/
@GET("approve/transaction")
suspend fun approveTransaction(
@Query("tokenAddress") tokenAddress: String,
@Query("amount") amount: String? = null,
): ApproveCalldataResponse
/**
* Get the number of tokens that the 1inch router is allowed to spend
*
* @param tokenAddress Token address you want to exchange
* @param walletAddress Wallet address for which you want to check
*
* @return [AllowanceResponse]
*/
@GET("approve/allowance")
suspend fun approveAllowance(
@Query("tokenAddress") tokenAddress: String,
@Query("walletAddress") walletAddress: String,
): Response<AllowanceResponse>
//endregion Approve
//region Info
/**
* List of tokens that are available for swap in the 1inch Aggregation protocol
*
* @return [TokensResponse]
*/
@GET("tokens")
suspend fun tokensAvailable(): TokensResponse
/**
* List of liquidity sources that are available for routing in the 1inch Aggregation protocol
*
* @return
*/
@GET("liquidity-sources")
suspend fun liquiditySources(): ProtocolsResponse
//endregion Info
//region Swap
/**
* Find the best quote to exchange via 1inch router
*
* @param fromTokenAddress Example : 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE
* @param toTokenAddress Example : 0x111111111117dc0aa78b770fa6a738034120c302
* @param amount amount of a token to sell, set in minimal divisible units e.g.:
* 1.00 DAI set as 1000000000000000000
* 51.03 USDC set as 51030000
*
* @param protocols default: all
* @param fee this percentage of fromTokenAddress token amount will be sent to referrerAddress,
* the rest will be used as input for a swap
* Min: 0; max: 3; Max: 0; max: 3; default: 0; !should be the same for quote and swap!
*
* @param gasLimit maximum amount of gas for a swap;
* @param connectorTokens token-connectors can be specified via this parameter.
* The more is set the longer route estimation will take.
* If not set, default token-connectors will be usedmax: 5; !should be the same for quote and swap!
*
* @param complexityLevel maximum number of token-connectors to be used in a transaction.
* The more is used the longer route estimation will take
* min: 0; max: 3; default: 2; !should be the same for quote and swap!
*
* @param mainRouteParts default: 10; max: 50 !should be the same for quote and swap!
* @param parts limit maximum number of parts each main route parts can be split into;
* should be the same for a quote and swap
* default: 20; max: 100
*
* @param gasPrice 1inch takes in account gas expenses to determine exchange route.
* It is important to use the same gas price on the quote and swap methods.
* Gas price set in wei: 12.5 GWEI set as 12500000000
* default: fast from network
*
* @return [QuoteResponse]
*/
@GET("quote")
suspend fun quote(
@Query("src") fromTokenAddress: String,
@Query("dst") toTokenAddress: String,
@Query("amount") amount: String,
@Query("protocols") protocols: String? = null,
@Query("fee") fee: String? = null,
@Query("gasLimit") gasLimit: String? = null,
@Query("connectorTokens") connectorTokens: String? = null,
@Query("complexityLevel") complexityLevel: String? = null,
@Query("mainRouteParts") mainRouteParts: String? = null,
@Query("parts") parts: String? = null,
@Query("gasPrice") gasPrice: String? = null,
@Query("includeTokensInfo") includeTokensInfo: Boolean = true,
): Response<QuoteResponse>
/**
* Generate data for calling the 1inch router for exchange
*
* @param fromTokenAddress Example : 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE
* @param toTokenAddress Example : 0x111111111117dc0aa78b770fa6a738034120c302
* @param amount amount of a token to sell, set in minimal divisible units e.g.:
* 1.00 DAI set as 1000000000000000000
* 51.03 USDC set as 51030000
*
* @param fromAddress The address that calls the 1inch contract
* @param slippage limit of price slippage you are willing to accept in percentage, may be set with decimals.
* &slippage=0.5 means 0.5% slippage is acceptable. Low values increase chances that transaction will fail,
* high values increase chances of front running. min: 0; max: 50;
*
* @param protocols default: all
* @param destinationAddress Receiver of destination currency. default: fromAddress
* @param fee this percentage of fromTokenAddress token amount will be sent to referrerAddress,
* the rest will be used as input for a swap
* Min: 0; max: 3; Max: 0; max: 3; default: 0; !should be the same for quote and swap!
*
* @param permit https://eips.ethereum.org/EIPS/eip-2612
* @param compatibilityMode Allows to build calldata without optimized routers
* @param burnChi If true, CHI will be burned from fromAddress to compensate gas.
* Check CHI balance and allowance before turning that on. CHI should be approved for the spender address
*
* @param connectorTokens token-connectors can be specified via this parameter.
* The more is set the longer route estimation will take.
* If not set, default token-connectors will be usedmax: 5; !should be the same for quote and swap!
*
* @param complexityLevel maximum number of token-connectors to be used in a transaction.
* The more is used the longer route estimation will take
* min: 0; max: 3; default: 2; !should be the same for quote and swap!
*
* @param mainRouteParts default: 10; max: 50 !should be the same for quote and swap!
* @param parts limit maximum number of parts each main route parts can be split into;
* should be the same for a quote and swap
* default: 20; max: 100
*
* @param gasLimit maximum amount of gas for a swap;
* @param gasPrice 1inch takes in account gas expenses to determine exchange route.
* It is important to use the same gas price on the quote and swap methods.
* Gas price set in wei: 12.5 GWEI set as 12500000000
* default: fast from network
*
* @return [SwapResponse]
*/
@GET("swap")
suspend fun swap(
@Query("src") fromTokenAddress: String,
@Query("dst") toTokenAddress: String,
@Query("amount") amount: String,
@Query("from") fromAddress: String,
@Query("slippage") slippage: Int,
@Query("protocols") protocols: String? = null,
@Query("receiver") destinationAddress: String? = null,
@Query("referrer") referrerAddress: String? = null,
@Query("fee") fee: String? = null,
@Query("disableEstimate") disableEstimate: Boolean? = null,
@Query("permit") permit: String? = null,
@Query("compatibility") compatibilityMode: Boolean? = null,
@Query("burnChi") burnChi: Boolean? = null,
@Query("allowPartialFill") allowPartialFill: Boolean? = null,
@Query("parts") parts: String? = null,
@Query("mainRouteParts") mainRouteParts: String? = null,
@Query("connectorTokens") connectorTokens: String? = null,
@Query("complexityLevel") complexityLevel: String? = null,
@Query("gasLimit") gasLimit: String? = null,
@Query("gasPrice") gasPrice: String? = null,
@Query("includeTokensInfo") includeTokensInfo: Boolean = true,
): Response<SwapResponse>
//endregion Swap
}

View file

@ -1,14 +0,0 @@
package com.tangem.datasource.api.oneinch
class OneInchApiFactory {
private val oneInchApiMap = mutableMapOf<String, OneInchApi>()
fun putApi(networkId: String, api: OneInchApi) {
oneInchApiMap[networkId] = api
}
fun getApi(networkId: String): OneInchApi {
return oneInchApiMap[networkId] ?: error("no api found for networkId $networkId")
}
}

View file

@ -1,35 +0,0 @@
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 com.tangem.datasource.di.NetworkMoshi
import retrofit2.HttpException
import retrofit2.Response
import javax.inject.Inject
class OneInchErrorsHandler @Inject constructor(@NetworkMoshi moshi: Moshi) {
private val errorMoshiAdapter = moshi.adapter(SwapErrorDto::class.java)
@Throws(OneIncResponseException::class)
fun <T> handleOneInchResponse(response: Response<T>): T {
return if (response.isSuccessful) {
response.body() ?: error("response body is null")
} else {
when (response.code()) {
HTTP_CODE_400 -> {
val swapErrorDto = errorMoshiAdapter.fromJson(response.errorBody()?.string() ?: "")
throw OneIncResponseException(swapErrorDto ?: throw HttpException(response))
}
else -> {
throw HttpException(response)
}
}
}
}
companion object {
private const val HTTP_CODE_400 = 400
}
}

View file

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

View file

@ -1,7 +0,0 @@
package com.tangem.datasource.api.oneinch.models
import com.squareup.moshi.Json
data class AllowanceResponse(
@Json(name = "allowance") val allowance: String,
)

View file

@ -1,18 +0,0 @@
package com.tangem.datasource.api.oneinch.models
import com.squareup.moshi.Json
/**
* Approve calldata response
*
* @property data The encoded data to call the approve method on the swapped token contract
* @property gasPrice Gas price for fast transaction processing
* @property toAddress Token address that will be allowed to exchange through 1inch router
* @property value Native token value in WEI (for approve is always 0)
*/
data class ApproveCalldataResponse(
@Json(name = "data") val data: String,
@Json(name = "gasPrice") val gasPrice: String,
@Json(name = "to") val toAddress: String,
@Json(name = "value") val value: String,
)

View file

@ -1,12 +0,0 @@
package com.tangem.datasource.api.oneinch.models
import com.squareup.moshi.Json
/**
* Approve spender response
*
* @property address Address of the 1inch router that must be trusted to spend funds for the exchange
*/
data class ApproveSpenderResponse(
@Json(name = "address") val address: String,
)

View file

@ -1,13 +0,0 @@
package com.tangem.datasource.api.oneinch.models
import com.squareup.moshi.Json
/**
* Path view dto
*/
data class PathViewDto(
@Json(name = "name") val name: String,
@Json(name = "part") val part: Int,
@Json(name = "fromTokenAddress") val fromTokenAddress: String,
@Json(name = "toTokenAddress") val toTokenAddress: String,
)

View file

@ -1,27 +0,0 @@
package com.tangem.datasource.api.oneinch.models
import com.squareup.moshi.Json
/**
* Protocols response
*
* @property protocols List of protocols that are available for routing in the 1inch Aggregation protocol
*/
data class ProtocolsResponse(
@Json(name = "protocols") val protocols: List<ProtocolImageDto>,
)
/**
* Protocol image
*
* @property id Protocol id
* @property title Protocol title
* @property image Protocol logo image
* @property imageColor Protocol logo image in color
*/
data class ProtocolImageDto(
@Json(name = "id") val id: String,
@Json(name = "title") val title: String,
@Json(name = "img") val image: String,
@Json(name = "img_color") val imageColor: String,
)

View file

@ -1,14 +0,0 @@
package com.tangem.datasource.api.oneinch.models
import com.squareup.moshi.Json
/**
* Quote response
*
* @property toToken Destination token info
* @property toTokenAmount Expected amount of destination token
*/
data class QuoteResponse(
@Json(name = "toToken") val toToken: TokenOneInchDto,
@Json(name = "toAmount") val toTokenAmount: String,
)

View file

@ -1,8 +0,0 @@
package com.tangem.datasource.api.oneinch.models
import com.squareup.moshi.Json
data class StatusResponse(
@Json(name = "status") val status: String,
@Json(name = "provider") val provider: String,
)

View file

@ -1,42 +0,0 @@
package com.tangem.datasource.api.oneinch.models
import com.squareup.moshi.Json
/**
* Swap error dto
*
* One of the following errors:
*
* -Insufficient liquidity
* -Cannot estimate
* -You may not have enough ETH balance for gas fee
* -FromTokenAddress cannot be equals to toTokenAddress
* -Cannot estimate. Don't forget about miner fee. Try to leave the buffer of ETH for gas
* -Not enough balance
* -Not enough allowance
*
* @property statusCode HTTP code
* @property error Error code description
* @property description Error description (one of the following)
* @property requestId Request id
* @property meta Meta information
* @constructor Create empty Swap error dto
*/
data class SwapErrorDto(
@Json(name = "statusCode") val statusCode: Int,
@Json(name = "error") val error: String,
@Json(name = "description") val description: String,
@Json(name = "requestId") val requestId: String,
@Json(name = "meta") val meta: List<NestErrorMeta>,
)
/**
* Nest error meta
*
* @property type Type of field
* @property value Value of field
*/
data class NestErrorMeta(
@Json(name = "type") val type: String,
@Json(name = "value") val value: String,
)

View file

@ -1,10 +0,0 @@
package com.tangem.datasource.api.oneinch.models
import com.squareup.moshi.Json
data class SwapResponse(
@Json(name = "fromToken") val fromToken: TokenOneInchDto,
@Json(name = "toToken") val toToken: TokenOneInchDto,
@Json(name = "toAmount") val toTokenAmount: String,
@Json(name = "tx") val transaction: TransactionDto,
)

View file

@ -1,20 +0,0 @@
package com.tangem.datasource.api.oneinch.models
import com.squareup.moshi.Json
/**
* Token one inch
*
* @property symbol token symbol
* @property name token name
* @property address token address
* @property decimals token decimals
* @property logoURI token logo image url
*/
data class TokenOneInchDto(
@Json(name = "symbol") val symbol: String,
@Json(name = "name") val name: String,
@Json(name = "address") val address: String,
@Json(name = "decimals") val decimals: Int,
@Json(name = "logoURI") val logoURI: String,
)

View file

@ -1,12 +0,0 @@
package com.tangem.datasource.api.oneinch.models
import com.squareup.moshi.Json
/**
* Tokens response
*
* @property tokens List of supported tokens
*/
data class TokensResponse(
@Json(name = "tokens") val tokens: Map<String, TokenOneInchDto>,
)

View file

@ -1,23 +0,0 @@
package com.tangem.datasource.api.oneinch.models
import com.squareup.moshi.Json
/**
* Transaction dto
*
* @property fromAddress transactions will be sent from this address
* @property toAddress transactions will be sent to our(1inch) contract address
* @property data The encoded data to call the approve method on the swapped token contract
* @property value Native token value in WEI (for approve is always 0)
* @property gasPrice maximum amount of gas for a swap default: 11500000; max: 11500000
* @property gas estimated amount of the gas limit, increase this value by 25%
* @constructor Create empty Transaction dto
*/
data class TransactionDto(
@Json(name = "from") val fromAddress: String,
@Json(name = "to") val toAddress: String,
@Json(name = "data") val data: String,
@Json(name = "value") val value: String,
@Json(name = "gasPrice") val gasPrice: String,
@Json(name = "gas") val gas: String,
)

View file

@ -103,11 +103,9 @@ internal class ConfigManagerImpl @Inject constructor() : ConfigManager {
amplitudeApiKey = configValues.amplitudeApiKey,
shopify = configValues.shopifyShop,
sprinklr = configValues.sprinklr,
swapReferrerAccount = configValues.swapReferrerAccount,
walletConnectProjectId = configValues.walletConnectProjectId,
tangemComAuthorization = configValues.tangemComAuthorization,
tangemExpressApiKey = configValues.tangemExpressApiKey,
oneInchApiKey = configValues.oneInchApiKey,
express = configValues.express,
)
}

View file

@ -16,9 +16,7 @@ data class Config(
val isCreatingTwinCardsAllowed: Boolean = false,
val shopify: ShopifyShop? = null,
val sprinklr: SprinklrConfig? = null,
val swapReferrerAccount: SwapReferrerAccount? = null,
val walletConnectProjectId: String = "",
val tangemComAuthorization: String? = null,
val tangemExpressApiKey: String = "",
val oneInchApiKey: String = "",
val express: ExpressModel? = null,
)

View file

@ -0,0 +1,12 @@
package com.tangem.datasource.config.models
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class ExpressModel(
@Json(name = "apiKey")
val apiKey: String,
@Json(name = "signVerifierPublicKey")
val signVerifierPublicKey: String,
)

View file

@ -35,14 +35,12 @@ class ConfigValueModel(
val sprinklr: SprinklrConfig?,
val tronGridApiKey: String,
val amplitudeApiKey: String,
val swapReferrerAccount: SwapReferrerAccount?,
val kaspaSecondaryApiUrl: String,
val walletConnectProjectId: String,
val tangemComAuthorization: String?,
val chiaFireAcademyApiKey: String?,
val chiaTangemApiKey: String?,
val tangemExpressApiKey: String,
val oneInchApiKey: String,
val express: ExpressModel?,
)
@JsonClass(generateAdapter = true)
@ -84,11 +82,6 @@ data class AppsFlyer(
val appsFlyerAppID: String,
)
data class SwapReferrerAccount(
val address: String,
val fee: String,
)
class ConfigModel(
val features: FeatureModel?,
val configValues: ConfigValueModel?,

View file

@ -0,0 +1,6 @@
package com.tangem.datasource.crypto
interface DataSignatureVerifier {
fun verifySignature(signature: String, data: String): Boolean
}

View file

@ -0,0 +1,17 @@
package com.tangem.datasource.crypto
import com.tangem.common.extensions.hexToBytes
import com.tangem.crypto.CryptoUtils
import com.tangem.datasource.config.ConfigManager
internal class Sha256SignatureVerifier(private val configManager: ConfigManager) : DataSignatureVerifier {
override fun verifySignature(signature: String, data: String): Boolean {
val pubKey = configManager.config.express?.signVerifierPublicKey ?: return false
return CryptoUtils.verify(
publicKey = pubKey.hexToBytes().takeLast(n = 65).toByteArray(),
message = data.toByteArray(),
signature = signature.hexToBytes(),
)
}
}

View file

@ -1,96 +0,0 @@
package com.tangem.datasource.di
import android.content.Context
import com.squareup.moshi.Moshi
import com.tangem.datasource.api.oneinch.OneInchApi
import com.tangem.datasource.api.oneinch.OneInchApiFactory
import com.tangem.datasource.utils.RequestHeader
import com.tangem.datasource.utils.addHeaders
import com.tangem.datasource.utils.addLoggers
import com.tangem.lib.auth.AuthBearerProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
class OneInchApisModule {
@Provides
@Singleton
fun provideOneInchApiFactory(
@NetworkMoshi moshi: Moshi,
@ApplicationContext context: Context,
auth1Inch: AuthBearerProvider,
): OneInchApiFactory {
val networks = mapOf(
ETH_NETWORK to ONE_INCH_ETH_PATH,
BSC_NETWORK to ONE_INCH_BSC_PATH,
POLYGON_NETWORK to ONE_INCH_POLYGON_PATH,
OPTIMISM_NETWORK to ONE_INCH_OPTIMISM_PATH,
ARBITRUM_NETWORK to ONE_INCH_ARBITRUM_PATH,
GNOSIS_NETWORK to ONE_INCH_GNOSIS_PATH,
AVALANCHE_NETWORK to ONE_INCH_AVALANCHE_PATH,
FANTOM_NETWORK to ONE_INCH_FANTOM_PATH,
)
val apiFactory = OneInchApiFactory()
for ((network, path) in networks) {
apiFactory.putApi(
networkId = network,
api = createOneInchApiWithUrl("$ONE_INCH_BASE_URL$path", moshi, context, auth1Inch),
)
}
return apiFactory
}
private fun createOneInchApiWithUrl(
url: String,
moshi: Moshi,
context: Context,
auth1Inch: AuthBearerProvider,
): OneInchApi {
return Retrofit.Builder()
.addConverterFactory(
MoshiConverterFactory.create(moshi),
)
.baseUrl(url)
.client(
OkHttpClient.Builder()
.addHeaders(RequestHeader.AuthBearerHeader(auth1Inch))
.addLoggers(context)
.build(),
)
.build()
.create(OneInchApi::class.java)
}
companion object {
private const val ONE_INCH_BASE_URL = "https://api.1inch.dev/swap/v5.2/"
private const val ONE_INCH_ETH_PATH = "1/"
private const val ONE_INCH_BSC_PATH = "56/"
private const val ONE_INCH_POLYGON_PATH = "137/"
private const val ONE_INCH_OPTIMISM_PATH = "10/"
private const val ONE_INCH_ARBITRUM_PATH = "42161/"
private const val ONE_INCH_GNOSIS_PATH = "100/"
private const val ONE_INCH_AVALANCHE_PATH = "43114/"
private const val ONE_INCH_FANTOM_PATH = "250/"
private const val ETH_NETWORK = "ethereum"
private const val BSC_NETWORK = "binance-smart-chain"
private const val POLYGON_NETWORK = "polygon-pos"
private const val OPTIMISM_NETWORK = "optimistic-ethereum"
private const val ARBITRUM_NETWORK = "arbitrum-one"
private const val GNOSIS_NETWORK = "xdai"
private const val AVALANCHE_NETWORK = "avalanche"
private const val FANTOM_NETWORK = "fantom"
}
}

View file

@ -0,0 +1,21 @@
package com.tangem.datasource.di
import com.tangem.datasource.config.ConfigManager
import com.tangem.datasource.crypto.DataSignatureVerifier
import com.tangem.datasource.crypto.Sha256SignatureVerifier
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 SecurityModule {
@Provides
@Singleton
fun provideDataSignatureVerifier(configManager: ConfigManager): DataSignatureVerifier {
return Sha256SignatureVerifier(configManager)
}
}

View file

@ -37,6 +37,9 @@ dependencies {
/** Tangem SDKs */
implementation(deps.tangem.blockchain)
/** Others */
implementation(deps.timber)
/** DI */
implementation(deps.hilt.android)

View file

@ -5,6 +5,7 @@ import arrow.core.left
import arrow.core.raise.catch
import arrow.core.raise.either
import arrow.core.right
import com.squareup.moshi.Moshi
import com.tangem.blockchain.common.*
import com.tangem.blockchain.extensions.Result
import com.tangem.data.tokens.utils.CryptoCurrencyFactory
@ -13,12 +14,12 @@ import com.tangem.datasource.api.common.response.ApiResponseError
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.express.TangemExpressApi
import com.tangem.datasource.api.express.models.request.PairsRequestBody
import com.tangem.datasource.api.express.models.response.ExchangeDataResponseWithTxDetails
import com.tangem.datasource.api.express.models.response.SwapPair
import com.tangem.datasource.api.express.models.response.SwapPairsWithProviders
import com.tangem.datasource.api.oneinch.OneInchApi
import com.tangem.datasource.api.oneinch.OneInchApiFactory
import com.tangem.datasource.api.express.models.response.TxDetails
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.config.ConfigManager
import com.tangem.datasource.crypto.DataSignatureVerifier
import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.tokens.model.CryptoCurrency
@ -34,20 +35,23 @@ import com.tangem.feature.swap.domain.models.domain.*
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.async
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.io.IOException
import java.math.BigDecimal
import java.util.UUID
import javax.inject.Inject
import com.tangem.datasource.api.express.models.request.LeastTokenInfo as NetworkLeastTokenInfo
@Suppress("LongParameterList", "LargeClass")
internal class SwapRepositoryImpl @Inject constructor(
internal class DefaultSwapRepository @Inject constructor(
private val tangemTechApi: TangemTechApi,
private val tangemExpressApi: TangemExpressApi,
private val oneInchApiFactory: OneInchApiFactory,
private val coroutineDispatcher: CoroutineDispatcherProvider,
private val configManager: ConfigManager,
private val walletManagersFacade: WalletManagersFacade,
private val walletsStateHolder: WalletsStateHolder,
private val errorsDataConverter: ErrorsDataConverter,
private val dataSignatureVerifier: DataSignatureVerifier,
moshi: Moshi,
) : SwapRepository {
private val tokensConverter = TokensConverter()
@ -56,6 +60,7 @@ internal class SwapRepositoryImpl @Inject constructor(
private val swapPairInfoConverter = SwapPairInfoConverter()
private val cryptoCurrencyFactory = CryptoCurrencyFactory()
private val exchangeStatusConverter = ExchangeStatusConverter()
private val txDetailsMoshiAdapter = moshi.adapter(TxDetails::class.java)
override suspend fun getPairs(
initialCurrency: LeastTokenInfo,
@ -253,12 +258,6 @@ internal class SwapRepositoryImpl @Inject constructor(
}
}
override suspend fun addressForTrust(networkId: String): String {
return withContext(coroutineDispatcher.io) {
getOneInchApi(networkId).approveSpender().address
}
}
override suspend fun getExchangeData(
fromContractAddress: String,
fromNetwork: String,
@ -273,6 +272,7 @@ internal class SwapRepositoryImpl @Inject constructor(
): Either<DataError, SwapDataModel> {
return withContext(coroutineDispatcher.io) {
try {
val requestId = UUID.randomUUID().toString()
val response = tangemExpressApi.getExchangeData(
fromContractAddress = fromContractAddress,
fromNetwork = fromNetwork,
@ -284,16 +284,36 @@ internal class SwapRepositoryImpl @Inject constructor(
providerId = providerId,
rateType = rateType.name.lowercase(),
toAddress = toAddress,
requestId = requestId,
).getOrThrow()
expressDataConverter.convert(response).right()
if (dataSignatureVerifier.verifySignature(response.signature, response.txDetailsJson)) {
val txDetails = parseTxDetails(response.txDetailsJson)
?: return@withContext DataError.UnknownError.left()
if (txDetails.requestId != requestId) {
return@withContext DataError.InvalidRequestIdError().left()
}
expressDataConverter.convert(
ExchangeDataResponseWithTxDetails(
dataResponse = response,
txDetails = txDetails,
),
).right()
} else {
DataError.InvalidSignatureError().left()
}
} catch (ex: Exception) {
getDataError(ex).left()
}
}
}
override fun getTangemFee(): Double {
return configManager.config.swapReferrerAccount?.fee?.toDoubleOrNull() ?: 0.0
private fun parseTxDetails(txDetailsJson: String): TxDetails? {
return try {
txDetailsMoshiAdapter.fromJson(txDetailsJson)
} catch (e: IOException) {
Timber.e(e, "error parsing txDetailsJson")
null
}
}
override suspend fun getAllowance(
@ -367,10 +387,6 @@ internal class SwapRepositoryImpl @Inject constructor(
)
}
private fun getOneInchApi(networkId: String): OneInchApi {
return oneInchApiFactory.getApi(networkId)
}
override fun getNativeTokenForNetwork(networkId: String): CryptoCurrency {
val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" }

View file

@ -1,36 +1,42 @@
package com.tangem.feature.swap.converters
import com.tangem.datasource.api.express.models.response.ExchangeDataResponse
import com.tangem.datasource.api.express.models.response.ExchangeDataResponseWithTxDetails
import com.tangem.datasource.api.express.models.response.TxDetails
import com.tangem.datasource.api.express.models.response.TxType
import com.tangem.feature.swap.domain.models.createFromAmountWithOffset
import com.tangem.feature.swap.domain.models.domain.ExpressTransactionModel
import com.tangem.feature.swap.domain.models.domain.SwapDataModel
import com.tangem.utils.converter.Converter
class ExpressDataConverter : Converter<ExchangeDataResponse, SwapDataModel> {
internal class ExpressDataConverter : Converter<ExchangeDataResponseWithTxDetails, SwapDataModel> {
override fun convert(value: ExchangeDataResponse): SwapDataModel {
override fun convert(value: ExchangeDataResponseWithTxDetails): SwapDataModel {
val data = value.dataResponse
return SwapDataModel(
toTokenAmount = createFromAmountWithOffset(value.toAmount, value.toDecimals),
transaction = convertTransaction(value),
toTokenAmount = createFromAmountWithOffset(data.toAmount, data.toDecimals),
transaction = convertTransaction(value.txDetails, data),
)
}
private fun convertTransaction(transactionDto: ExchangeDataResponse): ExpressTransactionModel {
private fun convertTransaction(
transactionDto: TxDetails,
dataResponse: ExchangeDataResponse,
): ExpressTransactionModel {
return if (transactionDto.txType == TxType.SWAP) {
ExpressTransactionModel.DEX(
fromAmount = createFromAmountWithOffset(transactionDto.fromAmount, transactionDto.fromDecimals),
toAmount = createFromAmountWithOffset(transactionDto.toAmount, transactionDto.toDecimals),
txId = transactionDto.txId,
fromAmount = createFromAmountWithOffset(dataResponse.fromAmount, dataResponse.fromDecimals),
toAmount = createFromAmountWithOffset(dataResponse.toAmount, dataResponse.toDecimals),
txId = dataResponse.txId,
txTo = transactionDto.txTo,
txFrom = requireNotNull(transactionDto.txFrom),
txData = requireNotNull(transactionDto.txData),
)
} else {
ExpressTransactionModel.CEX(
fromAmount = createFromAmountWithOffset(transactionDto.fromAmount, transactionDto.fromDecimals),
toAmount = createFromAmountWithOffset(transactionDto.toAmount, transactionDto.toDecimals),
txId = transactionDto.txId,
fromAmount = createFromAmountWithOffset(dataResponse.fromAmount, dataResponse.fromDecimals),
toAmount = createFromAmountWithOffset(dataResponse.toAmount, dataResponse.toDecimals),
txId = dataResponse.txId,
txTo = transactionDto.txTo,
externalTxId = requireNotNull(transactionDto.externalTxId),
externalTxUrl = requireNotNull(transactionDto.externalTxUrl),

View file

@ -3,18 +3,17 @@ package com.tangem.feature.swap.di
import com.squareup.moshi.Moshi
import com.tangem.datasource.api.express.TangemExpressApi
import com.tangem.datasource.api.express.models.response.ExpressErrorResponse
import com.tangem.datasource.api.oneinch.OneInchApiFactory
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.config.ConfigManager
import com.tangem.datasource.crypto.DataSignatureVerifier
import com.tangem.datasource.di.NetworkMoshi
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.legacy.WalletsStateHolder
import com.tangem.feature.swap.SwapRepositoryImpl
import com.tangem.feature.swap.converters.ErrorsDataConverter
import com.tangem.feature.swap.DefaultSwapTransactionRepository
import com.tangem.feature.swap.domain.api.SwapRepository
import com.tangem.feature.swap.DefaultSwapRepository
import com.tangem.feature.swap.converters.ErrorsDataConverter
import com.tangem.feature.swap.domain.SwapTransactionRepository
import com.tangem.feature.swap.domain.api.SwapRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
@ -31,22 +30,22 @@ internal class SwapDataModule {
internal fun provideSwapRepository(
tangemTechApi: TangemTechApi,
tangemExpressApi: TangemExpressApi,
oneInchApiFactory: OneInchApiFactory,
coroutineDispatcher: CoroutineDispatcherProvider,
configManager: ConfigManager,
dataSignature: DataSignatureVerifier,
walletManagerFacade: WalletManagersFacade,
walletsStateHolder: WalletsStateHolder,
errorsDataConverter: ErrorsDataConverter,
@NetworkMoshi moshi: Moshi,
): SwapRepository {
return SwapRepositoryImpl(
return DefaultSwapRepository(
tangemTechApi = tangemTechApi,
tangemExpressApi = tangemExpressApi,
oneInchApiFactory = oneInchApiFactory,
coroutineDispatcher = coroutineDispatcher,
configManager = configManager,
walletManagersFacade = walletManagerFacade,
walletsStateHolder = walletsStateHolder,
errorsDataConverter = errorsDataConverter,
dataSignatureVerifier = dataSignature,
moshi = moshi,
)
}

View file

@ -33,19 +33,6 @@ interface SwapRepository {
rateType: RateType,
): Either<DataError, QuoteModel>
/**
* Returns address of 1inch router that must be trusted
*
* @return address
*/
suspend fun addressForTrust(networkId: String): String
/**
* Returns a tangem fee for swap in percents
* Example: 0.35%
*/
fun getTangemFee(): Double
@Suppress("LongParameterList")
@Throws(IllegalStateException::class)
suspend fun getAllowance(

View file

@ -32,6 +32,10 @@ sealed class DataError {
data class UnknownErrorWithCode(override val code: Int) : DataError()
data class InvalidSignatureError(override val code: Int = 990) : DataError()
data class InvalidRequestIdError(override val code: Int = 991) : DataError()
object UnknownError : DataError() {
override val code: Int = -1
}

View file

@ -25,7 +25,6 @@ sealed interface SwapState {
val permissionState: PermissionDataState = PermissionDataState.Empty,
val swapDataModel: SwapDataModel? = null,
val txFee: TxFeeState,
val tangemFee: Double,
) : SwapState
data class EmptyAmountState(

View file

@ -653,11 +653,6 @@ internal class SwapInteractorImpl @Inject constructor(
return repository.getNativeTokenForNetwork(networkId)
}
@Deprecated("used in old swap mechanism")
private fun getTangemFee(): Double {
return repository.getTangemFee()
}
private fun getTokenDecimals(token: CryptoCurrency): Int {
return if (token is CryptoCurrency.Token) {
token.decimals
@ -1013,7 +1008,6 @@ internal class SwapInteractorImpl @Inject constructor(
),
networkCurrency = userWalletManager.getNetworkCurrency(networkId),
swapDataModel = swapData,
tangemFee = getTangemFee(),
txFee = txFeeState,
)
}

View file

@ -99,7 +99,7 @@ internal class StateBuilder(
return uiStateHolder.copy(
sendCardData = SwapCardState.SwapCardData(
type = TransactionCardType.ReadOnly(
headerResId = R.string.exchange_send_view_header
headerResId = R.string.exchange_send_view_header,
),
amountTextFieldValue = TextFieldValue(
text = "0",