Updated on 2026-08-14
This commit is contained in:
parent
905905a64e
commit
74c8c030c5
5 changed files with 197 additions and 38 deletions
|
|
@ -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?,
|
||||
|
|
|
|||
|
|
@ -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?,
|
||||
|
|
|
|||
|
|
@ -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?,
|
||||
|
|
|
|||
|
|
@ -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%
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue