Updated on 2026-08-14

This commit is contained in:
Tangem 2023-03-02 16:06:49 +03:00
commit d6c67f8b4a
61 changed files with 856 additions and 1907 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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<BlockchainNetwork, Result<Wallet>>(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))
@ -156,32 +94,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 +110,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<BlockchainNetwork>, 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<Action>(
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? {

View file

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

View file

@ -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<Currency>) = 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<Currency> {
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

View file

@ -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<Boolean>
/**
* 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<UserWallet?>
/**
* 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<UserWallet>
suspend fun select(userWalletId: UserWalletId): CompletionResult<UserWallet>
/**
* 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<UserWallet>
/**
* 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<UserWallet>
interface Lockable : UserWalletsListManager {
/**
* Indicates that all [UserWallet]s is locked
*
* @see [isLockedSync]
* @see [UserWallet.isLocked]
* */
val isLocked: Flow<Boolean>
/**
* 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<UserWallet?>
/**
* Remove [UserWallet]s from [userWallets] and set [isLocked] as true
* */
fun lock()
}
// For provider
companion object
}

View file

@ -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<Boolean>
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<UserWallet?> {
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
}

View file

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

View file

@ -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<List<UserWallet>>
@ -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<UserWallet?> {
override suspend fun unlock(): CompletionResult<UserWallet?> {
return unlockWithBiometryInternal()
.map { selectedUserWalletSync }
}
@ -62,20 +62,20 @@ internal class BiometricUserWalletsListManager(
state.update { State() }
}
override suspend fun selectWallet(userWalletId: UserWalletId): CompletionResult<UserWallet> = catching {
override suspend fun select(userWalletId: UserWalletId): CompletionResult<UserWallet> = 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<Unit> {
@ -289,7 +289,7 @@ internal class BiometricUserWalletsListManager(
}
private fun findSelectedUserWallet(userWallets: List<UserWallet> = state.value.userWallets): UserWallet? {
return userWallets.find {
return userWallets.firstOrNull {
it.walletId == state.value.selectedUserWalletId
}
}

View file

@ -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<List<UserWallet>>
get() = state
.mapLatest { listOfNotNull(it.userWallet) }
.distinctUntilChanged()
override val selectedUserWallet: Flow<UserWallet>
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<UserWallet> = catching {
state.value.userWallet
?.takeIf { it.walletId == userWalletId }
?: walletNotFound()
}
override suspend fun save(userWallet: UserWallet, canOverride: Boolean): CompletionResult<Unit> {
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<UserWallet> = 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<UserWalletId>): CompletionResult<Unit> = clear()
override suspend fun clear(): CompletionResult<Unit> = catching {
state.update { prevState ->
prevState.copy(
userWallet = null,
)
}
}
override suspend fun get(userWalletId: UserWalletId): CompletionResult<UserWallet> = catching {
state.value.userWallet ?: walletNotFound()
}
private fun saveInternal(userWallet: UserWallet): CompletionResult<Unit> = 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,
)
}

View file

@ -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<Blockchain>,
updatedCurrencies: List<Currency>,
): CompletionResult<Unit> {
val updatedBlockchains = updatedCurrencies
.filterIsInstance<Currency.Blockchain>()
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(

View file

@ -142,8 +142,8 @@ internal class DefaultWalletAmountsRepository(
Timber.e(
error,
"""
Unable to fetch fiat rates
|- Coins ids: $coinsIds
Unable to fetch fiat rates
|- Coins ids: $coinsIds
""".trimIndent(),
)
@ -278,6 +278,7 @@ internal class DefaultWalletAmountsRepository(
Unable to fetch amounts
|- User wallet id: ${walletStore.userWalletId}
|- Blockchain: ${walletStore.blockchain}
|- Derivation path: ${walletStore.derivationPath?.rawPath}
""".trimIndent(),
)
@ -310,6 +311,7 @@ internal class DefaultWalletAmountsRepository(
Fetched amounts
|- User wallet id: ${walletStore.userWalletId}
|- Blockchain: ${walletStore.blockchain}
|- Derivation path: ${walletStore.derivationPath?.rawPath}
""".trimIndent(),
)
@ -337,6 +339,7 @@ internal class DefaultWalletAmountsRepository(
Missed derivation
|- User wallet id: ${walletStore.userWalletId}
|- Blockchain: ${walletStore.blockchain}
|- Derivation path: ${walletStore.derivationPath?.rawPath}
""".trimIndent(),
)
@ -360,6 +363,7 @@ internal class DefaultWalletAmountsRepository(
Wallet manager is null
|- User wallet id: ${walletStore.userWalletId}
|- Blockchain: ${walletStore.blockchain}
|- Derivation path: ${walletStore.derivationPath?.rawPath}
""".trimIndent(),
)
@ -405,6 +409,7 @@ internal class DefaultWalletAmountsRepository(
Fetched wallet rent
|- User wallet id: ${walletStore.userWalletId}
|- Blockchain: ${walletStore.blockchain}
|- Derivation path: ${walletStore.derivationPath?.rawPath}
|- Rent: $rent
""".trimIndent(),
)

View file

@ -51,6 +51,7 @@ internal class DefaultWalletManagersRepository(
val foundWalletManager = findWalletManager(
userWalletId = userWallet.walletId,
blockchain = blockchainNetwork?.blockchain,
derivationPath = blockchainNetwork?.derivationPath,
)
foundWalletManager?.updateTokens(
@ -85,7 +86,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 +104,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)
}
}
@ -178,15 +193,19 @@ internal class DefaultWalletManagersRepository(
private suspend fun findWalletManager(
userWalletId: UserWalletId,
blockchain: Blockchain?,
derivationPath: String?,
): WalletManager? {
return walletManagersStorage.getAll()
.firstOrNull()
?.get(userWalletId)
?.let { userWalletManagers ->
if (blockchain == null) {
if (blockchain == null || derivationPath == null) {
userWalletManagers.firstOrNull()
} else {
userWalletManagers.firstOrNull { it.wallet.blockchain == blockchain }
userWalletManagers.firstOrNull {
it.wallet.blockchain == blockchain &&
it.wallet.publicKey.derivationPath?.rawPath == derivationPath
}
}
}
}

View file

@ -121,9 +121,10 @@ private inline fun List<WalletStoreModel>.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(),
)

View file

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

View file

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

View file

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

View file

@ -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<String>?) {
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) }
}
}
}

View file

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

View file

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

View file

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

View file

@ -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<AppState> = { _, 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

View file

@ -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
@ -33,7 +34,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 +61,7 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.rekotlin.Action
import org.rekotlin.Middleware
import timber.log.Timber
import java.util.*
/**
@ -333,17 +334,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,
),
)
}

View file

@ -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
@ -30,11 +30,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
@ -44,6 +41,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 {
@ -191,7 +189,7 @@ class TokensMiddleware {
}
}
fun deriveMissingBlockchains(
private fun deriveMissingBlockchains(
scanResponse: ScanResponse,
currencyList: List<Currency>,
onSuccess: (ScanResponse) -> Unit,
@ -268,89 +266,41 @@ class TokensMiddleware {
return DerivationData(
derivations = mapKeyOfWalletPublicKey to toDerive,
alreadyDerivedKeys = alreadyDerivedKeys,
mapKeyOfWalletPublicKey = mapKeyOfWalletPublicKey,
)
}
private class DerivationData(
val derivations: Pair<ByteArrayKey, List<DerivationPath>>,
val alreadyDerivedKeys: ExtendedPublicKeysMap,
val mapKeyOfWalletPublicKey: ByteArrayKey,
)
private fun submitAdd(scanResponse: ScanResponse, currencyList: List<Currency>) {
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<Currency>) {
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 {

View file

@ -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<BlockchainNetwork>,
val walletManagers: List<WalletManager>,
) : MultiWallet()
data class AddTokens(
val tokens: List<Token>,
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<BlockchainNetwork>,
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<Currency>) : MultiWallet()
data class ShowWalletBackupWarning(val show: Boolean) : MultiWallet()
object BackupWallet : MultiWallet()
object ScheduleCheckForMissingDerivation : MultiWallet()
data class AddMissingDerivations(val blockchains: List<BlockchainNetwork>) : 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<Currency>? = null,
) : WalletAction() {
data class Success(
val fiatRates: Map<Currency, BigDecimal?>,
) : 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()

View file

@ -77,36 +77,34 @@ data class WalletData(
if (currencyData.status == BalanceStatus.SameCurrencyTransactionInProgress) {
walletWarnings.add(WalletWarning.TransactionInProgress(currency.currencyName))
}
}
private fun assembleBlockchainWarnings(walletWarnings: MutableList<WalletWarning>) {
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<WalletWarning>) {
with(currency) {
if (!isToken()) return
private fun assembleBlockchainWarnings(walletWarnings: MutableList<WalletWarning>) = 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<WalletWarning>) = with(currency) {
if (!isToken()) return
if (blockchainAmountIsEmpty() && !tokenAmountIsEmpty()) {
walletWarnings.add(
WalletWarning.BalanceNotEnoughForFee(
currencyName = currencyName,
blockchainFullName = blockchain.fullName,
blockchainSymbol = blockchain.currency,
),
)
}
}

View file

@ -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<WalletData>
@ -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<WalletStore> {
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<WalletData>): WalletState {
private fun updateWalletsData(walletsData: List<WalletData>): 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<WalletStore>): 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<WalletData>.replaceSomeWalletsData(newWallets: List<WalletData>): List<WalletData> {
val remainingWallets: MutableList<WalletData> = 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 }

View file

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

View file

@ -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<Currency> =
(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<WalletManager>) {
walletManagers.forEach {
if (it.wallet.fundsAvailable(AmountType.Coin) == BigDecimal.ZERO) {
DemoHelper.injectDemoBalance(it)
}
}
}
private fun handleAddingWalletManagers(
globalState: GlobalState,
walletManagers: List<WalletManager>,
) {
globalState.feedbackManager?.infoHolder?.setWalletsInfo(walletManagers)
if (globalState.scanResponse?.isDemoCard() == true) {
addDummyBalances(walletManagers)
}
}
private fun addTokens(
tokens: List<Token>,
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
}
}
}
}

View file

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

View file

@ -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<WalletStore> = 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<Token>, 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),
)
}

View file

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

View file

@ -13,12 +13,6 @@ fun List<WalletData>.findProgressState(initialState: ProgressState = ProgressSta
.reduce(ProgressState::or)
}
fun List<WalletData>.calculateTotalFiatAmount(): BigDecimal {
return this
.map { it.currencyData.fiatAmount ?: BigDecimal.ZERO }
.reduce(BigDecimal::plus)
}
fun List<WalletData>.calculateTotalCryptoAmount(): BigDecimal {
return this
.map { it.currencyData.amount ?: BigDecimal.ZERO }

View file

@ -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<AddressData> {
val listOfAddressData = mutableListOf<AddressData>()
// 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<Currency, BigDecimal?>,
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)
}

View file

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

View file

@ -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<Walle
this,
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
val popBackTo = if (userWalletsListManager.hasSavedUserWallets) {
userWalletsListManager.lock()
AppScreen.Welcome
} else {
AppScreen.Home
}
store.dispatch(NavigationAction.PopBackTo(popBackTo))
store.dispatch(WalletAction.PopBackToInitialScreen)
}
},
)
@ -201,7 +194,11 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
}
}
val navigationIconRes = if (state.hasSavedWallets) R.drawable.ic_wallet_24 else R.drawable.ic_tap_card_24
val navigationIconRes = if (state.canSaveUserWallets) {
R.drawable.ic_wallet_24
} else {
R.drawable.ic_tap_card_24
}
binding.toolbar.setNavigationIcon(navigationIconRes)
}

View file

@ -3,28 +3,55 @@ package com.tangem.tap.features.wallet.ui
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.domain.userWalletList.UserWalletsListManager
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.store
import com.tangem.tap.userWalletsListManager
import com.tangem.tap.walletStoresManager
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import org.rekotlin.StoreSubscriber
// TODO: Kill me, please
@OptIn(ExperimentalCoroutinesApi::class)
internal class WalletViewModel : ViewModel(), StoreSubscriber<UserWalletsListManager?> {
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 }
}
}
}

View file

@ -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<WalletState> {
val totalBalanceStrategy: DiffStrategy<WalletState> = { 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)

View file

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

View file

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

View file

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

View file

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

View file

@ -29,7 +29,7 @@ class DerivationManagerImpl(
private val appStateHolder: AppStateHolder,
) : DerivationManager {
override suspend fun deriveMissingBlockchains(currency: Currency) = suspendCoroutine<Boolean> { 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<ByteArrayKey, List<DerivationPath>>,
val alreadyDerivedKeys: ExtendedPublicKeysMap,
val mapKeyOfWalletPublicKey: ByteArrayKey,
)
}

View file

@ -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<Currency> {
@ -110,21 +103,14 @@ class UserWalletManagerImpl(
val blockchain = requireNotNull(Blockchain.fromNetworkId(currency.networkId)) { "blockchain not found" }
val blockchainNetwork = BlockchainNetwork(blockchain, card)
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 {
@ -144,7 +130,9 @@ class UserWalletManagerImpl(
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 }
return walletManager?.wallet?.recentTransactions
?.lastOrNull { it.hash?.isNotEmpty() == true }
?.hash?.let { HEX_PREFIX + it }
}
override suspend fun getCurrentWalletTokensBalance(
@ -205,60 +193,11 @@ 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)
@ -269,7 +208,6 @@ class UserWalletManagerImpl(
companion object {
private const val HEX_PREFIX = "0x"
private const val DELAY_UPDATE_WALLET = 500L
}
}

View file

@ -1,6 +1,5 @@
package com.tangem.tap.proxy.di
import com.tangem.blockchain.common.WalletManagerFactory
import com.tangem.lib.crypto.DerivationManager
import com.tangem.lib.crypto.TransactionManager
import com.tangem.lib.crypto.UserWalletManager
@ -29,7 +28,6 @@ class ProxyModule {
fun provideUserWalletManager(appStateHolder: AppStateHolder): UserWalletManager {
return UserWalletManagerImpl(
appStateHolder = appStateHolder,
walletManagerFactory = WalletManagerFactory(),
)
}

View file

@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
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">
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">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline_horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.5" />
android:id="@+id/guideline_horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.5" />
<TextView
android:id="@+id/tv_currency"
@ -64,16 +64,19 @@
android:id="@+id/tv_status"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:ellipsize="end"
android:maxLines="1"
android:textColor="@color/text_tertiary"
android:textSize="12sp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/tv_amount"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/guideline_horizontal"
app:lineHeight="20dp"
tools:text="Unreachable..." />
tools:text="Transaction in progress and it very long text with some strange status"
tools:visibility="visible" />
<TextView
android:id="@+id/tv_exchange_rate"
@ -86,5 +89,7 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/guideline_horizontal"
app:lineHeight="20dp"
tools:text="46 908 $" />
tools:text="46 908 $"
tools:visibility="gone" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -61,11 +61,11 @@ object Versions {
// endregion Other libraries
// region Tangem
const val tangemBlockchainSdk = "develop-168"
const val tangemBlockchainSdk = "develop-173"
// 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-198"
// const val tangemCardSdk = "0.0.1" // Keep it! - used for local builds
// endregion Tangem
// region Tools

View file

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

View file

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

View file

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

View file

@ -13,15 +13,12 @@ val FirmwareVersion.Companion.SolanaTokensAvailable
get() = FirmwareVersion(4, 52)
fun CardDTO.supportedBlockchains(): List<Blockchain> {
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<Blockchain> {
}
}
}
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 {

View file

@ -35,8 +35,24 @@ internal class SwapRepositoryImpl @Inject constructor(
private val approveConverter = ApproveConverter()
override suspend fun getRates(currencyId: String, tokenIds: List<String>): Map<String, Double> {
// 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"
}
}

View file

@ -120,7 +120,7 @@ internal class SwapInteractorImpl @Inject constructor(
val result = transactionManager.sendApproveTransaction(
networkId = networkId,
feeAmount = fee,
estimatedGas = increasedEstimatedGas,
estimatedGas = estimatedGas,
destinationAddress = transactionData.toAddress,
dataToSign = transactionData.data,
)
@ -152,38 +152,54 @@ 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)
}
// load initial quotes data, it works despite balance
val quotesData = loadQuoteData(
networkId = networkId,
fromTokenAddress = fromTokenAddress,
toTokenAddress = toTokenAddress,
amount = amount,
fromToken = fromToken,
toToken = toToken,
)
// get fee from loaded quotes data, if error, use blockchain fee for 0 amount tx
val fee = getInchFee(quotesData)
val isFeeEnough = checkFeeIsEnough(
fee = fee,
spendAmount = amount,
networkId = networkId,
fromToken = fromToken,
)
val isBalanceEnough = isBalanceEnough(fromToken, amount, fee)
val preparedSwapConfigState = PreparedSwapConfigState(
isAllowedToSpend = isAllowedToSpend,
isBalanceEnough = isBalanceEnough,
isFeeEnough = isFeeEnough,
)
return if (isAllowedToSpend && isBalanceEnough && isFeeEnough) {
loadSwapData(
// if enough balance, fee and spend was allowed, request swap data
val swapData = loadSwapData(
networkId = networkId,
fromTokenAddress = fromTokenAddress,
toTokenAddress = toTokenAddress,
fromToken = fromToken,
toToken = toToken,
amount = amount,
preparedSwapConfigState = preparedSwapConfigState,
)
if (swapData is SwapState.QuotesLoadedState) {
swapData.copy(preparedSwapConfigState = preparedSwapConfigState)
} else {
swapData
}
} else {
loadQuoteData(
networkId = networkId,
fromTokenAddress = fromTokenAddress,
toTokenAddress = toTokenAddress,
amount = amount,
fromToken = fromToken,
toToken = toToken,
preparedSwapConfigState = preparedSwapConfigState,
)
if (quotesData is SwapState.QuotesLoadedState) {
quotesData.copy(preparedSwapConfigState = preparedSwapConfigState)
} else {
quotesData
}
}
}
@ -195,8 +211,7 @@ internal class SwapInteractorImpl @Inject constructor(
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 estimatedGas = swapData.transaction.gas.toIntOrNull() ?: DEFAULT_GAS
val fee = transactionManager.calculateFee(
networkId = networkId,
gasPrice = swapData.transaction.gasPrice,
@ -250,6 +265,12 @@ internal class SwapInteractorImpl @Inject constructor(
}
}
private fun getInchFee(quotesData: SwapState): BigDecimal? {
return if (quotesData is SwapState.QuotesLoadedState) {
quotesData.feeRaw
} else null
}
private fun selectToToken(
initialToken: Currency,
tokensInWallet: List<Currency>,
@ -294,25 +315,6 @@ 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,
@ -351,7 +353,6 @@ internal class SwapInteractorImpl @Inject constructor(
amount: SwapAmount,
fromToken: Currency,
toToken: Currency,
preparedSwapConfigState: PreparedSwapConfigState,
): SwapState {
repository.findBestQuote(
networkId = networkId,
@ -380,7 +381,7 @@ internal class SwapInteractorImpl @Inject constructor(
fromTokenAmount = quoteDataModel.fromTokenAmount,
toTokenAmount = quoteDataModel.toTokenAmount,
formattedFee = formattedFee,
preparedSwapConfigState = preparedSwapConfigState,
feeRaw = fee,
swapDataModel = null,
)
return updatePermissionState(
@ -422,7 +423,6 @@ internal class SwapInteractorImpl @Inject constructor(
fromToken: Currency,
toToken: Currency,
amount: SwapAmount,
preparedSwapConfigState: PreparedSwapConfigState,
): SwapState {
repository.prepareSwapTransaction(
networkId = networkId,
@ -452,8 +452,8 @@ internal class SwapInteractorImpl @Inject constructor(
fromTokenAmount = swapData.fromTokenAmount,
toTokenAmount = swapData.toTokenAmount,
formattedFee = formattedFee,
preparedSwapConfigState = preparedSwapConfigState,
swapDataModel = swapData,
feeRaw = fee,
)
return swapState.copy(
permissionState = PermissionDataState.Empty,
@ -472,7 +472,7 @@ internal class SwapInteractorImpl @Inject constructor(
fromTokenAmount: SwapAmount,
toTokenAmount: SwapAmount,
formattedFee: String,
preparedSwapConfigState: PreparedSwapConfigState,
feeRaw: BigDecimal,
swapDataModel: SwapDataModel?,
): SwapState.QuotesLoadedState {
val appCurrency = userWalletManager.getUserAppCurrency()
@ -511,9 +511,9 @@ internal class SwapInteractorImpl @Inject constructor(
toRate = rates[toToken.id] ?: 0.0,
),
networkCurrency = userWalletManager.getNetworkCurrency(networkId),
preparedSwapConfigState = preparedSwapConfigState,
swapDataModel = swapDataModel,
tangemFee = getTangemFee(),
feeRaw = feeRaw,
)
}
@ -639,7 +639,7 @@ internal class SwapInteractorImpl @Inject constructor(
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 = ""

View file

@ -5,6 +5,7 @@ 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 {
@ -14,10 +15,15 @@ sealed interface SwapState {
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 tangemFee: Double,
val feeRaw: BigDecimal,
) : SwapState
data class EmptyAmountState(

View file

@ -74,6 +74,7 @@ dependencies {
/** Other libraries */
implementation(Library.composeShimmer)
implementation(Library.kotlinSerialization)
implementation(Library.timber)
/** DI */
implementation(Library.hilt)

View file

@ -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
@ -343,6 +344,7 @@ private fun SwapWarnings(
// )
// }
}
SpacerH8()
}
}
}

View file

@ -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
@ -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.*
@ -122,7 +122,7 @@ internal class SwapViewModel @Inject constructor(
)
}
.onFailure {
Log.e("SwapViewModel", it.message ?: it.cause.toString())
Timber.e(it)
}
}
}
@ -198,11 +198,13 @@ 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() }
},
)