diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeDataResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeDataResponse.kt index 90785de95d..b240920600 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeDataResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeDataResponse.kt @@ -56,7 +56,7 @@ data class TxDetails( val txData: String?, // transaction data if DEX, null if CEX @Json(name = "txValue") - val txValue: String, // amount (same as fromAmount for Coin, but for bridge equal to otherNativeFee) + val txValue: String?, // amount (same as fromAmount for Coin, but for bridge equal to otherNativeFee) @Json(name = "otherNativeFee") val otherNativeFee: String?, diff --git a/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapDataModel.kt b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapDataModel.kt index 098e6cd413..1394bebe86 100644 --- a/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapDataModel.kt +++ b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapDataModel.kt @@ -12,7 +12,7 @@ sealed class SwapDataTransactionModel { abstract val fromAmount: BigDecimal abstract val toAmount: BigDecimal - abstract val txValue: String + abstract val txValue: String? abstract val txId: String abstract val txTo: String abstract val txExtraId: String? @@ -23,7 +23,7 @@ sealed class SwapDataTransactionModel { data class DEX( override val fromAmount: BigDecimal, override val toAmount: BigDecimal, - override val txValue: String, + override val txValue: String?, override val txId: String, override val txTo: String, override val txExtraId: String?, @@ -36,7 +36,7 @@ sealed class SwapDataTransactionModel { data class CEX( override val fromAmount: BigDecimal, override val toAmount: BigDecimal, - override val txValue: String, + override val txValue: String?, override val txId: String, override val txTo: String, override val txExtraId: String?, diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/ExpressTransactionModel.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/ExpressTransactionModel.kt index 36ce700f4b..05d1d541e3 100644 --- a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/ExpressTransactionModel.kt +++ b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/ExpressTransactionModel.kt @@ -8,7 +8,7 @@ sealed class ExpressTransactionModel { abstract val fromAmount: SwapAmount abstract val toAmount: SwapAmount - abstract val txValue: String + abstract val txValue: String? abstract val txId: String abstract val txTo: String abstract val txExtraId: String? @@ -19,7 +19,7 @@ sealed class ExpressTransactionModel { data class DEX( override val fromAmount: SwapAmount, override val toAmount: SwapAmount, - override val txValue: String, + override val txValue: String?, override val txId: String, override val txTo: String, override val txExtraId: String?, @@ -32,7 +32,7 @@ sealed class ExpressTransactionModel { data class CEX( override val fromAmount: SwapAmount, override val toAmount: SwapAmount, - override val txValue: String, + override val txValue: String?, override val txId: String, override val txTo: String, override val txExtraId: String?, diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 3d4cccd181..05128a4991 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -52,6 +52,8 @@ import timber.log.Timber import java.math.BigDecimal import java.math.BigInteger import java.math.RoundingMode +import android.util.Base64 +import com.tangem.blockchainsdk.utils.toNetworkId @Suppress("LargeClass", "LongParameterList") internal class SwapInteractorImpl @AssistedInject constructor( @@ -280,16 +282,29 @@ internal class SwapInteractorImpl @AssistedInject constructor( val networkId = fromToken.currency.network.backendId when (provider.type) { ExchangeProviderType.DEX, ExchangeProviderType.DEX_BRIDGE -> { - manageDex( - networkId = networkId, - fromToken = fromToken, - toToken = toToken, - provider = provider, - selectedFee = selectedFee, - amount = amount, - isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough, - expressOperationType = ExpressOperationType.SWAP, - ) + if (isSolana(networkId)) { + manageDexSolana( + networkId = networkId, + fromToken = fromToken, + toToken = toToken, + provider = provider, + selectedFee = selectedFee, + amount = amount, + isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough, + expressOperationType = ExpressOperationType.SWAP, + ) + } else { + manageDex( + networkId = networkId, + fromToken = fromToken, + toToken = toToken, + provider = provider, + selectedFee = selectedFee, + amount = amount, + isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough, + expressOperationType = ExpressOperationType.SWAP, + ) + } } ExchangeProviderType.CEX -> { manageCex( @@ -372,6 +387,57 @@ internal class SwapInteractorImpl @AssistedInject constructor( } } + private suspend fun manageDexSolana( + networkId: String, + fromToken: CryptoCurrencyStatus, + toToken: CryptoCurrencyStatus, + provider: SwapProvider, + selectedFee: FeeType, + amount: SwapAmount, + isBalanceWithoutFeeEnough: Boolean, + expressOperationType: ExpressOperationType, + ): Pair { + val maybeQuotes = repository.findBestQuote( + userWallet = userWallet, + fromContractAddress = fromToken.currency.getContractAddress(), + fromNetwork = fromToken.currency.network.backendId, + toContractAddress = toToken.currency.getContractAddress(), + toNetwork = toToken.currency.network.backendId, + fromAmount = amount.toStringWithRightOffset(), + fromDecimals = amount.decimals, + toDecimals = toToken.currency.decimals, + providerId = provider.providerId, + rateType = RateType.FLOAT, + ) + + return if (isBalanceWithoutFeeEnough) { + provider to loadDexSwapData( + provider = provider, + networkId = networkId, + fromToken = fromToken, + toToken = toToken, + amount = amount, + selectedFee = selectedFee, + expressOperationType = expressOperationType, + ) + } else { + provider to getQuotesState( + provider = provider, + quoteDataModel = maybeQuotes, + amount = amount, + fromToken = fromToken, + toToken = toToken, + networkId = networkId, + isAllowedToSpend = true, + isBalanceWithoutFeeEnough = false, + txFee = TxFeeState.Empty, + transactionFee = null, + includeFeeInAmount = IncludeFeeInAmount.Excluded, // exclude for dex + selectedFee = selectedFee, + ) + } + } + private suspend fun manageCex( networkId: String, fromToken: CryptoCurrencyStatus, @@ -538,15 +604,27 @@ internal class SwapInteractorImpl @AssistedInject constructor( ) } ExchangeProviderType.DEX, ExchangeProviderType.DEX_BRIDGE -> { - onSwapDex( - provider = swapProvider, - networkId = currencyToSend.currency.network.backendId, - swapData = requireNotNull(swapData), - currencyToSendStatus = currencyToSend, - currencyToGetStatus = currencyToGet, - txFee = fee, - amountToSwap = amountToSwap, - ) + val networkId = currencyToSend.currency.network.backendId + if (isSolana(networkId)) { + onSwapSolanaDex( + provider = swapProvider, + networkId = currencyToSend.currency.network.backendId, + swapData = requireNotNull(swapData), + currencyToSendStatus = currencyToSend, + currencyToGetStatus = currencyToGet, + amountToSwap = amountToSwap, + ) + } else { + onSwapDex( + provider = swapProvider, + networkId = currencyToSend.currency.network.backendId, + swapData = requireNotNull(swapData), + currencyToSendStatus = currencyToSend, + currencyToGetStatus = currencyToGet, + txFee = fee, + amountToSwap = amountToSwap, + ) + } } } } @@ -602,11 +680,12 @@ internal class SwapInteractorImpl @AssistedInject constructor( txFee: TxFee, ): SwapTransactionState { val amountDecimal = requireNotNull(toBigDecimalOrNull(amountToSwap)) { "wrong amount format" } + val txValue = requireNotNull(swapData.transaction.txValue) { "txValue is null" } val amount = SwapAmount(amountDecimal, currencyToSendStatus.currency.decimals) val derivationPath = currencyToSendStatus.currency.network.derivationPath.value val dexTransaction = swapData.transaction as ExpressTransactionModel.DEX val dataToSign = dexTransaction.txData - val amountToSend = createNativeAmountForDex(swapData.transaction.txValue, currencyToSendStatus.currency.network) + val amountToSend = createNativeAmountForDex(txValue, currencyToSendStatus.currency.network) val txData = createTransactionUseCase( amount = amountToSend, fee = txFee.fee, @@ -619,7 +698,59 @@ internal class SwapInteractorImpl @AssistedInject constructor( Timber.e(it, "Failed to create swap dex tx data") return SwapTransactionState.Error.UnknownError } + return handleSwapResult( + provider = provider, + networkId = networkId, + swapData = swapData, + currencyToSendStatus = currencyToSendStatus, + currencyToGetStatus = currencyToGetStatus, + amount = amount, + derivationPath = derivationPath, + txData = txData, + payInAddress = txData.destinationAddress, + ) + } + private suspend fun onSwapSolanaDex( + provider: SwapProvider, + networkId: String, + swapData: SwapDataModel, + currencyToSendStatus: CryptoCurrencyStatus, + currencyToGetStatus: CryptoCurrencyStatus, + amountToSwap: String, + ): SwapTransactionState { + val dexTransaction = swapData.transaction as? ExpressTransactionModel.DEX + val amountDecimal = requireNotNull(toBigDecimalOrNull(amountToSwap)) { "wrong amount format" } + val txDataBase64 = requireNotNull(dexTransaction?.txData) { "txData is null" } + val amount = SwapAmount(amountDecimal, currencyToSendStatus.currency.decimals) + val derivationPath = currencyToSendStatus.currency.network.derivationPath.value + val compiledTransaction = TransactionData.Compiled( + value = TransactionData.Compiled.Data.Bytes(Base64.decode(txDataBase64, Base64.NO_WRAP)), + ) + return handleSwapResult( + provider = provider, + networkId = networkId, + swapData = swapData, + currencyToSendStatus = currencyToSendStatus, + currencyToGetStatus = currencyToGetStatus, + amount = amount, + derivationPath = derivationPath, + txData = compiledTransaction, + payInAddress = swapData.transaction.txTo, + ) + } + + private suspend fun handleSwapResult( + provider: SwapProvider, + networkId: String, + swapData: SwapDataModel, + currencyToSendStatus: CryptoCurrencyStatus, + currencyToGetStatus: CryptoCurrencyStatus, + amount: SwapAmount, + derivationPath: String?, + txData: TransactionData, + payInAddress: String, + ): SwapTransactionState { val result = sendTransactionUseCase( txData = txData, userWallet = userWallet, @@ -632,7 +763,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( txId = swapData.transaction.txId, fromNetwork = currencyToSendStatus.currency.network.backendId, fromAddress = currencyToSendStatus.value.networkAddress?.defaultAddress?.value.orEmpty(), - payInAddress = txData.destinationAddress, + payInAddress = payInAddress, txHash = txHash, payInExtraId = swapData.transaction.txExtraId, ) @@ -1194,13 +1325,21 @@ internal class SwapInteractorImpl @AssistedInject constructor( val otherNativeFee = transaction.otherNativeFeeWei ?.movePointLeft(nativeCoinDecimals) ?: BigDecimal.ZERO - val txFeeState = getFeeDataForDexSwap( - network = fromToken.currency.network, - transaction = transaction, - fromToken = fromToken.currency, - ) - .patchTransactionFeeForSwap(INCREASE_GAS_LIMIT_FOR_DEX) - .toTxFeeState(fromToken.currency, otherNativeFee) + val txFeeState = if (isSolana(networkId)) { + getFeeDataForSolanaDexSwap( + network = fromToken.currency.network, + transaction = transaction, + ) + .toTxFeeState(fromToken.currency, otherNativeFee) + } else { + getFeeDataForDexSwap( + network = fromToken.currency.network, + transaction = transaction, + fromToken = fromToken.currency, + ) + .patchTransactionFeeForSwap(INCREASE_GAS_LIMIT_FOR_DEX) + .toTxFeeState(fromToken.currency, otherNativeFee) + } val includeFeeInAmount = IncludeFeeInAmount.Excluded // exclude for dex val feeByPriority = selectFeeByType(feeType = selectedFee, txFeeState = txFeeState) @@ -1270,11 +1409,12 @@ internal class SwapInteractorImpl @AssistedInject constructor( fromToken: CryptoCurrency, ): TransactionFee { return try { + val txAmountValue = transaction.txValue ?: error("unable to get txValue") val nativeBalance = userWalletManager.getNativeTokenBalance( networkId = network.backendId, derivationPath = fromToken.network.derivationPath.value, ) ?: ProxyAmount.empty() - val amountToSend = createNativeAmountForDex(transaction.txValue, fromToken.network) + val amountToSend = createNativeAmountForDex(txAmountValue, fromToken.network) // transaction.txValue is always native coin if (nativeBalance.value < amountToSend.value) { error("It's impossible to calculate fee for nativeBalance.value < amountToSend.value") @@ -1306,6 +1446,22 @@ internal class SwapInteractorImpl @AssistedInject constructor( } } + private suspend fun getFeeDataForSolanaDexSwap( + network: Network, + transaction: ExpressTransactionModel.DEX, + ): TransactionFee { + val txData = transaction.txData + val transactionData = TransactionData.Compiled( + value = TransactionData.Compiled.Data.Bytes(Base64.decode(txData, Base64.NO_WRAP)), + ) + + return getFeeUseCase( + transactionData = transactionData, + network = network, + userWallet = userWallet, + ).getOrNull() ?: error("unable to calculate fee") + } + @Suppress("LongParameterList") private suspend fun updateBalances( provider: SwapProvider, @@ -1858,6 +2014,10 @@ internal class SwapInteractorImpl @AssistedInject constructor( .orEmpty() } + private fun isSolana(networkId: String): Boolean { + return networkId == Blockchain.Solana.toNetworkId() + } + companion object { private const val INCREASE_GAS_LIMIT_FOR_DEX = 112 // 12% private const val INCREASE_GAS_LIMIT_FOR_SEND = 105 // 5% diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index a256ae0146..009240b10c 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,8 +5,7 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "develop-1228" -#tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds +tangemBlockchainSdk = "develop-1229" tangemCardSdk = "develop-561" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem12"