Updated on 2026-08-14
This commit is contained in:
commit
aaf9052e88
48 changed files with 215 additions and 712 deletions
|
|
@ -1 +1 @@
|
|||
Subproject commit 6ef8ce45d183905b5752e2d33c1d8bf2f4bcace6
|
||||
Subproject commit 0be5650f8fbcdff22c23fe2e2c7d754c4275a6f6
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.feature.onboarding.data.di
|
||||
package com.tangem.tap.di.domain
|
||||
|
||||
import android.content.Context
|
||||
import com.tangem.crypto.bip39.Wordlist
|
||||
|
|
@ -14,11 +14,11 @@ import javax.inject.Singleton
|
|||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
class OnboardingDataModule {
|
||||
class MnemonicModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSeedPhraseSdkRepository(@ApplicationContext context: Context): MnemonicRepository {
|
||||
fun provideMnemonicRepository(@ApplicationContext context: Context): MnemonicRepository {
|
||||
return DefaultMnemonicRepository(Wordlist.getWordlist(context))
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ import com.tangem.datasource.local.userwallet.UserWalletsStore
|
|||
import com.tangem.datasource.local.walletmanager.WalletManagersStore
|
||||
import com.tangem.domain.walletmanager.DefaultWalletManagersFacade
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.feature.onboarding.data.MnemonicRepository
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -24,6 +25,7 @@ internal object WalletManagersFacadeModule {
|
|||
walletManagersStore: WalletManagersStore,
|
||||
userWalletsStore: UserWalletsStore,
|
||||
configManager: ConfigManager,
|
||||
mnemonicRepository: MnemonicRepository,
|
||||
assetReader: AssetReader,
|
||||
@SdkMoshi moshi: Moshi,
|
||||
): WalletManagersFacade {
|
||||
|
|
@ -33,6 +35,7 @@ internal object WalletManagersFacadeModule {
|
|||
configManager = configManager,
|
||||
assetReader = assetReader,
|
||||
moshi = moshi,
|
||||
mnemonic = mnemonicRepository.generateDefaultMnemonic(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -21,7 +21,7 @@ class DemoTransactionSender(private val walletManager: WalletManager) : Transact
|
|||
)
|
||||
}
|
||||
|
||||
override suspend fun estimateFee(amount: Amount): Result<TransactionFee> {
|
||||
override suspend fun estimateFee(amount: Amount, destination: String): Result<TransactionFee> {
|
||||
return getFee(amount, walletManager.wallet.address)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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>,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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?,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem.datasource.crypto
|
||||
|
||||
interface DataSignatureVerifier {
|
||||
|
||||
fun verifySignature(signature: String, data: String): Boolean
|
||||
}
|
||||
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -10,11 +10,13 @@ import com.tangem.blockchain.blockchains.solana.RentProvider
|
|||
import com.tangem.blockchain.common.*
|
||||
import com.tangem.blockchain.common.address.Address
|
||||
import com.tangem.blockchain.common.address.AddressType
|
||||
import com.tangem.blockchain.common.address.EstimationFeeAddressFactory
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.blockchain.common.txhistory.TransactionHistoryRequest
|
||||
import com.tangem.blockchain.extensions.Result
|
||||
import com.tangem.blockchain.extensions.SimpleResult
|
||||
import com.tangem.crypto.bip39.Mnemonic
|
||||
import com.tangem.crypto.hdWallet.DerivationPath
|
||||
import com.tangem.datasource.asset.AssetReader
|
||||
import com.tangem.datasource.config.ConfigManager
|
||||
|
|
@ -45,6 +47,7 @@ class DefaultWalletManagersFacade(
|
|||
private val walletManagersStore: WalletManagersStore,
|
||||
private val userWalletsStore: UserWalletsStore,
|
||||
configManager: ConfigManager,
|
||||
mnemonic: Mnemonic,
|
||||
assetReader: AssetReader,
|
||||
moshi: Moshi,
|
||||
) : WalletManagersFacade {
|
||||
|
|
@ -55,6 +58,7 @@ class DefaultWalletManagersFacade(
|
|||
private val sdkTokenConverter by lazy { SdkTokenConverter() }
|
||||
private val txHistoryStateConverter by lazy { SdkTransactionHistoryStateConverter() }
|
||||
private val txHistoryItemConverter by lazy { SdkTransactionHistoryItemConverter(assetReader, moshi) }
|
||||
private val estimationFeeAddressFactory by lazy { EstimationFeeAddressFactory(mnemonic) }
|
||||
|
||||
override suspend fun update(
|
||||
userWalletId: UserWalletId,
|
||||
|
|
@ -444,7 +448,13 @@ class DefaultWalletManagersFacade(
|
|||
blockchain = blockchain,
|
||||
derivationPath = network.derivationPath.value,
|
||||
)
|
||||
return (walletManager as? TransactionSender)?.estimateFee(amount = amount)
|
||||
|
||||
val destination = estimationFeeAddressFactory.makeAddress(blockchain)
|
||||
|
||||
return (walletManager as? TransactionSender)?.estimateFee(
|
||||
amount = amount,
|
||||
destination = destination,
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun validateTransaction(
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import com.tangem.crypto.bip39.Wordlist
|
|||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class DefaultMnemonicRepository(
|
||||
class DefaultMnemonicRepository(
|
||||
private val bip39Wordlist: Wordlist,
|
||||
) : MnemonicRepository {
|
||||
|
||||
|
|
|
|||
|
|
@ -37,6 +37,9 @@ dependencies {
|
|||
/** Tangem SDKs */
|
||||
implementation(deps.tangem.blockchain)
|
||||
|
||||
/** Others */
|
||||
implementation(deps.timber)
|
||||
|
||||
/** DI */
|
||||
implementation(deps.hilt.android)
|
||||
|
||||
|
|
|
|||
|
|
@ -5,10 +5,8 @@ import arrow.core.left
|
|||
import arrow.core.raise.catch
|
||||
import arrow.core.raise.either
|
||||
import arrow.core.right
|
||||
import com.tangem.blockchain.common.Amount
|
||||
import com.tangem.blockchain.common.Approver
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.Token
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.blockchain.common.*
|
||||
import com.tangem.blockchain.extensions.Result
|
||||
import com.tangem.data.tokens.utils.CryptoCurrencyFactory
|
||||
import com.tangem.datasource.api.common.response.ApiResponse
|
||||
|
|
@ -16,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
|
||||
|
|
@ -37,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()
|
||||
|
|
@ -59,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,
|
||||
|
|
@ -256,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,
|
||||
|
|
@ -276,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,
|
||||
|
|
@ -287,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(
|
||||
|
|
@ -347,27 +364,27 @@ internal class SwapRepositoryImpl @Inject constructor(
|
|||
|
||||
return (walletManager as? Approver)?.getApproveData(
|
||||
spenderAddress,
|
||||
amount?.let { convertToAmount(it, currency, blockchain) },
|
||||
amount?.let { convertToAmount(it, currency) },
|
||||
) ?: error("Cannot cast to Approver")
|
||||
}
|
||||
|
||||
private fun convertToAmount(amount: BigDecimal, currency: CryptoCurrency, blockchain: Blockchain): Amount {
|
||||
return when (currency) {
|
||||
is CryptoCurrency.Token -> {
|
||||
Amount(value = amount, blockchain = blockchain)
|
||||
}
|
||||
is CryptoCurrency.Coin -> {
|
||||
Amount(
|
||||
currencySymbol = currency.symbol,
|
||||
value = amount,
|
||||
decimals = currency.decimals,
|
||||
private fun convertToAmount(amount: BigDecimal, currency: CryptoCurrency): Amount {
|
||||
return Amount(
|
||||
currencySymbol = currency.symbol,
|
||||
value = amount,
|
||||
decimals = currency.decimals,
|
||||
type = if (currency is CryptoCurrency.Token) {
|
||||
AmountType.Token(
|
||||
Token(
|
||||
symbol = currency.symbol,
|
||||
contractAddress = currency.contractAddress,
|
||||
decimals = currency.decimals,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getOneInchApi(networkId: String): OneInchApi {
|
||||
return oneInchApiFactory.getApi(networkId)
|
||||
} else {
|
||||
AmountType.Coin
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
override fun getNativeTokenForNetwork(networkId: String): CryptoCurrency {
|
||||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
package com.tangem.feature.swap.models
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.notifications.NotificationConfig
|
||||
import com.tangem.core.ui.components.states.Item
|
||||
|
|
@ -99,13 +101,17 @@ sealed interface FeeSelectState {
|
|||
|
||||
sealed interface TransactionCardType {
|
||||
|
||||
data class SendCard(
|
||||
val headerResId: Int
|
||||
|
||||
data class Inputtable(
|
||||
val onAmountChanged: ((String) -> Unit),
|
||||
val onFocusChanged: ((Boolean) -> Unit),
|
||||
@StringRes override val headerResId: Int = R.string.exchange_send_view_header,
|
||||
) : TransactionCardType
|
||||
|
||||
data class ReceiveCard(
|
||||
data class ReadOnly(
|
||||
val highPriceImpact: String? = null,
|
||||
@StringRes override val headerResId: Int = R.string.exchange_receive_view_header,
|
||||
) : TransactionCardType
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ internal class StateBuilder(
|
|||
return SwapStateHolder(
|
||||
blockchainId = networkInfo.blockchainId,
|
||||
sendCardData = SwapCardState.SwapCardData(
|
||||
type = TransactionCardType.SendCard(actions.onAmountChanged, actions.onAmountSelected),
|
||||
type = TransactionCardType.Inputtable(actions.onAmountChanged, actions.onAmountSelected),
|
||||
amountEquivalent = null,
|
||||
amountTextFieldValue = null,
|
||||
token = null,
|
||||
|
|
@ -64,7 +64,7 @@ internal class StateBuilder(
|
|||
isBalanceHidden = true,
|
||||
),
|
||||
receiveCardData = SwapCardState.SwapCardData(
|
||||
type = TransactionCardType.ReceiveCard(),
|
||||
type = TransactionCardType.ReadOnly(),
|
||||
amountEquivalent = null,
|
||||
tokenIconUrl = "",
|
||||
tokenCurrency = "",
|
||||
|
|
@ -98,7 +98,9 @@ internal class StateBuilder(
|
|||
if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder
|
||||
return uiStateHolder.copy(
|
||||
sendCardData = SwapCardState.SwapCardData(
|
||||
type = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.SendCard),
|
||||
type = TransactionCardType.ReadOnly(
|
||||
headerResId = R.string.exchange_send_view_header,
|
||||
),
|
||||
amountTextFieldValue = TextFieldValue(
|
||||
text = "0",
|
||||
),
|
||||
|
|
@ -114,7 +116,7 @@ internal class StateBuilder(
|
|||
isBalanceHidden = isBalanceHiddenProvider(),
|
||||
),
|
||||
receiveCardData = SwapCardState.Empty(
|
||||
type = TransactionCardType.ReceiveCard(),
|
||||
type = TransactionCardType.ReadOnly(),
|
||||
amountEquivalent = "0 ${appCurrencyProvider.invoke().symbol}",
|
||||
amountTextFieldValue = TextFieldValue(
|
||||
text = "0",
|
||||
|
|
@ -156,7 +158,7 @@ internal class StateBuilder(
|
|||
if (uiStateHolder.receiveCardData !is SwapCardState.SwapCardData) return uiStateHolder
|
||||
return uiStateHolder.copy(
|
||||
sendCardData = SwapCardState.SwapCardData(
|
||||
type = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.SendCard),
|
||||
type = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.Inputtable),
|
||||
amountTextFieldValue = uiStateHolder.sendCardData.amountTextFieldValue,
|
||||
amountEquivalent = null,
|
||||
token = uiStateHolder.sendCardData.token,
|
||||
|
|
@ -170,7 +172,7 @@ internal class StateBuilder(
|
|||
isBalanceHidden = isBalanceHiddenProvider(),
|
||||
),
|
||||
receiveCardData = SwapCardState.SwapCardData(
|
||||
type = TransactionCardType.ReceiveCard(),
|
||||
type = TransactionCardType.ReadOnly(),
|
||||
amountTextFieldValue = null,
|
||||
amountEquivalent = null,
|
||||
token = uiStateHolder.receiveCardData.token,
|
||||
|
|
@ -220,7 +222,7 @@ internal class StateBuilder(
|
|||
val toCurrencyStatus = quoteModel.toTokenInfo.cryptoCurrencyStatus
|
||||
return uiStateHolder.copy(
|
||||
sendCardData = SwapCardState.SwapCardData(
|
||||
type = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.SendCard),
|
||||
type = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.Inputtable),
|
||||
amountTextFieldValue = uiStateHolder.sendCardData.amountTextFieldValue,
|
||||
amountEquivalent = getFormattedFiatAmount(quoteModel.fromTokenInfo.amountFiat),
|
||||
token = fromCurrencyStatus,
|
||||
|
|
@ -234,7 +236,7 @@ internal class StateBuilder(
|
|||
isBalanceHidden = isBalanceHiddenProvider(),
|
||||
),
|
||||
receiveCardData = SwapCardState.SwapCardData(
|
||||
type = TransactionCardType.ReceiveCard(),
|
||||
type = TransactionCardType.ReadOnly(),
|
||||
amountTextFieldValue = TextFieldValue(quoteModel.toTokenInfo.tokenAmount.formatToUIRepresentation()),
|
||||
amountEquivalent = getFormattedFiatAmount(quoteModel.toTokenInfo.amountFiat),
|
||||
token = toCurrencyStatus,
|
||||
|
|
@ -419,7 +421,7 @@ internal class StateBuilder(
|
|||
)
|
||||
val receiveCardData = toToken?.let {
|
||||
SwapCardState.SwapCardData(
|
||||
type = TransactionCardType.ReceiveCard(),
|
||||
type = TransactionCardType.ReadOnly(),
|
||||
amountTextFieldValue = TextFieldValue(
|
||||
text = "0",
|
||||
),
|
||||
|
|
@ -435,7 +437,7 @@ internal class StateBuilder(
|
|||
isBalanceHidden = isBalanceHiddenProvider(),
|
||||
)
|
||||
} ?: SwapCardState.Empty(
|
||||
type = TransactionCardType.ReceiveCard(),
|
||||
type = TransactionCardType.ReadOnly(),
|
||||
amountEquivalent = "0 ${appCurrencyProvider.invoke().symbol}",
|
||||
amountTextFieldValue = TextFieldValue(
|
||||
text = "0",
|
||||
|
|
@ -533,7 +535,7 @@ internal class StateBuilder(
|
|||
if (uiStateHolder.receiveCardData !is SwapCardState.SwapCardData) return uiStateHolder
|
||||
return uiStateHolder.copy(
|
||||
sendCardData = SwapCardState.SwapCardData(
|
||||
type = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.SendCard),
|
||||
type = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.Inputtable),
|
||||
amountTextFieldValue = uiStateHolder.sendCardData.amountTextFieldValue,
|
||||
amountEquivalent = emptyAmountState.zeroAmountEquivalent,
|
||||
token = uiStateHolder.sendCardData.token,
|
||||
|
|
@ -547,7 +549,7 @@ internal class StateBuilder(
|
|||
isBalanceHidden = isBalanceHiddenProvider(),
|
||||
),
|
||||
receiveCardData = SwapCardState.SwapCardData(
|
||||
type = TransactionCardType.ReceiveCard(),
|
||||
type = TransactionCardType.ReadOnly(),
|
||||
amountTextFieldValue = TextFieldValue("0"),
|
||||
amountEquivalent = emptyAmountState.zeroAmountEquivalent,
|
||||
token = uiStateHolder.receiveCardData.token,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
package com.tangem.feature.swap.ui
|
||||
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
|
|
@ -429,7 +428,7 @@ private fun MainButton(state: SwapStateHolder, onPermissionWarningClick: () -> U
|
|||
// region preview
|
||||
|
||||
private val sendCard = SwapCardState.SwapCardData(
|
||||
type = TransactionCardType.SendCard({}) {},
|
||||
type = TransactionCardType.Inputtable({}, {}),
|
||||
amountTextFieldValue = TextFieldValue(),
|
||||
amountEquivalent = "1 000 000",
|
||||
tokenIconUrl = "",
|
||||
|
|
@ -444,7 +443,7 @@ private val sendCard = SwapCardState.SwapCardData(
|
|||
)
|
||||
|
||||
private val receiveCard = SwapCardState.SwapCardData(
|
||||
type = TransactionCardType.ReceiveCard(),
|
||||
type = TransactionCardType.ReadOnly(),
|
||||
amountTextFieldValue = TextFieldValue(),
|
||||
amountEquivalent = "1 000 000",
|
||||
tokenIconUrl = "",
|
||||
|
|
|
|||
|
|
@ -182,10 +182,7 @@ private fun Header(type: TransactionCardType, balance: String, modifier: Modifie
|
|||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
val title = when (type) {
|
||||
is TransactionCardType.ReceiveCard -> R.string.exchange_receive_view_header
|
||||
is TransactionCardType.SendCard -> R.string.exchange_send_view_header
|
||||
}
|
||||
val title = type.headerResId
|
||||
Text(
|
||||
text = stringResource(id = title),
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
|
|
@ -245,7 +242,7 @@ private fun Content(
|
|||
) {
|
||||
val sumTextModifier = Modifier.defaultMinSize(minHeight = TangemTheme.dimens.size32)
|
||||
when (type) {
|
||||
is TransactionCardType.ReceiveCard -> {
|
||||
is TransactionCardType.ReadOnly -> {
|
||||
if (textFieldValue != null) {
|
||||
ResizableText(
|
||||
text = textFieldValue.text,
|
||||
|
|
@ -263,7 +260,7 @@ private fun Content(
|
|||
)
|
||||
}
|
||||
}
|
||||
is TransactionCardType.SendCard -> {
|
||||
is TransactionCardType.Inputtable -> {
|
||||
AutoSizeTextField(
|
||||
modifier = sumTextModifier,
|
||||
textFieldValue = textFieldValue ?: TextFieldValue(),
|
||||
|
|
@ -276,7 +273,7 @@ private fun Content(
|
|||
SpacerH8()
|
||||
|
||||
if (amountEquivalent != null) {
|
||||
if (type is TransactionCardType.ReceiveCard && priceImpact !is PriceImpact.Empty) {
|
||||
if (type is TransactionCardType.ReadOnly && priceImpact !is PriceImpact.Empty) {
|
||||
Row {
|
||||
Text(
|
||||
text = makePriceImpactBalanceWarning(amountEquivalent, priceImpact.getIntPercentValue()),
|
||||
|
|
@ -485,7 +482,7 @@ private fun Preview_SwapMainCard_InDarkTheme() {
|
|||
@Composable
|
||||
private fun TransactionCardPreview() {
|
||||
TransactionCard(
|
||||
type = TransactionCardType.SendCard({}) {},
|
||||
type = TransactionCardType.Inputtable({}, {}),
|
||||
amountEquivalent = "1 000 000",
|
||||
tokenIconUrl = "",
|
||||
tokenCurrency = "DAI",
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ spr-client = "3.6.2"
|
|||
# endregion Other libraries
|
||||
|
||||
# region Tangem
|
||||
tangemBlockchainSdk = "release-app_5.4-419"
|
||||
tangemBlockchainSdk = "release-app_5.4-423"
|
||||
#tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds
|
||||
tangemCardSdk = "release-app_5.4-315"
|
||||
#tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue