diff --git a/app/src/main/java/com/tangem/tap/di/domain/YieldSupplyDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/YieldSupplyDomainModule.kt index e16015f499..1340cbbf2e 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/YieldSupplyDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/YieldSupplyDomainModule.kt @@ -78,6 +78,16 @@ internal object YieldSupplyDomainModule { ) } + @Provides + @Singleton + fun provideWrapYieldSwapCallDataWithUpgradeUseCase( + yieldSupplyTransactionRepository: YieldSupplyTransactionRepository, + ): WrapYieldSwapCallDataWithUpgradeUseCase { + return WrapYieldSwapCallDataWithUpgradeUseCase( + yieldSupplyTransactionRepository = yieldSupplyTransactionRepository, + ) + } + @Provides @Singleton fun provideYieldSupplyGetProtocolBalanceUseCase( diff --git a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json index 597f1553c9..8eed9a1b0a 100644 --- a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json @@ -55,6 +55,10 @@ "name": "WALLET_CONNECT_BITCOIN_ENABLED", "version": "undefined" }, + { + "name": "TWI_1326_YIELD_MODE_SWAP_ENABLED", + "version": "undefined" + }, { "name": "ADDRESS_SYNC_ENABLED", "version": "undefined" diff --git a/data/swap/build.gradle.kts b/data/swap/build.gradle.kts index 6541bb22cd..0297e4d3ea 100644 --- a/data/swap/build.gradle.kts +++ b/data/swap/build.gradle.kts @@ -46,6 +46,9 @@ dependencies { exclude(module = "joda-time") } + /** Core */ + implementation(projects.core.configToggles) + /** Libs */ implementation(projects.libs.blockchainSdk) implementation(projects.libs.crypto) diff --git a/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt b/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt index a888483d9c..8aec84f31e 100644 --- a/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt +++ b/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt @@ -6,6 +6,8 @@ import arrow.core.toOption import com.squareup.moshi.Moshi import com.tangem.blockchain.common.Blockchain 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.swap.converter.SwapDataConverter import com.tangem.data.swap.converter.SwapStatusConverter @@ -57,6 +59,7 @@ internal class DefaultSwapRepositoryV2 @Inject constructor( private val dataSignatureVerifier: DataSignatureVerifier, private val singleQuoteStatusSupplier: SingleQuoteStatusSupplier, private val singleQuoteStatusFetcher: SingleQuoteStatusFetcher, + private val featureTogglesManager: FeatureTogglesManager, @NetworkMoshi moshi: Moshi, ) : SwapRepositoryV2 { @@ -544,16 +547,21 @@ internal class DefaultSwapRepositoryV2 @Inject constructor( return setScale(decimals, RoundingMode.HALF_DOWN).movePointRight(decimals).toPlainString() } - private fun List.filterYieldSupplyProvider(cryptoCurrencyStatus: CryptoCurrencyStatus?) = - filter { provider -> - // !!!WARNING!!! Filter out dex provider if yield supply is active - val yieldSupplyStatus = cryptoCurrencyStatus?.value?.yieldSupplyStatus - if (yieldSupplyStatus != null && yieldSupplyStatus.isActive) { - provider.type == ExpressProviderType.CEX - } else { - true + private fun List.filterYieldSupplyProvider( + cryptoCurrencyStatus: CryptoCurrencyStatus?, + ): List { + return if (featureTogglesManager.isFeatureEnabled(FeatureToggles.TWI_1326_YIELD_MODE_SWAP_ENABLED)) { + this + } else { + filter { provider -> + if (cryptoCurrencyStatus?.value?.yieldSupplyStatus?.isActive == true) { + provider.type == ExpressProviderType.CEX + } else { + true + } } } + } } private val MEMO_RESTRICTED_NETWORKS = setOf( diff --git a/data/swap/src/main/java/com/tangem/data/swap/di/SwapDataModule.kt b/data/swap/src/main/java/com/tangem/data/swap/di/SwapDataModule.kt index 6fee629eea..b3ea0a86bb 100644 --- a/data/swap/src/main/java/com/tangem/data/swap/di/SwapDataModule.kt +++ b/data/swap/src/main/java/com/tangem/data/swap/di/SwapDataModule.kt @@ -18,6 +18,7 @@ import com.tangem.domain.quotes.single.SingleQuoteStatusFetcher import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier import com.tangem.domain.swap.SwapErrorResolver import com.tangem.domain.swap.SwapRepositoryV2 +import com.tangem.core.configtoggle.feature.FeatureTogglesManager import com.tangem.domain.swap.SwapTransactionRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module @@ -49,6 +50,7 @@ internal object SwapDataModule { dataSignatureVerifier: DataSignatureVerifier, singleQuoteStatusSupplier: SingleQuoteStatusSupplier, singleQuoteStatusFetcher: SingleQuoteStatusFetcher, + featureTogglesManager: FeatureTogglesManager, @NetworkMoshi moshi: Moshi, ): SwapRepositoryV2 { return DefaultSwapRepositoryV2( @@ -60,6 +62,7 @@ internal object SwapDataModule { moshi = moshi, singleQuoteStatusSupplier = singleQuoteStatusSupplier, singleQuoteStatusFetcher = singleQuoteStatusFetcher, + featureTogglesManager = featureTogglesManager, ) } diff --git a/data/swap/src/test/kotlin/com/tangem/data/swap/DefaultSwapRepositoryV2Test.kt b/data/swap/src/test/kotlin/com/tangem/data/swap/DefaultSwapRepositoryV2Test.kt index 92343937bb..8fd1a4d3a3 100644 --- a/data/swap/src/test/kotlin/com/tangem/data/swap/DefaultSwapRepositoryV2Test.kt +++ b/data/swap/src/test/kotlin/com/tangem/data/swap/DefaultSwapRepositoryV2Test.kt @@ -24,6 +24,8 @@ import com.tangem.domain.swap.models.SwapAmountType import com.tangem.domain.swap.models.SwapCurrencyStatus import com.tangem.domain.swap.models.SwapStatus 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 io.mockk.* import kotlinx.coroutines.test.runTest @@ -43,6 +45,9 @@ internal class DefaultSwapRepositoryV2Test { private val singleQuoteStatusSupplier: SingleQuoteStatusSupplier = mockk() private val singleQuoteStatusFetcher: SingleQuoteStatusFetcher = mockk() private val moshi: Moshi = Moshi.Builder().build() + private val featureTogglesManager: FeatureTogglesManager = mockk { + every { isFeatureEnabled(any()) } returns false + } private val repository = DefaultSwapRepositoryV2( tangemExpressApi = tangemExpressApi, @@ -52,6 +57,7 @@ internal class DefaultSwapRepositoryV2Test { dataSignatureVerifier = dataSignatureVerifier, singleQuoteStatusSupplier = singleQuoteStatusSupplier, singleQuoteStatusFetcher = singleQuoteStatusFetcher, + featureTogglesManager = featureTogglesManager, moshi = moshi, ) @@ -64,7 +70,9 @@ internal class DefaultSwapRepositoryV2Test { dataSignatureVerifier, singleQuoteStatusSupplier, singleQuoteStatusFetcher, + featureTogglesManager, ) + every { featureTogglesManager.isFeatureEnabled(any()) } returns false } // region getPairs(SwapCurrencyStatus, SwapCurrencyStatus) @@ -511,8 +519,9 @@ internal class DefaultSwapRepositoryV2Test { // region filterYieldSupplyProvider @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 + every { featureTogglesManager.isFeatureEnabled(FeatureToggles.TWI_1326_YIELD_MODE_SWAP_ENABLED) } returns false val primaryStatus = createCryptoCurrencyStatusWithActiveYield(primaryCoin) val secondaryStatus = createCryptoCurrencyStatus(secondaryCoin) val primarySwapCurrencyStatus = SwapCurrencyStatus( @@ -558,6 +567,54 @@ internal class DefaultSwapRepositoryV2Test { 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 // region getSwapData diff --git a/data/yield-supply/src/main/java/com/tangem/data/yield/supply/DefaultYieldModuleAddressProvider.kt b/data/yield-supply/src/main/java/com/tangem/data/yield/supply/DefaultYieldModuleAddressProvider.kt new file mode 100644 index 0000000000..bf80a11396 --- /dev/null +++ b/data/yield-supply/src/main/java/com/tangem/data/yield/supply/DefaultYieldModuleAddressProvider.kt @@ -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() + 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 } + } + } +} \ No newline at end of file diff --git a/data/yield-supply/src/main/java/com/tangem/data/yield/supply/DefaultYieldSupplyTransactionRepository.kt b/data/yield-supply/src/main/java/com/tangem/data/yield/supply/DefaultYieldSupplyTransactionRepository.kt index 19f06ecd0f..ac1c2517a6 100644 --- a/data/yield-supply/src/main/java/com/tangem/data/yield/supply/DefaultYieldSupplyTransactionRepository.kt +++ b/data/yield-supply/src/main/java/com/tangem/data/yield/supply/DefaultYieldSupplyTransactionRepository.kt @@ -11,6 +11,7 @@ import com.tangem.blockchain.yieldsupply.YieldSupplyContractCallDataProviderFact import com.tangem.blockchainsdk.utils.toBlockchain import com.tangem.domain.models.currency.CryptoCurrency 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.yield.supply.YieldSupplyStatus import com.tangem.domain.utils.convertToSdkAmount @@ -127,6 +128,11 @@ internal class DefaultYieldSupplyTransactionRepository( val amount = getEnterAmount(cryptoCurrency, yieldSupplyStatus) val emptyContractAddress = existingYieldAddress == null || existingYieldAddress == EthereumUtils.ZERO_ADDRESS + val activeYieldContractAddress = if (emptyContractAddress) { + calculatedYieldContractAddress + } else { + existingYieldAddress + } when { yieldSupplyStatus == null || emptyContractAddress -> { @@ -143,7 +149,7 @@ internal class DefaultYieldSupplyTransactionRepository( createInitTokenTransaction( walletManager = walletManager, cryptoCurrency = cryptoCurrency, - yieldContractAddress = calculatedYieldContractAddress, + yieldContractAddress = activeYieldContractAddress, amount = amount, maxNetworkFee = maxNetworkFee, ), @@ -152,7 +158,7 @@ internal class DefaultYieldSupplyTransactionRepository( createReactivateTokenTransaction( walletManager = walletManager, cryptoCurrency = cryptoCurrency, - yieldContractAddress = calculatedYieldContractAddress, + yieldContractAddress = activeYieldContractAddress, amount = amount, maxNetworkFee = maxNetworkFee, ), @@ -166,7 +172,7 @@ internal class DefaultYieldSupplyTransactionRepository( walletManager = walletManager, cryptoCurrency = cryptoCurrency, callData = ApprovalERC20TokenCallData( - spenderAddress = calculatedYieldContractAddress, + spenderAddress = activeYieldContractAddress, amount = null, ), destinationAddress = cryptoCurrency.contractAddress, @@ -182,7 +188,7 @@ internal class DefaultYieldSupplyTransactionRepository( walletManager = walletManager, cryptoCurrency = cryptoCurrency, amount = amount, - yieldContractAddress = calculatedYieldContractAddress, + yieldContractAddress = activeYieldContractAddress, ), ) } @@ -218,6 +224,20 @@ internal class DefaultYieldSupplyTransactionRepository( }.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( walletManager: WalletManager, cryptoCurrency: CryptoCurrency.Token, diff --git a/data/yield-supply/src/main/java/com/tangem/data/yield/supply/di/YieldSupplyDataModule.kt b/data/yield-supply/src/main/java/com/tangem/data/yield/supply/di/YieldSupplyDataModule.kt index c432bde85a..38ff5bba43 100644 --- a/data/yield-supply/src/main/java/com/tangem/data/yield/supply/di/YieldSupplyDataModule.kt +++ b/data/yield-supply/src/main/java/com/tangem/data/yield/supply/di/YieldSupplyDataModule.kt @@ -1,6 +1,7 @@ package com.tangem.data.yield.supply.di 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.DefaultYieldSupplyErrorResolver 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.YieldBoostStatusStore 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.YieldSupplyErrorResolver import com.tangem.domain.yield.supply.YieldSupplyTransactionRepository @@ -65,6 +67,18 @@ internal object YieldSupplyDataModule { return DefaultYieldSupplyErrorResolver } + @Provides + @Singleton + fun provideYieldModuleAddressProvider( + walletManagersFacade: WalletManagersFacade, + dispatchers: CoroutineDispatcherProvider, + ): YieldModuleAddressProvider { + return DefaultYieldModuleAddressProvider( + walletManagersFacade = walletManagersFacade, + dispatchers = dispatchers, + ) + } + @Provides @Singleton fun provideYieldPromoRepository( diff --git a/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapCurrencyStatus.kt b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapCurrencyStatus.kt index 4a0067251f..eb6af3b4ec 100644 --- a/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapCurrencyStatus.kt +++ b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapCurrencyStatus.kt @@ -30,4 +30,6 @@ data class SwapCurrencyStatus( get() = status.currency val userWalletId: UserWalletId get() = userWallet.walletId + val isYieldSupplyActive: Boolean + get() = status.value.yieldSupplyStatus?.isActive == true } \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/GetEthSpecificFeeUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/GetEthSpecificFeeUseCase.kt index ebb7a3bf0e..62bebaf8ba 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/GetEthSpecificFeeUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/GetEthSpecificFeeUseCase.kt @@ -46,7 +46,7 @@ class GetEthSpecificFeeUseCase( val minimalFee = getEthLegacyFee( gasPrice = gasPriceResult, gasLimit = gasLimit, - decimals = cryptoCurrency.decimals, + decimals = blockchain.decimals(), blockchain = blockchain, ) @@ -54,7 +54,7 @@ class GetEthSpecificFeeUseCase( val normalFee = getEthLegacyFee( gasPrice = normalGasPrice, gasLimit = gasLimit, - decimals = cryptoCurrency.decimals, + decimals = blockchain.decimals(), blockchain = blockchain, ) @@ -64,7 +64,7 @@ class GetEthSpecificFeeUseCase( val priorityFee = getEthLegacyFee( gasPrice = priorityGasPrice, gasLimit = gasLimit, - decimals = cryptoCurrency.decimals, + decimals = blockchain.decimals(), blockchain = blockchain, ) diff --git a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/YieldModuleAddressProvider.kt b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/YieldModuleAddressProvider.kt new file mode 100644 index 0000000000..f5ace8ca05 --- /dev/null +++ b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/YieldModuleAddressProvider.kt @@ -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) +} \ No newline at end of file diff --git a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/YieldSupplyTransactionRepository.kt b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/YieldSupplyTransactionRepository.kt index dccf5c9e1d..cfb76be389 100644 --- a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/YieldSupplyTransactionRepository.kt +++ b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/YieldSupplyTransactionRepository.kt @@ -1,9 +1,11 @@ package com.tangem.domain.yield.supply import com.tangem.blockchain.common.TransactionData +import com.tangem.blockchain.common.smartcontract.SmartContractCallData import com.tangem.blockchain.common.transaction.Fee import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWalletId import java.math.BigDecimal @@ -24,4 +26,14 @@ interface YieldSupplyTransactionRepository { suspend fun getYieldContractAddress(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): String? 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 } \ No newline at end of file diff --git a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/WrapYieldSwapCallDataWithUpgradeUseCase.kt b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/WrapYieldSwapCallDataWithUpgradeUseCase.kt new file mode 100644 index 0000000000..f70f62e664 --- /dev/null +++ b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/WrapYieldSwapCallDataWithUpgradeUseCase.kt @@ -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, + ) +} \ No newline at end of file diff --git a/features/swap/api/src/main/kotlin/com/tangem/features/swap/SwapFeatureToggles.kt b/features/swap/api/src/main/kotlin/com/tangem/features/swap/SwapFeatureToggles.kt index 0a5875e227..2b975af32f 100644 --- a/features/swap/api/src/main/kotlin/com/tangem/features/swap/SwapFeatureToggles.kt +++ b/features/swap/api/src/main/kotlin/com/tangem/features/swap/SwapFeatureToggles.kt @@ -1,6 +1,7 @@ package com.tangem.features.swap interface SwapFeatureToggles { + val isYieldSwapEnabled: Boolean val isSwapSwitchToTransferEnabled: Boolean val isSwapIntegratedApproveEnabled: Boolean val isSwapAbEnabled: Boolean diff --git a/features/swap/domain/build.gradle.kts b/features/swap/domain/build.gradle.kts index 63505328ce..00bc4cb0a8 100644 --- a/features/swap/domain/build.gradle.kts +++ b/features/swap/domain/build.gradle.kts @@ -51,8 +51,10 @@ dependencies { implementation(projects.domain.visa) implementation(projects.domain.visa.models) implementation(projects.domain.balanceHiding) + implementation(projects.domain.yieldSupply) /** Core modules */ + implementation(projects.core.configToggles) implementation(projects.core.utils) implementation(projects.core.ui) implementation(projects.core.datasource) diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index f902272fa7..059dd0647c 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -51,6 +51,7 @@ import com.tangem.domain.transaction.usecase.* import com.tangem.domain.transaction.usecase.gasless.CreateAndSendGaslessTransactionUseCase import com.tangem.domain.utils.convertToSdkAmount 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.fee.CexSwapFeeCalculator 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.toStringWithRightOffset import com.tangem.feature.swap.domain.models.ui.* +import com.tangem.features.swap.SwapFeatureToggles import com.tangem.utils.coroutines.runSuspendCatching import com.tangem.utils.extensions.orZero import com.tangem.utils.logging.TangemLogger @@ -99,12 +101,17 @@ internal class SwapInteractorImpl @Inject constructor( private val getSwapPairUseCase: GetSwapPairUseCase, private val dexSwapFeeCalculator: DexSwapFeeCalculator, private val cexSwapFeeCalculator: CexSwapFeeCalculator, + private val swapFeatureToggles: SwapFeatureToggles, + private val yieldModuleAddressProvider: YieldModuleAddressProvider, ) : SwapInteractor { private val getSelectedAppCurrencyUseCase by lazy(LazyThreadSafetyMode.NONE) { GetSelectedAppCurrencyUseCase(appCurrencyRepository) } + private val SwapCurrencyStatus.isYieldSwapActive: Boolean + get() = swapFeatureToggles.isYieldSwapEnabled && isYieldSupplyActive + override suspend fun getPair( fromSwapCurrencyStatus: SwapCurrencyStatus, toSwapCurrencyStatus: SwapCurrencyStatus, @@ -254,7 +261,9 @@ internal class SwapInteractorImpl @Inject constructor( reduceBalanceBy: BigDecimal, expressOperationType: ExpressOperationType, ): Pair { - if (fromSwapCurrencyStatus.status.value.yieldSupplyStatus?.isActive == true) { + if (fromSwapCurrencyStatus.status.value.yieldSupplyStatus?.isActive == true && + !swapFeatureToggles.isYieldSwapEnabled + ) { return provider to produceDexSwapDataError( error = ExpressDataError.DexActiveSupplyError(), fromSwapCurrencyStatus = fromSwapCurrencyStatus, @@ -286,19 +295,26 @@ internal class SwapInteractorImpl @Inject constructor( } val fromTokenAddress = getTokenAddress(fromSwapCurrencyStatus.currency) - val isAllowedToSpend = maybeQuotes.fold( - ifRight = { quotes -> - quotes.allowanceContract?.let { allowanceContract -> - getAllowanceInfoUseCase( - userWalletId = fromSwapCurrencyStatus.userWalletId, - cryptoCurrency = fromSwapCurrencyStatus.currency, - spenderAddress = allowanceContract, - requiredAmount = amount.value, - ).getOrNull() is AllowanceInfo.Enough - } != false - }, - ifLeft = { false }, - ) + val isYieldSwap = fromSwapCurrencyStatus.isYieldSwapActive && + fromSwapCurrencyStatus.currency is CryptoCurrency.Token + val isAllowedToSpend = if (isYieldSwap) { + maybeQuotes.isRight() && + fromSwapCurrencyStatus.status.value.yieldSupplyStatus?.isAllowedToSpend == true + } else { + maybeQuotes.fold( + ifRight = { quotes -> + quotes.allowanceContract?.let { allowanceContract -> + getAllowanceInfoUseCase( + userWalletId = fromSwapCurrencyStatus.userWalletId, + cryptoCurrency = fromSwapCurrencyStatus.currency, + spenderAddress = allowanceContract, + requiredAmount = amount.value, + ).getOrNull() is AllowanceInfo.Enough + } != false + }, + ifLeft = { false }, + ) + } if (isAllowedToSpend && allowPermissionsHandler.isAddressAllowanceInProgress(fromTokenAddress)) { allowPermissionsHandler.removeAddressFromProgress(fromTokenAddress) @@ -308,6 +324,7 @@ internal class SwapInteractorImpl @Inject constructor( ) } val isBalanceWithoutFeeEnough = isBalanceEnough(fromSwapCurrencyStatus, amount, null) + val quoteAllowanceContract = maybeQuotes.getOrNull()?.allowanceContract return if (isAllowedToSpend && isBalanceWithoutFeeEnough) { provider to loadDexSwapDataNoFee( provider = provider, @@ -315,6 +332,7 @@ internal class SwapInteractorImpl @Inject constructor( toSwapCurrencyStatus = toSwapCurrencyStatus, amount = amount, expressOperationType = expressOperationType, + quoteAllowanceContract = quoteAllowanceContract, ) } else { val quoteBalanceStatus = if (isBalanceWithoutFeeEnough) { @@ -377,6 +395,7 @@ internal class SwapInteractorImpl @Inject constructor( toSwapCurrencyStatus = toSwapCurrencyStatus, amount = amount, expressOperationType = expressOperationType, + quoteAllowanceContract = maybeQuotes.getOrNull()?.allowanceContract, ) } else { provider to getQuotesState( @@ -635,28 +654,54 @@ internal class SwapInteractorImpl @Inject constructor( swapFee: SwapFee, ): SwapTransactionState { 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 dexTransaction = swapData.transaction as ExpressTransactionModel.DEX val dataToSign = dexTransaction.txData - val amountToSend = createNativeAmountForDex(txValue, fromSwapCurrencyStatus.currency.network) - val txData = createTransactionUseCase( - amount = amountToSend, - fee = swapFee.fee, - memo = null, - destination = swapData.transaction.txTo, - userWalletId = fromSwapCurrencyStatus.userWalletId, - network = toSwapCurrencyStatus.currency.network, - txExtras = createDexTxExtras( - dataToSign, - fromSwapCurrencyStatus.currency.network, - swapFee.fee.getGasLimit(), - ), - ).getOrElse { error -> + val isYieldSwap = fromSwapCurrencyStatus.isYieldSwapActive + val fromCurrency = fromSwapCurrencyStatus.currency + + val txDataResult = if (isYieldSwap && fromCurrency is CryptoCurrency.Token) { + val spenderAddress = dexTransaction.allowanceContract + ?: return SwapTransactionState.Error.UnknownError + createYieldSwapDexTransaction( + fromSwapCurrencyStatus = fromSwapCurrencyStatus, + swapData = swapData, + dexCallData = dataToSign, + amount = amountDecimal, + fee = swapFee.fee, + spenderAddress = spenderAddress, + ) + } 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) 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( fromSwapCurrencyStatus = fromSwapCurrencyStatus, toSwapCurrencyStatus = toSwapCurrencyStatus, @@ -664,7 +709,7 @@ internal class SwapInteractorImpl @Inject constructor( swapData = swapData, amount = amount, txData = txData, - payInAddress = getPayoutAddress(txData), + payInAddress = payInAddress, ) } @@ -1001,11 +1046,23 @@ internal class SwapInteractorImpl @Inject constructor( val transaction = swapData?.transaction as? ExpressTransactionModel.DEX ?: return GetFeeError.UnknownError.left() - return dexSwapFeeCalculator.calculate( - fromSwapCurrencyStatus = fromStatus, - transaction = transaction, - selectedToken = selectedFeeToken, - ).fold( + val dexFeeResultEither = if (fromStatus.isYieldSwapActive && fromStatus.currency is CryptoCurrency.Token) { + val network = (fromStatus.currency as CryptoCurrency.Token).network + val yieldModuleAddress = yieldModuleAddressProvider.getOrFetch(fromStatus.userWalletId, network) + dexSwapFeeCalculator.calculateYield( + fromSwapCurrencyStatus = fromStatus, + transaction = transaction, + yieldModuleAddress = yieldModuleAddress, + ) + } else { + dexSwapFeeCalculator.calculate( + fromSwapCurrencyStatus = fromStatus, + transaction = transaction, + selectedToken = selectedFeeToken, + ) + } + + return dexFeeResultEither.fold( ifLeft = { error -> GetFeeError.DataError(error).left() }, ifRight = { dexFeeResult -> 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 { + 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 * [selectedFeeToken] is null the gasless use case (invoked inside [CexSwapFeeCalculator]) @@ -1501,6 +1594,7 @@ internal class SwapInteractorImpl @Inject constructor( toSwapCurrencyStatus: SwapCurrencyStatus, amount: SwapAmount, expressOperationType: ExpressOperationType, + quoteAllowanceContract: String? = null, ): SwapState { val fromNetworkAddress = fromSwapCurrencyStatus.status.value.networkAddress val dexFromAddress = fromNetworkAddress?.defaultAddress?.value.orEmpty() @@ -1521,7 +1615,14 @@ internal class SwapInteractorImpl @Inject constructor( toAddress = dexToAddress, refundAddress = fromNetworkAddress?.defaultAddress?.value, 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 -> val preparedSwapConfigState = PreparedSwapConfigState( 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( userWalletId = fromSwapCurrencyStatus.userWalletId, cryptoCurrency = fromToken, - spenderAddress = requireNotNull(quoteModel.allowanceContract) { "spenderAddress cant be null" }, + spenderAddress = spenderAddress, requiredAmount = swapAmount.value, ).getOrNull() return quotesLoadedState.copy( permissionState = PermissionDataState.PermissionRequired( isResetApproval = allowanceInfo is AllowanceInfo.ResetNeeded, - spenderAddress = quoteModel.allowanceContract, + spenderAddress = spenderAddress, ), ) } diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt index d4c16b5b71..3bcd2fdcb4 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt @@ -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.GetFeeForTokenUseCase 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.api.SwapFeedbackRepository import com.tangem.feature.swap.domain.api.SwapRepository @@ -75,6 +76,7 @@ internal class SwapDomainModule { createTransactionExtrasUseCase: CreateTransactionDataExtrasUseCase, walletManagersFacade: WalletManagersFacade, @SwapDexGasLimit patchEthGasLimitForSwap: PatchEthGasLimitForSwap, + wrapYieldSwapCallDataWithUpgradeUseCase: WrapYieldSwapCallDataWithUpgradeUseCase, ): DexSwapFeeCalculator = DexSwapFeeCalculator( getFeeUseCase = getFeeUseCase, getEthSpecificFeeUseCase = getEthSpecificFeeUseCase, @@ -82,6 +84,7 @@ internal class SwapDomainModule { createTransactionExtrasUseCase = createTransactionExtrasUseCase, walletManagersFacade = walletManagersFacade, patchEthGasLimitForSwap = patchEthGasLimitForSwap, + wrapYieldSwapCallDataWithUpgradeUseCase = wrapYieldSwapCallDataWithUpgradeUseCase, ) @Provides diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/fee/DexSwapFeeCalculator.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/fee/DexSwapFeeCalculator.kt index 8f711b7b88..183303e1e4 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/fee/DexSwapFeeCalculator.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/fee/DexSwapFeeCalculator.kt @@ -7,8 +7,13 @@ import com.tangem.blockchain.blockchains.solana.SolanaTransactionHelper import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.TransactionData +import com.tangem.blockchain.common.smartcontract.SmartContractCallData 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.common.extensions.hexToBytes import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus 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.gasless.GetFeeForTokenUseCase 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.domain.ExpressTransactionModel import com.tangem.lib.crypto.BlockchainUtils.SOLANA_TRANSACTION_SIZE_THRESHOLD_BYTES import com.tangem.lib.crypto.BlockchainUtils.isSolana import com.tangem.utils.logging.TangemLogger import java.math.BigDecimal +import java.math.BigInteger /** * Calculates the on-chain transaction fee for a DEX swap. @@ -52,6 +59,7 @@ class DexSwapFeeCalculator( private val createTransactionExtrasUseCase: CreateTransactionDataExtrasUseCase, private val walletManagersFacade: WalletManagersFacade, private val patchEthGasLimitForSwap: PatchEthGasLimitForSwap, + private val wrapYieldSwapCallDataWithUpgradeUseCase: WrapYieldSwapCallDataWithUpgradeUseCase, ) { 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 = 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 = 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") private suspend fun getFeeDataForDexSwap( fromSwapCurrencyStatus: SwapCurrencyStatus, diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/ExpressTransactionModel.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/ExpressTransactionModel.kt index 0986144ef2..7d01a26907 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/ExpressTransactionModel.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/ExpressTransactionModel.kt @@ -30,7 +30,7 @@ sealed class ExpressTransactionModel { val txData: String, val otherNativeFeeWei: BigDecimal?, val gas: BigInteger?, - val allowanceContract: String?, + val allowanceContract: String? = null, ) : ExpressTransactionModel() data class CEX( diff --git a/features/swap/domain/src/test/kotlin/com/tangem/feature/swap/domain/SwapInteractorImplFindBestQuoteTest.kt b/features/swap/domain/src/test/kotlin/com/tangem/feature/swap/domain/SwapInteractorImplFindBestQuoteTest.kt index 5e5d2ab44b..29811e9c8c 100644 --- a/features/swap/domain/src/test/kotlin/com/tangem/feature/swap/domain/SwapInteractorImplFindBestQuoteTest.kt +++ b/features/swap/domain/src/test/kotlin/com/tangem/feature/swap/domain/SwapInteractorImplFindBestQuoteTest.kt @@ -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.ExpressTransactionModel 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 io.mockk.coEvery import io.mockk.every @@ -872,6 +873,210 @@ internal class SwapInteractorImplFindBestQuoteTest : SwapInteractorImplTestBase( 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 diff --git a/features/swap/domain/src/test/kotlin/com/tangem/feature/swap/domain/SwapInteractorImplTestBase.kt b/features/swap/domain/src/test/kotlin/com/tangem/feature/swap/domain/SwapInteractorImplTestBase.kt index e6d4aaf1cb..80dbaf4494 100644 --- a/features/swap/domain/src/test/kotlin/com/tangem/feature/swap/domain/SwapInteractorImplTestBase.kt +++ b/features/swap/domain/src/test/kotlin/com/tangem/feature/swap/domain/SwapInteractorImplTestBase.kt @@ -33,6 +33,7 @@ import com.tangem.domain.tokens.repository.CurrencyChecksRepository import com.tangem.domain.transaction.usecase.* import com.tangem.domain.transaction.usecase.gasless.CreateAndSendGaslessTransactionUseCase 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.fee.CexSwapFeeCalculator 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.ui.AmountFormatter import com.tangem.feature.swap.domain.models.ui.SwapFee +import com.tangem.features.swap.SwapFeatureToggles import io.mockk.clearAllMocks import io.mockk.every import io.mockk.mockk @@ -84,6 +86,8 @@ internal open class SwapInteractorImplTestBase { protected val getSwapPairUseCase: GetSwapPairUseCase = mockk(relaxed = true) protected val dexSwapFeeCalculator: DexSwapFeeCalculator = 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 @@ -115,6 +119,8 @@ internal open class SwapInteractorImplTestBase { getSwapPairUseCase = getSwapPairUseCase, dexSwapFeeCalculator = dexSwapFeeCalculator, cexSwapFeeCalculator = cexSwapFeeCalculator, + swapFeatureToggles = swapFeatureToggles, + yieldModuleAddressProvider = yieldModuleAddressProvider, ) } @@ -158,6 +164,7 @@ internal fun buildSwapCurrencyStatus( decimals: Int = 18, userWalletId: UserWalletId = UserWalletId(stringValue = "deadbeef"), yieldSupplyActive: Boolean = false, + yieldSupplyAllowedToSpend: Boolean = true, ): SwapCurrencyStatus { val networkId = mockk(relaxed = true) { every { rawId } returns Network.RawID(networkRawId) @@ -196,6 +203,7 @@ internal fun buildSwapCurrencyStatus( val maybeYield: YieldSupplyStatus? = if (yieldSupplyActive) { mockk(relaxed = true) { every { isActive } returns true + every { isAllowedToSpend } returns yieldSupplyAllowedToSpend } } else { null diff --git a/features/swap/domain/src/test/kotlin/com/tangem/feature/swap/domain/fee/DexSwapFeeCalculatorTest.kt b/features/swap/domain/src/test/kotlin/com/tangem/feature/swap/domain/fee/DexSwapFeeCalculatorTest.kt index 9d57af4369..3f5b481b94 100644 --- a/features/swap/domain/src/test/kotlin/com/tangem/feature/swap/domain/fee/DexSwapFeeCalculatorTest.kt +++ b/features/swap/domain/src/test/kotlin/com/tangem/feature/swap/domain/fee/DexSwapFeeCalculatorTest.kt @@ -20,6 +20,7 @@ import com.tangem.domain.transaction.usecase.GetEthSpecificFeeUseCase import com.tangem.domain.transaction.usecase.GetFeeUseCase import com.tangem.domain.transaction.usecase.gasless.GetFeeForTokenUseCase 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.models.ExpressDataError import com.tangem.feature.swap.domain.models.SwapAmount @@ -59,6 +60,7 @@ internal class DexSwapFeeCalculatorTest { private val getFeeForTokenUseCase: GetFeeForTokenUseCase = mockk(relaxed = true) private val createTransactionExtrasUseCase: CreateTransactionDataExtrasUseCase = 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) @@ -70,6 +72,7 @@ internal class DexSwapFeeCalculatorTest { createTransactionExtrasUseCase = createTransactionExtrasUseCase, walletManagersFacade = walletManagersFacade, patchEthGasLimitForSwap = dexBump, + wrapYieldSwapCallDataWithUpgradeUseCase = wrapYieldSwapCallDataWithUpgradeUseCase, ) } diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapFeatureToggles.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapFeatureToggles.kt index 39e01a84c8..a98c00d50b 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapFeatureToggles.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapFeatureToggles.kt @@ -9,6 +9,10 @@ internal class DefaultSwapFeatureToggles @Inject constructor( featureTogglesManager: FeatureTogglesManager, ) : SwapFeatureToggles { + override val isYieldSwapEnabled: Boolean = featureTogglesManager.isFeatureEnabled( + toggle = FeatureToggles.TWI_1326_YIELD_MODE_SWAP_ENABLED, + ) + override val isSwapSwitchToTransferEnabled: Boolean = featureTogglesManager.isFeatureEnabled( toggle = FeatureToggles.AND_15207_SWAP_SWITCH_TO_TRANSFER_ENABLED, ) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index b745566b76..ad6ebb8cb7 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,7 +5,7 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "develop-1527" +tangemBlockchainSdk = "develop-1532" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-620" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/WalletManagerFactoryCreator.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/WalletManagerFactoryCreator.kt index 2213a287ff..58af77dd60 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/WalletManagerFactoryCreator.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/WalletManagerFactoryCreator.kt @@ -16,6 +16,7 @@ import javax.inject.Inject * @property accountCreator account creator * @property blockchainDataStorage blockchain data storage * @property blockchainSDKLogger blockchain SDK logger + * @property featureToggleValues blockchain feature toggle values * [REDACTED_AUTHOR] */ @@ -23,9 +24,7 @@ internal class WalletManagerFactoryCreator @Inject constructor( private val accountCreator: AccountCreator, private val blockchainDataStorage: BlockchainDataStorage, private val blockchainSDKLogger: BlockchainSDKLogger, - private val isSolanaTxHistoryEnabled: Boolean, - private val isSolanaScaledUiAmountEnabled: Boolean, - private val isHederaErc20Enabled: Boolean, + private val featureToggleValues: FeatureToggleValues, ) { fun create(config: BlockchainSdkConfig, blockchainProviderTypes: BlockchainProviderTypes): WalletManagerFactory { @@ -37,13 +36,21 @@ internal class WalletManagerFactoryCreator @Inject constructor( accountCreator = accountCreator, featureToggles = BlockchainFeatureToggles( isYieldSupplyEnabled = true, + isYieldModeSwapEnabled = featureToggleValues.isYieldModeSwapEnabled, isPendingTransactionsEnabled = true, - isSolanaTxHistoryEnabled = isSolanaTxHistoryEnabled, - isSolanaScaledUiAmountEnabled = isSolanaScaledUiAmountEnabled, - isHederaErc20Enabled = isHederaErc20Enabled, + isSolanaTxHistoryEnabled = featureToggleValues.isSolanaTxHistoryEnabled, + isSolanaScaledUiAmountEnabled = featureToggleValues.isSolanaScaledUiAmountEnabled, + isHederaErc20Enabled = featureToggleValues.isHederaErc20Enabled, ), blockchainDataStorage = blockchainDataStorage, loggers = listOf(blockchainSDKLogger), ) } + + data class FeatureToggleValues( + val isSolanaTxHistoryEnabled: Boolean, + val isSolanaScaledUiAmountEnabled: Boolean, + val isYieldModeSwapEnabled: Boolean, + val isHederaErc20Enabled: Boolean, + ) } \ No newline at end of file diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/di/BlockchainSDKFactoryModule.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/di/BlockchainSDKFactoryModule.kt index cd52d01c17..3ff97082fb 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/di/BlockchainSDKFactoryModule.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/di/BlockchainSDKFactoryModule.kt @@ -97,14 +97,19 @@ internal object BlockchainSDKFactoryModule { accountCreator = DefaultAccountCreator(tangemTechApi), blockchainDataStorage = DefaultBlockchainDataStorage(appPreferencesStore), blockchainSDKLogger = blockchainSDKLogger, - isSolanaTxHistoryEnabled = featureTogglesManager.isFeatureEnabled( - FeatureToggles.SOLANA_TX_HISTORY_ENABLED, - ), - isSolanaScaledUiAmountEnabled = featureTogglesManager.isFeatureEnabled( - FeatureToggles.SOLANA_SCALED_UI_AMOUNT_ENABLED, - ), - isHederaErc20Enabled = featureTogglesManager.isFeatureEnabled( - FeatureToggles.HEDERA_ERC20_ENABLED, + featureToggleValues = WalletManagerFactoryCreator.FeatureToggleValues( + isSolanaTxHistoryEnabled = featureTogglesManager.isFeatureEnabled( + FeatureToggles.SOLANA_TX_HISTORY_ENABLED, + ), + isSolanaScaledUiAmountEnabled = featureTogglesManager.isFeatureEnabled( + FeatureToggles.SOLANA_SCALED_UI_AMOUNT_ENABLED, + ), + isYieldModeSwapEnabled = featureTogglesManager.isFeatureEnabled( + FeatureToggles.TWI_1326_YIELD_MODE_SWAP_ENABLED, + ), + isHederaErc20Enabled = featureTogglesManager.isFeatureEnabled( + FeatureToggles.HEDERA_ERC20_ENABLED, + ), ), ) }