Updated on 2026-08-14

This commit is contained in:
Tangem 2023-01-17 16:23:33 +03:00
parent 78818a6efa
commit bd3ee9e237
33 changed files with 643 additions and 207 deletions

View file

@ -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) {

View file

@ -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 -> {

View file

@ -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 {

View file

@ -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,
)

View file

@ -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) }
}
}

View file

@ -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()
}
}
}

View file

@ -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))

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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(

View file

@ -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
}
}

View file

@ -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 {

View file

@ -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>
}

View file

@ -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
}

View file

@ -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 = "",
)
}
}
}
}

View file

@ -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,

View file

@ -1,4 +1,4 @@
package com.tangem.feature.swap.domain.models
package com.tangem.feature.swap.domain.models.domain
/**
* Approve model

View file

@ -1,4 +1,4 @@
package com.tangem.feature.swap.domain.models
package com.tangem.feature.swap.domain.models.domain
import kotlinx.serialization.Serializable

View file

@ -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

View file

@ -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

View file

@ -1,4 +1,4 @@
package com.tangem.feature.swap.domain.models
package com.tangem.feature.swap.domain.models.domain
/**
* Transaction model

View file

@ -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

View file

@ -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?,
)

View file

@ -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,
),
)
}
}

View file

@ -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,
)

View file

@ -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,
),
)
}
}

View file

@ -208,7 +208,7 @@ private fun Content(
}
is TransactionCardType.SendCard -> {
AutoSizeTextField(
amount = amount ?: "0",
amount = amount ?: "1",
onAmoutChanged = { type.onAmountChanged(it) },
)
}

View file

@ -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()
}
}

View file

@ -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
}
}

View file

@ -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

View file

@ -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