Updated on 2026-08-14

This commit is contained in:
Tangem 2023-10-02 19:00:40 +03:00
commit c7e5f2d530
14 changed files with 174 additions and 145 deletions

View file

@ -155,7 +155,7 @@ class TradeCryptoMiddleware {
val walletManager = store.state.daggerGraphState
.get(DaggerGraphState::walletManagersFacade)
.getOrCreateWalletManager(
userWallet = action.userWallet,
userWalletId = action.userWallet.walletId,
blockchain = blockchain,
derivationPath = currency.network.derivationPath.value,
)
@ -338,7 +338,7 @@ class TradeCryptoMiddleware {
val walletManager = store.state.daggerGraphState
.get(DaggerGraphState::walletManagersFacade)
.getOrCreateWalletManager(
userWallet = action.userWallet,
userWalletId = action.userWallet.walletId,
blockchain = blockchain,
derivationPath = currency.network.derivationPath.value,
)
@ -379,7 +379,7 @@ class TradeCryptoMiddleware {
val walletManager = store.state.daggerGraphState
.get(DaggerGraphState::walletManagersFacade)
.getOrCreateWalletManager(
userWallet = action.userWallet,
userWalletId = action.userWallet.walletId,
blockchain = blockchain,
derivationPath = currency.network.derivationPath.value,
)

View file

@ -419,7 +419,7 @@ class TransactionManagerImpl(
appStateHolder.userWalletsListManager?.selectedUserWalletSync,
) { "userWallet or userWalletsListManager is null" }
walletManagersFacade.getOrCreateWalletManager(
selectedUserWallet,
selectedUserWallet.walletId,
blockchain,
derivationPath,
)

View file

@ -230,7 +230,7 @@ class UserWalletManagerImpl(
appStateHolder.userWalletsListManager?.selectedUserWalletSync,
) { "userWallet or userWalletsListManager is null" }
walletManagersFacade.getOrCreateWalletManager(
selectedUserWallet,
selectedUserWallet.walletId,
blockchain,
derivationPath,
)

View file

@ -61,11 +61,10 @@ class DefaultWalletManagersFacade(
network: Network,
addressType: AddressType,
): String {
val userWallet = getUserWallet(userWalletId)
val blockchain = Blockchain.fromId(network.id.value)
val walletManager = getOrCreateWalletManager(
userWallet = userWallet,
userWalletId = userWalletId,
blockchain = blockchain,
derivationPath = network.derivationPath.value,
)
@ -79,10 +78,9 @@ class DefaultWalletManagersFacade(
}
override suspend fun getTxHistoryState(userWalletId: UserWalletId, network: Network): TxHistoryState {
val userWallet = getUserWallet(userWalletId)
val blockchain = Blockchain.fromId(network.id.value)
val walletManager = getOrCreateWalletManager(
userWallet = userWallet,
userWalletId = userWalletId,
blockchain = blockchain,
derivationPath = network.derivationPath.value,
)
@ -102,10 +100,9 @@ class DefaultWalletManagersFacade(
page: Int,
pageSize: Int,
): PaginationWrapper<TxHistoryItem> {
val userWallet = getUserWallet(userWalletId)
val blockchain = Blockchain.fromId(currency.network.id.value)
val walletManager = getOrCreateWalletManager(
userWallet = userWallet,
userWalletId = userWalletId,
blockchain = blockchain,
derivationPath = currency.network.derivationPath.value,
)
@ -154,7 +151,11 @@ class DefaultWalletManagersFacade(
return UpdateWalletManagerResult.MissedDerivation
}
val walletManager = getOrCreateWalletManager(userWallet, blockchain, derivationPath)
val walletManager = getOrCreateWalletManager(
userWalletId = userWallet.walletId,
blockchain = blockchain,
derivationPath = derivationPath,
)
if (walletManager == null || blockchain == Blockchain.Unknown) {
Timber.w("Unable to get a wallet manager for blockchain: $blockchain")
return UpdateWalletManagerResult.Unreachable
@ -195,12 +196,11 @@ class DefaultWalletManagersFacade(
}
override suspend fun getOrCreateWalletManager(
userWallet: UserWallet,
userWalletId: UserWalletId,
blockchain: Blockchain,
derivationPath: String?,
): WalletManager? {
val userWalletId = userWallet.walletId
val userWallet = getUserWallet(userWalletId)
var walletManager = walletManagersStore.getSyncOrNull(
userWalletId = userWalletId,
blockchain = blockchain,
@ -221,11 +221,10 @@ class DefaultWalletManagersFacade(
}
override suspend fun getAddress(userWalletId: UserWalletId, network: Network): List<Address> {
val userWallet = getUserWallet(userWalletId)
val blockchain = Blockchain.fromId(network.id.value)
return getOrCreateWalletManager(
userWallet = userWallet,
userWalletId = userWalletId,
blockchain = blockchain,
derivationPath = network.derivationPath.value,
)
@ -236,10 +235,9 @@ class DefaultWalletManagersFacade(
}
override suspend fun getRentInfo(userWalletId: UserWalletId, network: Network): CryptoCurrencyWarning.Rent? {
val userWallet = getUserWallet(userWalletId)
val blockchain = Blockchain.fromId(network.id.value)
val manager = getOrCreateWalletManager(
userWallet = userWallet,
userWalletId = userWalletId,
blockchain = blockchain,
derivationPath = network.derivationPath.value,
)
@ -267,10 +265,9 @@ class DefaultWalletManagersFacade(
}
override suspend fun getExistentialDeposit(userWalletId: UserWalletId, network: Network): BigDecimal? {
val userWallet = getUserWallet(userWalletId)
val blockchain = Blockchain.fromId(network.id.value)
val manager = getOrCreateWalletManager(
userWallet = userWallet,
userWalletId = userWalletId,
blockchain = blockchain,
derivationPath = network.derivationPath.value,
)

View file

@ -12,7 +12,6 @@ import com.tangem.domain.txhistory.models.PaginationWrapper
import com.tangem.domain.txhistory.models.TxHistoryItem
import com.tangem.domain.txhistory.models.TxHistoryState
import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import java.math.BigDecimal
@ -72,7 +71,7 @@ interface WalletManagersFacade {
// TODO: Remove after refactoring
suspend fun getOrCreateWalletManager(
userWallet: UserWallet,
userWalletId: UserWalletId,
blockchain: Blockchain,
derivationPath: String?,
): WalletManager?

View file

@ -1,6 +1,10 @@
package com.tangem.feature.swap
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.Approver
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.extensions.Result
import com.tangem.data.tokens.utils.CryptoCurrencyFactory
import com.tangem.datasource.api.oneinch.OneInchApi
import com.tangem.datasource.api.oneinch.OneInchApiFactory
@ -12,20 +16,21 @@ import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.feature.swap.converters.ApproveConverter
import com.tangem.domain.wallets.models.UserWalletId
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.data.AggregatedSwapDataModel
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.mapErrors
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext
import java.math.BigDecimal
import javax.inject.Inject
import com.tangem.blockchain.common.Token as SdkToken
@ -35,12 +40,12 @@ internal class SwapRepositoryImpl @Inject constructor(
private val oneInchErrorsHandler: OneInchErrorsHandler,
private val coroutineDispatcher: CoroutineDispatcherProvider,
private val configManager: ConfigManager,
private val walletManagersFacade: WalletManagersFacade,
) : SwapRepository {
private val tokensConverter = TokensConverter()
private val quotesConverter = QuotesConverter()
private val swapConverter = SwapConverter()
private val approveConverter = ApproveConverter()
override suspend fun getRates(currencyId: String, tokenIds: List<String>): Map<String, Double> {
// workaround cause backend do not return arbitrum and optimism rates
@ -104,29 +109,6 @@ internal class SwapRepositoryImpl @Inject constructor(
}
}
override suspend fun dataToApprove(networkId: String, tokenAddress: String, amount: String?): ApproveModel {
return withContext(coroutineDispatcher.io) {
approveConverter.convert(getOneInchApi(networkId).approveTransaction(tokenAddress, amount))
}
}
override suspend fun checkTokensSpendAllowance(
networkId: String,
tokenAddress: String,
walletAddress: String,
): AggregatedSwapDataModel<String> {
return withContext(coroutineDispatcher.io) {
try {
val response = oneInchErrorsHandler.handleOneInchResponse(
getOneInchApi(networkId).approveAllowance(tokenAddress, walletAddress),
)
AggregatedSwapDataModel(response.allowance)
} catch (ex: OneIncResponseException) {
AggregatedSwapDataModel(null, mapErrors(ex.data.description))
}
}
}
override suspend fun prepareSwapTransaction(
networkId: String,
fromTokenAddress: String,
@ -191,6 +173,73 @@ internal class SwapRepositoryImpl @Inject constructor(
} as CryptoCurrency
}
override suspend fun getAllowance(
userWalletId: UserWalletId,
networkId: String,
derivationPath: String?,
tokenDecimalCount: Int,
tokenAddress: String,
): BigDecimal {
val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" }
val walletManager = walletManagersFacade.getOrCreateWalletManager(
userWalletId = userWalletId,
blockchain = blockchain,
derivationPath = derivationPath,
)
val spenderAddress = addressForTrust(networkId)
val result = (walletManager as? Approver)?.getAllowance(
spenderAddress,
Token(
symbol = blockchain.currency,
contractAddress = tokenAddress,
decimals = tokenDecimalCount,
),
) ?: error("Cannot cast to Approver")
return when (result) {
is Result.Success -> result.data
is Result.Failure -> error(result.error)
}
}
override suspend fun getApproveData(
userWalletId: UserWalletId,
networkId: String,
derivationPath: String?,
currency: Currency,
amount: BigDecimal?,
): String {
val blockchain =
requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" }
val walletManager = walletManagersFacade.getOrCreateWalletManager(
userWalletId = userWalletId,
blockchain = blockchain,
derivationPath = derivationPath,
)
val spenderAddress = addressForTrust(networkId)
return (walletManager as? Approver)?.getApproveData(
spenderAddress,
amount?.let { convertToAmount(it, currency, blockchain) },
) ?: error("Cannot cast to Approver")
}
private fun convertToAmount(amount: BigDecimal, currency: Currency, blockchain: Blockchain): Amount {
return when (currency) {
is Currency.NativeToken -> {
Amount(value = amount, blockchain = blockchain)
}
is Currency.NonNativeToken -> {
Amount(
currencySymbol = currency.symbol,
value = amount,
decimals = currency.decimalCount,
)
}
}
}
private fun getOneInchApi(networkId: String): OneInchApi {
return oneInchApiFactory.getApi(networkId)
}

View file

@ -1,16 +0,0 @@
package com.tangem.feature.swap.converters
import com.tangem.datasource.api.oneinch.models.ApproveCalldataResponse
import com.tangem.feature.swap.domain.models.domain.ApproveModel
import com.tangem.utils.converter.Converter
class ApproveConverter : Converter<ApproveCalldataResponse, ApproveModel> {
override fun convert(value: ApproveCalldataResponse): ApproveModel {
return ApproveModel(
data = value.data,
gasPrice = value.gasPrice,
toAddress = value.toAddress,
)
}
}

View file

@ -4,6 +4,7 @@ import com.tangem.datasource.api.oneinch.OneInchApiFactory
import com.tangem.datasource.api.oneinch.OneInchErrorsHandler
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.config.ConfigManager
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.feature.swap.SwapRepositoryImpl
import com.tangem.feature.swap.domain.SwapRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -25,6 +26,7 @@ class SwapDataModule {
oneInchErrorsHandler: OneInchErrorsHandler,
coroutineDispatcher: CoroutineDispatcherProvider,
configManager: ConfigManager,
walletManagerFacade: WalletManagersFacade,
): SwapRepository {
return SwapRepositoryImpl(
tangemTechApi = tangemTechApi,
@ -32,6 +34,7 @@ class SwapDataModule {
oneInchErrorsHandler = oneInchErrorsHandler,
coroutineDispatcher = coroutineDispatcher,
configManager = configManager,
walletManagersFacade = walletManagerFacade,
)
}
}

View file

@ -6,7 +6,6 @@ import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
import com.tangem.feature.swap.domain.cache.SwapDataCache
import com.tangem.feature.swap.domain.converters.SwapCurrencyConverter
import com.tangem.feature.swap.domain.models.DataError
import com.tangem.feature.swap.domain.models.SwapAmount
import com.tangem.feature.swap.domain.models.domain.*
import com.tangem.feature.swap.domain.models.domain.Currency
@ -118,16 +117,20 @@ internal class SwapInteractorImpl @Inject constructor(
override suspend fun givePermissionToSwap(networkId: String, permissionOptions: PermissionOptions): TxState {
val dataToSign = if (permissionOptions.approveType == SwapApproveType.UNLIMITED) {
repository.dataToApprove(networkId, getTokenAddress(permissionOptions.fromToken)).data
getApproveData(
networkId = networkId,
derivationPath = derivationPath,
fromToken = permissionOptions.fromToken,
)
} else {
permissionOptions.approveData.approveModel.data
permissionOptions.approveData.approveData
}
val result = transactionManager.sendApproveTransaction(
txData = ApproveTxData(
networkId = networkId,
feeAmount = permissionOptions.txFee.feeValue,
gasLimit = permissionOptions.txFee.gasLimit,
destinationAddress = permissionOptions.approveData.approveModel.toAddress,
destinationAddress = getTokenAddress(permissionOptions.fromToken),
dataToSign = dataToSign,
),
derivationPath = derivationPath,
@ -164,7 +167,7 @@ internal class SwapInteractorImpl @Inject constructor(
val amount = SwapAmount(amountDecimal, getTokenDecimals(fromToken))
val fromTokenAddress = getTokenAddress(fromToken)
val toTokenAddress = getTokenAddress(toToken)
val isAllowedToSpend = isAllowedToSpend(networkId, fromTokenAddress, amount)
val isAllowedToSpend = isAllowedToSpend(networkId, fromToken, amount)
if (isAllowedToSpend && allowPermissionsHandler.isAddressAllowanceInProgress(fromTokenAddress)) {
allowPermissionsHandler.removeAddressFromProgress(fromTokenAddress)
transactionManager.updateWalletManager(networkId, derivationPath)
@ -340,14 +343,23 @@ internal class SwapInteractorImpl @Inject constructor(
}
}
private suspend fun isAllowedToSpend(networkId: String, fromTokenAddress: String, amount: SwapAmount): Boolean {
val allowance = repository.checkTokensSpendAllowance(
networkId = networkId,
tokenAddress = fromTokenAddress,
walletAddress = userWalletManager.getWalletAddress(networkId, derivationPath),
private suspend fun isAllowedToSpend(networkId: String, fromToken: Currency, amount: SwapAmount): Boolean {
return getSelectedWalletUseCase().fold(
ifRight = { userWallet ->
val allowance = repository.getAllowance(
userWallet.walletId,
networkId,
derivationPath,
getTokenDecimals(fromToken),
getTokenAddress(fromToken),
)
allowance >= amount.value
},
ifLeft = {
Timber.e("Swap Error on isAllowedToSpend")
false
},
)
val allowanceAmount = allowance.dataModel?.toBigDecimalOrNull() ?: BigDecimal.ZERO
return allowance.error == DataError.NoError && allowanceAmount >= amount.value.movePointRight(amount.decimals)
}
private fun createEmptyAmountState(networkId: String, fromToken: Currency, toToken: Currency): SwapState {
@ -558,18 +570,19 @@ internal class SwapInteractorImpl @Inject constructor(
)
}
// setting up amount for approve with given amount for swap [SwapApproveType.Limited]
val transactionData = repository.dataToApprove(
val transactionData = getApproveData(
networkId = networkId,
tokenAddress = getTokenAddress(fromToken),
amount = swapAmount.toStringWithRightOffset(),
derivationPath = derivationPath,
fromToken = fromToken,
swapAmount = swapAmount,
)
val feeData = transactionManager.getFee(
networkId = networkId,
amountToSend = BigDecimal.ZERO,
currencyToSend = userWalletManager.getNativeTokenForNetwork(networkId),
destinationAddress = transactionData.toAddress,
destinationAddress = getTokenAddress(fromToken),
increaseBy = INCREASE_GAS_LIMIT_BY,
data = transactionData.data,
data = transactionData,
derivationPath = derivationPath,
)
val feeState = proxyFeesToFeeState(networkId, feeData)
@ -584,10 +597,10 @@ internal class SwapInteractorImpl @Inject constructor(
currency = fromToken.symbol,
amount = INFINITY_SYMBOL,
walletAddress = getWalletAddress(networkId),
spenderAddress = transactionData.toAddress,
spenderAddress = getTokenAddress(fromToken),
requestApproveData = RequestApproveStateData(
fee = feeState,
approveModel = transactionData,
approveData = transactionData,
),
),
preparedSwapConfigState = quotesLoadedState.preparedSwapConfigState.copy(
@ -718,6 +731,29 @@ internal class SwapInteractorImpl @Inject constructor(
return (BigDecimal.ONE - toTokenFiatValue.divide(fromTokenFiatValue, 2, RoundingMode.HALF_UP)).toFloat()
}
private suspend fun getApproveData(
networkId: String,
derivationPath: String?,
fromToken: Currency,
swapAmount: SwapAmount? = null,
): String {
return getSelectedWalletUseCase().fold(
ifRight = { userWallet ->
repository.getApproveData(
userWalletId = userWallet.walletId,
networkId = networkId,
derivationPath = derivationPath,
currency = fromToken,
amount = swapAmount?.value,
)
},
ifLeft = {
Timber.e("Swap Error on getApproveData")
error("Swap Error on getApproveData")
},
)
}
companion object {
private const val DEFAULT_SLIPPAGE = 2
private const val ZERO_BALANCE = "0"

View file

@ -3,11 +3,12 @@ package com.tangem.feature.swap.domain
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.swap.domain.models.data.AggregatedSwapDataModel
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 SwapRepository {
@ -29,28 +30,6 @@ interface SwapRepository {
*/
suspend fun addressForTrust(networkId: String): String
/**
* Generate "data" for calling contract in order to allow 1inch spend funds
*
* @param tokenAddress token you want to exchange
* @param amount number of tokens is allowed. By default infinite
*/
suspend fun dataToApprove(networkId: String, tokenAddress: String, amount: String? = null): ApproveModel
/**
* Get the number of tokens that the 1inch router is allowed to spend
*
* @param tokenAddress Token address you want to exchange
* @param walletAddress address for which you want to check
*
* @return amount of tokens allowed to spend
*/
suspend fun checkTokensSpendAllowance(
networkId: String,
tokenAddress: String,
walletAddress: String,
): AggregatedSwapDataModel<String>
@Suppress("LongParameterList")
suspend fun prepareSwapTransaction(
networkId: String,
@ -68,4 +47,22 @@ interface SwapRepository {
fun getTangemFee(): Double
suspend fun getCryptoCurrency(userWallet: UserWallet, currency: Currency, network: Network): CryptoCurrency?
@Throws(IllegalStateException::class)
suspend fun getAllowance(
userWalletId: UserWalletId,
networkId: String,
derivationPath: String?,
tokenDecimalCount: Int,
tokenAddress: String,
): BigDecimal
@Throws(IllegalStateException::class)
suspend fun getApproveData(
userWalletId: UserWalletId,
networkId: String,
derivationPath: String?,
currency: Currency,
amount: BigDecimal?,
): String
}

View file

@ -1,21 +0,0 @@
package com.tangem.feature.swap.domain.models.cache
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.domain.SwapDataModel
data class SwapDataHolder(
val quoteModel: QuoteModel? = null,
val swapModel: SwapDataModel? = null,
val approveTxModel: ApproveModel? = null,
val amountToSwap: SwapAmount? = null,
val networkId: String? = null,
val exchangeCurrencies: ExchangeCurrencies? = null,
)
data class ExchangeCurrencies(
val fromCurrency: Currency? = null,
val toCurrency: Currency? = null,
)

View file

@ -1,14 +0,0 @@
package com.tangem.feature.swap.domain.models.domain
/**
* Approve model
*
* @property data The encoded data to call the approve method on the swapped token contract
* @property gasPrice Gas price for fast transaction processing
* @property toAddress Token address that will be allowed to exchange through 1inch router
*/
data class ApproveModel(
val data: String,
val gasPrice: String,
val toAddress: String,
)

View file

@ -2,7 +2,6 @@ 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
import java.math.BigDecimal
@ -59,7 +58,7 @@ data class TokenSwapInfo(
data class RequestApproveStateData(
val fee: TxFeeState,
val approveModel: ApproveModel,
val approveData: String,
)
data class SwapStateData(

View file

@ -82,7 +82,7 @@ okHttp-prettyLogging = "3.1.0"
# endregion Other libraries
# region Tangem
tangemBlockchainSdk = "develop-351"
tangemBlockchainSdk = "develop-354"
#tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds
tangemCardSdk = "develop-300"
#tangemCardSdk = "0.0.1" # Keep it! - used for local builds