Updated on 2026-08-14
This commit is contained in:
commit
d159d4e55b
24 changed files with 843 additions and 314 deletions
|
|
@ -323,7 +323,9 @@ class DetailsMiddleware {
|
|||
enableAccessCodesSaving: Boolean,
|
||||
): CompletionResult<Unit> {
|
||||
val userWallet = scanResponse?.let { UserWalletBuilder(it).build() }
|
||||
?: return CompletionResult.Failure(TangemSdkError.ExceptionError(IllegalStateException()))
|
||||
?: return CompletionResult.Failure(
|
||||
TangemSdkError.ExceptionError(IllegalStateException("scanResponse is null")),
|
||||
)
|
||||
|
||||
return userWalletsListManager.save(userWallet)
|
||||
.flatMap {
|
||||
|
|
|
|||
|
|
@ -32,10 +32,8 @@ class TransactionManagerImpl(
|
|||
private val appStateHolder: AppStateHolder,
|
||||
) : TransactionManager {
|
||||
|
||||
override suspend fun sendTransaction(
|
||||
override suspend fun sendApproveTransaction(
|
||||
networkId: String,
|
||||
amountToSend: BigDecimal,
|
||||
currencyToSend: Currency,
|
||||
feeAmount: BigDecimal,
|
||||
estimatedGas: Int,
|
||||
destinationAddress: String,
|
||||
|
|
@ -43,8 +41,58 @@ class TransactionManagerImpl(
|
|||
): SendTxResult {
|
||||
val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" }
|
||||
val walletManager = getActualWalletManager(blockchain)
|
||||
val amount = createAmount(amountToSend, currencyToSend, blockchain)
|
||||
walletManager.update()
|
||||
val amount = Amount(value = BigDecimal.ZERO, blockchain = blockchain)
|
||||
return sendTransactionInternal(
|
||||
walletManager = walletManager,
|
||||
amount = amount,
|
||||
blockchain = blockchain,
|
||||
feeAmount = feeAmount,
|
||||
estimatedGas = estimatedGas,
|
||||
destinationAddress = destinationAddress,
|
||||
dataToSign = dataToSign,
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun sendTransaction(
|
||||
networkId: String,
|
||||
amountToSend: BigDecimal,
|
||||
feeAmount: BigDecimal,
|
||||
estimatedGas: Int,
|
||||
destinationAddress: String,
|
||||
dataToSign: String,
|
||||
isSwap: Boolean,
|
||||
currencyToSend: Currency,
|
||||
): SendTxResult {
|
||||
val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" }
|
||||
val walletManager = getActualWalletManager(blockchain)
|
||||
walletManager.update()
|
||||
val amount = if (isSwap) {
|
||||
createAmountForSwap(amountToSend, currencyToSend, blockchain)
|
||||
} else {
|
||||
createAmount(amountToSend, currencyToSend, blockchain)
|
||||
}
|
||||
return sendTransactionInternal(
|
||||
walletManager = walletManager,
|
||||
amount = amount,
|
||||
blockchain = blockchain,
|
||||
feeAmount = feeAmount,
|
||||
estimatedGas = estimatedGas,
|
||||
destinationAddress = destinationAddress,
|
||||
dataToSign = dataToSign,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
private suspend fun sendTransactionInternal(
|
||||
walletManager: WalletManager,
|
||||
amount: Amount,
|
||||
blockchain: Blockchain,
|
||||
feeAmount: BigDecimal,
|
||||
estimatedGas: Int,
|
||||
destinationAddress: String,
|
||||
dataToSign: String,
|
||||
): SendTxResult {
|
||||
val txData = walletManager.createTransaction(
|
||||
amount = amount,
|
||||
fee = Amount(value = feeAmount, blockchain = blockchain),
|
||||
|
|
@ -66,6 +114,11 @@ class TransactionManagerImpl(
|
|||
return Blockchain.fromNetworkId(networkId)?.decimals() ?: error("blockchain not found")
|
||||
}
|
||||
|
||||
override suspend fun updateWalletManager(networkId: String) {
|
||||
val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" }
|
||||
getActualWalletManager(blockchain).update()
|
||||
}
|
||||
|
||||
override fun calculateFee(networkId: String, gasPrice: String, estimatedGas: Int): BigDecimal {
|
||||
val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" }
|
||||
val gasPriceValue = requireNotNull(gasPrice.toLongOrNull()) { "gasprice should be Long" }
|
||||
|
|
@ -119,7 +172,7 @@ class TransactionManagerImpl(
|
|||
return TangemSigner(
|
||||
card = actualCard,
|
||||
tangemSdk = tangemSdk,
|
||||
initialMessage = Message("test transaction title", "test description"),
|
||||
initialMessage = Message(),
|
||||
) { signResponse ->
|
||||
store.dispatch(
|
||||
GlobalAction.UpdateWalletSignedHashes(
|
||||
|
|
@ -179,6 +232,29 @@ class TransactionManagerImpl(
|
|||
}
|
||||
}
|
||||
|
||||
private fun createAmountForSwap(
|
||||
amount: BigDecimal,
|
||||
currency: Currency?,
|
||||
blockchain: Blockchain,
|
||||
): Amount {
|
||||
return when (currency) {
|
||||
is Currency.NativeToken,
|
||||
null,
|
||||
-> {
|
||||
Amount(value = amount, blockchain = blockchain)
|
||||
}
|
||||
is Currency.NonNativeToken -> {
|
||||
// 1. when creates swap amount for NonNativeToken, amount should be ZERO
|
||||
// 2. Amount has .Coin type, as workaround to use destinationAddress in bsdk, not contractAddress
|
||||
Amount(
|
||||
currencySymbol = currency.symbol,
|
||||
value = BigDecimal.ZERO,
|
||||
decimals = currency.decimalCount,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun convertNonNativeToken(token: Currency.NonNativeToken): Token {
|
||||
return Token(
|
||||
name = token.name,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.tap.proxy
|
||||
|
||||
import com.tangem.blockchain.common.AmountType
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.DerivationParams
|
||||
import com.tangem.blockchain.common.DerivationStyle
|
||||
|
|
@ -119,6 +120,25 @@ class UserWalletManagerImpl(
|
|||
}.toMap()
|
||||
}
|
||||
|
||||
override fun getNativeTokenBalance(networkId: String): ProxyAmount? {
|
||||
val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" }
|
||||
val walletManager = getActualWalletManager(blockchain)
|
||||
return walletManager.wallet.amounts.firstNotNullOfOrNull {
|
||||
it.takeIf { it.key is AmountType.Coin }
|
||||
}?.value?.let {
|
||||
ProxyAmount(
|
||||
it.currencySymbol,
|
||||
it.value ?: BigDecimal.ZERO,
|
||||
it.decimals,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getNetworkCurrency(networkId: String): String {
|
||||
val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" }
|
||||
return blockchain.currency
|
||||
}
|
||||
|
||||
override fun getUserAppCurrency(): ProxyFiatCurrency {
|
||||
val appCurrency = appStateHolder.appFiatCurrency
|
||||
return ProxyFiatCurrency(
|
||||
|
|
|
|||
|
|
@ -24,6 +24,14 @@ class OneInchApisModule {
|
|||
apiFactory.putApi(ETH_NETWORK, createOneInchApiWithUrl(ONE_INCH_BASE_URL + ONE_INCH_ETH_PATH, moshi))
|
||||
apiFactory.putApi(BSC_NETWORK, createOneInchApiWithUrl(ONE_INCH_BASE_URL + ONE_INCH_BSC_PATH, moshi))
|
||||
apiFactory.putApi(POLYGON_NETWORK, createOneInchApiWithUrl(ONE_INCH_BASE_URL + ONE_INCH_POLYGON_PATH, moshi))
|
||||
apiFactory.putApi(OPTIMISM_NETWORK, createOneInchApiWithUrl(ONE_INCH_BASE_URL + ONE_INCH_OPTIMISM_PATH, moshi))
|
||||
apiFactory.putApi(ARBITRUM_NETWORK, createOneInchApiWithUrl(ONE_INCH_BASE_URL + ONE_INCH_ARBITRUM_PATH, moshi))
|
||||
apiFactory.putApi(GNOSIS_NETWORK, createOneInchApiWithUrl(ONE_INCH_BASE_URL + ONE_INCH_GNOSIS_PATH, moshi))
|
||||
apiFactory.putApi(
|
||||
AVALANCHE_NETWORK,
|
||||
createOneInchApiWithUrl(ONE_INCH_BASE_URL + ONE_INCH_AVALANCHE_PATH, moshi),
|
||||
)
|
||||
apiFactory.putApi(FANTOM_NETWORK, createOneInchApiWithUrl(ONE_INCH_BASE_URL + ONE_INCH_FANTOM_PATH, moshi))
|
||||
return apiFactory
|
||||
}
|
||||
|
||||
|
|
@ -47,9 +55,19 @@ class OneInchApisModule {
|
|||
private const val ONE_INCH_ETH_PATH = "1/"
|
||||
private const val ONE_INCH_BSC_PATH = "56/"
|
||||
private const val ONE_INCH_POLYGON_PATH = "137/"
|
||||
private const val ONE_INCH_OPTIMISM_PATH = "10/"
|
||||
private const val ONE_INCH_ARBITRUM_PATH = "42161/"
|
||||
private const val ONE_INCH_GNOSIS_PATH = "100/"
|
||||
private const val ONE_INCH_AVALANCHE_PATH = "43114/"
|
||||
private const val ONE_INCH_FANTOM_PATH = "250/"
|
||||
|
||||
private const val ETH_NETWORK = "ethereum"
|
||||
private const val BSC_NETWORK = "binance-smart-chain"
|
||||
private const val POLYGON_NETWORK = "polygon-pos"
|
||||
private const val OPTIMISM_NETWORK = "optimistic-ethereum"
|
||||
private const val ARBITRUM_NETWORK = "arbitrum-one"
|
||||
private const val GNOSIS_NETWORK = "xdai"
|
||||
private const val AVALANCHE_NETWORK = "avalanche"
|
||||
private const val FANTOM_NETWORK = "fantom"
|
||||
}
|
||||
}
|
||||
|
|
@ -23,6 +23,25 @@ fun BigDecimal.toFormattedString(
|
|||
return df.format(this)
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
fun BigDecimal.toFormattedCurrencyString(
|
||||
decimals: Int,
|
||||
currency: String,
|
||||
roundingMode: RoundingMode = RoundingMode.DOWN,
|
||||
limitNumberOfDecimals: Boolean = true,
|
||||
): String {
|
||||
val decimalsForRounding = if (limitNumberOfDecimals) {
|
||||
if (decimals > 8) 8 else decimals
|
||||
} else {
|
||||
decimals
|
||||
}
|
||||
val formattedAmount = this.toFormattedString(
|
||||
decimals = decimalsForRounding,
|
||||
roundingMode = roundingMode,
|
||||
)
|
||||
return "$formattedAmount $currency"
|
||||
}
|
||||
|
||||
fun BigDecimal.toFiatString(
|
||||
rateValue: BigDecimal,
|
||||
fiatCurrencyName: String,
|
||||
|
|
|
|||
|
|
@ -1,37 +1,94 @@
|
|||
package com.tangem.feature.swap.domain
|
||||
|
||||
import com.tangem.feature.swap.domain.models.SwapAmount
|
||||
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.SwapDataModel
|
||||
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
|
||||
import com.tangem.feature.swap.domain.models.ui.TxState
|
||||
|
||||
interface SwapInteractor {
|
||||
|
||||
/**
|
||||
* Init tokens to swap, load tokens list available to swap for given network
|
||||
*
|
||||
* @param initialCurrency currency which to swap or receive
|
||||
* @return [TokensDataState] that contains info about all available to swap tokens for networkId
|
||||
* and preselected tokens which initially select to swap
|
||||
*/
|
||||
suspend fun initTokensToSwap(initialCurrency: Currency): TokensDataState
|
||||
|
||||
suspend fun onSearchToken(searchQuery: String): FoundTokensState
|
||||
|
||||
fun getExchangeCurrencies(): ExchangeCurrencies?
|
||||
/**
|
||||
* On search token, locally search tokens in previously loaded list to swap
|
||||
* searching in names and symbols
|
||||
*
|
||||
* @param networkId networkId for tokens
|
||||
* @param searchQuery string query for search
|
||||
* @return [FoundTokensState] that contains list of tokens matching condition query
|
||||
*/
|
||||
suspend fun onSearchToken(networkId: String, searchQuery: String): FoundTokensState
|
||||
|
||||
/**
|
||||
* Find specific token by id, null if not found
|
||||
*
|
||||
* @param id token id
|
||||
* @return [Currency] or null
|
||||
*/
|
||||
fun findTokenById(id: String): Currency?
|
||||
|
||||
/**
|
||||
* Give permission to swap
|
||||
* Gives permission to swap, this starts scan card process
|
||||
*
|
||||
* @param networkId network in which selected token
|
||||
* @param estimatedGas estimated gas for transaction
|
||||
* @param transactionData tx data to give approve, it loaded from 1inch in findBestQuote if needed
|
||||
* @param forTokenContractAddress token contract address for which needs permission
|
||||
*/
|
||||
@Throws(IllegalStateException::class)
|
||||
suspend fun givePermissionToSwap()
|
||||
suspend fun givePermissionToSwap(
|
||||
networkId: String,
|
||||
estimatedGas: Int,
|
||||
transactionData: ApproveModel,
|
||||
forTokenContractAddress: String,
|
||||
): TxState
|
||||
|
||||
/**
|
||||
* Find best quote for given tokens to swap
|
||||
* under the hood calls different methods to receive data, depends on permission for given token
|
||||
*
|
||||
* @param networkId network for tokens
|
||||
* @param fromToken [Currency] from which want to swap
|
||||
* @param toToken [Currency] that receive after swap
|
||||
* @param amountToSwap amount you want to swap
|
||||
* @return
|
||||
*/
|
||||
@Throws(IllegalStateException::class)
|
||||
suspend fun findBestQuote(
|
||||
networkId: String,
|
||||
fromToken: Currency,
|
||||
toToken: Currency,
|
||||
amount: SwapAmount,
|
||||
amountToSwap: String,
|
||||
): SwapState
|
||||
|
||||
/**
|
||||
* Starts swap transaction, perform sign transaction
|
||||
*
|
||||
* @param networkId network for tokens
|
||||
* @param swapData tx data to swap, contains data to sign
|
||||
* @param currencyToSend [Currency]
|
||||
* @param currencyToGet [Currency]
|
||||
* @param amountToSwap amount to swap
|
||||
* @return [TxState]
|
||||
*/
|
||||
@Throws(IllegalStateException::class)
|
||||
suspend fun onSwap(): SwapState
|
||||
suspend fun onSwap(
|
||||
networkId: String,
|
||||
swapData: SwapDataModel,
|
||||
currencyToSend: Currency,
|
||||
currencyToGet: Currency,
|
||||
amountToSwap: String,
|
||||
): TxState
|
||||
|
||||
fun getTokenDecimals(token: Currency): Int
|
||||
}
|
||||
|
|
@ -4,24 +4,28 @@ import com.tangem.feature.swap.domain.cache.SwapDataCache
|
|||
import com.tangem.feature.swap.domain.converters.CryptoCurrencyConverter
|
||||
import com.tangem.feature.swap.domain.models.DataError
|
||||
import com.tangem.feature.swap.domain.models.SwapAmount
|
||||
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.PreparedSwapConfigState
|
||||
import com.tangem.feature.swap.domain.models.domain.SwapDataModel
|
||||
import com.tangem.feature.swap.domain.models.toStringWithRightOffset
|
||||
import com.tangem.feature.swap.domain.models.ui.AmountFormatter
|
||||
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.RequestApproveStateData
|
||||
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.TokenSwapInfo
|
||||
import com.tangem.feature.swap.domain.models.ui.TokenWithBalance
|
||||
import com.tangem.feature.swap.domain.models.ui.TokensDataState
|
||||
import com.tangem.feature.swap.domain.models.ui.TxState
|
||||
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
|
||||
import java.math.BigDecimal
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -35,10 +39,10 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
) : SwapInteractor {
|
||||
|
||||
private val cryptoCurrencyConverter = CryptoCurrencyConverter()
|
||||
private val amountFormatter = AmountFormatter()
|
||||
|
||||
override suspend fun initTokensToSwap(initialCurrency: Currency): TokensDataState {
|
||||
val networkId = initialCurrency.networkId
|
||||
cache.cacheNetworkId(networkId)
|
||||
val availableTokens = cache.getAvailableTokens(networkId)
|
||||
val allLoadedTokens = availableTokens.ifEmpty {
|
||||
val tokens = repository.getExchangeableTokens(networkId)
|
||||
|
|
@ -77,8 +81,7 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
override suspend fun onSearchToken(searchQuery: String): FoundTokensState {
|
||||
val networkId = requireNotNull(cache.getNetworkId()) { "networkId is null" }
|
||||
override suspend fun onSearchToken(networkId: String, searchQuery: String): FoundTokensState {
|
||||
val searchQueryLowerCase = searchQuery.lowercase()
|
||||
val tokensInWallet = cache.getInWalletTokens()
|
||||
.filter {
|
||||
|
|
@ -99,67 +102,67 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
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 ->
|
||||
val currencyToSend =
|
||||
requireNotNull(cache.getExchangeCurrencies()?.fromCurrency) { "currency is not selected" }
|
||||
if (currencyToSend is Currency.NonNativeToken) {
|
||||
val estimatedGas =
|
||||
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")
|
||||
val fee = transactionManager.calculateFee(networkId, gasPrice.toPlainString(), estimatedGas)
|
||||
val result = transactionManager.sendTransaction(
|
||||
networkId = networkId,
|
||||
amountToSend = BigDecimal.ZERO,
|
||||
currencyToSend = cryptoCurrencyConverter.convert(currencyToSend),
|
||||
feeAmount = fee,
|
||||
estimatedGas = estimatedGas,
|
||||
destinationAddress = transactionData.toAddress,
|
||||
dataToSign = transactionData.data,
|
||||
)
|
||||
when (result) {
|
||||
SendTxResult.Success -> {
|
||||
allowPermissionsHandler.addAddressToInProgress(currencyToSend.contractAddress)
|
||||
}
|
||||
SendTxResult.UserCancelledError -> TODO()
|
||||
is SendTxResult.BlockchainSdkError -> TODO()
|
||||
is SendTxResult.TangemSdkError -> TODO()
|
||||
is SendTxResult.UnknownError -> TODO()
|
||||
}
|
||||
override suspend fun givePermissionToSwap(
|
||||
networkId: String,
|
||||
estimatedGas: Int,
|
||||
transactionData: ApproveModel,
|
||||
forTokenContractAddress: String,
|
||||
): TxState {
|
||||
val increasedEstimatedGas = increaseByPercents(TWENTY_FIVE_PERCENTS, estimatedGas)
|
||||
val gasPrice = transactionData.gasPrice.toBigDecimalOrNull() ?: error("cannot parse gasPrice")
|
||||
val fee = transactionManager.calculateFee(networkId, gasPrice.toPlainString(), increasedEstimatedGas)
|
||||
val result = transactionManager.sendApproveTransaction(
|
||||
networkId = networkId,
|
||||
feeAmount = fee,
|
||||
estimatedGas = increasedEstimatedGas,
|
||||
destinationAddress = transactionData.toAddress,
|
||||
dataToSign = transactionData.data,
|
||||
)
|
||||
return when (result) {
|
||||
SendTxResult.Success -> {
|
||||
allowPermissionsHandler.addAddressToInProgress(forTokenContractAddress)
|
||||
TxState.TxSent()
|
||||
}
|
||||
SendTxResult.UserCancelledError -> TxState.UserCancelled
|
||||
is SendTxResult.BlockchainSdkError -> TxState.BlockchainError
|
||||
is SendTxResult.TangemSdkError -> TxState.TangemSdkError
|
||||
is SendTxResult.UnknownError -> TxState.UnknownError
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun findBestQuote(fromToken: Currency, toToken: Currency, amount: SwapAmount): SwapState {
|
||||
val networkId = requireNotNull(cache.getNetworkId()) { "no networkId found, please call initTokens first" }
|
||||
override suspend fun findBestQuote(
|
||||
networkId: String,
|
||||
fromToken: Currency,
|
||||
toToken: Currency,
|
||||
amountToSwap: String,
|
||||
): SwapState {
|
||||
val amountDecimal = amountToSwap.toBigDecimalOrNull()
|
||||
if (amountDecimal == null || amountDecimal.compareTo(BigDecimal.ZERO) == 0) {
|
||||
return createEmptyAmountState(networkId, fromToken, toToken)
|
||||
}
|
||||
val amount = SwapAmount(amountDecimal, getTokenDecimals(fromToken))
|
||||
val fromTokenAddress = getTokenAddress(fromToken)
|
||||
val toTokenAddress = getTokenAddress(toToken)
|
||||
val isAllowedToSpend = checkAllowance(networkId, fromTokenAddress)
|
||||
val isNotZeroBalance = isNotZeroBalance(fromToken, networkId)
|
||||
cache.cacheExchangeCurrencies(fromToken, toToken)
|
||||
cache.cacheAmountToSwap(amount)
|
||||
return if (isAllowedToSpend && isNotZeroBalance) {
|
||||
if (allowPermissionsHandler.isAddressAllowanceInProgress(fromTokenAddress)) {
|
||||
allowPermissionsHandler.removeAddressFromProgress(toTokenAddress)
|
||||
}
|
||||
val fee = getAndUpdateFee(networkId, fromToken)
|
||||
val isBalanceEnough = isBalanceEnough(fromToken, networkId, amount, fee)
|
||||
val isFeeEnough = checkFeeIsEnough(fee, amount, networkId)
|
||||
if (isAllowedToSpend && allowPermissionsHandler.isAddressAllowanceInProgress(fromTokenAddress)) {
|
||||
allowPermissionsHandler.removeAddressFromProgress(fromTokenAddress)
|
||||
transactionManager.updateWalletManager(networkId)
|
||||
}
|
||||
val preparedSwapConfigState = PreparedSwapConfigState(
|
||||
isAllowedToSpend = isAllowedToSpend,
|
||||
isBalanceEnough = isBalanceEnough,
|
||||
isFeeEnough = isFeeEnough,
|
||||
)
|
||||
return if (isAllowedToSpend && isBalanceEnough && isFeeEnough) {
|
||||
loadSwapData(
|
||||
networkId = networkId,
|
||||
fromTokenAddress = fromTokenAddress,
|
||||
|
|
@ -167,6 +170,7 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
fromToken = fromToken,
|
||||
toToken = toToken,
|
||||
amount = amount,
|
||||
preparedSwapConfigState = preparedSwapConfigState,
|
||||
)
|
||||
} else {
|
||||
loadQuoteData(
|
||||
|
|
@ -176,16 +180,19 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
amount = amount,
|
||||
fromToken = fromToken,
|
||||
toToken = toToken,
|
||||
isAllowedToSpend = isAllowedToSpend,
|
||||
preparedSwapConfigState = preparedSwapConfigState,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun onSwap(): SwapState {
|
||||
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()) { "" }
|
||||
override suspend fun onSwap(
|
||||
networkId: String,
|
||||
swapData: SwapDataModel,
|
||||
currencyToSend: Currency,
|
||||
currencyToGet: Currency,
|
||||
amountToSwap: String,
|
||||
): TxState {
|
||||
val amount = requireNotNull(amountToSwap.toBigDecimalOrNull()) { "wrong amount format, use only digits" }
|
||||
val estimatedGas =
|
||||
increaseByPercents(TWENTY_FIVE_PERCENTS, swapData.transaction.gas.toIntOrNull() ?: DEFAULT_GAS)
|
||||
val fee = transactionManager.calculateFee(
|
||||
|
|
@ -195,22 +202,27 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
)
|
||||
val result = transactionManager.sendTransaction(
|
||||
networkId = networkId,
|
||||
amountToSend = amountToSwap.value,
|
||||
amountToSend = amount,
|
||||
currencyToSend = cryptoCurrencyConverter.convert(currencyToSend),
|
||||
feeAmount = fee,
|
||||
estimatedGas = estimatedGas,
|
||||
destinationAddress = swapData.transaction.toWalletAddress,
|
||||
dataToSign = swapData.transaction.data,
|
||||
isSwap = true,
|
||||
)
|
||||
when (result) {
|
||||
return when (result) {
|
||||
SendTxResult.Success -> {
|
||||
userWalletManager.addToken(cryptoCurrencyConverter.convert(currencyToGet))
|
||||
TxState.TxSent(
|
||||
fromAmount = amountFormatter.formatSwapAmountToUI(swapData.fromTokenAmount, currencyToSend.symbol),
|
||||
toAmount = amountFormatter.formatSwapAmountToUI(swapData.toTokenAmount, currencyToSend.symbol),
|
||||
)
|
||||
}
|
||||
SendTxResult.UserCancelledError -> TODO()
|
||||
is SendTxResult.BlockchainSdkError -> TODO()
|
||||
is SendTxResult.TangemSdkError -> TODO()
|
||||
is SendTxResult.UnknownError -> TODO()
|
||||
SendTxResult.UserCancelledError -> TxState.UserCancelled
|
||||
is SendTxResult.BlockchainSdkError -> TxState.BlockchainError
|
||||
is SendTxResult.TangemSdkError -> TxState.TangemSdkError
|
||||
is SendTxResult.UnknownError -> TxState.UnknownError
|
||||
}
|
||||
return SwapState.SwapError(DataError.UNKNOWN_ERROR)
|
||||
}
|
||||
|
||||
override fun getTokenDecimals(token: Currency): Int {
|
||||
|
|
@ -252,7 +264,7 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
TokenWithBalance(
|
||||
token = it,
|
||||
tokenBalanceData = TokenBalanceData(
|
||||
amount = balance?.let { b -> b.value.toFormattedString(b.decimals) },
|
||||
amount = balance?.let { amount -> amountFormatter.formatProxyAmountToUI(amount, "") },
|
||||
amountEquivalent = balance?.value?.toFiatString(
|
||||
rates[it.id]?.toBigDecimal() ?: BigDecimal.ZERO,
|
||||
appCurrency.symbol,
|
||||
|
|
@ -262,13 +274,50 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun getAndUpdateFee(networkId: String, fromToken: Currency): BigDecimal? {
|
||||
val lastFee = cache.getLastFeeForNetwork(networkId)
|
||||
if (lastFee == null) {
|
||||
if (userWalletManager.getNativeTokenBalance(networkId)?.value?.compareTo(BigDecimal.ZERO) == 0) {
|
||||
return null
|
||||
}
|
||||
val transactionData = repository.dataToApprove(networkId, getTokenAddress(fromToken))
|
||||
val fee = transactionManager.getFee(
|
||||
networkId,
|
||||
BigDecimal.ZERO,
|
||||
cryptoCurrencyConverter.convert(fromToken),
|
||||
transactionData.toAddress,
|
||||
).value
|
||||
cache.cacheLastFeeForNetwork(fee, networkId)
|
||||
return fee
|
||||
}
|
||||
return lastFee
|
||||
}
|
||||
|
||||
private suspend fun checkAllowance(networkId: String, fromTokenAddress: String): Boolean {
|
||||
val allowance = repository.checkTokensSpendAllowance(
|
||||
networkId = networkId,
|
||||
tokenAddress = fromTokenAddress,
|
||||
walletAddress = userWalletManager.getWalletAddress(networkId),
|
||||
)
|
||||
return allowance.error == DataError.NO_ERROR && allowance.dataModel != ZERO_BALANCE
|
||||
return allowance.error == DataError.NoError && allowance.dataModel != ZERO_BALANCE
|
||||
}
|
||||
|
||||
private fun createEmptyAmountState(
|
||||
networkId: String,
|
||||
fromToken: Currency,
|
||||
toToken: Currency,
|
||||
): SwapState {
|
||||
val tokensBalance = userWalletManager.getCurrentWalletTokensBalance(networkId)
|
||||
val fromTokenBalance = tokensBalance[fromToken.symbol]?.let {
|
||||
SwapAmount(it.value, it.decimals)
|
||||
}
|
||||
val toTokenBalance = tokensBalance[toToken.symbol]?.let {
|
||||
SwapAmount(it.value, it.decimals)
|
||||
}
|
||||
return SwapState.EmptyAmountState(
|
||||
fromTokenWalletBalance = fromTokenBalance?.let { amountFormatter.formatSwapAmountToUI(it, "") } ?: "",
|
||||
toTokenWalletBalance = toTokenBalance?.let { amountFormatter.formatSwapAmountToUI(it, "") } ?: "",
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -282,7 +331,7 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
amount: SwapAmount,
|
||||
fromToken: Currency,
|
||||
toToken: Currency,
|
||||
isAllowedToSpend: Boolean,
|
||||
preparedSwapConfigState: PreparedSwapConfigState,
|
||||
): SwapState {
|
||||
repository.findBestQuote(
|
||||
networkId = networkId,
|
||||
|
|
@ -292,9 +341,7 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
).let { quotes ->
|
||||
val quoteDataModel = quotes.dataModel
|
||||
if (quoteDataModel != null) {
|
||||
cache.cacheQuoteData(quoteModel = quoteDataModel)
|
||||
val transactionData = repository.dataToApprove(networkId, getTokenAddress(fromToken))
|
||||
cache.cacheApproveTransactionData(transactionData)
|
||||
val swapState = updateBalances(
|
||||
networkId = networkId,
|
||||
fromToken = fromToken,
|
||||
|
|
@ -306,7 +353,8 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
estimatedGas = quoteDataModel.estimatedGas,
|
||||
gasPrice = transactionData.gasPrice,
|
||||
),
|
||||
isAllowedToSpend = isAllowedToSpend,
|
||||
preparedSwapConfigState = preparedSwapConfigState,
|
||||
swapDataModel = null,
|
||||
)
|
||||
return updatePermissionState(
|
||||
networkId = networkId,
|
||||
|
|
@ -332,6 +380,7 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
fromToken: Currency,
|
||||
toToken: Currency,
|
||||
amount: SwapAmount,
|
||||
preparedSwapConfigState: PreparedSwapConfigState,
|
||||
): SwapState {
|
||||
repository.prepareSwapTransaction(
|
||||
networkId = networkId,
|
||||
|
|
@ -343,7 +392,6 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
).let {
|
||||
val swapData = it.dataModel
|
||||
if (swapData != null) {
|
||||
cache.cacheSwapData(swapData)
|
||||
val swapState = updateBalances(
|
||||
networkId = networkId,
|
||||
fromToken = fromToken,
|
||||
|
|
@ -355,7 +403,8 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
estimatedGas = swapData.transaction.gas.toIntOrNull() ?: DEFAULT_GAS,
|
||||
gasPrice = swapData.transaction.gasPrice,
|
||||
),
|
||||
isAllowedToSpend = true,
|
||||
preparedSwapConfigState = preparedSwapConfigState,
|
||||
swapDataModel = swapData,
|
||||
)
|
||||
return swapState.copy(
|
||||
permissionState = PermissionDataState.Empty,
|
||||
|
|
@ -374,34 +423,45 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
fromTokenAmount: SwapAmount,
|
||||
toTokenAmount: SwapAmount,
|
||||
fee: BigDecimal,
|
||||
isAllowedToSpend: Boolean,
|
||||
preparedSwapConfigState: PreparedSwapConfigState,
|
||||
swapDataModel: SwapDataModel?,
|
||||
): SwapState.QuotesLoadedState {
|
||||
val appCurrency = userWalletManager.getUserAppCurrency()
|
||||
val rates = repository.getRates(appCurrency.code, listOf(fromToken.id, toToken.id))
|
||||
val tokensBalance = userWalletManager.getCurrentWalletTokensBalance(networkId)
|
||||
val fromTokenBalance = tokensBalance[fromToken.symbol]?.let {
|
||||
it.value.toFormattedString(it.decimals)
|
||||
amountFormatter.formatProxyAmountToUI(it, "")
|
||||
}
|
||||
val toTokenBalance = tokensBalance[toToken.symbol]?.let {
|
||||
it.value.toFormattedString(it.decimals)
|
||||
amountFormatter.formatProxyAmountToUI(it, "")
|
||||
}
|
||||
return SwapState.QuotesLoadedState(
|
||||
fromTokenAmount = fromTokenAmount,
|
||||
toTokenAmount = toTokenAmount,
|
||||
fromTokenAddress = getTokenAddress(fromToken),
|
||||
toTokenAddress = getTokenAddress(toToken),
|
||||
fee = "${fee.toPlainString()} ${userWalletManager.getCurrencyByNetworkId(networkId)}",
|
||||
isAllowedToSpend = isAllowedToSpend,
|
||||
fromTokenWalletBalance = fromTokenBalance ?: ZERO_BALANCE,
|
||||
fromTokenFiatBalance = fromTokenAmount.value.toFiatString(
|
||||
rates[fromToken.id]?.toBigDecimal() ?: BigDecimal.ZERO,
|
||||
appCurrency.symbol,
|
||||
fromTokenInfo = TokenSwapInfo(
|
||||
tokenAmount = fromTokenAmount,
|
||||
tokenId = fromToken.id,
|
||||
tokenWalletBalance = fromTokenBalance ?: ZERO_BALANCE,
|
||||
tokenFiatBalance = fromTokenAmount.value.toFiatString(
|
||||
rates[fromToken.id]?.toBigDecimal() ?: BigDecimal.ZERO,
|
||||
appCurrency.symbol,
|
||||
),
|
||||
),
|
||||
toTokenWalletBalance = toTokenBalance ?: ZERO_BALANCE,
|
||||
toTokenFiatBalance = fromTokenAmount.value.toFiatString(
|
||||
rates[fromToken.id]?.toBigDecimal() ?: BigDecimal.ZERO,
|
||||
appCurrency.symbol,
|
||||
toTokenInfo = TokenSwapInfo(
|
||||
tokenAmount = toTokenAmount,
|
||||
tokenId = toToken.id,
|
||||
tokenWalletBalance = toTokenBalance ?: ZERO_BALANCE,
|
||||
tokenFiatBalance = toTokenAmount.value.toFiatString(
|
||||
rates[toToken.id]?.toBigDecimal() ?: BigDecimal.ZERO,
|
||||
appCurrency.symbol,
|
||||
),
|
||||
),
|
||||
fee = amountFormatter.formatBigDecimalAmountToUI(
|
||||
amount = fee,
|
||||
decimals = transactionManager.getNativeTokenDecimals(networkId),
|
||||
currency = userWalletManager.getCurrencyByNetworkId(networkId),
|
||||
),
|
||||
networkCurrency = userWalletManager.getNetworkCurrency(networkId),
|
||||
preparedSwapConfigState = preparedSwapConfigState,
|
||||
swapDataModel = swapDataModel,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -412,7 +472,7 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
estimatedGas: Int,
|
||||
transactionData: ApproveModel,
|
||||
): SwapState.QuotesLoadedState {
|
||||
if (allowPermissionsHandler.isAddressAllowanceInProgress(fromToken.networkId)) {
|
||||
if (allowPermissionsHandler.isAddressAllowanceInProgress(getTokenAddress(fromToken))) {
|
||||
return quotesLoadedState.copy(
|
||||
permissionState = PermissionDataState.PermissionLoading,
|
||||
)
|
||||
|
|
@ -420,7 +480,7 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
return quotesLoadedState.copy(
|
||||
permissionState = PermissionDataState.PermissionReadyForRequest(
|
||||
currency = userWalletManager.getCurrencyByNetworkId(networkId),
|
||||
amount = "infinite", // FIXME
|
||||
amount = "∞",
|
||||
walletAddress = getWalletAddress(networkId),
|
||||
spenderAddress = transactionData.toAddress,
|
||||
fee = transactionManager.calculateFee(
|
||||
|
|
@ -428,14 +488,18 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
gasPrice = transactionData.gasPrice,
|
||||
estimatedGas = estimatedGas,
|
||||
).toPlainString(),
|
||||
requestApproveData = RequestApproveStateData(
|
||||
estimatedGas = estimatedGas,
|
||||
approveModel = transactionData,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun isNotZeroBalance(fromToken: Currency, networkId: String): Boolean {
|
||||
private fun isBalanceEnough(fromToken: Currency, networkId: String, amount: SwapAmount, fee: BigDecimal?): Boolean {
|
||||
/** to compare [BigDecimal] use only comparator */
|
||||
return (userWalletManager.getCurrentWalletTokensBalance(networkId)[fromToken.symbol]?.value
|
||||
?: BigDecimal.ZERO).compareTo(BigDecimal.ZERO) != 0
|
||||
?: BigDecimal.ZERO) > amount.value.plus(fee ?: BigDecimal.ZERO)
|
||||
}
|
||||
|
||||
private fun getWalletAddress(networkId: String): String {
|
||||
|
|
@ -445,7 +509,7 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
private fun getTokenAddress(currency: Currency): String {
|
||||
return when (currency) {
|
||||
is Currency.NativeToken -> {
|
||||
DEFAULT_BLOCKCHAIN_ADDRESS
|
||||
DEFAULT_BLOCKCHAIN_INCH_ADDRESS
|
||||
}
|
||||
is Currency.NonNativeToken -> {
|
||||
currency.contractAddress
|
||||
|
|
@ -453,6 +517,20 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun checkFeeIsEnough(fee: BigDecimal?, spendAmount: SwapAmount, networkId: String): Boolean {
|
||||
if (fee == null) {
|
||||
return false
|
||||
}
|
||||
userWalletManager.getNativeTokenBalance(networkId)?.let { balance ->
|
||||
return (balance.value.minus(spendAmount.value) > fee.multiply(
|
||||
BigDecimal.valueOf(
|
||||
INCREASE_FEE_TO_CHECK_ENOUGH_PERCENT,
|
||||
),
|
||||
))
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
private fun increaseByPercents(percents: Int, value: Int): Int {
|
||||
return value * (percents / 100 + 1)
|
||||
|
|
@ -462,7 +540,8 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
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 DEFAULT_BLOCKCHAIN_INCH_ADDRESS = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"
|
||||
private const val TWENTY_FIVE_PERCENTS = 25
|
||||
private const val INCREASE_FEE_TO_CHECK_ENOUGH_PERCENT = 1.5
|
||||
}
|
||||
}
|
||||
|
|
@ -1,30 +1,16 @@
|
|||
package com.tangem.feature.swap.domain.cache
|
||||
|
||||
import com.tangem.feature.swap.domain.models.SwapAmount
|
||||
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
|
||||
import java.math.BigDecimal
|
||||
|
||||
interface SwapDataCache {
|
||||
|
||||
fun cacheNetworkId(networkId: String)
|
||||
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 cacheLastFeeForNetwork(fee: BigDecimal, networkId: String)
|
||||
fun getAvailableTokens(networkId: String): List<Currency>
|
||||
fun getApproveTransactionData(): ApproveModel?
|
||||
fun getLastQuote(): QuoteModel?
|
||||
fun getLastSwapData(): SwapDataModel?
|
||||
fun getNetworkId(): String?
|
||||
fun getAmountToSwap(): SwapAmount?
|
||||
fun getExchangeCurrencies(): ExchangeCurrencies?
|
||||
fun getInWalletTokens(): List<Currency>
|
||||
fun getLoadedTokens(): List<Currency>
|
||||
fun getLastFeeForNetwork(networkId: String): BigDecimal?
|
||||
}
|
||||
|
|
@ -1,34 +1,17 @@
|
|||
package com.tangem.feature.swap.domain.cache
|
||||
|
||||
import com.tangem.feature.swap.domain.models.SwapAmount
|
||||
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
|
||||
import java.math.BigDecimal
|
||||
|
||||
class SwapDataCacheImpl : SwapDataCache {
|
||||
|
||||
private var lastDataForSwap: SwapDataHolder = SwapDataHolder()
|
||||
private val availableTokensForNetwork: MutableMap<String, List<Currency>> = mutableMapOf()
|
||||
private val feesForNetworks: MutableMap<String, BigDecimal> = mutableMapOf()
|
||||
private val lastInWalletTokens = mutableListOf<Currency>()
|
||||
private val lastLoadedTokens = mutableListOf<Currency>()
|
||||
|
||||
override fun cacheQuoteData(
|
||||
quoteModel: QuoteModel,
|
||||
) {
|
||||
lastDataForSwap = lastDataForSwap.copy(quoteModel = quoteModel)
|
||||
}
|
||||
|
||||
override fun cacheExchangeCurrencies(fromToken: Currency, toToken: Currency) {
|
||||
lastDataForSwap =
|
||||
lastDataForSwap.copy(
|
||||
exchangeCurrencies = ExchangeCurrencies(
|
||||
fromCurrency = fromToken,
|
||||
toCurrency = toToken,
|
||||
),
|
||||
)
|
||||
override fun cacheLastFeeForNetwork(fee: BigDecimal, networkId: String) {
|
||||
feesForNetworks[networkId] = fee
|
||||
}
|
||||
|
||||
override fun cacheInWalletTokens(tokens: List<Currency>) {
|
||||
|
|
@ -49,51 +32,15 @@ class SwapDataCacheImpl : SwapDataCache {
|
|||
return lastLoadedTokens
|
||||
}
|
||||
|
||||
override fun cacheSwapData(swapDataModel: SwapDataModel) {
|
||||
lastDataForSwap = lastDataForSwap.copy(swapModel = swapDataModel)
|
||||
}
|
||||
|
||||
override fun getLastSwapData(): SwapDataModel? {
|
||||
return lastDataForSwap.swapModel
|
||||
}
|
||||
|
||||
override fun cacheApproveTransactionData(approve: ApproveModel) {
|
||||
lastDataForSwap = lastDataForSwap.copy(approveTxModel = approve)
|
||||
}
|
||||
|
||||
override fun getExchangeCurrencies(): ExchangeCurrencies? {
|
||||
return lastDataForSwap.exchangeCurrencies
|
||||
}
|
||||
|
||||
override fun cacheAvailableToSwapTokens(networkId: String, tokens: List<Currency>) {
|
||||
availableTokensForNetwork[networkId] = tokens
|
||||
}
|
||||
|
||||
override fun cacheNetworkId(networkId: String) {
|
||||
lastDataForSwap = lastDataForSwap.copy(networkId = networkId)
|
||||
}
|
||||
|
||||
override fun cacheAmountToSwap(amount: SwapAmount) {
|
||||
lastDataForSwap = lastDataForSwap.copy(amountToSwap = amount)
|
||||
}
|
||||
|
||||
override fun getNetworkId(): String? {
|
||||
return lastDataForSwap.networkId
|
||||
override fun getLastFeeForNetwork(networkId: String): BigDecimal? {
|
||||
return feesForNetworks[networkId]
|
||||
}
|
||||
|
||||
override fun getAvailableTokens(networkId: String): List<Currency> {
|
||||
return availableTokensForNetwork.getOrElse(networkId) { emptyList() }
|
||||
}
|
||||
|
||||
override fun getApproveTransactionData(): ApproveModel? {
|
||||
return lastDataForSwap.approveTxModel
|
||||
}
|
||||
|
||||
override fun getLastQuote(): QuoteModel? {
|
||||
return lastDataForSwap.quoteModel
|
||||
}
|
||||
|
||||
override fun getAmountToSwap(): SwapAmount? {
|
||||
return lastDataForSwap.amountToSwap
|
||||
}
|
||||
}
|
||||
|
|
@ -1,21 +1,18 @@
|
|||
package com.tangem.feature.swap.domain.models
|
||||
|
||||
enum class DataError {
|
||||
NO_ERROR,
|
||||
UNKNOWN_ERROR,
|
||||
INSUFFICIENT_LIQUIDITY,
|
||||
CANNOT_SYNC
|
||||
sealed class DataError {
|
||||
object NoError : DataError()
|
||||
data class UnknownError(val message: String) : DataError()
|
||||
object InsufficientLiquidity : DataError()
|
||||
}
|
||||
|
||||
fun mapErrors(error: String?): DataError {
|
||||
return if (error == null) {
|
||||
DataError.UNKNOWN_ERROR
|
||||
} else when {
|
||||
error == INSUFFICIENT_LIQUIDITY_ERROR -> DataError.INSUFFICIENT_LIQUIDITY
|
||||
error.startsWith(CANNOT_SYNC_ERROR) -> DataError.CANNOT_SYNC
|
||||
else -> DataError.UNKNOWN_ERROR
|
||||
DataError.NoError
|
||||
} else when (error) {
|
||||
INSUFFICIENT_LIQUIDITY_ERROR -> DataError.InsufficientLiquidity
|
||||
else -> DataError.UnknownError(error)
|
||||
}
|
||||
}
|
||||
|
||||
private const val INSUFFICIENT_LIQUIDITY_ERROR = "insufficient liquidity"
|
||||
private const val CANNOT_SYNC_ERROR = "cannot sync"
|
||||
private const val INSUFFICIENT_LIQUIDITY_ERROR = "insufficient liquidity"
|
||||
|
|
@ -11,5 +11,5 @@ import com.tangem.feature.swap.domain.models.DataError
|
|||
*/
|
||||
data class AggregatedSwapDataModel<T>(
|
||||
val dataModel: T?,
|
||||
val error: DataError = DataError.NO_ERROR,
|
||||
val error: DataError = DataError.NoError,
|
||||
)
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package com.tangem.feature.swap.domain.models.domain
|
||||
|
||||
data class PreparedSwapConfigState(
|
||||
val isAllowedToSpend: Boolean,
|
||||
val isBalanceEnough: Boolean,
|
||||
val isFeeEnough: Boolean,
|
||||
)
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
package com.tangem.feature.swap.domain.models.ui
|
||||
|
||||
import com.tangem.feature.swap.domain.models.SwapAmount
|
||||
import com.tangem.lib.crypto.models.ProxyAmount
|
||||
import com.tangem.utils.toFormattedCurrencyString
|
||||
import java.math.BigDecimal
|
||||
|
||||
class AmountFormatter {
|
||||
|
||||
/**
|
||||
* Use to convert crypto amount [SwapAmount] to UI representation
|
||||
*
|
||||
* @param swapAmount [SwapAmount]
|
||||
* @param currency currency symbol
|
||||
* @return formatted [String]
|
||||
*/
|
||||
fun formatSwapAmountToUI(swapAmount: SwapAmount, currency: String): String {
|
||||
return swapAmount.value.toFormattedCurrencyString(swapAmount.decimals, currency)
|
||||
}
|
||||
|
||||
/**
|
||||
* Use to convert crypto amount [ProxyAmount] to UI representation
|
||||
*
|
||||
* @param proxyAmount [ProxyAmount]
|
||||
* @param currency currency symbol
|
||||
* @return formatted [String]
|
||||
*/
|
||||
fun formatProxyAmountToUI(proxyAmount: ProxyAmount, currency: String): String {
|
||||
return proxyAmount.value.toFormattedCurrencyString(proxyAmount.decimals, currency)
|
||||
}
|
||||
|
||||
/**
|
||||
* Use to convert ONLY crypto amount [BigDecimal] to UI representation
|
||||
*
|
||||
* @param amount
|
||||
* @param decimals
|
||||
* @param currency
|
||||
* @return formatted [String]
|
||||
*/
|
||||
fun formatBigDecimalAmountToUI(amount: BigDecimal, decimals: Int, currency: String): String {
|
||||
return amount.toFormattedCurrencyString(decimals, currency)
|
||||
}
|
||||
}
|
||||
|
|
@ -2,26 +2,28 @@ package com.tangem.feature.swap.domain.models.ui
|
|||
|
||||
import com.tangem.feature.swap.domain.models.DataError
|
||||
import com.tangem.feature.swap.domain.models.SwapAmount
|
||||
import com.tangem.feature.swap.domain.models.domain.ApproveModel
|
||||
import com.tangem.feature.swap.domain.models.domain.PreparedSwapConfigState
|
||||
import com.tangem.feature.swap.domain.models.domain.SwapDataModel
|
||||
|
||||
sealed interface SwapState {
|
||||
|
||||
data class QuotesLoadedState(
|
||||
val fromTokenAmount: SwapAmount,
|
||||
val toTokenAmount: SwapAmount,
|
||||
val fromTokenAddress: String,
|
||||
val toTokenAddress: String,
|
||||
val fromTokenWalletBalance: String,
|
||||
val fromTokenFiatBalance: String,
|
||||
val toTokenWalletBalance: String,
|
||||
val toTokenFiatBalance: String,
|
||||
val fromTokenInfo: TokenSwapInfo,
|
||||
val toTokenInfo: TokenSwapInfo,
|
||||
val fee: String,
|
||||
val isAllowedToSpend: Boolean = false,
|
||||
val networkCurrency: String,
|
||||
val preparedSwapConfigState: PreparedSwapConfigState,
|
||||
val permissionState: PermissionDataState = PermissionDataState.Empty,
|
||||
val swapDataModel: SwapDataModel? = null,
|
||||
) : SwapState
|
||||
|
||||
data class SwapError(
|
||||
val errorType: DataError,
|
||||
data class EmptyAmountState(
|
||||
val fromTokenWalletBalance: String,
|
||||
val toTokenWalletBalance: String,
|
||||
) : SwapState
|
||||
|
||||
data class SwapError(val error: DataError) : SwapState
|
||||
}
|
||||
|
||||
sealed class PermissionDataState {
|
||||
|
|
@ -32,6 +34,7 @@ sealed class PermissionDataState {
|
|||
val walletAddress: String,
|
||||
val spenderAddress: String,
|
||||
val fee: String,
|
||||
val requestApproveData: RequestApproveStateData,
|
||||
) : PermissionDataState()
|
||||
|
||||
object PermissionFailed : PermissionDataState()
|
||||
|
|
@ -39,4 +42,16 @@ sealed class PermissionDataState {
|
|||
object PermissionLoading : PermissionDataState()
|
||||
|
||||
object Empty : PermissionDataState()
|
||||
}
|
||||
}
|
||||
|
||||
data class TokenSwapInfo(
|
||||
val tokenAmount: SwapAmount,
|
||||
val tokenId: String,
|
||||
val tokenWalletBalance: String,
|
||||
val tokenFiatBalance: String,
|
||||
)
|
||||
|
||||
data class RequestApproveStateData(
|
||||
val estimatedGas: Int,
|
||||
val approveModel: ApproveModel,
|
||||
)
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.feature.swap.domain.models.ui
|
||||
|
||||
sealed class TxState {
|
||||
|
||||
data class TxSent(val fromAmount: String? = null, val toAmount: String? = null) : TxState()
|
||||
object UserCancelled : TxState()
|
||||
object BlockchainError : TxState()
|
||||
object TangemSdkError : TxState()
|
||||
object UnknownError : TxState()
|
||||
}
|
||||
|
|
@ -30,6 +30,7 @@ data class SwapCardData(
|
|||
val amount: String?,
|
||||
val amountEquivalent: String?,
|
||||
val tokenIconUrl: String,
|
||||
val tokenId: String,
|
||||
val tokenCurrency: String,
|
||||
val balance: String,
|
||||
@DrawableRes val networkIconRes: Int? = null,
|
||||
|
|
@ -68,7 +69,7 @@ sealed interface TransactionCardType {
|
|||
|
||||
sealed interface SwapWarning {
|
||||
data class PermissionNeeded(val tokenCurrency: String) : SwapWarning
|
||||
data class InsufficientFunds(val tokenCurrency: String) : SwapWarning
|
||||
object InsufficientFunds : SwapWarning
|
||||
data class GenericWarning(val message: String?, val onClick: () -> Unit) : SwapWarning
|
||||
// data class RateExpired(val onClick: () -> Unit) : SwapWarning
|
||||
// object HighPriceImpact : SwapWarning
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
package com.tangem.feature.swap.ui
|
||||
|
||||
import com.tangem.feature.swap.converters.TokensDataConverter
|
||||
import com.tangem.feature.swap.domain.models.DataError
|
||||
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.domain.models.ui.TxState
|
||||
import com.tangem.feature.swap.models.ApprovePermissionButton
|
||||
import com.tangem.feature.swap.models.CancelPermissionButton
|
||||
import com.tangem.feature.swap.models.FeeState
|
||||
|
|
@ -13,6 +15,7 @@ import com.tangem.feature.swap.models.SwapButton
|
|||
import com.tangem.feature.swap.models.SwapCardData
|
||||
import com.tangem.feature.swap.models.SwapPermissionState
|
||||
import com.tangem.feature.swap.models.SwapStateHolder
|
||||
import com.tangem.feature.swap.models.SwapSuccessStateHolder
|
||||
import com.tangem.feature.swap.models.SwapWarning
|
||||
import com.tangem.feature.swap.models.TransactionCardType
|
||||
import com.tangem.feature.swap.models.UiActions
|
||||
|
|
@ -24,14 +27,15 @@ class StateBuilder(val actions: UiActions) {
|
|||
|
||||
private val tokensDataConverter = TokensDataConverter(actions.onSearchEntered, actions.onTokenSelected)
|
||||
|
||||
fun createInitialLoadingState(networkCurrency: String): SwapStateHolder {
|
||||
fun createInitialLoadingState(initialCurrency: Currency): SwapStateHolder {
|
||||
return SwapStateHolder(
|
||||
sendCardData = SwapCardData(
|
||||
type = TransactionCardType.SendCard(actions.onAmountChanged),
|
||||
amount = null,
|
||||
amountEquivalent = null,
|
||||
tokenIconUrl = "",
|
||||
tokenCurrency = "",
|
||||
tokenIconUrl = initialCurrency.logoUrl,
|
||||
tokenCurrency = initialCurrency.symbol,
|
||||
tokenId = initialCurrency.id,
|
||||
canSelectAnotherToken = false,
|
||||
balance = "",
|
||||
),
|
||||
|
|
@ -43,9 +47,10 @@ class StateBuilder(val actions: UiActions) {
|
|||
tokenCurrency = "",
|
||||
canSelectAnotherToken = false,
|
||||
balance = "",
|
||||
tokenId = "",
|
||||
),
|
||||
fee = FeeState.Loading,
|
||||
networkCurrency = networkCurrency,
|
||||
networkCurrency = initialCurrency.symbol,
|
||||
swapButton = SwapButton(enabled = false, loading = true, onClick = {}),
|
||||
onRefresh = {},
|
||||
onBackClicked = actions.onBackClicked,
|
||||
|
|
@ -66,6 +71,7 @@ class StateBuilder(val actions: UiActions) {
|
|||
amountEquivalent = uiStateHolder.sendCardData.amountEquivalent,
|
||||
tokenIconUrl = fromToken.logoUrl,
|
||||
tokenCurrency = fromToken.symbol,
|
||||
tokenId = fromToken.id,
|
||||
canSelectAnotherToken = mainTokenId != fromToken.id,
|
||||
balance = "",
|
||||
),
|
||||
|
|
@ -75,6 +81,7 @@ class StateBuilder(val actions: UiActions) {
|
|||
amountEquivalent = null,
|
||||
tokenIconUrl = toToken.logoUrl,
|
||||
tokenCurrency = toToken.symbol,
|
||||
tokenId = toToken.id,
|
||||
canSelectAnotherToken = mainTokenId != toToken.id,
|
||||
balance = "",
|
||||
),
|
||||
|
|
@ -89,40 +96,87 @@ class StateBuilder(val actions: UiActions) {
|
|||
quoteModel: SwapState.QuotesLoadedState,
|
||||
fromToken: Currency,
|
||||
): SwapStateHolder {
|
||||
val warnings = mutableListOf<SwapWarning>()
|
||||
if (!quoteModel.preparedSwapConfigState.isAllowedToSpend && quoteModel.preparedSwapConfigState.isFeeEnough) {
|
||||
warnings.add(SwapWarning.PermissionNeeded(fromToken.symbol))
|
||||
}
|
||||
if (!quoteModel.preparedSwapConfigState.isBalanceEnough) {
|
||||
warnings.add(SwapWarning.InsufficientFunds)
|
||||
}
|
||||
val feeState = if (quoteModel.preparedSwapConfigState.isFeeEnough) {
|
||||
FeeState.Loaded(quoteModel.fee)
|
||||
} else {
|
||||
FeeState.NotEnoughFundsWarning(quoteModel.fee)
|
||||
}
|
||||
return uiStateHolder.copy(
|
||||
sendCardData = SwapCardData(
|
||||
type = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.SendCard),
|
||||
amount = quoteModel.fromTokenAmount.formatToUIRepresentation(),
|
||||
amountEquivalent = quoteModel.fromTokenFiatBalance,
|
||||
amount = quoteModel.fromTokenInfo.tokenAmount.formatToUIRepresentation(),
|
||||
amountEquivalent = quoteModel.fromTokenInfo.tokenFiatBalance,
|
||||
tokenIconUrl = uiStateHolder.sendCardData.tokenIconUrl,
|
||||
tokenId = quoteModel.fromTokenInfo.tokenId,
|
||||
tokenCurrency = uiStateHolder.sendCardData.tokenCurrency,
|
||||
canSelectAnotherToken = uiStateHolder.sendCardData.canSelectAnotherToken,
|
||||
balance = quoteModel.fromTokenWalletBalance,
|
||||
balance = quoteModel.fromTokenInfo.tokenWalletBalance,
|
||||
),
|
||||
receiveCardData = SwapCardData(
|
||||
type = TransactionCardType.ReceiveCard(),
|
||||
amount = quoteModel.toTokenAmount.formatToUIRepresentation(),
|
||||
amountEquivalent = quoteModel.toTokenFiatBalance,
|
||||
amount = quoteModel.toTokenInfo.tokenAmount.formatToUIRepresentation(),
|
||||
amountEquivalent = quoteModel.toTokenInfo.tokenFiatBalance,
|
||||
tokenIconUrl = uiStateHolder.receiveCardData.tokenIconUrl,
|
||||
tokenId = quoteModel.toTokenInfo.tokenId,
|
||||
tokenCurrency = uiStateHolder.receiveCardData.tokenCurrency,
|
||||
canSelectAnotherToken = uiStateHolder.receiveCardData.canSelectAnotherToken,
|
||||
balance = quoteModel.toTokenWalletBalance,
|
||||
balance = quoteModel.toTokenInfo.tokenWalletBalance,
|
||||
),
|
||||
warnings = if (!quoteModel.isAllowedToSpend) {
|
||||
listOf(SwapWarning.PermissionNeeded(fromToken.symbol))
|
||||
} else {
|
||||
emptyList()
|
||||
},
|
||||
networkCurrency = quoteModel.networkCurrency,
|
||||
warnings = warnings,
|
||||
permissionState = convertPermissionState(quoteModel.permissionState, actions.onGivePermissionClick),
|
||||
fee = FeeState.Loaded(quoteModel.fee),
|
||||
fee = feeState,
|
||||
swapButton = SwapButton(
|
||||
enabled = quoteModel.isAllowedToSpend,
|
||||
enabled = quoteModel.preparedSwapConfigState.isAllowedToSpend
|
||||
&& quoteModel.preparedSwapConfigState.isBalanceEnough
|
||||
&& quoteModel.preparedSwapConfigState.isFeeEnough,
|
||||
loading = false,
|
||||
onClick = actions.onSwapClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun createQuotesEmptyAmountState(
|
||||
uiStateHolder: SwapStateHolder,
|
||||
emptyAmountState: SwapState.EmptyAmountState,
|
||||
): SwapStateHolder {
|
||||
return uiStateHolder.copy(
|
||||
sendCardData = SwapCardData(
|
||||
type = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.SendCard),
|
||||
amount = uiStateHolder.sendCardData.amount,
|
||||
amountEquivalent = uiStateHolder.sendCardData.amountEquivalent,
|
||||
tokenIconUrl = uiStateHolder.sendCardData.tokenIconUrl,
|
||||
tokenId = uiStateHolder.sendCardData.tokenId,
|
||||
tokenCurrency = uiStateHolder.sendCardData.tokenCurrency,
|
||||
canSelectAnotherToken = uiStateHolder.sendCardData.canSelectAnotherToken,
|
||||
balance = emptyAmountState.fromTokenWalletBalance,
|
||||
),
|
||||
receiveCardData = SwapCardData(
|
||||
type = TransactionCardType.ReceiveCard(),
|
||||
amount = "0",
|
||||
amountEquivalent = uiStateHolder.receiveCardData.amountEquivalent,
|
||||
tokenIconUrl = uiStateHolder.receiveCardData.tokenIconUrl,
|
||||
tokenId = uiStateHolder.receiveCardData.tokenId,
|
||||
tokenCurrency = uiStateHolder.receiveCardData.tokenCurrency,
|
||||
canSelectAnotherToken = uiStateHolder.receiveCardData.canSelectAnotherToken,
|
||||
balance = emptyAmountState.toTokenWalletBalance,
|
||||
),
|
||||
fee = FeeState.Empty,
|
||||
swapButton = SwapButton(
|
||||
enabled = false,
|
||||
loading = false,
|
||||
onClick = { },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun createSwapInProgressState(uiState: SwapStateHolder): SwapStateHolder {
|
||||
return uiState.copy(
|
||||
swapButton = uiState.swapButton.copy(
|
||||
|
|
@ -164,12 +218,78 @@ class StateBuilder(val actions: UiActions) {
|
|||
}
|
||||
}
|
||||
|
||||
fun updateSwapAmount(uiState: SwapStateHolder, amount: String, amountEquivalent: String): SwapStateHolder {
|
||||
fun updateSwapAmount(uiState: SwapStateHolder, amount: String): SwapStateHolder {
|
||||
return uiState.copy(
|
||||
sendCardData = uiState.sendCardData.copy(
|
||||
amount = amount,
|
||||
amountEquivalent = amountEquivalent,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun loadingPermissionState(uiState: SwapStateHolder): SwapStateHolder {
|
||||
return uiState.copy(
|
||||
permissionState = SwapPermissionState.InProgress,
|
||||
warnings = uiState.warnings.filterNot { it is SwapWarning.PermissionNeeded },
|
||||
)
|
||||
}
|
||||
|
||||
fun createSuccessState(uiState: SwapStateHolder, txState: TxState.TxSent): SwapStateHolder {
|
||||
return uiState.copy(
|
||||
successState = SwapSuccessStateHolder(
|
||||
fromTokenAmount = txState.fromAmount ?: "",
|
||||
toTokenAmount = txState.toAmount ?: "",
|
||||
onSecondaryButtonClick = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun createSwapErrorTransaction(uiState: SwapStateHolder, onAlertClick: () -> Unit): SwapStateHolder {
|
||||
return uiState.copy(
|
||||
swapButton = uiState.swapButton.copy(
|
||||
enabled = true,
|
||||
loading = false,
|
||||
),
|
||||
alert = SwapWarning.GenericWarning(
|
||||
message = null,
|
||||
onClick = onAlertClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun addWarning(uiState: SwapStateHolder, message: String?): SwapStateHolder {
|
||||
return if (message != null) {
|
||||
val renewWarnings = uiState.warnings.filterNot { it is SwapWarning.GenericWarning }.toMutableList()
|
||||
renewWarnings.add(SwapWarning.GenericWarning(message) {})
|
||||
uiState.copy(
|
||||
warnings = renewWarnings,
|
||||
)
|
||||
} else {
|
||||
uiState
|
||||
}
|
||||
}
|
||||
|
||||
fun mapError(uiState: SwapStateHolder, error: DataError): SwapStateHolder {
|
||||
return when (error) {
|
||||
//todo use if needed later
|
||||
// DataError.InsufficientLiquidity -> TODO()
|
||||
// DataError.NoError -> TODO()
|
||||
is DataError.UnknownError -> addWarning(uiState, error.message)
|
||||
else -> addWarning(uiState, null)
|
||||
}
|
||||
}
|
||||
|
||||
fun addAlert(uiState: SwapStateHolder, onClick: () -> Unit): SwapStateHolder {
|
||||
return uiState.copy(
|
||||
alert = SwapWarning.GenericWarning(
|
||||
message = null,
|
||||
onClick = onClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun clearAlert(uiState: SwapStateHolder): SwapStateHolder {
|
||||
return uiState.copy(
|
||||
alert = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -38,6 +38,7 @@ import com.tangem.core.ui.components.SmallInfoCard
|
|||
import com.tangem.core.ui.components.SmallInfoCardWithWarning
|
||||
import com.tangem.core.ui.components.SpacerH16
|
||||
import com.tangem.core.ui.components.keyboardAsState
|
||||
import com.tangem.core.ui.extensions.getActiveIconRes
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.feature.swap.models.FeeState
|
||||
import com.tangem.feature.swap.models.SwapButton
|
||||
|
|
@ -161,6 +162,7 @@ private fun MainInfo(state: SwapStateHolder) {
|
|||
tokenIconUrl = state.sendCardData.tokenIconUrl,
|
||||
tokenCurrency = state.sendCardData.tokenCurrency,
|
||||
networkIconRes = state.sendCardData.networkIconRes,
|
||||
iconPlaceholder = getActiveIconRes(state.sendCardData.tokenId),
|
||||
onChangeTokenClick = if (state.sendCardData.canSelectAnotherToken) state.onSelectTokenClick else null,
|
||||
)
|
||||
SpacerH16()
|
||||
|
|
@ -172,6 +174,7 @@ private fun MainInfo(state: SwapStateHolder) {
|
|||
tokenIconUrl = state.receiveCardData.tokenIconUrl,
|
||||
tokenCurrency = state.receiveCardData.tokenCurrency,
|
||||
networkIconRes = state.receiveCardData.networkIconRes,
|
||||
iconPlaceholder = getActiveIconRes(state.receiveCardData.tokenId),
|
||||
onChangeTokenClick = if (state.receiveCardData.canSelectAnotherToken) {
|
||||
state.onSelectTokenClick
|
||||
} else {
|
||||
|
|
@ -228,7 +231,7 @@ private fun FeeItem(feeState: FeeState, currency: String) {
|
|||
SmallInfoCardWithWarning(
|
||||
startText = titleString,
|
||||
endText = feeState.fee,
|
||||
warningText = stringResource(id = R.string.token_details_send_blocked_fee_format, currency),
|
||||
warningText = stringResource(id = R.string.token_details_send_blocked_fee_format, currency, currency),
|
||||
)
|
||||
}
|
||||
is FeeState.Empty -> {}
|
||||
|
|
@ -298,6 +301,7 @@ private val sendCard = SwapCardData(
|
|||
networkIconRes = R.drawable.img_polygon_22,
|
||||
canSelectAnotherToken = false,
|
||||
balance = "123",
|
||||
tokenId = "",
|
||||
)
|
||||
|
||||
private val receiveCard = SwapCardData(
|
||||
|
|
@ -309,6 +313,7 @@ private val receiveCard = SwapCardData(
|
|||
networkIconRes = R.drawable.img_polygon_22,
|
||||
canSelectAnotherToken = true,
|
||||
balance = "33333",
|
||||
tokenId = "",
|
||||
)
|
||||
|
||||
private val state = SwapStateHolder(
|
||||
|
|
|
|||
|
|
@ -74,6 +74,7 @@ fun TransactionCard(
|
|||
amountEquivalent: String?,
|
||||
tokenIconUrl: String,
|
||||
tokenCurrency: String,
|
||||
@DrawableRes iconPlaceholder: Int? = null,
|
||||
@DrawableRes networkIconRes: Int? = null,
|
||||
onChangeTokenClick: (() -> Unit)? = null,
|
||||
) {
|
||||
|
|
@ -104,6 +105,7 @@ fun TransactionCard(
|
|||
tokenIconUrl = tokenIconUrl,
|
||||
tokenCurrency = tokenCurrency,
|
||||
networkIconRes = networkIconRes,
|
||||
iconPlaceholder = iconPlaceholder,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -328,6 +330,7 @@ private fun AutoSizeTextField(amount: String, onAmountChange: (String) -> Unit)
|
|||
fun Token(
|
||||
tokenIconUrl: String,
|
||||
tokenCurrency: String,
|
||||
@DrawableRes iconPlaceholder: Int? = null,
|
||||
@DrawableRes networkIconRes: Int? = null,
|
||||
) {
|
||||
Column(
|
||||
|
|
@ -353,6 +356,8 @@ fun Token(
|
|||
model = ImageRequest.Builder(LocalContext.current)
|
||||
.data(tokenIconUrl)
|
||||
.crossfade(true)
|
||||
.error(iconPlaceholder ?: 0)
|
||||
.placeholder(iconPlaceholder ?: 0)
|
||||
.build(),
|
||||
loading = { TokenImageShimmer(modifier = tokenImageModifier) },
|
||||
// error = { CurrencyPlaceholderIcon(modifier = tokenImageModifier, tokenCurrency) },
|
||||
|
|
|
|||
|
|
@ -5,9 +5,11 @@ import kotlinx.coroutines.delay
|
|||
import kotlinx.coroutines.launch
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
class PeriodicTask(
|
||||
class PeriodicTask<T>(
|
||||
private val delay: Long,
|
||||
private val task: suspend () -> Unit,
|
||||
private val task: suspend () -> Result<T>,
|
||||
private val onSuccess: (T) -> Unit,
|
||||
private val onError: (Throwable) -> Unit,
|
||||
) {
|
||||
|
||||
private var isActive: AtomicBoolean = AtomicBoolean(false)
|
||||
|
|
@ -16,6 +18,18 @@ class PeriodicTask(
|
|||
isActive.set(true)
|
||||
while (isActive.get()) {
|
||||
task.invoke()
|
||||
.onSuccess {
|
||||
if (!isActive.get()) {
|
||||
return@onSuccess
|
||||
}
|
||||
onSuccess.invoke(it)
|
||||
}
|
||||
.onFailure {
|
||||
if (!isActive.get()) {
|
||||
return@onFailure
|
||||
}
|
||||
onError.invoke(it)
|
||||
}
|
||||
delay(delay)
|
||||
}
|
||||
}
|
||||
|
|
@ -25,11 +39,11 @@ class PeriodicTask(
|
|||
}
|
||||
}
|
||||
|
||||
class SingleTaskScheduler {
|
||||
class SingleTaskScheduler<T> {
|
||||
|
||||
private var lastTask: PeriodicTask? = null
|
||||
private var lastTask: PeriodicTask<T>? = null
|
||||
|
||||
fun scheduleTask(scope: CoroutineScope, task: PeriodicTask) {
|
||||
fun scheduleTask(scope: CoroutineScope, task: PeriodicTask<T>) {
|
||||
lastTask?.cancel()
|
||||
lastTask = task
|
||||
scope.launch {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.feature.swap.viewmodels
|
||||
|
||||
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.SwapDataModel
|
||||
|
||||
data class SwapProcessDataState(
|
||||
val networkId: String,
|
||||
val fromCurrency: Currency? = null,
|
||||
val toCurrency: Currency? = null,
|
||||
val amount: String? = null,
|
||||
val estimatedGas: Int? = null,
|
||||
val approveModel: ApproveModel? = null,
|
||||
val swapModel: SwapDataModel? = null,
|
||||
)
|
||||
|
|
@ -8,10 +8,12 @@ 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.createFromAmountWithoutOffset
|
||||
import com.tangem.feature.swap.domain.models.domain.Currency
|
||||
import com.tangem.feature.swap.domain.models.domain.SwapDataModel
|
||||
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.domain.models.ui.TxState
|
||||
import com.tangem.feature.swap.models.SwapStateHolder
|
||||
import com.tangem.feature.swap.models.UiActions
|
||||
import com.tangem.feature.swap.presentation.SwapFragment
|
||||
|
|
@ -35,18 +37,19 @@ internal class SwapViewModel @Inject constructor(
|
|||
savedStateHandle: SavedStateHandle,
|
||||
) : ViewModel() {
|
||||
|
||||
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))
|
||||
private val stateBuilder = StateBuilder(
|
||||
actions = createUiActions(),
|
||||
)
|
||||
private val amountDebouncer = Debouncer()
|
||||
private val singleTaskScheduler = SingleTaskScheduler<SwapState>()
|
||||
|
||||
private var dataState by mutableStateOf(SwapProcessDataState(networkId = currency.networkId))
|
||||
var uiState: SwapStateHolder by mutableStateOf(stateBuilder.createInitialLoadingState(currency))
|
||||
private set
|
||||
|
||||
// shows currency order (direct - swap initial to selected, reversed = selected to initial)
|
||||
|
|
@ -60,6 +63,11 @@ internal class SwapViewModel @Inject constructor(
|
|||
initTokens(currency)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
singleTaskScheduler.cancelTask()
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
fun setRouter(router: SwapRouter) {
|
||||
swapRouter = router
|
||||
uiState = uiState.copy(
|
||||
|
|
@ -75,6 +83,10 @@ internal class SwapViewModel @Inject constructor(
|
|||
swapInteractor.initTokensToSwap(currency)
|
||||
}
|
||||
.onSuccess { state ->
|
||||
dataState = dataState.copy(
|
||||
fromCurrency = state.preselectTokens.fromToken,
|
||||
toCurrency = state.preselectTokens.toToken,
|
||||
)
|
||||
updateTokensState(dataState = state.foundTokensState)
|
||||
startLoadingQuotes(
|
||||
fromToken = state.preselectTokens.fromToken,
|
||||
|
|
@ -97,71 +109,101 @@ internal class SwapViewModel @Inject constructor(
|
|||
uiState = stateBuilder.createQuotesLoadingState(uiState, fromToken, toToken, currency.id)
|
||||
singleTaskScheduler.scheduleTask(
|
||||
viewModelScope,
|
||||
PeriodicTask(
|
||||
delay = UPDATE_DELAY,
|
||||
) {
|
||||
loadQuotesInternal(
|
||||
fromToken = fromToken,
|
||||
toToken = toToken,
|
||||
amount = amount,
|
||||
)
|
||||
loadQuotesTask(
|
||||
fromToken = fromToken,
|
||||
toToken = toToken,
|
||||
amount = amount,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun loadQuotesTask(
|
||||
fromToken: Currency,
|
||||
toToken: Currency,
|
||||
amount: String,
|
||||
): PeriodicTask<SwapState> {
|
||||
return PeriodicTask(
|
||||
UPDATE_DELAY,
|
||||
task = {
|
||||
runCatching(dispatchers.io) {
|
||||
dataState = dataState.copy(
|
||||
amount = amount,
|
||||
swapModel = null,
|
||||
estimatedGas = null,
|
||||
approveModel = null,
|
||||
)
|
||||
swapInteractor.findBestQuote(
|
||||
networkId = dataState.networkId,
|
||||
fromToken = fromToken,
|
||||
toToken = toToken,
|
||||
amountToSwap = amount,
|
||||
)
|
||||
}
|
||||
},
|
||||
onSuccess = { swapState ->
|
||||
when (swapState) {
|
||||
is SwapState.QuotesLoadedState -> {
|
||||
fillDataState(swapState.permissionState, swapState.swapDataModel)
|
||||
uiState = stateBuilder.createQuotesLoadedState(
|
||||
uiStateHolder = uiState,
|
||||
quoteModel = swapState,
|
||||
fromToken = fromToken,
|
||||
)
|
||||
}
|
||||
is SwapState.EmptyAmountState -> {
|
||||
uiState = stateBuilder.createQuotesEmptyAmountState(
|
||||
uiStateHolder = uiState,
|
||||
emptyAmountState = swapState,
|
||||
)
|
||||
}
|
||||
is SwapState.SwapError -> {
|
||||
uiState = stateBuilder.mapError(uiState, swapState.error)
|
||||
}
|
||||
}
|
||||
},
|
||||
onError = {
|
||||
uiState = stateBuilder.addWarning(uiState, it.message)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun loadQuotesInternal(fromToken: Currency, toToken: Currency, amount: String) {
|
||||
viewModelScope.launch(dispatchers.main) {
|
||||
runCatching(dispatchers.io) {
|
||||
swapInteractor.findBestQuote(
|
||||
fromToken = fromToken,
|
||||
toToken = toToken,
|
||||
amount = createFromAmountWithoutOffset(
|
||||
amountWithoutOffset = amount,
|
||||
decimals = swapInteractor.getTokenDecimals(fromToken),
|
||||
),
|
||||
)
|
||||
}
|
||||
.onSuccess { swapState ->
|
||||
when (swapState) {
|
||||
is SwapState.QuotesLoadedState -> {
|
||||
uiState = stateBuilder.createQuotesLoadedState(
|
||||
uiStateHolder = uiState,
|
||||
quoteModel = swapState,
|
||||
fromToken = fromToken,
|
||||
)
|
||||
}
|
||||
is SwapState.SwapError -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
.onFailure { }
|
||||
private fun fillDataState(permissionState: PermissionDataState, swapDataModel: SwapDataModel?) {
|
||||
dataState = if (permissionState is PermissionDataState.PermissionReadyForRequest) {
|
||||
dataState.copy(
|
||||
estimatedGas = permissionState.requestApproveData.estimatedGas,
|
||||
approveModel = permissionState.requestApproveData.approveModel,
|
||||
)
|
||||
} else {
|
||||
dataState.copy(
|
||||
swapModel = swapDataModel,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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(dispatchers.io) {
|
||||
swapInteractor.onSwap()
|
||||
swapInteractor.onSwap(
|
||||
networkId = dataState.networkId,
|
||||
swapData = dataState.swapModel!!,
|
||||
currencyToSend = dataState.fromCurrency!!,
|
||||
currencyToGet = dataState.toCurrency!!,
|
||||
amountToSwap = dataState.amount!!,
|
||||
)
|
||||
}
|
||||
.onSuccess {
|
||||
when (it) {
|
||||
is SwapState.SwapError -> {
|
||||
is TxState.TxSent -> {
|
||||
uiState = stateBuilder.createSuccessState(uiState, it)
|
||||
swapRouter.openScreen(SwapScreen.Success)
|
||||
}
|
||||
else -> {
|
||||
uiState = stateBuilder.createSwapErrorTransaction(uiState) {
|
||||
uiState = stateBuilder.clearAlert(uiState)
|
||||
}
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
.onFailure { }
|
||||
|
|
@ -171,17 +213,38 @@ internal class SwapViewModel @Inject constructor(
|
|||
private fun givePermissionsToSwap() {
|
||||
viewModelScope.launch(dispatchers.main) {
|
||||
runCatching(dispatchers.io) {
|
||||
swapInteractor.givePermissionToSwap()
|
||||
swapInteractor.givePermissionToSwap(
|
||||
networkId = dataState.networkId,
|
||||
estimatedGas = dataState.estimatedGas!!,
|
||||
transactionData = dataState.approveModel!!,
|
||||
forTokenContractAddress = (dataState.fromCurrency as? Currency.NonNativeToken)?.contractAddress
|
||||
?: "",
|
||||
)
|
||||
}
|
||||
.onSuccess { }
|
||||
.onFailure { }
|
||||
.onSuccess {
|
||||
when (it) {
|
||||
is TxState.TxSent -> {
|
||||
uiState = stateBuilder.loadingPermissionState(uiState)
|
||||
}
|
||||
else -> {
|
||||
uiState = stateBuilder.addAlert(uiState) {
|
||||
uiState = stateBuilder.clearAlert(uiState)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.onFailure {
|
||||
uiState = stateBuilder.addAlert(uiState) {
|
||||
uiState = stateBuilder.clearAlert(uiState)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onSearchEntered(searchQuery: String) {
|
||||
viewModelScope.launch(dispatchers.main) {
|
||||
runCatching(dispatchers.io) {
|
||||
swapInteractor.onSearchToken(searchQuery)
|
||||
swapInteractor.onSearchToken(dataState.networkId, searchQuery)
|
||||
}
|
||||
.onSuccess {
|
||||
updateTokensState(it)
|
||||
|
|
@ -202,42 +265,55 @@ internal class SwapViewModel @Inject constructor(
|
|||
fromToken = currency
|
||||
toToken = foundToken
|
||||
}
|
||||
swapRouter.openScreen(SwapScreen.Main)
|
||||
dataState = dataState.copy(
|
||||
fromCurrency = fromToken,
|
||||
toCurrency = toToken,
|
||||
)
|
||||
startLoadingQuotes(fromToken, toToken, lastAmount.value)
|
||||
swapRouter.openScreen(SwapScreen.Main)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onChangeCardsClicked() {
|
||||
val currencies = swapInteractor.getExchangeCurrencies()
|
||||
val newFromToken = currencies?.toCurrency
|
||||
val newToToken = currencies?.fromCurrency
|
||||
val newFromToken = dataState.toCurrency
|
||||
val newToToken = dataState.fromCurrency
|
||||
if (newFromToken != null && newToToken != null) {
|
||||
dataState = dataState.copy(
|
||||
fromCurrency = newFromToken,
|
||||
toCurrency = newToToken,
|
||||
)
|
||||
isOrderReversed = !isOrderReversed
|
||||
startLoadingQuotes(newFromToken, newToToken, lastAmount.value)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onAmountChanged(value: String) {
|
||||
uiState = stateBuilder.updateSwapAmount(uiState, value, value)
|
||||
uiState = stateBuilder.updateSwapAmount(uiState, value)
|
||||
lastAmount.value = value
|
||||
amountDebouncer.debounce(DEBOUNCE_AMOUNT_DELAY, viewModelScope) {
|
||||
val currencies = swapInteractor.getExchangeCurrencies()
|
||||
val fromToken = currencies?.fromCurrency
|
||||
val toToken = currencies?.toCurrency
|
||||
val fromToken = dataState.fromCurrency
|
||||
val toToken = dataState.toCurrency
|
||||
if (fromToken != null && toToken != null) {
|
||||
startLoadingQuotes(fromToken, toToken, lastAmount.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
singleTaskScheduler.cancelTask()
|
||||
super.onCleared()
|
||||
private fun createUiActions(): UiActions {
|
||||
return UiActions(
|
||||
onSearchEntered = { onSearchEntered(it) },
|
||||
onTokenSelected = { onTokenSelect(it) },
|
||||
onAmountChanged = { onAmountChanged(it) },
|
||||
onSwapClick = { onSwapClick() },
|
||||
onGivePermissionClick = { givePermissionsToSwap() },
|
||||
onChangeCardsClicked = { onChangeCardsClicked() },
|
||||
onBackClicked = { onSearchEntered("") },
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val INITIAL_AMOUNT = "1"
|
||||
private const val UPDATE_DELAY = 10000L
|
||||
private const val INITIAL_AMOUNT = ""
|
||||
private const val UPDATE_DELAY = 60000L
|
||||
private const val DEBOUNCE_AMOUNT_DELAY = 1000L
|
||||
}
|
||||
}
|
||||
|
|
@ -7,16 +7,26 @@ import java.math.BigDecimal
|
|||
|
||||
interface TransactionManager {
|
||||
|
||||
@Throws(IllegalStateException::class)
|
||||
suspend fun sendApproveTransaction(
|
||||
networkId: String,
|
||||
feeAmount: BigDecimal,
|
||||
estimatedGas: Int,
|
||||
destinationAddress: String,
|
||||
dataToSign: String,
|
||||
): SendTxResult
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Throws(IllegalStateException::class)
|
||||
suspend fun sendTransaction(
|
||||
networkId: String,
|
||||
amountToSend: BigDecimal,
|
||||
currencyToSend: Currency,
|
||||
feeAmount: BigDecimal,
|
||||
estimatedGas: Int,
|
||||
destinationAddress: String,
|
||||
dataToSign: String,
|
||||
isSwap: Boolean,
|
||||
currencyToSend: Currency,
|
||||
): SendTxResult
|
||||
|
||||
@Throws(IllegalStateException::class)
|
||||
|
|
@ -30,5 +40,8 @@ interface TransactionManager {
|
|||
@Throws(IllegalStateException::class)
|
||||
fun getNativeTokenDecimals(networkId: String): Int
|
||||
|
||||
@Throws(IllegalStateException::class)
|
||||
suspend fun updateWalletManager(networkId: String)
|
||||
|
||||
fun calculateFee(networkId: String, gasPrice: String, estimatedGas: Int): BigDecimal
|
||||
}
|
||||
|
|
@ -49,6 +49,10 @@ interface UserWalletManager {
|
|||
@Throws(IllegalStateException::class)
|
||||
fun getCurrentWalletTokensBalance(networkId: String): Map<String, ProxyAmount>
|
||||
|
||||
fun getNativeTokenBalance(networkId: String): ProxyAmount?
|
||||
|
||||
fun getNetworkCurrency(networkId: String): String
|
||||
|
||||
/**
|
||||
* Returns selected app currency
|
||||
*/
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue