diff --git a/app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt b/app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt index 426f49ca25..9ad0fc6fa9 100644 --- a/app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt +++ b/app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt @@ -6,6 +6,7 @@ import androidx.lifecycle.lifecycleScope import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.redux.navigation.AppScreen import com.tangem.tap.common.redux.navigation.NavigationAction +import com.tangem.tap.domain.userWalletList.asLockable import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.delay @@ -95,8 +96,9 @@ internal class LockUserWalletsTimer( val startTime = System.currentTimeMillis() delay(duration) if (isActive) { - val userWalletsListManager = userWalletsListManagerSafe ?: return@launch - if (userWalletsListManager.hasSavedUserWallets) { + val userWalletsListManager = userWalletsListManagerSafe?.asLockable() + ?: return@launch + if (userWalletsListManager.hasUserWallets) { val currentTime = System.currentTimeMillis() Timber.d( """ diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index 6b5d0bd175..14d367046b 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -19,6 +19,7 @@ import com.tangem.tap.common.OnActivityResultCallback import com.tangem.tap.common.SnackbarHandler import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.redux.NotificationsHandler +import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.common.redux.navigation.AppScreen import com.tangem.tap.common.redux.navigation.NavigationAction import com.tangem.tap.common.shop.googlepay.GooglePayService @@ -27,6 +28,7 @@ import com.tangem.tap.common.shop.googlepay.GooglePayUtil.createPaymentsClient import com.tangem.tap.domain.TangemSdkManager import com.tangem.tap.domain.userWalletList.UserWalletsListManager import com.tangem.tap.domain.userWalletList.di.provideBiometricImplementation +import com.tangem.tap.domain.userWalletList.di.provideRuntimeImplementation import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction import com.tangem.tap.features.shop.redux.ShopAction import com.tangem.tap.features.welcome.redux.WelcomeAction @@ -45,11 +47,8 @@ import kotlin.coroutines.CoroutineContext lateinit var tangemSdk: TangemSdk lateinit var tangemSdkManager: TangemSdkManager lateinit var backupService: BackupService -lateinit var userWalletsListManager: UserWalletsListManager internal var lockUserWalletsTimer: LockUserWalletsTimer? = null private set -var userWalletsListManagerSafe: UserWalletsListManager? = null - private set var notificationsHandler: NotificationsHandler? = null private val coroutineContext: CoroutineContext @@ -60,6 +59,12 @@ private val mainCoroutineContext: CoroutineContext get() = Job() + Dispatchers.Main + FeatureCoroutineExceptionHandler.create("mainScope") val mainScope = CoroutineScope(mainCoroutineContext) +// TODO: Move to DI +val userWalletsListManagerSafe: UserWalletsListManager? + get() = store.state.globalState.userWalletsListManager +val userWalletsListManager: UserWalletsListManager + get() = userWalletsListManagerSafe!! + @AndroidEntryPoint class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbackHolder { @@ -83,14 +88,10 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac appStateHolder.tangemSdkManager = tangemSdkManager appStateHolder.tangemSdk = tangemSdk backupService = BackupService.init(tangemSdk, this) - userWalletsListManager = UserWalletsListManager.provideBiometricImplementation( - context = applicationContext, - tangemSdkManager = tangemSdkManager, - ) - appStateHolder.userWalletsListManager = userWalletsListManager - userWalletsListManagerSafe = userWalletsListManager lockUserWalletsTimer = LockUserWalletsTimer(owner = this) + initUserWalletsListManager() + store.dispatch( ShopAction.CheckIfGooglePayAvailable( GooglePayService(createPaymentsClient(this), this), @@ -98,6 +99,18 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac ) } + private fun initUserWalletsListManager() { + val manager = if (preferencesStorage.shouldSaveUserWallets) { + UserWalletsListManager.provideBiometricImplementation( + context = applicationContext, + tangemSdkManager = tangemSdkManager, + ) + } else { + UserWalletsListManager.provideRuntimeImplementation() + } + store.dispatch(GlobalAction.UpdateUserWalletsListManager(manager)) + } + private fun systemActions() { WindowCompat.setDecorFitsSystemWindows(window, false) @@ -122,7 +135,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac override fun onNewIntent(intent: Intent?) { super.onNewIntent(intent) - intentHandler.handleIntent(intent, userWalletsListManager.hasSavedUserWallets) + intentHandler.handleIntent(intent, userWalletsListManager.hasUserWallets) } override fun onStart() { @@ -200,11 +213,12 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac backStackIsEmpty -> { navigateToInitialScreen(intent) } + else -> Unit } } private fun navigateToInitialScreen(intent: Intent?) { - if (userWalletsListManager.hasSavedUserWallets) { + if (store.state.globalState.userWalletsListManager?.hasUserWallets == true) { store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Welcome)) store.dispatchOnMain(WelcomeAction.HandleIntentIfNeeded(intent)) } else { 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..4d50c59679 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,17 @@ 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/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/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/common/extensions/Store.kt b/app/src/main/java/com/tangem/tap/common/extensions/Store.kt index 69c5f1c085..bfd3fa99c2 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Store.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Store.kt @@ -1,7 +1,7 @@ package com.tangem.tap.common.extensions -import com.tangem.domain.common.ScanResponse import com.tangem.domain.common.extensions.withMainContext +import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.StateDialog import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.common.redux.navigation.NavigationAction @@ -16,10 +16,25 @@ import org.rekotlin.Store /** * Dispatch action with creating the new coroutine with the Main dispatcher + * + * @see dispatchWithMain */ fun Store<*>.dispatchOnMain(action: Action) { scope.launch(Dispatchers.Main) { - store.dispatch(action) + dispatch(action) + } +} + +/** + * Dispatch action on the Main coroutine context + * + * @param action [Action] to be dispatched + * + * @see dispatchOnMain + * */ +suspend fun Store<*>.dispatchWithMain(action: Action) { + withMainContext { + dispatch(action) } } @@ -27,9 +42,8 @@ fun Store<*>.dispatchNotification(resId: Int) { dispatchOnMain(GlobalAction.ShowNotification(resId)) } -@Suppress("UnusedReceiverParameter") -suspend fun Store<*>.onUserWalletSelected(userWallet: UserWallet, refresh: Boolean = false) { - store.state.globalState.tapWalletManager.onWalletSelected(userWallet, refresh) +suspend fun Store.onUserWalletSelected(userWallet: UserWallet, refresh: Boolean = false) { + state.globalState.tapWalletManager.onWalletSelected(userWallet, refresh) } fun Store<*>.dispatchToastNotification(resId: Int) { @@ -63,20 +77,18 @@ fun Store<*>.dispatchDialogHide() { /** * Dispatch action inside a coroutine with the Main dispatcher */ +@Deprecated( + message = "Use dispatchWithMain instead", + replaceWith = ReplaceWith(expression = "dispatchWithMain"), +) suspend fun dispatchOnMain(vararg actions: Action) { withMainContext { actions.forEach { store.dispatch(it) } } } -/** - * Dispatch action - */ -suspend fun Store<*>.onCardScanned(scanResponse: ScanResponse) { - store.state.globalState.tapWalletManager.onCardScanned(scanResponse) +fun Store<*>.dispatchOpenUrl(url: String) { + dispatch(NavigationAction.OpenUrl(url)) } -fun Store<*>.dispatchOpenUrl(url: String) { - store.dispatch(NavigationAction.OpenUrl(url)) -} fun Store<*>.dispatchShare(url: String) { - store.dispatch(NavigationAction.Share(url)) + dispatch(NavigationAction.Share(url)) } \ No newline at end of file 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/common/redux/global/GlobalAction.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt index c4c8a33527..f0f6e8244c 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt @@ -18,6 +18,7 @@ import com.tangem.tap.common.redux.ToastNotificationAction import com.tangem.tap.domain.TapError import com.tangem.tap.domain.configurable.warningMessage.WarningMessage import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager +import com.tangem.tap.domain.userWalletList.UserWalletsListManager import com.tangem.tap.features.details.redux.SecurityOption import org.rekotlin.Action @@ -105,4 +106,6 @@ sealed class GlobalAction : Action { object FetchUserCountry : GlobalAction() { data class Success(val countryCode: String) : GlobalAction() } + + data class UpdateUserWalletsListManager(val manager: UserWalletsListManager) : GlobalAction() } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt index 7934a195f6..3ed3751c74 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt @@ -84,6 +84,12 @@ fun globalReducer(action: Action, state: AppState, appStateHolder: AppStateHolde is GlobalAction.FetchUserCountry.Success -> globalState.copy( userCountryCode = action.countryCode, ) + is GlobalAction.UpdateUserWalletsListManager -> { + appStateHolder.userWalletsListManager = action.manager + globalState.copy( + userWalletsListManager = action.manager, + ) + } else -> globalState } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt index 845715809d..56059c6799 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt @@ -1,13 +1,14 @@ package com.tangem.tap.common.redux.global +import com.tangem.datasource.config.ConfigManager import com.tangem.domain.common.ScanResponse import com.tangem.tap.common.entities.FiatCurrency import com.tangem.tap.common.feedback.FeedbackManager import com.tangem.tap.common.redux.StateDialog import com.tangem.tap.domain.PayIdManager import com.tangem.tap.domain.TapWalletManager -import com.tangem.datasource.config.ConfigManager import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager +import com.tangem.tap.domain.userWalletList.UserWalletsListManager import com.tangem.tap.features.onboarding.OnboardingManager import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager import org.rekotlin.StateType @@ -26,6 +27,7 @@ data class GlobalState( val dialog: StateDialog? = null, val exchangeManager: CurrencyExchangeManager = CurrencyExchangeManager.dummy(), val userCountryCode: String? = null, + val userWalletsListManager: UserWalletsListManager? = null, ) : StateType typealias CryptoCurrencyName = String 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 24c1384e66..15a3835869 100644 --- a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt @@ -1,106 +1,42 @@ package com.tangem.tap.domain -import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.BlockchainSdkConfig import com.tangem.blockchain.common.Token import com.tangem.blockchain.common.Wallet -import com.tangem.blockchain.common.WalletManager import com.tangem.blockchain.common.WalletManagerFactory import com.tangem.common.doOnFailure import com.tangem.common.doOnSuccess -import com.tangem.common.services.Result import com.tangem.core.analytics.Analytics -import com.tangem.domain.common.CardDTO +import com.tangem.datasource.config.ConfigManager import com.tangem.domain.common.ScanResponse -import com.tangem.domain.common.TapWorkarounds.isTestCard -import com.tangem.domain.common.ThrottlerWithValues import com.tangem.domain.common.extensions.withMainContext import com.tangem.operations.attestation.Attestation import com.tangem.tap.common.extensions.dispatchOnMain -import com.tangem.tap.common.extensions.safeUpdate import com.tangem.tap.common.extensions.setContext import com.tangem.tap.common.redux.global.GlobalAction -import com.tangem.datasource.config.ConfigManager -import com.tangem.tap.domain.extensions.makePrimaryWalletManager -import com.tangem.tap.domain.extensions.makeWalletManagersForApp import com.tangem.tap.domain.model.UserWallet -import com.tangem.tap.domain.tokens.models.BlockchainNetwork import com.tangem.tap.domain.walletStores.WalletStoresError -import com.tangem.tap.features.demo.isDemoCard import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction +import com.tangem.tap.features.disclaimer.createDisclaimer +import com.tangem.tap.features.disclaimer.redux.DisclaimerAction import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction -import com.tangem.tap.features.wallet.models.toBlockchainNetworks import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.features.wallet.redux.middlewares.handleBasicAnalyticsEvent -import com.tangem.tap.network.NetworkConnectivity +import com.tangem.tap.preferencesStorage import com.tangem.tap.store import com.tangem.tap.tangemSdkManager -import com.tangem.tap.userTokensRepository import com.tangem.tap.walletStoresManager -import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import org.rekotlin.Action import timber.log.Timber class TapWalletManager { val walletManagerFactory: WalletManagerFactory by lazy { WalletManagerFactory(blockchainSdkConfig) } - // TODO("After adding DI") get dependencies by DI - val rates: RatesRepository by lazy { - RatesRepository( - tangemTechApi = store.state.domainNetworks.tangemTechService.api, - dispatchers = AppCoroutineDispatcherProvider(), - ) - } - private val blockchainSdkConfig by lazy { store.state.globalState.configManager?.config?.blockchainSdkConfig ?: BlockchainSdkConfig() } - private val walletManagersThrottler = - ThrottlerWithValues>(10000) - - suspend fun loadWalletData(walletManager: WalletManager) { - val blockchainNetwork = BlockchainNetwork.fromWalletManager(walletManager) - val result = if (walletManagersThrottler.isStillThrottled(blockchainNetwork)) { - walletManagersThrottler.geValue(blockchainNetwork)!! - } else { - val safeUpdateResult = walletManager.safeUpdate() - walletManagersThrottler.updateThrottlingTo(blockchainNetwork) - walletManagersThrottler.setValue(blockchainNetwork, safeUpdateResult) - safeUpdateResult - } - when (result) { - is Result.Success -> { - dispatchOnMain(WalletAction.LoadWallet.Success(result.data, blockchainNetwork)) - } - is Result.Failure -> { - when (result.error) { - is TapError.WalletManager.NoAccountError -> { - dispatchOnMain( - WalletAction.LoadWallet.NoAccount( - walletManager.wallet, - blockchainNetwork, - (result.error as TapError.WalletManager.NoAccountError).customMessage, - ), - ) - } - else -> { - dispatchOnMain( - WalletAction.LoadWallet.Failure( - walletManager.wallet, - result.error.localizedMessage, - ), - ) - } - } - } - } - } - suspend fun onWalletSelected(userWallet: UserWallet, refresh: Boolean) { Analytics.setContext(userWallet.scanResponse) val scanResponse = userWallet.scanResponse @@ -110,9 +46,11 @@ class TapWalletManager { tangemSdkManager.changeDisplayedCardIdNumbersCount(scanResponse) store.state.globalState.feedbackManager?.infoHolder?.setCardInfo(scanResponse) updateConfigManager(scanResponse) - withMainContext { + // Order is important + store.dispatch(DisclaimerAction.SetDisclaimer(card.createDisclaimer())) store.dispatch(WalletAction.UserWalletChanged(userWallet)) + store.dispatch(WalletAction.UpdateCanSaveUserWallets(preferencesStorage.shouldSaveUserWallets)) store.dispatch(TwinCardsAction.IfTwinsPrepareState(scanResponse)) store.dispatch(WalletConnectAction.ResetState) store.dispatch(GlobalAction.SaveScanResponse(scanResponse)) @@ -134,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, ) @@ -156,32 +97,6 @@ class TapWalletManager { } } - suspend fun onCardScanned(data: ScanResponse) { - walletManagersThrottler.clear() - store.state.globalState.feedbackManager?.infoHolder?.setCardInfo(data) - updateConfigManager(data) - - withMainContext { - store.dispatch(WalletAction.ResetState(data.card)) - store.dispatch(WalletConnectAction.ResetState) - store.dispatch(GlobalAction.SaveScanResponse(data)) - store.dispatch(WalletAction.SetIfTestnetCard(data.card.isTestCard)) - store.dispatch( - WalletAction.MultiWallet.SetIsMultiwalletAllowed( - data.cardTypesResolver.isMultiwalletAllowed(), - ), - ) - store.dispatch(WalletConnectAction.RestoreSessions(data)) - store.dispatch( - WalletAction.MultiWallet.ShowWalletBackupWarning( - show = data.card.settings.isBackupAllowed && - data.card.backupStatus == CardDTO.BackupStatus.NoBackup, - ), - ) - loadData(data) - } - } - fun updateConfigManager(data: ScanResponse) { val configManager = store.state.globalState.configManager val blockchain = data.cardTypesResolver.getBlockchain() @@ -198,126 +113,6 @@ class TapWalletManager { configManager?.resetToDefault(ConfigManager.IS_TOP_UP_ENABLED) } } - - suspend fun loadData(data: ScanResponse) { - dispatchOnMain(WalletAction.LoadCardInfo(data.card)) - getActionIfUnknownBlockchainOrEmptyWallet(data)?.let { - dispatchOnMain(it) - return - } - - if (data.cardTypesResolver.isMultiwalletAllowed()) { - dispatchOnMain(WalletAction.MultiWallet.ScheduleCheckForMissingDerivation) - loadMultiWalletData(data) - } else { - loadSingleWalletData(data) - } - - dispatchOnMain(WalletAction.LoadWallet()) - } - - private suspend fun loadMultiWalletData(scanResponse: ScanResponse) { - loadUserCurrencies(scanResponse, walletManagerFactory) - } - - private fun checkIfDerivationsAreMissing(blockchainNetworks: List, scanResponse: ScanResponse) { - blockchainNetworks.map { - if (it.tokens.isNotEmpty()) { - WalletAction.MultiWallet.AddTokens(it.tokens, it) - } - } - val missingDerivations = blockchainNetworks - .filter { - it.derivationPath != null && !scanResponse.hasDerivation(it.blockchain, it.derivationPath) - } - store.dispatch(WalletAction.MultiWallet.AddMissingDerivations(missingDerivations)) - } - - private suspend fun loadSingleWalletData(data: ScanResponse) { - val blockchain = data.cardTypesResolver.getBlockchain() - val primaryWalletManager = walletManagerFactory.makePrimaryWalletManager(data) - - if (blockchain != Blockchain.Unknown && primaryWalletManager != null) { - val blockchainNetwork = BlockchainNetwork.fromWalletManager(primaryWalletManager) - val actionsList = listOfNotNull( - WalletAction.MultiWallet.AddBlockchains( - blockchains = listOf(BlockchainNetwork.fromWalletManager(primaryWalletManager)), - walletManagers = listOf(primaryWalletManager), - ), - data.cardTypesResolver.getPrimaryToken()?.let { - primaryWalletManager.addToken(it) - primaryWalletManager.wallet.setAmount(Amount(it)) - WalletAction.MultiWallet.AddToken(it, blockchainNetwork, false) - }, - WalletAction.LoadFiatRate(), - ) - dispatchOnMain(*actionsList.toTypedArray()) - } - } - - private suspend fun loadUserCurrencies(scanResponse: ScanResponse, walletManagerFactory: WalletManagerFactory) { - val userTokens = userTokensRepository.getUserTokens(scanResponse.card) - withMainContext { - val blockchainNetworks = userTokens.toBlockchainNetworks() - val walletManagers = walletManagerFactory.makeWalletManagersForApp(scanResponse, userTokens) - store.dispatch( - WalletAction.MultiWallet.AddBlockchains( - blockchains = blockchainNetworks, - walletManagers = walletManagers, - ), - ) - - blockchainNetworks.filter { it.tokens.isNotEmpty() } - .map { - store.dispatch( - WalletAction.MultiWallet.AddTokens( - tokens = it.tokens, - blockchain = it, - ), - ) - } - checkIfDerivationsAreMissing(blockchainNetworks, scanResponse) - store.dispatch(WalletAction.LoadFiatRate(coinsList = userTokens)) - } - } - - suspend fun reloadData(data: ScanResponse) { - if (data.cardTypesResolver.isMultiwalletAllowed()) { - loadUserCurrencies(data, walletManagerFactory) - } - withContext(Dispatchers.Main) { - getActionIfUnknownBlockchainOrEmptyWallet(data)?.let { - store.dispatch(it) - return@withContext - } - if (!NetworkConnectivity.getInstance().isOnlineOrConnecting()) { - store.dispatch(WalletAction.LoadData.Failure(TapError.NoInternetConnection)) - return@withContext - } - - store.dispatch(WalletAction.LoadWallet()) - } - } - - private fun getActionIfUnknownBlockchainOrEmptyWallet(data: ScanResponse): WalletAction? { - return when { - // check order is important - data.cardTypesResolver.isTangemTwins() && !data.twinsIsTwinned() -> { - WalletAction.EmptyWallet - } - data.cardTypesResolver.getBlockchain() == Blockchain.Unknown && - !data.cardTypesResolver.isMultiwalletAllowed() -> { - WalletAction.LoadData.Failure(TapError.UnknownBlockchain) - } - data.isDemoCard() -> { - return null - } - data.card.wallets.isEmpty() -> { - WalletAction.EmptyWallet - } - else -> null - } - } } fun Wallet.getFirstToken(): Token? { diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/ScanCardProcessor.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/ScanCardProcessor.kt index 56e9beaf22..facfc3d62e 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/ScanCardProcessor.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/ScanCardProcessor.kt @@ -20,7 +20,6 @@ import com.tangem.tap.common.extensions.setContext import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.common.redux.navigation.AppScreen import com.tangem.tap.common.redux.navigation.NavigationAction -import com.tangem.tap.features.disclaimer.DisclaimerType import com.tangem.tap.features.disclaimer.createDisclaimer import com.tangem.tap.features.disclaimer.redux.DisclaimerAction import com.tangem.tap.features.disclaimer.redux.DisclaimerCallback @@ -150,7 +149,7 @@ object ScanCardProcessor { crossinline nextHandler: suspend (ScanResponse) -> Unit, crossinline onFailure: suspend (error: TangemError) -> Unit, ) { - val disclaimer = DisclaimerType.get(scanResponse.card).createDisclaimer(scanResponse.card) + val disclaimer = scanResponse.card.createDisclaimer() store.dispatchOnMain(DisclaimerAction.SetDisclaimer(disclaimer)) if (disclaimer.isAccepted()) { diff --git a/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensRepository.kt b/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensRepository.kt index 7d01846702..03b431d3e2 100644 --- a/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensRepository.kt @@ -19,6 +19,7 @@ import com.tangem.tap.network.NetworkConnectivity import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext +import timber.log.Timber class UserTokensRepository( private val storageService: UserTokensStorageService, @@ -37,26 +38,14 @@ class UserTokensRepository( return@withContext loadTokensOffline(card, userId) } - runCatching { tangemTechApi.getUserTokens(userId) } - .onSuccess { response -> - return@withContext response.tokens - .mapNotNull(Currency.Companion::fromTokenResponse).also { - storageService.saveUserTokens(userId, it.toUserTokensResponse()) - } - .distinct() - } - .onFailure { - return@withContext handleGetUserTokensFailure(card = card, userId = userId, error = it) - } - - error("Unreachable code because runCatching must return result") + return@withContext remoteGetUserTokens(card, userId) } // TODO("After adding DI") replace with CoroutineDispatcherProvider suspend fun saveUserTokens(card: CardDTO, tokens: List) = withContext(dispatchers.io) { val userId = getUserWalletId(card) ?: return@withContext val userTokens = tokens.toUserTokensResponse() - tangemTechApi.saveUserTokens(userId, userTokens) + remoteSaveUserTokens(userId, userTokens) storageService.saveUserTokens(userId, userTokens) } @@ -97,7 +86,7 @@ class UserTokensRepository( return when { error is TangemSdkError.NetworkError && error.customMessage.contains(NOT_FOUND_HTTP_CODE) -> storageService.getUserTokens(card).also { - tangemTechApi.saveUserTokens(userId = userId, userTokens = it.toUserTokensResponse()) + remoteSaveUserTokens(userId = userId, userTokens = it.toUserTokensResponse()) } else -> { val tokens = storageService.getUserTokens(userId) ?: storageService.getUserTokens(card) @@ -106,6 +95,31 @@ class UserTokensRepository( } } + private suspend fun remoteGetUserTokens(card: CardDTO, userId: String): List { + runCatching { + tangemTechApi.getUserTokens(userId) + }.onSuccess { response -> + return response.tokens + .mapNotNull(Currency.Companion::fromTokenResponse).also { + storageService.saveUserTokens(userId, it.toUserTokensResponse()) + } + .distinct() + }.onFailure { + return handleGetUserTokensFailure(card = card, userId = userId, error = it) + } + error("Unreachable code because runCatching must return result") + } + + private suspend fun remoteSaveUserTokens(userId: String, userTokens: UserTokensResponse) { + // it can throw okhttp3.internal.http2.StreamResetException: stream was reset: INTERNAL_ERROR + // if the /user-tokens endpoint disabled + runCatching { + tangemTechApi.saveUserTokens(userId, userTokens) + }.onFailure { + Timber.e(it) + } + } + private fun getUserWalletId(card: CardDTO): String? { return UserWalletIdBuilder.card(card).build() ?.stringValue 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 91d3b8dd91..ea9b4c6bb9 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 @@ -22,47 +22,19 @@ interface UserWalletsListManager { val selectedUserWalletSync: UserWallet? /** - * Indicates that all [UserWallet]s is unlocked - * - * @see [isLockedSync] - * @see [UserWallet.isLocked] + * Indicates that the [UserWalletsListManager] contains at least one saved [UserWallet] * */ - val isLocked: Flow - - /** - * Indicates that all [UserWallet]s is unlocked - * - * Sync version - * - * @see [isLocked] - * @see [UserWallet.isLocked] - * */ - val isLockedSync: Boolean - - val hasSavedUserWallets: Boolean - - /** - * Receive saved [UserWallet]s, populate [userWallets] flow with it and set [isLocked] as false. - * Require biometric user authorization - * - * @return [CompletionResult] of operation, with selected [UserWallet] - * or null if there is no selected [UserWallet] - * */ - suspend fun unlockWithBiometry(): CompletionResult - - /** - * Remove [UserWallet]s from [userWallets] and set [isLocked] as true - * */ - fun lock() + val hasUserWallets: Boolean /** * Set [UserWallet] with provided [UserWalletId] as selected * * @param userWalletId [UserWalletId] of [UserWallet] which must be selected * - * @return [CompletionResult] of operation with selected [UserWallet] + * @return [CompletionResult.Success] with selected [UserWallet] + * or [CompletionResult.Failure] with [NoSuchElementException] if [UserWallet] with [userWalletId] not found * */ - suspend fun selectWallet(userWalletId: UserWalletId): CompletionResult + suspend fun select(userWalletId: UserWalletId): CompletionResult /** * Save provided user wallet and set it as selected @@ -80,13 +52,16 @@ interface UserWalletsListManager { * Can terminate with [NoSuchElementException] if unable to find [UserWallet] with provided [UserWalletId] * @param userWalletId update [UserWallet] with that [UserWalletId] * @param update lambda that receives stored [UserWallet] and returns updated [UserWallet] - * @return [CompletionResult] of operation with updated [UserWallet] + * @return [CompletionResult.Success] with updated [UserWallet] + * or [CompletionResult.Failure] with [NoSuchElementException] if [UserWallet] with [userWalletId] not found * */ suspend fun update(userWalletId: UserWalletId, update: (UserWallet) -> UserWallet): CompletionResult /** * Delete saved [UserWallet]s with provided [UserWalletId]s * + * Sets [isLocked] as true if [userWallets] is empty or if all [userWallets] are locked + * * @param userWalletIds [UserWalletId]s of [UserWallet]s which must be deleted * * @return [CompletionResult] of operation @@ -102,12 +77,45 @@ interface UserWalletsListManager { /** * Get [UserWallet] with provided [UserWalletId] - * May terminate with [NoSuchElementException] if [UserWallet] is not found * - * @return [CompletionResult] of operation with found [UserWallet] + * @return [CompletionResult.Success] with found [UserWallet] + * or [CompletionResult.Failure] with [NoSuchElementException] if [UserWallet] with [userWalletId] not found * */ suspend fun get(userWalletId: UserWalletId): CompletionResult + interface Lockable : UserWalletsListManager { + /** + * Indicates that all [UserWallet]s is locked + * + * @see [isLockedSync] + * @see [UserWallet.isLocked] + * */ + val isLocked: Flow + + /** + * Indicates that all [UserWallet]s is locked + * + * Sync version + * + * @see [isLocked] + * @see [UserWallet.isLocked] + * */ + val isLockedSync: Boolean + + /** + * Receive saved [UserWallet]s, populate [userWallets] flow with it and set [isLocked] as false. + * + * @return [CompletionResult] of operation, with selected [UserWallet] + * or null if there is no selected [UserWallet] + * */ + suspend fun unlock(): CompletionResult + + /** + * Remove [UserWallet]s from [userWallets] and set [isLocked] as true + * */ + fun lock() + } + // For provider companion object } \ No newline at end of file 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 new file mode 100644 index 0000000000..6da40c3b93 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/UserWalletsListManagerExtensions.kt @@ -0,0 +1,57 @@ +package com.tangem.tap.domain.userWalletList + +import com.tangem.common.CompletionResult +import com.tangem.tap.domain.model.UserWallet +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf + +/** + * Indicates that the [UserWalletsListManager] implements [UserWalletsListManager.Lockable] + * */ +val UserWalletsListManager.isLockable: Boolean + get() = this is UserWalletsListManager.Lockable + +/** + * Indicates that the [UserWalletsListManager] is locked + * + * @return If [UserWalletsListManager] not implements [UserWalletsListManager.Lockable] returns [Flow] which + * produces only one false value + * */ +val UserWalletsListManager.isLocked: Flow + get() = asLockable()?.isLocked ?: flowOf(false) + +/** + * Indicates that the [UserWalletsListManager] is locked + * + * @return If [UserWalletsListManager] not implements [UserWalletsListManager.Lockable] returns false + * */ +val UserWalletsListManager.isLockedSync: Boolean + get() = asLockable()?.isLockedSync ?: false + +/** + * Call [UserWalletsListManager.Lockable.unlock] if [UserWalletsListManager] implements [UserWalletsListManager.Lockable] + * + * @return If [UserWalletsListManager] not implements [UserWalletsListManager.Lockable] + * returns [CompletionResult.Success] with [UserWalletsListManager.selectedUserWalletSync] + * */ +suspend fun UserWalletsListManager.unlockIfLockable(): CompletionResult { + return asLockable()?.unlock() ?: CompletionResult.Success(selectedUserWalletSync) +} + +/** + * Call [UserWalletsListManager.Lockable.lock] if [UserWalletsListManager] implements [UserWalletsListManager.Lockable] + * or do nothing otherwise + * */ +fun UserWalletsListManager.lockIfLockable() { + asLockable()?.lock() +} + +/** + * Safe cast [UserWalletsListManager] to [UserWalletsListManager.Lockable] + * + * @return If [UserWalletsListManager] not implements [UserWalletsListManager.Lockable] then returns null or + * [UserWalletsListManager.Lockable] otherwise + * */ +fun UserWalletsListManager.asLockable(): UserWalletsListManager.Lockable? { + return this as? UserWalletsListManager.Lockable +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerProvider.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerProvider.kt index 3773da7127..108547e8b2 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerProvider.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerProvider.kt @@ -10,6 +10,7 @@ import com.tangem.tangem_sdk_new.storage.createEncryptedSharedPreferences import com.tangem.tap.domain.TangemSdkManager import com.tangem.tap.domain.userWalletList.UserWalletsListManager import com.tangem.tap.domain.userWalletList.implementation.BiometricUserWalletsListManager +import com.tangem.tap.domain.userWalletList.implementation.RuntimeUserWalletsListManager import com.tangem.tap.domain.userWalletList.repository.implementation.BiometricUserWalletsKeysRepository import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultSelectedUserWalletRepository import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultUserWalletsPublicInformationRepository @@ -68,4 +69,8 @@ fun UserWalletsListManager.Companion.provideBiometricImplementation( sensitiveInformationRepository = sensitiveInformationRepository, selectedUserWalletRepository = selectedUserWalletRepository, ) +} + +fun UserWalletsListManager.Companion.provideRuntimeImplementation(): UserWalletsListManager { + return RuntimeUserWalletsListManager() } \ No newline at end of file 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 5c5be22e1f..060464fd2f 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 @@ -23,7 +23,7 @@ internal class BiometricUserWalletsListManager( private val publicInformationRepository: UserWalletsPublicInformationRepository, private val sensitiveInformationRepository: UserWalletsSensitiveInformationRepository, private val selectedUserWalletRepository: SelectedUserWalletRepository, -) : UserWalletsListManager { +) : UserWalletsListManager.Lockable { private val state = MutableStateFlow(State()) override val userWallets: Flow> @@ -50,10 +50,10 @@ internal class BiometricUserWalletsListManager( override val isLockedSync: Boolean get() = state.value.isLocked - override val hasSavedUserWallets: Boolean + override val hasUserWallets: Boolean get() = keysRepository.hasSavedEncryptionKeys() - override suspend fun unlockWithBiometry(): CompletionResult { + override suspend fun unlock(): CompletionResult { return unlockWithBiometryInternal() .map { selectedUserWalletSync } } @@ -62,20 +62,20 @@ internal class BiometricUserWalletsListManager( state.update { State() } } - override suspend fun selectWallet(userWalletId: UserWalletId): CompletionResult = catching { + override suspend fun select(userWalletId: UserWalletId): CompletionResult = catching { if (state.value.selectedUserWalletId == userWalletId) { return@catching findSelectedUserWallet()!! } selectedUserWalletRepository.set(userWalletId) - state.update { prevState -> + val newState = state.updateAndGet { prevState -> prevState.copy( selectedUserWalletId = userWalletId, ) } - findSelectedUserWallet()!! + newState.userWallets.first { it.walletId == userWalletId } } override suspend fun save(userWallet: UserWallet, canOverride: Boolean): CompletionResult { @@ -289,7 +289,7 @@ internal class BiometricUserWalletsListManager( } private fun findSelectedUserWallet(userWallets: List = state.value.userWallets): UserWallet? { - return userWallets.find { + return userWallets.firstOrNull { it.walletId == state.value.selectedUserWalletId } } 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 new file mode 100644 index 0000000000..01a31ebff4 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/RuntimeUserWalletsListManager.kt @@ -0,0 +1,103 @@ +package com.tangem.tap.domain.userWalletList.implementation + +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.UserWalletsListManager +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.flow.updateAndGet + +@OptIn(ExperimentalCoroutinesApi::class) +internal class RuntimeUserWalletsListManager : UserWalletsListManager { + private val state = MutableStateFlow(State()) + + override val userWallets: Flow> + get() = state + .mapLatest { listOfNotNull(it.userWallet) } + .distinctUntilChanged() + + override val selectedUserWallet: Flow + get() = state + .mapLatest { it.userWallet } + .filterNotNull() + .distinctUntilChanged() + + override val selectedUserWalletSync: UserWallet? + get() = state.value.userWallet + + override val hasUserWallets: Boolean + get() = state.value.userWallet != null + + override suspend fun select(userWalletId: UserWalletId): CompletionResult = catching { + state.value.userWallet + ?.takeIf { it.walletId == userWalletId } + ?: walletNotFound() + } + + override suspend fun save(userWallet: UserWallet, canOverride: Boolean): CompletionResult { + return if (canOverride) { + saveInternal(userWallet) + } else { + val isWalletSaved = state.value.userWallet?.walletId == userWallet.walletId + + if (isWalletSaved) { + CompletionResult.Failure(UserWalletListError.WalletAlreadySaved) + } else { + saveInternal(userWallet) + } + } + } + + override suspend fun update( + userWalletId: UserWalletId, + update: (UserWallet) -> UserWallet, + ): CompletionResult = catching { + val wallet = state.value.userWallet + ?.takeIf { it.walletId == userWalletId } + ?: walletNotFound() + + state.updateAndGet { prevState -> + prevState.copy( + userWallet = update(wallet), + ) + }.userWallet!! + } + + override suspend fun delete(userWalletIds: List): CompletionResult = clear() + + override suspend fun clear(): CompletionResult = catching { + state.update { prevState -> + prevState.copy( + userWallet = null, + ) + } + } + + override suspend fun get(userWalletId: UserWalletId): CompletionResult = catching { + state.value.userWallet ?: walletNotFound() + } + + private fun saveInternal(userWallet: UserWallet): CompletionResult = catching { + state.update { prevState -> + prevState.copy( + userWallet = userWallet, + ) + } + } + + private fun walletNotFound(): Nothing { + throw NoSuchElementException("User wallet not found") + } + + private data class State( + val userWallet: UserWallet? = null, + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletCurrencies/implementation/DefaultWalletCurrenciesManager.kt b/app/src/main/java/com/tangem/tap/domain/walletCurrencies/implementation/DefaultWalletCurrenciesManager.kt index b9292c096c..5fe6dd8e22 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletCurrencies/implementation/DefaultWalletCurrenciesManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletCurrencies/implementation/DefaultWalletCurrenciesManager.kt @@ -1,6 +1,5 @@ package com.tangem.tap.domain.walletCurrencies.implementation -import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.DerivationStyle import com.tangem.common.CompletionResult import com.tangem.common.flatMap @@ -69,7 +68,7 @@ internal class DefaultWalletCurrenciesManager( .flatMap { updateWalletStoresAmounts( userWallet = userWallet, - updatedBlockchains = currenciesToAdd.map { it.blockchain }.distinct(), + updatedCurrencies = currenciesToAddWithMissingBlockchains, ) } } @@ -220,11 +219,13 @@ internal class DefaultWalletCurrenciesManager( private suspend fun updateWalletStoresAmounts( userWallet: UserWallet, - updatedBlockchains: List, + updatedCurrencies: List, ): CompletionResult { + val updatedBlockchains = updatedCurrencies + .filterIsInstance() val updatedWalletStores = walletStoresRepository.get(userWallet.walletId) .firstOrNull() - ?.filter { it.blockchain in updatedBlockchains } + ?.filter { it.blockchainWalletData.currency in updatedBlockchains } ?: return CompletionResult.Success(Unit) return walletAmountsRepository.updateAmountsForWalletStores( 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 1080f7529f..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 } @@ -142,8 +151,8 @@ internal class DefaultWalletAmountsRepository( Timber.e( error, """ - Unable to fetch fiat rates - |- Coins ids: $coinsIds + Unable to fetch fiat rates + |- Coins ids: $coinsIds """.trimIndent(), ) @@ -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, @@ -278,6 +313,7 @@ internal class DefaultWalletAmountsRepository( Unable to fetch amounts |- User wallet id: ${walletStore.userWalletId} |- Blockchain: ${walletStore.blockchain} + |- Derivation path: ${walletStore.derivationPath?.rawPath} """.trimIndent(), ) @@ -310,6 +346,7 @@ internal class DefaultWalletAmountsRepository( Fetched amounts |- User wallet id: ${walletStore.userWalletId} |- Blockchain: ${walletStore.blockchain} + |- Derivation path: ${walletStore.derivationPath?.rawPath} """.trimIndent(), ) @@ -337,6 +374,7 @@ internal class DefaultWalletAmountsRepository( Missed derivation |- User wallet id: ${walletStore.userWalletId} |- Blockchain: ${walletStore.blockchain} + |- Derivation path: ${walletStore.derivationPath?.rawPath} """.trimIndent(), ) @@ -360,6 +398,7 @@ internal class DefaultWalletAmountsRepository( Wallet manager is null |- User wallet id: ${walletStore.userWalletId} |- Blockchain: ${walletStore.blockchain} + |- Derivation path: ${walletStore.derivationPath?.rawPath} """.trimIndent(), ) @@ -405,6 +444,7 @@ internal class DefaultWalletAmountsRepository( Fetched wallet rent |- User wallet id: ${walletStore.userWalletId} |- Blockchain: ${walletStore.blockchain} + |- Derivation path: ${walletStore.derivationPath?.rawPath} |- Rent: $rent """.trimIndent(), ) @@ -421,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 d9579349d6..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 @@ -1,5 +1,6 @@ package com.tangem.tap.domain.walletStores.repository.implementation +import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.DerivationParams import com.tangem.blockchain.common.DerivationStyle @@ -51,6 +52,7 @@ internal class DefaultWalletManagersRepository( val foundWalletManager = findWalletManager( userWalletId = userWallet.walletId, blockchain = blockchainNetwork?.blockchain, + derivationPath = blockchainNetwork?.derivationPath, ) foundWalletManager?.updateTokens( @@ -85,7 +87,13 @@ internal class DefaultWalletManagersRepository( return when { blockchain == Blockchain.Unknown || blockchain == null -> { val error = WalletStoresError.UnknownBlockchain() - Timber.e(error) + Timber.e( + error, + """ + Unknown blockchain while creating wallet manager + |- User wallet ID: ${userWallet.walletId} + """.trimIndent(), + ) CompletionResult.Failure(error) } walletManager != null -> { @@ -97,7 +105,15 @@ internal class DefaultWalletManagersRepository( } else -> { val error = WalletStoresError.WalletManagerNotCreated(blockchain) - Timber.e(error) + Timber.e( + error, + """ + Unable to create wallet manager + |- User wallet ID: ${userWallet.walletId} + |- Blockchain: $blockchain + |- Derivation path: ${blockchainNetwork?.derivationPath} + """.trimIndent(), + ) CompletionResult.Failure(error) } } @@ -156,17 +172,21 @@ internal class DefaultWalletManagersRepository( val tokens = blockchainNetwork?.tokens ?: listOfNotNull(scanResponse.cardTypesResolver.getPrimaryToken()) if (tokens != walletManager.cardTokens) { + // TODO: remove ability to manipulate with walletManager.cardTokens walletManager.cardTokens.clear() walletManager.wallet.removeAllTokens() if (tokens.isNotEmpty()) { walletManager.cardTokens.addAll(tokens) + // add empty amounts to prepare templates of tokens WalletDataModel + // see: WalletMangerWalletStoreBuilderImpl.build() + tokens.forEach { walletManager.wallet.setAmount(Amount(it)) } } } walletManager } .mapFailure { - val error = WalletStoresError.UpdateWalletManagerError( + val error = WalletStoresError.UpdateWalletManagerTokensError( blockchain = walletManager.wallet.blockchain, cause = it, ) @@ -178,6 +198,7 @@ internal class DefaultWalletManagersRepository( private suspend fun findWalletManager( userWalletId: UserWalletId, blockchain: Blockchain?, + derivationPath: String?, ): WalletManager? { return walletManagersStorage.getAll() .firstOrNull() @@ -186,7 +207,10 @@ internal class DefaultWalletManagersRepository( if (blockchain == null) { userWalletManagers.firstOrNull() } else { - userWalletManagers.firstOrNull { it.wallet.blockchain == blockchain } + userWalletManagers.firstOrNull { + it.wallet.blockchain == blockchain && + it.wallet.publicKey.derivationPath?.rawPath == derivationPath + } } } } diff --git a/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/utils/WalletStoreOperations.kt b/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/utils/WalletStoreOperations.kt index d052401b0c..efcd36e402 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/utils/WalletStoreOperations.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/utils/WalletStoreOperations.kt @@ -121,9 +121,10 @@ private inline fun List.replaceWalletStores( if (currentWalletStore != updatedWalletStore) { Timber.d( """ - Update wallet store in storage - |- User wallet ID: ${updatedWalletStore.userWalletId} - |- Blockchain: ${updatedWalletStore.blockchain} + Update wallet store in storage + |- User wallet ID: ${updatedWalletStore.userWalletId} + |- Blockchain: ${updatedWalletStore.blockchain} + |- Derivation path: ${updatedWalletStore.derivationPath?.rawPath} """.trimIndent(), ) 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..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 @@ -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,8 @@ class WalletConnectSdkHelper { ) return when (result) { SimpleResult.Success -> { + val sentFrom = AnalyticsParam.TxSentFrom.WalletConnect + Analytics.send(Basic.TransactionSent(sentFrom)) HEX_PREFIX + data.walletManager.wallet.recentTransactions.last().hash } is SimpleResult.Failure -> { diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt index 983be0f335..89dc7ed09b 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt @@ -4,6 +4,7 @@ import com.tangem.common.CompletionResult import com.tangem.common.core.TangemSdkError import com.tangem.common.doOnFailure import com.tangem.common.doOnSuccess +import com.tangem.common.extensions.guard import com.tangem.common.flatMap import com.tangem.core.analytics.Analytics import com.tangem.domain.common.ScanResponse @@ -12,6 +13,7 @@ import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Settings import com.tangem.tap.common.extensions.dispatchDialogShow import com.tangem.tap.common.extensions.dispatchOnMain +import com.tangem.tap.common.extensions.dispatchWithMain import com.tangem.tap.common.extensions.onUserWalletSelected import com.tangem.tap.common.redux.AppDialog import com.tangem.tap.common.redux.AppState @@ -20,9 +22,15 @@ import com.tangem.tap.common.redux.navigation.AppScreen import com.tangem.tap.common.redux.navigation.NavigationAction import com.tangem.tap.domain.model.builders.UserWalletBuilder import com.tangem.tap.domain.model.builders.UserWalletIdBuilder +import com.tangem.tap.domain.userWalletList.UserWalletsListManager +import com.tangem.tap.domain.userWalletList.di.provideBiometricImplementation +import com.tangem.tap.domain.userWalletList.di.provideRuntimeImplementation +import com.tangem.tap.domain.userWalletList.isLockedSync import com.tangem.tap.features.demo.DemoHelper import com.tangem.tap.features.onboarding.products.twins.redux.CreateTwinWalletMode import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction +import com.tangem.tap.features.wallet.redux.WalletAction +import com.tangem.tap.foregroundActivityObserver import com.tangem.tap.preferencesStorage import com.tangem.tap.scope import com.tangem.tap.store @@ -132,7 +140,6 @@ class DetailsMiddleware { store.onUserWalletSelected(selectedUserWallet) } } else { - userWalletsListManager.lock() store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Home)) } } @@ -234,7 +241,7 @@ class DetailsMiddleware { delay(timeMillis = 100) } } - store.dispatchOnMain( + store.dispatchWithMain( DetailsAction.AppSettings.BiometricsStatusChanged( needEnrollBiometrics = tangemSdkManager.needEnrollBiometrics, ), @@ -250,13 +257,13 @@ class DetailsMiddleware { private fun toggleSaveWallets(state: DetailsState, enable: Boolean) = scope.launch { // Nothing to change if (preferencesStorage.shouldSaveUserWallets == enable) { - store.dispatchOnMain(DetailsAction.AppSettings.SwitchPrivacySetting.Success) + store.dispatchWithMain(DetailsAction.AppSettings.SwitchPrivacySetting.Success) return@launch } toggleSaveWallets(state.scanResponse, enable) .doOnFailure { - store.dispatchOnMain( + store.dispatchWithMain( DetailsAction.AppSettings.SwitchPrivacySetting.Failure( prevState = !enable, setting = AppSetting.SaveWallets, @@ -264,7 +271,7 @@ class DetailsMiddleware { ) } .doOnSuccess { - store.dispatchOnMain(DetailsAction.AppSettings.SwitchPrivacySetting.Success) + store.dispatchWithMain(DetailsAction.AppSettings.SwitchPrivacySetting.Success) } } @@ -282,13 +289,13 @@ class DetailsMiddleware { private fun toggleSaveAccessCodes(state: DetailsState, enable: Boolean) = scope.launch { // Nothing to change if (preferencesStorage.shouldSaveAccessCodes == enable) { - store.dispatchOnMain(DetailsAction.AppSettings.SwitchPrivacySetting.Success) + store.dispatchWithMain(DetailsAction.AppSettings.SwitchPrivacySetting.Success) return@launch } toggleSaveAccessCodes(state.scanResponse, state.appSettingsState.saveWallets, enable) .doOnFailure { - store.dispatchOnMain( + store.dispatchWithMain( DetailsAction.AppSettings.SwitchPrivacySetting.Failure( prevState = !enable, setting = AppSetting.SaveAccessCode, @@ -296,7 +303,7 @@ class DetailsMiddleware { ) } .doOnSuccess { - store.dispatchOnMain(DetailsAction.AppSettings.SwitchPrivacySetting.Success) + store.dispatchWithMain(DetailsAction.AppSettings.SwitchPrivacySetting.Success) } } @@ -320,11 +327,14 @@ class DetailsMiddleware { scanResponse: ScanResponse?, enableAccessCodesSaving: Boolean, ): CompletionResult { - val userWallet = scanResponse?.let { UserWalletBuilder(it).build() } + val userWallet = userWalletsListManager.selectedUserWalletSync + ?: scanResponse?.let { UserWalletBuilder(it).build() } ?: return CompletionResult.Failure( - TangemSdkError.ExceptionError(IllegalStateException("scanResponse is null")), + error = TangemSdkError.ExceptionError(IllegalStateException("scanResponse is null")), ) + updateUserWalletsListManager(enableUserWalletsSaving = true) + return userWalletsListManager.save(userWallet) .flatMap { if (enableAccessCodesSaving) { @@ -339,7 +349,7 @@ class DetailsMiddleware { preferencesStorage.shouldShowSaveUserWalletScreen = false preferencesStorage.shouldSaveUserWallets = true - store.onUserWalletSelected(userWallet) + store.dispatchWithMain(WalletAction.UpdateCanSaveUserWallets(canSaveUserWallets = true)) } .doOnFailure { error -> Timber.e(error, "Unable to save user wallet") @@ -352,9 +362,11 @@ class DetailsMiddleware { .doOnSuccess { Analytics.send(Settings.AppSettings.SaveWalletSwitcherChanged(AnalyticsParam.OnOffState.Off)) deleteSavedAccessCodes() + updateUserWalletsListManager(enableUserWalletsSaving = false) preferencesStorage.shouldSaveUserWallets = false - store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Home)) + store.dispatchWithMain(WalletAction.UpdateCanSaveUserWallets(canSaveUserWallets = true)) + store.dispatchWithMain(NavigationAction.PopBackTo(AppScreen.Home)) } .doOnFailure { error -> Timber.e(error, "Unable to delete saved wallets") @@ -386,5 +398,27 @@ class DetailsMiddleware { Timber.e(error, "Unable to delete saved access codes") } } + + private suspend fun updateUserWalletsListManager(enableUserWalletsSaving: Boolean) { + val manager = if (enableUserWalletsSaving) { + createBiometricsUserWalletsManager() ?: return + } else { + UserWalletsListManager.provideRuntimeImplementation() + } + + store.dispatchWithMain(GlobalAction.UpdateUserWalletsListManager(manager)) + } + + private fun createBiometricsUserWalletsManager(): UserWalletsListManager? { + val context = foregroundActivityObserver.foregroundActivity?.applicationContext.guard { + Timber.e(IllegalStateException("No activities in foreground")) + return null + } + + return UserWalletsListManager.provideBiometricImplementation( + context = context, + tangemSdkManager = tangemSdkManager, + ) + } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/disclaimer/DisclaimerType.kt b/app/src/main/java/com/tangem/tap/features/disclaimer/DisclaimerType.kt index d6af0e0596..b88b4488ab 100644 --- a/app/src/main/java/com/tangem/tap/features/disclaimer/DisclaimerType.kt +++ b/app/src/main/java/com/tangem/tap/features/disclaimer/DisclaimerType.kt @@ -35,6 +35,8 @@ fun DisclaimerType.createDisclaimer(cardDTO: CardDTO): Disclaimer { } } +fun CardDTO.createDisclaimer(): Disclaimer = DisclaimerType.get(this).createDisclaimer(this) + private fun provideDisclaimerDataProvider(cardId: String): DisclaimerDataProvider { return object : DisclaimerDataProvider { override fun getLanguage(): String = Locale.getDefault().language diff --git a/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt index 72c24acdb4..0ea4b412cf 100644 --- a/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt @@ -5,13 +5,13 @@ import com.tangem.common.doOnResult import com.tangem.common.doOnSuccess import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.AnalyticsEvent +import com.tangem.domain.common.ScanResponse import com.tangem.tap.common.analytics.events.IntroductionProcess import com.tangem.tap.common.analytics.events.Shop import com.tangem.tap.common.entities.IndeterminateProgressButton import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.dispatchOpenUrl import com.tangem.tap.common.extensions.eraseContext -import com.tangem.tap.common.extensions.onCardScanned import com.tangem.tap.common.extensions.onUserWalletSelected import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction @@ -94,29 +94,32 @@ private fun readCard(analyticsEvent: AnalyticsEvent?) = scope.launch { changeButtonState(ButtonState.ENABLED) }, onSuccess = { scanResponse -> - scope.launch { - if (preferencesStorage.shouldSaveUserWallets) { - val userWallet = UserWalletBuilder(scanResponse).build() ?: return@launch - userWalletsListManager.save(userWallet) - .doOnFailure { error -> - Timber.e(error, "Unable to save user wallet") - store.onCardScanned(scanResponse) - } - .doOnSuccess { - scope.launch { store.onUserWalletSelected(userWallet) } - } - .doOnResult { - navigateTo(AppScreen.Wallet) - } - } else { - store.onCardScanned(scanResponse) - navigateTo(AppScreen.Wallet) - } - } + proceedWithScanResponse(scanResponse) }, ) } +fun proceedWithScanResponse(scanResponse: ScanResponse) { + scope.launch { + val userWallet = UserWalletBuilder(scanResponse).build() + if (userWallet == null) { + Timber.e("User wallet not created") + return@launch + } + + userWalletsListManager.save(userWallet) + .doOnFailure { error -> + Timber.e(error, "Unable to save user wallet") + } + .doOnSuccess { + scope.launch { store.onUserWalletSelected(userWallet) } + } + .doOnResult { + navigateTo(AppScreen.Wallet) + } + } +} + private suspend fun navigateTo(appScreen: AppScreen) { store.dispatchOnMain(NavigationAction.NavigateTo(appScreen)) delay(timeMillis = 200) diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt index e0d51ccbf7..fdf3a8b90b 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt @@ -1,21 +1,27 @@ package com.tangem.tap.features.onboarding +import com.tangem.common.doOnFailure +import com.tangem.common.doOnSuccess +import com.tangem.common.extensions.guard import com.tangem.core.analytics.Analytics import com.tangem.domain.common.ProductType import com.tangem.domain.common.ScanResponse import com.tangem.tap.common.extensions.dispatchOnMain -import com.tangem.tap.common.extensions.onCardScanned +import com.tangem.tap.common.extensions.onUserWalletSelected import com.tangem.tap.common.extensions.removeContext import com.tangem.tap.common.extensions.setContext import com.tangem.tap.common.redux.navigation.AppScreen import com.tangem.tap.common.redux.navigation.NavigationAction +import com.tangem.tap.domain.model.builders.UserWalletBuilder import com.tangem.tap.features.saveWallet.redux.SaveWalletAction import com.tangem.tap.preferencesStorage import com.tangem.tap.scope import com.tangem.tap.store import com.tangem.tap.tangemSdkManager +import com.tangem.tap.userWalletsListManager import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import timber.log.Timber /** [REDACTED_AUTHOR] @@ -62,6 +68,8 @@ object OnboardingHelper { when { // When should save user wallets, then save card without navigate to save wallet screen preferencesStorage.shouldSaveUserWallets -> scope.launch { + proceedWithScanResponse(scanResponse, backupCardsIds) + store.dispatchOnMain( SaveWalletAction.ProvideBackupInfo( scanResponse = scanResponse, @@ -75,7 +83,7 @@ object OnboardingHelper { // then open save wallet screen tangemSdkManager.canUseBiometry && preferencesStorage.shouldShowSaveUserWalletScreen -> scope.launch { - store.onCardScanned(scanResponse) + proceedWithScanResponse(scanResponse, backupCardsIds) delay(timeMillis = 1_200) @@ -90,7 +98,7 @@ object OnboardingHelper { } // If device has no biometry and save wallet screen has been shown, then go through old scenario else -> scope.launch { - store.onCardScanned(scanResponse) + proceedWithScanResponse(scanResponse, backupCardsIds) } } @@ -100,4 +108,22 @@ object OnboardingHelper { fun onInterrupted() { Analytics.removeContext() } + + private suspend fun proceedWithScanResponse(scanResponse: ScanResponse, backupCardsIds: List?) { + val userWallet = UserWalletBuilder(scanResponse) + .backupCardsIds(backupCardsIds?.toSet()) + .build() + .guard { + Timber.e("User wallet not created") + return + } + + userWalletsListManager.save(userWallet, canOverride = true) + .doOnFailure { error -> + Timber.e(error, "Unable to save user wallet") + } + .doOnSuccess { + scope.launch { store.onUserWalletSelected(userWallet) } + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/redux/OnboardingOtherCardsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/redux/OnboardingOtherCardsMiddleware.kt index acbe7a146c..faa3dd2441 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/redux/OnboardingOtherCardsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/redux/OnboardingOtherCardsMiddleware.kt @@ -11,10 +11,11 @@ import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.domain.tokens.models.BlockchainNetwork import com.tangem.tap.features.onboarding.OnboardingHelper -import com.tangem.tap.features.wallet.redux.WalletAction +import com.tangem.tap.features.wallet.models.toCurrencies import com.tangem.tap.scope import com.tangem.tap.store import com.tangem.tap.tangemSdkManager +import com.tangem.tap.userTokensRepository import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.rekotlin.Action @@ -112,9 +113,12 @@ private fun handleOtherCardsAction(action: Action) { ) } - store.dispatch( - WalletAction.MultiWallet.SaveCurrencies(blockchainNetworks, updatedResponse.card), - ) + scope.launch { + userTokensRepository.saveUserTokens( + card = result.data.card, + tokens = blockchainNetworks.toCurrencies(), + ) + } delay(DELAY_SDK_DIALOG_CLOSE) store.dispatch(OnboardingOtherCardsAction.SetStepOfScreen(OnboardingOtherCardsStep.Done)) diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt index 1f77d2ae6f..ff7ff2399e 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt @@ -26,6 +26,7 @@ import com.tangem.tap.domain.TapError import com.tangem.tap.domain.extensions.makePrimaryWalletManager import com.tangem.tap.domain.model.builders.UserWalletIdBuilder import com.tangem.tap.domain.twins.TwinCardsManager +import com.tangem.tap.domain.userWalletList.isLockedSync import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE import com.tangem.tap.features.onboarding.OnboardingDialog import com.tangem.tap.features.onboarding.OnboardingHelper @@ -335,7 +336,7 @@ private fun handle(action: Action, dispatch: DispatchFunction) { } private fun getPopBackScreen(): AppScreen { - return if (userWalletsListManager.hasSavedUserWallets) { + return if (userWalletsListManager.hasUserWallets) { if (userWalletsListManager.isLockedSync) { AppScreen.Welcome } else { diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt index 1ea9f428aa..4226d47b74 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt @@ -26,13 +26,14 @@ import com.tangem.tap.features.onboarding.OnboardingDialog import com.tangem.tap.features.onboarding.OnboardingHelper import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayActivationManagerFactory import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayAction +import com.tangem.tap.features.wallet.models.toCurrencies import com.tangem.tap.features.wallet.redux.Artwork -import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.preferencesStorage import com.tangem.tap.scope import com.tangem.tap.store import com.tangem.tap.tangemSdk import com.tangem.tap.tangemSdkManager +import com.tangem.tap.userTokensRepository import kotlinx.coroutines.launch import org.rekotlin.Action import org.rekotlin.DispatchFunction @@ -135,12 +136,12 @@ private fun handleWalletAction(action: Action, state: () -> AppState?, dispatch: BlockchainNetwork(blockchain, result.data.card) } - store.dispatch( - WalletAction.MultiWallet.SaveCurrencies( - blockchainNetworks = blockchainNetworks, + scope.launch { + userTokensRepository.saveUserTokens( card = result.data.card, - ), - ) + tokens = blockchainNetworks.toCurrencies(), + ) + } startCardActivation(updatedResponse) store.dispatch(OnboardingWalletAction.ResumeBackup) } diff --git a/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt index 2dfa79cb24..7c248c0f4f 100644 --- a/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt @@ -1,19 +1,27 @@ package com.tangem.tap.features.saveWallet.redux import com.tangem.common.CompletionResult +import com.tangem.common.core.TangemSdkError import com.tangem.common.doOnFailure import com.tangem.common.doOnSuccess +import com.tangem.common.extensions.guard import com.tangem.common.flatMap import com.tangem.core.analytics.Analytics import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.MainScreen import com.tangem.tap.common.analytics.events.Onboarding import com.tangem.tap.common.extensions.dispatchOnMain -import com.tangem.tap.common.extensions.onUserWalletSelected +import com.tangem.tap.common.extensions.dispatchWithMain import com.tangem.tap.common.redux.AppState +import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.common.redux.navigation.AppScreen import com.tangem.tap.common.redux.navigation.NavigationAction import com.tangem.tap.domain.model.builders.UserWalletBuilder +import com.tangem.tap.domain.userWalletList.UserWalletsListManager +import com.tangem.tap.domain.userWalletList.di.provideBiometricImplementation +import com.tangem.tap.domain.userWalletList.isLockable +import com.tangem.tap.features.wallet.redux.WalletAction +import com.tangem.tap.foregroundActivityObserver import com.tangem.tap.preferencesStorage import com.tangem.tap.scope import com.tangem.tap.store @@ -21,6 +29,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 SaveWalletMiddleware { val middleware: Middleware = { _, stateProvider -> @@ -86,32 +95,51 @@ internal class SaveWalletMiddleware { } scope.launch { - val userWallet = UserWalletBuilder(scanResponse) - .backupCardsIds(state.backupInfo?.backupCardsIds) - .build() ?: return@launch + val userWallet = userWalletsListManager.selectedUserWalletSync + ?: UserWalletBuilder(scanResponse) + .backupCardsIds(state.backupInfo?.backupCardsIds) + .build() + ?: return@launch - val isFirstSavedWallet = !userWalletsListManager.hasSavedUserWallets + provideBiometricUserWalletsListManager() + + val isFirstSavedWallet = !userWalletsListManager.hasUserWallets saveAccessCodeIfNeeded(state.backupInfo?.accessCode, userWallet.cardsInWallet) .flatMap { userWalletsListManager.save(userWallet, canOverride = true) } .doOnFailure { error -> - store.dispatchOnMain(SaveWalletAction.Save.Error(error)) + store.dispatchWithMain(SaveWalletAction.Save.Error(error)) } .doOnSuccess { preferencesStorage.shouldSaveUserWallets = true - // Enable saving access codes only if this is the first time user save the wallet preferencesStorage.shouldSaveAccessCodes = isFirstSavedWallet || preferencesStorage.shouldSaveAccessCodes - store.dispatchOnMain(SaveWalletAction.Save.Success) - - store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Wallet)) - store.onUserWalletSelected(userWallet) + store.dispatchWithMain(SaveWalletAction.Save.Success) + store.dispatchWithMain(NavigationAction.PopBackTo(AppScreen.Wallet)) + store.dispatchWithMain(WalletAction.UpdateCanSaveUserWallets(canSaveUserWallets = true)) } } } + private suspend fun provideBiometricUserWalletsListManager() { + if (store.state.globalState.userWalletsListManager?.isLockable == true) return + + val context = foregroundActivityObserver.foregroundActivity?.applicationContext.guard { + val error = IllegalStateException("No activities in foreground") + Timber.e(error) + store.dispatchWithMain(SaveWalletAction.Save.Error(TangemSdkError.ExceptionError(error))) + return + } + val manager = UserWalletsListManager.provideBiometricImplementation( + context = context, + tangemSdkManager = tangemSdkManager, + ) + + store.dispatchWithMain(GlobalAction.UpdateUserWalletsListManager(manager)) + } + private fun dismiss(state: SaveWalletState) { if (state.backupInfo != null) { // TODO: Remove after onboarding refactoring 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 c3431d6d45..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 @@ -12,6 +12,7 @@ import com.tangem.blockchain.common.TransactionSender import com.tangem.blockchain.common.WalletManager import com.tangem.blockchain.extensions.SimpleResult import com.tangem.common.core.TangemSdkError +import com.tangem.common.extensions.guard import com.tangem.common.services.Result import com.tangem.core.analytics.Analytics import com.tangem.domain.common.CardDTO @@ -19,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 @@ -33,7 +35,6 @@ import com.tangem.tap.domain.TangemSigner import com.tangem.tap.domain.TapError import com.tangem.tap.domain.configurable.warningMessage.WarningMessage import com.tangem.tap.domain.extensions.minimalAmount -import com.tangem.tap.domain.tokens.models.BlockchainNetwork import com.tangem.tap.features.demo.DemoTransactionSender import com.tangem.tap.features.demo.isDemoCard import com.tangem.tap.features.send.redux.AddressPayIdActionUi @@ -61,6 +62,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.rekotlin.Action import org.rekotlin.Middleware +import timber.log.Timber import java.util.* /** @@ -233,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) { @@ -333,17 +337,16 @@ private fun updateWarnings(dispatch: (Action) -> Unit) { } private suspend fun updateWallet(walletManager: WalletManager) { - val selectedUserWallet = userWalletsListManager.selectedUserWalletSync - if (selectedUserWallet != null) { - val wallet = walletManager.wallet - walletCurrenciesManager.update( - userWallet = selectedUserWallet, - currency = Currency.Blockchain( - blockchain = wallet.blockchain, - derivationPath = wallet.publicKey.derivationPath?.rawPath, - ), - ) - } else { - store.dispatchOnMain(WalletAction.LoadWallet(BlockchainNetwork.fromWalletManager(walletManager))) + val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard { + Timber.e("Unable to update wallet, no user wallet selected") + return } + val wallet = walletManager.wallet + walletCurrenciesManager.update( + userWallet = selectedUserWallet, + currency = Currency.Blockchain( + blockchain = wallet.blockchain, + derivationPath = wallet.publicKey.derivationPath?.rawPath, + ), + ) } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt index d072b93e1f..9d741424b7 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt @@ -1,11 +1,11 @@ package com.tangem.tap.features.tokens.redux import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.DerivationParams import com.tangem.blockchain.common.DerivationStyle import com.tangem.common.CompletionResult import com.tangem.common.card.EllipticCurve import com.tangem.common.extensions.ByteArrayKey +import com.tangem.common.extensions.guard import com.tangem.common.extensions.toMapKey import com.tangem.common.flatMap import com.tangem.common.hdWallet.DerivationPath @@ -29,11 +29,8 @@ import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.common.redux.navigation.AppScreen import com.tangem.tap.common.redux.navigation.NavigationAction import com.tangem.tap.domain.TapError -import com.tangem.tap.domain.extensions.makeWalletManagerForApp import com.tangem.tap.domain.tokens.LoadAvailableCoinsService -import com.tangem.tap.domain.tokens.models.BlockchainNetwork import com.tangem.tap.features.wallet.models.Currency -import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.scope import com.tangem.tap.store import com.tangem.tap.tangemSdkManager @@ -43,6 +40,7 @@ import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.rekotlin.Middleware +import timber.log.Timber @Suppress("LargeClass") class TokensMiddleware { @@ -190,7 +188,7 @@ class TokensMiddleware { } } - fun deriveMissingBlockchains( + private fun deriveMissingBlockchains( scanResponse: ScanResponse, currencyList: List, onSuccess: (ScanResponse) -> Unit, @@ -271,77 +269,33 @@ class TokensMiddleware { private class DerivationData(val derivations: Pair>) private fun submitAdd(scanResponse: ScanResponse, currencyList: List) { - val selectedUserWallet = userWalletsListManager.selectedUserWalletSync - if (selectedUserWallet != null) { - scope.launch { - userWalletsListManager.update( - userWalletId = selectedUserWallet.walletId, - update = { userWallet -> - userWallet.copy(scanResponse = scanResponse) - }, - ) - .flatMap { updatedUserWallet -> - walletCurrenciesManager.addCurrencies( - userWallet = updatedUserWallet, - currenciesToAdd = currencyList, - ) - } - } - } else { - val factory = store.state.globalState.tapWalletManager.walletManagerFactory - val derivationStyle = scanResponse.card.derivationStyle - - val addActions = currencyList.mapIndexedNotNull { index, currency -> - when (currency) { - is Currency.Blockchain -> { - val derivationPath = currency.derivationPath?.let { DerivationPath(it) } - val derivationParams = derivationStyle?.let { - when (derivationPath) { - null -> DerivationParams.Default(derivationStyle) - else -> DerivationParams.Custom(derivationPath) - } - } - val walletManager = factory.makeWalletManagerForApp( - scanResponse = scanResponse, - blockchain = currency.blockchain, - derivationParams = derivationParams, - ) ?: return@mapIndexedNotNull null - WalletAction.MultiWallet.AddBlockchain( - blockchain = BlockchainNetwork.fromWalletManager(walletManager), - walletManager = walletManager, - save = index == currencyList.lastIndex, - ) - } - is Currency.Token -> { - val rawDerivationPath = currency.derivationPath - ?: currency.blockchain.derivationPath(derivationStyle)?.rawPath - val blockchainNetwork = - BlockchainNetwork(currency.blockchain, rawDerivationPath, listOf(currency.token)) - WalletAction.MultiWallet.AddToken( - token = currency.token, - blockchain = blockchainNetwork, - save = index == currencyList.lastIndex, - ) - } + val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard { + Timber.e("Unable to add currencies, no user wallet selected") + return + } + scope.launch { + userWalletsListManager.update( + userWalletId = selectedUserWallet.walletId, + update = { userWallet -> + userWallet.copy(scanResponse = scanResponse) + }, + ) + .flatMap { updatedUserWallet -> + walletCurrenciesManager.addCurrencies( + userWallet = updatedUserWallet, + currenciesToAdd = currencyList, + ) } - } - addActions.forEach { store.dispatchOnMain(it) } } } private suspend fun removeCurrenciesIfNeeded(currencies: List) { - when { - currencies.isEmpty() -> Unit - userWalletsListManager.hasSavedUserWallets -> { - walletCurrenciesManager.removeCurrencies( - userWallet = userWalletsListManager.selectedUserWalletSync!!, - currenciesToRemove = currencies, - ) - } - else -> { - store.dispatch(WalletAction.MultiWallet.RemoveWallets(currencies)) - } + if (currencies.isEmpty()) return + val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard { + Timber.e("Unable to remove currencies, no user wallet selected") + return } + walletCurrenciesManager.removeCurrencies(selectedUserWallet, currencies) } private fun isNeedToDerive(scanResponse: ScanResponse, currency: Currency): Boolean { diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletAction.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletAction.kt index f839eb03fe..390b337f30 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletAction.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletAction.kt @@ -2,12 +2,8 @@ package com.tangem.tap.features.wallet.redux import android.content.Context import com.tangem.blockchain.common.Amount -import com.tangem.blockchain.common.Token -import com.tangem.blockchain.common.Wallet -import com.tangem.blockchain.common.WalletManager import com.tangem.blockchain.common.address.AddressType import com.tangem.core.analytics.AnalyticsEvent -import com.tangem.domain.common.CardDTO import com.tangem.tap.common.entities.FiatCurrency import com.tangem.tap.common.redux.NotificationAction import com.tangem.tap.domain.TapError @@ -20,13 +16,12 @@ import com.tangem.tap.features.wallet.models.TotalBalance import com.tangem.tap.features.wallet.redux.models.WalletDialog import com.tangem.wallet.R import org.rekotlin.Action -import java.math.BigDecimal sealed class WalletAction : Action { - data class ResetState(val newCard: CardDTO) : WalletAction() + object PopBackToInitialScreen : WalletAction() - data class SetIfTestnetCard(val isTestnet: Boolean) : WalletAction() + data class UpdateCanSaveUserWallets(val canSaveUserWallets: Boolean) : WalletAction() object LoadData : WalletAction() { object Refresh : WalletAction() @@ -34,71 +29,14 @@ sealed class WalletAction : Action { data class Failure(val error: TapError?) : WalletAction() } - data class LoadWallet( - val blockchain: BlockchainNetwork? = null, - val walletManager: WalletManager? = null, - ) : WalletAction() { - data class Success(val wallet: Wallet, val blockchain: BlockchainNetwork) : WalletAction() - data class NoAccount( - val wallet: Wallet, - val blockchain: BlockchainNetwork, - val amountToCreateAccount: String, - ) : WalletAction() - - data class Failure(val wallet: Wallet, val errorMessage: String? = null) : WalletAction() - } - - data class SetArtworkId(val artworkId: String?) : WalletAction() - - sealed class UserTokens : WalletAction() { - object Loading : UserTokens() - object Loaded : UserTokens() - } - sealed class MultiWallet : WalletAction() { - data class SetIsMultiwalletAllowed(val isMultiwalletAllowed: Boolean) : MultiWallet() - - data class AddBlockchains( - val blockchains: List, - val walletManagers: List, - ) : MultiWallet() - - data class AddTokens( - val tokens: List, - val blockchain: BlockchainNetwork, - ) : MultiWallet() - - data class AddBlockchain( - val blockchain: BlockchainNetwork, - val walletManager: WalletManager?, - val save: Boolean, - ) : MultiWallet() - - data class AddToken( - val token: Token, - val blockchain: BlockchainNetwork, - val save: Boolean, - ) : MultiWallet() - - data class SaveCurrencies( - val blockchainNetworks: List, - val card: CardDTO? = null, - ) : MultiWallet() - - data class TokenLoaded( - val amount: Amount, - val token: Token, - val blockchain: BlockchainNetwork, - ) : MultiWallet() data class SelectWallet(val currency: Currency?) : MultiWallet() data class SetSingleWalletCurrency(val currency: Currency?) : MultiWallet() data class TryToRemoveWallet(val currency: Currency) : MultiWallet() data class RemoveWallet(val currency: Currency) : MultiWallet() - data class RemoveWallets(val currencies: List) : MultiWallet() - data class ShowWalletBackupWarning(val show: Boolean) : MultiWallet() object BackupWallet : MultiWallet() object ScheduleCheckForMissingDerivation : MultiWallet() data class AddMissingDerivations(val blockchains: List) : MultiWallet() @@ -125,24 +63,6 @@ sealed class WalletAction : Action { class CheckRemainingSignatures(val remainingSignatures: Int?) : Warnings() } - data class LoadFiatRate( - val wallet: Wallet? = null, - val coinsList: List? = null, - ) : WalletAction() { - data class Success( - val fiatRates: Map, - ) : WalletAction() - - object Failure : WalletAction() - } - - class LoadCardInfo(val card: CardDTO) : WalletAction() - - data class LoadArtwork(val card: CardDTO, val artworkId: String?) : WalletAction() { - data class Success(val artwork: Artwork) : WalletAction() - object Failure : WalletAction() - } - data class Scan(val onScanSuccessEvent: AnalyticsEvent?) : WalletAction() data class Send(val amount: Amount? = null) : WalletAction() @@ -179,7 +99,6 @@ sealed class WalletAction : Action { data class ExploreAddress(val exploreUrl: String, val context: Context) : WalletAction() object CreateWallet : WalletAction() - object EmptyWallet : WalletAction() object ChangeWallet : WalletAction() object ShowSaveWalletIfNeeded : WalletAction() @@ -202,14 +121,6 @@ sealed class WalletAction : Action { data class ChangeSelectedAddress(val type: AddressType) : WalletAction() - data class SetWalletRent( - val wallet: Wallet, - val minRent: String, - val rentExempt: String, - ) : WalletAction() - - data class RemoveWalletRent(val wallet: Wallet) : WalletAction() - sealed class AppCurrencyAction : WalletAction() { object ChooseAppCurrency : AppCurrencyAction() data class SelectAppCurrency(val fiatCurrency: FiatCurrency) : AppCurrencyAction() 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 b6ed172e1a..3dcbf37cb7 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 @@ -77,36 +77,34 @@ data class WalletData( if (currencyData.status == BalanceStatus.SameCurrencyTransactionInProgress) { walletWarnings.add(WalletWarning.TransactionInProgress(currency.currencyName)) } - } - - private fun assembleBlockchainWarnings(walletWarnings: MutableList) { - if (!currency.isBlockchain()) return - - if (existentialDepositString != null) { - val warning = WalletWarning.ExistentialDeposit( - currencyName = currency.currencyName, - edStringValueWithSymbol = "$existentialDepositString ${currency.currencySymbol}", - ) - walletWarnings.add(warning) - } if (walletRent != null) { walletWarnings.add(WalletWarning.Rent(walletRent)) } } - private fun assembleTokenWarnings(walletWarnings: MutableList) { - with(currency) { - if (!isToken()) return + private fun assembleBlockchainWarnings(walletWarnings: MutableList) = with(currency) { + if (!isBlockchain()) return - if (blockchainAmountIsEmpty() && !tokenAmountIsEmpty()) { - walletWarnings.add( - WalletWarning.BalanceNotEnoughForFee( - currencyName = currencyName, - blockchainFullName = blockchain.fullName, - blockchainSymbol = blockchain.currency, - ), - ) - } + if (existentialDepositString != null) { + val warning = WalletWarning.ExistentialDeposit( + currencyName = currencyName, + edStringValueWithSymbol = "$existentialDepositString $currencySymbol", + ) + walletWarnings.add(warning) + } + } + + private fun assembleTokenWarnings(walletWarnings: MutableList) = with(currency) { + if (!isToken()) return + + if (blockchainAmountIsEmpty() && !tokenAmountIsEmpty()) { + walletWarnings.add( + WalletWarning.BalanceNotEnoughForFee( + currencyName = currencyName, + blockchainFullName = blockchain.fullName, + blockchainSymbol = blockchain.currency, + ), + ) } } diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletState.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletState.kt index 73c49d69ef..81b306448b 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletState.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletState.kt @@ -2,8 +2,6 @@ package com.tangem.tap.features.wallet.redux import android.graphics.Bitmap import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.Token -import com.tangem.blockchain.common.Wallet import com.tangem.blockchain.common.WalletManager import com.tangem.blockchain.common.address.AddressType import com.tangem.tap.common.entities.Button @@ -11,16 +9,13 @@ import com.tangem.tap.common.extensions.toQrCode import com.tangem.tap.common.redux.global.CryptoCurrencyName import com.tangem.tap.common.toggleWidget.WidgetState import com.tangem.tap.domain.configurable.warningMessage.WarningMessage -import com.tangem.tap.domain.getFirstToken import com.tangem.tap.domain.tokens.models.BlockchainNetwork import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsState import com.tangem.tap.features.wallet.models.Currency import com.tangem.tap.features.wallet.models.TotalBalance -import com.tangem.tap.features.wallet.redux.reducers.calculateTotalFiatAmount import com.tangem.tap.features.wallet.redux.reducers.findProgressState import com.tangem.tap.features.wallet.ui.BalanceStatus import com.tangem.tap.store -import com.tangem.tap.userWalletsListManager import org.rekotlin.StateType import kotlin.properties.ReadOnlyProperty @@ -45,6 +40,7 @@ data class WalletState( val derivationsCheckIsScheduled: Boolean = false, val loadingUserTokens: Boolean = false, val walletCardsCount: Int? = null, + val canSaveUserWallets: Boolean = false, ) : StateType { val walletsDataFromStores: List @@ -87,12 +83,6 @@ data class WalletState( val primaryWalletData: WalletData? get() = primaryWalletStore?.walletsData?.firstOrNull() - val primaryBlockchain: Blockchain? - get() = primaryWalletManager?.wallet?.blockchain - - val primaryToken: Token? - get() = primaryWalletManager?.wallet?.getFirstToken() - val primaryTokenData: WalletData? get() = primaryWalletStore?.walletsData?.toMutableList() ?.apply { remove(primaryWalletData) } @@ -102,9 +92,6 @@ data class WalletState( primaryWalletData?.currencyData?.status != BalanceStatus.EmptyCard && primaryWalletData?.currencyData?.status != BalanceStatus.UnknownBlockchain - val hasSavedWallets: Boolean - get() = userWalletsListManager.hasSavedUserWallets - fun getWalletManager(currency: Currency?): WalletManager? { if (currency?.blockchain == null) return null return getWalletStore(currency)?.walletManager @@ -131,14 +118,7 @@ data class WalletState( } } - fun getWalletStore(wallet: Wallet?): WalletStore? { - if (wallet == null) return null - val currency = - Currency.Blockchain(wallet.blockchain, wallet.publicKey.derivationPath?.rawPath) - return getWalletStore(currency) - } - - fun getWalletStore(blockchainNetwork: BlockchainNetwork?): WalletStore? { + private fun getWalletStore(blockchainNetwork: BlockchainNetwork?): WalletStore? { if (blockchainNetwork == null) return null return walletsStores.firstOrNull { it.blockchainNetwork.derivationPath == blockchainNetwork.derivationPath && @@ -151,26 +131,12 @@ data class WalletState( return getWalletStore(currency)?.walletsData?.firstOrNull { it.currency == currency } } - fun replaceWalletStoreInWalletsStores(wallet: WalletStore?): List { - if (wallet == null) return walletsStores - var changed = false - val updatedWallets = walletsStores.map { - if (it.blockchainNetwork == wallet.blockchainNetwork) { - changed = true - wallet - } else { - it - } - } - return if (changed) updatedWallets else walletsStores + wallet - } - fun updateWalletData(walletData: WalletData?): WalletState { if (walletData == null) return this return updateWalletsData(listOf(walletData)) } - fun updateWalletsData(walletsData: List): WalletState { + private fun updateWalletsData(walletsData: List): WalletState { val walletStores = walletsData .map { BlockchainNetwork(it.currency.blockchain, it.currency.derivationPath, emptyList()) } .distinct().map { getWalletStore(it) }.mapNotNull { it?.updateWallets(walletsData) } @@ -178,12 +144,6 @@ data class WalletState( return updateWalletsStores(walletStores) } - fun updateWalletStore(walletStore: WalletStore?): WalletState { - return copy(walletsStores = replaceWalletStoreInWalletsStores(walletStore)) - .updateTotalBalance() - .updateProgressState() - } - private fun updateWalletsStores(walletStores: List): WalletState { val walletStoresMutable = walletStores.toMutableList() val updatedWallets = walletsStores.map { oldWalletStore -> @@ -198,54 +158,9 @@ data class WalletState( } } return copy(walletsStores = updatedWallets + walletStoresMutable) - .updateTotalBalance() .updateProgressState() } - fun removeWalletData(walletData: WalletData?): WalletState { - if (walletData == null) return this - return when (val currency = walletData.currency) { - is Currency.Blockchain -> { - val walletStores = walletsStores.filterNot { - it.blockchainNetwork.blockchain == currency.blockchain && - it.blockchainNetwork.derivationPath == currency.derivationPath - } - copy(walletsStores = walletStores) - .updateTotalBalance() - .updateProgressState() - } - is Currency.Token -> { - val walletStore = getWalletStore(walletData.currency) - val walletDataList = walletStore?.walletsData - ?.filterNot { it.currency == walletData.currency } - ?: emptyList() - val updatedWalletManager = walletStore?.walletManager?.also { it.removeToken(currency.token) } - val updatedWalletStore = walletStore?.copy( - walletsData = walletDataList, - walletManager = updatedWalletManager, - ) - updateWalletStore(updatedWalletStore) - } - } - } - - private fun updateTotalBalance(): WalletState { - val walletsData = this.walletsStores - .flatMap(WalletStore::walletsData) - - return if (walletsData.isNotEmpty()) { - this.copy( - totalBalance = TotalBalance( - state = walletsData.findProgressState(), - fiatAmount = walletsData.calculateTotalFiatAmount(), - fiatCurrency = store.state.globalState.appCurrency, - ), - ) - } else { - this.copy(totalBalance = null) - } - } - private fun updateProgressState(): WalletState { val walletsData = this.walletsStores .flatMap(WalletStore::walletsData) @@ -267,21 +182,6 @@ data class WalletState( } } -fun List.replaceSomeWalletsData(newWallets: List): List { - val remainingWallets: MutableList = newWallets.toMutableList() - val updatedWallets = this.map { wallet -> - val newWallet = newWallets - .firstOrNull { wallet.currency == it.currency } - if (newWallet == null) { - wallet - } else { - remainingWallets.remove(newWallet) - newWallet - } - } - return updatedWallets + remainingWallets -} - enum class ProgressState : WidgetState { Loading, Refreshing, Done, Error } enum class ErrorType { NoInternetConnection } diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/AppCurrencyMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/AppCurrencyMiddleware.kt index a2b7aa110e..974ef88df8 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/AppCurrencyMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/AppCurrencyMiddleware.kt @@ -1,7 +1,8 @@ package com.tangem.tap.features.wallet.redux.middlewares -import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse +import com.tangem.common.extensions.guard import com.tangem.core.analytics.Analytics +import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.MainScreen import com.tangem.tap.common.entities.FiatCurrency @@ -18,6 +19,7 @@ import com.tangem.tap.scope import com.tangem.tap.store import com.tangem.tap.userWalletsListManager import kotlinx.coroutines.launch +import timber.log.Timber class AppCurrencyMiddleware( private val walletRepository: WalletRepository, @@ -66,14 +68,12 @@ class AppCurrencyMiddleware( store.dispatch(GlobalAction.ChangeAppCurrency(action.fiatCurrency)) store.dispatch(DetailsAction.ChangeAppCurrency(action.fiatCurrency)) store.dispatch(WalletSelectorAction.ChangeAppCurrency(action.fiatCurrency)) - val selectedUserWallet = userWalletsListManager.selectedUserWalletSync - if (selectedUserWallet != null) { - scope.launch { - tapWalletManager.loadData(selectedUserWallet, refresh = true) - } - } else { - tapWalletManager.rates.clear() - store.dispatch(WalletAction.LoadFiatRate()) + val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard { + Timber.e("Unable to select currency, no user wallet selected") + return + } + scope.launch { + tapWalletManager.loadData(selectedUserWallet, refresh = true) } } diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt index 2749e3db13..2642cbeb50 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt @@ -1,8 +1,5 @@ package com.tangem.tap.features.wallet.redux.middlewares -import com.tangem.blockchain.common.AmountType -import com.tangem.blockchain.common.Token -import com.tangem.blockchain.common.WalletManager import com.tangem.common.doOnSuccess import com.tangem.common.extensions.guard import com.tangem.common.flatMap @@ -13,23 +10,14 @@ import com.tangem.tap.common.extensions.addContext import com.tangem.tap.common.extensions.dispatchDialogShow import com.tangem.tap.common.extensions.dispatchErrorNotification import com.tangem.tap.common.extensions.dispatchOnMain -import com.tangem.tap.common.extensions.safeUpdate import com.tangem.tap.common.redux.global.GlobalAction -import com.tangem.tap.common.redux.global.GlobalState import com.tangem.tap.common.redux.navigation.AppScreen import com.tangem.tap.common.redux.navigation.NavigationAction import com.tangem.tap.domain.TapError -import com.tangem.tap.domain.extensions.makeWalletManagerForApp import com.tangem.tap.domain.model.UserWallet -import com.tangem.tap.domain.tokens.models.BlockchainNetwork -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.toCurrencies import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.features.wallet.redux.WalletState import com.tangem.tap.features.wallet.redux.models.WalletDialog -import com.tangem.tap.features.wallet.redux.reducers.toWallet import com.tangem.tap.scope import com.tangem.tap.store import com.tangem.tap.tangemSdkManager @@ -38,69 +26,20 @@ import com.tangem.tap.userWalletsListManager import com.tangem.tap.walletCurrenciesManager import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import java.math.BigDecimal +import timber.log.Timber class MultiWalletMiddleware { @Suppress("LongMethod", "ComplexMethod") fun handle( action: WalletAction.MultiWallet, walletState: WalletState?, - globalState: GlobalState?, ) { - val globalState = globalState ?: return - when (action) { - is WalletAction.MultiWallet.AddBlockchains -> { - handleAddingWalletManagers(globalState, action.walletManagers) - } is WalletAction.MultiWallet.SelectWallet -> { if (action.currency != null) { store.dispatch(NavigationAction.NavigateTo(AppScreen.WalletDetails)) } } - is WalletAction.MultiWallet.AddToken -> { - addTokens(listOf(action.token), action.blockchain, walletState, globalState, action.save) - } - is WalletAction.MultiWallet.AddTokens -> { - addTokens(action.tokens, action.blockchain, walletState, globalState, save = false) - } - is WalletAction.MultiWallet.AddBlockchain -> { - action.walletManager?.let { - handleAddingWalletManagers(globalState, listOf(action.walletManager)) - } - val currencies: List = - (walletState?.currencies ?: emptyList()) + action.blockchain.toCurrencies() - - if (action.save && globalState.scanResponse != null) { - scope.launch { - userTokensRepository.saveUserTokens( - card = globalState.scanResponse.card, - tokens = currencies, - ) - } - } - - store.dispatch( - WalletAction.LoadFiatRate( - coinsList = listOf( - Currency.Blockchain( - action.blockchain.blockchain, - action.blockchain.derivationPath, - ), - ), - ), - ) - store.dispatch( - WalletAction.LoadWallet( - blockchain = action.blockchain, - walletManager = action.walletManager, - ), - ) - } - is WalletAction.MultiWallet.SaveCurrencies -> { - val card = action.card ?: globalState.scanResponse?.card ?: return - scope.launch { userTokensRepository.saveUserTokens(card, action.blockchainNetworks.toCurrencies()) } - } is WalletAction.MultiWallet.TryToRemoveWallet -> { val currency = action.currency val walletManager = walletState?.getWalletManager(currency).guard { @@ -130,60 +69,36 @@ class MultiWalletMiddleware { } } is WalletAction.MultiWallet.RemoveWallet -> { - val selectedUserWallet = userWalletsListManager.selectedUserWalletSync - if (selectedUserWallet != null) { - scope.launch { - walletCurrenciesManager.removeCurrency( - userWallet = selectedUserWallet, - currencyToRemove = action.currency, - ) - } - } else { - val currency = action.currency - val card = globalState.scanResponse?.card.guard { - store.dispatchErrorNotification(TapError.UnsupportedState("card is NULL")) - store.dispatch(NavigationAction.PopBackTo(AppScreen.Home)) - return - } - var currencies = walletState?.currencies ?: emptyList() - currencies = currencies.filterNot { it == currency } - if (currency.isBlockchain()) { - currencies.filter { - it.blockchain == currency.blockchain && it.derivationPath == currency.derivationPath - } - } - scope.launch { userTokensRepository.saveUserTokens(card, currencies) } - } - } - is WalletAction.MultiWallet.RemoveWallets -> { - val card = globalState.scanResponse?.card.guard { - store.dispatchErrorNotification(TapError.UnsupportedState("card is NULL")) - store.dispatch(NavigationAction.PopBackTo(AppScreen.Home)) + val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard { + Timber.e("Unable to remove wallet, no user wallet selected") return } - var currencies = walletState?.currencies ?: emptyList() - currencies = currencies.filterNot { action.currencies.contains(it) } - scope.launch { userTokensRepository.saveUserTokens(card, currencies) } - store.dispatch(WalletAction.MultiWallet.SelectWallet(null)) - } - is WalletAction.MultiWallet.ShowWalletBackupWarning -> Unit - is WalletAction.MultiWallet.BackupWallet -> { - store.state.globalState.scanResponse?.let { - Analytics.addContext(it) - store.dispatch(GlobalAction.Onboarding.Start(it, canSkipBackup = false)) - store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingWallet)) + scope.launch { + walletCurrenciesManager.removeCurrency( + userWallet = selectedUserWallet, + currencyToRemove = action.currency, + ) } } + is WalletAction.MultiWallet.BackupWallet -> { + val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard { + Timber.e("Unable to backup wallet, no user wallet selected") + return + } + val scanResponse = selectedUserWallet.scanResponse + Analytics.addContext(scanResponse) + store.dispatch(GlobalAction.Onboarding.Start(scanResponse, canSkipBackup = false)) + store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingWallet)) + } is WalletAction.MultiWallet.AddMissingDerivations -> { scope.launch { handleBasicAnalyticsEvent() } } is WalletAction.MultiWallet.ScanToGetDerivations -> { - val selectedWallet = userWalletsListManager.selectedUserWalletSync - if (selectedWallet != null) { - scanAndUpdateCard(selectedWallet, walletState) - } else { - store.dispatch(WalletAction.Scan(onScanSuccessEvent = null)) + val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard { + Timber.e("Unable to scan to get derivations, no user wallet selected") + return } + scanAndUpdateCard(selectedUserWallet, walletState) } else -> {} } @@ -215,86 +130,4 @@ class MultiWalletMiddleware { store.state.globalState.tapWalletManager.loadData(updatedUserWallet, refresh = true) } } - - private fun addDummyBalances(walletManagers: List) { - walletManagers.forEach { - if (it.wallet.fundsAvailable(AmountType.Coin) == BigDecimal.ZERO) { - DemoHelper.injectDemoBalance(it) - } - } - } - - private fun handleAddingWalletManagers( - globalState: GlobalState, - walletManagers: List, - ) { - globalState.feedbackManager?.infoHolder?.setWalletsInfo(walletManagers) - if (globalState.scanResponse?.isDemoCard() == true) { - addDummyBalances(walletManagers) - } - } - - private fun addTokens( - tokens: List, - blockchainNetwork: BlockchainNetwork, - walletState: WalletState?, - globalState: GlobalState?, - save: Boolean, - ) { - if (tokens.isEmpty()) return - val scanResponse = globalState?.scanResponse ?: return - val wmFactory = globalState.tapWalletManager.walletManagerFactory - val walletState = walletState ?: return - val walletManager = walletState.getWalletManager(blockchainNetwork)?.also { - if (save) { - val wallets = tokens.mapNotNull { token -> token.toWallet(walletState, blockchainNetwork) } - val currencies = walletState.updateWalletsData(wallets).currencies - scope.launch { userTokensRepository.saveUserTokens(scanResponse.card, currencies) } - } - } ?: wmFactory.makeWalletManagerForApp(scanResponse, blockchainNetwork)?.also { - store.dispatchOnMain( - WalletAction.MultiWallet.AddBlockchain( - blockchain = blockchainNetwork.updateTokens(tokens), - walletManager = it, - save = save, - ), - ) - } - - store.dispatchOnMain( - WalletAction.LoadFiatRate( - coinsList = tokens.map { token -> - Currency.Token( - token, - blockchainNetwork.blockchain, - blockchainNetwork.derivationPath, - ) - }, - ), - ) - if (tokens.isNotEmpty()) walletManager?.addTokens(tokens) - - scope.launch { - when (val result = walletManager?.safeUpdate()) { - is com.tangem.common.services.Result.Success -> { - val wallet = result.data - wallet.getTokens() - .filter { tokens.contains(it) } - .mapNotNull { token -> - wallet.getTokenAmount(token)?.let { amount -> Pair(token, amount) } - } - .forEach { (token, tokenAmount) -> - store.dispatchOnMain( - WalletAction.MultiWallet.TokenLoaded( - amount = tokenAmount, - token = token, - blockchain = blockchainNetwork, - ), - ) - } - } - else -> Unit - } - } - } } \ No newline at end of file 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/features/wallet/redux/middlewares/WalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt index 41a28fa9c9..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 @@ -1,17 +1,11 @@ package com.tangem.tap.features.wallet.redux.middlewares import com.google.firebase.crashlytics.FirebaseCrashlytics -import com.tangem.blockchain.blockchains.solana.RentProvider import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.AmountType -import com.tangem.blockchain.common.WalletManager import com.tangem.common.CompletionResult -import com.tangem.common.extensions.isZero -import com.tangem.common.services.Result +import com.tangem.common.extensions.guard import com.tangem.core.analytics.Analytics -import com.tangem.domain.common.extensions.withMainContext -import com.tangem.operations.attestation.Attestation -import com.tangem.operations.attestation.OnlineCardVerifier import com.tangem.tap.common.analytics.converters.BasicEventsPreChecker import com.tangem.tap.common.analytics.converters.BasicEventsSourceData import com.tangem.tap.common.analytics.events.AnalyticsParam @@ -19,32 +13,24 @@ import com.tangem.tap.common.analytics.events.Basic import com.tangem.tap.common.analytics.events.MainScreen import com.tangem.tap.common.analytics.events.Token import com.tangem.tap.common.extensions.copyToClipboard -import com.tangem.tap.common.extensions.dispatchDebugErrorNotification import com.tangem.tap.common.extensions.dispatchErrorNotification import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.dispatchOpenUrl import com.tangem.tap.common.extensions.dispatchToastNotification import com.tangem.tap.common.extensions.isGreaterThan -import com.tangem.tap.common.extensions.onCardScanned import com.tangem.tap.common.extensions.shareText -import com.tangem.tap.common.extensions.stripZeroPlainString import com.tangem.tap.common.redux.AppState -import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.common.redux.navigation.AppScreen import com.tangem.tap.common.redux.navigation.NavigationAction import com.tangem.tap.domain.TapError -import com.tangem.tap.domain.failedRates -import com.tangem.tap.domain.loadedRates import com.tangem.tap.domain.model.WalletDataModel import com.tangem.tap.domain.model.WalletStoreModel import com.tangem.tap.domain.model.builders.UserWalletIdBuilder +import com.tangem.tap.domain.userWalletList.lockIfLockable import com.tangem.tap.features.demo.DemoHelper import com.tangem.tap.features.home.redux.HomeAction import com.tangem.tap.features.send.redux.PrepareSendScreen import com.tangem.tap.features.wallet.models.Currency -import com.tangem.tap.features.wallet.models.PendingTransactionType -import com.tangem.tap.features.wallet.models.filterByCoin -import com.tangem.tap.features.wallet.models.getPendingTransactions import com.tangem.tap.features.wallet.models.getSendableAmounts import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.features.wallet.redux.WalletData @@ -63,8 +49,6 @@ import com.tangem.tap.userWalletsListManagerSafe import com.tangem.tap.walletStoresManager import com.tangem.wallet.R import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.rekotlin.Action @@ -107,97 +91,26 @@ class WalletMiddleware { when (action) { is WalletAction.TradeCryptoAction -> tradeCryptoMiddleware.handle(state, action) is WalletAction.Warnings -> warningsMiddleware.handle(action, globalState) - is WalletAction.MultiWallet -> multiWalletMiddleware.handle(action, walletState, globalState) + is WalletAction.MultiWallet -> multiWalletMiddleware.handle(action, walletState) is WalletAction.AppCurrencyAction -> appCurrencyMiddleware.handle(action) is WalletAction.DialogAction -> walletDialogMiddleware.handle(action) - is WalletAction.LoadWallet -> { - scope.launch { - if (action.blockchain == null) { - walletState.walletManagers.map { walletManager -> - async { globalState.tapWalletManager.loadWalletData(walletManager) } - }.awaitAll() - handleBasicAnalyticsEvent() - } else { - val walletManager = walletState.getWalletManager(action.blockchain) - ?: action.walletManager - walletManager?.let { globalState.tapWalletManager.loadWalletData(it) } - } - } - } - is WalletAction.LoadWallet.Success -> { - checkForRentWarning(walletState.getWalletManager(action.blockchain)) - val coinAmount = action.wallet.amounts[AmountType.Coin]?.value - if (coinAmount?.isZero() == false && walletState.getWalletData(action.blockchain) == null) { - store.dispatch( - WalletAction.MultiWallet.AddBlockchain( - blockchain = action.blockchain, - walletManager = null, - save = true, - ), - ) - store.dispatch(WalletAction.LoadWallet.Success(action.wallet, action.blockchain)) - } - store.dispatch(WalletAction.Warnings.CheckHashesCount.CheckHashesCountOnline) - warningsMiddleware.tryToShowAppRatingWarning(action.wallet) - } - is WalletAction.LoadFiatRate -> { - val appCurrencyId = globalState.appCurrency.code - scope.launch { - val coinsList = when { - action.wallet != null -> { - val wallet = action.wallet - wallet.getTokens() - .map { Currency.Token(it, wallet.blockchain, wallet.publicKey.derivationPath?.rawPath) } - .plus(Currency.Blockchain(wallet.blockchain, wallet.publicKey.derivationPath?.rawPath)) - } - action.coinsList != null -> action.coinsList - else -> { - if (walletState.isMultiwalletAllowed) { - walletState.walletsDataFromStores.map { it.currency } - } else { - val derivationPath = walletState.primaryWalletData?.currency?.derivationPath - val primaryBlockchain = walletState.primaryBlockchain - val primaryToken = walletState.primaryToken - listOfNotNull( - primaryBlockchain?.let { Currency.Blockchain(it, derivationPath) }, - primaryToken?.let { Currency.Token(it, primaryBlockchain!!, derivationPath) }, - ) - } - } - } - val ratesResult = globalState.tapWalletManager.rates.loadFiatRate( - currencyId = appCurrencyId, - coinsList = coinsList, - ) - when (ratesResult) { - is Result.Success -> { - ratesResult.data.loadedRates.let { - dispatchOnMain(WalletAction.LoadFiatRate.Success(it)) - } - ratesResult.data.failedRates.forEach { (currency, throwable) -> - Timber.e( - throwable, - "Loading rates failed for [%s]", - currency.currencySymbol, - ) - } - } - is Result.Failure -> { - store.dispatchDebugErrorNotification("LoadFiatRate.Failure") - dispatchOnMain(WalletAction.LoadFiatRate.Failure) - } - } - } - } is WalletAction.CreateWallet -> { scope.launch { - val result = tangemSdkManager.createWallet(globalState.scanResponse?.card?.cardId) - when (result) { + when (val result = tangemSdkManager.createWallet(globalState.scanResponse?.card?.cardId)) { is CompletionResult.Success -> { - val scanResponse = globalState.scanResponse?.copy(card = result.data) - scanResponse?.let { store.onCardScanned(scanResponse) } + val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard { + Timber.e("Unable to create wallet, no user wallet selected") + return@launch + } + userWalletsListManager.update(selectedUserWallet.walletId) { userWallet -> + userWallet.copy( + scanResponse = userWallet.scanResponse.copy( + card = result.data, + ), + ) + } } - is CompletionResult.Failure -> {} + is CompletionResult.Failure -> Unit } } } @@ -208,58 +121,29 @@ class WalletMiddleware { store.dispatchOnMain(HomeAction.ReadCard(action.onScanSuccessEvent)) } } - is WalletAction.LoadCardInfo -> { - val attestationFailed = action.card.attestation.status == Attestation.Status.Failed - store.dispatchOnMain(GlobalAction.SetIfCardVerifiedOnline(!attestationFailed)) - - scope.launch { - val response = OnlineCardVerifier().getCardInfo(action.card.cardId, action.card.cardPublicKey) - when (response) { - is Result.Success -> { - val actionList = listOf( - WalletAction.SetArtworkId(response.data.artwork?.id), - WalletAction.LoadArtwork(action.card, response.data.artwork?.id), - ) - withMainContext { actionList.forEach { store.dispatch(it) } } - } - is Result.Failure -> {} - } - store.dispatchOnMain(WalletAction.Warnings.CheckIfNeeded) - } - } is WalletAction.LoadData, is WalletAction.LoadData.Refresh, -> { - val selectedWallet = userWalletsListManager.selectedUserWalletSync + val selectedWallet = userWalletsListManager.selectedUserWalletSync.guard { + Timber.e("Unable to load/refresh wallets data, no user wallet selected") + return + } scope.launch { - if (selectedWallet != null) { - globalState.tapWalletManager.loadData( - userWallet = selectedWallet, - refresh = action is WalletAction.LoadData.Refresh, - ) - } else { - val scanResponse = globalState.scanResponse ?: return@launch - - if (walletState.walletsDataFromStores.isNotEmpty()) { - globalState.tapWalletManager.reloadData(scanResponse) - } else { - globalState.tapWalletManager.loadData(scanResponse) - } - } + globalState.tapWalletManager.loadData( + userWallet = selectedWallet, + refresh = action is WalletAction.LoadData.Refresh, + ) } } is NetworkStateChanged -> { store.dispatch(WalletAction.Warnings.CheckHashesCount.CheckHashesCountOnline) if (!action.isOnline) return - val selectedUserWallet = userWalletsListManagerSafe?.selectedUserWalletSync - if (selectedUserWallet != null) { - scope.launch { globalState.tapWalletManager.loadData(selectedUserWallet) } - } else { - globalState.scanResponse?.let { scanNoteResponse -> - scope.launch { globalState.tapWalletManager.loadData(scanNoteResponse) } - } + val selectedUserWallet = userWalletsListManagerSafe?.selectedUserWalletSync.guard { + Timber.e("Unable to proceed with changed network state, no user wallet selected") + return } + scope.launch { globalState.tapWalletManager.loadData(selectedUserWallet, refresh = true) } } is WalletAction.CopyAddress -> { Analytics.send(Token.Receive.ButtonCopyAddress()) @@ -300,7 +184,7 @@ class WalletMiddleware { showSaveWalletIfNeeded() } is WalletAction.ChangeWallet -> { - changeWallet() + changeWallet(walletState) } is WalletAction.UserWalletChanged -> Unit is WalletAction.WalletStoresChanged -> { @@ -311,6 +195,16 @@ class WalletMiddleware { tryToShowAppRatingWarning(action.walletStores) } is WalletAction.TotalFiatBalanceChanged -> Unit + is WalletAction.PopBackToInitialScreen -> { + userWalletsListManager.lockIfLockable() + val screen = if (walletState.canSaveUserWallets) { + AppScreen.Welcome + } else { + AppScreen.Home + } + + store.dispatchOnMain(NavigationAction.PopBackTo(screen)) + } } } @@ -375,9 +269,9 @@ class WalletMiddleware { } } - private fun changeWallet() { + private fun changeWallet(state: WalletState) { when { - userWalletsListManager.hasSavedUserWallets -> { + state.canSaveUserWallets -> { Analytics.send(MainScreen.ButtonMyWallets()) store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.WalletSelector)) } @@ -457,48 +351,6 @@ class WalletMiddleware { tokenRate = tokenRate, ) } - - private fun checkForRentWarning(walletManager: WalletManager?) { - val rentProvider = walletManager as? RentProvider ?: return - - scope.launch { - when (val result = rentProvider.minimalBalanceForRentExemption()) { - is com.tangem.blockchain.extensions.Result.Success -> { - fun isNeedToShowWarning(balance: BigDecimal, rentExempt: BigDecimal): Boolean { - return balance < rentExempt - } - - val balance = walletManager.wallet.fundsAvailable(AmountType.Coin) - val outgoingTxs = walletManager.wallet.getPendingTransactions( - PendingTransactionType.Outgoing, - ).filterByCoin() - - val rentExempt = result.data - val show = if (outgoingTxs.isEmpty()) { - isNeedToShowWarning(balance, rentExempt) - } else { - val outgoingAmount = outgoingTxs.sumOf { it.amountValue ?: BigDecimal.ZERO } - val rest = balance.minus(outgoingAmount) - isNeedToShowWarning(rest, rentExempt) - } - - val currency = walletManager.wallet.blockchain.currency - if (show) { - dispatchOnMain( - WalletAction.SetWalletRent( - wallet = walletManager.wallet, - minRent = "${rentProvider.rentAmount().stripZeroPlainString()} $currency", - rentExempt = "${rentExempt.stripZeroPlainString()} $currency", - ), - ) - } else { - dispatchOnMain(WalletAction.RemoveWalletRent(walletManager.wallet)) - } - } - is com.tangem.blockchain.extensions.Result.Failure -> {} - } - } - } } suspend fun handleBasicAnalyticsEvent() { diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/MultiWalletReducer.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/MultiWalletReducer.kt index 1acc7600fd..2dcfeaf198 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/MultiWalletReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/MultiWalletReducer.kt @@ -1,173 +1,13 @@ package com.tangem.tap.features.wallet.redux.reducers -import com.google.firebase.crashlytics.FirebaseCrashlytics -import com.tangem.blockchain.blockchains.polkadot.ExistentialDepositProvider -import com.tangem.blockchain.common.AmountType -import com.tangem.blockchain.common.Token -import com.tangem.blockchain.common.WalletManager -import com.tangem.tap.common.extensions.dispatchOnMain -import com.tangem.tap.common.extensions.dispatchToastNotification -import com.tangem.tap.common.extensions.getBlockchainTxHistory -import com.tangem.tap.common.extensions.getTokenTxHistory -import com.tangem.tap.common.extensions.toFiatString -import com.tangem.tap.common.extensions.toFormattedCurrencyString -import com.tangem.tap.common.redux.navigation.AppScreen -import com.tangem.tap.common.redux.navigation.NavigationAction -import com.tangem.tap.domain.tokens.models.BlockchainNetwork -import com.tangem.tap.features.wallet.models.Currency -import com.tangem.tap.features.wallet.models.WalletRent -import com.tangem.tap.features.wallet.models.filterByToken -import com.tangem.tap.features.wallet.models.getPendingTransactions -import com.tangem.tap.features.wallet.models.removeUnknownTransactions import com.tangem.tap.features.wallet.redux.ProgressState import com.tangem.tap.features.wallet.redux.WalletAction -import com.tangem.tap.features.wallet.redux.WalletData -import com.tangem.tap.features.wallet.redux.WalletMainButton import com.tangem.tap.features.wallet.redux.WalletState -import com.tangem.tap.features.wallet.redux.WalletState.Companion.UNKNOWN_AMOUNT_SIGN -import com.tangem.tap.features.wallet.redux.WalletStore -import com.tangem.tap.features.wallet.ui.BalanceStatus -import com.tangem.tap.features.wallet.ui.BalanceWidgetData -import com.tangem.tap.store -import com.tangem.tap.userWalletsListManager -import com.tangem.wallet.R -import java.math.BigDecimal class MultiWalletReducer { @Suppress("LongMethod", "ComplexMethod") fun reduce(action: WalletAction.MultiWallet, state: WalletState): WalletState { return when (action) { - is WalletAction.MultiWallet.AddBlockchains -> { - val walletStores: List = action.blockchains.map { blockchain -> - val walletManager = action.walletManagers.firstOrNull { - it.wallet.blockchain == blockchain.blockchain && - it.wallet.publicKey.derivationPath?.rawPath == blockchain.derivationPath - } - val wallet = walletManager?.wallet - val walletData = WalletData( - currencyData = BalanceWidgetData( - status = BalanceStatus.Loading, - currency = blockchain.blockchain.fullName, - currencySymbol = blockchain.blockchain.currency, - ), - walletAddresses = createAddressList(wallet), - mainButton = WalletMainButton.SendButton(false), - currency = Currency.Blockchain( - blockchain.blockchain, - blockchain.derivationPath, - ), - existentialDepositString = getExistentialDeposit(walletManager), - historyTransactions = walletManager?.getBlockchainTxHistory(), - ) - - WalletStore( - walletManager = walletManager, - blockchainNetwork = blockchain, - walletsData = listOf(walletData), - ) - } - - state.copy( - walletsStores = walletStores, - selectedCurrency = findSelectedCurrency( - walletsStores = walletStores, - currentSelectedCurrency = state.selectedCurrency, - isMultiWalletAllowed = state.isMultiwalletAllowed, - ), - ) - } - is WalletAction.MultiWallet.AddBlockchain -> { - val walletManager = action.walletManager ?: state.getWalletManager(action.blockchain) - val wallet = walletManager?.wallet - - val walletData = WalletData( - currencyData = BalanceWidgetData( - status = BalanceStatus.Loading, - currency = action.blockchain.blockchain.fullName, - currencySymbol = action.blockchain.blockchain.currency, - ), - walletAddresses = createAddressList(wallet), - mainButton = WalletMainButton.SendButton(false), - currency = Currency.Blockchain( - action.blockchain.blockchain, - action.blockchain.derivationPath, - ), - existentialDepositString = getExistentialDeposit(walletManager), - historyTransactions = walletManager?.getBlockchainTxHistory(), - ) - val walletStore = WalletStore( - walletManager = walletManager, - blockchainNetwork = action.blockchain, - walletsData = listOf(walletData), - ) - - val newState = state.updateWalletStore(walletStore) - if (wallet != null && wallet.amounts[AmountType.Coin]?.value != null) { - OnWalletLoadedReducer().reduce(wallet, action.blockchain, newState) - } else { - newState - } - } - is WalletAction.MultiWallet.AddTokens -> addTokens(action.tokens, action.blockchain, state) - is WalletAction.MultiWallet.AddToken -> addTokens(listOf(action.token), action.blockchain, state) - is WalletAction.MultiWallet.TokenLoaded -> { - val currency = Currency.fromBlockchainNetwork(action.blockchain, action.token) - val walletManager = state.getWalletManager(currency) - if (walletManager == null) { - val screen = if (userWalletsListManager.hasSavedUserWallets) { - AppScreen.Welcome - } else { - AppScreen.Home - } - store.dispatchOnMain(NavigationAction.PopBackTo(screen)) - - FirebaseCrashlytics.getInstance().recordException( - IllegalStateException("MultiWallet.TokenLoaded: walletManager is null"), - ) - - store.dispatchToastNotification(R.string.internal_error_wallet_manager_not_found) - return state - } - val wallet = walletManager.wallet - val pendingTransactions = wallet.getPendingTransactions() - val tokenPendingTransactions = pendingTransactions.filterByToken(action.token) - val tokenBalanceStatus = when { - tokenPendingTransactions.isNotEmpty() -> BalanceStatus.TransactionInProgress - pendingTransactions.isNotEmpty() -> BalanceStatus.SameCurrencyTransactionInProgress - else -> BalanceStatus.VerifiedOnline - } - val tokenWalletData = state.getWalletData(currency) - val isTokenSendButtonEnabled = tokenWalletData?.shouldEnableTokenSendButton() == true && - pendingTransactions.isEmpty() - - val newTokenWalletData = tokenWalletData?.copy( - currencyData = tokenWalletData.currencyData.copy( - status = tokenBalanceStatus, - amount = action.amount.value, - amountFormatted = action.amount.value?.toFormattedCurrencyString( - decimals = action.amount.decimals, - currency = action.amount.currencySymbol, - ), - fiatAmountFormatted = tokenWalletData.fiatRate?.let { - action.amount.value?.toFiatString(it, store.state.globalState.appCurrency.symbol) - } ?: UNKNOWN_AMOUNT_SIGN, - blockchainAmount = wallet.amounts[AmountType.Coin]?.value ?: BigDecimal.ZERO, - ), - pendingTransactions = pendingTransactions.removeUnknownTransactions(), - mainButton = WalletMainButton.SendButton(isTokenSendButtonEnabled), - currency = Currency.Token( - token = action.token, - blockchain = action.blockchain.blockchain, - derivationPath = action.blockchain.derivationPath, - ), - walletRent = findWalletRent(state.getWalletStore(walletManager.wallet)), - historyTransactions = walletManager.getTokenTxHistory(action.token), - ) - state.updateWalletData(newTokenWalletData) - } - is WalletAction.MultiWallet.SetIsMultiwalletAllowed -> - state.copy(isMultiwalletAllowed = action.isMultiwalletAllowed) - is WalletAction.MultiWallet.SelectWallet -> { state.copy(selectedCurrency = action.currency) } @@ -175,16 +15,6 @@ class MultiWalletReducer { state.copy(selectedCurrency = action.currency) } is WalletAction.MultiWallet.TryToRemoveWallet -> state - is WalletAction.MultiWallet.RemoveWallet -> { - state.removeWalletData(state.getWalletData(action.currency)) - } - is WalletAction.MultiWallet.RemoveWallets -> { - var updatedState = state - action.currencies.forEach { updatedState = updatedState.removeWalletData(state.getWalletData(it)) } - updatedState - } - is WalletAction.MultiWallet.SaveCurrencies -> state - is WalletAction.MultiWallet.ShowWalletBackupWarning -> state.copy(showBackupWarning = action.show) is WalletAction.MultiWallet.ScheduleCheckForMissingDerivation -> state.copy( derivationsCheckIsScheduled = true, ) @@ -194,39 +24,7 @@ class MultiWalletReducer { ) is WalletAction.MultiWallet.BackupWallet -> state is WalletAction.MultiWallet.ScanToGetDerivations -> state.copy(state = ProgressState.Loading) + else -> state } } - - private fun findWalletRent(walletStore: WalletStore?): WalletRent? { - return walletStore?.walletsData?.firstOrNull { it.walletRent != null }?.walletRent - } - - private fun getExistentialDeposit(walletManager: WalletManager?): String? { - return (walletManager as? ExistentialDepositProvider)?.getExistentialDeposit()?.toPlainString() - } - - private fun addTokens(tokens: List, blockchain: BlockchainNetwork, state: WalletState): WalletState { - val wallets = tokens.mapNotNull { token -> token.toWallet(state, blockchain) } - return state.updateWalletsData(wallets) - } -} - -fun Token.toWallet(state: WalletState, blockchain: BlockchainNetwork): WalletData? { - val currency = Currency.fromBlockchainNetwork(blockchain, this) - if (state.currencies.contains(currency)) return null - - val walletManager = state.getWalletManager(currency) - val walletAddresses = createAddressList(walletManager?.wallet) - - return WalletData( - currencyData = BalanceWidgetData( - status = BalanceStatus.Loading, - currency = this.name, - currencySymbol = this.symbol, - ), - walletAddresses = walletAddresses, - mainButton = WalletMainButton.SendButton(false), - currency = currency, - historyTransactions = walletManager?.getTokenTxHistory(this), - ) } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/OnWalletLoadedReducer.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/OnWalletLoadedReducer.kt deleted file mode 100644 index 086fe57a83..0000000000 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/OnWalletLoadedReducer.kt +++ /dev/null @@ -1,154 +0,0 @@ -package com.tangem.tap.features.wallet.redux.reducers - -import com.tangem.blockchain.common.AmountType -import com.tangem.blockchain.common.Wallet -import com.tangem.common.extensions.isZero -import com.tangem.tap.common.extensions.toFiatValue -import com.tangem.tap.common.extensions.toFormattedCurrencyString -import com.tangem.tap.common.extensions.toFormattedFiatValue -import com.tangem.tap.domain.tokens.models.BlockchainNetwork -import com.tangem.tap.features.wallet.models.Currency -import com.tangem.tap.features.wallet.models.filterByToken -import com.tangem.tap.features.wallet.models.getPendingTransactions -import com.tangem.tap.features.wallet.models.removeUnknownTransactions -import com.tangem.tap.features.wallet.redux.ProgressState -import com.tangem.tap.features.wallet.redux.WalletMainButton -import com.tangem.tap.features.wallet.redux.WalletState -import com.tangem.tap.features.wallet.redux.WalletState.Companion.UNKNOWN_AMOUNT_SIGN -import com.tangem.tap.features.wallet.redux.replaceSomeWalletsData -import com.tangem.tap.features.wallet.ui.BalanceStatus -import com.tangem.tap.features.wallet.ui.BalanceWidgetData -import com.tangem.tap.store - -class OnWalletLoadedReducer { - - fun reduce(wallet: Wallet, blockchainNetwork: BlockchainNetwork, walletState: WalletState): WalletState { - return if (!walletState.isMultiwalletAllowed) { - onSingleWalletLoaded(wallet, walletState) - } else { - onMultiWalletLoaded(wallet, blockchainNetwork, walletState) - } - } - - @Suppress("LongMethod") - private fun onMultiWalletLoaded( - wallet: Wallet, - blockchainNetwork: BlockchainNetwork, - walletState: WalletState, - ): WalletState { - val walletData = walletState.getWalletData(blockchainNetwork) ?: return walletState - - val fiatCurrency = store.state.globalState.appCurrency - val coinAmountValue = wallet.amounts[AmountType.Coin]?.value - val formattedAmount = coinAmountValue?.toFormattedCurrencyString( - wallet.blockchain.decimals(), - wallet.blockchain.currency, - ) - - val pendingTransactions = wallet.getPendingTransactions() - val isCoinSendButtonEnabled = coinAmountValue?.isZero() == false && pendingTransactions.isEmpty() - val balanceStatus = if (pendingTransactions.isNotEmpty()) { - BalanceStatus.TransactionInProgress - } else { - BalanceStatus.VerifiedOnline - } - - val fiatAmount = walletData.fiatRate?.let { coinAmountValue?.toFiatValue(it) } - val fiatAmountFormatted = fiatAmount?.toFormattedFiatValue(fiatCurrency.symbol) ?: UNKNOWN_AMOUNT_SIGN - - val newWalletData = walletData.copy( - currencyData = walletData.currencyData.copy( - status = balanceStatus, - currency = wallet.blockchain.fullName, - currencySymbol = wallet.blockchain.currency, - blockchainAmount = coinAmountValue, - amount = coinAmountValue, - amountFormatted = formattedAmount, - fiatAmount = fiatAmount, - fiatAmountFormatted = fiatAmountFormatted, - ), - pendingTransactions = pendingTransactions.removeUnknownTransactions(), - mainButton = WalletMainButton.SendButton(isCoinSendButtonEnabled), - currency = Currency.fromBlockchainNetwork(blockchainNetwork), - ) - - val tokens = wallet.getTokens().mapNotNull { token -> - val currency = Currency.fromBlockchainNetwork(blockchainNetwork, token) - val tokenWalletData = walletState.getWalletData(currency) - val tokenPendingTransactions = pendingTransactions.filterByToken(token) - val tokenBalanceStatus = when { - tokenPendingTransactions.isNotEmpty() -> BalanceStatus.TransactionInProgress - pendingTransactions.isNotEmpty() -> BalanceStatus.SameCurrencyTransactionInProgress - else -> BalanceStatus.VerifiedOnline - } - val tokenAmountValue = wallet.getTokenAmount(token)?.value - val tokenFiatAmount = tokenWalletData?.fiatRate?.let { tokenAmountValue?.toFiatValue(it) } - val tokenFiatAmountFormatted = tokenFiatAmount?.toFormattedFiatValue(fiatCurrency.symbol) - ?: UNKNOWN_AMOUNT_SIGN - - val isTokenSendButtonEnabled = tokenWalletData?.shouldEnableTokenSendButton() == true && - pendingTransactions.isEmpty() - tokenWalletData?.copy( - currencyData = tokenWalletData.currencyData.copy( - status = tokenBalanceStatus, - blockchainAmount = coinAmountValue, - amount = tokenAmountValue, - amountFormatted = tokenAmountValue?.toFormattedCurrencyString( - token.decimals, - token.symbol, - ), - fiatAmount = tokenFiatAmount, - fiatAmountFormatted = tokenFiatAmountFormatted, - ), - pendingTransactions = tokenPendingTransactions.removeUnknownTransactions(), - mainButton = WalletMainButton.SendButton(isTokenSendButtonEnabled), - ) - } - val newWalletsData = tokens + newWalletData - val walletsData = walletState.walletsDataFromStores.replaceSomeWalletsData(newWalletsData) - - return walletState.updateWalletsData(walletsData) - } - - private fun onSingleWalletLoaded(wallet: Wallet, walletState: WalletState): WalletState { - if (wallet.blockchain != walletState.primaryBlockchain) return walletState - - val fiatCurrencyName = store.state.globalState.appCurrency.code - val amount = wallet.amounts[AmountType.Coin]?.value - val formattedAmount = amount?.toFormattedCurrencyString( - wallet.blockchain.decimals(), - wallet.blockchain.currency, - ) - val fiatAmount = walletState.primaryWalletData?.fiatRate?.let { amount?.toFiatValue(it) } - val fiatAmountFormatted = fiatAmount?.toFormattedFiatValue(fiatCurrencyName) ?: UNKNOWN_AMOUNT_SIGN - - val pendingTransactions = wallet.getPendingTransactions() - val sendButtonEnabled = amount?.isZero() == false && pendingTransactions.isEmpty() - val balanceStatus = if (pendingTransactions.isNotEmpty()) { - BalanceStatus.TransactionInProgress - } else { - BalanceStatus.VerifiedOnline - } - val walletData = walletState.primaryWalletData?.copy( - currencyData = BalanceWidgetData( - status = balanceStatus, - currency = wallet.blockchain.fullName, - currencySymbol = wallet.blockchain.currency, - blockchainAmount = amount, - amount = amount, - amountFormatted = formattedAmount, - fiatAmount = fiatAmount, - fiatAmountFormatted = fiatAmountFormatted, - ), - pendingTransactions = pendingTransactions.removeUnknownTransactions(), - mainButton = WalletMainButton.SendButton(sendButtonEnabled), - ) - val wallets = listOfNotNull(walletData) - val updatedStore = walletState.getWalletStore(walletData?.currency)?.updateWallets(wallets) - - return walletState.updateWalletStore(updatedStore).copy( - state = ProgressState.Done, - error = null, - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/TotalBalanceOperations.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/TotalBalanceOperations.kt index d5865ab287..ab255c0eb9 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/TotalBalanceOperations.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/TotalBalanceOperations.kt @@ -13,12 +13,6 @@ fun List.findProgressState(initialState: ProgressState = ProgressSta .reduce(ProgressState::or) } -fun List.calculateTotalFiatAmount(): BigDecimal { - return this - .map { it.currencyData.fiatAmount ?: BigDecimal.ZERO } - .reduce(BigDecimal::plus) -} - fun List.calculateTotalCryptoAmount(): BigDecimal { return this .map { it.currencyData.amount ?: BigDecimal.ZERO } diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/WalletReducer.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/WalletReducer.kt index c1157544cb..78e56b7d93 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/WalletReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/WalletReducer.kt @@ -1,23 +1,13 @@ package com.tangem.tap.features.wallet.redux.reducers -import com.tangem.blockchain.common.AmountType import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.Wallet -import com.tangem.common.extensions.mapNotNullValues import com.tangem.domain.common.CardDTO import com.tangem.domain.common.TapWorkarounds.isTestCard -import com.tangem.domain.common.TwinCardNumber -import com.tangem.tap.common.entities.FiatCurrency -import com.tangem.tap.common.extensions.toFiatRateString -import com.tangem.tap.common.extensions.toFiatValue -import com.tangem.tap.common.extensions.toFormattedCurrencyString -import com.tangem.tap.common.extensions.toFormattedFiatValue import com.tangem.tap.common.redux.AppState import com.tangem.tap.domain.TapError -import com.tangem.tap.domain.extensions.getArtworkUrl import com.tangem.tap.domain.tokens.models.BlockchainNetwork import com.tangem.tap.features.wallet.models.Currency -import com.tangem.tap.features.wallet.models.WalletRent import com.tangem.tap.features.wallet.redux.AddressData import com.tangem.tap.features.wallet.redux.Artwork import com.tangem.tap.features.wallet.redux.ErrorType @@ -25,15 +15,12 @@ import com.tangem.tap.features.wallet.redux.ProgressState import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.features.wallet.redux.WalletAddresses import com.tangem.tap.features.wallet.redux.WalletData -import com.tangem.tap.features.wallet.redux.WalletMainButton import com.tangem.tap.features.wallet.redux.WalletState import com.tangem.tap.features.wallet.redux.WalletStore -import com.tangem.tap.features.wallet.redux.replaceSomeWalletsData import com.tangem.tap.features.wallet.ui.BalanceStatus import com.tangem.tap.features.wallet.ui.BalanceWidgetData import com.tangem.tap.proxy.AppStateHolder import org.rekotlin.Action -import java.math.BigDecimal object WalletReducer { fun reduce(action: Action, state: AppState, appStateHolder: AppStateHolder): WalletState = @@ -43,7 +30,6 @@ object WalletReducer { @Suppress("LongMethod", "ComplexMethod") private fun internalReduce(action: Action, state: AppState, appStateHolder: AppStateHolder): WalletState { val multiWalletReducer = MultiWalletReducer() - val onWalletLoadedReducer = OnWalletLoadedReducer() val appCurrencyReducer = AppCurrencyReducer() if (action !is WalletAction) return state.walletState @@ -53,36 +39,6 @@ private fun internalReduce(action: Action, state: AppState, appStateHolder: AppS when (action) { is WalletAction.Warnings -> newState = handleCheckSignedHashesActions(action, newState) is WalletAction.MultiWallet -> newState = multiWalletReducer.reduce(action, newState) - - is WalletAction.ResetState -> { - newState = WalletState( - cardId = action.newCard.cardId, - walletCardsCount = action.newCard.findCardsCount(), - ) - } - is WalletAction.SetIfTestnetCard -> newState = newState.copy(isTestnet = action.isTestnet) - is WalletAction.EmptyWallet -> { - newState = newState.copy( - state = ProgressState.Done, - walletsStores = listOf( - WalletStore( - walletManager = null, - blockchainNetwork = BlockchainNetwork( - Blockchain.Unknown, - null, - emptyList(), - ), - walletsData = listOf( - WalletData( - currencyData = BalanceWidgetData(BalanceStatus.EmptyCard), - mainButton = WalletMainButton.CreateWalletButton(true), - currency = Currency.Blockchain(Blockchain.Unknown, null), - ), - ), - ), - ), - ) - } is WalletAction.LoadData.Failure -> { when (action.error) { is TapError.NoInternetConnection -> { @@ -133,7 +89,6 @@ private fun internalReduce(action: Action, state: AppState, appStateHolder: AppS } } } - is WalletAction.LoadData -> { newState = newState.copy( state = ProgressState.Loading, @@ -146,152 +101,6 @@ private fun internalReduce(action: Action, state: AppState, appStateHolder: AppS error = null, ) } - is WalletAction.LoadWallet -> { - val balanceStatus = if (newState.state == ProgressState.Refreshing) { - BalanceStatus.Refreshing - } else { - BalanceStatus.Loading - } - if (action.blockchain == null) { - val wallets = newState.walletsStores.map { walletStore -> - walletStore.copy( - walletsData = walletStore.walletsData.map { walletData -> - walletData.copy( - currencyData = walletData.currencyData.copy( - status = - if (walletStore.walletManager != null) balanceStatus else BalanceStatus.Unreachable, - currency = walletData.currencyData.currency, - currencySymbol = walletData.currencyData.currencySymbol, - ), - mainButton = WalletMainButton.SendButton(false), - ) - }, - ) - } - newState = newState.copy( - state = ProgressState.Loading, - walletsStores = wallets, - ) - } else { - val walletManager = newState.getWalletManager(action.blockchain) ?: return newState - val currencies = listOf(Currency.fromBlockchainNetwork(action.blockchain)) + - walletManager.cardTokens.map { - Currency.fromBlockchainNetwork(action.blockchain, it) - } - val newWalletsData = newState.walletsDataFromStores.filter { currencies.contains(it.currency) } - .map { wallet -> - wallet.copy( - currencyData = wallet.currencyData.copy( - status = balanceStatus, - currency = wallet.currencyData.currency, - currencySymbol = wallet.currencyData.currencySymbol, - ), - mainButton = WalletMainButton.SendButton(false), - ) - } - val walletsData = newState.walletsDataFromStores.replaceSomeWalletsData(newWalletsData) - val walletStore = newState.getWalletStore(action.blockchain)?.updateWallets(walletsData) - newState = newState.updateWalletStore(walletStore) - } - } - is WalletAction.LoadWallet.Success -> newState = onWalletLoadedReducer.reduce( - wallet = action.wallet, - blockchainNetwork = action.blockchain, - walletState = newState, - ) - is WalletAction.LoadWallet.NoAccount -> { - val amount = BigDecimal.ZERO - val fiatAmount = BigDecimal.ZERO - val walletData = newState.getWalletData(action.blockchain)?.let { walletData -> - val walletBlockchain = walletData.currency.blockchain - walletData.copy( - currencyData = BalanceWidgetData( - status = BalanceStatus.NoAccount, - currency = action.wallet.blockchain.fullName, - currencySymbol = action.wallet.blockchain.currency, - amount = amount, - amountFormatted = amount.toFormattedCurrencyString( - decimals = walletBlockchain.decimals(), - currency = walletBlockchain.currency, - ), - fiatAmount = fiatAmount, - fiatAmountFormatted = fiatAmount.toFormattedFiatValue( - fiatCurrencyName = state.globalState.appCurrency.symbol, - ), - amountToCreateAccount = action.amountToCreateAccount, - ), - ) - } - val updatedWalletStore = newState.getWalletStore(action.blockchain) - ?.updateWallets(listOfNotNull(walletData)) - - newState = newState.updateWalletStore(updatedWalletStore) - } - - is WalletAction.LoadWallet.Failure -> { - val message = if (newState.error == ErrorType.NoInternetConnection) { - null - } else { - action.errorMessage - } - val walletStore = newState.getWalletStore(action.wallet) - val walletData = walletStore?.walletsData?.first { it.currency is Currency.Blockchain } - val newWalletData = walletData?.copy( - currencyData = walletData.currencyData.copy( - status = BalanceStatus.Unreachable, - errorMessage = message, - ), - ) - val tokenWallets = action.wallet.getTokens() - .mapNotNull { token -> - walletStore?.blockchainNetwork?.let { - newState.getWalletData(Currency.fromBlockchainNetwork(it, token)) - } - } - .map { - it.copy( - currencyData = it.currencyData.copy( - status = BalanceStatus.Unreachable, - errorMessage = message, - ), - ) - } - val updatedWallets = walletStore!!.updateWallets(listOfNotNull(newWalletData) + tokenWallets).walletsData - - newState = newState.updateWalletsData(updatedWallets) - - val progressState = - if (newState.walletsDataFromStores.any { it.currencyData.status == BalanceStatus.Loading }) { - ProgressState.Loading - } else { - ProgressState.Done - } - - newState = newState.copy( - state = progressState, - ) - } - is WalletAction.SetArtworkId -> { - val cardImage = if (newState.cardImage?.artworkId == action.artworkId) { - newState.cardImage - } else { - null - } - newState = newState.copy(cardImage = cardImage) - } - - is WalletAction.LoadFiatRate.Success -> { - newState = setNewFiatRate(action.fiatRates, state.globalState.appCurrency, newState) - } - is WalletAction.LoadArtwork -> { - val artworkUrl = action.card.getArtworkUrl(action.artworkId) - ?: when (state.twinCardsState.cardNumber) { - TwinCardNumber.First -> Artwork.TWIN_CARD_1 - TwinCardNumber.Second -> Artwork.TWIN_CARD_2 - else -> Artwork.DEFAULT_IMG_URL - } - newState = newState.copy(cardImage = Artwork(artworkId = artworkUrl)) - } is WalletAction.TradeCryptoAction -> return newState is WalletAction.ChangeSelectedAddress -> { val walletAddresses = newState.getWalletData(newState.selectedCurrency)?.walletAddresses @@ -308,22 +117,9 @@ private fun internalReduce(action: Action, state: AppState, appStateHolder: AppS ), ) } - is WalletAction.SetWalletRent -> { - val walletStore = newState.getWalletStore(action.wallet) ?: return newState - val walletRent = WalletRent(action.minRent, action.rentExempt) - val walletsData = walletStore.walletsData.map { it.copy(walletRent = walletRent) } - newState = newState.updateWalletsData(walletsData) - } - is WalletAction.RemoveWalletRent -> { - val walletStore = newState.getWalletStore(action.wallet) ?: return newState - val walletsData = walletStore.walletsData.map { it.copy(walletRent = null) } - newState = newState.updateWalletsData(walletsData) - } is WalletAction.AppCurrencyAction -> { newState = appCurrencyReducer.reduce(action, newState) } - is WalletAction.UserTokens.Loading -> newState = newState.copy(loadingUserTokens = true) - is WalletAction.UserTokens.Loaded -> newState = newState.copy(loadingUserTokens = false) is WalletAction.UserWalletChanged -> with(action.userWallet) { val card = scanResponse.card newState = WalletState( @@ -367,6 +163,11 @@ private fun internalReduce(action: Action, state: AppState, appStateHolder: AppS ), ) } + is WalletAction.UpdateCanSaveUserWallets -> { + newState = newState.copy( + canSaveUserWallets = action.canSaveUserWallets, + ) + } else -> Unit } appStateHolder.walletState = newState @@ -390,20 +191,6 @@ private fun CardDTO.findCardsCount(): Int? { return (this.backupStatus as? CardDTO.BackupStatus.Active)?.cardCount?.inc() } -fun createAddressList(wallet: Wallet?, walletAddresses: WalletAddresses? = null): WalletAddresses? { - if (wallet == null) return null - - val listOfAddressData = wallet.createAddressesData() - // restore a selected wallet address - var indexOfSelectedWallet = 0 - walletAddresses?.let { - val index = - listOfAddressData.indexOfFirst { it.address == walletAddresses.selectedAddress.address } - if (index != -1) indexOfSelectedWallet = index - } - return WalletAddresses(listOfAddressData[indexOfSelectedWallet], listOfAddressData) -} - fun Wallet.createAddressesData(): List { val listOfAddressData = mutableListOf() // put a defaultAddress at the first place @@ -435,41 +222,4 @@ private fun handleCheckSignedHashesActions( is WalletAction.Warnings.Set -> state.copy(mainWarningsList = action.warningList) else -> state } -} - -private fun setNewFiatRate( - fiatRates: Map, - appCurrency: FiatCurrency, - state: WalletState, -): WalletState { - val rateFormatter: (BigDecimal) -> String = { rate: BigDecimal -> - rate.toFiatRateString(fiatCurrencyName = appCurrency.symbol) - } - - val newWalletsData = fiatRates.mapNotNullValues { it.value }.mapNotNull { (currency, rate) -> - val walletStore = state.getWalletStore(currency) ?: return@mapNotNull null - val wallet = walletStore.walletManager?.wallet - val walletData = state.getWalletData(currency) ?: return@mapNotNull null - val currencyData = walletData.currencyData - - var fiatAmount = when (currency) { - is Currency.Blockchain -> wallet?.amounts?.get(AmountType.Coin)?.value?.toFiatValue(rate) - is Currency.Token -> wallet?.getTokenAmount(currency.token)?.value?.toFiatValue(rate) - } - if (currencyData.status == BalanceStatus.NoAccount && fiatAmount == null) { - fiatAmount = BigDecimal.ZERO.setScale(2) - } - val fiatAmountFormatted = fiatAmount?.toFormattedFiatValue(appCurrency.symbol) - - walletData.copy( - currencyData = currencyData.copy( - fiatAmountFormatted = fiatAmountFormatted, - fiatAmount = fiatAmount, - ), - fiatRate = rate, - fiatRateString = rateFormatter(rate), - ) - } - - return state.updateWalletsData(newWalletsData) } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt index fcabcee21b..af465cf7b2 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt @@ -20,8 +20,10 @@ import com.badoo.mvicore.DiffStrategy import com.badoo.mvicore.ModelWatcher import com.badoo.mvicore.modelWatcher import com.tangem.common.doOnResult +import com.tangem.common.extensions.guard import com.tangem.core.analytics.Analytics import com.tangem.domain.common.TapWorkarounds.derivationStyle +import com.tangem.domain.common.extensions.withMainContext import com.tangem.feature.swap.domain.SwapInteractor import com.tangem.tangem_sdk_new.extensions.dpToPx import com.tangem.tap.common.SnackbarHandler @@ -30,7 +32,6 @@ import com.tangem.tap.common.analytics.events.DetailsScreen import com.tangem.tap.common.analytics.events.Token import com.tangem.tap.common.extensions.appendIfNotNull import com.tangem.tap.common.extensions.beginDelayedTransition -import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.fitChipsByGroupWidth import com.tangem.tap.common.extensions.getColor import com.tangem.tap.common.extensions.getString @@ -39,7 +40,6 @@ import com.tangem.tap.common.extensions.show import com.tangem.tap.common.extensions.toQrCode import com.tangem.tap.common.recyclerView.SpaceItemDecoration import com.tangem.tap.common.redux.navigation.NavigationAction -import com.tangem.tap.domain.tokens.models.BlockchainNetwork import com.tangem.tap.features.wallet.models.Currency import com.tangem.tap.features.wallet.models.PendingTransaction import com.tangem.tap.features.wallet.models.PendingTransactionType @@ -63,8 +63,8 @@ import com.tangem.wallet.databinding.FragmentWalletDetailsBinding import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import org.rekotlin.StoreSubscriber +import timber.log.Timber import javax.inject.Inject @Suppress("LargeClass", "MagicNumber") @@ -248,24 +248,16 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), if (currencyData.status != BalanceStatus.Loading && currencyData.status != BalanceStatus.Refreshing) { Analytics.send(Token.Refreshed()) lifecycleScope.launch(Dispatchers.Default) { - val selectedUserWallet = userWalletsListManagerSafe?.selectedUserWalletSync - if (selectedUserWallet != null) { - walletCurrenciesManager.update(selectedUserWallet, currency) - .doOnResult { - withContext(Dispatchers.Main) { - binding.srlWalletDetails.isRefreshing = false - } - } - } else { - val blockchainNetwork = BlockchainNetwork( - blockchain = currency.blockchain, - derivationPath = currency.derivationPath, - tokens = emptyList(), - ) - - store.dispatchOnMain(WalletAction.LoadWallet(blockchainNetwork)) - store.dispatchOnMain(WalletAction.LoadFiatRate(coinsList = listOf(currency))) + val selectedUserWallet = userWalletsListManagerSafe?.selectedUserWalletSync.guard { + Timber.e("Unable to refresh wallet details screen, no user wallet selected") + return@launch } + walletCurrenciesManager.update(selectedUserWallet, currency) + .doOnResult { + withMainContext { + binding.srlWalletDetails.isRefreshing = false + } + } } } } diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt index b170637f3b..3e99ac95ed 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt @@ -43,7 +43,6 @@ import com.tangem.tap.features.wallet.ui.wallet.SingleWalletView import com.tangem.tap.features.wallet.ui.wallet.WalletView import com.tangem.tap.features.wallet.ui.wallet.saltPay.SaltPayWalletView import com.tangem.tap.store -import com.tangem.tap.userWalletsListManager import com.tangem.wallet.BuildConfig import com.tangem.wallet.R import com.tangem.wallet.databinding.FragmentWalletBinding @@ -81,13 +80,7 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber { + private var observeWalletStoresUpdatesJob: Job? = null + set(value) { + field?.cancel() + field = value + } + + init { + subscribeToUserWalletsListManagerUpdates() + } + + override fun onCleared() { + store.unsubscribe(this) + } + + override fun newState(state: UserWalletsListManager?) { + // Restarting observing of wallet store updates when the manager changes + if (state != null) { + bootstrapSelectedWalletStoresChanges(state) + } + } -internal class WalletViewModel : ViewModel() { fun launch() { - bootstrapSelectedWalletStoresChanges() + val manager = store.state.globalState.userWalletsListManager + if (manager != null) { + bootstrapSelectedWalletStoresChanges(manager) + } bootstrapShowSaveWalletIfNeeded() } - private fun bootstrapSelectedWalletStoresChanges() { - userWalletsListManager.selectedUserWallet + private fun bootstrapSelectedWalletStoresChanges(manager: UserWalletsListManager) { + observeWalletStoresUpdatesJob = manager.selectedUserWallet .map { it.walletId } - .distinctUntilChanged() .flatMapLatest { selectedUserWalletId -> walletStoresManager.get(selectedUserWalletId) } @@ -40,4 +67,14 @@ internal class WalletViewModel : ViewModel() { store.dispatchOnMain(WalletAction.ShowSaveWalletIfNeeded) } } + + private fun subscribeToUserWalletsListManagerUpdates() { + store.subscribe(this) { appState -> + appState + .skip { old, new -> + old.globalState.userWalletsListManager == new.globalState.userWalletsListManager + } + .select { it.globalState.userWalletsListManager } + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/MultiWalletView.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/MultiWalletView.kt index d1eab75515..4d2bdc1fdc 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/MultiWalletView.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/MultiWalletView.kt @@ -1,10 +1,8 @@ package com.tangem.tap.features.wallet.ui.wallet import android.widget.Button -import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.recyclerview.widget.LinearLayoutManager -import com.badoo.mvicore.DiffStrategy import com.badoo.mvicore.modelWatcher import com.tangem.core.analytics.Analytics import com.tangem.domain.common.TapWorkarounds.derivationStyle @@ -20,7 +18,6 @@ import com.tangem.tap.common.redux.navigation.NavigationAction import com.tangem.tap.domain.tokens.CurrenciesRepository import com.tangem.tap.features.tokens.redux.TokensAction import com.tangem.tap.features.wallet.models.TotalBalance -import com.tangem.tap.features.wallet.redux.ProgressState import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.features.wallet.redux.WalletState import com.tangem.tap.features.wallet.ui.BalanceStatus @@ -36,13 +33,6 @@ class MultiWalletView : WalletView() { private lateinit var walletsAdapter: WalletAdapter private val watcher = modelWatcher { - val totalBalanceStrategy: DiffStrategy = { old, new -> - old.cardId != new.cardId || - old.totalBalance != new.totalBalance || - old.state != new.state || - old.walletsStores.size != new.walletsStores.size - } - // !!! Workaround !!! // Checking state properties instead of state params can reduce application performance, // but here it is necessary because the WalletStore has an unsuitable equals method @@ -67,12 +57,11 @@ class MultiWalletView : WalletView() { handleBackupWarning(it, showBackupWarnings) } } - watch({ it }, totalBalanceStrategy) { walletState -> + (WalletState::totalBalance or WalletState::walletsDataFromStores) { walletState -> binding?.let { handleTotalBalance( binding = it, totalBalance = walletState.totalBalance, - progressState = walletState.state, walletsCount = walletState.walletsDataFromStores.size, ) } @@ -179,10 +168,9 @@ class MultiWalletView : WalletView() { private fun handleTotalBalance( binding: FragmentWalletBinding, totalBalance: TotalBalance?, - progressState: ProgressState, walletsCount: Int, ) = with(binding.lCardTotalBalance) { - isGone = progressState == ProgressState.Done && walletsCount == 0 + isVisible = walletsCount > 0 onChangeFiatCurrencyClick = { store.dispatch(WalletAction.AppCurrencyAction.ChooseAppCurrency) diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/saltPay/SaltPayWalletView.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/saltPay/SaltPayWalletView.kt index e73d4ab074..a379f290a8 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/saltPay/SaltPayWalletView.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/saltPay/SaltPayWalletView.kt @@ -3,7 +3,6 @@ package com.tangem.tap.features.wallet.ui.wallet.saltPay import android.view.LayoutInflater import android.view.ViewGroup import com.tangem.core.analytics.Analytics -import com.tangem.domain.common.extensions.debounce import com.tangem.domain.common.extensions.withMainContext import com.tangem.tap.common.ShimmerData import com.tangem.tap.common.ShimmerRecyclerAdapter @@ -21,7 +20,6 @@ import com.tangem.tap.features.wallet.ui.wallet.WalletView import com.tangem.tap.features.wallet.ui.wallet.saltPay.rv.HistoryItemData import com.tangem.tap.features.wallet.ui.wallet.saltPay.rv.HistoryTransactionData import com.tangem.tap.features.wallet.ui.wallet.saltPay.rv.TxHistoryAdapter -import com.tangem.tap.mainScope import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager import com.tangem.tap.scope import com.tangem.tap.store @@ -31,7 +29,6 @@ import com.tangem.wallet.databinding.LayoutSaltPayBalanceBinding import com.tangem.wallet.databinding.LayoutSaltPayTxHistoryBinding import com.tangem.wallet.databinding.LayoutSaltPayWalletBinding import kotlinx.coroutines.launch -import org.rekotlin.Action import timber.log.Timber /** @@ -41,8 +38,6 @@ class SaltPayWalletView : WalletView() { private var saltPayBinding: LayoutSaltPayWalletBinding? = null - private val actionDebouncer = debounce(500, mainScope) { store.dispatch(it) } - private val balanceWidget: LayoutSaltPayBalanceBinding? get() = saltPayBinding?.lSaltPayBalance @@ -131,7 +126,6 @@ class SaltPayWalletView : WalletView() { if (tokenData.currencyData.fiatAmount == null) { veilBalance.veil() - actionDebouncer(WalletAction.LoadFiatRate()) } else { veilBalance.unVeil() tvBalance.text = tokenData.currencyData.fiatAmount.formatAmountAsSpannedString( diff --git a/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorMiddleware.kt b/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorMiddleware.kt index 828150fa17..bfd8936b17 100644 --- a/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorMiddleware.kt @@ -23,6 +23,7 @@ import com.tangem.tap.domain.model.WalletStoreModel import com.tangem.tap.domain.model.builders.UserWalletBuilder import com.tangem.tap.domain.model.builders.UserWalletIdBuilder import com.tangem.tap.domain.scanCard.ScanCardProcessor +import com.tangem.tap.domain.userWalletList.unlockIfLockable import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayActivationError import com.tangem.tap.preferencesStorage import com.tangem.tap.scope @@ -63,7 +64,7 @@ internal class WalletSelectorMiddleware { updateBalances(action.walletsStores, state) } is WalletSelectorAction.UnlockWithBiometry -> { - unlockWalletsWithBiometry() + unlockWallets() } is WalletSelectorAction.AddWallet -> { addWallet() @@ -115,11 +116,11 @@ internal class WalletSelectorMiddleware { } } - private fun unlockWalletsWithBiometry() { + private fun unlockWallets() { Analytics.send(MyWallets.Button.UnlockWithBiometrics()) scope.launch { - userWalletsListManager.unlockWithBiometry() + userWalletsListManager.unlockIfLockable() .doOnFailure { error -> Timber.e(error, "Unable to unlock all user wallets") store.dispatchOnMain(WalletSelectorAction.UnlockWithBiometry.Error(error)) @@ -195,7 +196,7 @@ internal class WalletSelectorMiddleware { if (userWallet.isLocked) { unlockUserWalletWithScannedCard(userWallet) } else { - userWalletsListManager.selectWallet(userWalletId) + userWalletsListManager.select(userWalletId) } } .doOnFailure { error -> 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 41e5ca44f0..b048098525 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 @@ -8,6 +8,7 @@ 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.isLocked import com.tangem.tap.features.details.ui.cardsettings.TextReference import com.tangem.tap.features.walletSelector.redux.WalletSelectorAction import com.tangem.tap.features.walletSelector.redux.WalletSelectorState 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 94d5b55b3c..c8cde60e79 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 @@ -17,6 +17,7 @@ import com.tangem.tap.common.redux.navigation.AppScreen import com.tangem.tap.common.redux.navigation.NavigationAction import com.tangem.tap.domain.model.builders.UserWalletBuilder import com.tangem.tap.domain.scanCard.ScanCardProcessor +import com.tangem.tap.domain.userWalletList.unlockIfLockable import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayActivationError import com.tangem.tap.intentHandler import com.tangem.tap.preferencesStorage @@ -66,7 +67,7 @@ internal class WelcomeMiddleware { private fun proceedWithBiometrics(state: WelcomeState) { scope.launch { - userWalletsListManager.unlockWithBiometry() + userWalletsListManager.unlockIfLockable() .doOnFailure { error -> store.dispatchOnMain(WelcomeAction.ProceedWithBiometrics.Error(error)) } 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 246d1ac726..ae2cdcfd1a 100644 --- a/app/src/main/java/com/tangem/tap/proxy/DerivationManagerImpl.kt +++ b/app/src/main/java/com/tangem/tap/proxy/DerivationManagerImpl.kt @@ -29,7 +29,7 @@ class DerivationManagerImpl( private val appStateHolder: AppStateHolder, ) : DerivationManager { - override suspend fun deriveMissingBlockchains(currency: Currency) = suspendCoroutine { continuation -> + override suspend fun deriveMissingBlockchains(currency: Currency) = suspendCoroutine { continuation -> val blockchain = Blockchain.fromNetworkId(currency.networkId) val card = appStateHolder.getActualCard() if (blockchain != null && card != null) { @@ -89,7 +89,7 @@ class DerivationManagerImpl( } scope.launch { - val selectedWallet = appStateHolder.userWalletsListManager?.selectedUserWalletSync + val selectedUserWallet = appStateHolder.userWalletsListManager?.selectedUserWalletSync val result = appStateHolder.tangemSdkManager?.derivePublicKeys( scanResponse.card.cardId, @@ -110,8 +110,8 @@ class DerivationManagerImpl( val updatedScanResponse = scanResponse.copy( derivedKeys = updatedDerivedKeys, ) - if (selectedWallet != null) { - val userWallet = selectedWallet.copy( + if (selectedUserWallet != null) { + val userWallet = selectedUserWallet.copy( scanResponse = updatedScanResponse, ) @@ -166,14 +166,10 @@ class DerivationManagerImpl( return DerivationData( derivations = mapKeyOfWalletPublicKey to toDerive, - alreadyDerivedKeys = alreadyDerivedKeys, - mapKeyOfWalletPublicKey = mapKeyOfWalletPublicKey, ) } private class DerivationData( val derivations: Pair>, - val alreadyDerivedKeys: ExtendedPublicKeysMap, - val mapKeyOfWalletPublicKey: ByteArrayKey, ) } \ No newline at end of file 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..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,31 +17,41 @@ 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.AnalyticsEventHandler 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 +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 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: AnalyticsEventHandler, ) : TransactionManager { override suspend fun sendApproveTransaction( networkId: String, feeAmount: BigDecimal, - estimatedGas: Int, + 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( @@ -49,7 +59,7 @@ class TransactionManagerImpl( amount = amount, blockchain = blockchain, feeAmount = feeAmount, - estimatedGas = estimatedGas, + gasLimit = gasLimit, destinationAddress = destinationAddress, dataToSign = dataToSign, ) @@ -59,14 +69,15 @@ class TransactionManagerImpl( networkId: String, amountToSend: BigDecimal, feeAmount: BigDecimal, - estimatedGas: Int, + gasLimit: Int, destinationAddress: String, 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) @@ -78,7 +89,7 @@ class TransactionManagerImpl( amount = amount, blockchain = blockchain, feeAmount = feeAmount, - estimatedGas = estimatedGas, + gasLimit = gasLimit, destinationAddress = destinationAddress, dataToSign = dataToSign, ) @@ -90,7 +101,7 @@ class TransactionManagerImpl( amount: Amount, blockchain: Blockchain, feeAmount: BigDecimal, - estimatedGas: Int, + gasLimit: Int, destinationAddress: String, dataToSign: String, ): SendTxResult { @@ -98,12 +109,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) @@ -120,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 { @@ -137,19 +148,55 @@ class TransactionManagerImpl( amountToSend: BigDecimal, currencyToSend: Currency, destinationAddress: String, - ): ProxyAmount { + data: String?, + derivationPath: 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")) + val walletManager = getActualWalletManager(blockchain, derivationPath) + 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) + } } } } @@ -163,9 +210,43 @@ 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 -> 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() @@ -215,31 +296,22 @@ 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( 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/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt b/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt index 250bf73562..a983ea5687 100644 --- a/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt +++ b/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt @@ -2,13 +2,9 @@ package com.tangem.tap.proxy import com.tangem.blockchain.common.AmountType import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.DerivationParams -import com.tangem.blockchain.common.DerivationStyle import com.tangem.blockchain.common.Token import com.tangem.blockchain.common.WalletManager -import com.tangem.blockchain.common.WalletManagerFactory -import com.tangem.domain.common.CardDTO -import com.tangem.domain.common.TapWorkarounds.derivationStyle +import com.tangem.common.extensions.guard import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.domain.common.extensions.toCoinId import com.tangem.domain.common.extensions.toNetworkId @@ -19,20 +15,17 @@ import com.tangem.lib.crypto.models.Currency.NonNativeToken import com.tangem.lib.crypto.models.ProxyAmount import com.tangem.lib.crypto.models.ProxyFiatCurrency import com.tangem.tap.common.extensions.dispatchOnMain -import com.tangem.tap.domain.extensions.makeWalletManagerForApp import com.tangem.tap.domain.model.builders.UserWalletIdBuilder import com.tangem.tap.domain.tokens.models.BlockchainNetwork import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.userWalletsListManager import com.tangem.tap.walletCurrenciesManager -import kotlinx.coroutines.delay -import org.rekotlin.Action +import timber.log.Timber import java.math.BigDecimal import com.tangem.tap.features.wallet.models.Currency as WalletCurrency class UserWalletManagerImpl( private val appStateHolder: AppStateHolder, - private val walletManagerFactory: WalletManagerFactory, ) : UserWalletManager { override suspend fun getUserTokens(networkId: String, isExcludeCustom: Boolean): List { @@ -92,67 +85,49 @@ 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 - if (selectedUserWallet != null) { - walletCurrenciesManager.addCurrencies( - userWallet = selectedUserWallet, - currenciesToAdd = listOf(currency.toWalletCurrency(blockchainNetwork)), - ) - } else { - val walletManager = getOrCreateBlockchain(blockchainNetwork, blockchain) - if (currency is NonNativeToken && !walletManager.cardTokens.contains(currency.toSdkToken())) { - val action = addNonNativeTokenToWalletAction(currency, card, blockchain) - val mainStore = requireNotNull(appStateHolder.mainStore) { "mainStore is null" } - mainStore.dispatchOnMain(action) - delay(DELAY_UPDATE_WALLET) - } + val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard { + Timber.e("Unable to add token, no user wallet selected") + return } + walletCurrenciesManager.addCurrencies( + userWallet = selectedUserWallet, + currenciesToAdd = listOf(currency.toWalletCurrency(blockchainNetwork)), + ) } - 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()?.hash?.let { HEX_PREFIX + it } + 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 @@ -177,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 { @@ -205,63 +180,13 @@ class UserWalletManagerImpl( ) } - private suspend fun getOrCreateBlockchain( - blockchainNetwork: BlockchainNetwork, - blockchain: Blockchain, - ): WalletManager { - val card = requireNotNull(appStateHolder.getActualCard()) { "card not found" } - val scanResponse = requireNotNull(appStateHolder.scanResponse) { "scanResponse not found" } - var walletManager = appStateHolder.walletState?.getWalletManager(blockchainNetwork) - if (walletManager == null) { - walletManager = walletManagerFactory.makeWalletManagerForApp( - scanResponse = scanResponse, - blockchain = blockchain, - derivationParams = createDerivationParams(card.derivationStyle), - ) - val action = WalletAction.MultiWallet.AddBlockchain( - blockchain = blockchainNetwork, - walletManager = walletManager, - save = true, - ) - val mainStore = requireNotNull(appStateHolder.mainStore) { "mainStore is null" } - mainStore.dispatchOnMain(action) - // workaround to wait until blockchain adds to walletStores and update appStateHolder.walletState - delay(DELAY_UPDATE_WALLET) - } - - return requireNotNull(walletManager) { "cant create walletManager" } - } - - private fun addNonNativeTokenToWalletAction(token: NonNativeToken, card: CardDTO, blockchain: Blockchain): Action { - return WalletAction.MultiWallet.AddToken( - token = Token( - id = token.id, - name = token.name, - symbol = token.symbol, - contractAddress = token.contractAddress, - decimals = token.decimalCount, - ), - blockchain = BlockchainNetwork( - blockchain, - card, - ), - save = true, - ) - } - override fun refreshWallet() { // workaround, should update wallet after transaction appStateHolder.mainStore?.dispatchOnMain(WalletAction.LoadData.Refresh) } - private fun createDerivationParams(derivationStyle: DerivationStyle?): DerivationParams? { - // todo clarify if its need to add Custom - return derivationStyle?.let { DerivationParams.Default(derivationStyle) } - } - - 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" } @@ -269,7 +194,6 @@ class UserWalletManagerImpl( companion object { private const val HEX_PREFIX = "0x" - private const val DELAY_UPDATE_WALLET = 500L } } 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 1a65b1ddd0..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.blockchain.common.WalletManagerFactory +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 @@ -29,14 +29,16 @@ class ProxyModule { fun provideUserWalletManager(appStateHolder: AppStateHolder): UserWalletManager { return UserWalletManagerImpl( appStateHolder = appStateHolder, - walletManagerFactory = WalletManagerFactory(), ) } @Provides @Singleton - fun provideTransactionManager(appStateHolder: AppStateHolder): TransactionManager { - return TransactionManagerImpl(appStateHolder) + fun provideTransactionManager( + appStateHolder: AppStateHolder, + analytics: AnalyticsEventHandler, + ): TransactionManager { + return TransactionManagerImpl(appStateHolder, analytics) } @Provides diff --git a/app/src/main/res/layout/item_currency_wallet_content.xml b/app/src/main/res/layout/item_currency_wallet_content.xml index aa767d069c..b27a1d1edb 100644 --- a/app/src/main/res/layout/item_currency_wallet_content.xml +++ b/app/src/main/res/layout/item_currency_wallet_content.xml @@ -1,17 +1,17 @@ + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:minHeight="42dp"> + android:id="@+id/guideline_horizontal" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" + app:layout_constraintGuide_percent="0.5" /> + tools:text="Transaction in progress and it very long text with some strange status" + tools:visibility="visible" /> + tools:text="46 908 $" + tools:visibility="gone" /> + diff --git a/buildSrc/src/main/java/Versions.kt b/buildSrc/src/main/java/Versions.kt index ff548135ef..f1e23c39a8 100644 --- a/buildSrc/src/main/java/Versions.kt +++ b/buildSrc/src/main/java/Versions.kt @@ -62,11 +62,11 @@ object Versions { // endregion Other libraries // region Tangem - const val tangemBlockchainSdk = "develop-168" + const val tangemBlockchainSdk = "develop-174" // const val tangemBlockchainSdk = "0.0.1" // Keep it! - used for local builds - const val tangemCardSdk = "develop-191" - // const val tangemCardSgk = "0.0.1" // Keep it! - used for local builds + const val tangemCardSdk = "develop-199" + // const val tangemCardSdk = "0.0.1" // Keep it! - used for local builds // endregion Tangem // region Tools diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/MoshiConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/MoshiConverter.kt index 1457807f3e..510ac95179 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/MoshiConverter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/MoshiConverter.kt @@ -3,6 +3,7 @@ package com.tangem.datasource.api.common import com.squareup.moshi.Moshi import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import com.tangem.common.json.MoshiJsonConverter +import com.tangem.common.json.TangemSdkAdapter import retrofit2.converter.moshi.MoshiConverterFactory /** @@ -15,6 +16,7 @@ object MoshiConverter { val networkMoshi: Moshi = Moshi.Builder() .add(KotlinJsonAdapterFactory()) .add(BigDecimalAdapter()) + .add(TangemSdkAdapter.ByteArrayAdapter()) .build() val networkMoshiConverter: MoshiConverterFactory = MoshiConverterFactory.create(networkMoshi) diff --git a/core/datasource/src/main/java/com/tangem/datasource/config/ConfigManagerImpl.kt b/core/datasource/src/main/java/com/tangem/datasource/config/ConfigManagerImpl.kt index e438e85955..d0c6d148c2 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/config/ConfigManagerImpl.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/config/ConfigManagerImpl.kt @@ -99,7 +99,7 @@ internal class ConfigManagerImpl @Inject constructor() : ConfigManager { ), infuraProjectId = configValues.infuraProjectId, tronGridApiKey = configValues.tronGridApiKey, - saltPayAuthToken = configValues.saltPay.credentials.token, + saltPayAuthToken = configValues.saltPay.credentials.basicAuthToken, nowNodeCredentials = NowNodeCredentials(configValues.nowNodesApiKey), getBlockCredentials = GetBlockCredentials(configValues.getBlockApiKey), ), diff --git a/core/datasource/src/main/java/com/tangem/datasource/config/models/SaltPayConfig.kt b/core/datasource/src/main/java/com/tangem/datasource/config/models/SaltPayConfig.kt index f8148bcf9e..8e38b0f971 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/config/models/SaltPayConfig.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/config/models/SaltPayConfig.kt @@ -1,7 +1,5 @@ package com.tangem.datasource.config.models -import org.spongycastle.util.encoders.Base64.toBase64String - /** [REDACTED_AUTHOR] */ @@ -34,5 +32,5 @@ data class Credentials( val user: String, val password: String, ) { - val token: String by lazy { "Basic ${toBase64String("$user:$password".toByteArray())}" } + val basicAuthToken: String by lazy { okhttp3.Credentials.basic(user, password) } } \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/common/extensions/CardSdk.kt b/domain/src/main/java/com/tangem/domain/common/extensions/CardSdk.kt index bd7a3d7643..c7d205bbe0 100644 --- a/domain/src/main/java/com/tangem/domain/common/extensions/CardSdk.kt +++ b/domain/src/main/java/com/tangem/domain/common/extensions/CardSdk.kt @@ -13,15 +13,12 @@ val FirmwareVersion.Companion.SolanaTokensAvailable get() = FirmwareVersion(4, 52) fun CardDTO.supportedBlockchains(): List { - val supportedBlockchains = when { - firmwareVersion < FirmwareVersion.MultiWalletAvailable -> { - Blockchain.fromCurve(EllipticCurve.Secp256k1) - } - - else -> { - (Blockchain.fromCurve(EllipticCurve.Secp256k1) + Blockchain.fromCurve(EllipticCurve.Ed25519)).distinct() - } + val supportedBlockchains = if (firmwareVersion < FirmwareVersion.MultiWalletAvailable) { + Blockchain.fromCurve(EllipticCurve.Secp256k1) + } else { + wallets.flatMap { Blockchain.fromCurve(it.curve) }.distinct() } + return supportedBlockchains .filter { isTestCard == it.isTestnet() } .filter { it.isSupportedInApp() } @@ -38,12 +35,8 @@ fun CardDTO.supportedTokens(): List { } } } - val filtered = tokensSupportedByCard.filter { isTestCard == it.isTestnet() } - return filtered -} -fun CardDTO.canHandleBlockchain(blockchain: Blockchain): Boolean { - return this.supportedBlockchains().contains(blockchain) + return tokensSupportedByCard.filter { isTestCard == it.isTestnet() } } fun CardDTO.canHandleToken(blockchain: Blockchain): Boolean { 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/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt index 199838f94c..e0671b7ffd 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt @@ -35,8 +35,24 @@ internal class SwapRepositoryImpl @Inject constructor( private val approveConverter = ApproveConverter() override suspend fun getRates(currencyId: String, tokenIds: List): Map { + // workaround cause backend do not return arbitrum and optimism rates + val addedTokens = if (tokenIds.contains(OPTIMISM_ID) || tokenIds.contains(ARBITRUM_ID)) { + tokenIds.toMutableList().apply { + add(ETHEREUM_ID) + } + } else { + tokenIds + } return withContext(coroutineDispatcher.io) { - tangemTechApi.getRates(currencyId.lowercase(), tokenIds.joinToString(",")).rates + val rates = tangemTechApi.getRates(currencyId.lowercase(), addedTokens.joinToString(",")).rates + val ethRate = rates[ETHEREUM_ID] + rates.mapValues { + if (it.key == OPTIMISM_ID || it.key == ARBITRUM_ID) { + ethRate ?: 0.0 + } else { + it.value + } + } } } @@ -136,4 +152,11 @@ internal class SwapRepositoryImpl @Inject constructor( private fun getOneInchApi(networkId: String): OneInchApi { return oneInchApiFactory.getApi(networkId) } + + companion object { + // TODO("get this ids from blockchain enum later") + private const val OPTIMISM_ID = "optimistic-ethereum" + private const val ARBITRUM_ID = "arbitrum-one" + private const val ETHEREUM_ID = "ethereum" + } } \ No newline at end of file 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..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 @@ -1,16 +1,18 @@ 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 interface SwapInteractor { + fun initDerivationPath(derivationPath: String?) + /** * Init tokens to swap, load tokens list available to swap for given network * @@ -42,15 +44,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 +76,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 +85,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 23b6c794b8..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 @@ -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 @@ -40,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 @@ -64,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 }) @@ -110,24 +114,21 @@ 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 = increasedEstimatedGas, - destinationAddress = transactionData.toAddress, - dataToSign = transactionData.data, + feeAmount = approveData.fee, + 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 @@ -152,19 +153,12 @@ internal class SwapInteractorImpl @Inject constructor( val fromTokenAddress = getTokenAddress(fromToken) val toTokenAddress = getTokenAddress(toToken) val isAllowedToSpend = checkAllowance(networkId, fromTokenAddress) - val fee = getAndUpdateFee(networkId, fromToken) - val isBalanceEnough = isBalanceEnough(fromToken, amount, fee) - val isFeeEnough = checkFeeIsEnough(fee, amount, networkId, fromToken) if (isAllowedToSpend && allowPermissionsHandler.isAddressAllowanceInProgress(fromTokenAddress)) { allowPermissionsHandler.removeAddressFromProgress(fromTokenAddress) - transactionManager.updateWalletManager(networkId) + transactionManager.updateWalletManager(networkId, derivationPath) } - val preparedSwapConfigState = PreparedSwapConfigState( - isAllowedToSpend = isAllowedToSpend, - isBalanceEnough = isBalanceEnough, - isFeeEnough = isFeeEnough, - ) - return if (isAllowedToSpend && isBalanceEnough && isFeeEnough) { + val isBalanceWithoutFeeEnough = isBalanceEnough(fromToken, amount, null) + return if (isAllowedToSpend && isBalanceWithoutFeeEnough) { loadSwapData( networkId = networkId, fromTokenAddress = fromTokenAddress, @@ -172,7 +166,6 @@ internal class SwapInteractorImpl @Inject constructor( fromToken = fromToken, toToken = toToken, amount = amount, - preparedSwapConfigState = preparedSwapConfigState, ) } else { loadQuoteData( @@ -182,44 +175,45 @@ internal class SwapInteractorImpl @Inject constructor( amount = amount, fromToken = fromToken, toToken = toToken, - preparedSwapConfigState = preparedSwapConfigState, + 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 = - increaseByPercents(TWENTY_FIVE_PERCENTS, 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, + 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(swapData.fromTokenAmount, currencyToSend.symbol), - toAmount = amountFormatter.formatSwapAmountToUI(swapData.toTokenAmount, currencyToGet.symbol), - txAddress = userWalletManager.getLastTransactionHash(networkId) ?: "", + fromAmount = amountFormatter.formatSwapAmountToUI( + swapStateData.swapModel.fromTokenAmount, + currencyToSend.symbol, + ), + toAmount = amountFormatter.formatSwapAmountToUI( + swapStateData.swapModel.toTokenAmount, + currencyToGet.symbol, + ), + txAddress = userWalletManager.getLastTransactionHash(networkId, derivationPath) ?: "", ) } SendTxResult.UserCancelledError -> TxState.UserCancelled @@ -294,30 +288,11 @@ internal class SwapInteractorImpl @Inject constructor( } } - private suspend fun getAndUpdateFee(networkId: String, fromToken: Currency): BigDecimal? { - val lastFee = cache.getLastFeeForNetwork(networkId) - if (lastFee == null) { - if (userWalletManager.getNativeTokenBalance(networkId)?.value?.compareTo(BigDecimal.ZERO) == 0) { - return null - } - val transactionData = repository.dataToApprove(networkId, getTokenAddress(fromToken)) - val fee = transactionManager.getFee( - networkId, - BigDecimal.ZERO, - cryptoCurrencyConverter.convert(fromToken), - transactionData.toAddress, - ).value - cache.cacheLastFeeForNetwork(fee, networkId) - return fee - } - return lastFee - } - private suspend fun checkAllowance(networkId: String, fromTokenAddress: String): Boolean { 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 } @@ -351,7 +326,8 @@ internal class SwapInteractorImpl @Inject constructor( amount: SwapAmount, fromToken: Currency, toToken: Currency, - preparedSwapConfigState: PreparedSwapConfigState, + isAllowedToSpend: Boolean, + isBalanceWithoutFeeEnough: Boolean, ): SwapState { repository.findBestQuote( networkId = networkId, @@ -361,35 +337,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, - preparedSwapConfigState = preparedSwapConfigState, - 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) @@ -399,13 +365,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() @@ -422,7 +386,6 @@ internal class SwapInteractorImpl @Inject constructor( fromToken: Currency, toToken: Currency, amount: SwapAmount, - preparedSwapConfigState: PreparedSwapConfigState, ): SwapState { repository.prepareSwapTransaction( networkId = networkId, @@ -434,17 +397,27 @@ 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, + derivationPath = derivationPath, ) - 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 +425,19 @@ internal class SwapInteractorImpl @Inject constructor( fromTokenAmount = swapData.fromTokenAmount, toTokenAmount = swapData.toTokenAmount, formattedFee = formattedFee, - preparedSwapConfigState = preparedSwapConfigState, - swapDataModel = swapData, + 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 +452,8 @@ internal class SwapInteractorImpl @Inject constructor( toToken: Currency, fromTokenAmount: SwapAmount, toTokenAmount: SwapAmount, - formattedFee: String, - preparedSwapConfigState: PreparedSwapConfigState, - swapDataModel: SwapDataModel?, + formattedFee: String?, + swapStateData: SwapStateData?, ): SwapState.QuotesLoadedState { val appCurrency = userWalletManager.getUserAppCurrency() val nativeToken = userWalletManager.getNativeTokenForNetwork(networkId) @@ -511,21 +491,18 @@ internal class SwapInteractorImpl @Inject constructor( toRate = rates[toToken.id] ?: 0.0, ), networkCurrency = userWalletManager.getNetworkCurrency(networkId), - preparedSwapConfigState = preparedSwapConfigState, - swapDataModel = swapDataModel, + swapDataModel = swapStateData, tangemFee = getTangemFee(), ) } @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 +514,21 @@ 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, + derivationPath = derivationPath, + ) + 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 +537,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, ), ), @@ -559,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) }) } @@ -574,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 { @@ -597,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 -> { @@ -613,11 +607,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,10 +625,8 @@ 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.5 + private const val INCREASE_FEE_TO_CHECK_ENOUGH_PERCENT = 1.4 private const val USDT_SYMBOL = "USDT" private const val USDC_SYMBOL = "USDC" private const val INFINITY_SYMBOL = "∞" 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 1515683e56..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 @@ -5,18 +5,23 @@ 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.PreparedSwapConfigState import com.tangem.feature.swap.domain.models.domain.SwapDataModel +import java.math.BigDecimal 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, + val preparedSwapConfigState: PreparedSwapConfigState = PreparedSwapConfigState( + isAllowedToSpend = false, + isBalanceEnough = false, + isFeeEnough = false, + ), val permissionState: PermissionDataState = PermissionDataState.Empty, - val swapDataModel: SwapDataModel? = null, + val swapDataModel: SwapStateData? = null, val tangemFee: Double, ) : SwapState @@ -55,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/build.gradle.kts b/features/swap/presentation/build.gradle.kts index 684ec4d57c..287b9e6ad6 100644 --- a/features/swap/presentation/build.gradle.kts +++ b/features/swap/presentation/build.gradle.kts @@ -74,6 +74,7 @@ dependencies { /** Other libraries */ implementation(Library.composeShimmer) implementation(Library.kotlinSerialization) + implementation(Library.timber) /** DI */ implementation(Library.hilt) 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/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 1b52c30bb6..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 @@ -41,6 +41,7 @@ import com.tangem.core.ui.components.SimpleOkDialog import com.tangem.core.ui.components.SmallInfoCard import com.tangem.core.ui.components.SmallInfoCardWithDisclaimer import com.tangem.core.ui.components.SmallInfoCardWithWarning +import com.tangem.core.ui.components.SpacerH8 import com.tangem.core.ui.components.WarningCard import com.tangem.core.ui.components.appbar.AppBarWithBackButton import com.tangem.core.ui.components.keyboardAsState @@ -254,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( @@ -270,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 = "") @@ -343,6 +348,7 @@ private fun SwapWarnings( // ) // } } + SpacerH8() } } } 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 ddf5f6876c..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 @@ -1,6 +1,5 @@ package com.tangem.feature.swap.viewmodels -import android.util.Log import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue @@ -13,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 @@ -32,6 +31,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.Json +import timber.log.Timber import java.text.DecimalFormat import java.text.NumberFormat import java.util.* @@ -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) } @@ -122,7 +124,7 @@ internal class SwapViewModel @Inject constructor( ) } .onFailure { - Log.e("SwapViewModel", it.message ?: it.cause.toString()) + Timber.e(it) } } } @@ -169,9 +171,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, @@ -198,25 +199,26 @@ internal class SwapViewModel @Inject constructor( ) } is SwapState.SwapError -> { + Timber.e("SwapError when loading quotes ${swapState.error}") uiState = stateBuilder.mapError(uiState, swapState.error) { startLoadingQuotesFromLastState() } } } }, onError = { + Timber.e("Error when loading quotes: $it") uiState = stateBuilder.addWarning(uiState, null) { startLoadingQuotesFromLastState() } }, ) } - 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, ) } } @@ -228,7 +230,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), @@ -274,8 +276,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..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 @@ -1,20 +1,22 @@ 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 interface TransactionManager { + @Suppress("LongParameterList") @Throws(IllegalStateException::class) suspend fun sendApproveTransaction( networkId: String, feeAmount: BigDecimal, - estimatedGas: Int, + gasLimit: Int, destinationAddress: String, dataToSign: String, + derivationPath: String?, ): SendTxResult @Suppress("LongParameterList") @@ -23,26 +25,30 @@ interface TransactionManager { networkId: String, amountToSend: BigDecimal, feeAmount: BigDecimal, - estimatedGas: Int, + gasLimit: Int, destinationAddress: String, dataToSign: String, isSwap: Boolean, currencyToSend: Currency, + derivationPath: String?, ): SendTxResult + @Suppress("LongParameterList") @Throws(IllegalStateException::class) suspend fun getFee( networkId: String, amountToSend: BigDecimal, currencyToSend: Currency, destinationAddress: String, - ): ProxyAmount + 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 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