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: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"))

View file

@ -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<CurrencyAction> {
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,
)
}

View file

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

View file

@ -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<Walle
@Inject
lateinit var swapInteractor: SwapInteractor
@Inject
lateinit var swapFeatureToggleManager: SwapFeatureToggleManager
private lateinit var warningsAdapter: WarningMessagesAdapter
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.swapFeatureToggleManager = swapFeatureToggleManager
walletView.onNewState(state)

View file

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

View file

@ -1,5 +1,6 @@
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.tap.features.wallet.redux.WalletState
import com.tangem.tap.features.wallet.ui.WalletFragment
@ -8,6 +9,7 @@ import com.tangem.wallet.databinding.FragmentWalletBinding
abstract class WalletView {
var swapInteractor: SwapInteractor? = null
var swapFeatureToggleManager: SwapFeatureToggleManager? = null
protected var fragment: WalletFragment? = 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.blockchain.blockchains.ethereum.EthereumTransactionExtras
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.Blockchain
import com.tangem.blockchain.common.BlockchainSdkError
@ -155,11 +156,83 @@ class TransactionManagerImpl(
val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" }
val walletManager = getActualWalletManager(blockchain, derivationPath)
if (walletManager is EthereumWalletManager) {
if (walletManager is OptimismWalletManager) {
return getFeeForOptimismBlockchain(
walletManager = walletManager,
amount = createAmount(amountToSend, currencyToSend, blockchain),
destinationAddress = destinationAddress,
data = data,
)
}
return getFeeForEthereumBlockchain(
walletManager = walletManager,
blockchain = blockchain,
amountToSend = amountToSend,
currency = currencyToSend,
destinationAddress = destinationAddress,
data = data,
increaseBy = increaseBy,
)
} else {
return getFeeForBlockchain(
walletManager = walletManager,
amountToSend = amountToSend,
currency = currencyToSend,
blockchain = blockchain,
destinationAddress = destinationAddress,
)
}
}
override fun getBlockchainInfo(networkId: String): ProxyNetworkInfo {
val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" }
return ProxyNetworkInfo(
name = blockchain.fullName,
blockchainId = blockchain.id,
blockchainCurrency = blockchain.currency,
)
}
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 = currencyToSend,
currency = currency,
destinationAddress = destinationAddress,
data = data,
).let {
@ -188,17 +261,24 @@ class TransactionManagerImpl(
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 {
val fee = (walletManager as? TransactionSender)?.getFee(
amount = createAmount(amountToSend, currencyToSend, blockchain),
destination = destinationAddress,
) ?: error("Cannot cast to TransactionSender")
walletManager.getFee(amount, destinationAddress, data)
}
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")),
gasLimit = walletManager.gasLimit ?: BigInteger.ZERO,
fee = convertToProxyAmount(fee.data.lastOrNull() ?: error("no fee found")),
)
}
is Result.Failure -> {
@ -206,16 +286,6 @@ class TransactionManagerImpl(
}
}
}
}
override fun getBlockchainInfo(networkId: String): ProxyNetworkInfo {
val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" }
return ProxyNetworkInfo(
name = blockchain.fullName,
blockchainId = blockchain.id,
blockchainCurrency = blockchain.currency,
)
}
@Suppress("LongParameterList")
private suspend fun getGasLimit(

View file

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

View file

@ -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<String, Boolean> = featureTogglesMap

View file

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

View file

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

View file

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

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 {
/** 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"))

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 {
private const val INITIAL_AMOUNT = ""
private const val UPDATE_DELAY = 10000L
private const val UPDATE_DELAY = 1000000L
private const val DEBOUNCE_AMOUNT_DELAY = 1000L
}
}

View file

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

View file

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