Updated on 2026-08-14
This commit is contained in:
parent
7f2e5650e2
commit
16c7de9054
27 changed files with 796 additions and 70 deletions
|
|
@ -78,6 +78,16 @@ internal object YieldSupplyDomainModule {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideWrapYieldSwapCallDataWithUpgradeUseCase(
|
||||||
|
yieldSupplyTransactionRepository: YieldSupplyTransactionRepository,
|
||||||
|
): WrapYieldSwapCallDataWithUpgradeUseCase {
|
||||||
|
return WrapYieldSwapCallDataWithUpgradeUseCase(
|
||||||
|
yieldSupplyTransactionRepository = yieldSupplyTransactionRepository,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideYieldSupplyGetProtocolBalanceUseCase(
|
fun provideYieldSupplyGetProtocolBalanceUseCase(
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,10 @@
|
||||||
"name": "WALLET_CONNECT_BITCOIN_ENABLED",
|
"name": "WALLET_CONNECT_BITCOIN_ENABLED",
|
||||||
"version": "undefined"
|
"version": "undefined"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "TWI_1326_YIELD_MODE_SWAP_ENABLED",
|
||||||
|
"version": "undefined"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "ADDRESS_SYNC_ENABLED",
|
"name": "ADDRESS_SYNC_ENABLED",
|
||||||
"version": "undefined"
|
"version": "undefined"
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,9 @@ dependencies {
|
||||||
exclude(module = "joda-time")
|
exclude(module = "joda-time")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Core */
|
||||||
|
implementation(projects.core.configToggles)
|
||||||
|
|
||||||
/** Libs */
|
/** Libs */
|
||||||
implementation(projects.libs.blockchainSdk)
|
implementation(projects.libs.blockchainSdk)
|
||||||
implementation(projects.libs.crypto)
|
implementation(projects.libs.crypto)
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ import arrow.core.toOption
|
||||||
import com.squareup.moshi.Moshi
|
import com.squareup.moshi.Moshi
|
||||||
import com.tangem.blockchain.common.Blockchain
|
import com.tangem.blockchain.common.Blockchain
|
||||||
import com.tangem.blockchainsdk.utils.toNetworkId
|
import com.tangem.blockchainsdk.utils.toNetworkId
|
||||||
|
import com.tangem.core.configtoggle.FeatureToggles
|
||||||
|
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||||
import com.tangem.data.common.api.safeApiCall
|
import com.tangem.data.common.api.safeApiCall
|
||||||
import com.tangem.data.swap.converter.SwapDataConverter
|
import com.tangem.data.swap.converter.SwapDataConverter
|
||||||
import com.tangem.data.swap.converter.SwapStatusConverter
|
import com.tangem.data.swap.converter.SwapStatusConverter
|
||||||
|
|
@ -57,6 +59,7 @@ internal class DefaultSwapRepositoryV2 @Inject constructor(
|
||||||
private val dataSignatureVerifier: DataSignatureVerifier,
|
private val dataSignatureVerifier: DataSignatureVerifier,
|
||||||
private val singleQuoteStatusSupplier: SingleQuoteStatusSupplier,
|
private val singleQuoteStatusSupplier: SingleQuoteStatusSupplier,
|
||||||
private val singleQuoteStatusFetcher: SingleQuoteStatusFetcher,
|
private val singleQuoteStatusFetcher: SingleQuoteStatusFetcher,
|
||||||
|
private val featureTogglesManager: FeatureTogglesManager,
|
||||||
@NetworkMoshi moshi: Moshi,
|
@NetworkMoshi moshi: Moshi,
|
||||||
) : SwapRepositoryV2 {
|
) : SwapRepositoryV2 {
|
||||||
|
|
||||||
|
|
@ -544,16 +547,21 @@ internal class DefaultSwapRepositoryV2 @Inject constructor(
|
||||||
return setScale(decimals, RoundingMode.HALF_DOWN).movePointRight(decimals).toPlainString()
|
return setScale(decimals, RoundingMode.HALF_DOWN).movePointRight(decimals).toPlainString()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun List<ExpressProvider>.filterYieldSupplyProvider(cryptoCurrencyStatus: CryptoCurrencyStatus?) =
|
private fun List<ExpressProvider>.filterYieldSupplyProvider(
|
||||||
filter { provider ->
|
cryptoCurrencyStatus: CryptoCurrencyStatus?,
|
||||||
// !!!WARNING!!! Filter out dex provider if yield supply is active
|
): List<ExpressProvider> {
|
||||||
val yieldSupplyStatus = cryptoCurrencyStatus?.value?.yieldSupplyStatus
|
return if (featureTogglesManager.isFeatureEnabled(FeatureToggles.TWI_1326_YIELD_MODE_SWAP_ENABLED)) {
|
||||||
if (yieldSupplyStatus != null && yieldSupplyStatus.isActive) {
|
this
|
||||||
provider.type == ExpressProviderType.CEX
|
} else {
|
||||||
} else {
|
filter { provider ->
|
||||||
true
|
if (cryptoCurrencyStatus?.value?.yieldSupplyStatus?.isActive == true) {
|
||||||
|
provider.type == ExpressProviderType.CEX
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val MEMO_RESTRICTED_NETWORKS = setOf(
|
private val MEMO_RESTRICTED_NETWORKS = setOf(
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import com.tangem.domain.quotes.single.SingleQuoteStatusFetcher
|
||||||
import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier
|
import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier
|
||||||
import com.tangem.domain.swap.SwapErrorResolver
|
import com.tangem.domain.swap.SwapErrorResolver
|
||||||
import com.tangem.domain.swap.SwapRepositoryV2
|
import com.tangem.domain.swap.SwapRepositoryV2
|
||||||
|
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||||
import com.tangem.domain.swap.SwapTransactionRepository
|
import com.tangem.domain.swap.SwapTransactionRepository
|
||||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
|
|
@ -49,6 +50,7 @@ internal object SwapDataModule {
|
||||||
dataSignatureVerifier: DataSignatureVerifier,
|
dataSignatureVerifier: DataSignatureVerifier,
|
||||||
singleQuoteStatusSupplier: SingleQuoteStatusSupplier,
|
singleQuoteStatusSupplier: SingleQuoteStatusSupplier,
|
||||||
singleQuoteStatusFetcher: SingleQuoteStatusFetcher,
|
singleQuoteStatusFetcher: SingleQuoteStatusFetcher,
|
||||||
|
featureTogglesManager: FeatureTogglesManager,
|
||||||
@NetworkMoshi moshi: Moshi,
|
@NetworkMoshi moshi: Moshi,
|
||||||
): SwapRepositoryV2 {
|
): SwapRepositoryV2 {
|
||||||
return DefaultSwapRepositoryV2(
|
return DefaultSwapRepositoryV2(
|
||||||
|
|
@ -60,6 +62,7 @@ internal object SwapDataModule {
|
||||||
moshi = moshi,
|
moshi = moshi,
|
||||||
singleQuoteStatusSupplier = singleQuoteStatusSupplier,
|
singleQuoteStatusSupplier = singleQuoteStatusSupplier,
|
||||||
singleQuoteStatusFetcher = singleQuoteStatusFetcher,
|
singleQuoteStatusFetcher = singleQuoteStatusFetcher,
|
||||||
|
featureTogglesManager = featureTogglesManager,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,8 @@ import com.tangem.domain.swap.models.SwapAmountType
|
||||||
import com.tangem.domain.swap.models.SwapCurrencyStatus
|
import com.tangem.domain.swap.models.SwapCurrencyStatus
|
||||||
import com.tangem.domain.swap.models.SwapStatus
|
import com.tangem.domain.swap.models.SwapStatus
|
||||||
import com.tangem.domain.swap.models.SwapTxType
|
import com.tangem.domain.swap.models.SwapTxType
|
||||||
|
import com.tangem.core.configtoggle.FeatureToggles
|
||||||
|
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||||
import io.mockk.*
|
import io.mockk.*
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
|
|
@ -43,6 +45,9 @@ internal class DefaultSwapRepositoryV2Test {
|
||||||
private val singleQuoteStatusSupplier: SingleQuoteStatusSupplier = mockk()
|
private val singleQuoteStatusSupplier: SingleQuoteStatusSupplier = mockk()
|
||||||
private val singleQuoteStatusFetcher: SingleQuoteStatusFetcher = mockk()
|
private val singleQuoteStatusFetcher: SingleQuoteStatusFetcher = mockk()
|
||||||
private val moshi: Moshi = Moshi.Builder().build()
|
private val moshi: Moshi = Moshi.Builder().build()
|
||||||
|
private val featureTogglesManager: FeatureTogglesManager = mockk {
|
||||||
|
every { isFeatureEnabled(any()) } returns false
|
||||||
|
}
|
||||||
|
|
||||||
private val repository = DefaultSwapRepositoryV2(
|
private val repository = DefaultSwapRepositoryV2(
|
||||||
tangemExpressApi = tangemExpressApi,
|
tangemExpressApi = tangemExpressApi,
|
||||||
|
|
@ -52,6 +57,7 @@ internal class DefaultSwapRepositoryV2Test {
|
||||||
dataSignatureVerifier = dataSignatureVerifier,
|
dataSignatureVerifier = dataSignatureVerifier,
|
||||||
singleQuoteStatusSupplier = singleQuoteStatusSupplier,
|
singleQuoteStatusSupplier = singleQuoteStatusSupplier,
|
||||||
singleQuoteStatusFetcher = singleQuoteStatusFetcher,
|
singleQuoteStatusFetcher = singleQuoteStatusFetcher,
|
||||||
|
featureTogglesManager = featureTogglesManager,
|
||||||
moshi = moshi,
|
moshi = moshi,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -64,7 +70,9 @@ internal class DefaultSwapRepositoryV2Test {
|
||||||
dataSignatureVerifier,
|
dataSignatureVerifier,
|
||||||
singleQuoteStatusSupplier,
|
singleQuoteStatusSupplier,
|
||||||
singleQuoteStatusFetcher,
|
singleQuoteStatusFetcher,
|
||||||
|
featureTogglesManager,
|
||||||
)
|
)
|
||||||
|
every { featureTogglesManager.isFeatureEnabled(any()) } returns false
|
||||||
}
|
}
|
||||||
|
|
||||||
// region getPairs(SwapCurrencyStatus, SwapCurrencyStatus)
|
// region getPairs(SwapCurrencyStatus, SwapCurrencyStatus)
|
||||||
|
|
@ -511,8 +519,9 @@ internal class DefaultSwapRepositoryV2Test {
|
||||||
// region filterYieldSupplyProvider
|
// region filterYieldSupplyProvider
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `getPairs filters out DEX providers when yield supply is active`() = runTest {
|
fun `getPairs filters out DEX providers when yield supply is active and flag is off`() = runTest {
|
||||||
// Arrange
|
// Arrange
|
||||||
|
every { featureTogglesManager.isFeatureEnabled(FeatureToggles.TWI_1326_YIELD_MODE_SWAP_ENABLED) } returns false
|
||||||
val primaryStatus = createCryptoCurrencyStatusWithActiveYield(primaryCoin)
|
val primaryStatus = createCryptoCurrencyStatusWithActiveYield(primaryCoin)
|
||||||
val secondaryStatus = createCryptoCurrencyStatus(secondaryCoin)
|
val secondaryStatus = createCryptoCurrencyStatus(secondaryCoin)
|
||||||
val primarySwapCurrencyStatus = SwapCurrencyStatus(
|
val primarySwapCurrencyStatus = SwapCurrencyStatus(
|
||||||
|
|
@ -558,6 +567,54 @@ internal class DefaultSwapRepositoryV2Test {
|
||||||
assertThat(providers.first().type).isEqualTo(ExpressProviderType.CEX)
|
assertThat(providers.first().type).isEqualTo(ExpressProviderType.CEX)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `getPairs keeps DEX providers when yield supply is active and flag is on`() = runTest {
|
||||||
|
// Arrange
|
||||||
|
every { featureTogglesManager.isFeatureEnabled(FeatureToggles.TWI_1326_YIELD_MODE_SWAP_ENABLED) } returns true
|
||||||
|
val primaryStatus = createCryptoCurrencyStatusWithActiveYield(primaryCoin)
|
||||||
|
val secondaryStatus = createCryptoCurrencyStatus(secondaryCoin)
|
||||||
|
val primarySwapCurrencyStatus = SwapCurrencyStatus(
|
||||||
|
userWallet = userWallet,
|
||||||
|
status = primaryStatus,
|
||||||
|
account = mockk(),
|
||||||
|
)
|
||||||
|
val secondarySwapCurrencyStatus = SwapCurrencyStatus(
|
||||||
|
userWallet = userWallet,
|
||||||
|
status = secondaryStatus,
|
||||||
|
account = mockk(),
|
||||||
|
)
|
||||||
|
|
||||||
|
val swapPair = SwapPair(
|
||||||
|
from = LeastTokenInfo(contractAddress = "0", network = ETH_BACKEND_ID),
|
||||||
|
to = LeastTokenInfo(contractAddress = "0", network = BTC_BACKEND_ID),
|
||||||
|
providers = listOf(
|
||||||
|
SwapPairProvider(providerId = PROVIDER_ID, rateTypes = listOf(RateType.FLOAT)),
|
||||||
|
SwapPairProvider(providerId = CEX_PROVIDER_ID, rateTypes = listOf(RateType.FLOAT)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
coEvery {
|
||||||
|
tangemExpressApi.getPairs(any(), any(), any())
|
||||||
|
} returns ApiResponse.Success(listOf(swapPair))
|
||||||
|
|
||||||
|
coEvery {
|
||||||
|
expressRepository.getProviders(any(), any())
|
||||||
|
} returns listOf(dexProvider, cexProvider)
|
||||||
|
|
||||||
|
// Act
|
||||||
|
val result = repository.getPairs(
|
||||||
|
primarySwapCurrencyStatus = primarySwapCurrencyStatus,
|
||||||
|
secondarySwapCurrencyStatus = secondarySwapCurrencyStatus,
|
||||||
|
filterProviderTypes = emptyList(),
|
||||||
|
swapTxType = SwapTxType.Swap,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Assert — both providers should remain
|
||||||
|
assertThat(result).hasSize(2)
|
||||||
|
val providers = result.first().providers
|
||||||
|
assertThat(providers).hasSize(2)
|
||||||
|
}
|
||||||
|
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
// region getSwapData
|
// region getSwapData
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
package com.tangem.data.yield.supply
|
||||||
|
|
||||||
|
import com.tangem.blockchain.blockchains.ethereum.EthereumUtils
|
||||||
|
import com.tangem.blockchainsdk.utils.toBlockchain
|
||||||
|
import com.tangem.domain.models.network.Network
|
||||||
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
|
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||||
|
import com.tangem.domain.yield.supply.YieldModuleAddressProvider
|
||||||
|
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
|
internal class DefaultYieldModuleAddressProvider(
|
||||||
|
private val walletManagersFacade: WalletManagersFacade,
|
||||||
|
private val dispatchers: CoroutineDispatcherProvider,
|
||||||
|
) : YieldModuleAddressProvider {
|
||||||
|
|
||||||
|
private data class Key(val userWalletId: UserWalletId, val networkRawId: String)
|
||||||
|
|
||||||
|
private val cache = ConcurrentHashMap<Key, String>()
|
||||||
|
private val mutex = Mutex()
|
||||||
|
|
||||||
|
override suspend fun getOrFetch(userWalletId: UserWalletId, network: Network): String? {
|
||||||
|
val key = Key(userWalletId, network.rawId)
|
||||||
|
cache[key]?.let { return it }
|
||||||
|
return withContext(dispatchers.io) {
|
||||||
|
mutex.withLock {
|
||||||
|
cache[key]?.let { return@withLock it }
|
||||||
|
val walletManager = walletManagersFacade.getOrCreateWalletManager(
|
||||||
|
userWalletId = userWalletId,
|
||||||
|
blockchain = network.toBlockchain(),
|
||||||
|
derivationPath = network.derivationPath.value,
|
||||||
|
) ?: error("Wallet manager not found for $network")
|
||||||
|
// SDK returns ZERO_ADDRESS on internal failure (e.g. RPC error). Treat that as
|
||||||
|
// "unavailable" so callers are forced by the type system to fall back instead
|
||||||
|
// of using it as a destination.
|
||||||
|
val address = walletManager.getYieldModuleAddress()
|
||||||
|
.takeIf { it != EthereumUtils.ZERO_ADDRESS }
|
||||||
|
if (address != null) cache[key] = address
|
||||||
|
address
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun invalidate(userWalletId: UserWalletId?) {
|
||||||
|
if (userWalletId == null) {
|
||||||
|
cache.clear()
|
||||||
|
} else {
|
||||||
|
cache.keys.removeAll { it.userWalletId == userWalletId }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -11,6 +11,7 @@ import com.tangem.blockchain.yieldsupply.YieldSupplyContractCallDataProviderFact
|
||||||
import com.tangem.blockchainsdk.utils.toBlockchain
|
import com.tangem.blockchainsdk.utils.toBlockchain
|
||||||
import com.tangem.domain.models.currency.CryptoCurrency
|
import com.tangem.domain.models.currency.CryptoCurrency
|
||||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||||
|
import com.tangem.domain.models.network.Network
|
||||||
import com.tangem.domain.models.wallet.UserWalletId
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
import com.tangem.domain.models.yield.supply.YieldSupplyStatus
|
import com.tangem.domain.models.yield.supply.YieldSupplyStatus
|
||||||
import com.tangem.domain.utils.convertToSdkAmount
|
import com.tangem.domain.utils.convertToSdkAmount
|
||||||
|
|
@ -127,6 +128,11 @@ internal class DefaultYieldSupplyTransactionRepository(
|
||||||
val amount = getEnterAmount(cryptoCurrency, yieldSupplyStatus)
|
val amount = getEnterAmount(cryptoCurrency, yieldSupplyStatus)
|
||||||
|
|
||||||
val emptyContractAddress = existingYieldAddress == null || existingYieldAddress == EthereumUtils.ZERO_ADDRESS
|
val emptyContractAddress = existingYieldAddress == null || existingYieldAddress == EthereumUtils.ZERO_ADDRESS
|
||||||
|
val activeYieldContractAddress = if (emptyContractAddress) {
|
||||||
|
calculatedYieldContractAddress
|
||||||
|
} else {
|
||||||
|
existingYieldAddress
|
||||||
|
}
|
||||||
|
|
||||||
when {
|
when {
|
||||||
yieldSupplyStatus == null || emptyContractAddress -> {
|
yieldSupplyStatus == null || emptyContractAddress -> {
|
||||||
|
|
@ -143,7 +149,7 @@ internal class DefaultYieldSupplyTransactionRepository(
|
||||||
createInitTokenTransaction(
|
createInitTokenTransaction(
|
||||||
walletManager = walletManager,
|
walletManager = walletManager,
|
||||||
cryptoCurrency = cryptoCurrency,
|
cryptoCurrency = cryptoCurrency,
|
||||||
yieldContractAddress = calculatedYieldContractAddress,
|
yieldContractAddress = activeYieldContractAddress,
|
||||||
amount = amount,
|
amount = amount,
|
||||||
maxNetworkFee = maxNetworkFee,
|
maxNetworkFee = maxNetworkFee,
|
||||||
),
|
),
|
||||||
|
|
@ -152,7 +158,7 @@ internal class DefaultYieldSupplyTransactionRepository(
|
||||||
createReactivateTokenTransaction(
|
createReactivateTokenTransaction(
|
||||||
walletManager = walletManager,
|
walletManager = walletManager,
|
||||||
cryptoCurrency = cryptoCurrency,
|
cryptoCurrency = cryptoCurrency,
|
||||||
yieldContractAddress = calculatedYieldContractAddress,
|
yieldContractAddress = activeYieldContractAddress,
|
||||||
amount = amount,
|
amount = amount,
|
||||||
maxNetworkFee = maxNetworkFee,
|
maxNetworkFee = maxNetworkFee,
|
||||||
),
|
),
|
||||||
|
|
@ -166,7 +172,7 @@ internal class DefaultYieldSupplyTransactionRepository(
|
||||||
walletManager = walletManager,
|
walletManager = walletManager,
|
||||||
cryptoCurrency = cryptoCurrency,
|
cryptoCurrency = cryptoCurrency,
|
||||||
callData = ApprovalERC20TokenCallData(
|
callData = ApprovalERC20TokenCallData(
|
||||||
spenderAddress = calculatedYieldContractAddress,
|
spenderAddress = activeYieldContractAddress,
|
||||||
amount = null,
|
amount = null,
|
||||||
),
|
),
|
||||||
destinationAddress = cryptoCurrency.contractAddress,
|
destinationAddress = cryptoCurrency.contractAddress,
|
||||||
|
|
@ -182,7 +188,7 @@ internal class DefaultYieldSupplyTransactionRepository(
|
||||||
walletManager = walletManager,
|
walletManager = walletManager,
|
||||||
cryptoCurrency = cryptoCurrency,
|
cryptoCurrency = cryptoCurrency,
|
||||||
amount = amount,
|
amount = amount,
|
||||||
yieldContractAddress = calculatedYieldContractAddress,
|
yieldContractAddress = activeYieldContractAddress,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -218,6 +224,20 @@ internal class DefaultYieldSupplyTransactionRepository(
|
||||||
}.onFailure { TangemLogger.e("Error", it) }.getOrThrow()
|
}.onFailure { TangemLogger.e("Error", it) }.getOrThrow()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun wrapYieldSwapCallDataWithUpgradeIfNeeded(
|
||||||
|
userWalletId: UserWalletId,
|
||||||
|
network: Network,
|
||||||
|
callData: SmartContractCallData,
|
||||||
|
): SmartContractCallData = withContext(dispatchers.io) {
|
||||||
|
val walletManager = walletManagersFacade.getOrCreateWalletManager(
|
||||||
|
userWalletId = userWalletId,
|
||||||
|
blockchain = network.toBlockchain(),
|
||||||
|
derivationPath = network.derivationPath.value,
|
||||||
|
) ?: error("Wallet manager not found for $network")
|
||||||
|
val versionStatus = walletManager.checkModuleVersionStatus()
|
||||||
|
YieldSupplyContractCallDataProviderFactory.wrapWithUpgradeIfNeeded(versionStatus, callData)
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun getYieldTokenStatus(
|
private suspend fun getYieldTokenStatus(
|
||||||
walletManager: WalletManager,
|
walletManager: WalletManager,
|
||||||
cryptoCurrency: CryptoCurrency.Token,
|
cryptoCurrency: CryptoCurrency.Token,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package com.tangem.data.yield.supply.di
|
package com.tangem.data.yield.supply.di
|
||||||
|
|
||||||
import com.tangem.core.analytics.api.AnalyticsExceptionHandler
|
import com.tangem.core.analytics.api.AnalyticsExceptionHandler
|
||||||
|
import com.tangem.data.yield.supply.DefaultYieldModuleAddressProvider
|
||||||
import com.tangem.data.yield.supply.DefaultYieldSupplyRepository
|
import com.tangem.data.yield.supply.DefaultYieldSupplyRepository
|
||||||
import com.tangem.data.yield.supply.DefaultYieldSupplyErrorResolver
|
import com.tangem.data.yield.supply.DefaultYieldSupplyErrorResolver
|
||||||
import com.tangem.data.yield.supply.DefaultYieldSupplyTransactionRepository
|
import com.tangem.data.yield.supply.DefaultYieldSupplyTransactionRepository
|
||||||
|
|
@ -12,6 +13,7 @@ import com.tangem.datasource.local.yieldsupply.YieldMarketsStore
|
||||||
import com.tangem.datasource.local.yieldsupply.promo.YieldBoostPromoStore
|
import com.tangem.datasource.local.yieldsupply.promo.YieldBoostPromoStore
|
||||||
import com.tangem.datasource.local.yieldsupply.promo.YieldBoostStatusStore
|
import com.tangem.datasource.local.yieldsupply.promo.YieldBoostStatusStore
|
||||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||||
|
import com.tangem.domain.yield.supply.YieldModuleAddressProvider
|
||||||
import com.tangem.domain.yield.supply.YieldSupplyRepository
|
import com.tangem.domain.yield.supply.YieldSupplyRepository
|
||||||
import com.tangem.domain.yield.supply.YieldSupplyErrorResolver
|
import com.tangem.domain.yield.supply.YieldSupplyErrorResolver
|
||||||
import com.tangem.domain.yield.supply.YieldSupplyTransactionRepository
|
import com.tangem.domain.yield.supply.YieldSupplyTransactionRepository
|
||||||
|
|
@ -65,6 +67,18 @@ internal object YieldSupplyDataModule {
|
||||||
return DefaultYieldSupplyErrorResolver
|
return DefaultYieldSupplyErrorResolver
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideYieldModuleAddressProvider(
|
||||||
|
walletManagersFacade: WalletManagersFacade,
|
||||||
|
dispatchers: CoroutineDispatcherProvider,
|
||||||
|
): YieldModuleAddressProvider {
|
||||||
|
return DefaultYieldModuleAddressProvider(
|
||||||
|
walletManagersFacade = walletManagersFacade,
|
||||||
|
dispatchers = dispatchers,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideYieldPromoRepository(
|
fun provideYieldPromoRepository(
|
||||||
|
|
|
||||||
|
|
@ -30,4 +30,6 @@ data class SwapCurrencyStatus(
|
||||||
get() = status.currency
|
get() = status.currency
|
||||||
val userWalletId: UserWalletId
|
val userWalletId: UserWalletId
|
||||||
get() = userWallet.walletId
|
get() = userWallet.walletId
|
||||||
|
val isYieldSupplyActive: Boolean
|
||||||
|
get() = status.value.yieldSupplyStatus?.isActive == true
|
||||||
}
|
}
|
||||||
|
|
@ -46,7 +46,7 @@ class GetEthSpecificFeeUseCase(
|
||||||
val minimalFee = getEthLegacyFee(
|
val minimalFee = getEthLegacyFee(
|
||||||
gasPrice = gasPriceResult,
|
gasPrice = gasPriceResult,
|
||||||
gasLimit = gasLimit,
|
gasLimit = gasLimit,
|
||||||
decimals = cryptoCurrency.decimals,
|
decimals = blockchain.decimals(),
|
||||||
blockchain = blockchain,
|
blockchain = blockchain,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -54,7 +54,7 @@ class GetEthSpecificFeeUseCase(
|
||||||
val normalFee = getEthLegacyFee(
|
val normalFee = getEthLegacyFee(
|
||||||
gasPrice = normalGasPrice,
|
gasPrice = normalGasPrice,
|
||||||
gasLimit = gasLimit,
|
gasLimit = gasLimit,
|
||||||
decimals = cryptoCurrency.decimals,
|
decimals = blockchain.decimals(),
|
||||||
blockchain = blockchain,
|
blockchain = blockchain,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -64,7 +64,7 @@ class GetEthSpecificFeeUseCase(
|
||||||
val priorityFee = getEthLegacyFee(
|
val priorityFee = getEthLegacyFee(
|
||||||
gasPrice = priorityGasPrice,
|
gasPrice = priorityGasPrice,
|
||||||
gasLimit = gasLimit,
|
gasLimit = gasLimit,
|
||||||
decimals = cryptoCurrency.decimals,
|
decimals = blockchain.decimals(),
|
||||||
blockchain = blockchain,
|
blockchain = blockchain,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
package com.tangem.domain.yield.supply
|
||||||
|
|
||||||
|
import com.tangem.domain.models.network.Network
|
||||||
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the yield-module proxy address for a `(wallet, network)` pair and caches the result.
|
||||||
|
*
|
||||||
|
* The address is derived from on-chain state (factory contract + user's wallet) and is stable
|
||||||
|
* for the lifetime of the wallet, so caching the result avoids redundant blockchain calls.
|
||||||
|
*
|
||||||
|
* Call [invalidate] when the wallet's yield-module state may have changed (e.g. after a
|
||||||
|
* successful upgrade or removal of yield-supply).
|
||||||
|
*/
|
||||||
|
interface YieldModuleAddressProvider {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the yield-module proxy address, or `null` if the address is currently unavailable
|
||||||
|
* (e.g. RPC failure inside the SDK).
|
||||||
|
*/
|
||||||
|
suspend fun getOrFetch(userWalletId: UserWalletId, network: Network): String?
|
||||||
|
|
||||||
|
/** Drops cached entries for [userWalletId], or the entire cache when [userWalletId] is `null`. */
|
||||||
|
fun invalidate(userWalletId: UserWalletId? = null)
|
||||||
|
}
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
package com.tangem.domain.yield.supply
|
package com.tangem.domain.yield.supply
|
||||||
|
|
||||||
import com.tangem.blockchain.common.TransactionData
|
import com.tangem.blockchain.common.TransactionData
|
||||||
|
import com.tangem.blockchain.common.smartcontract.SmartContractCallData
|
||||||
import com.tangem.blockchain.common.transaction.Fee
|
import com.tangem.blockchain.common.transaction.Fee
|
||||||
import com.tangem.domain.models.currency.CryptoCurrency
|
import com.tangem.domain.models.currency.CryptoCurrency
|
||||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||||
|
import com.tangem.domain.models.network.Network
|
||||||
import com.tangem.domain.models.wallet.UserWalletId
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
|
|
||||||
|
|
@ -24,4 +26,14 @@ interface YieldSupplyTransactionRepository {
|
||||||
suspend fun getYieldContractAddress(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): String?
|
suspend fun getYieldContractAddress(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): String?
|
||||||
|
|
||||||
suspend fun getEffectiveProtocolBalance(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): BigDecimal?
|
suspend fun getEffectiveProtocolBalance(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): BigDecimal?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks the version status of the user's yield-module contract and wraps [callData] with an
|
||||||
|
* upgrade transaction if the deployed version is out of date.
|
||||||
|
*/
|
||||||
|
suspend fun wrapYieldSwapCallDataWithUpgradeIfNeeded(
|
||||||
|
userWalletId: UserWalletId,
|
||||||
|
network: Network,
|
||||||
|
callData: SmartContractCallData,
|
||||||
|
): SmartContractCallData
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
package com.tangem.domain.yield.supply.usecase
|
||||||
|
|
||||||
|
import com.tangem.blockchain.common.smartcontract.SmartContractCallData
|
||||||
|
import com.tangem.domain.models.network.Network
|
||||||
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
|
import com.tangem.domain.yield.supply.YieldSupplyTransactionRepository
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wraps a yield-swap call data with a yield-module upgrade transaction when the user's deployed
|
||||||
|
* yield-module contract version is out of date.
|
||||||
|
*/
|
||||||
|
class WrapYieldSwapCallDataWithUpgradeUseCase(
|
||||||
|
private val yieldSupplyTransactionRepository: YieldSupplyTransactionRepository,
|
||||||
|
) {
|
||||||
|
|
||||||
|
suspend operator fun invoke(
|
||||||
|
userWalletId: UserWalletId,
|
||||||
|
network: Network,
|
||||||
|
callData: SmartContractCallData,
|
||||||
|
): SmartContractCallData = yieldSupplyTransactionRepository.wrapYieldSwapCallDataWithUpgradeIfNeeded(
|
||||||
|
userWalletId = userWalletId,
|
||||||
|
network = network,
|
||||||
|
callData = callData,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package com.tangem.features.swap
|
package com.tangem.features.swap
|
||||||
|
|
||||||
interface SwapFeatureToggles {
|
interface SwapFeatureToggles {
|
||||||
|
val isYieldSwapEnabled: Boolean
|
||||||
val isSwapSwitchToTransferEnabled: Boolean
|
val isSwapSwitchToTransferEnabled: Boolean
|
||||||
val isSwapIntegratedApproveEnabled: Boolean
|
val isSwapIntegratedApproveEnabled: Boolean
|
||||||
val isSwapAbEnabled: Boolean
|
val isSwapAbEnabled: Boolean
|
||||||
|
|
|
||||||
|
|
@ -51,8 +51,10 @@ dependencies {
|
||||||
implementation(projects.domain.visa)
|
implementation(projects.domain.visa)
|
||||||
implementation(projects.domain.visa.models)
|
implementation(projects.domain.visa.models)
|
||||||
implementation(projects.domain.balanceHiding)
|
implementation(projects.domain.balanceHiding)
|
||||||
|
implementation(projects.domain.yieldSupply)
|
||||||
|
|
||||||
/** Core modules */
|
/** Core modules */
|
||||||
|
implementation(projects.core.configToggles)
|
||||||
implementation(projects.core.utils)
|
implementation(projects.core.utils)
|
||||||
implementation(projects.core.ui)
|
implementation(projects.core.ui)
|
||||||
implementation(projects.core.datasource)
|
implementation(projects.core.datasource)
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@ import com.tangem.domain.transaction.usecase.*
|
||||||
import com.tangem.domain.transaction.usecase.gasless.CreateAndSendGaslessTransactionUseCase
|
import com.tangem.domain.transaction.usecase.gasless.CreateAndSendGaslessTransactionUseCase
|
||||||
import com.tangem.domain.utils.convertToSdkAmount
|
import com.tangem.domain.utils.convertToSdkAmount
|
||||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||||
|
import com.tangem.domain.yield.supply.YieldModuleAddressProvider
|
||||||
import com.tangem.feature.swap.domain.api.SwapRepository
|
import com.tangem.feature.swap.domain.api.SwapRepository
|
||||||
import com.tangem.feature.swap.domain.fee.CexSwapFeeCalculator
|
import com.tangem.feature.swap.domain.fee.CexSwapFeeCalculator
|
||||||
import com.tangem.feature.swap.domain.fee.DexSwapFeeCalculator
|
import com.tangem.feature.swap.domain.fee.DexSwapFeeCalculator
|
||||||
|
|
@ -61,6 +62,7 @@ import com.tangem.feature.swap.domain.models.SwapAmount
|
||||||
import com.tangem.feature.swap.domain.models.domain.*
|
import com.tangem.feature.swap.domain.models.domain.*
|
||||||
import com.tangem.feature.swap.domain.models.toStringWithRightOffset
|
import com.tangem.feature.swap.domain.models.toStringWithRightOffset
|
||||||
import com.tangem.feature.swap.domain.models.ui.*
|
import com.tangem.feature.swap.domain.models.ui.*
|
||||||
|
import com.tangem.features.swap.SwapFeatureToggles
|
||||||
import com.tangem.utils.coroutines.runSuspendCatching
|
import com.tangem.utils.coroutines.runSuspendCatching
|
||||||
import com.tangem.utils.extensions.orZero
|
import com.tangem.utils.extensions.orZero
|
||||||
import com.tangem.utils.logging.TangemLogger
|
import com.tangem.utils.logging.TangemLogger
|
||||||
|
|
@ -99,12 +101,17 @@ internal class SwapInteractorImpl @Inject constructor(
|
||||||
private val getSwapPairUseCase: GetSwapPairUseCase,
|
private val getSwapPairUseCase: GetSwapPairUseCase,
|
||||||
private val dexSwapFeeCalculator: DexSwapFeeCalculator,
|
private val dexSwapFeeCalculator: DexSwapFeeCalculator,
|
||||||
private val cexSwapFeeCalculator: CexSwapFeeCalculator,
|
private val cexSwapFeeCalculator: CexSwapFeeCalculator,
|
||||||
|
private val swapFeatureToggles: SwapFeatureToggles,
|
||||||
|
private val yieldModuleAddressProvider: YieldModuleAddressProvider,
|
||||||
) : SwapInteractor {
|
) : SwapInteractor {
|
||||||
|
|
||||||
private val getSelectedAppCurrencyUseCase by lazy(LazyThreadSafetyMode.NONE) {
|
private val getSelectedAppCurrencyUseCase by lazy(LazyThreadSafetyMode.NONE) {
|
||||||
GetSelectedAppCurrencyUseCase(appCurrencyRepository)
|
GetSelectedAppCurrencyUseCase(appCurrencyRepository)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val SwapCurrencyStatus.isYieldSwapActive: Boolean
|
||||||
|
get() = swapFeatureToggles.isYieldSwapEnabled && isYieldSupplyActive
|
||||||
|
|
||||||
override suspend fun getPair(
|
override suspend fun getPair(
|
||||||
fromSwapCurrencyStatus: SwapCurrencyStatus,
|
fromSwapCurrencyStatus: SwapCurrencyStatus,
|
||||||
toSwapCurrencyStatus: SwapCurrencyStatus,
|
toSwapCurrencyStatus: SwapCurrencyStatus,
|
||||||
|
|
@ -254,7 +261,9 @@ internal class SwapInteractorImpl @Inject constructor(
|
||||||
reduceBalanceBy: BigDecimal,
|
reduceBalanceBy: BigDecimal,
|
||||||
expressOperationType: ExpressOperationType,
|
expressOperationType: ExpressOperationType,
|
||||||
): Pair<SwapProvider, SwapState> {
|
): Pair<SwapProvider, SwapState> {
|
||||||
if (fromSwapCurrencyStatus.status.value.yieldSupplyStatus?.isActive == true) {
|
if (fromSwapCurrencyStatus.status.value.yieldSupplyStatus?.isActive == true &&
|
||||||
|
!swapFeatureToggles.isYieldSwapEnabled
|
||||||
|
) {
|
||||||
return provider to produceDexSwapDataError(
|
return provider to produceDexSwapDataError(
|
||||||
error = ExpressDataError.DexActiveSupplyError(),
|
error = ExpressDataError.DexActiveSupplyError(),
|
||||||
fromSwapCurrencyStatus = fromSwapCurrencyStatus,
|
fromSwapCurrencyStatus = fromSwapCurrencyStatus,
|
||||||
|
|
@ -286,19 +295,26 @@ internal class SwapInteractorImpl @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
val fromTokenAddress = getTokenAddress(fromSwapCurrencyStatus.currency)
|
val fromTokenAddress = getTokenAddress(fromSwapCurrencyStatus.currency)
|
||||||
val isAllowedToSpend = maybeQuotes.fold(
|
val isYieldSwap = fromSwapCurrencyStatus.isYieldSwapActive &&
|
||||||
ifRight = { quotes ->
|
fromSwapCurrencyStatus.currency is CryptoCurrency.Token
|
||||||
quotes.allowanceContract?.let { allowanceContract ->
|
val isAllowedToSpend = if (isYieldSwap) {
|
||||||
getAllowanceInfoUseCase(
|
maybeQuotes.isRight() &&
|
||||||
userWalletId = fromSwapCurrencyStatus.userWalletId,
|
fromSwapCurrencyStatus.status.value.yieldSupplyStatus?.isAllowedToSpend == true
|
||||||
cryptoCurrency = fromSwapCurrencyStatus.currency,
|
} else {
|
||||||
spenderAddress = allowanceContract,
|
maybeQuotes.fold(
|
||||||
requiredAmount = amount.value,
|
ifRight = { quotes ->
|
||||||
).getOrNull() is AllowanceInfo.Enough
|
quotes.allowanceContract?.let { allowanceContract ->
|
||||||
} != false
|
getAllowanceInfoUseCase(
|
||||||
},
|
userWalletId = fromSwapCurrencyStatus.userWalletId,
|
||||||
ifLeft = { false },
|
cryptoCurrency = fromSwapCurrencyStatus.currency,
|
||||||
)
|
spenderAddress = allowanceContract,
|
||||||
|
requiredAmount = amount.value,
|
||||||
|
).getOrNull() is AllowanceInfo.Enough
|
||||||
|
} != false
|
||||||
|
},
|
||||||
|
ifLeft = { false },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (isAllowedToSpend && allowPermissionsHandler.isAddressAllowanceInProgress(fromTokenAddress)) {
|
if (isAllowedToSpend && allowPermissionsHandler.isAddressAllowanceInProgress(fromTokenAddress)) {
|
||||||
allowPermissionsHandler.removeAddressFromProgress(fromTokenAddress)
|
allowPermissionsHandler.removeAddressFromProgress(fromTokenAddress)
|
||||||
|
|
@ -308,6 +324,7 @@ internal class SwapInteractorImpl @Inject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val isBalanceWithoutFeeEnough = isBalanceEnough(fromSwapCurrencyStatus, amount, null)
|
val isBalanceWithoutFeeEnough = isBalanceEnough(fromSwapCurrencyStatus, amount, null)
|
||||||
|
val quoteAllowanceContract = maybeQuotes.getOrNull()?.allowanceContract
|
||||||
return if (isAllowedToSpend && isBalanceWithoutFeeEnough) {
|
return if (isAllowedToSpend && isBalanceWithoutFeeEnough) {
|
||||||
provider to loadDexSwapDataNoFee(
|
provider to loadDexSwapDataNoFee(
|
||||||
provider = provider,
|
provider = provider,
|
||||||
|
|
@ -315,6 +332,7 @@ internal class SwapInteractorImpl @Inject constructor(
|
||||||
toSwapCurrencyStatus = toSwapCurrencyStatus,
|
toSwapCurrencyStatus = toSwapCurrencyStatus,
|
||||||
amount = amount,
|
amount = amount,
|
||||||
expressOperationType = expressOperationType,
|
expressOperationType = expressOperationType,
|
||||||
|
quoteAllowanceContract = quoteAllowanceContract,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
val quoteBalanceStatus = if (isBalanceWithoutFeeEnough) {
|
val quoteBalanceStatus = if (isBalanceWithoutFeeEnough) {
|
||||||
|
|
@ -377,6 +395,7 @@ internal class SwapInteractorImpl @Inject constructor(
|
||||||
toSwapCurrencyStatus = toSwapCurrencyStatus,
|
toSwapCurrencyStatus = toSwapCurrencyStatus,
|
||||||
amount = amount,
|
amount = amount,
|
||||||
expressOperationType = expressOperationType,
|
expressOperationType = expressOperationType,
|
||||||
|
quoteAllowanceContract = maybeQuotes.getOrNull()?.allowanceContract,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
provider to getQuotesState(
|
provider to getQuotesState(
|
||||||
|
|
@ -635,28 +654,54 @@ internal class SwapInteractorImpl @Inject constructor(
|
||||||
swapFee: SwapFee,
|
swapFee: SwapFee,
|
||||||
): SwapTransactionState {
|
): SwapTransactionState {
|
||||||
val amountDecimal = requireNotNull(toBigDecimalOrNull(amountToSwap)) { "wrong amount format" }
|
val amountDecimal = requireNotNull(toBigDecimalOrNull(amountToSwap)) { "wrong amount format" }
|
||||||
val txValue = requireNotNull(swapData.transaction.txValue) { "txValue is null" }
|
|
||||||
val amount = SwapAmount(amountDecimal, fromSwapCurrencyStatus.currency.decimals)
|
val amount = SwapAmount(amountDecimal, fromSwapCurrencyStatus.currency.decimals)
|
||||||
val dexTransaction = swapData.transaction as ExpressTransactionModel.DEX
|
val dexTransaction = swapData.transaction as ExpressTransactionModel.DEX
|
||||||
val dataToSign = dexTransaction.txData
|
val dataToSign = dexTransaction.txData
|
||||||
val amountToSend = createNativeAmountForDex(txValue, fromSwapCurrencyStatus.currency.network)
|
val isYieldSwap = fromSwapCurrencyStatus.isYieldSwapActive
|
||||||
val txData = createTransactionUseCase(
|
val fromCurrency = fromSwapCurrencyStatus.currency
|
||||||
amount = amountToSend,
|
|
||||||
fee = swapFee.fee,
|
val txDataResult = if (isYieldSwap && fromCurrency is CryptoCurrency.Token) {
|
||||||
memo = null,
|
val spenderAddress = dexTransaction.allowanceContract
|
||||||
destination = swapData.transaction.txTo,
|
?: return SwapTransactionState.Error.UnknownError
|
||||||
userWalletId = fromSwapCurrencyStatus.userWalletId,
|
createYieldSwapDexTransaction(
|
||||||
network = toSwapCurrencyStatus.currency.network,
|
fromSwapCurrencyStatus = fromSwapCurrencyStatus,
|
||||||
txExtras = createDexTxExtras(
|
swapData = swapData,
|
||||||
dataToSign,
|
dexCallData = dataToSign,
|
||||||
fromSwapCurrencyStatus.currency.network,
|
amount = amountDecimal,
|
||||||
swapFee.fee.getGasLimit(),
|
fee = swapFee.fee,
|
||||||
),
|
spenderAddress = spenderAddress,
|
||||||
).getOrElse { error ->
|
)
|
||||||
|
} else {
|
||||||
|
val txValue = requireNotNull(swapData.transaction.txValue) { "txValue is null" }
|
||||||
|
val amountToSend = createNativeAmountForDex(txValue, fromCurrency.network)
|
||||||
|
createTransactionUseCase(
|
||||||
|
amount = amountToSend,
|
||||||
|
fee = swapFee.fee,
|
||||||
|
memo = null,
|
||||||
|
destination = swapData.transaction.txTo,
|
||||||
|
userWalletId = fromSwapCurrencyStatus.userWalletId,
|
||||||
|
network = fromCurrency.network,
|
||||||
|
txExtras = createDexTxExtras(
|
||||||
|
dataToSign,
|
||||||
|
fromCurrency.network,
|
||||||
|
swapFee.fee.getGasLimit(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val txData = txDataResult.getOrElse { error ->
|
||||||
TangemLogger.e("Failed to create swap dex tx data", error)
|
TangemLogger.e("Failed to create swap dex tx data", error)
|
||||||
return SwapTransactionState.Error.UnknownError
|
return SwapTransactionState.Error.UnknownError
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val payInAddress = if (isYieldSwap && fromCurrency is CryptoCurrency.Token) {
|
||||||
|
swapData.transaction.txTo
|
||||||
|
} else if (txData is TransactionData.Uncompiled) {
|
||||||
|
getPayoutAddress(txData)
|
||||||
|
} else {
|
||||||
|
swapData.transaction.txTo
|
||||||
|
}
|
||||||
|
|
||||||
return handleSwapResult(
|
return handleSwapResult(
|
||||||
fromSwapCurrencyStatus = fromSwapCurrencyStatus,
|
fromSwapCurrencyStatus = fromSwapCurrencyStatus,
|
||||||
toSwapCurrencyStatus = toSwapCurrencyStatus,
|
toSwapCurrencyStatus = toSwapCurrencyStatus,
|
||||||
|
|
@ -664,7 +709,7 @@ internal class SwapInteractorImpl @Inject constructor(
|
||||||
swapData = swapData,
|
swapData = swapData,
|
||||||
amount = amount,
|
amount = amount,
|
||||||
txData = txData,
|
txData = txData,
|
||||||
payInAddress = getPayoutAddress(txData),
|
payInAddress = payInAddress,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1001,11 +1046,23 @@ internal class SwapInteractorImpl @Inject constructor(
|
||||||
val transaction = swapData?.transaction as? ExpressTransactionModel.DEX
|
val transaction = swapData?.transaction as? ExpressTransactionModel.DEX
|
||||||
?: return GetFeeError.UnknownError.left()
|
?: return GetFeeError.UnknownError.left()
|
||||||
|
|
||||||
return dexSwapFeeCalculator.calculate(
|
val dexFeeResultEither = if (fromStatus.isYieldSwapActive && fromStatus.currency is CryptoCurrency.Token) {
|
||||||
fromSwapCurrencyStatus = fromStatus,
|
val network = (fromStatus.currency as CryptoCurrency.Token).network
|
||||||
transaction = transaction,
|
val yieldModuleAddress = yieldModuleAddressProvider.getOrFetch(fromStatus.userWalletId, network)
|
||||||
selectedToken = selectedFeeToken,
|
dexSwapFeeCalculator.calculateYield(
|
||||||
).fold(
|
fromSwapCurrencyStatus = fromStatus,
|
||||||
|
transaction = transaction,
|
||||||
|
yieldModuleAddress = yieldModuleAddress,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
dexSwapFeeCalculator.calculate(
|
||||||
|
fromSwapCurrencyStatus = fromStatus,
|
||||||
|
transaction = transaction,
|
||||||
|
selectedToken = selectedFeeToken,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return dexFeeResultEither.fold(
|
||||||
ifLeft = { error -> GetFeeError.DataError(error).left() },
|
ifLeft = { error -> GetFeeError.DataError(error).left() },
|
||||||
ifRight = { dexFeeResult ->
|
ifRight = { dexFeeResult ->
|
||||||
val feeToken = selectedFeeToken
|
val feeToken = selectedFeeToken
|
||||||
|
|
@ -1021,6 +1078,42 @@ internal class SwapInteractorImpl @Inject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun createYieldSwapDexTransaction(
|
||||||
|
fromSwapCurrencyStatus: SwapCurrencyStatus,
|
||||||
|
swapData: SwapDataModel,
|
||||||
|
dexCallData: String,
|
||||||
|
amount: BigDecimal,
|
||||||
|
fee: Fee,
|
||||||
|
spenderAddress: String,
|
||||||
|
): Either<Throwable, TransactionData> {
|
||||||
|
val fromCurrency = fromSwapCurrencyStatus.currency as CryptoCurrency.Token
|
||||||
|
val network = fromCurrency.network
|
||||||
|
val yieldModuleAddress = yieldModuleAddressProvider.getOrFetch(fromSwapCurrencyStatus.userWalletId, network)
|
||||||
|
?: return Either.Left(IllegalStateException("Yield module address is not available for ${network.id}"))
|
||||||
|
val wrappedCallData = dexSwapFeeCalculator.buildYieldSwapCallData(
|
||||||
|
fromSwapCurrencyStatus = fromSwapCurrencyStatus,
|
||||||
|
txTo = swapData.transaction.txTo,
|
||||||
|
dexCallData = dexCallData,
|
||||||
|
amount = amount,
|
||||||
|
spenderAddress = spenderAddress,
|
||||||
|
)
|
||||||
|
val txExtras = createTransactionExtrasUseCase(
|
||||||
|
callData = wrappedCallData,
|
||||||
|
network = network,
|
||||||
|
gasLimit = fee.getGasLimit()?.toBigInteger(),
|
||||||
|
).getOrNull() ?: error("Failed to create yield swap extras")
|
||||||
|
|
||||||
|
return createTransactionUseCase(
|
||||||
|
amount = createNativeAmountForDex("0", network),
|
||||||
|
fee = fee,
|
||||||
|
memo = null,
|
||||||
|
destination = yieldModuleAddress,
|
||||||
|
userWalletId = fromSwapCurrencyStatus.userWalletId,
|
||||||
|
network = network,
|
||||||
|
txExtras = txExtras,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* [REDACTED_TASK_KEY] — CEX branch of [loadSwapFee]. Native-fallback behavior is preserved: when
|
* [REDACTED_TASK_KEY] — CEX branch of [loadSwapFee]. Native-fallback behavior is preserved: when
|
||||||
* [selectedFeeToken] is null the gasless use case (invoked inside [CexSwapFeeCalculator])
|
* [selectedFeeToken] is null the gasless use case (invoked inside [CexSwapFeeCalculator])
|
||||||
|
|
@ -1501,6 +1594,7 @@ internal class SwapInteractorImpl @Inject constructor(
|
||||||
toSwapCurrencyStatus: SwapCurrencyStatus,
|
toSwapCurrencyStatus: SwapCurrencyStatus,
|
||||||
amount: SwapAmount,
|
amount: SwapAmount,
|
||||||
expressOperationType: ExpressOperationType,
|
expressOperationType: ExpressOperationType,
|
||||||
|
quoteAllowanceContract: String? = null,
|
||||||
): SwapState {
|
): SwapState {
|
||||||
val fromNetworkAddress = fromSwapCurrencyStatus.status.value.networkAddress
|
val fromNetworkAddress = fromSwapCurrencyStatus.status.value.networkAddress
|
||||||
val dexFromAddress = fromNetworkAddress?.defaultAddress?.value.orEmpty()
|
val dexFromAddress = fromNetworkAddress?.defaultAddress?.value.orEmpty()
|
||||||
|
|
@ -1521,7 +1615,14 @@ internal class SwapInteractorImpl @Inject constructor(
|
||||||
toAddress = dexToAddress,
|
toAddress = dexToAddress,
|
||||||
refundAddress = fromNetworkAddress?.defaultAddress?.value,
|
refundAddress = fromNetworkAddress?.defaultAddress?.value,
|
||||||
expressOperationType = expressOperationType,
|
expressOperationType = expressOperationType,
|
||||||
).fold(
|
).map { swapData ->
|
||||||
|
val dexTx = swapData.transaction as? ExpressTransactionModel.DEX
|
||||||
|
if (dexTx != null && quoteAllowanceContract != null && dexTx.allowanceContract == null) {
|
||||||
|
swapData.copy(transaction = dexTx.copy(allowanceContract = quoteAllowanceContract))
|
||||||
|
} else {
|
||||||
|
swapData
|
||||||
|
}
|
||||||
|
}.fold(
|
||||||
ifRight = { swapData ->
|
ifRight = { swapData ->
|
||||||
val preparedSwapConfigState = PreparedSwapConfigState(
|
val preparedSwapConfigState = PreparedSwapConfigState(
|
||||||
balanceStatus = SwapBalanceStatus.Pending,
|
balanceStatus = SwapBalanceStatus.Pending,
|
||||||
|
|
@ -1640,17 +1741,31 @@ internal class SwapInteractorImpl @Inject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val isYieldSwap = fromSwapCurrencyStatus.isYieldSwapActive && fromToken is CryptoCurrency.Token
|
||||||
|
val spenderAddress = if (isYieldSwap) {
|
||||||
|
yieldModuleAddressProvider.getOrFetch(fromSwapCurrencyStatus.userWalletId, fromToken.network)
|
||||||
|
?: run {
|
||||||
|
TangemLogger.e(
|
||||||
|
"Yield-swap approval skipped: yield-module address unresolved for " +
|
||||||
|
"walletId=${fromSwapCurrencyStatus.userWalletId} network=${fromToken.network.rawId}",
|
||||||
|
)
|
||||||
|
return quotesLoadedState.copy(permissionState = PermissionDataState.Empty)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
requireNotNull(quoteModel.allowanceContract) { "spenderAddress cant be null" }
|
||||||
|
}
|
||||||
|
|
||||||
val allowanceInfo = getAllowanceInfoUseCase(
|
val allowanceInfo = getAllowanceInfoUseCase(
|
||||||
userWalletId = fromSwapCurrencyStatus.userWalletId,
|
userWalletId = fromSwapCurrencyStatus.userWalletId,
|
||||||
cryptoCurrency = fromToken,
|
cryptoCurrency = fromToken,
|
||||||
spenderAddress = requireNotNull(quoteModel.allowanceContract) { "spenderAddress cant be null" },
|
spenderAddress = spenderAddress,
|
||||||
requiredAmount = swapAmount.value,
|
requiredAmount = swapAmount.value,
|
||||||
).getOrNull()
|
).getOrNull()
|
||||||
|
|
||||||
return quotesLoadedState.copy(
|
return quotesLoadedState.copy(
|
||||||
permissionState = PermissionDataState.PermissionRequired(
|
permissionState = PermissionDataState.PermissionRequired(
|
||||||
isResetApproval = allowanceInfo is AllowanceInfo.ResetNeeded,
|
isResetApproval = allowanceInfo is AllowanceInfo.ResetNeeded,
|
||||||
spenderAddress = quoteModel.allowanceContract,
|
spenderAddress = spenderAddress,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import com.tangem.domain.transaction.usecase.gasless.EstimateFeeForGaslessTxUseC
|
||||||
import com.tangem.domain.transaction.usecase.gasless.EstimateFeeForTokenUseCase
|
import com.tangem.domain.transaction.usecase.gasless.EstimateFeeForTokenUseCase
|
||||||
import com.tangem.domain.transaction.usecase.gasless.GetFeeForTokenUseCase
|
import com.tangem.domain.transaction.usecase.gasless.GetFeeForTokenUseCase
|
||||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||||
|
import com.tangem.domain.yield.supply.usecase.WrapYieldSwapCallDataWithUpgradeUseCase
|
||||||
import com.tangem.feature.swap.domain.*
|
import com.tangem.feature.swap.domain.*
|
||||||
import com.tangem.feature.swap.domain.api.SwapFeedbackRepository
|
import com.tangem.feature.swap.domain.api.SwapFeedbackRepository
|
||||||
import com.tangem.feature.swap.domain.api.SwapRepository
|
import com.tangem.feature.swap.domain.api.SwapRepository
|
||||||
|
|
@ -75,6 +76,7 @@ internal class SwapDomainModule {
|
||||||
createTransactionExtrasUseCase: CreateTransactionDataExtrasUseCase,
|
createTransactionExtrasUseCase: CreateTransactionDataExtrasUseCase,
|
||||||
walletManagersFacade: WalletManagersFacade,
|
walletManagersFacade: WalletManagersFacade,
|
||||||
@SwapDexGasLimit patchEthGasLimitForSwap: PatchEthGasLimitForSwap,
|
@SwapDexGasLimit patchEthGasLimitForSwap: PatchEthGasLimitForSwap,
|
||||||
|
wrapYieldSwapCallDataWithUpgradeUseCase: WrapYieldSwapCallDataWithUpgradeUseCase,
|
||||||
): DexSwapFeeCalculator = DexSwapFeeCalculator(
|
): DexSwapFeeCalculator = DexSwapFeeCalculator(
|
||||||
getFeeUseCase = getFeeUseCase,
|
getFeeUseCase = getFeeUseCase,
|
||||||
getEthSpecificFeeUseCase = getEthSpecificFeeUseCase,
|
getEthSpecificFeeUseCase = getEthSpecificFeeUseCase,
|
||||||
|
|
@ -82,6 +84,7 @@ internal class SwapDomainModule {
|
||||||
createTransactionExtrasUseCase = createTransactionExtrasUseCase,
|
createTransactionExtrasUseCase = createTransactionExtrasUseCase,
|
||||||
walletManagersFacade = walletManagersFacade,
|
walletManagersFacade = walletManagersFacade,
|
||||||
patchEthGasLimitForSwap = patchEthGasLimitForSwap,
|
patchEthGasLimitForSwap = patchEthGasLimitForSwap,
|
||||||
|
wrapYieldSwapCallDataWithUpgradeUseCase = wrapYieldSwapCallDataWithUpgradeUseCase,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,13 @@ import com.tangem.blockchain.blockchains.solana.SolanaTransactionHelper
|
||||||
import com.tangem.blockchain.common.Amount
|
import com.tangem.blockchain.common.Amount
|
||||||
import com.tangem.blockchain.common.Blockchain
|
import com.tangem.blockchain.common.Blockchain
|
||||||
import com.tangem.blockchain.common.TransactionData
|
import com.tangem.blockchain.common.TransactionData
|
||||||
|
import com.tangem.blockchain.common.smartcontract.SmartContractCallData
|
||||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||||
|
import com.tangem.blockchain.yieldsupply.providers.YieldModuleUpgradeUnavailableException
|
||||||
|
import com.tangem.blockchain.yieldsupply.providers.YieldModuleVersionIndeterminateException
|
||||||
|
import com.tangem.blockchain.yieldsupply.providers.ethereum.yield.EthereumYieldSupplySwapCallData
|
||||||
import com.tangem.blockchainsdk.utils.fromNetworkId
|
import com.tangem.blockchainsdk.utils.fromNetworkId
|
||||||
|
import com.tangem.common.extensions.hexToBytes
|
||||||
import com.tangem.domain.models.currency.CryptoCurrency
|
import com.tangem.domain.models.currency.CryptoCurrency
|
||||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||||
import com.tangem.domain.models.network.Network
|
import com.tangem.domain.models.network.Network
|
||||||
|
|
@ -19,12 +24,14 @@ import com.tangem.domain.transaction.usecase.GetEthSpecificFeeUseCase
|
||||||
import com.tangem.domain.transaction.usecase.GetFeeUseCase
|
import com.tangem.domain.transaction.usecase.GetFeeUseCase
|
||||||
import com.tangem.domain.transaction.usecase.gasless.GetFeeForTokenUseCase
|
import com.tangem.domain.transaction.usecase.gasless.GetFeeForTokenUseCase
|
||||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||||
|
import com.tangem.domain.yield.supply.usecase.WrapYieldSwapCallDataWithUpgradeUseCase
|
||||||
import com.tangem.feature.swap.domain.models.ExpressDataError
|
import com.tangem.feature.swap.domain.models.ExpressDataError
|
||||||
import com.tangem.feature.swap.domain.models.domain.ExpressTransactionModel
|
import com.tangem.feature.swap.domain.models.domain.ExpressTransactionModel
|
||||||
import com.tangem.lib.crypto.BlockchainUtils.SOLANA_TRANSACTION_SIZE_THRESHOLD_BYTES
|
import com.tangem.lib.crypto.BlockchainUtils.SOLANA_TRANSACTION_SIZE_THRESHOLD_BYTES
|
||||||
import com.tangem.lib.crypto.BlockchainUtils.isSolana
|
import com.tangem.lib.crypto.BlockchainUtils.isSolana
|
||||||
import com.tangem.utils.logging.TangemLogger
|
import com.tangem.utils.logging.TangemLogger
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
|
import java.math.BigInteger
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculates the on-chain transaction fee for a DEX swap.
|
* Calculates the on-chain transaction fee for a DEX swap.
|
||||||
|
|
@ -52,6 +59,7 @@ class DexSwapFeeCalculator(
|
||||||
private val createTransactionExtrasUseCase: CreateTransactionDataExtrasUseCase,
|
private val createTransactionExtrasUseCase: CreateTransactionDataExtrasUseCase,
|
||||||
private val walletManagersFacade: WalletManagersFacade,
|
private val walletManagersFacade: WalletManagersFacade,
|
||||||
private val patchEthGasLimitForSwap: PatchEthGasLimitForSwap,
|
private val patchEthGasLimitForSwap: PatchEthGasLimitForSwap,
|
||||||
|
private val wrapYieldSwapCallDataWithUpgradeUseCase: WrapYieldSwapCallDataWithUpgradeUseCase,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend fun calculate(
|
suspend fun calculate(
|
||||||
|
|
@ -115,6 +123,134 @@ class DexSwapFeeCalculator(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Yield-mode DEX fee path: routes the swap through the user's yield module proxy.
|
||||||
|
*
|
||||||
|
* Native fee is computed for a [TransactionData.Uncompiled] addressed to [yieldModuleAddress],
|
||||||
|
* carrying the wrapped call data produced by [buildYieldSwapCallData]. The 12% gas-limit bump
|
||||||
|
* is applied to match the non-yield DEX flow.
|
||||||
|
*
|
||||||
|
* Fallback to [GetEthSpecificFeeUseCase] (with the gas limit carried by the Express transaction
|
||||||
|
* model) is applied in two cases:
|
||||||
|
* - [yieldModuleAddress] is `null` — yield module address could not be resolved upstream;
|
||||||
|
* - the fee estimation call throws `IllegalStateException` (e.g. payload too large).
|
||||||
|
*
|
||||||
|
* Yield-module errors ([YieldModuleUpgradeUnavailableException],
|
||||||
|
* [YieldModuleVersionIndeterminateException]) are mapped to [ExpressDataError.UnknownError]
|
||||||
|
* to keep the unified error surface a single type.
|
||||||
|
*/
|
||||||
|
suspend fun calculateYield(
|
||||||
|
fromSwapCurrencyStatus: SwapCurrencyStatus,
|
||||||
|
transaction: ExpressTransactionModel.DEX,
|
||||||
|
yieldModuleAddress: String?,
|
||||||
|
): Either<ExpressDataError, DexFeeResult> = either {
|
||||||
|
val fromCurrency = fromSwapCurrencyStatus.currency as? CryptoCurrency.Token
|
||||||
|
?: raise(ExpressDataError.UnknownError())
|
||||||
|
val network = fromCurrency.network
|
||||||
|
|
||||||
|
val nativeBalance = walletManagersFacade.getNativeTokenBalance(
|
||||||
|
userWalletId = fromSwapCurrencyStatus.userWalletId,
|
||||||
|
networkId = network.rawId,
|
||||||
|
derivationPath = network.derivationPath.value,
|
||||||
|
)
|
||||||
|
if (nativeBalance.signum() == 0) raise(ExpressDataError.UnknownError())
|
||||||
|
|
||||||
|
if (yieldModuleAddress == null) {
|
||||||
|
val gasLimit = transaction.gas ?: raise(ExpressDataError.UnknownError())
|
||||||
|
return@either ethSpecificFeeFallback(fromSwapCurrencyStatus, gasLimit).bind()
|
||||||
|
}
|
||||||
|
|
||||||
|
val spenderAddress = transaction.allowanceContract
|
||||||
|
?: raise(ExpressDataError.UnknownError())
|
||||||
|
|
||||||
|
val rawFee = try {
|
||||||
|
val wrappedCallData = buildYieldSwapCallData(
|
||||||
|
fromSwapCurrencyStatus = fromSwapCurrencyStatus,
|
||||||
|
txTo = transaction.txTo,
|
||||||
|
dexCallData = transaction.txData,
|
||||||
|
amount = transaction.fromAmount.value,
|
||||||
|
spenderAddress = spenderAddress,
|
||||||
|
)
|
||||||
|
val extras = createTransactionExtrasUseCase(
|
||||||
|
callData = wrappedCallData,
|
||||||
|
network = network,
|
||||||
|
).getOrNull() ?: raise(ExpressDataError.UnknownError())
|
||||||
|
|
||||||
|
val transactionData = TransactionData.Uncompiled(
|
||||||
|
amount = createNativeAmountForDex("0", network),
|
||||||
|
destinationAddress = yieldModuleAddress,
|
||||||
|
fee = null,
|
||||||
|
sourceAddress = transaction.txFrom,
|
||||||
|
extras = extras,
|
||||||
|
)
|
||||||
|
getFeeUseCase(
|
||||||
|
transactionData = transactionData,
|
||||||
|
network = network,
|
||||||
|
userWallet = fromSwapCurrencyStatus.userWallet,
|
||||||
|
).getOrNull() ?: raise(ExpressDataError.UnknownError())
|
||||||
|
} catch (_: YieldModuleUpgradeUnavailableException) {
|
||||||
|
raise(ExpressDataError.UnknownError())
|
||||||
|
} catch (_: YieldModuleVersionIndeterminateException) {
|
||||||
|
raise(ExpressDataError.UnknownError())
|
||||||
|
} catch (_: IllegalStateException) {
|
||||||
|
val gasLimit = transaction.gas ?: raise(ExpressDataError.UnknownError())
|
||||||
|
return@either ethSpecificFeeFallback(fromSwapCurrencyStatus, gasLimit).bind()
|
||||||
|
}
|
||||||
|
|
||||||
|
val patched = patchEthGasLimitForSwap(rawFee)
|
||||||
|
DexFeeResult(
|
||||||
|
transactionFee = TransactionFeeResult.Loaded(patched),
|
||||||
|
otherNativeFee = BigDecimal.ZERO,
|
||||||
|
gas = transaction.gas,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wraps a DEX call data into a yield-supply swap call data, ready to be sent through the
|
||||||
|
* user's yield module. Shared with [SwapInteractorImpl.createYieldSwapDexTransaction], which
|
||||||
|
* is why this helper is exposed at the calculator level rather than kept private.
|
||||||
|
*/
|
||||||
|
suspend fun buildYieldSwapCallData(
|
||||||
|
fromSwapCurrencyStatus: SwapCurrencyStatus,
|
||||||
|
txTo: String,
|
||||||
|
dexCallData: String,
|
||||||
|
amount: BigDecimal,
|
||||||
|
spenderAddress: String,
|
||||||
|
): SmartContractCallData {
|
||||||
|
val fromCurrency = fromSwapCurrencyStatus.currency as CryptoCurrency.Token
|
||||||
|
val amountInWei = amount.movePointRight(fromCurrency.decimals).toBigInteger()
|
||||||
|
val dexCallDataBytes = dexCallData.removePrefix("0x").hexToBytes()
|
||||||
|
val swapCallData = EthereumYieldSupplySwapCallData(
|
||||||
|
tokenIn = fromCurrency.contractAddress,
|
||||||
|
amountIn = amountInWei,
|
||||||
|
target = txTo,
|
||||||
|
spender = spenderAddress,
|
||||||
|
swapData = dexCallDataBytes,
|
||||||
|
)
|
||||||
|
return wrapYieldSwapCallDataWithUpgradeUseCase(
|
||||||
|
userWalletId = fromSwapCurrencyStatus.userWalletId,
|
||||||
|
network = fromCurrency.network,
|
||||||
|
callData = swapCallData,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun ethSpecificFeeFallback(
|
||||||
|
fromSwapCurrencyStatus: SwapCurrencyStatus,
|
||||||
|
gasLimit: BigInteger,
|
||||||
|
): Either<ExpressDataError, DexFeeResult> = either {
|
||||||
|
val fee = getEthSpecificFeeUseCase(
|
||||||
|
userWallet = fromSwapCurrencyStatus.userWallet,
|
||||||
|
cryptoCurrency = fromSwapCurrencyStatus.currency,
|
||||||
|
gasLimit = gasLimit,
|
||||||
|
).getOrNull() ?: raise(ExpressDataError.UnknownError())
|
||||||
|
val patched = patchEthGasLimitForSwap(fee)
|
||||||
|
DexFeeResult(
|
||||||
|
transactionFee = TransactionFeeResult.Loaded(patched),
|
||||||
|
otherNativeFee = BigDecimal.ZERO,
|
||||||
|
gas = gasLimit,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Suppress("CyclomaticComplexMethod")
|
@Suppress("CyclomaticComplexMethod")
|
||||||
private suspend fun getFeeDataForDexSwap(
|
private suspend fun getFeeDataForDexSwap(
|
||||||
fromSwapCurrencyStatus: SwapCurrencyStatus,
|
fromSwapCurrencyStatus: SwapCurrencyStatus,
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ sealed class ExpressTransactionModel {
|
||||||
val txData: String,
|
val txData: String,
|
||||||
val otherNativeFeeWei: BigDecimal?,
|
val otherNativeFeeWei: BigDecimal?,
|
||||||
val gas: BigInteger?,
|
val gas: BigInteger?,
|
||||||
val allowanceContract: String?,
|
val allowanceContract: String? = null,
|
||||||
) : ExpressTransactionModel()
|
) : ExpressTransactionModel()
|
||||||
|
|
||||||
data class CEX(
|
data class CEX(
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import com.tangem.feature.swap.domain.models.SwapAmount
|
||||||
import com.tangem.feature.swap.domain.models.domain.ExchangeProviderType
|
import com.tangem.feature.swap.domain.models.domain.ExchangeProviderType
|
||||||
import com.tangem.feature.swap.domain.models.domain.ExpressTransactionModel
|
import com.tangem.feature.swap.domain.models.domain.ExpressTransactionModel
|
||||||
import com.tangem.feature.swap.domain.models.domain.SwapDataModel
|
import com.tangem.feature.swap.domain.models.domain.SwapDataModel
|
||||||
|
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.SwapState
|
||||||
import io.mockk.coEvery
|
import io.mockk.coEvery
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
|
|
@ -872,6 +873,210 @@ internal class SwapInteractorImplFindBestQuoteTest : SwapInteractorImplTestBase(
|
||||||
assertThat(result[cexProvider]).isInstanceOf(SwapState.QuotesLoadedState::class.java)
|
assertThat(result[cexProvider]).isInstanceOf(SwapState.QuotesLoadedState::class.java)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
inner class YieldSwapApprovalPath {
|
||||||
|
|
||||||
|
private val yieldProxyAddress = "0xYieldModuleProxy"
|
||||||
|
private val yieldTokenContract = "0xTokenContract"
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
fun enableYieldSwap() {
|
||||||
|
every { swapFeatureToggles.isYieldSwapEnabled } returns true
|
||||||
|
coEvery {
|
||||||
|
yieldModuleAddressProvider.getOrFetch(any(), any())
|
||||||
|
} returns yieldProxyAddress
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `should proceed to QuotesLoadedState when yield-supply is active and isAllowedToSpend is true`() = runTest {
|
||||||
|
// Given — yield active, approve to proxy in place → swap proceeds via loadDexSwapDataNoFee
|
||||||
|
val dexProvider = buildSwapProvider(ExchangeProviderType.DEX)
|
||||||
|
val fromStatus = buildSwapCurrencyStatus(
|
||||||
|
networkRawId = ethNetwork,
|
||||||
|
contractAddress = yieldTokenContract,
|
||||||
|
isCoin = false,
|
||||||
|
amount = BigDecimal("10"),
|
||||||
|
yieldSupplyActive = true,
|
||||||
|
yieldSupplyAllowedToSpend = true,
|
||||||
|
)
|
||||||
|
val toStatus = buildSwapCurrencyStatus(networkRawId = btcNetwork)
|
||||||
|
val quoteModel = buildQuoteModel()
|
||||||
|
val swapData = buildSwapDataModelDex()
|
||||||
|
|
||||||
|
coEvery {
|
||||||
|
repository.findBestQuote(
|
||||||
|
userWallet = any(), fromContractAddress = any(), fromNetwork = any(),
|
||||||
|
toContractAddress = any(), toNetwork = any(), fromAmount = any(),
|
||||||
|
fromDecimals = any(), toDecimals = any(),
|
||||||
|
providerId = dexProvider.providerId, rateType = any(),
|
||||||
|
)
|
||||||
|
} returns quoteModel.right()
|
||||||
|
coEvery {
|
||||||
|
repository.getExchangeData(
|
||||||
|
userWallet = any(), fromContractAddress = any(), fromNetwork = any(),
|
||||||
|
toContractAddress = any(), fromAddress = any(), toNetwork = any(),
|
||||||
|
fromAmount = any(), fromDecimals = any(), toDecimals = any(),
|
||||||
|
providerId = dexProvider.providerId, rateType = any(), toAddress = any(),
|
||||||
|
expressOperationType = any(), refundAddress = any(),
|
||||||
|
)
|
||||||
|
} returns swapData.right()
|
||||||
|
|
||||||
|
// When
|
||||||
|
val result = sut.findBestQuote(
|
||||||
|
fromSwapCurrencyStatus = fromStatus,
|
||||||
|
toSwapCurrencyStatus = toStatus,
|
||||||
|
providers = listOf(dexProvider),
|
||||||
|
amountToSwap = "1.0",
|
||||||
|
reduceBalanceBy = BigDecimal.ZERO,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Then — proceeds (no PermissionRequired), permissionState is Empty
|
||||||
|
val state = result[dexProvider]
|
||||||
|
assertThat(state).isInstanceOf(SwapState.QuotesLoadedState::class.java)
|
||||||
|
val loaded = state as SwapState.QuotesLoadedState
|
||||||
|
assertThat(loaded.permissionState).isEqualTo(PermissionDataState.Empty)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `should request approval to yield-module proxy when isAllowedToSpend is false`() = runTest {
|
||||||
|
// Given — yield active, approve to proxy revoked → flow must surface PermissionRequired
|
||||||
|
val dexProvider = buildSwapProvider(ExchangeProviderType.DEX)
|
||||||
|
val fromStatus = buildSwapCurrencyStatus(
|
||||||
|
networkRawId = ethNetwork,
|
||||||
|
contractAddress = yieldTokenContract,
|
||||||
|
isCoin = false,
|
||||||
|
amount = BigDecimal("10"),
|
||||||
|
yieldSupplyActive = true,
|
||||||
|
yieldSupplyAllowedToSpend = false,
|
||||||
|
)
|
||||||
|
val toStatus = buildSwapCurrencyStatus(networkRawId = btcNetwork)
|
||||||
|
val quoteModel = buildQuoteModel(allowanceContract = "0xDexRouterShouldNotBeUsed")
|
||||||
|
|
||||||
|
coEvery {
|
||||||
|
repository.findBestQuote(
|
||||||
|
userWallet = any(), fromContractAddress = any(), fromNetwork = any(),
|
||||||
|
toContractAddress = any(), toNetwork = any(), fromAmount = any(),
|
||||||
|
fromDecimals = any(), toDecimals = any(),
|
||||||
|
providerId = dexProvider.providerId, rateType = any(),
|
||||||
|
)
|
||||||
|
} returns quoteModel.right()
|
||||||
|
|
||||||
|
// When
|
||||||
|
val result = sut.findBestQuote(
|
||||||
|
fromSwapCurrencyStatus = fromStatus,
|
||||||
|
toSwapCurrencyStatus = toStatus,
|
||||||
|
providers = listOf(dexProvider),
|
||||||
|
amountToSwap = "1.0",
|
||||||
|
reduceBalanceBy = BigDecimal.ZERO,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Then — PermissionRequired with spender = yield-module proxy (not DEX router)
|
||||||
|
val state = result[dexProvider]
|
||||||
|
assertThat(state).isInstanceOf(SwapState.QuotesLoadedState::class.java)
|
||||||
|
val loaded = state as SwapState.QuotesLoadedState
|
||||||
|
assertThat(loaded.permissionState).isInstanceOf(PermissionDataState.PermissionRequired::class.java)
|
||||||
|
val required = loaded.permissionState as PermissionDataState.PermissionRequired
|
||||||
|
assertThat(required.spenderAddress).isEqualTo(yieldProxyAddress)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `should set isResetApproval=true when yield-token allowance requires reset before re-approval`() = runTest {
|
||||||
|
// Given — Tether-like token: any non-zero allowance must be reset to zero before re-approve.
|
||||||
|
// Yield approve to proxy was revoked → onchain allowance is partial → ResetNeeded.
|
||||||
|
val dexProvider = buildSwapProvider(ExchangeProviderType.DEX)
|
||||||
|
val fromStatus = buildSwapCurrencyStatus(
|
||||||
|
networkRawId = ethNetwork,
|
||||||
|
contractAddress = yieldTokenContract,
|
||||||
|
isCoin = false,
|
||||||
|
amount = BigDecimal("10"),
|
||||||
|
yieldSupplyActive = true,
|
||||||
|
yieldSupplyAllowedToSpend = false,
|
||||||
|
)
|
||||||
|
val toStatus = buildSwapCurrencyStatus(networkRawId = btcNetwork)
|
||||||
|
val quoteModel = buildQuoteModel(allowanceContract = "0xDexRouterIgnoredForYield")
|
||||||
|
coEvery {
|
||||||
|
repository.findBestQuote(
|
||||||
|
userWallet = any(), fromContractAddress = any(), fromNetwork = any(),
|
||||||
|
toContractAddress = any(), toNetwork = any(), fromAmount = any(),
|
||||||
|
fromDecimals = any(), toDecimals = any(),
|
||||||
|
providerId = dexProvider.providerId, rateType = any(),
|
||||||
|
)
|
||||||
|
} returns quoteModel.right()
|
||||||
|
// Override default Enough stub: simulate partial-allowance state for yield-proxy spender.
|
||||||
|
coEvery {
|
||||||
|
getAllowanceInfoUseCase.invoke(
|
||||||
|
userWalletId = any(),
|
||||||
|
cryptoCurrency = any(),
|
||||||
|
spenderAddress = yieldProxyAddress,
|
||||||
|
requiredAmount = any(),
|
||||||
|
)
|
||||||
|
} returns (
|
||||||
|
AllowanceInfo.ResetNeeded(
|
||||||
|
allowance = BigDecimal("0.5"),
|
||||||
|
requiredAmount = BigDecimal("1"),
|
||||||
|
) as AllowanceInfo
|
||||||
|
).right()
|
||||||
|
|
||||||
|
// When
|
||||||
|
val result = sut.findBestQuote(
|
||||||
|
fromSwapCurrencyStatus = fromStatus,
|
||||||
|
toSwapCurrencyStatus = toStatus,
|
||||||
|
providers = listOf(dexProvider),
|
||||||
|
amountToSwap = "1.0",
|
||||||
|
reduceBalanceBy = BigDecimal.ZERO,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Then — PermissionRequired with isResetApproval=true and spender = yield-module proxy
|
||||||
|
val state = result[dexProvider]
|
||||||
|
assertThat(state).isInstanceOf(SwapState.QuotesLoadedState::class.java)
|
||||||
|
val loaded = state as SwapState.QuotesLoadedState
|
||||||
|
assertThat(loaded.permissionState).isInstanceOf(PermissionDataState.PermissionRequired::class.java)
|
||||||
|
val required = loaded.permissionState as PermissionDataState.PermissionRequired
|
||||||
|
assertThat(required.spenderAddress).isEqualTo(yieldProxyAddress)
|
||||||
|
assertThat(required.isResetApproval).isTrue()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `should fallback to no-permission state when yield-module proxy address is unresolvable`() = runTest {
|
||||||
|
// Given — yield store returns null (e.g. network unreachable on first resolve)
|
||||||
|
coEvery { yieldModuleAddressProvider.getOrFetch(any(), any()) } returns null
|
||||||
|
val dexProvider = buildSwapProvider(ExchangeProviderType.DEX)
|
||||||
|
val fromStatus = buildSwapCurrencyStatus(
|
||||||
|
networkRawId = ethNetwork,
|
||||||
|
contractAddress = yieldTokenContract,
|
||||||
|
isCoin = false,
|
||||||
|
amount = BigDecimal("10"),
|
||||||
|
yieldSupplyActive = true,
|
||||||
|
yieldSupplyAllowedToSpend = false,
|
||||||
|
)
|
||||||
|
val toStatus = buildSwapCurrencyStatus(networkRawId = btcNetwork)
|
||||||
|
val quoteModel = buildQuoteModel(allowanceContract = "0xDexRouter")
|
||||||
|
coEvery {
|
||||||
|
repository.findBestQuote(
|
||||||
|
userWallet = any(), fromContractAddress = any(), fromNetwork = any(),
|
||||||
|
toContractAddress = any(), toNetwork = any(), fromAmount = any(),
|
||||||
|
fromDecimals = any(), toDecimals = any(),
|
||||||
|
providerId = dexProvider.providerId, rateType = any(),
|
||||||
|
)
|
||||||
|
} returns quoteModel.right()
|
||||||
|
|
||||||
|
// When
|
||||||
|
val result = sut.findBestQuote(
|
||||||
|
fromSwapCurrencyStatus = fromStatus,
|
||||||
|
toSwapCurrencyStatus = toStatus,
|
||||||
|
providers = listOf(dexProvider),
|
||||||
|
amountToSwap = "1.0",
|
||||||
|
reduceBalanceBy = BigDecimal.ZERO,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Then — falls back to PermissionDataState.Empty (no approval UI shown to avoid bogus DEX-router approve)
|
||||||
|
val state = result[dexProvider]
|
||||||
|
assertThat(state).isInstanceOf(SwapState.QuotesLoadedState::class.java)
|
||||||
|
val loaded = state as SwapState.QuotesLoadedState
|
||||||
|
assertThat(loaded.permissionState).isEqualTo(PermissionDataState.Empty)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// region — test-local helpers
|
// region — test-local helpers
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ import com.tangem.domain.tokens.repository.CurrencyChecksRepository
|
||||||
import com.tangem.domain.transaction.usecase.*
|
import com.tangem.domain.transaction.usecase.*
|
||||||
import com.tangem.domain.transaction.usecase.gasless.CreateAndSendGaslessTransactionUseCase
|
import com.tangem.domain.transaction.usecase.gasless.CreateAndSendGaslessTransactionUseCase
|
||||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||||
|
import com.tangem.domain.yield.supply.YieldModuleAddressProvider
|
||||||
import com.tangem.feature.swap.domain.api.SwapRepository
|
import com.tangem.feature.swap.domain.api.SwapRepository
|
||||||
import com.tangem.feature.swap.domain.fee.CexSwapFeeCalculator
|
import com.tangem.feature.swap.domain.fee.CexSwapFeeCalculator
|
||||||
import com.tangem.feature.swap.domain.fee.DexSwapFeeCalculator
|
import com.tangem.feature.swap.domain.fee.DexSwapFeeCalculator
|
||||||
|
|
@ -41,6 +42,7 @@ import com.tangem.feature.swap.domain.models.SwapAmount
|
||||||
import com.tangem.feature.swap.domain.models.domain.*
|
import com.tangem.feature.swap.domain.models.domain.*
|
||||||
import com.tangem.feature.swap.domain.models.ui.AmountFormatter
|
import com.tangem.feature.swap.domain.models.ui.AmountFormatter
|
||||||
import com.tangem.feature.swap.domain.models.ui.SwapFee
|
import com.tangem.feature.swap.domain.models.ui.SwapFee
|
||||||
|
import com.tangem.features.swap.SwapFeatureToggles
|
||||||
import io.mockk.clearAllMocks
|
import io.mockk.clearAllMocks
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
|
|
@ -84,6 +86,8 @@ internal open class SwapInteractorImplTestBase {
|
||||||
protected val getSwapPairUseCase: GetSwapPairUseCase = mockk(relaxed = true)
|
protected val getSwapPairUseCase: GetSwapPairUseCase = mockk(relaxed = true)
|
||||||
protected val dexSwapFeeCalculator: DexSwapFeeCalculator = mockk(relaxed = true)
|
protected val dexSwapFeeCalculator: DexSwapFeeCalculator = mockk(relaxed = true)
|
||||||
protected val cexSwapFeeCalculator: CexSwapFeeCalculator = mockk(relaxed = true)
|
protected val cexSwapFeeCalculator: CexSwapFeeCalculator = mockk(relaxed = true)
|
||||||
|
protected val swapFeatureToggles: SwapFeatureToggles = mockk(relaxed = true)
|
||||||
|
protected val yieldModuleAddressProvider: YieldModuleAddressProvider = mockk(relaxed = true)
|
||||||
|
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
|
|
@ -115,6 +119,8 @@ internal open class SwapInteractorImplTestBase {
|
||||||
getSwapPairUseCase = getSwapPairUseCase,
|
getSwapPairUseCase = getSwapPairUseCase,
|
||||||
dexSwapFeeCalculator = dexSwapFeeCalculator,
|
dexSwapFeeCalculator = dexSwapFeeCalculator,
|
||||||
cexSwapFeeCalculator = cexSwapFeeCalculator,
|
cexSwapFeeCalculator = cexSwapFeeCalculator,
|
||||||
|
swapFeatureToggles = swapFeatureToggles,
|
||||||
|
yieldModuleAddressProvider = yieldModuleAddressProvider,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -158,6 +164,7 @@ internal fun buildSwapCurrencyStatus(
|
||||||
decimals: Int = 18,
|
decimals: Int = 18,
|
||||||
userWalletId: UserWalletId = UserWalletId(stringValue = "deadbeef"),
|
userWalletId: UserWalletId = UserWalletId(stringValue = "deadbeef"),
|
||||||
yieldSupplyActive: Boolean = false,
|
yieldSupplyActive: Boolean = false,
|
||||||
|
yieldSupplyAllowedToSpend: Boolean = true,
|
||||||
): SwapCurrencyStatus {
|
): SwapCurrencyStatus {
|
||||||
val networkId = mockk<Network.ID>(relaxed = true) {
|
val networkId = mockk<Network.ID>(relaxed = true) {
|
||||||
every { rawId } returns Network.RawID(networkRawId)
|
every { rawId } returns Network.RawID(networkRawId)
|
||||||
|
|
@ -196,6 +203,7 @@ internal fun buildSwapCurrencyStatus(
|
||||||
val maybeYield: YieldSupplyStatus? = if (yieldSupplyActive) {
|
val maybeYield: YieldSupplyStatus? = if (yieldSupplyActive) {
|
||||||
mockk<YieldSupplyStatus>(relaxed = true) {
|
mockk<YieldSupplyStatus>(relaxed = true) {
|
||||||
every { isActive } returns true
|
every { isActive } returns true
|
||||||
|
every { isAllowedToSpend } returns yieldSupplyAllowedToSpend
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import com.tangem.domain.transaction.usecase.GetEthSpecificFeeUseCase
|
||||||
import com.tangem.domain.transaction.usecase.GetFeeUseCase
|
import com.tangem.domain.transaction.usecase.GetFeeUseCase
|
||||||
import com.tangem.domain.transaction.usecase.gasless.GetFeeForTokenUseCase
|
import com.tangem.domain.transaction.usecase.gasless.GetFeeForTokenUseCase
|
||||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||||
|
import com.tangem.domain.yield.supply.usecase.WrapYieldSwapCallDataWithUpgradeUseCase
|
||||||
import com.tangem.feature.swap.domain.buildSwapCurrencyStatus
|
import com.tangem.feature.swap.domain.buildSwapCurrencyStatus
|
||||||
import com.tangem.feature.swap.domain.models.ExpressDataError
|
import com.tangem.feature.swap.domain.models.ExpressDataError
|
||||||
import com.tangem.feature.swap.domain.models.SwapAmount
|
import com.tangem.feature.swap.domain.models.SwapAmount
|
||||||
|
|
@ -59,6 +60,7 @@ internal class DexSwapFeeCalculatorTest {
|
||||||
private val getFeeForTokenUseCase: GetFeeForTokenUseCase = mockk(relaxed = true)
|
private val getFeeForTokenUseCase: GetFeeForTokenUseCase = mockk(relaxed = true)
|
||||||
private val createTransactionExtrasUseCase: CreateTransactionDataExtrasUseCase = mockk(relaxed = true)
|
private val createTransactionExtrasUseCase: CreateTransactionDataExtrasUseCase = mockk(relaxed = true)
|
||||||
private val walletManagersFacade: WalletManagersFacade = mockk(relaxed = true)
|
private val walletManagersFacade: WalletManagersFacade = mockk(relaxed = true)
|
||||||
|
private val wrapYieldSwapCallDataWithUpgradeUseCase: WrapYieldSwapCallDataWithUpgradeUseCase = mockk(relaxed = true)
|
||||||
|
|
||||||
private val dexBump = PatchEthGasLimitForSwap(percentage = PatchEthGasLimitForSwap.DEX_PERCENTAGE)
|
private val dexBump = PatchEthGasLimitForSwap(percentage = PatchEthGasLimitForSwap.DEX_PERCENTAGE)
|
||||||
|
|
||||||
|
|
@ -70,6 +72,7 @@ internal class DexSwapFeeCalculatorTest {
|
||||||
createTransactionExtrasUseCase = createTransactionExtrasUseCase,
|
createTransactionExtrasUseCase = createTransactionExtrasUseCase,
|
||||||
walletManagersFacade = walletManagersFacade,
|
walletManagersFacade = walletManagersFacade,
|
||||||
patchEthGasLimitForSwap = dexBump,
|
patchEthGasLimitForSwap = dexBump,
|
||||||
|
wrapYieldSwapCallDataWithUpgradeUseCase = wrapYieldSwapCallDataWithUpgradeUseCase,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,10 @@ internal class DefaultSwapFeatureToggles @Inject constructor(
|
||||||
featureTogglesManager: FeatureTogglesManager,
|
featureTogglesManager: FeatureTogglesManager,
|
||||||
) : SwapFeatureToggles {
|
) : SwapFeatureToggles {
|
||||||
|
|
||||||
|
override val isYieldSwapEnabled: Boolean = featureTogglesManager.isFeatureEnabled(
|
||||||
|
toggle = FeatureToggles.TWI_1326_YIELD_MODE_SWAP_ENABLED,
|
||||||
|
)
|
||||||
|
|
||||||
override val isSwapSwitchToTransferEnabled: Boolean = featureTogglesManager.isFeatureEnabled(
|
override val isSwapSwitchToTransferEnabled: Boolean = featureTogglesManager.isFeatureEnabled(
|
||||||
toggle = FeatureToggles.AND_15207_SWAP_SWITCH_TO_TRANSFER_ENABLED,
|
toggle = FeatureToggles.AND_15207_SWAP_SWITCH_TO_TRANSFER_ENABLED,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
# https://github.com/tangem/tangem-sdk-android/
|
# https://github.com/tangem/tangem-sdk-android/
|
||||||
# https://github.com/tangem/vico
|
# https://github.com/tangem/vico
|
||||||
|
|
||||||
tangemBlockchainSdk = "develop-1527"
|
tangemBlockchainSdk = "develop-1532"
|
||||||
#tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds
|
#tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds
|
||||||
tangemCardSdk = "develop-620"
|
tangemCardSdk = "develop-620"
|
||||||
#tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^
|
#tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import javax.inject.Inject
|
||||||
* @property accountCreator account creator
|
* @property accountCreator account creator
|
||||||
* @property blockchainDataStorage blockchain data storage
|
* @property blockchainDataStorage blockchain data storage
|
||||||
* @property blockchainSDKLogger blockchain SDK logger
|
* @property blockchainSDKLogger blockchain SDK logger
|
||||||
|
* @property featureToggleValues blockchain feature toggle values
|
||||||
*
|
*
|
||||||
[REDACTED_AUTHOR]
|
[REDACTED_AUTHOR]
|
||||||
*/
|
*/
|
||||||
|
|
@ -23,9 +24,7 @@ internal class WalletManagerFactoryCreator @Inject constructor(
|
||||||
private val accountCreator: AccountCreator,
|
private val accountCreator: AccountCreator,
|
||||||
private val blockchainDataStorage: BlockchainDataStorage,
|
private val blockchainDataStorage: BlockchainDataStorage,
|
||||||
private val blockchainSDKLogger: BlockchainSDKLogger,
|
private val blockchainSDKLogger: BlockchainSDKLogger,
|
||||||
private val isSolanaTxHistoryEnabled: Boolean,
|
private val featureToggleValues: FeatureToggleValues,
|
||||||
private val isSolanaScaledUiAmountEnabled: Boolean,
|
|
||||||
private val isHederaErc20Enabled: Boolean,
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun create(config: BlockchainSdkConfig, blockchainProviderTypes: BlockchainProviderTypes): WalletManagerFactory {
|
fun create(config: BlockchainSdkConfig, blockchainProviderTypes: BlockchainProviderTypes): WalletManagerFactory {
|
||||||
|
|
@ -37,13 +36,21 @@ internal class WalletManagerFactoryCreator @Inject constructor(
|
||||||
accountCreator = accountCreator,
|
accountCreator = accountCreator,
|
||||||
featureToggles = BlockchainFeatureToggles(
|
featureToggles = BlockchainFeatureToggles(
|
||||||
isYieldSupplyEnabled = true,
|
isYieldSupplyEnabled = true,
|
||||||
|
isYieldModeSwapEnabled = featureToggleValues.isYieldModeSwapEnabled,
|
||||||
isPendingTransactionsEnabled = true,
|
isPendingTransactionsEnabled = true,
|
||||||
isSolanaTxHistoryEnabled = isSolanaTxHistoryEnabled,
|
isSolanaTxHistoryEnabled = featureToggleValues.isSolanaTxHistoryEnabled,
|
||||||
isSolanaScaledUiAmountEnabled = isSolanaScaledUiAmountEnabled,
|
isSolanaScaledUiAmountEnabled = featureToggleValues.isSolanaScaledUiAmountEnabled,
|
||||||
isHederaErc20Enabled = isHederaErc20Enabled,
|
isHederaErc20Enabled = featureToggleValues.isHederaErc20Enabled,
|
||||||
),
|
),
|
||||||
blockchainDataStorage = blockchainDataStorage,
|
blockchainDataStorage = blockchainDataStorage,
|
||||||
loggers = listOf(blockchainSDKLogger),
|
loggers = listOf(blockchainSDKLogger),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class FeatureToggleValues(
|
||||||
|
val isSolanaTxHistoryEnabled: Boolean,
|
||||||
|
val isSolanaScaledUiAmountEnabled: Boolean,
|
||||||
|
val isYieldModeSwapEnabled: Boolean,
|
||||||
|
val isHederaErc20Enabled: Boolean,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -97,14 +97,19 @@ internal object BlockchainSDKFactoryModule {
|
||||||
accountCreator = DefaultAccountCreator(tangemTechApi),
|
accountCreator = DefaultAccountCreator(tangemTechApi),
|
||||||
blockchainDataStorage = DefaultBlockchainDataStorage(appPreferencesStore),
|
blockchainDataStorage = DefaultBlockchainDataStorage(appPreferencesStore),
|
||||||
blockchainSDKLogger = blockchainSDKLogger,
|
blockchainSDKLogger = blockchainSDKLogger,
|
||||||
isSolanaTxHistoryEnabled = featureTogglesManager.isFeatureEnabled(
|
featureToggleValues = WalletManagerFactoryCreator.FeatureToggleValues(
|
||||||
FeatureToggles.SOLANA_TX_HISTORY_ENABLED,
|
isSolanaTxHistoryEnabled = featureTogglesManager.isFeatureEnabled(
|
||||||
),
|
FeatureToggles.SOLANA_TX_HISTORY_ENABLED,
|
||||||
isSolanaScaledUiAmountEnabled = featureTogglesManager.isFeatureEnabled(
|
),
|
||||||
FeatureToggles.SOLANA_SCALED_UI_AMOUNT_ENABLED,
|
isSolanaScaledUiAmountEnabled = featureTogglesManager.isFeatureEnabled(
|
||||||
),
|
FeatureToggles.SOLANA_SCALED_UI_AMOUNT_ENABLED,
|
||||||
isHederaErc20Enabled = featureTogglesManager.isFeatureEnabled(
|
),
|
||||||
FeatureToggles.HEDERA_ERC20_ENABLED,
|
isYieldModeSwapEnabled = featureTogglesManager.isFeatureEnabled(
|
||||||
|
FeatureToggles.TWI_1326_YIELD_MODE_SWAP_ENABLED,
|
||||||
|
),
|
||||||
|
isHederaErc20Enabled = featureTogglesManager.isFeatureEnabled(
|
||||||
|
FeatureToggles.HEDERA_ERC20_ENABLED,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue