Updated on 2026-08-14

This commit is contained in:
Tangem 2023-03-20 12:35:36 +03:00
parent a30e44b1cf
commit 0907ba002c
23 changed files with 217 additions and 65 deletions

View file

@ -26,6 +26,7 @@ dependencies {
implementation(project(":features:referral:presentation")) implementation(project(":features:referral:presentation"))
implementation(project(":features:referral:domain")) implementation(project(":features:referral:domain"))
implementation(project(":features:referral:data")) implementation(project(":features:referral:data"))
implementation(project(":features:swap:api"))
implementation(project(":features:swap:presentation")) implementation(project(":features:swap:presentation"))
implementation(project(":features:swap:domain")) implementation(project(":features:swap:domain"))
implementation(project(":features:swap:data")) implementation(project(":features:swap:data"))

View file

@ -4,6 +4,7 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.common.TransactionData
import com.tangem.common.extensions.isZero import com.tangem.common.extensions.isZero
import com.tangem.domain.common.extensions.toNetworkId import com.tangem.domain.common.extensions.toNetworkId
import com.tangem.feature.swap.api.SwapFeatureToggleManager
import com.tangem.feature.swap.domain.SwapInteractor import com.tangem.feature.swap.domain.SwapInteractor
import com.tangem.tap.features.wallet.models.Currency import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.models.PendingTransaction import com.tangem.tap.features.wallet.models.PendingTransaction
@ -36,22 +37,26 @@ data class WalletData(
return exchangeManager.availableForSell(currency) 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()) && return swapInteractor.isAvailableToSwap(currency.blockchain.toNetworkId()) &&
currency.blockchain.id != Blockchain.Optimism.id && // disable optimism blockchain for now isOptimismEnabled &&
isExchangeFeatureOn &&
!currency.isCustomCurrency(null) !currency.isCustomCurrency(null)
} }
fun getAvailableActions( fun getAvailableActions(
swapInteractor: SwapInteractor, swapInteractor: SwapInteractor,
exchangeManager: CurrencyExchangeManager, exchangeManager: CurrencyExchangeManager,
isExchangeFeatureOn: Boolean, swapFeatureToggleManager: SwapFeatureToggleManager,
): Set<CurrencyAction> { ): Set<CurrencyAction> {
return setOfNotNull( return setOfNotNull(
if (isAvailableToBuy(exchangeManager)) CurrencyAction.Buy else null, if (isAvailableToBuy(exchangeManager)) CurrencyAction.Buy else null,
if (isAvailableToSell(exchangeManager)) CurrencyAction.Sell 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,
) )
} }

View file

