Updated on 2026-08-14

This commit is contained in:
Tangem 2025-09-16 17:13:25 +03:00
parent 905905a64e
commit 74c8c030c5
5 changed files with 197 additions and 38 deletions

View file

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

View file

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

View file

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

View file

@ -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<SwapProvider, SwapState> {
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%

View file

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