From c50c2a668b9a4d4d69ac54e768e55d0157bde988 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 3 Mar 2023 18:50:00 +0300 Subject: [PATCH 01/14] Updated on 2026-08-14 --- .../tangem/tap/common/analytics/events/AnalyticsParam.kt | 8 ++++++++ .../java/com/tangem/tap/common/analytics/events/Basic.kt | 7 ++++++- .../tap/domain/walletconnect/WalletConnectSdkHelper.kt | 3 +++ .../features/send/redux/middlewares/SendMiddleware.kt | 3 +++ .../java/com/tangem/tap/proxy/TransactionManagerImpl.kt | 9 ++++++++- app/src/main/java/com/tangem/tap/proxy/di/ProxyModule.kt | 5 +++-- 6 files changed, 31 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt index cebc4c405e..ce8e64b0f6 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt @@ -70,7 +70,15 @@ sealed class AnalyticsParam { object MyWallets : ScannedFrom("My Wallets") } + sealed class TxSentFrom(val value: String) { + object Send : TxSentFrom("Send") + object Swap : TxSentFrom("Swap") + object WalletConnect : TxSentFrom("WalletConnect") + object Sell : TxSentFrom("Sell") + } + companion object Key { + const val Source = "Source" const val Batch = "Batch" const val ProductType = "Product Type" const val Firmware = "Firmware" diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/Basic.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/Basic.kt index 465cfe98bf..76b00ef40f 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/Basic.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/Basic.kt @@ -16,7 +16,7 @@ sealed class Basic( ) : Basic( event = "Card Was Scanned", params = mapOf( - "Source" to source.value, + AnalyticsParam.Source to source.value, ), ) @@ -38,6 +38,11 @@ sealed class Basic( params = mapOf(AnalyticsParam.Currency to currency.value), ) + class TransactionSent(sentFrom: AnalyticsParam.TxSentFrom) : Basic( + event = "Transaction sent", + params = mapOf(AnalyticsParam.Source to sentFrom.value), + ) + class ScanError(error: Throwable) : Basic( event = "Scan", error = error, diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt index 316d44de9c..9dac430e4c 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt @@ -25,6 +25,8 @@ import com.tangem.common.extensions.toHexString import com.tangem.core.analytics.Analytics import com.tangem.crypto.CryptoUtils import com.tangem.operations.sign.SignHashCommand +import com.tangem.tap.common.analytics.events.AnalyticsParam +import com.tangem.tap.common.analytics.events.Basic import com.tangem.tap.common.analytics.events.WalletConnect import com.tangem.tap.common.extensions.safeUpdate import com.tangem.tap.common.extensions.toFormattedString @@ -144,6 +146,7 @@ class WalletConnectSdkHelper { ) return when (result) { SimpleResult.Success -> { + Analytics.send(Basic.TransactionSent(AnalyticsParam.TxSentFrom.WalletConnect)) HEX_PREFIX + data.walletManager.wallet.recentTransactions.last().hash } is SimpleResult.Failure -> { diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt index d86397139b..00d9ef32c1 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt @@ -20,6 +20,7 @@ import com.tangem.domain.common.TapWorkarounds.isStart2Coin import com.tangem.domain.common.extensions.withMainContext import com.tangem.tap.DELAY_SDK_DIALOG_CLOSE import com.tangem.tap.common.analytics.events.AnalyticsParam +import com.tangem.tap.common.analytics.events.Basic import com.tangem.tap.common.analytics.events.Token import com.tangem.tap.common.extensions.dispatchDialogShow import com.tangem.tap.common.extensions.dispatchErrorNotification @@ -234,8 +235,10 @@ private fun sendTransaction( dispatch(SendAction.SendSuccess) if (externalTransactionData != null) { + Analytics.send(Basic.TransactionSent(AnalyticsParam.TxSentFrom.Sell)) dispatch(WalletAction.TradeCryptoAction.FinishSelling(externalTransactionData.transactionId)) } else { + Analytics.send(Basic.TransactionSent(AnalyticsParam.TxSentFrom.Send)) dispatch(NavigationAction.PopBackTo()) } scope.launch(Dispatchers.IO) { diff --git a/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt b/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt index 30a360ad10..49da15b6c1 100644 --- a/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt +++ b/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt @@ -17,12 +17,15 @@ import com.tangem.blockchain.extensions.SimpleResult import com.tangem.blockchain.extensions.isNetworkError import com.tangem.common.core.TangemSdkError import com.tangem.common.extensions.hexToBytes +import com.tangem.core.analytics.api.AnalyticsHandler import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.lib.crypto.TransactionManager import com.tangem.lib.crypto.models.Currency import com.tangem.lib.crypto.models.ProxyAmount import com.tangem.lib.crypto.models.ProxyNetworkInfo import com.tangem.lib.crypto.models.transactions.SendTxResult +import com.tangem.tap.common.analytics.events.AnalyticsParam +import com.tangem.tap.common.analytics.events.Basic import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.domain.TangemSigner import com.tangem.tap.domain.tokens.models.BlockchainNetwork @@ -31,6 +34,7 @@ import java.math.BigDecimal class TransactionManagerImpl( private val appStateHolder: AppStateHolder, + private val analytics: AnalyticsHandler, ) : TransactionManager { override suspend fun sendApproveTransaction( @@ -165,7 +169,10 @@ class TransactionManagerImpl( private fun handleSendResult(result: SimpleResult): SendTxResult { when (result) { - is SimpleResult.Success -> return SendTxResult.Success + is SimpleResult.Success -> { + analytics.send(Basic.TransactionSent(AnalyticsParam.TxSentFrom.Swap)) + return SendTxResult.Success + } is SimpleResult.Failure -> { if (result.isNetworkError()) return SendTxResult.NetworkError(result.error) val error = result.error as? BlockchainSdkError ?: return SendTxResult.UnknownError() diff --git a/app/src/main/java/com/tangem/tap/proxy/di/ProxyModule.kt b/app/src/main/java/com/tangem/tap/proxy/di/ProxyModule.kt index a1fa2cd32a..8cf83557a8 100644 --- a/app/src/main/java/com/tangem/tap/proxy/di/ProxyModule.kt +++ b/app/src/main/java/com/tangem/tap/proxy/di/ProxyModule.kt @@ -1,5 +1,6 @@ package com.tangem.tap.proxy.di +import com.tangem.core.analytics.api.AnalyticsHandler import com.tangem.lib.crypto.DerivationManager import com.tangem.lib.crypto.TransactionManager import com.tangem.lib.crypto.UserWalletManager @@ -33,8 +34,8 @@ class ProxyModule { @Provides @Singleton - fun provideTransactionManager(appStateHolder: AppStateHolder): TransactionManager { - return TransactionManagerImpl(appStateHolder) + fun provideTransactionManager(appStateHolder: AppStateHolder, analytics: AnalyticsHandler): TransactionManager { + return TransactionManagerImpl(appStateHolder, analytics) } @Provides From b7510a7aa45e4d9ef1af6b970ed097ccd63d1beb Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 6 Mar 2023 10:34:04 +0300 Subject: [PATCH 02/14] Updated on 2026-08-14 --- .../com/tangem/tap/common/IntentHandler.kt | 18 ++++++++++++++++++ .../tap/common/analytics/events/Token.kt | 6 ++++++ .../walletconnect/WalletConnectSdkHelper.kt | 3 ++- 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/tangem/tap/common/IntentHandler.kt b/app/src/main/java/com/tangem/tap/common/IntentHandler.kt index f1cd3b1027..12451bdd67 100644 --- a/app/src/main/java/com/tangem/tap/common/IntentHandler.kt +++ b/app/src/main/java/com/tangem/tap/common/IntentHandler.kt @@ -1,15 +1,20 @@ package com.tangem.tap.common import android.content.Intent +import android.net.Uri import android.nfc.NfcAdapter import android.nfc.Tag import android.os.Build +import com.tangem.core.analytics.Analytics +import com.tangem.tap.common.analytics.events.AnalyticsParam +import com.tangem.tap.common.analytics.events.Token import com.tangem.tap.common.extensions.removePrefixOrNull import com.tangem.tap.domain.walletconnect.WalletConnectManager import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction import com.tangem.tap.features.home.redux.HomeAction import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.features.welcome.redux.WelcomeAction +import com.tangem.tap.network.exchangeServices.ExchangeUrlBuilder import com.tangem.tap.scope import com.tangem.tap.store import kotlinx.coroutines.delay @@ -27,6 +32,7 @@ class IntentHandler { fun handleIntent(intent: Intent?, hasSavedUserWallets: Boolean) { handleBackgroundScan(intent, hasSavedUserWallets) handleWalletConnectLink(intent) + handleBuyCurrencyCallback(intent) handleSellCurrencyCallback(intent) } @@ -72,6 +78,18 @@ class IntentHandler { return true } + private fun handleBuyCurrencyCallback(intent: Intent?) { + val data = intent?.data ?: return + + val successUri = Uri.parse(ExchangeUrlBuilder.SUCCESS_URL) + if (data.host == successUri.host && data.authority == successUri.authority) { + val currency = store.state.walletState.selectedCurrency ?: return + + val currencyType = AnalyticsParam.CurrencyType.Currency(currency) + Analytics.send(Token.Bought(currencyType)) + } + } + fun handleSellCurrencyCallback(intent: Intent?) { try { val transactionID = diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/Token.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/Token.kt index 0ad3ca514e..63dd109942 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/Token.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/Token.kt @@ -46,6 +46,12 @@ sealed class Token( params = mapOf("Token" to type.value), ) + class Bought(type: CurrencyType) : Token( + category = "Token", + event = "Token bought", + params = mapOf("Token" to type.value), + ) + sealed class Receive( event: String, params: Map = mapOf(), diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt index 9dac430e4c..b963a290d4 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt @@ -146,7 +146,8 @@ class WalletConnectSdkHelper { ) return when (result) { SimpleResult.Success -> { - Analytics.send(Basic.TransactionSent(AnalyticsParam.TxSentFrom.WalletConnect)) + val sentFrom = AnalyticsParam.TxSentFrom.WalletConnect + Analytics.send(Basic.TransactionSent(sentFrom)) HEX_PREFIX + data.walletManager.wallet.recentTransactions.last().hash } is SimpleResult.Failure -> { From 07400563cfa16393bce9e4842580dd7dc69acd72 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 3 Mar 2023 17:17:40 +0300 Subject: [PATCH 03/14] Updated on 2026-08-14 --- .../tap/common/extensions/WalletManager.kt | 4 ++ .../com/tangem/tap/domain/TapWalletManager.kt | 9 ++- .../domain/walletStores/WalletStoresError.kt | 4 +- .../DefaultWalletStoresManager.kt | 4 +- .../DefaultWalletAmountsRepository.kt | 57 +++++++++++++------ .../DefaultWalletManagersRepository.kt | 2 +- .../redux/middlewares/WalletMiddleware.kt | 2 +- 7 files changed, 56 insertions(+), 26 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt b/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt index 1426f3a118..0939e05be9 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt @@ -25,6 +25,10 @@ import timber.log.Timber /** [REDACTED_AUTHOR] */ +@Deprecated( + message = "Use WalletStoresManager.fetch({userWalletId}, refresh = true) (to update all user wallet tokens)" + + "or WalletCurrenciesManager.update(...) (to update only one user wallet blockchain and its tokens) instead", +) @Suppress("MagicNumber") suspend fun WalletManager.safeUpdate(): Result = try { val scanResponse = store.state.globalState.scanResponse diff --git a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt index aae069fed0..15a3835869 100644 --- a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt @@ -72,9 +72,12 @@ class TapWalletManager { .doOnFailure { error -> val errorAction = when (error) { is WalletStoresError -> when (error) { - is WalletStoresError.FetchFiatRatesError, - is WalletStoresError.UpdateWalletManagerError, - -> WalletAction.LoadData.Failure(error = null) + is WalletStoresError.FetchFiatRatesError -> WalletAction.LoadData.Failure(error = null) + is WalletStoresError.UpdateWalletManagerTokensError -> WalletAction.LoadData.Failure( + error = TapError.WalletManager.InternalError( + message = error.cause.localizedMessage ?: error.customMessage, + ), + ) is WalletStoresError.WalletManagerNotCreated -> WalletAction.LoadData.Failure( error = TapError.WalletManager.CreationError, ) diff --git a/app/src/main/java/com/tangem/tap/domain/walletStores/WalletStoresError.kt b/app/src/main/java/com/tangem/tap/domain/walletStores/WalletStoresError.kt index 826c158a8b..20fe0b0848 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletStores/WalletStoresError.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletStores/WalletStoresError.kt @@ -35,10 +35,10 @@ sealed class WalletStoresError(code: Int) : TangemError(code) { } @Suppress("MagicNumber") - class UpdateWalletManagerError( + class UpdateWalletManagerTokensError( blockchain: Blockchain, override val cause: Throwable, ) : WalletStoresError(600015) { - override var customMessage: String = "Unable to update wallet manager for currency $blockchain: $cause" + override var customMessage: String = "Unable to update wallet manager tokens for currency $blockchain: $cause" } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletStores/implementation/DefaultWalletStoresManager.kt b/app/src/main/java/com/tangem/tap/domain/walletStores/implementation/DefaultWalletStoresManager.kt index 06b2f358ae..00f000f14d 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletStores/implementation/DefaultWalletStoresManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletStores/implementation/DefaultWalletStoresManager.kt @@ -153,7 +153,7 @@ internal class DefaultWalletStoresManager( .flatMapOnFailure { error -> when (error) { is WalletStoresError.WalletManagerNotCreated, - is WalletStoresError.UpdateWalletManagerError, + is WalletStoresError.UpdateWalletManagerTokensError, -> storeWalletStore(null) else -> CompletionResult.Failure(error) } @@ -178,7 +178,7 @@ internal class DefaultWalletStoresManager( .flatMapOnFailure { error -> when (error) { is WalletStoresError.WalletManagerNotCreated, - is WalletStoresError.UpdateWalletManagerError, + is WalletStoresError.UpdateWalletManagerTokensError, -> CompletionResult.Success(Unit) else -> CompletionResult.Failure(error) } diff --git a/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/DefaultWalletAmountsRepository.kt b/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/DefaultWalletAmountsRepository.kt index dff146b175..f42dd9ced5 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/DefaultWalletAmountsRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/DefaultWalletAmountsRepository.kt @@ -2,7 +2,9 @@ package com.tangem.tap.domain.walletStores.repository.implementation import com.tangem.blockchain.blockchains.solana.RentProvider import com.tangem.blockchain.common.AmountType +import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.BlockchainSdkError +import com.tangem.blockchain.common.TransactionHistoryProvider import com.tangem.blockchain.common.Wallet import com.tangem.blockchain.common.WalletManager import com.tangem.blockchain.extensions.Result.Failure @@ -17,9 +19,9 @@ import com.tangem.common.map import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.domain.common.ScanResponse import com.tangem.domain.common.util.UserWalletId +import com.tangem.tap.common.TestActions import com.tangem.tap.common.entities.FiatCurrency import com.tangem.tap.common.extensions.replaceByOrAdd -import com.tangem.tap.common.extensions.safeUpdate import com.tangem.tap.domain.model.UserWallet import com.tangem.tap.domain.model.WalletStoreModel import com.tangem.tap.domain.walletStores.WalletStoresError @@ -37,6 +39,7 @@ import com.tangem.tap.domain.walletStores.repository.implementation.utils.update import com.tangem.tap.domain.walletStores.storage.WalletManagerStorage import com.tangem.tap.domain.walletStores.storage.WalletStoresStorage import com.tangem.tap.features.demo.DemoHelper +import com.tangem.tap.features.demo.isDemoCard import com.tangem.tap.features.wallet.models.Currency import com.tangem.tap.features.wallet.models.PendingTransactionType import com.tangem.tap.features.wallet.models.filterByCoin @@ -47,10 +50,12 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.withContext import timber.log.Timber import java.math.BigDecimal +import kotlin.time.Duration @Suppress("LargeClass") internal class DefaultWalletAmountsRepository( @@ -118,8 +123,12 @@ internal class DefaultWalletAmountsRepository( walletStores: List?, fiatCurrency: FiatCurrency, ): CompletionResult { - val walletStoresInternal = walletStores ?: getWalletStores(userWallets) + // FIXME: Use NetworkConnectionManager when it is added to DI + if (!NetworkConnectivity.getInstance().isOnlineOrConnecting()) { + return CompletionResult.Failure(WalletStoresError.NoInternetConnection) + } + val walletStoresInternal = walletStores ?: getWalletStores(userWallets) val currencies = walletStoresInternal .asSequence() .flatMap { it.walletsData } @@ -177,6 +186,14 @@ internal class DefaultWalletAmountsRepository( scanResponse: ScanResponse, walletStores: List, ): CompletionResult = coroutineScope { + // FIXME: Use NetworkConnectionManager when it is added to DI + if (!NetworkConnectivity.getInstance().isOnlineOrConnecting()) { + walletStores.forEach { + updateWalletStoreWithUnreachable(it) + } + return@coroutineScope CompletionResult.Failure(WalletStoresError.NoInternetConnection) + } + walletStores.map { walletStore -> async { // TODO: Find wallet manager via [com.tangem.tap.domain.walletStores.repository.WalletManagersRepository] @@ -206,8 +223,8 @@ internal class DefaultWalletAmountsRepository( updateWalletStoreWithUnreachable(walletStore) } else -> { - withInternetConnection { walletManager.safeUpdate() } - .map { updateWalletManagerWithAmounts(userWalletId, walletManager) } + updateWalletManager(scanResponse, walletManager) + .map { updateWalletManagerInStorage(userWalletId, walletManager) } .flatMap { updateWalletStoreWithAmounts( walletStore = walletStore, @@ -228,6 +245,24 @@ internal class DefaultWalletAmountsRepository( } } + private suspend fun updateWalletManager( + scanResponse: ScanResponse, + walletManager: WalletManager, + demoCardsDelay: Duration = with(Duration) { 500.milliseconds }, + ): CompletionResult = catching { + if (scanResponse.isDemoCard() || TestActions.testAmountInjectionForWalletManagerEnabled) { + delay(demoCardsDelay) + TestActions.testAmountInjectionForWalletManagerEnabled = false + } else { + walletManager.update() + + val wallet = walletManager.wallet + if (wallet.blockchain == Blockchain.SaltPay && walletManager is TransactionHistoryProvider) { + walletManager.getTransactionHistory(wallet.address, wallet.blockchain, wallet.getTokens()) + } + } + } + private suspend fun fetchWalletStoreRentIfNeeded( walletStore: WalletStoreModel, walletManager: WalletManager, @@ -426,19 +461,7 @@ internal class DefaultWalletAmountsRepository( } } - private suspend inline fun withInternetConnection(crossinline block: suspend () -> Unit): CompletionResult { - return if (!NetworkConnectivity.getInstance().isOnlineOrConnecting()) { - val error = WalletStoresError.NoInternetConnection - Timber.e(error) - CompletionResult.Failure(error) - } else { - withContext(Dispatchers.IO) { - catching { block() } - } - } - } - - private suspend fun updateWalletManagerWithAmounts( + private suspend fun updateWalletManagerInStorage( userWalletId: UserWalletId, walletManager: WalletManager, ) = withContext(Dispatchers.Default) { diff --git a/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/DefaultWalletManagersRepository.kt b/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/DefaultWalletManagersRepository.kt index f1e521cd40..12af45a458 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/DefaultWalletManagersRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/DefaultWalletManagersRepository.kt @@ -186,7 +186,7 @@ internal class DefaultWalletManagersRepository( walletManager } .mapFailure { - val error = WalletStoresError.UpdateWalletManagerError( + val error = WalletStoresError.UpdateWalletManagerTokensError( blockchain = walletManager.wallet.blockchain, cause = it, ) diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt index 276265fa9e..1866921b7e 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt @@ -143,7 +143,7 @@ class WalletMiddleware { Timber.e("Unable to proceed with changed network state, no user wallet selected") return } - scope.launch { globalState.tapWalletManager.loadData(selectedUserWallet) } + scope.launch { globalState.tapWalletManager.loadData(selectedUserWallet, refresh = true) } } is WalletAction.CopyAddress -> { Analytics.send(Token.Receive.ButtonCopyAddress()) From 17a17f3eb654ebf861dfd1fddf36c0de6a323795 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 6 Mar 2023 16:13:05 +0300 Subject: [PATCH 04/14] Updated on 2026-08-14 --- .../implementation/DefaultWalletManagersRepository.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/DefaultWalletManagersRepository.kt b/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/DefaultWalletManagersRepository.kt index 12af45a458..c7848db278 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/DefaultWalletManagersRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/DefaultWalletManagersRepository.kt @@ -204,7 +204,7 @@ internal class DefaultWalletManagersRepository( .firstOrNull() ?.get(userWalletId) ?.let { userWalletManagers -> - if (blockchain == null || derivationPath == null) { + if (blockchain == null) { userWalletManagers.firstOrNull() } else { userWalletManagers.firstOrNull { From f0c92b05cbfef8ac861c9863a670c3571942b364 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 6 Mar 2023 18:10:51 +0300 Subject: [PATCH 05/14] Updated on 2026-08-14 --- .../tap/proxy/TransactionManagerImpl.kt | 109 ++++++++-- buildSrc/src/main/java/Versions.kt | 4 +- .../feature/swap/domain/SwapInteractor.kt | 14 +- .../feature/swap/domain/SwapInteractorImpl.kt | 199 ++++++++---------- .../swap/domain/models/ui/SwapState.kt | 14 +- .../tangem/feature/swap/ui/StateBuilder.kt | 4 +- .../feature/swap/ui/SwapScreenContent.kt | 36 ++-- .../swap/viewmodels/SwapProcessDataState.kt | 9 +- .../feature/swap/viewmodels/SwapViewModel.kt | 19 +- .../tangem/lib/crypto/TransactionManager.kt | 9 +- .../com/tangem/lib/crypto/models/ProxyFee.kt | 8 + 11 files changed, 243 insertions(+), 182 deletions(-) create mode 100644 libs/crypto/src/main/java/com/tangem/lib/crypto/models/ProxyFee.kt diff --git a/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt b/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt index 49da15b6c1..bc8fe9a242 100644 --- a/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt +++ b/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt @@ -22,6 +22,7 @@ import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.lib.crypto.TransactionManager import com.tangem.lib.crypto.models.Currency import com.tangem.lib.crypto.models.ProxyAmount +import com.tangem.lib.crypto.models.ProxyFee import com.tangem.lib.crypto.models.ProxyNetworkInfo import com.tangem.lib.crypto.models.transactions.SendTxResult import com.tangem.tap.common.analytics.events.AnalyticsParam @@ -31,7 +32,11 @@ import com.tangem.tap.domain.TangemSigner import com.tangem.tap.domain.tokens.models.BlockchainNetwork import com.tangem.tap.tangemSdk import java.math.BigDecimal +import java.math.BigInteger +import java.math.MathContext +import java.math.RoundingMode +@Suppress("LargeClass") class TransactionManagerImpl( private val appStateHolder: AppStateHolder, private val analytics: AnalyticsHandler, @@ -40,7 +45,7 @@ class TransactionManagerImpl( override suspend fun sendApproveTransaction( networkId: String, feeAmount: BigDecimal, - estimatedGas: Int, + gasLimit: Int, destinationAddress: String, dataToSign: String, ): SendTxResult { @@ -53,7 +58,7 @@ class TransactionManagerImpl( amount = amount, blockchain = blockchain, feeAmount = feeAmount, - estimatedGas = estimatedGas, + gasLimit = gasLimit, destinationAddress = destinationAddress, dataToSign = dataToSign, ) @@ -63,7 +68,7 @@ class TransactionManagerImpl( networkId: String, amountToSend: BigDecimal, feeAmount: BigDecimal, - estimatedGas: Int, + gasLimit: Int, destinationAddress: String, dataToSign: String, isSwap: Boolean, @@ -82,7 +87,7 @@ class TransactionManagerImpl( amount = amount, blockchain = blockchain, feeAmount = feeAmount, - estimatedGas = estimatedGas, + gasLimit = gasLimit, destinationAddress = destinationAddress, dataToSign = dataToSign, ) @@ -94,7 +99,7 @@ class TransactionManagerImpl( amount: Amount, blockchain: Blockchain, feeAmount: BigDecimal, - estimatedGas: Int, + gasLimit: Int, destinationAddress: String, dataToSign: String, ): SendTxResult { @@ -102,12 +107,12 @@ class TransactionManagerImpl( amount = amount, fee = Amount(value = feeAmount, blockchain = blockchain), destination = destinationAddress, - ).copy(hash = dataToSign, extras = createExtras(walletManager, estimatedGas, dataToSign)) + ).copy(hash = dataToSign, extras = createExtras(walletManager, gasLimit, dataToSign)) val signer = transactionSigner(walletManager) val sendResult = try { - (walletManager as TransactionSender).send(txData, signer) + (walletManager as? TransactionSender)?.send(txData, signer) ?: error("Cannot cast to TransactionSender") } catch (ex: Exception) { FirebaseCrashlytics.getInstance().recordException(ex) return SendTxResult.UnknownError(ex) @@ -141,19 +146,54 @@ class TransactionManagerImpl( amountToSend: BigDecimal, currencyToSend: Currency, destinationAddress: String, - ): ProxyAmount { + data: String?, + ): ProxyFee { val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" } val walletManager = getActualWalletManager(blockchain) - val fee = (walletManager as TransactionSender).getFee( - amount = createAmount(amountToSend, currencyToSend, blockchain), - destination = destinationAddress, - ) - when (fee) { - is Result.Success -> { - return convertToProxyAmount(fee.data.firstOrNull() ?: error("no fee found")) + if (walletManager is EthereumWalletManager) { + val gasLimit = getGasLimit( + evmWalletManager = walletManager, + blockchain = blockchain, + amount = amountToSend, + currency = currencyToSend, + destinationAddress = destinationAddress, + data = data, + ) + 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) + } } - is Result.Failure -> { - error(fee.error.message ?: fee.error.customMessage) + } 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) + } } } } @@ -167,6 +207,37 @@ class TransactionManagerImpl( ) } + @Suppress("LongParameterList") + private suspend fun getGasLimit( + evmWalletManager: EthereumWalletManager, + blockchain: Blockchain, + amount: BigDecimal, + currency: Currency, + destinationAddress: String, + data: String?, + ): BigInteger { + val result = if (data.isNullOrEmpty()) { + evmWalletManager.getGasLimit( + amount = createAmount(amount, currency, blockchain), + destination = destinationAddress, + ) + } else { + evmWalletManager.getGasLimit( + amount = createAmount(amount, currency, blockchain), + destination = destinationAddress, + data = data, + ) + } + when (result) { + is Result.Success -> { + return result.data + } + is Result.Failure -> { + error(result.error.message ?: result.error.customMessage) + } + } + } + private fun handleSendResult(result: SimpleResult): SendTxResult { when (result) { is SimpleResult.Success -> { @@ -239,14 +310,14 @@ class TransactionManagerImpl( private fun createExtras( walletManager: WalletManager, - estimatedGas: Int, + gasLimit: Int, transactionHash: String, ): TransactionExtras? { return when (walletManager) { is EthereumWalletManager -> { return EthereumTransactionExtras( data = transactionHash.removePrefix(HEX_PREFIX).hexToBytes(), - gasLimit = estimatedGas.toBigInteger(), + gasLimit = gasLimit.toBigInteger(), ) } else -> { diff --git a/buildSrc/src/main/java/Versions.kt b/buildSrc/src/main/java/Versions.kt index adb41a5c61..b02e115cd7 100644 --- a/buildSrc/src/main/java/Versions.kt +++ b/buildSrc/src/main/java/Versions.kt @@ -61,10 +61,10 @@ object Versions { // endregion Other libraries // region Tangem - const val tangemBlockchainSdk = "develop-173" + const val tangemBlockchainSdk = "develop-174" // const val tangemBlockchainSdk = "0.0.1" // Keep it! - used for local builds - const val tangemCardSdk = "develop-198" + const val tangemCardSdk = "develop-199" // const val tangemCardSdk = "0.0.1" // Keep it! - used for local builds // endregion Tangem diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt index 4a5da9708f..1ea5c8378c 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt @@ -1,11 +1,11 @@ package com.tangem.feature.swap.domain import com.tangem.feature.swap.domain.models.SwapAmount -import com.tangem.feature.swap.domain.models.domain.ApproveModel import com.tangem.feature.swap.domain.models.domain.Currency -import com.tangem.feature.swap.domain.models.domain.SwapDataModel import com.tangem.feature.swap.domain.models.ui.FoundTokensState +import com.tangem.feature.swap.domain.models.ui.RequestApproveStateData import com.tangem.feature.swap.domain.models.ui.SwapState +import com.tangem.feature.swap.domain.models.ui.SwapStateData import com.tangem.feature.swap.domain.models.ui.TokensDataState import com.tangem.feature.swap.domain.models.ui.TxState @@ -42,15 +42,13 @@ interface SwapInteractor { * Gives permission to swap, this starts scan card process * * @param networkId network in which selected token - * @param estimatedGas estimated gas for transaction - * @param transactionData tx data to give approve, it loaded from 1inch in findBestQuote if needed + * @param approveData tx data to give approve, it loaded from 1inch in findBestQuote if needed * @param forTokenContractAddress token contract address for which needs permission */ @Throws(IllegalStateException::class) suspend fun givePermissionToSwap( networkId: String, - estimatedGas: Int, - transactionData: ApproveModel, + approveData: RequestApproveStateData, forTokenContractAddress: String, ): TxState @@ -76,7 +74,7 @@ interface SwapInteractor { * Starts swap transaction, perform sign transaction * * @param networkId network for tokens - * @param swapData tx data to swap, contains data to sign + * @param swapStateData tx data to swap, contains data to sign * @param currencyToSend [Currency] * @param currencyToGet [Currency] * @param amountToSwap amount to swap @@ -85,7 +83,7 @@ interface SwapInteractor { @Throws(IllegalStateException::class) suspend fun onSwap( networkId: String, - swapData: SwapDataModel, + swapStateData: SwapStateData, currencyToSend: Currency, currencyToGet: Currency, amountToSwap: String, 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 3b78f398f7..4127ecc179 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 @@ -4,10 +4,8 @@ import com.tangem.feature.swap.domain.cache.SwapDataCache import com.tangem.feature.swap.domain.converters.CryptoCurrencyConverter import com.tangem.feature.swap.domain.models.DataError import com.tangem.feature.swap.domain.models.SwapAmount -import com.tangem.feature.swap.domain.models.domain.ApproveModel import com.tangem.feature.swap.domain.models.domain.Currency import com.tangem.feature.swap.domain.models.domain.PreparedSwapConfigState -import com.tangem.feature.swap.domain.models.domain.SwapDataModel import com.tangem.feature.swap.domain.models.toStringWithRightOffset import com.tangem.feature.swap.domain.models.ui.AmountFormatter import com.tangem.feature.swap.domain.models.ui.FoundTokensState @@ -15,6 +13,7 @@ import com.tangem.feature.swap.domain.models.ui.PermissionDataState import com.tangem.feature.swap.domain.models.ui.PreselectTokens import com.tangem.feature.swap.domain.models.ui.RequestApproveStateData import com.tangem.feature.swap.domain.models.ui.SwapState +import com.tangem.feature.swap.domain.models.ui.SwapStateData import com.tangem.feature.swap.domain.models.ui.TokenBalanceData import com.tangem.feature.swap.domain.models.ui.TokenSwapInfo import com.tangem.feature.swap.domain.models.ui.TokenWithBalance @@ -110,19 +109,15 @@ internal class SwapInteractorImpl @Inject constructor( override suspend fun givePermissionToSwap( networkId: String, - estimatedGas: Int, - transactionData: ApproveModel, + approveData: RequestApproveStateData, forTokenContractAddress: String, ): TxState { - val increasedEstimatedGas = increaseByPercents(TWENTY_FIVE_PERCENTS, estimatedGas) - val gasPrice = transactionData.gasPrice.toBigDecimalOrNull() ?: error("cannot parse gasPrice") - val fee = transactionManager.calculateFee(networkId, gasPrice.toPlainString(), increasedEstimatedGas) val result = transactionManager.sendApproveTransaction( networkId = networkId, - feeAmount = fee, - estimatedGas = estimatedGas, - destinationAddress = transactionData.toAddress, - dataToSign = transactionData.data, + feeAmount = approveData.fee, + gasLimit = approveData.gasLimit, + destinationAddress = approveData.approveModel.toAddress, + dataToSign = approveData.approveModel.data, ) return when (result) { is SendTxResult.Success -> { @@ -156,32 +151,9 @@ internal class SwapInteractorImpl @Inject constructor( allowPermissionsHandler.removeAddressFromProgress(fromTokenAddress) transactionManager.updateWalletManager(networkId) } - // load initial quotes data, it works despite balance - val quotesData = loadQuoteData( - networkId = networkId, - fromTokenAddress = fromTokenAddress, - toTokenAddress = toTokenAddress, - amount = amount, - fromToken = fromToken, - toToken = toToken, - ) - // get fee from loaded quotes data, if error, use blockchain fee for 0 amount tx - val fee = getInchFee(quotesData) - val isFeeEnough = checkFeeIsEnough( - fee = fee, - spendAmount = amount, - networkId = networkId, - fromToken = fromToken, - ) - val isBalanceEnough = isBalanceEnough(fromToken, amount, fee) - val preparedSwapConfigState = PreparedSwapConfigState( - isAllowedToSpend = isAllowedToSpend, - isBalanceEnough = isBalanceEnough, - isFeeEnough = isFeeEnough, - ) - return if (isAllowedToSpend && isBalanceEnough && isFeeEnough) { - // if enough balance, fee and spend was allowed, request swap data - val swapData = loadSwapData( + val isBalanceWithoutFeeEnough = isBalanceEnough(fromToken, amount, null) + return if (isAllowedToSpend && isBalanceWithoutFeeEnough) { + loadSwapData( networkId = networkId, fromTokenAddress = fromTokenAddress, toTokenAddress = toTokenAddress, @@ -189,42 +161,36 @@ internal class SwapInteractorImpl @Inject constructor( toToken = toToken, amount = amount, ) - if (swapData is SwapState.QuotesLoadedState) { - swapData.copy(preparedSwapConfigState = preparedSwapConfigState) - } else { - swapData - } } else { - if (quotesData is SwapState.QuotesLoadedState) { - quotesData.copy(preparedSwapConfigState = preparedSwapConfigState) - } else { - quotesData - } + loadQuoteData( + networkId = networkId, + fromTokenAddress = fromTokenAddress, + toTokenAddress = toTokenAddress, + amount = amount, + fromToken = fromToken, + toToken = toToken, + isAllowedToSpend = isAllowedToSpend, + isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough, + ) } } override suspend fun onSwap( networkId: String, - swapData: SwapDataModel, + swapStateData: SwapStateData, currencyToSend: Currency, currencyToGet: Currency, amountToSwap: String, ): TxState { val amount = requireNotNull(toBigDecimalOrNull(amountToSwap)) { "wrong amount format, use only digits" } - val estimatedGas = swapData.transaction.gas.toIntOrNull() ?: DEFAULT_GAS - val fee = transactionManager.calculateFee( - networkId = networkId, - gasPrice = swapData.transaction.gasPrice, - estimatedGas = estimatedGas, - ) val result = transactionManager.sendTransaction( networkId = networkId, amountToSend = amount, currencyToSend = cryptoCurrencyConverter.convert(currencyToSend), - feeAmount = fee, - estimatedGas = estimatedGas, - destinationAddress = swapData.transaction.toWalletAddress, - dataToSign = swapData.transaction.data, + feeAmount = swapStateData.fee, + gasLimit = swapStateData.gasLimit, + destinationAddress = swapStateData.swapModel.transaction.toWalletAddress, + dataToSign = swapStateData.swapModel.transaction.data, isSwap = true, ) return when (result) { @@ -232,8 +198,14 @@ internal class SwapInteractorImpl @Inject constructor( userWalletManager.addToken(cryptoCurrencyConverter.convert(currencyToGet)) userWalletManager.refreshWallet() TxState.TxSent( - fromAmount = amountFormatter.formatSwapAmountToUI(swapData.fromTokenAmount, currencyToSend.symbol), - toAmount = amountFormatter.formatSwapAmountToUI(swapData.toTokenAmount, currencyToGet.symbol), + fromAmount = amountFormatter.formatSwapAmountToUI( + swapStateData.swapModel.fromTokenAmount, + currencyToSend.symbol, + ), + toAmount = amountFormatter.formatSwapAmountToUI( + swapStateData.swapModel.toTokenAmount, + currencyToGet.symbol, + ), txAddress = userWalletManager.getLastTransactionHash(networkId) ?: "", ) } @@ -265,12 +237,6 @@ internal class SwapInteractorImpl @Inject constructor( } } - private fun getInchFee(quotesData: SwapState): BigDecimal? { - return if (quotesData is SwapState.QuotesLoadedState) { - quotesData.feeRaw - } else null - } - private fun selectToToken( initialToken: Currency, tokensInWallet: List, @@ -353,6 +319,8 @@ internal class SwapInteractorImpl @Inject constructor( amount: SwapAmount, fromToken: Currency, toToken: Currency, + isAllowedToSpend: Boolean, + isBalanceWithoutFeeEnough: Boolean, ): SwapState { repository.findBestQuote( networkId = networkId, @@ -362,35 +330,25 @@ internal class SwapInteractorImpl @Inject constructor( ).let { quotes -> val quoteDataModel = quotes.dataModel if (quoteDataModel != null) { - val transactionData = repository.dataToApprove(networkId, getTokenAddress(fromToken)) - val fee = transactionManager.calculateFee( - networkId = networkId, - estimatedGas = quoteDataModel.estimatedGas, - gasPrice = transactionData.gasPrice, - ) - val feeFiat = getFormattedFiatFee(networkId, fromToken.id, toToken.id, fee) - val formattedFee = amountFormatter.formatBigDecimalAmountToUI( - amount = fee, - decimals = transactionManager.getNativeTokenDecimals(networkId), - currency = userWalletManager.getNetworkCurrency(networkId), - ) + feeFiat val swapState = updateBalances( networkId = networkId, fromToken = fromToken, toToken = toToken, fromTokenAmount = quoteDataModel.fromTokenAmount, toTokenAmount = quoteDataModel.toTokenAmount, - formattedFee = formattedFee, - feeRaw = fee, - swapDataModel = null, + swapStateData = null, + formattedFee = null, ) return updatePermissionState( networkId = networkId, fromToken = fromToken, quotesLoadedState = swapState, - estimatedGas = quoteDataModel.estimatedGas, - transactionData = transactionData, - formattedFee = formattedFee, + ).copy( + preparedSwapConfigState = PreparedSwapConfigState( + isAllowedToSpend = isAllowedToSpend, + isBalanceEnough = isBalanceWithoutFeeEnough, + isFeeEnough = true, + ), ) } else { return SwapState.SwapError(quotes.error) @@ -400,13 +358,11 @@ internal class SwapInteractorImpl @Inject constructor( private suspend fun getFormattedFiatFee( networkId: String, - fromTokenId: String, - toTokenId: String, fee: BigDecimal, ): String { val appCurrency = userWalletManager.getUserAppCurrency() val nativeToken = userWalletManager.getNativeTokenForNetwork(networkId) - val rates = repository.getRates(appCurrency.code, listOf(fromTokenId, toTokenId, nativeToken.id)) + val rates = repository.getRates(appCurrency.code, listOf(nativeToken.id)) return rates[nativeToken.id]?.toBigDecimal()?.let { rate -> " (${fee.toFiatString(rate, appCurrency.symbol, true)})" }.orEmpty() @@ -434,17 +390,26 @@ internal class SwapInteractorImpl @Inject constructor( ).let { val swapData = it.dataModel if (swapData != null) { - val fee = transactionManager.calculateFee( + val feeData = transactionManager.getFee( networkId = networkId, - estimatedGas = swapData.transaction.gas.toIntOrNull() ?: DEFAULT_GAS, - gasPrice = swapData.transaction.gasPrice, + amountToSend = amount.value, + currencyToSend = cryptoCurrencyConverter.convert(fromToken), + destinationAddress = swapData.transaction.toWalletAddress, + data = swapData.transaction.data, ) - val feeFiat = getFormattedFiatFee(networkId, fromToken.id, toToken.id, fee) + val feeFiat = getFormattedFiatFee(networkId, feeData.fee.value) val formattedFee = amountFormatter.formatBigDecimalAmountToUI( - amount = fee, + amount = feeData.fee.value, decimals = transactionManager.getNativeTokenDecimals(networkId), currency = userWalletManager.getNetworkCurrency(networkId), ) + feeFiat + val isBalanceIncludeFeeEnough = isBalanceEnough(fromToken, amount, feeData.fee.value) + val isFeeEnough = checkFeeIsEnough( + fee = feeData.fee.value, + spendAmount = amount, + networkId = networkId, + fromToken = fromToken, + ) val swapState = updateBalances( networkId = networkId, fromToken = fromToken, @@ -452,11 +417,19 @@ internal class SwapInteractorImpl @Inject constructor( fromTokenAmount = swapData.fromTokenAmount, toTokenAmount = swapData.toTokenAmount, formattedFee = formattedFee, - swapDataModel = swapData, - feeRaw = fee, + swapStateData = SwapStateData( + gasLimit = feeData.gasLimit.toInt(), + fee = feeData.fee.value, + swapModel = swapData, + ), ) return swapState.copy( permissionState = PermissionDataState.Empty, + preparedSwapConfigState = PreparedSwapConfigState( + isAllowedToSpend = true, + isBalanceEnough = isBalanceIncludeFeeEnough, + isFeeEnough = isFeeEnough, + ), ) } else { return SwapState.SwapError(it.error) @@ -471,9 +444,8 @@ internal class SwapInteractorImpl @Inject constructor( toToken: Currency, fromTokenAmount: SwapAmount, toTokenAmount: SwapAmount, - formattedFee: String, - feeRaw: BigDecimal, - swapDataModel: SwapDataModel?, + formattedFee: String?, + swapStateData: SwapStateData?, ): SwapState.QuotesLoadedState { val appCurrency = userWalletManager.getUserAppCurrency() val nativeToken = userWalletManager.getNativeTokenForNetwork(networkId) @@ -511,21 +483,18 @@ internal class SwapInteractorImpl @Inject constructor( toRate = rates[toToken.id] ?: 0.0, ), networkCurrency = userWalletManager.getNetworkCurrency(networkId), - swapDataModel = swapDataModel, + swapDataModel = swapStateData, tangemFee = getTangemFee(), - feeRaw = feeRaw, ) } @Suppress("LongParameterList") - private fun updatePermissionState( + private suspend fun updatePermissionState( networkId: String, fromToken: Currency, quotesLoadedState: SwapState.QuotesLoadedState, - estimatedGas: Int, - transactionData: ApproveModel, - formattedFee: String, ): SwapState.QuotesLoadedState { + // if token balance ZERO not show permission state to avoid user to spend money for fee val isTokenZeroBalance = getTokenBalance(fromToken).value.compareTo(BigDecimal.ZERO) == 0 if (isTokenZeroBalance) { return quotesLoadedState.copy( @@ -537,6 +506,20 @@ internal class SwapInteractorImpl @Inject constructor( permissionState = PermissionDataState.PermissionLoading, ) } + val transactionData = repository.dataToApprove(networkId, getTokenAddress(fromToken)) + val feeData = transactionManager.getFee( + networkId = networkId, + amountToSend = BigDecimal.ZERO, + currencyToSend = userWalletManager.getNativeTokenForNetwork(networkId), + destinationAddress = transactionData.toAddress, + data = transactionData.data, + ) + val feeFiat = getFormattedFiatFee(networkId, feeData.fee.value) + val formattedFee = amountFormatter.formatBigDecimalAmountToUI( + amount = feeData.fee.value, + decimals = transactionManager.getNativeTokenDecimals(networkId), + currency = userWalletManager.getNetworkCurrency(networkId), + ) + feeFiat return quotesLoadedState.copy( permissionState = PermissionDataState.PermissionReadyForRequest( currency = fromToken.symbol, @@ -545,7 +528,8 @@ internal class SwapInteractorImpl @Inject constructor( spenderAddress = transactionData.toAddress, fee = formattedFee, requestApproveData = RequestApproveStateData( - estimatedGas = estimatedGas, + fee = feeData.fee.value, + gasLimit = feeData.gasLimit.toInt(), approveModel = transactionData, ), ), @@ -613,11 +597,6 @@ internal class SwapInteractorImpl @Inject constructor( } } - @Suppress("MagicNumber") - private fun increaseByPercents(percents: Int, value: Int): Int { - return value * (percents / 100 + 1) - } - private fun toBigDecimalOrNull(amountToSwap: String): BigDecimal? { return amountToSwap.replace(",", ".").toBigDecimalOrNull() } @@ -636,9 +615,7 @@ internal class SwapInteractorImpl @Inject constructor( companion object { private const val DEFAULT_SLIPPAGE = 2 private const val ZERO_BALANCE = "0" - private const val DEFAULT_GAS = 300000 private const val DEFAULT_BLOCKCHAIN_INCH_ADDRESS = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE" - private const val TWENTY_FIVE_PERCENTS = 25 private const val INCREASE_FEE_TO_CHECK_ENOUGH_PERCENT = 1.4 private const val USDT_SYMBOL = "USDT" private const val USDC_SYMBOL = "USDC" diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt index deace8a843..af6b0745f3 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt @@ -12,7 +12,7 @@ sealed interface SwapState { data class QuotesLoadedState( val fromTokenInfo: TokenSwapInfo, val toTokenInfo: TokenSwapInfo, - val fee: String, + val fee: String?, val priceImpact: Float, val networkCurrency: String, val preparedSwapConfigState: PreparedSwapConfigState = PreparedSwapConfigState( @@ -21,9 +21,8 @@ sealed interface SwapState { isFeeEnough = false, ), val permissionState: PermissionDataState = PermissionDataState.Empty, - val swapDataModel: SwapDataModel? = null, + val swapDataModel: SwapStateData? = null, val tangemFee: Double, - val feeRaw: BigDecimal, ) : SwapState data class EmptyAmountState( @@ -61,6 +60,13 @@ data class TokenSwapInfo( ) data class RequestApproveStateData( - val estimatedGas: Int, + val fee: BigDecimal, + val gasLimit: Int, val approveModel: ApproveModel, +) + +data class SwapStateData( + val fee: BigDecimal, + val gasLimit: Int, + val swapModel: SwapDataModel, ) \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 31d4ce3cb4..c5924121b8 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -131,9 +131,9 @@ internal class StateBuilder(val actions: UiActions) { warnings.add(SwapWarning.HighPriceImpact((quoteModel.priceImpact * HUNDRED_PERCENTS).toInt())) } val feeState = if (quoteModel.preparedSwapConfigState.isFeeEnough) { - FeeState.Loaded(tangemFee = quoteModel.tangemFee, fee = quoteModel.fee) + FeeState.Loaded(tangemFee = quoteModel.tangemFee, fee = quoteModel.fee ?: "") } else { - FeeState.NotEnoughFundsWarning(tangemFee = quoteModel.tangemFee, fee = quoteModel.fee) + FeeState.NotEnoughFundsWarning(tangemFee = quoteModel.tangemFee, fee = quoteModel.fee ?: "") } return uiStateHolder.copy( sendCardData = SwapCardData( diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt index 0d9a788af5..78df67253d 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt @@ -255,12 +255,14 @@ private fun FeeItem(feeState: FeeState, currency: String) { val disclaimer = stringResource(id = R.string.swapping_tangem_fee_disclaimer, "${feeState.tangemFee}%") when (feeState) { is FeeState.Loaded -> { - SmallInfoCardWithDisclaimer( - startText = titleString, - endText = feeState.fee, - disclaimer = disclaimer, - isLoading = false, - ) + if (feeState.fee.isNotEmpty()) { + SmallInfoCardWithDisclaimer( + startText = titleString, + endText = feeState.fee, + disclaimer = disclaimer, + isLoading = false, + ) + } } FeeState.Loading -> { SmallInfoCardWithDisclaimer( @@ -271,16 +273,18 @@ private fun FeeItem(feeState: FeeState, currency: String) { ) } is FeeState.NotEnoughFundsWarning -> { - SmallInfoCardWithWarning( - startText = titleString, - endText = feeState.fee, - disclaimer = disclaimer, - warningText = stringResource( - id = R.string.swapping_not_enough_funds_for_fee, - currency, - currency, - ), - ) + if (feeState.fee.isNotEmpty()) { + SmallInfoCardWithWarning( + startText = titleString, + endText = feeState.fee, + disclaimer = disclaimer, + warningText = stringResource( + id = R.string.swapping_not_enough_funds_for_fee, + currency, + currency, + ), + ) + } } is FeeState.Empty -> { SmallInfoCard(startText = titleString, endText = "") diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapProcessDataState.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapProcessDataState.kt index 4fe4e4bfb8..fdc185ac48 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapProcessDataState.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapProcessDataState.kt @@ -1,15 +1,14 @@ package com.tangem.feature.swap.viewmodels -import com.tangem.feature.swap.domain.models.domain.ApproveModel import com.tangem.feature.swap.domain.models.domain.Currency -import com.tangem.feature.swap.domain.models.domain.SwapDataModel +import com.tangem.feature.swap.domain.models.ui.RequestApproveStateData +import com.tangem.feature.swap.domain.models.ui.SwapStateData data class SwapProcessDataState( val networkId: String, val fromCurrency: Currency? = null, val toCurrency: Currency? = null, val amount: String? = null, - val estimatedGas: Int? = null, - val approveModel: ApproveModel? = null, - val swapModel: SwapDataModel? = null, + val approveDataModel: RequestApproveStateData? = null, + val swapDataModel: SwapStateData? = null, ) \ 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 8851cc69d2..47a659a127 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 @@ -12,11 +12,11 @@ import com.tangem.feature.swap.analytics.SwapEvents import com.tangem.feature.swap.domain.BlockchainInteractor import com.tangem.feature.swap.domain.SwapInteractor import com.tangem.feature.swap.domain.models.domain.Currency -import com.tangem.feature.swap.domain.models.domain.SwapDataModel import com.tangem.feature.swap.domain.models.formatToUIRepresentation import com.tangem.feature.swap.domain.models.ui.FoundTokensState import com.tangem.feature.swap.domain.models.ui.PermissionDataState import com.tangem.feature.swap.domain.models.ui.SwapState +import com.tangem.feature.swap.domain.models.ui.SwapStateData import com.tangem.feature.swap.domain.models.ui.TxState import com.tangem.feature.swap.models.SwapStateHolder import com.tangem.feature.swap.models.UiActions @@ -169,9 +169,8 @@ internal class SwapViewModel @Inject constructor( runCatching(dispatchers.io) { dataState = dataState.copy( amount = amount, - swapModel = null, - estimatedGas = null, - approveModel = null, + swapDataModel = null, + approveDataModel = null, ) swapInteractor.findBestQuote( networkId = dataState.networkId, @@ -210,15 +209,14 @@ internal class SwapViewModel @Inject constructor( ) } - private fun fillDataState(permissionState: PermissionDataState, swapDataModel: SwapDataModel?) { + private fun fillDataState(permissionState: PermissionDataState, swapDataModel: SwapStateData?) { dataState = if (permissionState is PermissionDataState.PermissionReadyForRequest) { dataState.copy( - estimatedGas = permissionState.requestApproveData.estimatedGas, - approveModel = permissionState.requestApproveData.approveModel, + approveDataModel = permissionState.requestApproveData, ) } else { dataState.copy( - swapModel = swapDataModel, + swapDataModel = swapDataModel, ) } } @@ -230,7 +228,7 @@ internal class SwapViewModel @Inject constructor( runCatching(dispatchers.io) { swapInteractor.onSwap( networkId = dataState.networkId, - swapData = requireNotNull(dataState.swapModel), + swapStateData = requireNotNull(dataState.swapDataModel), currencyToSend = requireNotNull(dataState.fromCurrency), currencyToGet = requireNotNull(dataState.toCurrency), amountToSwap = requireNotNull(dataState.amount), @@ -276,8 +274,7 @@ internal class SwapViewModel @Inject constructor( runCatching(dispatchers.io) { swapInteractor.givePermissionToSwap( networkId = dataState.networkId, - estimatedGas = dataState.estimatedGas!!, - transactionData = dataState.approveModel!!, + approveData = dataState.approveDataModel!!, forTokenContractAddress = (dataState.fromCurrency as? Currency.NonNativeToken)?.contractAddress ?: "", ) diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/TransactionManager.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/TransactionManager.kt index 3c40b3a4c0..f167603b88 100644 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/TransactionManager.kt +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/TransactionManager.kt @@ -1,7 +1,7 @@ package com.tangem.lib.crypto import com.tangem.lib.crypto.models.Currency -import com.tangem.lib.crypto.models.ProxyAmount +import com.tangem.lib.crypto.models.ProxyFee import com.tangem.lib.crypto.models.ProxyNetworkInfo import com.tangem.lib.crypto.models.transactions.SendTxResult import java.math.BigDecimal @@ -12,7 +12,7 @@ interface TransactionManager { suspend fun sendApproveTransaction( networkId: String, feeAmount: BigDecimal, - estimatedGas: Int, + gasLimit: Int, destinationAddress: String, dataToSign: String, ): SendTxResult @@ -23,7 +23,7 @@ interface TransactionManager { networkId: String, amountToSend: BigDecimal, feeAmount: BigDecimal, - estimatedGas: Int, + gasLimit: Int, destinationAddress: String, dataToSign: String, isSwap: Boolean, @@ -36,7 +36,8 @@ interface TransactionManager { amountToSend: BigDecimal, currencyToSend: Currency, destinationAddress: String, - ): ProxyAmount + data: String?, + ): ProxyFee @Throws(IllegalStateException::class) fun getNativeTokenDecimals(networkId: String): Int diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/models/ProxyFee.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/models/ProxyFee.kt new file mode 100644 index 0000000000..56b92d5688 --- /dev/null +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/models/ProxyFee.kt @@ -0,0 +1,8 @@ +package com.tangem.lib.crypto.models + +import java.math.BigInteger + +data class ProxyFee( + val gasLimit: BigInteger, + val fee: ProxyAmount, +) \ No newline at end of file From 25d39f9f0b20a5c38f5d8a7d8ef591e6f4c578ce Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 6 Mar 2023 18:19:26 +0300 Subject: [PATCH 06/14] Updated on 2026-08-14 --- .../com/tangem/tap/common/IntentHandler.kt | 1 - .../middlewares/TradeCryptoMiddleware.kt | 6 ++- .../tap/proxy/TransactionManagerImpl.kt | 30 +++++------ .../tangem/tap/proxy/UserWalletManagerImpl.kt | 52 +++++++------------ .../referral/domain/ReferralInteractorImpl.kt | 6 +-- .../feature/swap/domain/SwapInteractor.kt | 2 + .../feature/swap/domain/SwapInteractorImpl.kt | 26 +++++++--- .../feature/swap/presentation/SwapFragment.kt | 1 + .../feature/swap/viewmodels/SwapViewModel.kt | 2 + .../tangem/lib/crypto/TransactionManager.kt | 7 ++- .../tangem/lib/crypto/UserWalletManager.kt | 19 ++++--- 11 files changed, 81 insertions(+), 71 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/common/IntentHandler.kt b/app/src/main/java/com/tangem/tap/common/IntentHandler.kt index 12451bdd67..4d50c59679 100644 --- a/app/src/main/java/com/tangem/tap/common/IntentHandler.kt +++ b/app/src/main/java/com/tangem/tap/common/IntentHandler.kt @@ -84,7 +84,6 @@ class IntentHandler { val successUri = Uri.parse(ExchangeUrlBuilder.SUCCESS_URL) if (data.host == successUri.host && data.authority == successUri.authority) { val currency = store.state.walletState.selectedCurrency ?: return - val currencyType = AnalyticsParam.CurrencyType.Currency(currency) Analytics.send(Token.Bought(currencyType)) } diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt index 96967b136a..ec7597b8c7 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt @@ -161,7 +161,11 @@ class TradeCryptoMiddleware { private fun openSwap() { val currency = store.state.walletState.selectedWalletData?.currency?.toSwapCurrency() - val bundle = bundleOf(SwapFragment.CURRENCY_BUNDLE_KEY to Json.encodeToString(currency)) + val bundle = + bundleOf( + SwapFragment.CURRENCY_BUNDLE_KEY to Json.encodeToString(currency), + SwapFragment.DERIVATION_PATH to store.state.walletState.selectedWalletData?.currency?.derivationPath + ) store.dispatchOnMain(NavigationAction.NavigateTo(screen = AppScreen.Swap, bundle = bundle)) } diff --git a/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt b/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt index bc8fe9a242..7ac0ea6a9f 100644 --- a/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt +++ b/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt @@ -48,9 +48,10 @@ class TransactionManagerImpl( gasLimit: Int, destinationAddress: String, dataToSign: String, + derivationPath: String?, ): SendTxResult { val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" } - val walletManager = getActualWalletManager(blockchain) + val walletManager = getActualWalletManager(blockchain, derivationPath) walletManager.update() val amount = Amount(value = BigDecimal.ZERO, blockchain = blockchain) return sendTransactionInternal( @@ -73,9 +74,10 @@ class TransactionManagerImpl( dataToSign: String, isSwap: Boolean, currencyToSend: Currency, + derivationPath: String?, ): SendTxResult { val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" } - val walletManager = getActualWalletManager(blockchain) + val walletManager = getActualWalletManager(blockchain, derivationPath) walletManager.update() val amount = if (isSwap) { createAmountForSwap(amountToSend, currencyToSend, blockchain) @@ -129,9 +131,9 @@ class TransactionManagerImpl( return Blockchain.fromNetworkId(networkId)?.decimals() ?: error("blockchain not found") } - override suspend fun updateWalletManager(networkId: String) { + override suspend fun updateWalletManager(networkId: String, derivationPath: String?) { val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" } - getActualWalletManager(blockchain).update() + getActualWalletManager(blockchain, derivationPath).update() } override fun calculateFee(networkId: String, gasPrice: String, estimatedGas: Int): BigDecimal { @@ -147,9 +149,10 @@ class TransactionManagerImpl( currencyToSend: Currency, destinationAddress: String, data: String?, + derivationPath: String?, ): ProxyFee { val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" } - val walletManager = getActualWalletManager(blockchain) + val walletManager = getActualWalletManager(blockchain, derivationPath) if (walletManager is EthereumWalletManager) { val gasLimit = getGasLimit( evmWalletManager = walletManager, @@ -293,19 +296,10 @@ class TransactionManagerImpl( } } - private fun getActualWalletManager(blockchain: Blockchain): WalletManager { - val card = appStateHolder.getActualCard() - if (card != null) { - val blockchainNetwork = BlockchainNetwork(blockchain, card) - val walletManager = appStateHolder.walletState?.getWalletManager(blockchainNetwork) - if (walletManager != null) { - return walletManager - } else { - error("no wallet manager found") - } - } else { - error("card not found") - } + private fun getActualWalletManager(blockchain: Blockchain, derivationPath: String?): WalletManager { + val blockchainNetwork = BlockchainNetwork(blockchain, derivationPath, emptyList()) + val walletManager = appStateHolder.walletState?.getWalletManager(blockchainNetwork) + return requireNotNull(walletManager) { "no wallet manager found" } } private fun createExtras( diff --git a/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt b/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt index c0c0c1ac91..a983ea5687 100644 --- a/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt +++ b/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt @@ -85,23 +85,17 @@ class UserWalletManagerImpl( ?: "" } - override suspend fun isTokenAdded(currency: Currency): Boolean { - val card = requireNotNull(appStateHolder.getActualCard()) { "card is null" } + override suspend fun isTokenAdded(currency: Currency, derivationPath: String?): Boolean { val blockchain = requireNotNull(Blockchain.fromNetworkId(currency.networkId)) { "blockchain not found" } - val blockchainNetwork = BlockchainNetwork(blockchain, card) - val walletManager = appStateHolder.walletState?.getWalletManager(blockchainNetwork) - if (walletManager != null) { - return walletManager.cardTokens.any { - it.id == currency.id - } + val walletManager = getActualWalletManager(blockchain, derivationPath) + return walletManager.cardTokens.any { + it.id == currency.id } - return false } - override suspend fun addToken(currency: Currency) { - val card = requireNotNull(appStateHolder.getActualCard()) { "card not found" } + override suspend fun addToken(currency: Currency, derivationPath: String?) { val blockchain = requireNotNull(Blockchain.fromNetworkId(currency.networkId)) { "blockchain not found" } - val blockchainNetwork = BlockchainNetwork(blockchain, card) + val blockchainNetwork = BlockchainNetwork(blockchain, derivationPath, emptyList()) val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard { Timber.e("Unable to add token, no user wallet selected") @@ -113,34 +107,27 @@ class UserWalletManagerImpl( ) } - override fun getWalletAddress(networkId: String): String { + override fun getWalletAddress(networkId: String, derivationPath: String?): String { val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" } - val card = requireNotNull(appStateHolder.getActualCard()) { "card not found" } - val blockchainNetwork = BlockchainNetwork(blockchain, card) - val walletManager = appStateHolder.walletState?.getWalletManager(blockchainNetwork) - if (walletManager != null) { - return walletManager.wallet.address - } else { - error("no wallet manager found") - } + val walletManager = getActualWalletManager(blockchain, derivationPath) + return walletManager.wallet.address } - override fun getLastTransactionHash(networkId: String): String? { + override fun getLastTransactionHash(networkId: String, derivationPath: String?): String? { val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" } - val card = requireNotNull(appStateHolder.getActualCard()) { "card not found" } - val blockchainNetwork = BlockchainNetwork(blockchain, card) - val walletManager = appStateHolder.walletState?.getWalletManager(blockchainNetwork) - return walletManager?.wallet?.recentTransactions - ?.lastOrNull { it.hash?.isNotEmpty() == true } + val walletManager = getActualWalletManager(blockchain, derivationPath) + return walletManager.wallet.recentTransactions + .lastOrNull { it.hash?.isNotEmpty() == true } ?.hash?.let { HEX_PREFIX + it } } override suspend fun getCurrentWalletTokensBalance( networkId: String, extraTokens: List, + derivationPath: String?, ): Map { val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" } - val walletManager = getActualWalletManager(blockchain) + val walletManager = getActualWalletManager(blockchain, derivationPath) // workaround for get balance for tokens that doesn't exist in wallet val extraTokensToLoadBalance = extraTokens @@ -165,9 +152,9 @@ class UserWalletManagerImpl( return balances } - override fun getNativeTokenBalance(networkId: String): ProxyAmount? { + override fun getNativeTokenBalance(networkId: String, derivationPath: String?): ProxyAmount? { val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" } - val walletManager = getActualWalletManager(blockchain) + val walletManager = getActualWalletManager(blockchain, derivationPath) return walletManager.wallet.amounts.firstNotNullOfOrNull { it.takeIf { it.key is AmountType.Coin } }?.value?.let { @@ -198,9 +185,8 @@ class UserWalletManagerImpl( appStateHolder.mainStore?.dispatchOnMain(WalletAction.LoadData.Refresh) } - private fun getActualWalletManager(blockchain: Blockchain): WalletManager { - val card = requireNotNull(appStateHolder.getActualCard()) { "card not found" } - val blockchainNetwork = BlockchainNetwork(blockchain, card) + private fun getActualWalletManager(blockchain: Blockchain, derivationPath: String?): WalletManager { + val blockchainNetwork = BlockchainNetwork(blockchain, derivationPath, emptyList()) return requireNotNull(appStateHolder.walletState?.getWalletManager(blockchainNetwork)) { "No wallet manager found" } diff --git a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt index c7943b3cb6..22e20981e5 100644 --- a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt +++ b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt @@ -26,7 +26,7 @@ internal class ReferralInteractorImpl( if (tokensForReferral.isNotEmpty()) { val currency = tokensConverter.convert(tokensForReferral.first()) deriveOrAddTokens(currency) - val publicAddress = userWalletManager.getWalletAddress(currency.networkId) + val publicAddress = userWalletManager.getWalletAddress(currency.networkId, null) return repository.startReferral( walletId = userWalletManager.getWalletId(), networkId = currency.networkId, @@ -42,8 +42,8 @@ internal class ReferralInteractorImpl( if (!derivationManager.hasDerivation(currency.networkId)) { derivationManager.deriveMissingBlockchains(currency) } - if (!userWalletManager.isTokenAdded(currency)) { - userWalletManager.addToken(currency) + if (!userWalletManager.isTokenAdded(currency, null)) { + userWalletManager.addToken(currency, null) } } diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt index 1ea5c8378c..461f048ca8 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt @@ -11,6 +11,8 @@ import com.tangem.feature.swap.domain.models.ui.TxState interface SwapInteractor { + fun initDerivationPath(derivationPath: String?) + /** * Init tokens to swap, load tokens list available to swap for given network * 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 4127ecc179..98350f3244 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 @@ -39,6 +39,11 @@ internal class SwapInteractorImpl @Inject constructor( private val cryptoCurrencyConverter = CryptoCurrencyConverter() private val amountFormatter = AmountFormatter() + private var derivationPath: String? = null + + override fun initDerivationPath(derivationPath: String?) { + this.derivationPath = derivationPath + } override suspend fun initTokensToSwap(initialCurrency: Currency): TokensDataState { val networkId = initialCurrency.networkId @@ -63,7 +68,7 @@ internal class SwapInteractorImpl @Inject constructor( .filter { !loadedOnWalletsMap.contains(it.symbol) } - val tokensBalance = userWalletManager.getCurrentWalletTokensBalance(networkId, emptyList()) + val tokensBalance = userWalletManager.getCurrentWalletTokensBalance(networkId, emptyList(), derivationPath) .mapValues { SwapAmount(it.value.value, it.value.decimals) } val appCurrency = userWalletManager.getUserAppCurrency() val rates = repository.getRates(appCurrency.code, tokensInWallet.map { it.id }) @@ -118,11 +123,12 @@ internal class SwapInteractorImpl @Inject constructor( gasLimit = approveData.gasLimit, destinationAddress = approveData.approveModel.toAddress, dataToSign = approveData.approveModel.data, + derivationPath = derivationPath, ) return when (result) { is SendTxResult.Success -> { allowPermissionsHandler.addAddressToInProgress(forTokenContractAddress) - TxState.TxSent(txAddress = userWalletManager.getLastTransactionHash(networkId) ?: "") + TxState.TxSent(txAddress = userWalletManager.getLastTransactionHash(networkId, derivationPath) ?: "") } SendTxResult.UserCancelledError -> TxState.UserCancelled is SendTxResult.BlockchainSdkError -> TxState.BlockchainError @@ -149,7 +155,7 @@ internal class SwapInteractorImpl @Inject constructor( val isAllowedToSpend = checkAllowance(networkId, fromTokenAddress) if (isAllowedToSpend && allowPermissionsHandler.isAddressAllowanceInProgress(fromTokenAddress)) { allowPermissionsHandler.removeAddressFromProgress(fromTokenAddress) - transactionManager.updateWalletManager(networkId) + transactionManager.updateWalletManager(networkId, derivationPath) } val isBalanceWithoutFeeEnough = isBalanceEnough(fromToken, amount, null) return if (isAllowedToSpend && isBalanceWithoutFeeEnough) { @@ -192,10 +198,11 @@ internal class SwapInteractorImpl @Inject constructor( destinationAddress = swapStateData.swapModel.transaction.toWalletAddress, dataToSign = swapStateData.swapModel.transaction.data, isSwap = true, + derivationPath = derivationPath, ) return when (result) { is SendTxResult.Success -> { - userWalletManager.addToken(cryptoCurrencyConverter.convert(currencyToGet)) + userWalletManager.addToken(cryptoCurrencyConverter.convert(currencyToGet), derivationPath) userWalletManager.refreshWallet() TxState.TxSent( fromAmount = amountFormatter.formatSwapAmountToUI( @@ -206,7 +213,7 @@ internal class SwapInteractorImpl @Inject constructor( swapStateData.swapModel.toTokenAmount, currencyToGet.symbol, ), - txAddress = userWalletManager.getLastTransactionHash(networkId) ?: "", + txAddress = userWalletManager.getLastTransactionHash(networkId, derivationPath) ?: "", ) } SendTxResult.UserCancelledError -> TxState.UserCancelled @@ -285,7 +292,7 @@ internal class SwapInteractorImpl @Inject constructor( val allowance = repository.checkTokensSpendAllowance( networkId = networkId, tokenAddress = fromTokenAddress, - walletAddress = userWalletManager.getWalletAddress(networkId), + walletAddress = userWalletManager.getWalletAddress(networkId, derivationPath), ) return allowance.error == DataError.NoError && allowance.dataModel != ZERO_BALANCE } @@ -396,6 +403,7 @@ internal class SwapInteractorImpl @Inject constructor( currencyToSend = cryptoCurrencyConverter.convert(fromToken), destinationAddress = swapData.transaction.toWalletAddress, data = swapData.transaction.data, + derivationPath = derivationPath, ) val feeFiat = getFormattedFiatFee(networkId, feeData.fee.value) val formattedFee = amountFormatter.formatBigDecimalAmountToUI( @@ -513,6 +521,7 @@ internal class SwapInteractorImpl @Inject constructor( currencyToSend = userWalletManager.getNativeTokenForNetwork(networkId), destinationAddress = transactionData.toAddress, data = transactionData.data, + derivationPath = derivationPath, ) val feeFiat = getFormattedFiatFee(networkId, feeData.fee.value) val formattedFee = amountFormatter.formatBigDecimalAmountToUI( @@ -543,6 +552,7 @@ internal class SwapInteractorImpl @Inject constructor( userWalletManager.getCurrentWalletTokensBalance( networkId = networkId, extraTokens = tokensToSync.map { cryptoCurrencyConverter.convert(it) }, + derivationPath = derivationPath, ) cache.cacheBalances(tokensBalance.mapValues { SwapAmount(it.value.value, it.value.decimals) }) } @@ -558,7 +568,7 @@ internal class SwapInteractorImpl @Inject constructor( } private fun getWalletAddress(networkId: String): String { - return userWalletManager.getWalletAddress(networkId) + return userWalletManager.getWalletAddress(networkId, derivationPath) } private fun getTokenAddress(currency: Currency): String { @@ -581,7 +591,7 @@ internal class SwapInteractorImpl @Inject constructor( if (fee == null) { return false } - val nativeTokenBalance = userWalletManager.getNativeTokenBalance(networkId) + val nativeTokenBalance = userWalletManager.getNativeTokenBalance(networkId, derivationPath) val percentsToFeeIncrease = BigDecimal.valueOf(INCREASE_FEE_TO_CHECK_ENOUGH_PERCENT) return when (fromToken) { is Currency.NativeToken -> { diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/presentation/SwapFragment.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/presentation/SwapFragment.kt index fbece11798..d2d2622426 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/presentation/SwapFragment.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/presentation/SwapFragment.kt @@ -72,5 +72,6 @@ class SwapFragment : Fragment() { companion object { const val CURRENCY_BUNDLE_KEY = "swap_currency" + const val DERIVATION_PATH = "DERIVATION_STYLE" } } \ 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 47a659a127..b18ceab795 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 @@ -52,6 +52,7 @@ internal class SwapViewModel @Inject constructor( savedStateHandle[SwapFragment.CURRENCY_BUNDLE_KEY] ?: error("no expected parameter Currency found"), ) + private val derivationPath = savedStateHandle.get(SwapFragment.DERIVATION_PATH) private val stateBuilder = StateBuilder( actions = createUiActions(), @@ -78,6 +79,7 @@ internal class SwapViewModel @Inject constructor( get() = swapRouter.currentScreen init { + swapInteractor.initDerivationPath(derivationPath) initTokens(currency) } diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/TransactionManager.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/TransactionManager.kt index f167603b88..02c08a1d6b 100644 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/TransactionManager.kt +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/TransactionManager.kt @@ -8,6 +8,7 @@ import java.math.BigDecimal interface TransactionManager { + @Suppress("LongParameterList") @Throws(IllegalStateException::class) suspend fun sendApproveTransaction( networkId: String, @@ -15,6 +16,7 @@ interface TransactionManager { gasLimit: Int, destinationAddress: String, dataToSign: String, + derivationPath: String?, ): SendTxResult @Suppress("LongParameterList") @@ -28,8 +30,10 @@ interface TransactionManager { dataToSign: String, isSwap: Boolean, currencyToSend: Currency, + derivationPath: String?, ): SendTxResult + @Suppress("LongParameterList") @Throws(IllegalStateException::class) suspend fun getFee( networkId: String, @@ -37,13 +41,14 @@ interface TransactionManager { currencyToSend: Currency, destinationAddress: String, data: String?, + derivationPath: String?, ): ProxyFee @Throws(IllegalStateException::class) fun getNativeTokenDecimals(networkId: String): Int @Throws(IllegalStateException::class) - suspend fun updateWalletManager(networkId: String) + suspend fun updateWalletManager(networkId: String, derivationPath: String?) fun calculateFee(networkId: String, gasPrice: String, estimatedGas: Int): BigDecimal diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/UserWalletManager.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/UserWalletManager.kt index 258148f3ca..e9c0d1beea 100644 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/UserWalletManager.kt +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/UserWalletManager.kt @@ -29,15 +29,16 @@ interface UserWalletManager { * @param currency to receive referral payments */ @Throws(IllegalStateException::class) - suspend fun isTokenAdded(currency: Currency): Boolean + suspend fun isTokenAdded(currency: Currency, derivationPath: String?): Boolean /** * Adds token to wallet if its not * * @param currency to add to wallet + * @param derivationPath if null uses default */ @Throws(IllegalStateException::class) - suspend fun addToken(currency: Currency) + suspend fun addToken(currency: Currency, derivationPath: String?) fun refreshWallet() @@ -45,21 +46,27 @@ interface UserWalletManager { * Returns wallet public address for token * * @param networkId for currency + * @param derivationPath if null uses default */ @Throws(IllegalStateException::class) - fun getWalletAddress(networkId: String): String + fun getWalletAddress(networkId: String, derivationPath: String?): String /** * Return balances from wallet found by networkId * * @param networkId + * @param derivationPath if null uses default * @return map of */ @Throws(IllegalStateException::class) - suspend fun getCurrentWalletTokensBalance(networkId: String, extraTokens: List): Map + suspend fun getCurrentWalletTokensBalance( + networkId: String, + extraTokens: List, + derivationPath: String?, + ): Map @Throws(IllegalStateException::class) - fun getNativeTokenBalance(networkId: String): ProxyAmount? + fun getNativeTokenBalance(networkId: String, derivationPath: String?): ProxyAmount? /** * @param networkId @@ -74,5 +81,5 @@ interface UserWalletManager { fun getUserAppCurrency(): ProxyFiatCurrency @Throws(IllegalStateException::class) - fun getLastTransactionHash(networkId: String): String? + fun getLastTransactionHash(networkId: String, derivationPath: String?): String? } \ No newline at end of file From c2aef024437e7aed8ae9e623408e8322273894c2 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 6 Mar 2023 18:31:06 +0300 Subject: [PATCH 07/14] Updated on 2026-08-14 --- .../java/com/tangem/tap/proxy/TransactionManagerImpl.kt | 4 ++-- app/src/main/java/com/tangem/tap/proxy/di/ProxyModule.kt | 7 +++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt b/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt index 7ac0ea6a9f..14349d169f 100644 --- a/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt +++ b/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt @@ -17,7 +17,7 @@ import com.tangem.blockchain.extensions.SimpleResult import com.tangem.blockchain.extensions.isNetworkError import com.tangem.common.core.TangemSdkError import com.tangem.common.extensions.hexToBytes -import com.tangem.core.analytics.api.AnalyticsHandler +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.lib.crypto.TransactionManager import com.tangem.lib.crypto.models.Currency @@ -39,7 +39,7 @@ import java.math.RoundingMode @Suppress("LargeClass") class TransactionManagerImpl( private val appStateHolder: AppStateHolder, - private val analytics: AnalyticsHandler, + private val analytics: AnalyticsEventHandler, ) : TransactionManager { override suspend fun sendApproveTransaction( diff --git a/app/src/main/java/com/tangem/tap/proxy/di/ProxyModule.kt b/app/src/main/java/com/tangem/tap/proxy/di/ProxyModule.kt index 8cf83557a8..1189c0895d 100644 --- a/app/src/main/java/com/tangem/tap/proxy/di/ProxyModule.kt +++ b/app/src/main/java/com/tangem/tap/proxy/di/ProxyModule.kt @@ -1,6 +1,6 @@ package com.tangem.tap.proxy.di -import com.tangem.core.analytics.api.AnalyticsHandler +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.lib.crypto.DerivationManager import com.tangem.lib.crypto.TransactionManager import com.tangem.lib.crypto.UserWalletManager @@ -34,7 +34,10 @@ class ProxyModule { @Provides @Singleton - fun provideTransactionManager(appStateHolder: AppStateHolder, analytics: AnalyticsHandler): TransactionManager { + fun provideTransactionManager( + appStateHolder: AppStateHolder, + analytics: AnalyticsEventHandler, + ): TransactionManager { return TransactionManagerImpl(appStateHolder, analytics) } From 605d7da00d801223895cbe457f2133a1eadba7b9 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 6 Mar 2023 20:32:39 +0300 Subject: [PATCH 08/14] Updated on 2026-08-14 --- .../common/analytics/TangemSdkErrorMapper.kt | 2 + .../userWalletList/UserWalletListError.kt | 16 -- .../userWalletList/UserWalletsListError.kt | 29 ++++ .../userWalletList/UserWalletsListManager.kt | 6 +- .../UserWalletsListManagerExtensions.kt | 17 ++- .../BiometricUserWalletsListManager.kt | 48 +++++- .../RuntimeUserWalletsListManager.kt | 4 +- .../BiometricUserWalletsKeysRepository.kt | 45 +++++- .../ui/WalletSelectorBottomSheetFragment.kt | 7 +- .../ui/WalletSelectorViewModel.kt | 28 ++-- ....kt => BiometricsLockoutWarningContent.kt} | 20 +-- .../KeyInvalidatedWarningContent.kt | 56 +++++++ .../walletSelector/ui/model/DialogModel.kt | 10 +- .../welcome/redux/WelcomeMiddleware.kt | 13 +- .../features/welcome/ui/WelcomeFragment.kt | 6 +- .../features/welcome/ui/WelcomeScreenState.kt | 4 +- .../features/welcome/ui/WelcomeViewModel.kt | 27 ++-- .../ui/components/BiometricsLockoutDialog.kt | 96 ------------ .../welcome/ui/components/WarningDialog.kt | 138 ++++++++++++++++++ .../ui/model/BiometricsLockoutDialog.kt | 6 - .../features/welcome/ui/model/WarningModel.kt | 12 ++ buildSrc/src/main/java/Versions.kt | 2 +- core/res/src/main/res/values-de/strings.xml | 5 +- core/res/src/main/res/values-fr/strings.xml | 5 +- core/res/src/main/res/values-it/strings.xml | 5 +- core/res/src/main/res/values-ru/strings.xml | 3 + .../src/main/res/values-zh-rTW/strings.xml | 5 +- core/res/src/main/res/values/strings.xml | 5 +- 28 files changed, 415 insertions(+), 205 deletions(-) delete mode 100644 app/src/main/java/com/tangem/tap/domain/userWalletList/UserWalletListError.kt create mode 100644 app/src/main/java/com/tangem/tap/domain/userWalletList/UserWalletsListError.kt rename app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/{BiometricsLockoutDialogContent.kt => BiometricsLockoutWarningContent.kt} (81%) create mode 100644 app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/KeyInvalidatedWarningContent.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/welcome/ui/components/BiometricsLockoutDialog.kt create mode 100644 app/src/main/java/com/tangem/tap/features/welcome/ui/components/WarningDialog.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/welcome/ui/model/BiometricsLockoutDialog.kt create mode 100644 app/src/main/java/com/tangem/tap/features/welcome/ui/model/WarningModel.kt diff --git a/app/src/main/java/com/tangem/tap/common/analytics/TangemSdkErrorMapper.kt b/app/src/main/java/com/tangem/tap/common/analytics/TangemSdkErrorMapper.kt index b6fb925fd4..20e273fda2 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/TangemSdkErrorMapper.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/TangemSdkErrorMapper.kt @@ -117,6 +117,8 @@ object TangemSdkErrorMapper { is TangemSdkError.BiometricsAuthenticationLockout -> error is TangemSdkError.BiometricsAuthenticationPermanentLockout -> error is TangemSdkError.UserCanceledBiometricsAuthentication -> error + is TangemSdkError.EncryptionOperationFailed -> error + is TangemSdkError.InvalidEncryptionKey -> error } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/UserWalletListError.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/UserWalletListError.kt deleted file mode 100644 index bf42b9c005..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/UserWalletListError.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.tangem.tap.domain.userWalletList - -import com.tangem.common.core.TangemError -import com.tangem.wallet.R - -sealed class UserWalletListError(code: Int) : TangemError(code) { - override val silent: Boolean - get() = (cause as? TangemError)?.silent == true - - override val messageResId: Int? = null - - object WalletAlreadySaved : UserWalletListError(code = 60001) { - override var customMessage: String = "This wallet has already been saved, you can add another one" - override val messageResId: Int = R.string.user_wallet_list_error_wallet_already_saved - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/UserWalletsListError.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/UserWalletsListError.kt new file mode 100644 index 0000000000..03ec981f62 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/UserWalletsListError.kt @@ -0,0 +1,29 @@ +package com.tangem.tap.domain.userWalletList + +import com.tangem.common.core.TangemError +import com.tangem.wallet.R + +sealed class UserWalletsListError(code: Int) : TangemError(code) { + override val silent: Boolean + get() = (cause as? TangemError)?.silent == true + + override val messageResId: Int? = null + + object WalletAlreadySaved : UserWalletsListError(code = 60001) { + override var customMessage: String = "This wallet has already been saved, you can add another one" + override val messageResId: Int = R.string.user_wallet_list_error_wallet_already_saved + } + + object InvalidEncryptionKey : UserWalletsListError(code = 60002) { + override var customMessage: String = "Invalid encryption key" + } + + data class BiometricsAuthenticationLockout(val isPermanent: Boolean) : UserWalletsListError(code = 60003) { + override var customMessage: String = "Biometric authentication lockout, permanent: $isPermanent" + } + + data class UnableToUnlockUserWallets(override val cause: Throwable? = null) : UserWalletsListError(code = 60004) { + override var customMessage: String = "An error has occurred, please scan your card to log in" + override val messageResId: Int = R.string.user_wallet_list_error_unable_to_unlock + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/UserWalletsListManager.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/UserWalletsListManager.kt index ea9b4c6bb9..4286bfd8c0 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/UserWalletsListManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/UserWalletsListManager.kt @@ -39,7 +39,7 @@ interface UserWalletsListManager { /** * Save provided user wallet and set it as selected * @param userWallet [UserWallet] to save - * @param canOverride If false, then terminate with [UserWalletListError.WalletAlreadySaved] when user tries + * @param canOverride If false, then terminate with [UserWalletsListError.WalletAlreadySaved] when user tries * to save an already saved card * @return [CompletionResult] of operation */ @@ -47,7 +47,7 @@ interface UserWalletsListManager { /** * Same as [save] but not change selected user wallet ID - * and not terminate with [UserWalletListError.WalletAlreadySaved] if [UserWallet] already saved + * and not terminate with [UserWalletsListError.WalletAlreadySaved] if [UserWallet] already saved * * Can terminate with [NoSuchElementException] if unable to find [UserWallet] with provided [UserWalletId] * @param userWalletId update [UserWallet] with that [UserWalletId] @@ -108,7 +108,7 @@ interface UserWalletsListManager { * @return [CompletionResult] of operation, with selected [UserWallet] * or null if there is no selected [UserWallet] * */ - suspend fun unlock(): CompletionResult + suspend fun unlock(): CompletionResult /** * Remove [UserWallet]s from [userWallets] and set [isLocked] as true diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/UserWalletsListManagerExtensions.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/UserWalletsListManagerExtensions.kt index 6da40c3b93..b795c7df78 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/UserWalletsListManagerExtensions.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/UserWalletsListManagerExtensions.kt @@ -16,6 +16,8 @@ val UserWalletsListManager.isLockable: Boolean * * @return If [UserWalletsListManager] not implements [UserWalletsListManager.Lockable] returns [Flow] which * produces only one false value + * + * @see UserWalletsListManager.Lockable.isLockedSync * */ val UserWalletsListManager.isLocked: Flow get() = asLockable()?.isLocked ?: flowOf(false) @@ -24,6 +26,8 @@ val UserWalletsListManager.isLocked: Flow * Indicates that the [UserWalletsListManager] is locked * * @return If [UserWalletsListManager] not implements [UserWalletsListManager.Lockable] returns false + * + * @see UserWalletsListManager.Lockable.isLockedSync * */ val UserWalletsListManager.isLockedSync: Boolean get() = asLockable()?.isLockedSync ?: false @@ -32,15 +36,22 @@ val UserWalletsListManager.isLockedSync: Boolean * Call [UserWalletsListManager.Lockable.unlock] if [UserWalletsListManager] implements [UserWalletsListManager.Lockable] * * @return If [UserWalletsListManager] not implements [UserWalletsListManager.Lockable] - * returns [CompletionResult.Success] with [UserWalletsListManager.selectedUserWalletSync] + * returns [CompletionResult.Failure] with [UserWalletsListError.UnableToUnlockUserWallets] + * + * If [UserWalletsListManager] implements [UserWalletsListManager.Lockable] + * returns [CompletionResult.Success] with selected [UserWallet] + * + * @see UserWalletsListManager.Lockable.unlock * */ -suspend fun UserWalletsListManager.unlockIfLockable(): CompletionResult { - return asLockable()?.unlock() ?: CompletionResult.Success(selectedUserWalletSync) +suspend fun UserWalletsListManager.unlockIfLockable(): CompletionResult { + return asLockable()?.unlock() ?: CompletionResult.Failure(UserWalletsListError.UnableToUnlockUserWallets()) } /** * Call [UserWalletsListManager.Lockable.lock] if [UserWalletsListManager] implements [UserWalletsListManager.Lockable] * or do nothing otherwise + * + * @see UserWalletsListManager.Lockable.lock * */ fun UserWalletsListManager.lockIfLockable() { asLockable()?.lock() diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt index 060464fd2f..b4bcf48c73 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt @@ -1,9 +1,10 @@ package com.tangem.tap.domain.userWalletList.implementation import com.tangem.common.* +import com.tangem.common.extensions.guard import com.tangem.domain.common.util.UserWalletId import com.tangem.tap.domain.model.UserWallet -import com.tangem.tap.domain.userWalletList.UserWalletListError +import com.tangem.tap.domain.userWalletList.UserWalletsListError import com.tangem.tap.domain.userWalletList.UserWalletsListManager import com.tangem.tap.domain.userWalletList.model.UserWalletEncryptionKey import com.tangem.tap.domain.userWalletList.repository.SelectedUserWalletRepository @@ -15,7 +16,6 @@ import com.tangem.tap.domain.userWalletList.utils.toUserWallets import com.tangem.tap.domain.userWalletList.utils.updateWith import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* -import timber.log.Timber @OptIn(ExperimentalCoroutinesApi::class) internal class BiometricUserWalletsListManager( @@ -53,9 +53,32 @@ internal class BiometricUserWalletsListManager( override val hasUserWallets: Boolean get() = keysRepository.hasSavedEncryptionKeys() - override suspend fun unlock(): CompletionResult { + override suspend fun unlock(): CompletionResult { + // Occurs when user has locked user wallets after unlocking with biometry + // e.g. after biometric storage master key invalidation + if (state.value.hasLockedUserWalletsAfterUnlock) { + return CompletionResult.Failure( + error = UserWalletsListError.UnableToUnlockUserWallets( + cause = IllegalStateException("Permanently locked"), + ), + ) + } + return unlockWithBiometryInternal() - .map { selectedUserWalletSync } + .mapFailure { error -> + if (error is UserWalletsListError) { + error + } else { + UserWalletsListError.UnableToUnlockUserWallets(cause = error) + } + } + .map { + selectedUserWalletSync.guard { + throw UserWalletsListError.UnableToUnlockUserWallets( + cause = IllegalStateException("No user wallet selected"), + ) + } + } } override fun lock() { @@ -88,7 +111,7 @@ internal class BiometricUserWalletsListManager( } if (isWalletSaved) { - CompletionResult.Failure(UserWalletListError.WalletAlreadySaved) + CompletionResult.Failure(UserWalletsListError.WalletAlreadySaved) } else { saveInternal(userWallet, changeSelectedUserWallet = true) } @@ -188,8 +211,10 @@ internal class BiometricUserWalletsListManager( .flatMap { loadModels() } .map { state.update { prevState -> + val hasLockedUserWallets = prevState.userWallets.any { it.isLocked } prevState.copy( - isLocked = false, + isLocked = hasLockedUserWallets, + hasLockedUserWalletsAfterUnlock = hasLockedUserWallets, ) } } @@ -234,7 +259,13 @@ internal class BiometricUserWalletsListManager( } } .doOnFailure { error -> - Timber.e(error, "Unable to load user wallets") + if (error is UserWalletsListError.InvalidEncryptionKey) { + state.update { prevState -> + prevState.copy( + hasLockedUserWalletsAfterUnlock = true, + ) + } + } } } @@ -290,7 +321,7 @@ internal class BiometricUserWalletsListManager( private fun findSelectedUserWallet(userWallets: List = state.value.userWallets): UserWallet? { return userWallets.firstOrNull { - it.walletId == state.value.selectedUserWalletId + it.walletId == state.value.selectedUserWalletId && !it.isLocked } } @@ -299,5 +330,6 @@ internal class BiometricUserWalletsListManager( val userWallets: List = emptyList(), val selectedUserWalletId: UserWalletId? = null, val isLocked: Boolean = true, + val hasLockedUserWalletsAfterUnlock: Boolean = false, ) } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/RuntimeUserWalletsListManager.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/RuntimeUserWalletsListManager.kt index 01a31ebff4..f726dba792 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/RuntimeUserWalletsListManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/RuntimeUserWalletsListManager.kt @@ -4,7 +4,7 @@ import com.tangem.common.CompletionResult import com.tangem.common.catching import com.tangem.domain.common.util.UserWalletId import com.tangem.tap.domain.model.UserWallet -import com.tangem.tap.domain.userWalletList.UserWalletListError +import com.tangem.tap.domain.userWalletList.UserWalletsListError import com.tangem.tap.domain.userWalletList.UserWalletsListManager import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow @@ -49,7 +49,7 @@ internal class RuntimeUserWalletsListManager : UserWalletsListManager { val isWalletSaved = state.value.userWallet?.walletId == userWallet.walletId if (isWalletSaved) { - CompletionResult.Failure(UserWalletListError.WalletAlreadySaved) + CompletionResult.Failure(UserWalletsListError.WalletAlreadySaved) } else { saveInternal(userWallet) } diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/BiometricUserWalletsKeysRepository.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/BiometricUserWalletsKeysRepository.kt index f3f39a647f..0307b8b7d4 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/BiometricUserWalletsKeysRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/BiometricUserWalletsKeysRepository.kt @@ -8,10 +8,13 @@ import com.tangem.common.biometric.BiometricManager import com.tangem.common.biometric.BiometricStorage import com.tangem.common.core.TangemSdkError import com.tangem.common.doOnFailure +import com.tangem.common.flatMapOnFailure import com.tangem.common.fold import com.tangem.common.map +import com.tangem.common.mapFailure import com.tangem.common.services.secure.SecureStorage import com.tangem.domain.common.util.UserWalletId +import com.tangem.tap.domain.userWalletList.UserWalletsListError import com.tangem.tap.domain.userWalletList.model.UserWalletEncryptionKey import com.tangem.tap.domain.userWalletList.repository.UserWalletsKeysRepository import kotlinx.coroutines.Dispatchers @@ -37,6 +40,16 @@ internal class BiometricUserWalletsKeysRepository( override suspend fun getAll(): CompletionResult> { return withContext(Dispatchers.IO) { getAllInternal() + .mapFailure { error -> + when (error) { + is TangemSdkError.BiometricsAuthenticationLockout -> + UserWalletsListError.BiometricsAuthenticationLockout(isPermanent = false) + is TangemSdkError.BiometricsAuthenticationPermanentLockout -> + UserWalletsListError.BiometricsAuthenticationLockout(isPermanent = true) + is TangemSdkError.InvalidEncryptionKey -> UserWalletsListError.InvalidEncryptionKey + else -> error + } + } } } @@ -79,12 +92,28 @@ internal class BiometricUserWalletsKeysRepository( return getUserWalletsIds() .map { userWalletId -> // This is possible because the Card SDK cipher key has an expiration time - // If this operation runs more than that expiration time, the user will not receive all encryption keys + // If this operation runs more than that expiration time, the user will have to re-authorize + // to receive all keys getEncryptionKey(userWalletId) + .flatMapOnFailure { error -> + // If key decryption failed then skip it + if (error is TangemSdkError.EncryptionOperationFailed) { + CompletionResult.Success(data = null) + } else { + CompletionResult.Failure(error) + } + } .doOnFailure { error -> - // If the user cancels biometric authentication, cancel the request for all keys - if (error is TangemSdkError.UserCanceledBiometricsAuthentication) { - return CompletionResult.Failure(error) + when (error) { + is TangemSdkError.UserCanceledBiometricsAuthentication -> { + // If the user cancels biometric authentication, cancel the request for all keys + return CompletionResult.Failure(error) + } + is TangemSdkError.InvalidEncryptionKey -> { + if (error.isKeyRegenerated) { + deleteEncryptionKey(userWalletId) + } + } } } } @@ -94,20 +123,20 @@ internal class BiometricUserWalletsKeysRepository( } private suspend fun getEncryptionKey(userWalletId: UserWalletId): CompletionResult { - return biometricStorage.get(StorageKey.WalletEncryptionKey(userWalletId).name) + return biometricStorage.get(StorageKey.UserWalletEncryptionKey(userWalletId).name) .map { it.decodeToKey() } } private suspend fun storeEncryptionKey(encryptionKey: UserWalletEncryptionKey): CompletionResult { return biometricStorage.store( - key = StorageKey.WalletEncryptionKey(encryptionKey.walletId).name, + key = StorageKey.UserWalletEncryptionKey(encryptionKey.walletId).name, data = encryptionKey.encode(), ) .map { storeUserWalletId(encryptionKey.walletId) } } private suspend fun deleteEncryptionKey(userWalletId: UserWalletId): CompletionResult { - return biometricStorage.delete(StorageKey.WalletEncryptionKey(userWalletId).name) + return biometricStorage.delete(StorageKey.UserWalletEncryptionKey(userWalletId).name) } private suspend fun getUserWalletsIds(): List { @@ -175,7 +204,7 @@ internal class BiometricUserWalletsKeysRepository( private sealed interface StorageKey { val name: String - class WalletEncryptionKey(userWalletId: UserWalletId) : StorageKey { + class UserWalletEncryptionKey(userWalletId: UserWalletId) : StorageKey { override val name: String = "user_wallet_encryption_key_${userWalletId.stringValue}" } diff --git a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/WalletSelectorBottomSheetFragment.kt b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/WalletSelectorBottomSheetFragment.kt index 11fec17c6d..dbb367554c 100644 --- a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/WalletSelectorBottomSheetFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/WalletSelectorBottomSheetFragment.kt @@ -25,11 +25,13 @@ import com.tangem.core.ui.fragments.ComposeBottomSheetFragment import com.tangem.core.ui.res.TangemTheme import com.tangem.tap.common.analytics.events.MyWallets import com.tangem.tap.features.details.ui.cardsettings.resolveReference -import com.tangem.tap.features.walletSelector.ui.components.BiometricsLockoutDialogContent +import com.tangem.tap.features.walletSelector.ui.components.BiometricsLockoutWarningContent +import com.tangem.tap.features.walletSelector.ui.components.KeyInvalidatedWarningContent import com.tangem.tap.features.walletSelector.ui.components.RemoveWalletDialogContent import com.tangem.tap.features.walletSelector.ui.components.RenameWalletDialogContent import com.tangem.tap.features.walletSelector.ui.components.WalletSelectorScreenContent import com.tangem.tap.features.walletSelector.ui.model.DialogModel +import com.tangem.tap.features.walletSelector.ui.model.WarningModel internal class WalletSelectorBottomSheetFragment : ComposeBottomSheetFragment() { private val viewModel by viewModels() @@ -93,7 +95,8 @@ internal class WalletSelectorBottomSheetFragment : ComposeBottomSheetFragment RemoveWalletDialogContent(dialog) is DialogModel.RenameWalletDialog -> RenameWalletDialogContent(dialog) - is DialogModel.BiometricsLockoutDialog -> BiometricsLockoutDialogContent(dialog) + is WarningModel.BiometricsLockoutWarning -> BiometricsLockoutWarningContent(dialog) + is WarningModel.KeyInvalidatedWarning -> KeyInvalidatedWarningContent(dialog) } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/WalletSelectorViewModel.kt b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/WalletSelectorViewModel.kt index b048098525..9384a5f22b 100644 --- a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/WalletSelectorViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/WalletSelectorViewModel.kt @@ -3,11 +3,11 @@ package com.tangem.tap.features.walletSelector.ui import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.tangem.common.core.TangemError -import com.tangem.common.core.TangemSdkError import com.tangem.core.analytics.Analytics import com.tangem.domain.common.util.UserWalletId import com.tangem.tap.common.analytics.events.MyWallets import com.tangem.tap.common.extensions.dispatchOnMain +import com.tangem.tap.domain.userWalletList.UserWalletsListError import com.tangem.tap.domain.userWalletList.isLocked import com.tangem.tap.features.details.ui.cardsettings.TextReference import com.tangem.tap.features.walletSelector.redux.WalletSelectorAction @@ -15,6 +15,7 @@ import com.tangem.tap.features.walletSelector.redux.WalletSelectorState import com.tangem.tap.features.walletSelector.ui.model.DialogModel import com.tangem.tap.features.walletSelector.ui.model.MultiCurrencyUserWalletItem import com.tangem.tap.features.walletSelector.ui.model.SingleCurrencyUserWalletItem +import com.tangem.tap.features.walletSelector.ui.model.WarningModel import com.tangem.tap.store import com.tangem.tap.userWalletsListManager import com.tangem.tap.walletStoresManager @@ -159,7 +160,7 @@ internal class WalletSelectorViewModel : ViewModel(), StoreSubscriber error.messageResId?.let { TextReference.Res(it) } ?: TextReference.Str(error.customMessage) @@ -184,24 +185,23 @@ internal class WalletSelectorViewModel : ViewModel(), StoreSubscriber DialogModel.BiometricsLockoutDialog( - isPermanent = false, - onDismiss = this::dismissDialog, + is UserWalletsListError.BiometricsAuthenticationLockout -> WarningModel.BiometricsLockoutWarning( + isPermanent = error.isPermanent, + onDismiss = this::dismissWarningDialog, ) - is TangemSdkError.BiometricsAuthenticationPermanentLockout -> DialogModel.BiometricsLockoutDialog( - isPermanent = true, - onDismiss = this::dismissDialog, + is UserWalletsListError.InvalidEncryptionKey -> WarningModel.KeyInvalidatedWarning( + onDismiss = this::dismissWarningDialog, ) - else -> currentDialogModel + else -> currentDialog } } - private fun dismissDialog() { + private fun dismissWarningDialog() { stateInternal.update { prevState -> prevState.copy( dialog = null, diff --git a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/BiometricsLockoutDialogContent.kt b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/BiometricsLockoutWarningContent.kt similarity index 81% rename from app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/BiometricsLockoutDialogContent.kt rename to app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/BiometricsLockoutWarningContent.kt index cb724fe222..1416b5af61 100644 --- a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/BiometricsLockoutDialogContent.kt +++ b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/BiometricsLockoutWarningContent.kt @@ -8,26 +8,26 @@ import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.components.BasicDialog import com.tangem.core.ui.components.DialogButton import com.tangem.core.ui.res.TangemTheme -import com.tangem.tap.features.walletSelector.ui.model.DialogModel +import com.tangem.tap.features.walletSelector.ui.model.WarningModel import com.tangem.wallet.R @Composable -internal fun BiometricsLockoutDialogContent( - dialog: DialogModel.BiometricsLockoutDialog, +internal fun BiometricsLockoutWarningContent( + warning: WarningModel.BiometricsLockoutWarning, ) { BasicDialog( title = stringResource(id = R.string.biometric_lockout_warning_title), message = stringResource( - id = if (dialog.isPermanent) { + id = if (warning.isPermanent) { R.string.biometric_lockout_permanent_warning_description } else { R.string.biometric_lockout_warning_description }, ), - onDismissDialog = dialog.onDismiss, + onDismissDialog = warning.onDismiss, confirmButton = DialogButton( title = stringResource(id = R.string.common_ok), - onClick = dialog.onDismiss, + onClick = warning.onDismiss, ), ) } @@ -38,8 +38,8 @@ private fun BiometricsLockoutDialogSample( modifier: Modifier = Modifier, ) { Column(modifier = modifier) { - BiometricsLockoutDialogContent( - dialog = DialogModel.BiometricsLockoutDialog( + BiometricsLockoutWarningContent( + warning = WarningModel.BiometricsLockoutWarning( isPermanent = false, onDismiss = {}, ), @@ -68,8 +68,8 @@ private fun BiometricsLockoutDialog_Permanent_Sample( modifier: Modifier = Modifier, ) { Column(modifier = modifier) { - BiometricsLockoutDialogContent( - dialog = DialogModel.BiometricsLockoutDialog( + BiometricsLockoutWarningContent( + warning = WarningModel.BiometricsLockoutWarning( isPermanent = true, onDismiss = {}, ), diff --git a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/KeyInvalidatedWarningContent.kt b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/KeyInvalidatedWarningContent.kt new file mode 100644 index 0000000000..0b31a73101 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/KeyInvalidatedWarningContent.kt @@ -0,0 +1,56 @@ +package com.tangem.tap.features.walletSelector.ui.components + +import androidx.compose.foundation.layout.Column +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.components.BasicDialog +import com.tangem.core.ui.components.DialogButton +import com.tangem.core.ui.res.TangemTheme +import com.tangem.tap.features.walletSelector.ui.model.WarningModel +import com.tangem.wallet.R + +@Composable +internal fun KeyInvalidatedWarningContent( + warning: WarningModel.KeyInvalidatedWarning, +) { + BasicDialog( + title = stringResource(id = R.string.common_attention), + message = stringResource(id = R.string.key_invalidated_warning_description), + onDismissDialog = warning.onDismiss, + confirmButton = DialogButton( + title = stringResource(id = R.string.common_ok), + onClick = warning.onDismiss, + ), + ) +} + +// region Preview +@Composable +private fun KeyInvalidatedWarningSample( + modifier: Modifier = Modifier, +) { + Column(modifier = modifier) { + KeyInvalidatedWarningContent( + warning = WarningModel.KeyInvalidatedWarning(onDismiss = {}), + ) + } +} + +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun KeyInvalidatedWarningPreview_Light() { + TangemTheme { + KeyInvalidatedWarningSample() + } +} + +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun KeyInvalidatedWarningPreview_Dark() { + TangemTheme(isDark = true) { + KeyInvalidatedWarningSample() + } +} +// endregion Preview \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/model/DialogModel.kt b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/model/DialogModel.kt index 309df35752..8024e00021 100644 --- a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/model/DialogModel.kt +++ b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/model/DialogModel.kt @@ -11,9 +11,15 @@ internal sealed interface DialogModel { val onConfirm: () -> Unit, val onDismiss: () -> Unit, ) : DialogModel +} - data class BiometricsLockoutDialog( +internal sealed interface WarningModel : DialogModel { + data class BiometricsLockoutWarning( val isPermanent: Boolean, val onDismiss: () -> Unit, - ) : DialogModel + ) : WarningModel + + data class KeyInvalidatedWarning( + val onDismiss: () -> Unit, + ) : WarningModel } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt index c8cde60e79..84ff91eebf 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt @@ -27,6 +27,7 @@ import com.tangem.tap.tangemSdkManager import com.tangem.tap.userWalletsListManager import kotlinx.coroutines.launch import org.rekotlin.Middleware +import timber.log.Timber internal class WelcomeMiddleware { val middleware: Middleware = { _, appStateProvider -> @@ -69,16 +70,15 @@ internal class WelcomeMiddleware { scope.launch { userWalletsListManager.unlockIfLockable() .doOnFailure { error -> + Timber.e(error, "Unable to unlock user wallets with biometrics") store.dispatchOnMain(WelcomeAction.ProceedWithBiometrics.Error(error)) } .doOnSuccess { selectedUserWallet -> - if (selectedUserWallet != null) { - store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Wallet)) - store.dispatchOnMain(WelcomeAction.ProceedWithBiometrics.Success) - store.onUserWalletSelected(selectedUserWallet) + store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Wallet)) + store.dispatchOnMain(WelcomeAction.ProceedWithBiometrics.Success) + store.onUserWalletSelected(selectedUserWallet) - intentHandler.handleWalletConnectLink(state.intent) - } + intentHandler.handleWalletConnectLink(state.intent) } } } @@ -89,6 +89,7 @@ internal class WelcomeMiddleware { userWalletsListManager.save(userWallet, canOverride = true) .doOnFailure { error -> + Timber.e(error, "Unable to save user wallet") store.dispatchOnMain(WelcomeAction.ProceedWithCard.Error(error)) } .doOnSuccess { diff --git a/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeFragment.kt b/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeFragment.kt index b80d65ce7a..e346adb6d1 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeFragment.kt @@ -24,7 +24,7 @@ import com.tangem.core.ui.fragments.ComposeFragment import com.tangem.core.ui.res.TangemTheme import com.tangem.tap.features.details.ui.cardsettings.resolveReference import com.tangem.tap.features.welcome.redux.WelcomeAction -import com.tangem.tap.features.welcome.ui.components.BiometricsLockoutDialog +import com.tangem.tap.features.welcome.ui.components.WarningDialog import com.tangem.tap.features.welcome.ui.components.WelcomeScreenContent import com.tangem.tap.store import com.tangem.wallet.R @@ -49,7 +49,7 @@ internal class WelcomeFragment : ComposeFragment() { ) { val snackbarHostState = remember { SnackbarHostState() } val errorMessage by rememberUpdatedState(newValue = state.error?.resolveReference()) - val biometricsLockoutDialog by rememberUpdatedState(newValue = state.biometricsLockoutDialog) + val warning by rememberUpdatedState(newValue = state.warning) val backgroundColor = colorResource(id = R.color.background_primary) SystemBarsEffect { @@ -80,7 +80,7 @@ internal class WelcomeFragment : ComposeFragment() { ) } - BiometricsLockoutDialog(biometricsLockoutDialog) + WarningDialog(warning) LaunchedEffect(key1 = errorMessage) { errorMessage?.let { diff --git a/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeScreenState.kt b/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeScreenState.kt index f9ea592c62..701042816e 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeScreenState.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeScreenState.kt @@ -2,12 +2,12 @@ package com.tangem.tap.features.welcome.ui import androidx.compose.runtime.Immutable import com.tangem.tap.features.details.ui.cardsettings.TextReference -import com.tangem.tap.features.welcome.ui.model.BiometricsLockoutDialog +import com.tangem.tap.features.welcome.ui.model.WarningModel @Immutable internal data class WelcomeScreenState( val showUnlockWithBiometricsProgress: Boolean = false, val showUnlockWithCardProgress: Boolean = false, - val biometricsLockoutDialog: BiometricsLockoutDialog? = null, + val warning: WarningModel? = null, val error: TextReference? = null, ) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeViewModel.kt b/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeViewModel.kt index 042034cfd8..4bbcdf76ea 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeViewModel.kt @@ -2,14 +2,14 @@ package com.tangem.tap.features.welcome.ui import androidx.lifecycle.ViewModel import com.tangem.common.core.TangemError -import com.tangem.common.core.TangemSdkError import com.tangem.core.analytics.Analytics import com.tangem.tap.common.analytics.events.SignIn import com.tangem.tap.common.redux.global.GlobalAction +import com.tangem.tap.domain.userWalletList.UserWalletsListError import com.tangem.tap.features.details.ui.cardsettings.TextReference import com.tangem.tap.features.welcome.redux.WelcomeAction import com.tangem.tap.features.welcome.redux.WelcomeState -import com.tangem.tap.features.welcome.ui.model.BiometricsLockoutDialog +import com.tangem.tap.features.welcome.ui.model.WarningModel import com.tangem.tap.store import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -41,15 +41,15 @@ internal class WelcomeViewModel : ViewModel(), StoreSubscriber { // TODO: Refactor errors handling override fun newState(state: WelcomeState) { - val biometricsLockoutDialog = createBiometricLockoutDialogIfNeeded(state.error) + val warning = createWarningIfNeeded(state.error) stateInternal.update { prevState -> prevState.copy( showUnlockWithBiometricsProgress = state.isUnlockWithBiometricsInProgress, showUnlockWithCardProgress = state.isUnlockWithCardInProgress, - biometricsLockoutDialog = biometricsLockoutDialog, + warning = warning, error = state.error - ?.takeIf { !it.silent && biometricsLockoutDialog == null } + ?.takeIf { !it.silent && warning == null } ?.let { e -> e.messageResId?.let { TextReference.Res(it) } ?: TextReference.Str(e.customMessage) @@ -62,24 +62,23 @@ internal class WelcomeViewModel : ViewModel(), StoreSubscriber { store.unsubscribe(this) } - private fun createBiometricLockoutDialogIfNeeded(error: TangemError?): BiometricsLockoutDialog? { + private fun createWarningIfNeeded(error: TangemError?): WarningModel? { return when (error) { - is TangemSdkError.BiometricsAuthenticationLockout -> BiometricsLockoutDialog( - isPermanent = false, - onDismiss = this::dismissDialog, + is UserWalletsListError.BiometricsAuthenticationLockout -> WarningModel.BiometricsLockoutWarning( + isPermanent = error.isPermanent, + onDismiss = this::dismissWarning, ) - is TangemSdkError.BiometricsAuthenticationPermanentLockout -> BiometricsLockoutDialog( - isPermanent = true, - onDismiss = this::dismissDialog, + is UserWalletsListError.InvalidEncryptionKey -> WarningModel.KeyInvalidatedWarning( + onDismiss = this::dismissWarning, ) else -> null } } - private fun dismissDialog() { + private fun dismissWarning() { stateInternal.update { prevState -> prevState.copy( - biometricsLockoutDialog = null, + warning = null, ) } closeError() diff --git a/app/src/main/java/com/tangem/tap/features/welcome/ui/components/BiometricsLockoutDialog.kt b/app/src/main/java/com/tangem/tap/features/welcome/ui/components/BiometricsLockoutDialog.kt deleted file mode 100644 index 0a594b864d..0000000000 --- a/app/src/main/java/com/tangem/tap/features/welcome/ui/components/BiometricsLockoutDialog.kt +++ /dev/null @@ -1,96 +0,0 @@ -package com.tangem.tap.features.welcome.ui.components - -import androidx.compose.foundation.layout.Column -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.components.BasicDialog -import com.tangem.core.ui.components.DialogButton -import com.tangem.core.ui.res.TangemTheme -import com.tangem.tap.features.welcome.ui.model.BiometricsLockoutDialog -import com.tangem.wallet.R - -@Composable -internal fun BiometricsLockoutDialog( - dialog: BiometricsLockoutDialog?, -) { - if (dialog == null) return - BasicDialog( - title = stringResource(id = R.string.biometric_lockout_warning_title), - message = stringResource( - id = if (dialog.isPermanent) { - R.string.biometric_lockout_permanent_warning_description - } else { - R.string.biometric_lockout_warning_description - }, - ), - onDismissDialog = dialog.onDismiss, - confirmButton = DialogButton( - title = stringResource(id = R.string.common_ok), - onClick = dialog.onDismiss, - ), - ) -} - -// region Preview -@Composable -private fun BiometricsLockoutDialogSample( - modifier: Modifier = Modifier, -) { - Column(modifier = modifier) { - BiometricsLockoutDialog( - dialog = BiometricsLockoutDialog( - isPermanent = false, - onDismiss = {}, - ), - ) - } -} - -@Preview(showBackground = true, widthDp = 360) -@Composable -private fun BiometricsLockoutDialogPreview_Light() { - TangemTheme { - BiometricsLockoutDialogSample() - } -} - -@Preview(showBackground = true, widthDp = 360) -@Composable -private fun BiometricsLockoutDialogPreview_Dark() { - TangemTheme(isDark = true) { - BiometricsLockoutDialogSample() - } -} - -@Composable -private fun BiometricsLockoutDialog_Permanent_Sample( - modifier: Modifier = Modifier, -) { - Column(modifier = modifier) { - BiometricsLockoutDialog( - dialog = BiometricsLockoutDialog( - isPermanent = true, - onDismiss = {}, - ), - ) - } -} - -@Preview(showBackground = true, widthDp = 360) -@Composable -private fun BiometricsLockoutDialog_Permanent_Preview_Light() { - TangemTheme { - BiometricsLockoutDialog_Permanent_Sample() - } -} - -@Preview(showBackground = true, widthDp = 360) -@Composable -private fun BiometricsLockoutDialog_Permanent_Preview_Dark() { - TangemTheme(isDark = true) { - BiometricsLockoutDialog_Permanent_Sample() - } -} -// endregion Preview \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WarningDialog.kt b/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WarningDialog.kt new file mode 100644 index 0000000000..fb974cc393 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WarningDialog.kt @@ -0,0 +1,138 @@ +package com.tangem.tap.features.welcome.ui.components + +import androidx.compose.foundation.layout.Column +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.components.BasicDialog +import com.tangem.core.ui.components.DialogButton +import com.tangem.core.ui.res.TangemTheme +import com.tangem.tap.features.welcome.ui.model.WarningModel +import com.tangem.wallet.R + +@Composable +internal fun WarningDialog( + warning: WarningModel?, +) { + when (warning) { + null -> Unit + is WarningModel.BiometricsLockoutWarning -> { + BasicDialog( + title = stringResource(id = R.string.biometric_lockout_warning_title), + message = stringResource( + id = if (warning.isPermanent) { + R.string.biometric_lockout_permanent_warning_description + } else { + R.string.biometric_lockout_warning_description + }, + ), + onDismissDialog = warning.onDismiss, + confirmButton = DialogButton( + title = stringResource(id = R.string.common_ok), + onClick = warning.onDismiss, + ), + ) + } + is WarningModel.KeyInvalidatedWarning -> { + BasicDialog( + title = stringResource(id = R.string.common_attention), + message = stringResource(id = R.string.key_invalidated_warning_description), + onDismissDialog = warning.onDismiss, + confirmButton = DialogButton( + title = stringResource(id = R.string.common_ok), + onClick = warning.onDismiss, + ), + ) + } + } +} + +// region Preview +@Composable +private fun BiometricsLockoutDialogSample( + modifier: Modifier = Modifier, +) { + Column(modifier = modifier) { + WarningDialog( + warning = WarningModel.BiometricsLockoutWarning( + isPermanent = false, + onDismiss = {}, + ), + ) + } +} + +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun BiometricsLockoutDialogPreview_Light() { + TangemTheme { + BiometricsLockoutDialogSample() + } +} + +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun BiometricsLockoutDialogPreview_Dark() { + TangemTheme(isDark = true) { + BiometricsLockoutDialogSample() + } +} + +@Composable +private fun BiometricsLockoutDialog_Permanent_Sample( + modifier: Modifier = Modifier, +) { + Column(modifier = modifier) { + WarningDialog( + warning = WarningModel.BiometricsLockoutWarning( + isPermanent = true, + onDismiss = {}, + ), + ) + } +} + +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun BiometricsLockoutDialog_Permanent_Preview_Light() { + TangemTheme { + BiometricsLockoutDialog_Permanent_Sample() + } +} + +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun BiometricsLockoutDialog_Permanent_Preview_Dark() { + TangemTheme(isDark = true) { + BiometricsLockoutDialog_Permanent_Sample() + } +} + +// region Preview +@Composable +private fun KeyInvalidatedWarningSample( + modifier: Modifier = Modifier, +) { + Column(modifier = modifier) { + WarningDialog(warning = WarningModel.KeyInvalidatedWarning(onDismiss = {})) + } +} + +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun KeyInvalidatedWarningPreview_Light() { + TangemTheme { + KeyInvalidatedWarningSample() + } +} + +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun KeyInvalidatedWarningPreview_Dark() { + TangemTheme(isDark = true) { + KeyInvalidatedWarningSample() + } +} +// endregion Preview +// endregion Preview \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/welcome/ui/model/BiometricsLockoutDialog.kt b/app/src/main/java/com/tangem/tap/features/welcome/ui/model/BiometricsLockoutDialog.kt deleted file mode 100644 index bb1e4bf3cd..0000000000 --- a/app/src/main/java/com/tangem/tap/features/welcome/ui/model/BiometricsLockoutDialog.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.tangem.tap.features.welcome.ui.model - -internal data class BiometricsLockoutDialog( - val isPermanent: Boolean, - val onDismiss: () -> Unit, -) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/welcome/ui/model/WarningModel.kt b/app/src/main/java/com/tangem/tap/features/welcome/ui/model/WarningModel.kt new file mode 100644 index 0000000000..4940a4642a --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/welcome/ui/model/WarningModel.kt @@ -0,0 +1,12 @@ +package com.tangem.tap.features.welcome.ui.model + +internal sealed interface WarningModel { + data class BiometricsLockoutWarning( + val isPermanent: Boolean, + val onDismiss: () -> Unit, + ) : WarningModel + + data class KeyInvalidatedWarning( + val onDismiss: () -> Unit, + ) : WarningModel +} \ No newline at end of file diff --git a/buildSrc/src/main/java/Versions.kt b/buildSrc/src/main/java/Versions.kt index b02e115cd7..c59727ad73 100644 --- a/buildSrc/src/main/java/Versions.kt +++ b/buildSrc/src/main/java/Versions.kt @@ -64,7 +64,7 @@ object Versions { const val tangemBlockchainSdk = "develop-174" // const val tangemBlockchainSdk = "0.0.1" // Keep it! - used for local builds - const val tangemCardSdk = "develop-199" + const val tangemCardSdk = "develop-200" // const val tangemCardSdk = "0.0.1" // Keep it! - used for local builds // endregion Tangem diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index ffa90384f5..a0dd22a168 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -339,7 +339,7 @@ Swapping this amount of selected tokens will cause a significant price impact and reduce your outcome. Insufficient funds Not enough funds for fee in your %1$s wallet to create a transaction. Top up your %2$s wallet first. - Transaction in progress... + Transaction in progress… Waiting Approve Give Permission @@ -371,7 +371,7 @@ You don\'t have any transactions yet Failed to load transactions Transactions - In progress... + In progress… You\'ve scanned the same card. To create a twin wallet you need to scan the card with number %d This one that you are holding in your hands and the other one with number %s.\n\nBoth cards can be used to extract funds from this wallet. One wallet. Two cards. @@ -384,6 +384,7 @@ Add new wallet Are you sure you want to delete this wallet? %d selected + An error has occurred, please scan your card to log in This wallet has already been saved, you can add another one Multi-currency Wallet name diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index a9c847c30f..43efbd281e 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -339,7 +339,7 @@ Swapping this amount of selected tokens will cause a significant price impact and reduce your outcome. Insufficient funds Not enough funds for fee in your %1$s wallet to create a transaction. Top up your %2$s wallet first. - Transaction in progress... + Transaction in progress… Waiting Approve Give Permission @@ -371,7 +371,7 @@ You don\'t have any transactions yet Failed to load transactions Transactions - In progress... + In progress… You\'ve scanned the same card. To create a twin wallet you need to scan the card with number %d This one that you are holding in your hands and the other one with number %s.\n\nBoth cards can be used to extract funds from this wallet. One wallet. Two cards. @@ -384,6 +384,7 @@ Add new wallet Are you sure you want to delete this wallet? %d selected + An error has occurred, please scan your card to log in This wallet has already been saved, you can add another one Multi-currency Wallet name diff --git a/core/res/src/main/res/values-it/strings.xml b/core/res/src/main/res/values-it/strings.xml index 4ae4f65caf..b1209fc917 100644 --- a/core/res/src/main/res/values-it/strings.xml +++ b/core/res/src/main/res/values-it/strings.xml @@ -339,7 +339,7 @@ Swapping this amount of selected tokens will cause a significant price impact and reduce your outcome. Insufficient funds Not enough funds for fee in your %1$s wallet to create a transaction. Top up your %2$s wallet first. - Transaction in progress... + Transaction in progress… Waiting Approve Give Permission @@ -371,7 +371,7 @@ You don\'t have any transactions yet Failed to load transactions Transactions - In progress... + In progress… You\'ve scanned the same card. To create a twin wallet you need to scan the card with number %d This one that you are holding in your hands and the other one with number %s.\n\nBoth cards can be used to extract funds from this wallet. One wallet. Two cards. @@ -384,6 +384,7 @@ Add new wallet Are you sure you want to delete this wallet? %d selected + An error has occurred, please scan your card to log in This wallet has already been saved, you can add another one Multi-currency Wallet name diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 86620877b5..41175e518c 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -132,6 +132,7 @@ Нажмите, чтобы подписать Приложите карту Внутренняя ошибка: не удается найти менеджер кошельков + Вы обновили данные биометрии, отсканируйте свою карту для входа Управление токенами Чтобы защитить свои активы, мы советуем вам выполнить эту процедуру Бэкап кошелька не был произведен @@ -384,6 +385,7 @@ Добавить новый кошелек Вы уверены, что хотите удалить этот кошелек? %d выбрано + Произошла ошибка, пожалуйста, отсканируйте свою карту для входа Этот кошелек уже был сохранен, вы можете добавить другой Мультивалютные Имя кошелька @@ -418,6 +420,7 @@ Не удалось установить сессию WalletConnect за отведённое время. Пожалуйста, повторите попытку позже. Запрос на подключение через WalletConnect содержит неподдерживаемые блокчеины. Неподдерживаемые блокчеины:\n Cоединение с этим Dapp сервисом не может быть установлено из-за его технической реализации. + Неверная карта выбрана в приложении Tangem Произошла непредвиденная ошибка. Код ошибки: %d Попробуйте, пожалуйста, позже. Если проблема будет продолжать возникать - обратитесь в службу поддержки. Сеть %s не найдена. Пожалуйста, добавьте её и попробуйте заново. Нет открытых сессий WalletConnect diff --git a/core/res/src/main/res/values-zh-rTW/strings.xml b/core/res/src/main/res/values-zh-rTW/strings.xml index 96b180a83f..dcc7b86b54 100644 --- a/core/res/src/main/res/values-zh-rTW/strings.xml +++ b/core/res/src/main/res/values-zh-rTW/strings.xml @@ -339,7 +339,7 @@ Swapping this amount of selected tokens will cause a significant price impact and reduce your outcome. 餘額不足 您的 %1$s 錢包中沒有足夠的資金來創建交易。首先為您的 %2$s 錢包充值 - 交易進行中... + 交易進行中… 等待中 允許 賦予權限 @@ -371,7 +371,7 @@ 您還沒有任何交易 無法加載交易 交易 - 進行中... + 進行中… 您掃描了同一張卡片。要創建雙錢包,您需要掃描編號為 %d 的卡 這一個是你手裡拿著的,另一個是編號為 %s 的,這兩張卡都可以用來從這個錢包中提取資金 一個錢包,兩張卡片 @@ -384,6 +384,7 @@ 添加新錢包 您確定要刪除此錢包? 已選擇 %d + An error has occurred, please scan your card to log in 此錢包已保存,您可以再添加一個 多幣種 錢包名稱 diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index afa7b78109..7425f3b247 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -132,6 +132,7 @@ Tap to sign Tap the card Internal error: wallet manager not found + You have updated biometrics, scan your card to enter Manage tokens To protect your assets, we advise you to carry out this procedure Your wallet has not been backed up @@ -371,7 +372,7 @@ You don\'t have any transactions yet Failed to load transactions Transactions - In progress... + In progress… You\'ve scanned the same card. To create a twin wallet you need to scan the card with number %d This one that you are holding in your hands and the other one with number %s.\n\nBoth cards can be used to extract funds from this wallet. One wallet. Two cards. @@ -384,6 +385,7 @@ Add new wallet Are you sure you want to delete this wallet? %d selected + An error has occurred, please scan your card to log in This wallet has already been saved, you can add another one Multi-currency Wallet name @@ -418,6 +420,7 @@ Failed to establish WalletConnect session: timeout error. Please, try again later. Session request contains unsupported blockchains for WalletConnect connection. Unsupported blockchains:\n Connection with this Dapp cannot be established due to its technical implementation. + Wrong card selected in Tangem App We\'ve encountered unknown error. Error code: %d. If the problem persists — feel free to contact our support %s network not found. Please, add it first and try again. No opened WalletConnect sessions From efad059d42cee3be2445fdb5c377b6d8242f6468 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 7 Mar 2023 13:32:47 +0300 Subject: [PATCH 09/14] Updated on 2026-08-14 --- .../tangem/tap/proxy/UserWalletManagerImpl.kt | 10 ++- .../feature/swap/domain/SwapInteractor.kt | 3 +- .../feature/swap/domain/SwapInteractorImpl.kt | 64 ++++++++++++------- .../swap/domain/cache/SwapDataCache.kt | 4 +- .../swap/domain/cache/SwapDataCacheImpl.kt | 14 ++-- .../feature/swap/viewmodels/SwapViewModel.kt | 2 +- .../tangem/lib/crypto/UserWalletManager.kt | 3 +- 7 files changed, 65 insertions(+), 35 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt b/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt index a983ea5687..72836cacca 100644 --- a/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt +++ b/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt @@ -28,7 +28,11 @@ class UserWalletManagerImpl( private val appStateHolder: AppStateHolder, ) : UserWalletManager { - override suspend fun getUserTokens(networkId: String, isExcludeCustom: Boolean): List { + override suspend fun getUserTokens( + networkId: String, + derivationPath: String?, + isExcludeCustom: Boolean, + ): List { val card = appStateHolder.getActualCard() val userTokensRepository = requireNotNull(appStateHolder.userTokensRepository) { "userTokensRepository is null" } @@ -40,7 +44,9 @@ class UserWalletManagerImpl( } else { true } - it.blockchain.toNetworkId() == networkId && checkCustom + it.blockchain.toNetworkId() == networkId && + checkCustom && + it.derivationPath == derivationPath } .map { if (it is com.tangem.tap.features.wallet.models.Currency.Token) { diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt index 461f048ca8..0c74003585 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt @@ -94,9 +94,10 @@ interface SwapInteractor { /** * Returns token in wallet balance * + * @param networkId * @param token */ - fun getTokenBalance(token: Currency): SwapAmount + fun getTokenBalance(networkId: String, token: Currency): SwapAmount fun isAvailableToSwap(networkId: String): Boolean } \ No newline at end of file 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 98350f3244..f1ef395ed1 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 @@ -56,14 +56,18 @@ internal class SwapInteractorImpl @Inject constructor( // replace tokens in wallet tokens list with loaded same val loadedOnWalletsMap = mutableSetOf() - val tokensInWallet = userWalletManager.getUserTokens(networkId = networkId, isExcludeCustom = true) - .filter { it.symbol != initialCurrency.symbol } - .map { token -> - allLoadedTokens.firstOrNull { it.symbol == token.symbol }?.let { - loadedOnWalletsMap.add(it.symbol) - it - } ?: cryptoCurrencyConverter.convertBack(token) - } + val tokensInWallet = + userWalletManager.getUserTokens( + networkId = networkId, + derivationPath = derivationPath, + isExcludeCustom = true, + ).filter { it.symbol != initialCurrency.symbol } + .map { token -> + allLoadedTokens.firstOrNull { it.symbol == token.symbol }?.let { + loadedOnWalletsMap.add(it.symbol) + it + } ?: cryptoCurrencyConverter.convertBack(token) + } val loadedTokens = allLoadedTokens .filter { !loadedOnWalletsMap.contains(it.symbol) @@ -72,7 +76,7 @@ internal class SwapInteractorImpl @Inject constructor( .mapValues { SwapAmount(it.value.value, it.value.decimals) } val appCurrency = userWalletManager.getUserAppCurrency() val rates = repository.getRates(appCurrency.code, tokensInWallet.map { it.id }) - cache.cacheBalances(tokensBalance) + cache.cacheBalances(networkId, derivationPath, tokensBalance) cache.cacheLoadedTokens(loadedTokens.map { TokenWithBalance(it) }) cache.cacheInWalletTokens(getTokensWithBalance(tokensInWallet, tokensBalance, rates, appCurrency)) return TokensDataState( @@ -147,7 +151,7 @@ internal class SwapInteractorImpl @Inject constructor( syncWalletBalanceForTokens(networkId, listOf(fromToken, toToken)) val amountDecimal = toBigDecimalOrNull(amountToSwap) if (amountDecimal == null || amountDecimal.compareTo(BigDecimal.ZERO) == 0) { - return createEmptyAmountState(fromToken, toToken) + return createEmptyAmountState(networkId, fromToken, toToken) } val amount = SwapAmount(amountDecimal, getTokenDecimals(fromToken)) val fromTokenAddress = getTokenAddress(fromToken) @@ -157,7 +161,7 @@ internal class SwapInteractorImpl @Inject constructor( allowPermissionsHandler.removeAddressFromProgress(fromTokenAddress) transactionManager.updateWalletManager(networkId, derivationPath) } - val isBalanceWithoutFeeEnough = isBalanceEnough(fromToken, amount, null) + val isBalanceWithoutFeeEnough = isBalanceEnough(networkId, fromToken, amount, null) return if (isAllowedToSpend && isBalanceWithoutFeeEnough) { loadSwapData( networkId = networkId, @@ -224,8 +228,12 @@ internal class SwapInteractorImpl @Inject constructor( } } - override fun getTokenBalance(token: Currency): SwapAmount { - return cache.getBalanceForToken(token.symbol) ?: SwapAmount(BigDecimal.ZERO, getTokenDecimals(token)) + override fun getTokenBalance(networkId: String, token: Currency): SwapAmount { + return cache.getBalanceForToken( + networkId = networkId, + derivationPath = derivationPath, + symbol = token.symbol, + ) ?: SwapAmount(BigDecimal.ZERO, getTokenDecimals(token)) } override fun isAvailableToSwap(networkId: String): Boolean { @@ -298,12 +306,13 @@ internal class SwapInteractorImpl @Inject constructor( } private fun createEmptyAmountState( + networkId: String, fromToken: Currency, toToken: Currency, ): SwapState { val appCurrency = userWalletManager.getUserAppCurrency() - val fromTokenBalance = cache.getBalanceForToken(fromToken.symbol) - val toTokenBalance = cache.getBalanceForToken(toToken.symbol) + val fromTokenBalance = cache.getBalanceForToken(networkId, derivationPath, fromToken.symbol) + val toTokenBalance = cache.getBalanceForToken(networkId, derivationPath, toToken.symbol) return SwapState.EmptyAmountState( fromTokenWalletBalance = fromTokenBalance?.let { amountFormatter.formatSwapAmountToUI(it, "") }.orEmpty(), toTokenWalletBalance = toTokenBalance?.let { amountFormatter.formatSwapAmountToUI(it, "") }.orEmpty(), @@ -411,7 +420,7 @@ internal class SwapInteractorImpl @Inject constructor( decimals = transactionManager.getNativeTokenDecimals(networkId), currency = userWalletManager.getNetworkCurrency(networkId), ) + feeFiat - val isBalanceIncludeFeeEnough = isBalanceEnough(fromToken, amount, feeData.fee.value) + val isBalanceIncludeFeeEnough = isBalanceEnough(networkId, fromToken, amount, feeData.fee.value) val isFeeEnough = checkFeeIsEnough( fee = feeData.fee.value, spendAmount = amount, @@ -458,8 +467,8 @@ internal class SwapInteractorImpl @Inject constructor( val appCurrency = userWalletManager.getUserAppCurrency() val nativeToken = userWalletManager.getNativeTokenForNetwork(networkId) val rates = repository.getRates(appCurrency.code, listOf(fromToken.id, toToken.id, nativeToken.id)) - val fromTokenBalance = cache.getBalanceForToken(fromToken.symbol) - val toTokenBalance = cache.getBalanceForToken(toToken.symbol) + val fromTokenBalance = cache.getBalanceForToken(networkId, derivationPath, fromToken.symbol) + val toTokenBalance = cache.getBalanceForToken(networkId, derivationPath, toToken.symbol) return SwapState.QuotesLoadedState( fromTokenInfo = TokenSwapInfo( tokenAmount = fromTokenAmount, @@ -503,7 +512,7 @@ internal class SwapInteractorImpl @Inject constructor( quotesLoadedState: SwapState.QuotesLoadedState, ): SwapState.QuotesLoadedState { // if token balance ZERO not show permission state to avoid user to spend money for fee - val isTokenZeroBalance = getTokenBalance(fromToken).value.compareTo(BigDecimal.ZERO) == 0 + val isTokenZeroBalance = getTokenBalance(networkId, fromToken).value.compareTo(BigDecimal.ZERO) == 0 if (isTokenZeroBalance) { return quotesLoadedState.copy( permissionState = PermissionDataState.Empty, @@ -546,7 +555,7 @@ internal class SwapInteractorImpl @Inject constructor( } private suspend fun syncWalletBalanceForTokens(networkId: String, tokens: List) { - val tokensToSync = tokens.filter { cache.getBalanceForToken(it.symbol) == null } + val tokensToSync = tokens.filter { cache.getBalanceForToken(networkId, derivationPath, it.symbol) == null } if (tokensToSync.isNotEmpty()) { val tokensBalance = userWalletManager.getCurrentWalletTokensBalance( @@ -554,12 +563,21 @@ internal class SwapInteractorImpl @Inject constructor( extraTokens = tokensToSync.map { cryptoCurrencyConverter.convert(it) }, derivationPath = derivationPath, ) - cache.cacheBalances(tokensBalance.mapValues { SwapAmount(it.value.value, it.value.decimals) }) + cache.cacheBalances( + networkId = networkId, + derivationPath = derivationPath, + balances = tokensBalance.mapValues { SwapAmount(it.value.value, it.value.decimals) }, + ) } } - private fun isBalanceEnough(fromToken: Currency, amount: SwapAmount, fee: BigDecimal?): Boolean { - val tokenBalance = getTokenBalance(fromToken).value + private fun isBalanceEnough( + networkId: String, + fromToken: Currency, + amount: SwapAmount, + fee: BigDecimal?, + ): Boolean { + val tokenBalance = getTokenBalance(networkId, fromToken).value return if (fromToken is Currency.NonNativeToken) { tokenBalance >= amount.value } else { diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/cache/SwapDataCache.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/cache/SwapDataCache.kt index 36a0ef8ef3..7f4bf919cc 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/cache/SwapDataCache.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/cache/SwapDataCache.kt @@ -10,11 +10,11 @@ interface SwapDataCache { fun cacheAvailableToSwapTokens(networkId: String, tokens: List) fun cacheInWalletTokens(tokens: List) fun cacheLoadedTokens(tokens: List) - fun cacheBalances(balances: Map) + fun cacheBalances(networkId: String, derivationPath: String?, balances: Map) fun cacheLastFeeForNetwork(fee: BigDecimal, networkId: String) fun getAvailableTokens(networkId: String): List fun getInWalletTokens(): List fun getLoadedTokens(): List - fun getBalanceForToken(symbol: String): SwapAmount? + fun getBalanceForToken(networkId: String, derivationPath: String?, symbol: String): SwapAmount? fun getLastFeeForNetwork(networkId: String): BigDecimal? } \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/cache/SwapDataCacheImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/cache/SwapDataCacheImpl.kt index f044693ece..4379d70d9f 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/cache/SwapDataCacheImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/cache/SwapDataCacheImpl.kt @@ -9,7 +9,7 @@ class SwapDataCacheImpl : SwapDataCache { private val availableTokensForNetwork: MutableMap> = mutableMapOf() private val feesForNetworks: MutableMap = mutableMapOf() - private val tokensBalances: MutableMap = mutableMapOf() + private val tokensBalances: MutableMap> = mutableMapOf() private val lastInWalletTokens = mutableListOf() private val lastLoadedTokens = mutableListOf() @@ -35,12 +35,12 @@ class SwapDataCacheImpl : SwapDataCache { return lastLoadedTokens } - override fun getBalanceForToken(symbol: String): SwapAmount? { - return tokensBalances[symbol] + override fun getBalanceForToken(networkId: String, derivationPath: String?, symbol: String): SwapAmount? { + return tokensBalances[createKeyFrom(networkId, derivationPath)]?.get(symbol) } - override fun cacheBalances(balances: Map) { - tokensBalances.putAll(balances) + override fun cacheBalances(networkId: String, derivationPath: String?, balances: Map) { + tokensBalances[createKeyFrom(networkId, derivationPath)] = balances } override fun cacheAvailableToSwapTokens(networkId: String, tokens: List) { @@ -54,4 +54,8 @@ class SwapDataCacheImpl : SwapDataCache { override fun getAvailableTokens(networkId: String): List { return availableTokensForNetwork.getOrElse(networkId) { emptyList() } } + + private fun createKeyFrom(networkId: String, derivationPath: String?): String { + return "$networkId;$derivationPath" + } } \ 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 b18ceab795..e441f3bf63 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 @@ -371,7 +371,7 @@ internal class SwapViewModel @Inject constructor( private fun onMaxAmountClicked() { dataState.fromCurrency?.let { - val balance = swapInteractor.getTokenBalance(it) + val balance = swapInteractor.getTokenBalance(currency.networkId, it) onAmountChanged(balance.formatToUIRepresentation()) } } diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/UserWalletManager.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/UserWalletManager.kt index e9c0d1beea..23659c303f 100644 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/UserWalletManager.kt +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/UserWalletManager.kt @@ -13,7 +13,7 @@ interface UserWalletManager { * Returns all user tokens (merged from local and backend) */ @Throws(IllegalStateException::class) - suspend fun getUserTokens(networkId: String, isExcludeCustom: Boolean): List + suspend fun getUserTokens(networkId: String, derivationPath: String?, isExcludeCustom: Boolean): List @Throws(IllegalStateException::class) fun getNativeTokenForNetwork(networkId: String): Currency @@ -55,6 +55,7 @@ interface UserWalletManager { * Return balances from wallet found by networkId * * @param networkId + * @param extraTokens tokens you want to check balance that not exists in wallet * @param derivationPath if null uses default * @return map of */ From 360dfb21f3fbe68c2dbc87b1ce72cacfcec0ad82 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 7 Mar 2023 19:50:25 +0300 Subject: [PATCH 10/14] Updated on 2026-08-14 --- .../java/com/tangem/tap/features/wallet/redux/WalletData.kt | 2 ++ 1 file changed, 2 insertions(+) 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 3dcbf37cb7..f50513a1ae 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 @@ -1,5 +1,6 @@ package com.tangem.tap.features.wallet.redux +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 @@ -37,6 +38,7 @@ data class WalletData( fun isAvailableToSwap(swapInteractor: SwapInteractor, isExchangeFeatureOn: Boolean): Boolean { return swapInteractor.isAvailableToSwap(currency.blockchain.toNetworkId()) && + currency.blockchain.id != Blockchain.Optimism.id && // disable optimism blockchain for now isExchangeFeatureOn && !currency.isCustomCurrency(null) } From dc383831d3f967d2932d4dec90149c5d45df1e58 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 7 Mar 2023 20:11:03 +0300 Subject: [PATCH 11/14] Updated on 2026-08-14 --- fastlane/Fastfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 93ecf5d376..d93ec16fc3 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -18,7 +18,7 @@ default_platform(:android) platform :android do before_all do |lane, options| - FileUtils.mv("../app/src/main/assets/tangem-app-config/android/google-services.json", "../app") + FileUtils.cp("../app/src/main/assets/tangem-app-config/android/google-services.json", "../app") end desc "Runs all the tests" From 6b95243f9a660a237f0a68a4d1401f732ee5d0d5 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 8 Mar 2023 19:16:58 +0300 Subject: [PATCH 12/14] Updated on 2026-08-14 --- .../com/tangem/tap/proxy/DerivationManagerImpl.kt | 13 ++++++++++--- .../com/tangem/tap/proxy/UserWalletManagerImpl.kt | 11 ++++++++--- .../referral/domain/ReferralInteractorImpl.kt | 15 +++++++++------ .../com/tangem/lib/crypto/DerivationManager.kt | 11 +++++++++-- 4 files changed, 36 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/proxy/DerivationManagerImpl.kt b/app/src/main/java/com/tangem/tap/proxy/DerivationManagerImpl.kt index ae2cdcfd1a..4fc1c8bfb4 100644 --- a/app/src/main/java/com/tangem/tap/proxy/DerivationManagerImpl.kt +++ b/app/src/main/java/com/tangem/tap/proxy/DerivationManagerImpl.kt @@ -59,12 +59,19 @@ class DerivationManagerImpl( } } - override fun hasDerivation(networkId: String): Boolean { + override fun getDerivationPathForBlockchain(networkId: String): String? { + val scanResponse = appStateHolder.scanResponse + val blockchain = Blockchain.fromNetworkId(networkId) + if (scanResponse != null && blockchain != null) { + return blockchain.derivationPath(appStateHolder.getActualCard()?.derivationStyle)?.rawPath + } + return null + } + + override fun hasDerivation(networkId: String, derivationPath: String): Boolean { val scanResponse = appStateHolder.scanResponse val blockchain = Blockchain.fromNetworkId(networkId) if (scanResponse != null && blockchain != null) { - val derivationPath = blockchain.derivationPath(appStateHolder.getActualCard()?.derivationStyle)?.rawPath - if (derivationPath.isNullOrEmpty()) return false return scanResponse.hasDerivation( blockchain, derivationPath, diff --git a/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt b/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt index 72836cacca..a6b1dc5348 100644 --- a/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt +++ b/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt @@ -93,9 +93,13 @@ class UserWalletManagerImpl( override suspend fun isTokenAdded(currency: Currency, derivationPath: String?): Boolean { val blockchain = requireNotNull(Blockchain.fromNetworkId(currency.networkId)) { "blockchain not found" } - val walletManager = getActualWalletManager(blockchain, derivationPath) - return walletManager.cardTokens.any { - it.id == currency.id + return try { + val walletManager = getActualWalletManager(blockchain, derivationPath) + walletManager.cardTokens.any { + it.id == currency.id + } + } catch (e: IllegalArgumentException) { + false } } @@ -191,6 +195,7 @@ class UserWalletManagerImpl( appStateHolder.mainStore?.dispatchOnMain(WalletAction.LoadData.Refresh) } + @kotlin.jvm.Throws(IllegalArgumentException::class) private fun getActualWalletManager(blockchain: Blockchain, derivationPath: String?): WalletManager { val blockchainNetwork = BlockchainNetwork(blockchain, derivationPath, emptyList()) return requireNotNull(appStateHolder.walletState?.getWalletManager(blockchainNetwork)) { diff --git a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt index 22e20981e5..3ed378f876 100644 --- a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt +++ b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt @@ -25,8 +25,8 @@ internal class ReferralInteractorImpl( override suspend fun startReferral(): ReferralData { if (tokensForReferral.isNotEmpty()) { val currency = tokensConverter.convert(tokensForReferral.first()) - deriveOrAddTokens(currency) - val publicAddress = userWalletManager.getWalletAddress(currency.networkId, null) + val derivationPath = deriveOrAddTokens(currency) + val publicAddress = userWalletManager.getWalletAddress(currency.networkId, derivationPath) return repository.startReferral( walletId = userWalletManager.getWalletId(), networkId = currency.networkId, @@ -38,13 +38,16 @@ internal class ReferralInteractorImpl( } } - private suspend fun deriveOrAddTokens(currency: Currency) { - if (!derivationManager.hasDerivation(currency.networkId)) { + private suspend fun deriveOrAddTokens(currency: Currency): String { + val derivationPath = derivationManager.getDerivationPathForBlockchain(currency.networkId) + if (derivationPath.isNullOrEmpty()) error("derivationPath shouldn't be empty") + if (!derivationManager.hasDerivation(currency.networkId, derivationPath)) { derivationManager.deriveMissingBlockchains(currency) } - if (!userWalletManager.isTokenAdded(currency, null)) { - userWalletManager.addToken(currency, null) + if (!userWalletManager.isTokenAdded(currency, derivationPath)) { + userWalletManager.addToken(currency, derivationPath) } + return derivationPath } private fun saveRefTokens(tokens: List) { diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/DerivationManager.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/DerivationManager.kt index b9b2cfecea..7dd3d51970 100644 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/DerivationManager.kt +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/DerivationManager.kt @@ -12,7 +12,14 @@ interface DerivationManager { suspend fun deriveMissingBlockchains(currency: Currency): Boolean /** - * Checks that given [networkId] has derivations + * Returns derivationPath or null for blockchain with given networkId + * + * @param networkId */ - fun hasDerivation(networkId: String): Boolean + fun getDerivationPathForBlockchain(networkId: String): String? + + /** + * Checks that given [networkId] has derivations for [derivationPath] + */ + fun hasDerivation(networkId: String, derivationPath: String): Boolean } \ No newline at end of file From 9bb0c2ec48323ec317a5f43b7ec88f61781b0439 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 9 Mar 2023 12:35:17 +0300 Subject: [PATCH 13/14] Updated on 2026-08-14 --- .../com/tangem/tap/proxy/TransactionManagerImpl.kt | 9 ++++++++- .../feature/swap/domain/SwapInteractorImpl.kt | 3 +++ .../com/tangem/lib/crypto/TransactionManager.kt | 13 +++++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt b/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt index 14349d169f..6ec7b292bb 100644 --- a/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt +++ b/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt @@ -148,6 +148,7 @@ class TransactionManagerImpl( amountToSend: BigDecimal, currencyToSend: Currency, destinationAddress: String, + increaseBy: Int?, data: String?, derivationPath: String?, ): ProxyFee { @@ -161,7 +162,13 @@ class TransactionManagerImpl( currency = currencyToSend, 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( 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 f1ef395ed1..2bb5569c19 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 @@ -411,6 +411,7 @@ internal class SwapInteractorImpl @Inject constructor( amountToSend = amount.value, currencyToSend = cryptoCurrencyConverter.convert(fromToken), destinationAddress = swapData.transaction.toWalletAddress, + increaseBy = INCREASE_GAS_LIMIT_BY, data = swapData.transaction.data, derivationPath = derivationPath, ) @@ -529,6 +530,7 @@ internal class SwapInteractorImpl @Inject constructor( amountToSend = BigDecimal.ZERO, currencyToSend = userWalletManager.getNativeTokenForNetwork(networkId), destinationAddress = transactionData.toAddress, + increaseBy = INCREASE_GAS_LIMIT_BY, data = transactionData.data, derivationPath = derivationPath, ) @@ -645,6 +647,7 @@ internal class SwapInteractorImpl @Inject constructor( private const val ZERO_BALANCE = "0" private const val DEFAULT_BLOCKCHAIN_INCH_ADDRESS = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE" private const val INCREASE_FEE_TO_CHECK_ENOUGH_PERCENT = 1.4 + private const val INCREASE_GAS_LIMIT_BY = 125 private const val USDT_SYMBOL = "USDT" private const val USDC_SYMBOL = "USDC" private const val INFINITY_SYMBOL = "∞" diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/TransactionManager.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/TransactionManager.kt index 02c08a1d6b..97deab1626 100644 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/TransactionManager.kt +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/TransactionManager.kt @@ -33,6 +33,18 @@ interface TransactionManager { derivationPath: String?, ): SendTxResult + /** + * Get fee + * + * @param networkId network id of blockchain + * @param amountToSend amount + * @param currencyToSend currency to send in tx + * @param destinationAddress address to send tx + * @param increaseBy percents in format 125 = 25% + * @param data data for tx + * @param derivationPath derivation path + * @return + */ @Suppress("LongParameterList") @Throws(IllegalStateException::class) suspend fun getFee( @@ -40,6 +52,7 @@ interface TransactionManager { amountToSend: BigDecimal, currencyToSend: Currency, destinationAddress: String, + increaseBy: Int?, data: String?, derivationPath: String?, ): ProxyFee From bfd219d2f799bdfa02c1ccd220c7a42bae2f1daa Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 9 Mar 2023 15:10:06 +0300 Subject: [PATCH 14/14] Updated on 2026-08-14 --- .../repository/implementation/utils/WalletDataOperations.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/utils/WalletDataOperations.kt b/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/utils/WalletDataOperations.kt index 48b0c4d71e..4346396f56 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/utils/WalletDataOperations.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/utils/WalletDataOperations.kt @@ -102,6 +102,7 @@ internal fun WalletDataModel.updateWithAmount(wallet: Wallet): WalletDataModel { internal fun WalletDataModel.updateWithDemoAmount(wallet: Wallet): WalletDataModel { val amount = DemoHelper.config.getBalance(wallet.blockchain) + wallet.setAmount(amount) return this.copy( status = WalletDataModel.VerifiedOnline(amount = amount.value ?: BigDecimal.ZERO), )