@ -24,6 +24,7 @@ import com.tangem.common.extensions.guard
import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.Analytics
import com.tangem.domain.common.TapWorkarounds.derivationStyle import com.tangem.domain.common.TapWorkarounds.derivationStyle
import com.tangem.domain.common.extensions.withMainContext import com.tangem.domain.common.extensions.withMainContext
import com.tangem.feature.swap.api.SwapFeatureToggleManager
import com.tangem.feature.swap.domain.SwapInteractor import com.tangem.feature.swap.domain.SwapInteractor
import com.tangem.sdk.extensions.dpToPx import com.tangem.sdk.extensions.dpToPx
import com.tangem.tap.common.SnackbarHandler import com.tangem.tap.common.SnackbarHandler
@ -75,6 +76,9 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
@Inject @Inject
lateinit var swapInteractor: SwapInteractor lateinit var swapInteractor: SwapInteractor
@Inject
lateinit var swapFeatureToggleManager: SwapFeatureToggleManager
private lateinit var pendingTransactionAdapter: PendingTransactionsAdapter private lateinit var pendingTransactionAdapter: PendingTransactionsAdapter
private lateinit var warningMessagesAdapter: WalletDetailWarningMessagesAdapter private lateinit var warningMessagesAdapter: WalletDetailWarningMessagesAdapter
@ -290,7 +294,7 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
WalletAction.DialogAction.ChooseTradeActionDialog( WalletAction.DialogAction.ChooseTradeActionDialog(
buyAllowed = selectedWallet.isAvailableToBuy(exchangeManager), buyAllowed = selectedWallet.isAvailableToBuy(exchangeManager),
sellAllowed = selectedWallet.isAvailableToSell(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( val actions = selectedWallet.getAvailableActions(
swapInteractor = swapInteractor, swapInteractor = swapInteractor,
exchangeManager = exchangeManager, exchangeManager = exchangeManager,
isExchangeFeatureOn = isExchangeServiceFeatureOn, swapFeatureToggleManager = swapFeatureToggleManager,
) )
binding.rowButtons.updateButtonsVisibility( binding.rowButtons.updateButtonsVisibility(
actions = actions, actions = actions,

View file

@ -20,6 +20,7 @@ import coil.size.Scale
import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.Analytics
import com.tangem.core.ui.fragments.setStatusBarColor import com.tangem.core.ui.fragments.setStatusBarColor
import com.tangem.core.ui.utils.OneTouchClickListener import com.tangem.core.ui.utils.OneTouchClickListener
import com.tangem.feature.swap.api.SwapFeatureToggleManager
import com.tangem.feature.swap.domain.SwapInteractor import com.tangem.feature.swap.domain.SwapInteractor
import com.tangem.tap.MainActivity import com.tangem.tap.MainActivity
import com.tangem.tap.common.analytics.events.MainScreen import com.tangem.tap.common.analytics.events.MainScreen
@ -56,6 +57,9 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
@Inject @Inject
lateinit var swapInteractor: SwapInteractor lateinit var swapInteractor: SwapInteractor
@Inject
lateinit var swapFeatureToggleManager: SwapFeatureToggleManager
private lateinit var warningsAdapter: WarningMessagesAdapter private lateinit var warningsAdapter: WarningMessagesAdapter
private val binding: FragmentWalletBinding by viewBinding(FragmentWalletBinding::bind) private val binding: FragmentWalletBinding by viewBinding(FragmentWalletBinding::bind)
@ -170,6 +174,7 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
} }
walletView.swapInteractor = swapInteractor walletView.swapInteractor = swapInteractor
walletView.swapFeatureToggleManager = swapFeatureToggleManager
walletView.onNewState(state) walletView.onNewState(state)

View file

@ -135,6 +135,7 @@ class SingleWalletView : WalletView() {
isExchangeServiceFeatureEnabled: Boolean, isExchangeServiceFeatureEnabled: Boolean,
) { ) {
val swapInteractor = this.swapInteractor ?: return val swapInteractor = this.swapInteractor ?: return
val swapFeatureToggleManager = this.swapFeatureToggleManager ?: return
val exchangeManager = store.state.globalState.exchangeManager val exchangeManager = store.state.globalState.exchangeManager
binding?.rowButtons?.apply { binding?.rowButtons?.apply {
@ -154,7 +155,7 @@ class SingleWalletView : WalletView() {
val actions = walletData.getAvailableActions( val actions = walletData.getAvailableActions(
swapInteractor = swapInteractor, swapInteractor = swapInteractor,
exchangeManager = exchangeManager, exchangeManager = exchangeManager,
isExchangeFeatureOn = isExchangeServiceFeatureEnabled, swapFeatureToggleManager = swapFeatureToggleManager,
) )
binding?.rowButtons?.updateButtonsVisibility( binding?.rowButtons?.updateButtonsVisibility(
actions = actions, actions = actions,

View file

@ -1,5 +1,6 @@
package com.tangem.tap.features.wallet.ui.wallet package com.tangem.tap.features.wallet.ui.wallet
import com.tangem.feature.swap.api.SwapFeatureToggleManager
import com.tangem.feature.swap.domain.SwapInteractor import com.tangem.feature.swap.domain.SwapInteractor
import com.tangem.tap.features.wallet.redux.WalletState import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.features.wallet.ui.WalletFragment import com.tangem.tap.features.wallet.ui.WalletFragment
@ -8,6 +9,7 @@ import com.tangem.wallet.databinding.FragmentWalletBinding
abstract class WalletView { abstract class WalletView {
var swapInteractor: SwapInteractor? = null var swapInteractor: SwapInteractor? = null
var swapFeatureToggleManager: SwapFeatureToggleManager? = null
protected var fragment: WalletFragment? = null protected var fragment: WalletFragment? = null
protected var binding: FragmentWalletBinding? = null protected var binding: FragmentWalletBinding? = null

View file

@ -4,6 +4,7 @@ import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.tangem.Message import com.tangem.Message
import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras
import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager
import com.tangem.blockchain.blockchains.optimism.OptimismWalletManager
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.BlockchainSdkError import com.tangem.blockchain.common.BlockchainSdkError
@ -155,56 +156,31 @@ class TransactionManagerImpl(
val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" } val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" }
val walletManager = getActualWalletManager(blockchain, derivationPath) val walletManager = getActualWalletManager(blockchain, derivationPath)
if (walletManager is EthereumWalletManager) { if (walletManager is EthereumWalletManager) {
val gasLimit = getGasLimit( if (walletManager is OptimismWalletManager) {
evmWalletManager = walletManager, return getFeeForOptimismBlockchain(
walletManager = walletManager,
amount = createAmount(amountToSend, currencyToSend, blockchain),
destinationAddress = destinationAddress,
data = data,
)
}
return getFeeForEthereumBlockchain(
walletManager = walletManager,
blockchain = blockchain, blockchain = blockchain,
amount = amountToSend, amountToSend = amountToSend,
currency = currencyToSend, currency = currencyToSend,
destinationAddress = destinationAddress, destinationAddress = destinationAddress,
data = data, data = data,
).let { increaseBy = increaseBy,
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)
}
}
} else { } else {
val fee = (walletManager as? TransactionSender)?.getFee( return getFeeForBlockchain(
amount = createAmount(amountToSend, currencyToSend, blockchain), walletManager = walletManager,
destination = destinationAddress, amountToSend = amountToSend,
) ?: error("Cannot cast to TransactionSender") currency = currencyToSend,
when (fee) { blockchain = blockchain,
is Result.Success -> { destinationAddress = destinationAddress,
// 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)
}
}
} }
} }
@ -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") @Suppress("LongParameterList")
private suspend fun getGasLimit( private suspend fun getGasLimit(
evmWalletManager: EthereumWalletManager, evmWalletManager: EthereumWalletManager,

View file

@ -1,10 +1,6 @@
[ [
{ {
"name": "INACTIVE_TEST_FEATURE_ENABLED", "name": "OPTIMISM_SWAP_FEATURE_ENABLED",
"version": "undefined" "version": "4.3.1"
},
{
"name": "ACTIVE2_TEST_FEATURE_ENABLED",
"version": "1.0.0"
} }
] ]

View file

@ -42,7 +42,7 @@ internal class DevFeatureTogglesManager(
.toMutableMap() .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<String, Boolean> = featureTogglesMap override fun getFeatureToggles(): Map<String, Boolean> = featureTogglesMap

View file

@ -25,7 +25,7 @@ internal class ProdFeatureTogglesManager(
.associateToggles(currentVersion = versionProvider.get() ?: "") .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) @VisibleForTesting(otherwise = VisibleForTesting.NONE)
fun getProdFeatureToggles() = featureToggles fun getProdFeatureToggles() = featureToggles

View file

@ -144,7 +144,7 @@ internal class DevFeatureTogglesManagerTest {
@Test @Test
fun `get feature availability if feature toggle exists`() { fun `get feature availability if feature toggle exists`() {
val featureToggles = mutableMapOf( val featureToggles = mutableMapOf(
"INACTIVE_TEST_FEATURE_ENABLED" to false, "INACTIVE_TEST_FEATURE_ENABLED" to true,
"ACTIVE2_TEST_FEATURE_ENABLED" to true, "ACTIVE2_TEST_FEATURE_ENABLED" to true,
) )
manager.setFeatureToggles(featureToggles) manager.setFeatureToggles(featureToggles)

View file

@ -105,7 +105,7 @@ internal class ProdFeatureTogglesManagerTest {
@Test @Test
fun `get feature availability if feature toggle exists`() { fun `get feature availability if feature toggle exists`() {
val featureToggles = mapOf( val featureToggles = mapOf(
"INACTIVE_TEST_FEATURE_ENABLED" to false, "INACTIVE_TEST_FEATURE_ENABLED" to true,
"ACTIVE2_TEST_FEATURE_ENABLED" to true, "ACTIVE2_TEST_FEATURE_ENABLED" to true,
) )
manager.setProdFeatureToggles(featureToggles) manager.setProdFeatureToggles(featureToggles)

1
features/swap/api/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -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)
}

View file

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="com.tangem.feature.swap.api" />

View file

@ -0,0 +1,7 @@
package com.tangem.feature.swap.api
/** Feature toggles manager of "swap" feature */
interface SwapFeatureToggleManager {
val isOptimismSwapEnabled: Boolean
}

View file

@ -10,6 +10,7 @@ plugins {
dependencies { dependencies {
/** Core modules */ /** Core modules */
implementation(project(":core:analytics")) implementation(project(":core:analytics"))
implementation(project(":core:featuretoggles"))
implementation(project(":core:utils")) implementation(project(":core:utils"))
implementation(project(":core:ui")) implementation(project(":core:ui"))
@ -27,6 +28,9 @@ dependencies {
implementation(deps.compose.coil) implementation(deps.compose.coil)
implementation(deps.compose.constraintLayout) implementation(deps.compose.constraintLayout)
/** Api */
implementation(project(":features:swap:api"))
/** Domain */ /** Domain */
implementation(project(":features:swap:domain")) implementation(project(":features:swap:domain"))

View file

@ -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)
}
}

View file

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

View file

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

View file

@ -438,7 +438,7 @@ internal class SwapViewModel @Inject constructor(
companion object { companion object {
private const val INITIAL_AMOUNT = "" private const val INITIAL_AMOUNT = ""
private const val UPDATE_DELAY = 10000L private const val UPDATE_DELAY = 1000000L
private const val DEBOUNCE_AMOUNT_DELAY = 1000L private const val DEBOUNCE_AMOUNT_DELAY = 1000L
} }
} }

View file

@ -68,7 +68,7 @@ kotlinSerialization = "1.4.1"
# endregion Other libraries # endregion Other libraries
# region Tangem # region Tangem
tangemBlockchainSdk = "develop-176" tangemBlockchainSdk = "develop-177"
# tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds # tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds
tangemCardSdk = "develop-204" tangemCardSdk = "develop-204"
# tangemCardSdk = "0.0.1" # Keep it! - used for local builds # tangemCardSdk = "0.0.1" # Keep it! - used for local builds

View file

@ -50,6 +50,7 @@ include(":libs:auth")
include(":features:referral:data") include(":features:referral:data")
include(":features:referral:domain") include(":features:referral:domain")
include(":features:referral:presentation") include(":features:referral:presentation")
include(":features:swap:api")
include(":features:swap:data") include(":features:swap:data")
include(":features:swap:domain") include(":features:swap:domain")
include(":features:swap:presentation") include(":features:swap:presentation")