diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e504d7d5c7..3e26bcc1f8 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -26,6 +26,7 @@ dependencies { implementation(project(":features:referral:presentation")) implementation(project(":features:referral:domain")) implementation(project(":features:referral:data")) + implementation(project(":features:swap:api")) implementation(project(":features:swap:presentation")) implementation(project(":features:swap:domain")) implementation(project(":features:swap:data")) diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletData.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletData.kt index f50513a1ae..96eb9d55c8 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletData.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletData.kt @@ -4,6 +4,7 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.TransactionData import com.tangem.common.extensions.isZero import com.tangem.domain.common.extensions.toNetworkId +import com.tangem.feature.swap.api.SwapFeatureToggleManager import com.tangem.feature.swap.domain.SwapInteractor import com.tangem.tap.features.wallet.models.Currency import com.tangem.tap.features.wallet.models.PendingTransaction @@ -36,22 +37,26 @@ data class WalletData( return exchangeManager.availableForSell(currency) } - fun isAvailableToSwap(swapInteractor: SwapInteractor, isExchangeFeatureOn: Boolean): Boolean { + fun isAvailableToSwap( + swapFeatureToggleManager: SwapFeatureToggleManager, + swapInteractor: SwapInteractor, + ): Boolean { + val isOptimismEnabled = + currency.blockchain.id == Blockchain.Optimism.id && swapFeatureToggleManager.isOptimismSwapEnabled return swapInteractor.isAvailableToSwap(currency.blockchain.toNetworkId()) && - currency.blockchain.id != Blockchain.Optimism.id && // disable optimism blockchain for now - isExchangeFeatureOn && + isOptimismEnabled && !currency.isCustomCurrency(null) } fun getAvailableActions( swapInteractor: SwapInteractor, exchangeManager: CurrencyExchangeManager, - isExchangeFeatureOn: Boolean, + swapFeatureToggleManager: SwapFeatureToggleManager, ): Set { return setOfNotNull( if (isAvailableToBuy(exchangeManager)) CurrencyAction.Buy else null, if (isAvailableToSell(exchangeManager)) CurrencyAction.Sell else null, - if (isAvailableToSwap(swapInteractor, isExchangeFeatureOn)) CurrencyAction.Swap else null, + if (isAvailableToSwap(swapFeatureToggleManager, swapInteractor)) CurrencyAction.Swap else null, ) } diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt index 1eebe4d266..2668f1c79a 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt @@ -24,6 +24,7 @@ import com.tangem.common.extensions.guard import com.tangem.core.analytics.Analytics import com.tangem.domain.common.TapWorkarounds.derivationStyle import com.tangem.domain.common.extensions.withMainContext +import com.tangem.feature.swap.api.SwapFeatureToggleManager import com.tangem.feature.swap.domain.SwapInteractor import com.tangem.sdk.extensions.dpToPx import com.tangem.tap.common.SnackbarHandler @@ -75,6 +76,9 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), @Inject lateinit var swapInteractor: SwapInteractor + @Inject + lateinit var swapFeatureToggleManager: SwapFeatureToggleManager + private lateinit var pendingTransactionAdapter: PendingTransactionsAdapter private lateinit var warningMessagesAdapter: WalletDetailWarningMessagesAdapter @@ -290,7 +294,7 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), WalletAction.DialogAction.ChooseTradeActionDialog( buyAllowed = selectedWallet.isAvailableToBuy(exchangeManager), sellAllowed = selectedWallet.isAvailableToSell(exchangeManager), - swapAllowed = selectedWallet.isAvailableToSwap(swapInteractor, isExchangeServiceFeatureOn), + swapAllowed = selectedWallet.isAvailableToSwap(swapFeatureToggleManager, swapInteractor), ), ) } @@ -298,7 +302,7 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), val actions = selectedWallet.getAvailableActions( swapInteractor = swapInteractor, exchangeManager = exchangeManager, - isExchangeFeatureOn = isExchangeServiceFeatureOn, + swapFeatureToggleManager = swapFeatureToggleManager, ) binding.rowButtons.updateButtonsVisibility( actions = actions, diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt index 06cda93c02..de756d8466 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt @@ -20,6 +20,7 @@ import coil.size.Scale import com.tangem.core.analytics.Analytics import com.tangem.core.ui.fragments.setStatusBarColor import com.tangem.core.ui.utils.OneTouchClickListener +import com.tangem.feature.swap.api.SwapFeatureToggleManager import com.tangem.feature.swap.domain.SwapInteractor import com.tangem.tap.MainActivity import com.tangem.tap.common.analytics.events.MainScreen @@ -56,6 +57,9 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber { - val fee = gasLimit.multiply(gasPrice.data).toBigDecimal( - scale = blockchain.decimals(), - mathContext = MathContext(blockchain.decimals(), RoundingMode.HALF_EVEN), - ) - ProxyFee( - gasLimit = gasLimit, - fee = ProxyAmount( - currencySymbol = blockchain.currency, - value = fee, - decimals = blockchain.decimals(), - ), - ) - } - is Result.Failure -> { - error(gasPrice.error.message ?: gasPrice.error.customMessage) - } - } + increaseBy = increaseBy, + ) } else { - val fee = (walletManager as? TransactionSender)?.getFee( - amount = createAmount(amountToSend, currencyToSend, blockchain), - destination = destinationAddress, - ) ?: error("Cannot cast to TransactionSender") - when (fee) { - is Result.Success -> { - // for not EVM blockchains set gasLimit ZERO for now - return ProxyFee( - gasLimit = BigInteger.ZERO, - fee = convertToProxyAmount(fee.data.firstOrNull() ?: error("no fee found")), - ) - } - is Result.Failure -> { - error(fee.error.message ?: fee.error.customMessage) - } - } + return getFeeForBlockchain( + walletManager = walletManager, + amountToSend = amountToSend, + currency = currencyToSend, + blockchain = blockchain, + destinationAddress = destinationAddress, + ) } } @@ -217,6 +193,100 @@ class TransactionManagerImpl( ) } + private suspend fun getFeeForBlockchain( + walletManager: WalletManager, + amountToSend: BigDecimal, + currency: Currency, + blockchain: Blockchain, + destinationAddress: String, + ): ProxyFee { + val fee = (walletManager as? TransactionSender)?.getFee( + amount = createAmount(amountToSend, currency, blockchain), + destination = destinationAddress, + ) ?: error("Cannot cast to TransactionSender") + return when (fee) { + is Result.Success -> { + // for not EVM blockchains set gasLimit ZERO for now + ProxyFee( + gasLimit = BigInteger.ZERO, + fee = convertToProxyAmount(fee.data.firstOrNull() ?: error("no fee found")), + ) + } + is Result.Failure -> { + error(fee.error.message ?: fee.error.customMessage) + } + } + } + + @Suppress("LongParameterList") + private suspend fun getFeeForEthereumBlockchain( + walletManager: EthereumWalletManager, + blockchain: Blockchain, + amountToSend: BigDecimal, + currency: Currency, + destinationAddress: String, + data: String?, + increaseBy: Int?, + ): ProxyFee { + val gasLimit = getGasLimit( + evmWalletManager = walletManager, + blockchain = blockchain, + amount = amountToSend, + currency = currency, + destinationAddress = destinationAddress, + data = data, + ).let { + if (increaseBy != null && increaseBy != 0) { + it.multiply(increaseBy.toBigInteger()).divide(BigInteger("100")) + } else { + it + } + } + return when (val gasPrice = walletManager.getGasPrice()) { + is Result.Success -> { + val fee = gasLimit.multiply(gasPrice.data).toBigDecimal( + scale = blockchain.decimals(), + mathContext = MathContext(blockchain.decimals(), RoundingMode.HALF_EVEN), + ) + ProxyFee( + gasLimit = gasLimit, + fee = ProxyAmount( + currencySymbol = blockchain.currency, + value = fee, + decimals = blockchain.decimals(), + ), + ) + } + is Result.Failure -> { + error(gasPrice.error.message ?: gasPrice.error.customMessage) + } + } + } + + private suspend fun getFeeForOptimismBlockchain( + walletManager: OptimismWalletManager, + amount: Amount, + destinationAddress: String, + data: String?, + ): ProxyFee { + val fee = if (data.isNullOrEmpty()) { + walletManager.getFee(amount, destinationAddress) + } else { + walletManager.getFee(amount, destinationAddress, data) + } + when (fee) { + is Result.Success -> { + return ProxyFee( + gasLimit = walletManager.gasLimit ?: BigInteger.ZERO, + fee = convertToProxyAmount(fee.data.lastOrNull() ?: error("no fee found")), + ) + } + is Result.Failure -> { + error(fee.error.message ?: fee.error.customMessage) + } + } + } + @Suppress("LongParameterList") private suspend fun getGasLimit( evmWalletManager: EthereumWalletManager, diff --git a/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json index 34473131eb..74e4d91915 100644 --- a/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json @@ -1,10 +1,6 @@ [ { - "name": "INACTIVE_TEST_FEATURE_ENABLED", - "version": "undefined" - }, - { - "name": "ACTIVE2_TEST_FEATURE_ENABLED", - "version": "1.0.0" + "name": "OPTIMISM_SWAP_FEATURE_ENABLED", + "version": "4.3.1" } ] \ No newline at end of file diff --git a/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/manager/DevFeatureTogglesManager.kt b/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/manager/DevFeatureTogglesManager.kt index 5f01cdba6e..12e0a496b6 100644 --- a/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/manager/DevFeatureTogglesManager.kt +++ b/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/manager/DevFeatureTogglesManager.kt @@ -42,7 +42,7 @@ internal class DevFeatureTogglesManager( .toMutableMap() } - override fun isFeatureEnabled(name: String): Boolean = featureTogglesMap.any { it.key == name } + override fun isFeatureEnabled(name: String): Boolean = featureTogglesMap[name] ?: false override fun getFeatureToggles(): Map = featureTogglesMap diff --git a/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/manager/ProdFeatureTogglesManager.kt b/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/manager/ProdFeatureTogglesManager.kt index be8a4046d2..0f88c3791c 100644 --- a/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/manager/ProdFeatureTogglesManager.kt +++ b/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/manager/ProdFeatureTogglesManager.kt @@ -25,7 +25,7 @@ internal class ProdFeatureTogglesManager( .associateToggles(currentVersion = versionProvider.get() ?: "") } - override fun isFeatureEnabled(name: String): Boolean = featureToggles.any { it.key == name } + override fun isFeatureEnabled(name: String): Boolean = featureToggles[name] ?: false @VisibleForTesting(otherwise = VisibleForTesting.NONE) fun getProdFeatureToggles() = featureToggles diff --git a/core/featuretoggles/src/test/kotlin/com/tangem/core/featuretoggle/manager/DevFeatureTogglesManagerTest.kt b/core/featuretoggles/src/test/kotlin/com/tangem/core/featuretoggle/manager/DevFeatureTogglesManagerTest.kt index 06ada09a84..51ac267ee2 100644 --- a/core/featuretoggles/src/test/kotlin/com/tangem/core/featuretoggle/manager/DevFeatureTogglesManagerTest.kt +++ b/core/featuretoggles/src/test/kotlin/com/tangem/core/featuretoggle/manager/DevFeatureTogglesManagerTest.kt @@ -144,7 +144,7 @@ internal class DevFeatureTogglesManagerTest { @Test fun `get feature availability if feature toggle exists`() { val featureToggles = mutableMapOf( - "INACTIVE_TEST_FEATURE_ENABLED" to false, + "INACTIVE_TEST_FEATURE_ENABLED" to true, "ACTIVE2_TEST_FEATURE_ENABLED" to true, ) manager.setFeatureToggles(featureToggles) diff --git a/core/featuretoggles/src/test/kotlin/com/tangem/core/featuretoggle/manager/ProdFeatureTogglesManagerTest.kt b/core/featuretoggles/src/test/kotlin/com/tangem/core/featuretoggle/manager/ProdFeatureTogglesManagerTest.kt index 06c5023987..a8b90c4af8 100644 --- a/core/featuretoggles/src/test/kotlin/com/tangem/core/featuretoggle/manager/ProdFeatureTogglesManagerTest.kt +++ b/core/featuretoggles/src/test/kotlin/com/tangem/core/featuretoggle/manager/ProdFeatureTogglesManagerTest.kt @@ -105,7 +105,7 @@ internal class ProdFeatureTogglesManagerTest { @Test fun `get feature availability if feature toggle exists`() { val featureToggles = mapOf( - "INACTIVE_TEST_FEATURE_ENABLED" to false, + "INACTIVE_TEST_FEATURE_ENABLED" to true, "ACTIVE2_TEST_FEATURE_ENABLED" to true, ) manager.setProdFeatureToggles(featureToggles) diff --git a/features/swap/api/.gitignore b/features/swap/api/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/features/swap/api/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/features/swap/api/build.gradle.kts b/features/swap/api/build.gradle.kts new file mode 100644 index 0000000000..882b05bd82 --- /dev/null +++ b/features/swap/api/build.gradle.kts @@ -0,0 +1,14 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.kotlin.serialization) + alias(deps.plugins.hilt.android) + id("configuration") +} + +dependencies { + /** DI */ + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) +} \ No newline at end of file diff --git a/features/swap/api/src/main/AndroidManifest.xml b/features/swap/api/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..b46e5ddb0b --- /dev/null +++ b/features/swap/api/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/features/swap/api/src/main/java/com/tangem/feature/swap/api/SwapFeatureToggleManager.kt b/features/swap/api/src/main/java/com/tangem/feature/swap/api/SwapFeatureToggleManager.kt new file mode 100644 index 0000000000..c9b4962de4 --- /dev/null +++ b/features/swap/api/src/main/java/com/tangem/feature/swap/api/SwapFeatureToggleManager.kt @@ -0,0 +1,7 @@ +package com.tangem.feature.swap.api + +/** Feature toggles manager of "swap" feature */ +interface SwapFeatureToggleManager { + + val isOptimismSwapEnabled: Boolean +} \ No newline at end of file diff --git a/features/swap/presentation/build.gradle.kts b/features/swap/presentation/build.gradle.kts index 2622155859..873966b94c 100644 --- a/features/swap/presentation/build.gradle.kts +++ b/features/swap/presentation/build.gradle.kts @@ -10,6 +10,7 @@ plugins { dependencies { /** Core modules */ implementation(project(":core:analytics")) + implementation(project(":core:featuretoggles")) implementation(project(":core:utils")) implementation(project(":core:ui")) @@ -27,6 +28,9 @@ dependencies { implementation(deps.compose.coil) implementation(deps.compose.constraintLayout) + /** Api */ + implementation(project(":features:swap:api")) + /** Domain */ implementation(project(":features:swap:domain")) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/di/SwapFeatureTogglesManagerModule.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/di/SwapFeatureTogglesManagerModule.kt new file mode 100644 index 0000000000..15ef8179e1 --- /dev/null +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/di/SwapFeatureTogglesManagerModule.kt @@ -0,0 +1,21 @@ +package com.tangem.feature.swap.di + +import com.tangem.core.featuretoggle.manager.FeatureTogglesManager +import com.tangem.feature.swap.api.SwapFeatureToggleManager +import com.tangem.feature.swap.toggles.DefaultFeatureTogglesManager +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ActivityRetainedComponent +import dagger.hilt.android.scopes.ActivityRetainedScoped + +@Module +@InstallIn(ActivityRetainedComponent::class) +internal object SwapFeatureTogglesManagerModule { + + @Provides + @ActivityRetainedScoped + fun provideSwapFeatureTogglesManager(featureTogglesManager: FeatureTogglesManager): SwapFeatureToggleManager { + return DefaultFeatureTogglesManager(featureTogglesManager) + } +} \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/toggles/DefaultFeatureTogglesManager.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/toggles/DefaultFeatureTogglesManager.kt new file mode 100644 index 0000000000..2448fefafd --- /dev/null +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/toggles/DefaultFeatureTogglesManager.kt @@ -0,0 +1,12 @@ +package com.tangem.feature.swap.toggles + +import com.tangem.core.featuretoggle.manager.FeatureTogglesManager + +/** Feature toggles manager implementation of "swap" feature */ +class DefaultFeatureTogglesManager( + private val featureTogglesManager: FeatureTogglesManager, +) : InnerSwapFeatureTogglesManager { + + override val isOptimismSwapEnabled: Boolean + get() = featureTogglesManager.isFeatureEnabled(name = "OPTIMISM_SWAP_FEATURE_ENABLED") +} \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/toggles/InnerSwapFeatureTogglesManager.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/toggles/InnerSwapFeatureTogglesManager.kt new file mode 100644 index 0000000000..f1035d737a --- /dev/null +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/toggles/InnerSwapFeatureTogglesManager.kt @@ -0,0 +1,6 @@ +package com.tangem.feature.swap.toggles + +import com.tangem.feature.swap.api.SwapFeatureToggleManager + +/** Feature toggles manager of "swap" feature for internal logic */ +internal interface InnerSwapFeatureTogglesManager : SwapFeatureToggleManager \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index e441f3bf63..d802d67613 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -438,7 +438,7 @@ internal class SwapViewModel @Inject constructor( companion object { private const val INITIAL_AMOUNT = "" - private const val UPDATE_DELAY = 10000L + private const val UPDATE_DELAY = 1000000L private const val DEBOUNCE_AMOUNT_DELAY = 1000L } } \ No newline at end of file diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 8388640d72..5a50bb5667 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -68,7 +68,7 @@ kotlinSerialization = "1.4.1" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "develop-176" +tangemBlockchainSdk = "develop-177" # tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-204" # tangemCardSdk = "0.0.1" # Keep it! - used for local builds diff --git a/settings.gradle.kts b/settings.gradle.kts index 4915044b10..dbb3202511 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -50,6 +50,7 @@ include(":libs:auth") include(":features:referral:data") include(":features:referral:domain") include(":features:referral:presentation") +include(":features:swap:api") include(":features:swap:data") include(":features:swap:domain") include(":features:swap:presentation")