Updated on 2026-08-14
This commit is contained in:
commit
e98780ff93
33 changed files with 643 additions and 207 deletions
|
|
@ -28,7 +28,7 @@ import com.tangem.tap.store
|
|||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import com.tangem.feature.swap.domain.models.Currency as SwapCurrency
|
||||
import com.tangem.feature.swap.domain.models.domain.Currency as SwapCurrency
|
||||
|
||||
class TradeCryptoMiddleware {
|
||||
fun handle(state: () -> AppState?, action: WalletAction.TradeCryptoAction) {
|
||||
|
|
|
|||
|
|
@ -27,7 +27,6 @@ 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,
|
||||
|
|
@ -38,6 +37,7 @@ class TransactionManagerImpl(
|
|||
amountToSend: BigDecimal,
|
||||
currencyToSend: Currency,
|
||||
feeAmount: BigDecimal,
|
||||
estimatedGas: Int,
|
||||
destinationAddress: String,
|
||||
dataToSign: String,
|
||||
): SendTxResult {
|
||||
|
|
@ -49,7 +49,7 @@ class TransactionManagerImpl(
|
|||
amount = amount,
|
||||
fee = Amount(value = feeAmount, blockchain = blockchain),
|
||||
destination = destinationAddress,
|
||||
).copy(hash = dataToSign, extras = createExtras(walletManager, feeAmount, dataToSign))
|
||||
).copy(hash = dataToSign, extras = createExtras(walletManager, estimatedGas, dataToSign))
|
||||
|
||||
val signer = transactionSigner(walletManager)
|
||||
|
||||
|
|
@ -95,10 +95,6 @@ class TransactionManagerImpl(
|
|||
}
|
||||
}
|
||||
|
||||
override fun getNativeAddress(networkId: String): String {
|
||||
return "" //todo implement
|
||||
}
|
||||
|
||||
private fun handleSendResult(result: SimpleResult): SendTxResult {
|
||||
when (result) {
|
||||
is SimpleResult.Success -> {
|
||||
|
|
@ -155,14 +151,14 @@ class TransactionManagerImpl(
|
|||
|
||||
private fun createExtras(
|
||||
walletManager: WalletManager,
|
||||
feeAmount: BigDecimal,
|
||||
estimatedGas: Int,
|
||||
transactionHash: String,
|
||||
): TransactionExtras? {
|
||||
return when (walletManager) {
|
||||
is EthereumWalletManager -> {
|
||||
return EthereumTransactionExtras(
|
||||
data = transactionHash.removePrefix(HEX_PREFIX).hexToBytes(),
|
||||
gasLimit = BigInteger.valueOf(DEFAULT_GAS_LIMIT),
|
||||
gasLimit = estimatedGas.toBigInteger(),
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
|
|
|
|||
|
|
@ -27,13 +27,13 @@ class UserWalletManagerImpl(
|
|||
private val walletManagerFactory: WalletManagerFactory,
|
||||
) : UserWalletManager {
|
||||
|
||||
override suspend fun getUserTokens(): List<Currency> {
|
||||
override suspend fun getUserTokens(networkId: String): List<Currency> {
|
||||
val card = appStateHolder.getActualCard()
|
||||
val userTokensRepository =
|
||||
requireNotNull(appStateHolder.userTokensRepository) { "userTokensRepository is null" }
|
||||
if (card != null) {
|
||||
return if (card != null) {
|
||||
userTokensRepository.getUserTokens(card)
|
||||
.filter { it.isToken() }
|
||||
.filter { it.blockchain.toNetworkId() == networkId }
|
||||
.map {
|
||||
if (it is com.tangem.tap.features.wallet.models.Currency.Token) {
|
||||
NonNativeToken(
|
||||
|
|
@ -53,8 +53,9 @@ class UserWalletManagerImpl(
|
|||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
override fun getWalletId(): String {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,5 @@ data class SwapResponse(
|
|||
@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 = "transaction") val transaction: TransactionDto,
|
||||
@Json(name = "tx") val transaction: TransactionDto,
|
||||
)
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package com.tangem.utils.converter
|
||||
|
||||
interface TwoWayConverter<I, O> : Converter<I, O> {
|
||||
fun convertBack(value: O): I
|
||||
fun convertListBack(input: List<O>): List<I> {
|
||||
return input.map { convertBack(it) }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,31 @@
|
|||
package com.tangem.utils.coroutines
|
||||
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
suspend fun <R> runCatching(dispatcher: CoroutineDispatcher, block: suspend () -> R): Result<R> {
|
||||
return runCatching {
|
||||
withContext(dispatcher) { block() }
|
||||
}
|
||||
}
|
||||
|
||||
class Debouncer {
|
||||
|
||||
private var debounceJob: Job? = null
|
||||
|
||||
fun debounce(
|
||||
waitMs: Long = 300L,
|
||||
coroutineScope: CoroutineScope,
|
||||
destinationFunction: () -> Unit,
|
||||
) {
|
||||
debounceJob?.cancel()
|
||||
debounceJob = coroutineScope.launch {
|
||||
delay(waitMs)
|
||||
destinationFunction.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -10,10 +10,10 @@ import com.tangem.feature.swap.converters.QuotesConverter
|
|||
import com.tangem.feature.swap.converters.SwapConverter
|
||||
import com.tangem.feature.swap.converters.TokensConverter
|
||||
import com.tangem.feature.swap.domain.SwapRepository
|
||||
import com.tangem.feature.swap.domain.models.ApproveModel
|
||||
import com.tangem.feature.swap.domain.models.Currency
|
||||
import com.tangem.feature.swap.domain.models.QuoteModel
|
||||
import com.tangem.feature.swap.domain.models.SwapDataModel
|
||||
import com.tangem.feature.swap.domain.models.domain.ApproveModel
|
||||
import com.tangem.feature.swap.domain.models.domain.Currency
|
||||
import com.tangem.feature.swap.domain.models.domain.QuoteModel
|
||||
import com.tangem.feature.swap.domain.models.domain.SwapDataModel
|
||||
import com.tangem.feature.swap.domain.models.data.AggregatedSwapDataModel
|
||||
import com.tangem.feature.swap.domain.models.mapErrors
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
|
|
@ -109,6 +109,7 @@ internal class SwapRepositoryImpl @Inject constructor(
|
|||
slippage = slippage,
|
||||
),
|
||||
)
|
||||
|
||||
AggregatedSwapDataModel(swapConverter.convert(swapResponse))
|
||||
} catch (ex: OneIncResponseException) {
|
||||
AggregatedSwapDataModel(null, mapErrors(ex.data.description))
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package com.tangem.feature.swap.converters
|
||||
|
||||
import com.tangem.datasource.api.oneinch.models.ApproveCalldataResponse
|
||||
import com.tangem.feature.swap.domain.models.ApproveModel
|
||||
import com.tangem.feature.swap.domain.models.domain.ApproveModel
|
||||
import com.tangem.utils.converter.Converter
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package com.tangem.feature.swap.converters
|
||||
|
||||
import com.tangem.datasource.api.oneinch.models.QuoteResponse
|
||||
import com.tangem.feature.swap.domain.models.QuoteModel
|
||||
import com.tangem.feature.swap.domain.models.domain.QuoteModel
|
||||
import com.tangem.feature.swap.domain.models.createFromAmountWithOffset
|
||||
import com.tangem.utils.converter.Converter
|
||||
import javax.inject.Inject
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ package com.tangem.feature.swap.converters
|
|||
|
||||
import com.tangem.datasource.api.oneinch.models.SwapResponse
|
||||
import com.tangem.datasource.api.oneinch.models.TransactionDto
|
||||
import com.tangem.feature.swap.domain.models.SwapDataModel
|
||||
import com.tangem.feature.swap.domain.models.TransactionModel
|
||||
import com.tangem.feature.swap.domain.models.domain.SwapDataModel
|
||||
import com.tangem.feature.swap.domain.models.domain.TransactionModel
|
||||
import com.tangem.feature.swap.domain.models.createFromAmountWithOffset
|
||||
import com.tangem.utils.converter.Converter
|
||||
import javax.inject.Inject
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package com.tangem.feature.swap.converters
|
||||
|
||||
import com.tangem.datasource.api.tangemTech.models.CoinsResponse
|
||||
import com.tangem.feature.swap.domain.models.Currency
|
||||
import com.tangem.feature.swap.domain.models.domain.Currency
|
||||
import com.tangem.utils.converter.Converter
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
|
|||
|
|
@ -1,20 +1,27 @@
|
|||
package com.tangem.feature.swap.domain
|
||||
|
||||
import com.tangem.feature.swap.domain.models.Currency
|
||||
import com.tangem.feature.swap.domain.models.SwapAmount
|
||||
import com.tangem.feature.swap.domain.models.SwapState
|
||||
import com.tangem.feature.swap.domain.models.cache.ExchangeCurrencies
|
||||
import com.tangem.feature.swap.domain.models.domain.Currency
|
||||
import com.tangem.feature.swap.domain.models.ui.FoundTokensState
|
||||
import com.tangem.feature.swap.domain.models.ui.SwapState
|
||||
import com.tangem.feature.swap.domain.models.ui.TokensDataState
|
||||
|
||||
interface SwapInteractor {
|
||||
|
||||
suspend fun getTokensToSwap(networkId: String): List<Currency>
|
||||
suspend fun initTokensToSwap(initialCurrency: Currency): TokensDataState
|
||||
|
||||
suspend fun onSearchToken(searchQuery: String): FoundTokensState
|
||||
|
||||
fun getExchangeCurrencies(): ExchangeCurrencies?
|
||||
|
||||
fun findTokenById(id: String): Currency?
|
||||
|
||||
/**
|
||||
* Give permission to swap
|
||||
*
|
||||
* @param tokenToApprove use token which you want to swap
|
||||
*/
|
||||
@Throws(IllegalStateException::class)
|
||||
suspend fun givePermissionToSwap(tokenToApprove: Currency)
|
||||
suspend fun givePermissionToSwap()
|
||||
|
||||
@Throws(IllegalStateException::class)
|
||||
suspend fun findBestQuote(
|
||||
|
|
|
|||
|
|
@ -2,16 +2,23 @@ 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.ApproveModel
|
||||
import com.tangem.feature.swap.domain.models.Currency
|
||||
import com.tangem.feature.swap.domain.models.DataError
|
||||
import com.tangem.feature.swap.domain.models.PermissionDataState
|
||||
import com.tangem.feature.swap.domain.models.SwapAmount
|
||||
import com.tangem.feature.swap.domain.models.SwapState
|
||||
import com.tangem.feature.swap.domain.models.TransactionModel
|
||||
import com.tangem.feature.swap.domain.models.cache.ExchangeCurrencies
|
||||
import com.tangem.feature.swap.domain.models.domain.ApproveModel
|
||||
import com.tangem.feature.swap.domain.models.domain.Currency
|
||||
import com.tangem.feature.swap.domain.models.toStringWithRightOffset
|
||||
import com.tangem.feature.swap.domain.models.ui.FoundTokensState
|
||||
import com.tangem.feature.swap.domain.models.ui.PermissionDataState
|
||||
import com.tangem.feature.swap.domain.models.ui.PreselectTokens
|
||||
import com.tangem.feature.swap.domain.models.ui.SwapState
|
||||
import com.tangem.feature.swap.domain.models.ui.TokenBalanceData
|
||||
import com.tangem.feature.swap.domain.models.ui.TokenWithBalance
|
||||
import com.tangem.feature.swap.domain.models.ui.TokensDataState
|
||||
import com.tangem.lib.crypto.TransactionManager
|
||||
import com.tangem.lib.crypto.UserWalletManager
|
||||
import com.tangem.lib.crypto.models.ProxyAmount
|
||||
import com.tangem.lib.crypto.models.ProxyFiatCurrency
|
||||
import com.tangem.lib.crypto.models.transactions.SendTxResult
|
||||
import com.tangem.utils.toFiatString
|
||||
import com.tangem.utils.toFormattedString
|
||||
|
|
@ -28,21 +35,91 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
|
||||
private val cryptoCurrencyConverter = CryptoCurrencyConverter()
|
||||
|
||||
override suspend fun getTokensToSwap(networkId: String): List<Currency> {
|
||||
override suspend fun initTokensToSwap(initialCurrency: Currency): TokensDataState {
|
||||
val networkId = initialCurrency.networkId
|
||||
cache.cacheNetworkId(networkId)
|
||||
val availableTokens = cache.getAvailableTokens(networkId)
|
||||
return availableTokens.ifEmpty {
|
||||
val allLoadedTokens = availableTokens.ifEmpty {
|
||||
val tokens = repository.getExchangeableTokens(networkId)
|
||||
cache.cacheAvailableToSwapTokens(networkId, tokens)
|
||||
cache.cacheNetworkId(networkId)
|
||||
tokens
|
||||
}
|
||||
}.filter { it.symbol != initialCurrency.symbol }
|
||||
|
||||
//replace tokens in wallet tokens list with loaded same
|
||||
val loadedOnWalletsMap = mutableSetOf<String>()
|
||||
val tokensInWallet = userWalletManager.getUserTokens(networkId)
|
||||
.filter { it.symbol != initialCurrency.symbol }
|
||||
.map { token ->
|
||||
allLoadedTokens.firstOrNull { it.symbol == token.symbol }?.let {
|
||||
loadedOnWalletsMap.add(it.symbol)
|
||||
it
|
||||
} ?: cryptoCurrencyConverter.convertBack(token)
|
||||
}
|
||||
val loadedTokens = allLoadedTokens
|
||||
.filter {
|
||||
!loadedOnWalletsMap.contains(it.symbol)
|
||||
}
|
||||
cache.cacheLoadedTokens(loadedTokens)
|
||||
cache.cacheInWalletTokens(tokensInWallet)
|
||||
val tokensBalance = userWalletManager.getCurrentWalletTokensBalance(networkId)
|
||||
val appCurrency = userWalletManager.getUserAppCurrency()
|
||||
val rates = repository.getRates(appCurrency.code, tokensInWallet.map { it.id })
|
||||
return TokensDataState(
|
||||
preselectTokens = PreselectTokens(
|
||||
fromToken = initialCurrency,
|
||||
toToken = selectToToken(initialCurrency, tokensInWallet, loadedTokens),
|
||||
),
|
||||
foundTokensState = FoundTokensState(
|
||||
tokensInWallet = getTokensWithBalance(tokensInWallet, tokensBalance, rates, appCurrency),
|
||||
loadedTokens = loadedTokens.map { TokenWithBalance(it) },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun givePermissionToSwap(tokenToApprove: Currency) {
|
||||
override suspend fun onSearchToken(searchQuery: String): FoundTokensState {
|
||||
val networkId = requireNotNull(cache.getNetworkId()) { "networkId is null" }
|
||||
val searchQueryLowerCase = searchQuery.lowercase()
|
||||
val tokensInWallet = cache.getInWalletTokens()
|
||||
.filter {
|
||||
it.name.lowercase().contains(searchQueryLowerCase)
|
||||
|| it.symbol.lowercase().contains(searchQueryLowerCase)
|
||||
}
|
||||
val loadedTokens = cache.getLoadedTokens()
|
||||
.filter {
|
||||
it.name.lowercase().contains(searchQueryLowerCase)
|
||||
|| it.symbol.lowercase().contains(searchQueryLowerCase)
|
||||
}
|
||||
val tokensBalance = userWalletManager.getCurrentWalletTokensBalance(networkId)
|
||||
val appCurrency = userWalletManager.getUserAppCurrency()
|
||||
val rates = repository.getRates(appCurrency.code, tokensInWallet.map { it.id })
|
||||
return FoundTokensState(
|
||||
tokensInWallet = getTokensWithBalance(tokensInWallet, tokensBalance, rates, appCurrency),
|
||||
loadedTokens = loadedTokens.map { TokenWithBalance(it) },
|
||||
)
|
||||
}
|
||||
|
||||
override fun getExchangeCurrencies(): ExchangeCurrencies? {
|
||||
return cache.getExchangeCurrencies()
|
||||
}
|
||||
|
||||
override fun findTokenById(id: String): Currency? {
|
||||
val tokensInWallet = cache.getInWalletTokens()
|
||||
val loadedTokens = cache.getLoadedTokens()
|
||||
return tokensInWallet.firstOrNull { it.id == id } ?: loadedTokens.firstOrNull { it.id == id }
|
||||
}
|
||||
|
||||
override suspend fun givePermissionToSwap() {
|
||||
cache.getNetworkId()?.let { networkId ->
|
||||
if (tokenToApprove is Currency.NonNativeToken) {
|
||||
val currencyToSend =
|
||||
requireNotNull(cache.getExchangeCurrencies()?.fromCurrency) { "currency is not selected" }
|
||||
if (currencyToSend is Currency.NonNativeToken) {
|
||||
val estimatedGas =
|
||||
requireNotNull(cache.getLastQuote()?.estimatedGas) { "estimatedGas not found call findBestQuote" }
|
||||
increaseByPercents(
|
||||
TWENTY_FIVE_PERCENTS,
|
||||
requireNotNull(cache.getLastQuote()?.estimatedGas) {
|
||||
"estimatedGas not found call findBestQuote"
|
||||
},
|
||||
)
|
||||
val transactionData =
|
||||
requireNotNull(cache.getApproveTransactionData()) { "getApproveTransactionData not found, call findQuotes" }
|
||||
val gasPrice = transactionData.gasPrice.toBigDecimalOrNull() ?: error("cannot parse gasPrice")
|
||||
|
|
@ -50,14 +127,15 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
val result = transactionManager.sendTransaction(
|
||||
networkId = networkId,
|
||||
amountToSend = BigDecimal.ZERO,
|
||||
currencyToSend = cryptoCurrencyConverter.convert(tokenToApprove),
|
||||
currencyToSend = cryptoCurrencyConverter.convert(currencyToSend),
|
||||
feeAmount = fee,
|
||||
estimatedGas = estimatedGas,
|
||||
destinationAddress = transactionData.toAddress,
|
||||
dataToSign = transactionData.data,
|
||||
)
|
||||
when (result) {
|
||||
SendTxResult.Success -> {
|
||||
allowPermissionsHandler.addAddressToInProgress(tokenToApprove.contractAddress)
|
||||
allowPermissionsHandler.addAddressToInProgress(currencyToSend.contractAddress)
|
||||
}
|
||||
SendTxResult.UserCancelledError -> TODO()
|
||||
is SendTxResult.BlockchainSdkError -> TODO()
|
||||
|
|
@ -69,12 +147,14 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
}
|
||||
|
||||
override suspend fun findBestQuote(fromToken: Currency, toToken: Currency, amount: SwapAmount): SwapState {
|
||||
val networkId = requireNotNull(cache.getNetworkId()) { "no networkId found, please call getTokensToSwap first" }
|
||||
val networkId = requireNotNull(cache.getNetworkId()) { "no networkId found, please call initTokens first" }
|
||||
val fromTokenAddress = getTokenAddress(fromToken)
|
||||
val toTokenAddress = getTokenAddress(toToken)
|
||||
val isAllowedToSpend = checkAllowance(networkId, fromTokenAddress)
|
||||
val isBalanceZero = isNotZeroBalance(fromToken, networkId)
|
||||
return if (isAllowedToSpend && isBalanceZero) {
|
||||
val isNotZeroBalance = isNotZeroBalance(fromToken, networkId)
|
||||
cache.cacheExchangeCurrencies(fromToken, toToken)
|
||||
cache.cacheAmountToSwap(amount)
|
||||
return if (isAllowedToSpend && isNotZeroBalance) {
|
||||
if (allowPermissionsHandler.isAddressAllowanceInProgress(fromTokenAddress)) {
|
||||
allowPermissionsHandler.removeAddressFromProgress(toTokenAddress)
|
||||
}
|
||||
|
|
@ -94,37 +174,41 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
amount = amount,
|
||||
fromToken = fromToken,
|
||||
toToken = toToken,
|
||||
isAllowedToSpend = isAllowedToSpend,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun onSwap(): SwapState {
|
||||
val quoteModel = cache.getLastQuote()
|
||||
val amountToSwap = cache.getAmountToSwap()
|
||||
val networkId = cache.getNetworkId()
|
||||
if (quoteModel != null && amountToSwap != null && networkId != null) {
|
||||
repository.prepareSwapTransaction(
|
||||
networkId = networkId,
|
||||
fromTokenAddress = quoteModel.fromTokenAddress,
|
||||
toTokenAddress = quoteModel.toTokenAddress,
|
||||
amount = amountToSwap.toStringWithRightOffset(),
|
||||
slippage = DEFAULT_SLIPPAGE,
|
||||
fromWalletAddress = getWalletAddress(networkId),
|
||||
).let {
|
||||
val swapData = it.dataModel
|
||||
if (swapData != null) {
|
||||
signTransactionData(swapData.transaction) //todo implement
|
||||
return SwapState.SwapSuccess(
|
||||
quoteModel.fromTokenAmount,
|
||||
quoteModel.toTokenAmount,
|
||||
)
|
||||
} else {
|
||||
return SwapState.SwapError(it.error)
|
||||
}
|
||||
val swapData = requireNotNull(cache.getLastSwapData()) { "swap data is not ready" }
|
||||
val networkId = requireNotNull(cache.getNetworkId()) { "no networkId found, please call getTokensToSwap first" }
|
||||
val currencyToSend = requireNotNull(cache.getExchangeCurrencies()?.fromCurrency) { "currency is not selected" }
|
||||
val amountToSwap = requireNotNull(cache.getAmountToSwap()) { "" }
|
||||
val estimatedGas =
|
||||
increaseByPercents(TWENTY_FIVE_PERCENTS, swapData.transaction.gas.toIntOrNull() ?: DEFAULT_GAS)
|
||||
val fee = transactionManager.calculateFee(
|
||||
networkId = networkId,
|
||||
gasPrice = swapData.transaction.gasPrice,
|
||||
estimatedGas = estimatedGas,
|
||||
)
|
||||
val result = transactionManager.sendTransaction(
|
||||
networkId = networkId,
|
||||
amountToSend = amountToSwap.value,
|
||||
currencyToSend = cryptoCurrencyConverter.convert(currencyToSend),
|
||||
feeAmount = fee,
|
||||
estimatedGas = estimatedGas,
|
||||
destinationAddress = swapData.transaction.toWalletAddress,
|
||||
dataToSign = swapData.transaction.data,
|
||||
)
|
||||
when (result) {
|
||||
SendTxResult.Success -> {
|
||||
}
|
||||
} else {
|
||||
throw IllegalStateException("cache is empty, call 'findBestQuote' first")
|
||||
SendTxResult.UserCancelledError -> TODO()
|
||||
is SendTxResult.BlockchainSdkError -> TODO()
|
||||
is SendTxResult.TangemSdkError -> TODO()
|
||||
is SendTxResult.UnknownError -> TODO()
|
||||
}
|
||||
return SwapState.SwapError(DataError.UNKNOWN_ERROR)
|
||||
}
|
||||
|
||||
override fun getTokenDecimals(token: Currency): Int {
|
||||
|
|
@ -135,6 +219,47 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun selectToToken(
|
||||
initialToken: Currency,
|
||||
tokensInWallet: List<Currency>,
|
||||
loadedTokens: List<Currency>,
|
||||
): Currency {
|
||||
val toToken = if (tokensInWallet.isNotEmpty()) {
|
||||
tokensInWallet.firstOrNull { it.symbol != initialToken.symbol }
|
||||
?: loadedTokens.first { it.symbol != initialToken.symbol }
|
||||
} else {
|
||||
val findUsdt = loadedTokens.firstOrNull { it.symbol == "USDT" && it.symbol != initialToken.symbol }
|
||||
if (findUsdt == null) {
|
||||
val findUsdc = loadedTokens.firstOrNull { it.symbol == "USDC" && it.symbol != initialToken.symbol }
|
||||
findUsdc ?: loadedTokens.first { it.symbol != initialToken.symbol }
|
||||
} else {
|
||||
findUsdt
|
||||
}
|
||||
}
|
||||
return toToken
|
||||
}
|
||||
|
||||
private fun getTokensWithBalance(
|
||||
tokens: List<Currency>,
|
||||
balances: Map<String, ProxyAmount>,
|
||||
rates: Map<String, Double>,
|
||||
appCurrency: ProxyFiatCurrency,
|
||||
): List<TokenWithBalance> {
|
||||
return tokens.map {
|
||||
val balance = balances[it.id]
|
||||
TokenWithBalance(
|
||||
token = it,
|
||||
tokenBalanceData = TokenBalanceData(
|
||||
amount = balance?.let { b -> b.value.toFormattedString(b.decimals) },
|
||||
amountEquivalent = balance?.value?.toFiatString(
|
||||
rates[it.id]?.toBigDecimal() ?: BigDecimal.ZERO,
|
||||
appCurrency.symbol,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun checkAllowance(networkId: String, fromTokenAddress: String): Boolean {
|
||||
val allowance = repository.checkTokensSpendAllowance(
|
||||
networkId = networkId,
|
||||
|
|
@ -154,6 +279,7 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
amount: SwapAmount,
|
||||
fromToken: Currency,
|
||||
toToken: Currency,
|
||||
isAllowedToSpend: Boolean,
|
||||
): SwapState {
|
||||
repository.findBestQuote(
|
||||
networkId = networkId,
|
||||
|
|
@ -163,12 +289,7 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
).let { quotes ->
|
||||
val quoteDataModel = quotes.dataModel
|
||||
if (quoteDataModel != null) {
|
||||
cache.cacheSwapParams(
|
||||
quoteModel = quoteDataModel,
|
||||
amount = amount,
|
||||
fromCurrency = fromToken,
|
||||
toCurrency = toToken,
|
||||
)
|
||||
cache.cacheQuoteData(quoteModel = quoteDataModel)
|
||||
val transactionData = repository.dataToApprove(networkId, getTokenAddress(fromToken))
|
||||
cache.cacheApproveTransactionData(transactionData)
|
||||
val swapState = updateBalances(
|
||||
|
|
@ -182,7 +303,7 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
estimatedGas = quoteDataModel.estimatedGas,
|
||||
gasPrice = transactionData.gasPrice,
|
||||
),
|
||||
isAllowedToSpend = false,
|
||||
isAllowedToSpend = isAllowedToSpend,
|
||||
)
|
||||
return updatePermissionState(
|
||||
networkId = networkId,
|
||||
|
|
@ -230,7 +351,7 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
estimatedGas = swapData.transaction.gas.toIntOrNull() ?: DEFAULT_GAS,
|
||||
gasPrice = swapData.transaction.gasPrice,
|
||||
),
|
||||
isAllowedToSpend = false,
|
||||
isAllowedToSpend = true,
|
||||
)
|
||||
return swapState.copy(
|
||||
permissionState = PermissionDataState.Empty,
|
||||
|
|
@ -264,7 +385,7 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
toTokenAmount = toTokenAmount,
|
||||
fromTokenAddress = getTokenAddress(fromToken),
|
||||
toTokenAddress = getTokenAddress(toToken),
|
||||
fee = fee.toPlainString(),
|
||||
fee = "${fee.toPlainString()} ${userWalletManager.getCurrencyByNetworkId(networkId)}",
|
||||
isAllowedToSpend = isAllowedToSpend,
|
||||
fromTokenWalletBalance = fromTokenBalance ?: ZERO_BALANCE,
|
||||
fromTokenFiatBalance = fromTokenAmount.value.toFiatString(
|
||||
|
|
@ -319,7 +440,7 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
private fun getTokenAddress(currency: Currency): String {
|
||||
return when (currency) {
|
||||
is Currency.NativeToken -> {
|
||||
transactionManager.getNativeAddress(currency.networkId)
|
||||
DEFAULT_BLOCKCHAIN_ADDRESS
|
||||
}
|
||||
is Currency.NonNativeToken -> {
|
||||
currency.contractAddress
|
||||
|
|
@ -327,13 +448,15 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
//todo implement after merge referral
|
||||
private fun signTransactionData(transaction: TransactionModel) {
|
||||
private fun increaseByPercents(percents: Int, value: Int): Int {
|
||||
return value * (percents / 100 + 1)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val DEFAULT_SLIPPAGE = 2
|
||||
private const val ZERO_BALANCE = "0"
|
||||
private const val DEFAULT_GAS = 300000
|
||||
private const val DEFAULT_BLOCKCHAIN_ADDRESS = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"
|
||||
private const val TWENTY_FIVE_PERCENTS = 25
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
package com.tangem.feature.swap.domain
|
||||
|
||||
import com.tangem.feature.swap.domain.models.ApproveModel
|
||||
import com.tangem.feature.swap.domain.models.Currency
|
||||
import com.tangem.feature.swap.domain.models.QuoteModel
|
||||
import com.tangem.feature.swap.domain.models.SwapDataModel
|
||||
import com.tangem.feature.swap.domain.models.domain.ApproveModel
|
||||
import com.tangem.feature.swap.domain.models.domain.Currency
|
||||
import com.tangem.feature.swap.domain.models.domain.QuoteModel
|
||||
import com.tangem.feature.swap.domain.models.domain.SwapDataModel
|
||||
import com.tangem.feature.swap.domain.models.data.AggregatedSwapDataModel
|
||||
|
||||
interface SwapRepository {
|
||||
|
|
|
|||
|
|
@ -1,24 +1,30 @@
|
|||
package com.tangem.feature.swap.domain.cache
|
||||
|
||||
import com.tangem.feature.swap.domain.models.ApproveModel
|
||||
import com.tangem.feature.swap.domain.models.Currency
|
||||
import com.tangem.feature.swap.domain.models.QuoteModel
|
||||
import com.tangem.feature.swap.domain.models.SwapAmount
|
||||
import com.tangem.feature.swap.domain.models.SwapDataModel
|
||||
import com.tangem.feature.swap.domain.models.cache.ExchangeCurrencies
|
||||
import com.tangem.feature.swap.domain.models.domain.ApproveModel
|
||||
import com.tangem.feature.swap.domain.models.domain.Currency
|
||||
import com.tangem.feature.swap.domain.models.domain.QuoteModel
|
||||
import com.tangem.feature.swap.domain.models.domain.SwapDataModel
|
||||
|
||||
interface SwapDataCache {
|
||||
|
||||
fun cacheNetworkId(networkId: String)
|
||||
fun cacheSwapParams(quoteModel: QuoteModel, amount: SwapAmount, fromCurrency: Currency, toCurrency: Currency)
|
||||
fun cacheApproveTransactionData(approve: ApproveModel)
|
||||
fun cacheQuoteData(quoteModel: QuoteModel)
|
||||
fun cacheSwapData(swapDataModel: SwapDataModel)
|
||||
fun cacheAmountToSwap(amount: SwapAmount)
|
||||
fun cacheApproveTransactionData(approve: ApproveModel)
|
||||
fun cacheAvailableToSwapTokens(networkId: String, tokens: List<Currency>)
|
||||
fun cacheInWalletTokens(tokens: List<Currency>)
|
||||
fun cacheLoadedTokens(tokens: List<Currency>)
|
||||
fun cacheExchangeCurrencies(fromToken: Currency, toToken: Currency)
|
||||
fun getAvailableTokens(networkId: String): List<Currency>
|
||||
fun getApproveTransactionData(): ApproveModel?
|
||||
fun getLastQuote(): QuoteModel?
|
||||
fun getLastSwapData(): SwapDataModel?
|
||||
fun getAmountToSwap(): SwapAmount?
|
||||
fun getNetworkId(): String?
|
||||
fun getAmountToSwap(): SwapAmount?
|
||||
fun getExchangeCurrencies(): ExchangeCurrencies?
|
||||
fun getInWalletTokens(): List<Currency>
|
||||
fun getLoadedTokens(): List<Currency>
|
||||
}
|
||||
|
|
@ -1,35 +1,54 @@
|
|||
package com.tangem.feature.swap.domain.cache
|
||||
|
||||
import com.tangem.feature.swap.domain.models.ApproveModel
|
||||
import com.tangem.feature.swap.domain.models.Currency
|
||||
import com.tangem.feature.swap.domain.models.QuoteModel
|
||||
import com.tangem.feature.swap.domain.models.SwapAmount
|
||||
import com.tangem.feature.swap.domain.models.SwapDataModel
|
||||
import com.tangem.feature.swap.domain.models.cache.ExchangeCurrencies
|
||||
import com.tangem.feature.swap.domain.models.cache.SwapDataHolder
|
||||
import com.tangem.feature.swap.domain.models.domain.ApproveModel
|
||||
import com.tangem.feature.swap.domain.models.domain.Currency
|
||||
import com.tangem.feature.swap.domain.models.domain.QuoteModel
|
||||
import com.tangem.feature.swap.domain.models.domain.SwapDataModel
|
||||
|
||||
class SwapDataCacheImpl : SwapDataCache {
|
||||
|
||||
private var lastDataForSwap: SwapDataHolder = SwapDataHolder()
|
||||
private val availableTokensForNetwork: MutableMap<String, List<Currency>> = mutableMapOf()
|
||||
private val lastInWalletTokens = mutableListOf<Currency>()
|
||||
private val lastLoadedTokens = mutableListOf<Currency>()
|
||||
|
||||
override fun cacheSwapParams(
|
||||
override fun cacheQuoteData(
|
||||
quoteModel: QuoteModel,
|
||||
amount: SwapAmount,
|
||||
fromCurrency: Currency,
|
||||
toCurrency: Currency,
|
||||
) {
|
||||
lastDataForSwap = lastDataForSwap.copy(quoteModel = quoteModel)
|
||||
}
|
||||
|
||||
override fun cacheExchangeCurrencies(fromToken: Currency, toToken: Currency) {
|
||||
lastDataForSwap =
|
||||
lastDataForSwap.copy(
|
||||
quoteModel = quoteModel,
|
||||
amountToSwap = amount,
|
||||
exchangeCurrencies = ExchangeCurrencies(
|
||||
fromCurrency = fromCurrency,
|
||||
toCurrency = toCurrency,
|
||||
fromCurrency = fromToken,
|
||||
toCurrency = toToken,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
override fun cacheInWalletTokens(tokens: List<Currency>) {
|
||||
lastInWalletTokens.clear()
|
||||
lastInWalletTokens.addAll(tokens)
|
||||
}
|
||||
|
||||
override fun cacheLoadedTokens(tokens: List<Currency>) {
|
||||
lastLoadedTokens.clear()
|
||||
lastLoadedTokens.addAll(tokens)
|
||||
}
|
||||
|
||||
override fun getInWalletTokens(): List<Currency> {
|
||||
return lastInWalletTokens
|
||||
}
|
||||
|
||||
override fun getLoadedTokens(): List<Currency> {
|
||||
return lastLoadedTokens
|
||||
}
|
||||
|
||||
override fun cacheSwapData(swapDataModel: SwapDataModel) {
|
||||
lastDataForSwap = lastDataForSwap.copy(swapModel = swapDataModel)
|
||||
}
|
||||
|
|
@ -54,6 +73,10 @@ class SwapDataCacheImpl : SwapDataCache {
|
|||
lastDataForSwap = lastDataForSwap.copy(networkId = networkId)
|
||||
}
|
||||
|
||||
override fun cacheAmountToSwap(amount: SwapAmount) {
|
||||
lastDataForSwap = lastDataForSwap.copy(amountToSwap = amount)
|
||||
}
|
||||
|
||||
override fun getNetworkId(): String? {
|
||||
return lastDataForSwap.networkId
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
package com.tangem.feature.swap.domain.converters
|
||||
|
||||
import com.tangem.feature.swap.domain.models.Currency
|
||||
import com.tangem.utils.converter.Converter
|
||||
import com.tangem.feature.swap.domain.models.domain.Currency
|
||||
import com.tangem.utils.converter.TwoWayConverter
|
||||
import com.tangem.lib.crypto.models.Currency as CryptoCurrency
|
||||
|
||||
class CryptoCurrencyConverter : Converter<Currency, com.tangem.lib.crypto.models.Currency> {
|
||||
class CryptoCurrencyConverter : TwoWayConverter<Currency, CryptoCurrency> {
|
||||
|
||||
override fun convert(value: Currency): com.tangem.lib.crypto.models.Currency {
|
||||
override fun convert(value: Currency): CryptoCurrency {
|
||||
return when (value) {
|
||||
is Currency.NonNativeToken -> {
|
||||
com.tangem.lib.crypto.models.Currency.NonNativeToken(
|
||||
CryptoCurrency.NonNativeToken(
|
||||
id = value.id,
|
||||
name = value.name,
|
||||
symbol = value.symbol,
|
||||
|
|
@ -18,7 +19,7 @@ class CryptoCurrencyConverter : Converter<Currency, com.tangem.lib.crypto.models
|
|||
)
|
||||
}
|
||||
is Currency.NativeToken -> {
|
||||
com.tangem.lib.crypto.models.Currency.NativeToken(
|
||||
CryptoCurrency.NativeToken(
|
||||
id = value.id,
|
||||
name = value.name,
|
||||
symbol = value.symbol,
|
||||
|
|
@ -27,4 +28,29 @@ class CryptoCurrencyConverter : Converter<Currency, com.tangem.lib.crypto.models
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun convertBack(value: CryptoCurrency): Currency {
|
||||
return when (value) {
|
||||
is CryptoCurrency.NonNativeToken -> {
|
||||
Currency.NonNativeToken(
|
||||
id = value.id,
|
||||
name = value.name,
|
||||
symbol = value.symbol,
|
||||
networkId = value.networkId,
|
||||
contractAddress = value.contractAddress,
|
||||
decimalCount = value.decimalCount,
|
||||
logoUrl = "",
|
||||
)
|
||||
}
|
||||
is CryptoCurrency.NativeToken -> {
|
||||
Currency.NativeToken(
|
||||
id = value.id,
|
||||
name = value.name,
|
||||
symbol = value.symbol,
|
||||
networkId = value.networkId,
|
||||
logoUrl = "",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
package com.tangem.feature.swap.domain.models.cache
|
||||
|
||||
import com.tangem.feature.swap.domain.models.ApproveModel
|
||||
import com.tangem.feature.swap.domain.models.Currency
|
||||
import com.tangem.feature.swap.domain.models.QuoteModel
|
||||
import com.tangem.feature.swap.domain.models.domain.ApproveModel
|
||||
import com.tangem.feature.swap.domain.models.domain.Currency
|
||||
import com.tangem.feature.swap.domain.models.domain.QuoteModel
|
||||
import com.tangem.feature.swap.domain.models.SwapAmount
|
||||
import com.tangem.feature.swap.domain.models.SwapDataModel
|
||||
import com.tangem.feature.swap.domain.models.domain.SwapDataModel
|
||||
|
||||
data class SwapDataHolder(
|
||||
val quoteModel: QuoteModel? = null,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.feature.swap.domain.models
|
||||
package com.tangem.feature.swap.domain.models.domain
|
||||
|
||||
/**
|
||||
* Approve model
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.feature.swap.domain.models
|
||||
package com.tangem.feature.swap.domain.models.domain
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
package com.tangem.feature.swap.domain.models
|
||||
package com.tangem.feature.swap.domain.models.domain
|
||||
|
||||
import com.tangem.feature.swap.domain.models.SwapAmount
|
||||
|
||||
/**
|
||||
* Quote model holds data about current amounts of exchange and fees
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
package com.tangem.feature.swap.domain.models
|
||||
package com.tangem.feature.swap.domain.models.domain
|
||||
|
||||
import com.tangem.feature.swap.domain.models.SwapAmount
|
||||
|
||||
/**
|
||||
* Swap transaction model
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.feature.swap.domain.models
|
||||
package com.tangem.feature.swap.domain.models.domain
|
||||
|
||||
/**
|
||||
* Transaction model
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.feature.swap.domain.models
|
||||
package com.tangem.feature.swap.domain.models.ui
|
||||
|
||||
import java.math.BigDecimal
|
||||
import com.tangem.feature.swap.domain.models.DataError
|
||||
import com.tangem.feature.swap.domain.models.SwapAmount
|
||||
|
||||
sealed interface SwapState {
|
||||
|
||||
|
|
@ -18,11 +19,6 @@ sealed interface SwapState {
|
|||
val permissionState: PermissionDataState = PermissionDataState.Empty,
|
||||
) : SwapState
|
||||
|
||||
data class SwapSuccess(
|
||||
val fromTokenAmount: SwapAmount,
|
||||
val toTokenAmount: SwapAmount,
|
||||
) : SwapState
|
||||
|
||||
data class SwapError(
|
||||
val errorType: DataError,
|
||||
) : SwapState
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package com.tangem.feature.swap.domain.models.ui
|
||||
|
||||
import com.tangem.feature.swap.domain.models.domain.Currency
|
||||
|
||||
data class TokensDataState(
|
||||
val preselectTokens: PreselectTokens,
|
||||
val foundTokensState: FoundTokensState,
|
||||
)
|
||||
|
||||
data class FoundTokensState(
|
||||
val tokensInWallet: List<TokenWithBalance>,
|
||||
val loadedTokens: List<TokenWithBalance>,
|
||||
)
|
||||
|
||||
data class PreselectTokens(
|
||||
val fromToken: Currency,
|
||||
val toToken: Currency,
|
||||
)
|
||||
|
||||
data class TokenWithBalance(
|
||||
val token: Currency,
|
||||
val tokenBalanceData: TokenBalanceData? = null,
|
||||
)
|
||||
|
||||
data class TokenBalanceData(
|
||||
val amount: String?,
|
||||
val amountEquivalent: String?,
|
||||
)
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package com.tangem.feature.swap.converters
|
||||
|
||||
import com.tangem.feature.swap.domain.models.ui.FoundTokensState
|
||||
import com.tangem.feature.swap.domain.models.ui.TokenWithBalance
|
||||
import com.tangem.feature.swap.models.SwapSelectTokenStateHolder
|
||||
import com.tangem.feature.swap.models.TokenBalanceData
|
||||
import com.tangem.feature.swap.models.TokenToSelect
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
class TokensDataConverter(
|
||||
private val onSearchEntered: (String) -> Unit,
|
||||
private val onTokenSelected: (String) -> Unit,
|
||||
) : Converter<FoundTokensState, SwapSelectTokenStateHolder> {
|
||||
|
||||
override fun convert(value: FoundTokensState): SwapSelectTokenStateHolder {
|
||||
return SwapSelectTokenStateHolder(
|
||||
tokens = (value.tokensInWallet + value.loadedTokens).map { tokenWithBalanceToTokenToSelect(it) },
|
||||
onSearchEntered = onSearchEntered,
|
||||
onTokenSelected = onTokenSelected,
|
||||
)
|
||||
}
|
||||
|
||||
private fun tokenWithBalanceToTokenToSelect(tokenWithBalance: TokenWithBalance): TokenToSelect {
|
||||
return TokenToSelect(
|
||||
id = tokenWithBalance.token.id,
|
||||
name = tokenWithBalance.token.name,
|
||||
symbol = tokenWithBalance.token.symbol,
|
||||
iconUrl = tokenWithBalance.token.logoUrl,
|
||||
addedTokenBalanceData = TokenBalanceData(
|
||||
amount = tokenWithBalance.tokenBalanceData?.amount,
|
||||
amountEquivalent = tokenWithBalance.tokenBalanceData?.amountEquivalent,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.tangem.feature.swap.models
|
||||
|
||||
data class UiActions(
|
||||
val onSearchEntered: (String) -> Unit,
|
||||
val onTokenSelected: (String) -> Unit,
|
||||
val onAmountChanged: (String) -> Unit,
|
||||
val onSwapClick: () -> Unit,
|
||||
val onGivePermissionClick: () -> Unit,
|
||||
val onChangeCardsClicked: () -> Unit,
|
||||
val onBackClicked: () -> Unit,
|
||||
)
|
||||
|
|
@ -1,9 +1,11 @@
|
|||
package com.tangem.feature.swap.ui
|
||||
|
||||
import com.tangem.feature.swap.domain.models.Currency
|
||||
import com.tangem.feature.swap.domain.models.PermissionDataState
|
||||
import com.tangem.feature.swap.domain.models.SwapState
|
||||
import com.tangem.feature.swap.converters.TokensDataConverter
|
||||
import com.tangem.feature.swap.domain.models.domain.Currency
|
||||
import com.tangem.feature.swap.domain.models.formatToUIRepresentation
|
||||
import com.tangem.feature.swap.domain.models.ui.FoundTokensState
|
||||
import com.tangem.feature.swap.domain.models.ui.PermissionDataState
|
||||
import com.tangem.feature.swap.domain.models.ui.SwapState
|
||||
import com.tangem.feature.swap.models.ApprovePermissionButton
|
||||
import com.tangem.feature.swap.models.CancelPermissionButton
|
||||
import com.tangem.feature.swap.models.FeeState
|
||||
|
|
@ -13,16 +15,19 @@ import com.tangem.feature.swap.models.SwapPermissionState
|
|||
import com.tangem.feature.swap.models.SwapStateHolder
|
||||
import com.tangem.feature.swap.models.SwapWarning
|
||||
import com.tangem.feature.swap.models.TransactionCardType
|
||||
import com.tangem.feature.swap.models.UiActions
|
||||
|
||||
/**
|
||||
* State builder creates a specific states for SwapScreen
|
||||
*/
|
||||
class StateBuilder {
|
||||
class StateBuilder(val actions: UiActions) {
|
||||
|
||||
fun createInitialLoadingState(networkCurrency: String, onAmountChanged: (String) -> Unit): SwapStateHolder {
|
||||
private val tokensDataConverter = TokensDataConverter(actions.onSearchEntered, actions.onTokenSelected)
|
||||
|
||||
fun createInitialLoadingState(networkCurrency: String): SwapStateHolder {
|
||||
return SwapStateHolder(
|
||||
sendCardData = SwapCardData(
|
||||
type = TransactionCardType.SendCard(onAmountChanged),
|
||||
type = TransactionCardType.SendCard(actions.onAmountChanged),
|
||||
amount = null,
|
||||
amountEquivalent = null,
|
||||
tokenIconUrl = "",
|
||||
|
|
@ -43,8 +48,8 @@ class StateBuilder {
|
|||
networkCurrency = networkCurrency,
|
||||
swapButton = SwapButton(enabled = false, loading = true, onClick = {}),
|
||||
onRefresh = {},
|
||||
onBackClicked = {},
|
||||
onChangeCardsClicked = {},
|
||||
onBackClicked = actions.onBackClicked,
|
||||
onChangeCardsClicked = actions.onChangeCardsClicked,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -66,8 +71,8 @@ class StateBuilder {
|
|||
),
|
||||
receiveCardData = SwapCardData(
|
||||
type = TransactionCardType.ReceiveCard(),
|
||||
amount = uiStateHolder.receiveCardData.amount,
|
||||
amountEquivalent = uiStateHolder.receiveCardData.amountEquivalent,
|
||||
amount = null,
|
||||
amountEquivalent = null,
|
||||
tokenIconUrl = toToken.logoUrl,
|
||||
tokenCurrency = toToken.symbol,
|
||||
canSelectAnotherToken = mainTokenId != toToken.id,
|
||||
|
|
@ -75,6 +80,7 @@ class StateBuilder {
|
|||
),
|
||||
fee = FeeState.Loading,
|
||||
swapButton = SwapButton(enabled = false, loading = true, onClick = {}),
|
||||
permissionState = uiStateHolder.permissionState,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -82,8 +88,6 @@ class StateBuilder {
|
|||
uiStateHolder: SwapStateHolder,
|
||||
quoteModel: SwapState.QuotesLoadedState,
|
||||
fromToken: Currency,
|
||||
onSwapClick: () -> Unit,
|
||||
onGivePermissionClick: () -> Unit,
|
||||
): SwapStateHolder {
|
||||
return uiStateHolder.copy(
|
||||
sendCardData = SwapCardData(
|
||||
|
|
@ -109,16 +113,31 @@ class StateBuilder {
|
|||
} else {
|
||||
emptyList()
|
||||
},
|
||||
permissionState = convertPermissionState(quoteModel.permissionState, onGivePermissionClick),
|
||||
permissionState = convertPermissionState(quoteModel.permissionState, actions.onGivePermissionClick),
|
||||
fee = FeeState.Loaded(quoteModel.fee),
|
||||
swapButton = SwapButton(
|
||||
enabled = quoteModel.isAllowedToSpend,
|
||||
loading = false,
|
||||
onClick = onSwapClick,
|
||||
onClick = actions.onSwapClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun createSwapInProgressState(uiState: SwapStateHolder): SwapStateHolder {
|
||||
return uiState.copy(
|
||||
swapButton = uiState.swapButton.copy(
|
||||
loading = true,
|
||||
enabled = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun addTokensToState(uiState: SwapStateHolder, dataState: FoundTokensState): SwapStateHolder {
|
||||
return uiState.copy(
|
||||
selectTokenState = tokensDataConverter.convert(dataState),
|
||||
)
|
||||
}
|
||||
|
||||
private fun convertPermissionState(
|
||||
permissionDataState: PermissionDataState,
|
||||
onGivePermissionClick: () -> Unit,
|
||||
|
|
@ -144,4 +163,13 @@ class StateBuilder {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateSwapAmount(uiState: SwapStateHolder, amount: String, amountEquivalent: String): SwapStateHolder {
|
||||
return uiState.copy(
|
||||
sendCardData = uiState.sendCardData.copy(
|
||||
amount = amount,
|
||||
amountEquivalent = amountEquivalent,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -208,7 +208,7 @@ private fun Content(
|
|||
}
|
||||
is TransactionCardType.SendCard -> {
|
||||
AutoSizeTextField(
|
||||
amount = amount ?: "0",
|
||||
amount = amount ?: "1",
|
||||
onAmoutChanged = { type.onAmountChanged(it) },
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
package com.tangem.feature.swap.viewmodels
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
class PeriodicTask(
|
||||
private val delay: Long,
|
||||
private val task: suspend () -> Unit,
|
||||
) {
|
||||
|
||||
private var isActive: AtomicBoolean = AtomicBoolean(false)
|
||||
|
||||
suspend fun runTaskWithDelay() {
|
||||
isActive.set(true)
|
||||
while (isActive.get()) {
|
||||
task.invoke()
|
||||
delay(delay)
|
||||
}
|
||||
}
|
||||
|
||||
fun cancel() {
|
||||
isActive.set(false)
|
||||
}
|
||||
}
|
||||
|
||||
class SingleTaskScheduler {
|
||||
|
||||
private var lastTask: PeriodicTask? = null
|
||||
|
||||
fun scheduleTask(scope: CoroutineScope, task: PeriodicTask) {
|
||||
lastTask?.cancel()
|
||||
lastTask = task
|
||||
scope.launch {
|
||||
task.runTaskWithDelay()
|
||||
}
|
||||
}
|
||||
|
||||
fun cancelTask() {
|
||||
lastTask?.cancel()
|
||||
}
|
||||
}
|
||||
|
|
@ -8,18 +8,21 @@ import androidx.lifecycle.SavedStateHandle
|
|||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.tangem.feature.swap.domain.SwapInteractor
|
||||
import com.tangem.feature.swap.domain.models.Currency
|
||||
import com.tangem.feature.swap.domain.models.SwapState
|
||||
import com.tangem.feature.swap.domain.models.createFromAmountWithoutOffset
|
||||
import com.tangem.feature.swap.domain.models.domain.Currency
|
||||
import com.tangem.feature.swap.domain.models.ui.FoundTokensState
|
||||
import com.tangem.feature.swap.domain.models.ui.SwapState
|
||||
import com.tangem.feature.swap.models.SwapStateHolder
|
||||
import com.tangem.feature.swap.models.UiActions
|
||||
import com.tangem.feature.swap.presentation.SwapFragment
|
||||
import com.tangem.feature.swap.router.SwapRouter
|
||||
import com.tangem.feature.swap.router.SwapScreen
|
||||
import com.tangem.feature.swap.ui.StateBuilder
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.Debouncer
|
||||
import com.tangem.utils.coroutines.runCatching
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import javax.inject.Inject
|
||||
|
|
@ -32,23 +35,31 @@ internal class SwapViewModel @Inject constructor(
|
|||
savedStateHandle: SavedStateHandle,
|
||||
) : ViewModel() {
|
||||
|
||||
private val stateBuilder = StateBuilder()
|
||||
private val stateBuilder = StateBuilder(
|
||||
actions = createUiActions(),
|
||||
)
|
||||
|
||||
private val amountDebouncer = Debouncer()
|
||||
private val singleTaskScheduler = SingleTaskScheduler()
|
||||
private val currency = Json.decodeFromString<Currency>(
|
||||
savedStateHandle[SwapFragment.CURRENCY_BUNDLE_KEY]
|
||||
?: error("no expected parameter Currency found"),
|
||||
)
|
||||
|
||||
var uiState: SwapStateHolder by mutableStateOf(
|
||||
stateBuilder.createInitialLoadingState(currency.symbol)
|
||||
{ /** [REDACTED_TODO_COMMENT]*/ },
|
||||
)
|
||||
var uiState: SwapStateHolder by mutableStateOf(stateBuilder.createInitialLoadingState(currency.symbol))
|
||||
private set
|
||||
|
||||
//shows currency order (direct - swap initial to selected, reversed = selected to initial)
|
||||
private var isOrderReversed = false
|
||||
private val lastAmount = mutableStateOf(INITIAL_AMOUNT)
|
||||
private var swapRouter: SwapRouter by Delegates.notNull()
|
||||
|
||||
var currentScreen = SwapScreen.Main
|
||||
get() = swapRouter.currentScreen
|
||||
|
||||
init {
|
||||
initTokens(currency)
|
||||
}
|
||||
|
||||
fun setRouter(router: SwapRouter) {
|
||||
swapRouter = router
|
||||
uiState = uiState.copy(
|
||||
|
|
@ -58,21 +69,18 @@ internal class SwapViewModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
init {
|
||||
private fun initTokens(currency: Currency) {
|
||||
viewModelScope.launch(dispatchers.main) {
|
||||
runCatching {
|
||||
withContext(dispatchers.io) {
|
||||
swapInteractor.getTokensToSwap(currency.networkId)
|
||||
}
|
||||
runCatching(dispatchers.io) {
|
||||
swapInteractor.initTokensToSwap(currency)
|
||||
}
|
||||
.onSuccess { tokens ->
|
||||
if (tokens.size > MIN_TOKENS_IN_LIST) {
|
||||
tokens.firstOrNull { token ->
|
||||
token.id == currency.id
|
||||
}?.let {
|
||||
loadQuotes(it, tokens[1], INITIAL_AMOUNT)
|
||||
}
|
||||
}
|
||||
.onSuccess { state ->
|
||||
updateTokensState(dataState = state.foundTokensState)
|
||||
startLoadingQuotes(
|
||||
fromToken = state.preselectTokens.fromToken,
|
||||
toToken = state.preselectTokens.toToken,
|
||||
amount = lastAmount.value,
|
||||
)
|
||||
}
|
||||
.onFailure {
|
||||
Log.e("SwapViewModel", it.message ?: it.cause.toString())
|
||||
|
|
@ -80,20 +88,38 @@ internal class SwapViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun loadQuotes(fromToken: Currency, toToken: Currency, amount: String) {
|
||||
private fun updateTokensState(dataState: FoundTokensState) {
|
||||
uiState = stateBuilder.addTokensToState(uiState, dataState)
|
||||
}
|
||||
|
||||
private fun startLoadingQuotes(fromToken: Currency, toToken: Currency, amount: String) {
|
||||
singleTaskScheduler.cancelTask()
|
||||
uiState = stateBuilder.createQuotesLoadingState(uiState, fromToken, toToken, currency.id)
|
||||
singleTaskScheduler.scheduleTask(
|
||||
viewModelScope,
|
||||
PeriodicTask(
|
||||
delay = UPDATE_DELAY,
|
||||
) {
|
||||
loadQuotesInternal(
|
||||
fromToken = fromToken,
|
||||
toToken = toToken,
|
||||
amount = amount,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun loadQuotesInternal(fromToken: Currency, toToken: Currency, amount: String) {
|
||||
viewModelScope.launch(dispatchers.main) {
|
||||
runCatching {
|
||||
withContext(dispatchers.io) {
|
||||
swapInteractor.findBestQuote(
|
||||
fromToken = fromToken,
|
||||
toToken = toToken,
|
||||
amount = createFromAmountWithoutOffset(
|
||||
amountWithoutOffset = amount,
|
||||
decimals = swapInteractor.getTokenDecimals(fromToken),
|
||||
),
|
||||
)
|
||||
}
|
||||
runCatching(dispatchers.io) {
|
||||
swapInteractor.findBestQuote(
|
||||
fromToken = fromToken,
|
||||
toToken = toToken,
|
||||
amount = createFromAmountWithoutOffset(
|
||||
amountWithoutOffset = amount,
|
||||
decimals = swapInteractor.getTokenDecimals(fromToken),
|
||||
),
|
||||
)
|
||||
}
|
||||
.onSuccess { swapState ->
|
||||
when (swapState) {
|
||||
|
|
@ -102,13 +128,8 @@ internal class SwapViewModel @Inject constructor(
|
|||
uiStateHolder = uiState,
|
||||
quoteModel = swapState,
|
||||
fromToken = fromToken,
|
||||
onSwapClick = { onSwapClick(toToken, swapState) },
|
||||
onGivePermissionClick = { givePermissionsToSwap(fromToken) },
|
||||
)
|
||||
}
|
||||
is SwapState.SwapSuccess -> {
|
||||
//todo implement
|
||||
}
|
||||
is SwapState.SwapError -> {
|
||||
}
|
||||
}
|
||||
|
|
@ -117,46 +138,106 @@ internal class SwapViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun onSwapClick(toToken: Currency, quoteModel: SwapState.QuotesLoadedState) {
|
||||
private fun createUiActions(): UiActions {
|
||||
return UiActions(
|
||||
onSearchEntered = { onSearchEntered(it) },
|
||||
onTokenSelected = { onTokenSelect(it) },
|
||||
onAmountChanged = { onAmountChanged(it) },
|
||||
onSwapClick = { onSwapClick() },
|
||||
onGivePermissionClick = { givePermissionsToSwap() },
|
||||
onChangeCardsClicked = { onChangeCardsClicked() },
|
||||
onBackClicked = { onSearchEntered("") },
|
||||
)
|
||||
}
|
||||
|
||||
private fun onSwapClick() {
|
||||
singleTaskScheduler.cancelTask()
|
||||
uiState = stateBuilder.createSwapInProgressState(uiState)
|
||||
viewModelScope.launch(dispatchers.main) {
|
||||
runCatching {
|
||||
withContext(dispatchers.io) {
|
||||
if (!quoteModel.isAllowedToSpend) {
|
||||
swapInteractor.givePermissionToSwap(toToken)
|
||||
} else {
|
||||
swapInteractor.onSwap()
|
||||
}
|
||||
}
|
||||
runCatching(dispatchers.io) {
|
||||
swapInteractor.onSwap()
|
||||
}
|
||||
.onSuccess {
|
||||
if (it is SwapState) {
|
||||
when (it) {
|
||||
is SwapState.SwapSuccess -> {
|
||||
}
|
||||
is SwapState.SwapError -> {
|
||||
}
|
||||
else -> {}
|
||||
when (it) {
|
||||
is SwapState.SwapError -> {
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
.onFailure { }
|
||||
}
|
||||
}
|
||||
|
||||
private fun givePermissionsToSwap(tokenToApprove: Currency) {
|
||||
private fun givePermissionsToSwap() {
|
||||
viewModelScope.launch(dispatchers.main) {
|
||||
runCatching {
|
||||
withContext(dispatchers.io) {
|
||||
swapInteractor.givePermissionToSwap(tokenToApprove)
|
||||
}
|
||||
runCatching(dispatchers.io) {
|
||||
swapInteractor.givePermissionToSwap()
|
||||
}
|
||||
.onSuccess { }
|
||||
.onFailure { }
|
||||
}
|
||||
}
|
||||
|
||||
private fun onSearchEntered(searchQuery: String) {
|
||||
viewModelScope.launch(dispatchers.main) {
|
||||
runCatching(dispatchers.io) {
|
||||
swapInteractor.onSearchToken(searchQuery)
|
||||
}
|
||||
.onSuccess {
|
||||
updateTokensState(it)
|
||||
}
|
||||
.onFailure { }
|
||||
}
|
||||
}
|
||||
|
||||
private fun onTokenSelect(id: String) {
|
||||
val foundToken = swapInteractor.findTokenById(id)
|
||||
if (foundToken != null) {
|
||||
val fromToken: Currency
|
||||
val toToken: Currency
|
||||
if (isOrderReversed) {
|
||||
fromToken = foundToken
|
||||
toToken = currency
|
||||
} else {
|
||||
fromToken = currency
|
||||
toToken = foundToken
|
||||
}
|
||||
swapRouter.openScreen(SwapScreen.Main)
|
||||
startLoadingQuotes(fromToken, toToken, lastAmount.value)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onChangeCardsClicked() {
|
||||
val currencies = swapInteractor.getExchangeCurrencies()
|
||||
val newFromToken = currencies?.toCurrency
|
||||
val newToToken = currencies?.fromCurrency
|
||||
if (newFromToken != null && newToToken != null) {
|
||||
isOrderReversed = !isOrderReversed
|
||||
startLoadingQuotes(newFromToken, newToToken, lastAmount.value)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onAmountChanged(value: String) {
|
||||
uiState = stateBuilder.updateSwapAmount(uiState, value, value)
|
||||
lastAmount.value = value
|
||||
amountDebouncer.debounce(DEBOUNCE_AMOUNT_DELAY, viewModelScope) {
|
||||
val currencies = swapInteractor.getExchangeCurrencies()
|
||||
val fromToken = currencies?.fromCurrency
|
||||
val toToken = currencies?.toCurrency
|
||||
if (fromToken != null && toToken != null) {
|
||||
startLoadingQuotes(fromToken, toToken, lastAmount.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
singleTaskScheduler.cancelTask()
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val MIN_TOKENS_IN_LIST = 2
|
||||
private const val INITIAL_AMOUNT = "1"
|
||||
private const val UPDATE_DELAY = 10000L
|
||||
private const val DEBOUNCE_AMOUNT_DELAY = 1000L
|
||||
}
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ interface TransactionManager {
|
|||
amountToSend: BigDecimal,
|
||||
currencyToSend: Currency,
|
||||
feeAmount: BigDecimal,
|
||||
estimatedGas: Int,
|
||||
destinationAddress: String,
|
||||
dataToSign: String,
|
||||
): SendTxResult
|
||||
|
|
@ -25,8 +26,6 @@ interface TransactionManager {
|
|||
destinationAddress: String,
|
||||
): ProxyAmount
|
||||
|
||||
fun getNativeAddress(networkId: String): String
|
||||
|
||||
@Throws(IllegalStateException::class)
|
||||
fun getNativeTokenDecimals(networkId: String): Int
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ interface UserWalletManager {
|
|||
/**
|
||||
* Returns all user tokens (merged from local and backend)
|
||||
*/
|
||||
suspend fun getUserTokens(): List<Currency>
|
||||
suspend fun getUserTokens(networkId: String): List<Currency>
|
||||
|
||||
/**
|
||||
* Returns user walletId
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue