Updated on 2026-08-14

This commit is contained in:
Tangem 2022-12-26 16:28:20 +03:00
parent 6dee2eeed7
commit e7c330a5e1
16 changed files with 263 additions and 76 deletions

View file

@ -12,7 +12,7 @@ import com.tangem.domain.common.TapWorkarounds.derivationStyle
import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.lib.crypto.DerivationManager
import com.tangem.lib.crypto.models.Currency
import com.tangem.lib.crypto.models.NonNativeToken
import com.tangem.lib.crypto.models.Currency.NonNativeToken
import com.tangem.operations.derivation.ExtendedPublicKeysMap
import com.tangem.tap.DELAY_SDK_DIALOG_CLOSE
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification

View file

@ -2,20 +2,24 @@ package com.tangem.tap.proxy
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.tangem.Message
import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras
import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.BlockchainSdkError
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.TransactionExtras
import com.tangem.blockchain.common.TransactionSender
import com.tangem.blockchain.common.TransactionSigner
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.extensions.Result
import com.tangem.blockchain.extensions.SimpleResult
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.hexToBytes
import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.lib.crypto.TransactionManager
import com.tangem.lib.crypto.models.Currency
import com.tangem.lib.crypto.models.NativeToken
import com.tangem.lib.crypto.models.NonNativeToken
import com.tangem.lib.crypto.models.ProxyAmount
import com.tangem.lib.crypto.models.transactions.SendTxResult
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.TangemSigner
@ -23,6 +27,7 @@ import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.store
import com.tangem.tap.tangemSdk
import java.math.BigDecimal
import java.math.BigInteger
class TransactionManagerImpl(
private val appStateHolder: AppStateHolder,
@ -39,22 +44,13 @@ class TransactionManagerImpl(
): SendTxResult {
val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" }
val walletManager = getActualWalletManager(blockchain)
val amount = when (currencyToSend) {
is NativeToken -> {
Amount(value = amountToSend, blockchain = blockchain)
}
is NonNativeToken -> {
Amount(
convertNonNativeToken(currencyToSend),
amountToSend,
)
}
}
val amount = createAmount(amountToSend, currencyToSend, blockchain)
val txData = walletManager.createTransaction(
amount = amount,
fee = Amount(value = feeAmount, blockchain = blockchain),
destination = destinationAddress,
).copy(hash = dataToSign)
).copy(hash = dataToSign, extras = createExtras(walletManager, feeAmount, dataToSign))
val signer = transactionSigner(walletManager)
@ -67,6 +63,33 @@ class TransactionManagerImpl(
return handleSendResult(sendResult)
}
@Throws(IllegalStateException::class)
override suspend fun getFee(
networkId: String,
amountToSend: BigDecimal,
currencyToSend: Currency,
destinationAddress: String,
): ProxyAmount {
val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" }
val walletManager = getActualWalletManager(blockchain)
val fee = (walletManager as TransactionSender).getFee(
amount = createAmount(amountToSend, currencyToSend, blockchain),
destination = destinationAddress,
)
when (fee) {
is Result.Success -> {
return convertToProxyAmount(fee.data.firstOrNull() ?: error("no fee found"))
}
is Result.Failure -> {
error(fee.error.message ?: fee.error.customMessage)
}
}
}
override fun getNativeAddress(networkId: String): String {
return "" //todo implement
}
private fun handleSendResult(result: SimpleResult): SendTxResult {
when (result) {
is SimpleResult.Success -> {
@ -121,7 +144,40 @@ class TransactionManagerImpl(
}
}
private fun convertNonNativeToken(token: NonNativeToken): Token {
private fun createExtras(
walletManager: WalletManager,
feeAmount: BigDecimal,
transactionHash: String,
): TransactionExtras? {
return when (walletManager) {
is EthereumWalletManager -> {
return EthereumTransactionExtras(
data = transactionHash.removePrefix(HEX_PREFIX).hexToBytes(),
gasLimit = BigInteger.valueOf(DEFAULT_GAS_LIMIT),
)
}
else -> {
null
}
}
}
private fun createAmount(
amount: BigDecimal,
currency: Currency,
blockchain: Blockchain,
): Amount {
return when (currency) {
is Currency.NativeToken -> {
Amount(value = amount, blockchain = blockchain)
}
is Currency.NonNativeToken -> {
Amount(convertNonNativeToken(currency), amount)
}
}
}
private fun convertNonNativeToken(token: Currency.NonNativeToken): Token {
return Token(
name = token.name,
symbol = token.symbol,
@ -130,4 +186,17 @@ class TransactionManagerImpl(
id = token.id,
)
}
private fun convertToProxyAmount(amount: Amount): ProxyAmount {
return ProxyAmount(
currencySymbol = amount.currencySymbol,
value = amount.value ?: BigDecimal.ZERO,
decimals = amount.decimals,
)
}
companion object {
private const val HEX_PREFIX = "0x"
private const val DEFAULT_GAS_LIMIT = 300000L
}
}

View file

@ -11,8 +11,8 @@ import com.tangem.domain.common.extensions.toNetworkId
import com.tangem.domain.common.util.userWalletId
import com.tangem.lib.crypto.UserWalletManager
import com.tangem.lib.crypto.models.Currency
import com.tangem.lib.crypto.models.NativeToken
import com.tangem.lib.crypto.models.NonNativeToken
import com.tangem.lib.crypto.models.Currency.NativeToken
import com.tangem.lib.crypto.models.Currency.NonNativeToken
import com.tangem.tap.domain.extensions.makeWalletManagerForApp
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.wallet.redux.WalletAction

View file

@ -9,7 +9,6 @@ import com.squareup.moshi.Json
* @property toToken Destination token info
* @property toTokenAmount Expected amount of destination token
* @property fromTokenAmount Amount of source token
* @property protocols Selected protocols in a path
* @property estimatedGas gas fee
*/
data class QuoteResponse(
@ -17,6 +16,5 @@ data class QuoteResponse(
@Json(name = "toToken") val toToken: TokenOneInchDto,
@Json(name = "toTokenAmount") val toTokenAmount: String,
@Json(name = "fromTokenAmount") val fromTokenAmount: String,
@Json(name = "protocols") val protocols: List<PathViewDto>,
@Json(name = "estimatedGas") val estimatedGas: Int,
)

View file

@ -2,8 +2,8 @@ package com.tangem.feature.referral.domain.converter
import com.tangem.feature.referral.domain.models.TokenData
import com.tangem.lib.crypto.models.Currency
import com.tangem.lib.crypto.models.NativeToken
import com.tangem.lib.crypto.models.NonNativeToken
import com.tangem.lib.crypto.models.Currency.NativeToken
import com.tangem.lib.crypto.models.Currency.NonNativeToken
import com.tangem.utils.converter.Converter
import javax.inject.Inject

View file

@ -41,8 +41,7 @@ internal class SwapRepositoryImpl @Inject constructor(
networkId: String, fromTokenAddress: String, toTokenAddress: String,
amount:
String,
):
AggregatedSwapDataModel<QuoteModel> {
): AggregatedSwapDataModel<QuoteModel> {
return withContext(coroutineDispatcher.io) {
try {
val response = oneInchErrorsHandler.handleOneInchResponse(

View file

@ -2,6 +2,7 @@ package com.tangem.feature.swap.domain
import com.tangem.feature.swap.domain.models.data.Currency
import com.tangem.feature.swap.domain.models.data.SwapState
import java.math.BigDecimal
interface SwapInteractor {
@ -9,11 +10,13 @@ interface SwapInteractor {
suspend fun getTokenBalance(tokenId: String): String
suspend fun givePermissionToSwap(tokenAddress: String)
suspend fun getFee(): BigDecimal
suspend fun givePermissionToSwap(tokenToApprove: Currency)
suspend fun findBestQuote(
fromTokenAddress: String,
toTokenAddress: String,
fromToken: Currency,
toToken: Currency,
amount: String,
): SwapState

View file

@ -1,6 +1,7 @@
package com.tangem.feature.swap.domain
import com.tangem.feature.swap.domain.cache.SwapDataCache
import com.tangem.feature.swap.domain.converters.CryptoCurrencyConverter
import com.tangem.feature.swap.domain.models.data.Currency
import com.tangem.feature.swap.domain.models.data.DataError
import com.tangem.feature.swap.domain.models.data.SwapState
@ -17,6 +18,8 @@ internal class SwapInteractorImpl @Inject constructor(
private val cache: SwapDataCache,
) : SwapInteractor {
private val cryptoCurrencyConverter = CryptoCurrencyConverter()
override suspend fun getTokensToSwap(networkId: String): List<Currency> {
val availableTokens = cache.getAvailableTokens(networkId)
return availableTokens.ifEmpty {
@ -31,24 +34,48 @@ internal class SwapInteractorImpl @Inject constructor(
TODO("Not yet implemented")
}
override suspend fun givePermissionToSwap(tokenAddress: String) {
override suspend fun getFee(): BigDecimal {
return transactionManager.getFee(
networkId = cache.getNetworkId() ?: error("no networkId found, call getTokensToSwap first"),
amountToSend = cache.getAmountToSwap() ?: error("no amount found, call findBestQuote first"),
currencyToSend = cryptoCurrencyConverter.convert(
cache.getExchangeCurrencies()?.fromCurrency ?: error(
"no fromCurrency found, call findBestQuote first",
),
),
destinationAddress = getTokenAddress(
cache.getExchangeCurrencies()?.toCurrency ?: error(
"no toCurrency found, call findBestQuote first",
),
),
).value
}
override suspend fun givePermissionToSwap(tokenToApprove: Currency) {
cache.getNetworkId()?.let { networkId ->
val oneInchAddress = repository.addressForTrust(networkId)
val transactionData = repository.dataToApprove(networkId, tokenAddress)
//todo for test
// transactionManager.sendTransaction(
// networkId = networkId,
// amountToSend = BigDecimal.ZERO,
// currencyToSend = ,
// feeAmount = BigDecimal.ZERO,
// destinationAddress = oneInchAddress,
// dataToSign = transactionData.data
// )
if (tokenToApprove is Currency.NonNativeToken) {
val estimatedGas =
requireNotNull(cache.getLastQuote()?.estimatedGas) { "estimatedGas not found call findBestQuote" }
val transactionData = repository.dataToApprove(networkId, tokenToApprove.contractAddress)
val gasPrice = transactionData.gasPrice.toBigDecimalOrNull() ?: error("cannot parse gasPrice")
val fee = gasPrice.multiply(estimatedGas.toBigDecimal()).movePointLeft(tokenToApprove.decimalCount)
val result = transactionManager.sendTransaction(
networkId = networkId,
amountToSend = BigDecimal.ZERO,
currencyToSend = cryptoCurrencyConverter.convert(tokenToApprove),
feeAmount = fee,
destinationAddress = transactionData.toAddress,
dataToSign = transactionData.data,
)
}
}
}
override suspend fun findBestQuote(fromTokenAddress: String, toTokenAddress: String, amount: String): SwapState {
override suspend fun findBestQuote(fromToken: Currency, toToken: Currency, amount: String): SwapState {
val networkId = cache.getNetworkId()
val fromTokenAddress = getTokenAddress(fromToken)
val toTokenAddress = getTokenAddress(toToken)
val bigDecimalAmount = amount.toBigDecimalOrNull() ?: error("wrong amount format, use only digits")
val isAllowedToSpend = if (networkId.isNullOrEmpty()) {
error("no networkId found, please call getTokensToSwap first")
} else {
@ -68,7 +95,12 @@ internal class SwapInteractorImpl @Inject constructor(
).let { quotes ->
val quoteDataModel = quotes.dataModel
if (quoteDataModel != null) {
cache.cacheSwapParams(quoteDataModel.copy(isAllowedToSpend = isAllowedToSpend), amount)
cache.cacheSwapParams(
quoteModel = quoteDataModel.copy(isAllowedToSpend = isAllowedToSpend),
amount = bigDecimalAmount,
fromCurrency = fromToken,
toCurrency = toToken,
)
return quoteDataModel
} else {
return SwapState.SwapError(quotes.error)
@ -85,7 +117,7 @@ internal class SwapInteractorImpl @Inject constructor(
networkId = networkId,
fromTokenAddress = quoteModel.fromTokenAmount,
toTokenAddress = quoteModel.toTokenAmount,
amount = amountToSwap,
amount = amountToSwap.toPlainString(),
slippage = DEFAULT_SLIPPAGE,
fromWalletAddress = getWalletAddress(networkId),
).let {
@ -109,6 +141,17 @@ internal class SwapInteractorImpl @Inject constructor(
return userWalletManager.getWalletAddress(networkId)
}
private fun getTokenAddress(currency: Currency): String {
return when (currency) {
is Currency.NativeToken -> {
transactionManager.getNativeAddress(currency.networkId)
}
is Currency.NonNativeToken -> {
currency.contractAddress
}
}
}
//todo implement after merge referral
private fun signTransactionData(transaction: TransactionModel) {
}

View file

@ -1,15 +1,18 @@
package com.tangem.feature.swap.domain.cache
import com.tangem.feature.swap.domain.models.ExchangeCurrencies
import com.tangem.feature.swap.domain.models.data.Currency
import com.tangem.feature.swap.domain.models.data.SwapState.QuoteModel
import java.math.BigDecimal
interface SwapDataCache {
fun cacheNetworkId(networkId: String)
fun cacheSwapParams(quoteModel: QuoteModel, amount: String)
fun cacheSwapParams(quoteModel: QuoteModel, amount: BigDecimal, fromCurrency: Currency, toCurrency: Currency)
fun cacheAvailableToSwapTokens(networkId: String, tokens: List<Currency>)
fun getAvailableTokens(networkId: String): List<Currency>
fun getLastQuote(): QuoteModel?
fun getAmountToSwap(): String?
fun getAmountToSwap(): BigDecimal?
fun getNetworkId(): String?
fun getExchangeCurrencies(): ExchangeCurrencies?
}

View file

@ -1,16 +1,35 @@
package com.tangem.feature.swap.domain.cache
import com.tangem.feature.swap.domain.models.ExchangeCurrencies
import com.tangem.feature.swap.domain.models.SwapDataHolder
import com.tangem.feature.swap.domain.models.data.Currency
import com.tangem.feature.swap.domain.models.data.SwapState.QuoteModel
import java.math.BigDecimal
class SwapDataCacheImpl : SwapDataCache {
private var lastDataForSwap: SwapDataHolder = SwapDataHolder()
private val availableTokensForNetwork: MutableMap<String, List<Currency>> = mutableMapOf()
override fun cacheSwapParams(quoteModel: QuoteModel, amount: String) {
lastDataForSwap = lastDataForSwap.copy(quoteModel = quoteModel, amountToSwap = amount)
override fun cacheSwapParams(
quoteModel: QuoteModel,
amount: BigDecimal,
fromCurrency: Currency,
toCurrency: Currency,
) {
lastDataForSwap =
lastDataForSwap.copy(
quoteModel = quoteModel,
amountToSwap = amount,
exchangeCurrencies = ExchangeCurrencies(
fromCurrency = fromCurrency,
toCurrency = toCurrency,
),
)
}
override fun getExchangeCurrencies(): ExchangeCurrencies? {
return lastDataForSwap.exchangeCurrencies
}
override fun cacheAvailableToSwapTokens(networkId: String, tokens: List<Currency>) {
@ -33,7 +52,7 @@ class SwapDataCacheImpl : SwapDataCache {
return lastDataForSwap.quoteModel
}
override fun getAmountToSwap(): String? {
override fun getAmountToSwap(): BigDecimal? {
return lastDataForSwap.amountToSwap
}
}

View file

@ -0,0 +1,30 @@
package com.tangem.feature.swap.domain.converters
import com.tangem.feature.swap.domain.models.data.Currency
import com.tangem.utils.converter.Converter
class CryptoCurrencyConverter : Converter<Currency, com.tangem.lib.crypto.models.Currency> {
override fun convert(value: Currency): com.tangem.lib.crypto.models.Currency {
return when (value) {
is Currency.NonNativeToken -> {
com.tangem.lib.crypto.models.Currency.NonNativeToken(
id = value.id,
name = value.name,
symbol = value.symbol,
networkId = value.networkId,
contractAddress = value.contractAddress,
decimalCount = value.decimalCount,
)
}
is Currency.NativeToken -> {
com.tangem.lib.crypto.models.Currency.NativeToken(
id = value.id,
name = value.name,
symbol = value.symbol,
networkId = value.networkId,
)
}
}
}
}

View file

@ -1,10 +1,17 @@
package com.tangem.feature.swap.domain.models
import com.tangem.feature.swap.domain.models.data.Currency
import com.tangem.feature.swap.domain.models.data.SwapState.QuoteModel
import java.math.BigDecimal
data class SwapDataHolder(
val quoteModel:
QuoteModel? = null,
val amountToSwap: String? = null,
val quoteModel: QuoteModel? = null,
val amountToSwap: BigDecimal? = null,
val networkId: String? = null,
val exchangeCurrencies: ExchangeCurrencies? = null,
)
data class ExchangeCurrencies(
val fromCurrency: Currency? = null,
val toCurrency: Currency? = null,
)

View file

@ -61,23 +61,12 @@ internal class SwapViewModel @Inject constructor(
val tokens = referralInteractor.getTokensToSwap("binance-smart-chain")
val token = tokens[1]
val token2 = tokens[2]
if (token is Currency.NonNativeToken && token2 is Currency.NonNativeToken) {
findBestQuotes(token.contractAddress, token2.contractAddress)
}
}.onFailure {
Log.e("SwapViewModel", it.message ?: it.cause.toString())
}
}
}
private suspend fun findBestQuotes(fromTokenAddress: String, toTokenAddress: String) {
referralInteractor.findBestQuote(
fromTokenAddress,
toTokenAddress,
"1",
)
}
private fun createTestUiState(): SwapStateHolder {
val sendCard = SwapCardData(
type = TransactionCardType.SendCard("123", false),

View file

@ -1,6 +1,7 @@
package com.tangem.lib.crypto
import com.tangem.lib.crypto.models.Currency
import com.tangem.lib.crypto.models.ProxyAmount
import com.tangem.lib.crypto.models.transactions.SendTxResult
import java.math.BigDecimal
@ -14,4 +15,13 @@ interface TransactionManager {
destinationAddress: String,
dataToSign: String,
): SendTxResult
suspend fun getFee(
networkId: String,
amountToSend: BigDecimal,
currencyToSend: Currency,
destinationAddress: String,
): ProxyAmount
fun getNativeAddress(networkId: String): String
}

View file

@ -9,20 +9,21 @@ sealed class Currency(
open val name: String,
open val symbol: String,
open val networkId: String,
)
) {
data class NativeToken(
override val id: String,
override val name: String,
override val symbol: String,
override val networkId: String,
) : Currency(id, name, symbol, networkId)
data class NativeToken(
override val id: String,
override val name: String,
override val symbol: String,
override val networkId: String,
) : Currency(id, name, symbol, networkId)
class NonNativeToken(
override val id: String,
override val name: String,
override val symbol: String,
override val networkId: String,
val contractAddress: String,
val decimalCount: Int,
) : Currency(id, name, symbol, networkId)
class NonNativeToken(
override val id: String,
override val name: String,
override val symbol: String,
override val networkId: String,
val contractAddress: String,
val decimalCount: Int,
) : Currency(id, name, symbol, networkId)
}

View file

@ -0,0 +1,16 @@
package com.tangem.lib.crypto.models
import java.math.BigDecimal
/**
* Proxy amount is always has a Coin type
*
* @property currencySymbol
* @property value amount in [BigDecimal]
* @property decimals count for token
*/
data class ProxyAmount(
val currencySymbol: String,
var value: BigDecimal,
val decimals: Int,
)