Updated on 2026-08-14

This commit is contained in:
Tangem 2024-03-18 16:20:25 +03:00
parent 7bcadd9f4c
commit e1480e005e
10 changed files with 154 additions and 116 deletions

View file

@ -3,6 +3,7 @@ package com.tangem.tap.domain.userWalletList.implementation
import com.tangem.common.* import com.tangem.common.*
import com.tangem.domain.wallets.legacy.UserWalletsListError import com.tangem.domain.wallets.legacy.UserWalletsListError
import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.legacy.UserWalletsListManager.Lockable.UnlockType
import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.tap.domain.userWalletList.model.UserWalletEncryptionKey import com.tangem.tap.domain.userWalletList.model.UserWalletEncryptionKey
@ -56,8 +57,8 @@ internal class BiometricUserWalletsListManager(
override val walletsCount: Int override val walletsCount: Int
get() = state.value.userWallets.size get() = state.value.userWallets.size
override suspend fun unlock(throwIfNotAllWalletsUnlocked: Boolean): CompletionResult<UserWallet> { override suspend fun unlock(type: UnlockType): CompletionResult<UserWallet> {
return unlockWithBiometryInternal() return unlockAndSetSelectedUserWallet(type)
.mapFailure { error -> .mapFailure { error ->
Timber.e(error, "Unable to unlock user wallets") Timber.e(error, "Unable to unlock user wallets")
if (error is UserWalletsListError) { if (error is UserWalletsListError) {
@ -66,16 +67,8 @@ internal class BiometricUserWalletsListManager(
UserWalletsListError.UnableToUnlockUserWallets(error) UserWalletsListError.UnableToUnlockUserWallets(error)
} }
} }
.map { .map { selectedUserWallet ->
val userWallets = state.value.userWallets if (selectedUserWallet == null || selectedUserWallet.isLocked) {
if (throwIfNotAllWalletsUnlocked && userWallets.any(UserWallet::isLocked)) {
Timber.e("Some user wallets remain locked")
throw UserWalletsListError.NotAllUserWalletsUnlocked
}
val selectedUserWallet = selectedUserWalletSync
if (selectedUserWallet == null) {
Timber.e("Unable to find selected user wallet") Timber.e("Unable to find selected user wallet")
throw UserWalletsListError.NoUserWalletSelected throw UserWalletsListError.NoUserWalletSelected
} else { } else {
@ -186,99 +179,116 @@ internal class BiometricUserWalletsListManager(
changeSelectedUserWallet: Boolean, changeSelectedUserWallet: Boolean,
canOverridePublicInfo: Boolean, canOverridePublicInfo: Boolean,
): CompletionResult<Unit> { ): CompletionResult<Unit> {
return saveEncryptionKeyIfNotNull(userWallet)
.flatMap { sensitiveInformationRepository.save(userWallet, encryptionKey = it) }
.flatMap { publicInformationRepository.save(userWallet, canOverridePublicInfo) }
.map {
if (changeSelectedUserWallet) {
selectedUserWalletRepository.set(userWallet.walletId)
}
}
.flatMap { loadModels() }
.doOnSuccess {
state.update { prevState ->
prevState.copy(
selectedUserWalletId = if (changeSelectedUserWallet) {
userWallet.walletId
} else {
prevState.selectedUserWalletId
},
isLocked = prevState.userWallets.any { it.isLocked },
)
}
}
}
private suspend fun unlockWithBiometryInternal(): CompletionResult<Unit> {
return keysRepository.getAll()
.map { keys ->
state.update { prevState ->
prevState.copy(
encryptionKeys = (keys + prevState.encryptionKeys).distinctBy { it.walletId },
)
}
}
.flatMap { loadModels() }
.map {
state.update { prevState ->
val hasLockedUserWallets = prevState.userWallets.any { it.isLocked }
prevState.copy(isLocked = hasLockedUserWallets)
}
}
}
private suspend fun saveEncryptionKeyIfNotNull(userWallet: UserWallet): CompletionResult<ByteArray?> {
val encryptionKey = userWallet.scanResponse.card.encryptionKey val encryptionKey = userWallet.scanResponse.card.encryptionKey
?.let { UserWalletEncryptionKey(userWallet.walletId, it) } ?.let { UserWalletEncryptionKey(userWallet.walletId, it) }
?: return CompletionResult.Success(Unit) // No encryption key, no need to save
return if (encryptionKey != null) { return keysRepository.save(encryptionKey)
keysRepository.save(encryptionKey) .flatMap { sensitiveInformationRepository.save(userWallet, encryptionKey = encryptionKey.encryptionKey) }
.doOnSuccess { .flatMap { publicInformationRepository.save(userWallet, canOverridePublicInfo) }
state.update { prevState -> .flatMap {
prevState.copy( loadUserWallets(
encryptionKeys = prevState.encryptionKeys encryptionKeys = state.value.encryptionKeys
.plus(encryptionKey) .plus(encryptionKey)
.distinctBy { it.walletId }, .distinctBy(UserWalletEncryptionKey::walletId),
) )
} }
.doOnSuccess { loadedState ->
if (changeSelectedUserWallet) {
selectedUserWalletRepository.set(userWallet.walletId)
state.value = loadedState.copy(
selectedUserWalletId = userWallet.walletId,
)
} else {
state.value = loadedState
} }
.map { encryptionKey.encryptionKey } }
} else { .map { /* Type erasing */ }
CompletionResult.Success(data = null)
}
} }
private suspend fun loadModels(): CompletionResult<Unit> { private suspend fun unlockAndSetSelectedUserWallet(type: UnlockType): CompletionResult<UserWallet?> {
return getSavedUserWallets() return keysRepository.getAll()
.map { userWallets -> .flatMap { encryptionKeys ->
if (userWallets.isNotEmpty()) { loadUserWallets(
state.update { prevState -> encryptionKeys = state.value.encryptionKeys
val wallets = (userWallets + prevState.userWallets).distinctBy { it.walletId } .plus(encryptionKeys)
.distinctBy(UserWalletEncryptionKey::walletId),
)
}
.map { loadedState ->
when (type) {
UnlockType.ALL -> {
if (loadedState.isLocked) {
Timber.e("Some user wallets remain locked")
prevState.copy( state.value = loadedState
userWallets = wallets,
selectedUserWalletId = findOrSetSelectedWalletId(prevState.selectedUserWalletId, wallets), throw UserWalletsListError.NotAllUserWalletsUnlocked
} else {
val selectedWallet = findOrSetSelectedWallet(
state.value.selectedUserWalletId,
loadedState.userWallets,
)
state.value = loadedState.copy(
selectedUserWalletId = selectedWallet?.walletId,
)
selectedWallet
}
}
UnlockType.ANY -> {
val selectedWallet = findOrSetSelectedWallet(
state.value.selectedUserWalletId,
loadedState.userWallets,
) )
state.value = loadedState.copy(
selectedUserWalletId = selectedWallet?.walletId,
)
selectedWallet
}
UnlockType.ALL_WITHOUT_SELECT -> {
state.value = loadedState
findSelectedUserWallet()
} }
} }
} }
} }
private suspend fun getSavedUserWallets(): CompletionResult<List<UserWallet>> { private suspend fun loadUserWallets(encryptionKeys: List<UserWalletEncryptionKey>): CompletionResult<State> {
return publicInformationRepository.getAll() return publicInformationRepository.getAll()
.map { it.toUserWallets() } .map { it.toUserWallets() }
.flatMap { userWallets -> .flatMap { userWallets ->
sensitiveInformationRepository.getAll(state.value.encryptionKeys) sensitiveInformationRepository.getAll(encryptionKeys)
.map { walletIdToSensitiveInformation -> .map { walletIdToSensitiveInformation ->
userWallets.updateWith(walletIdToSensitiveInformation) userWallets.updateWith(walletIdToSensitiveInformation)
} }
} }
.map { userWallets ->
val prevState = state.value
if (userWallets.isNotEmpty()) {
val newUserWallets = (userWallets + prevState.userWallets)
.distinctBy(UserWallet::walletId)
prevState.copy(
userWallets = newUserWallets,
isLocked = newUserWallets.any(UserWallet::isLocked),
)
} else {
prevState
}
}
} }
private fun findOrSetSelectedWalletId( private fun findOrSetSelectedWallet(
prevSelectedWalletId: UserWalletId?, prevSelectedWalletId: UserWalletId?,
userWallets: List<UserWallet>, userWallets: List<UserWallet>,
): UserWalletId? { ): UserWallet? {
val selectedWalletId = prevSelectedWalletId ?: selectedUserWalletRepository.get() val selectedWalletId = prevSelectedWalletId ?: selectedUserWalletRepository.get()
var possibleSelectedUserWallet = findSelectedUserWallet(userWallets, selectedWalletId) var possibleSelectedUserWallet = findSelectedUserWallet(userWallets, selectedWalletId)
@ -290,7 +300,7 @@ internal class BiometricUserWalletsListManager(
} }
} }
return possibleSelectedUserWallet?.walletId return possibleSelectedUserWallet
} }
private fun changeSelectedUserWalletIdIfNeeded(walletsIdsToRemove: List<UserWalletId>) { private fun changeSelectedUserWalletIdIfNeeded(walletsIdsToRemove: List<UserWalletId>) {

View file

@ -100,10 +100,10 @@ internal class GeneralUserWalletsListManager(
return implementation.value.get(userWalletId) return implementation.value.get(userWalletId)
} }
override suspend fun unlock(throwIfNotAllWalletsUnlocked: Boolean): CompletionResult<UserWallet> { override suspend fun unlock(type: UserWalletsListManager.Lockable.UnlockType): CompletionResult<UserWallet> {
val implementation = implementation.value val implementation = implementation.value
return if (implementation is UserWalletsListManager.Lockable) { return if (implementation is UserWalletsListManager.Lockable) {
implementation.unlock() implementation.unlock(type)
} else { } else {
error("RuntimeUserWalletsListManager is not lockable") error("RuntimeUserWalletsListManager is not lockable")
} }

View file

@ -14,7 +14,7 @@ internal class DelegatedKeystoreManager(
override suspend fun get( override suspend fun get(
masterKeyConfig: KeystoreManager.MasterKeyConfig, masterKeyConfig: KeystoreManager.MasterKeyConfig,
keyAliases: Collection<String>, keyAliases: Set<String>,
): Map<String, SecretKey> { ): Map<String, SecretKey> {
return keystoreManagerProvider().get(masterKeyConfig, keyAliases) return keystoreManagerProvider().get(masterKeyConfig, keyAliases)
} }

View file

@ -89,7 +89,6 @@ internal class BiometricUserWalletsKeysRepository(
.doOnFailure { error -> .doOnFailure { error ->
when (error) { when (error) {
is TangemSdkError.KeystoreInvalidated -> { is TangemSdkError.KeystoreInvalidated -> {
// If the biometric cryptography key was invalidated, then delete all encryption keys
getUserWalletsIds().forEach { userWalletId -> getUserWalletsIds().forEach { userWalletId ->
deleteEncryptionKey(userWalletId) deleteEncryptionKey(userWalletId)
} }
@ -120,7 +119,7 @@ internal class BiometricUserWalletsKeysRepository(
} }
private fun deleteEncryptionKey(userWalletId: UserWalletId) { private fun deleteEncryptionKey(userWalletId: UserWalletId) {
return authenticatedStorage.delete(StorageKey.UserWalletEncryptionKey(userWalletId).name) authenticatedStorage.delete(StorageKey.UserWalletEncryptionKey(userWalletId).name)
} }
private suspend fun getUserWalletsIds(): List<UserWalletId> { private suspend fun getUserWalletsIds(): List<UserWalletId> {

View file

@ -14,6 +14,7 @@ import com.tangem.core.navigation.NavigationAction
import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.userwallets.UserWalletBuilder import com.tangem.domain.userwallets.UserWalletBuilder
import com.tangem.domain.wallets.legacy.UserWalletsListManager.Lockable.UnlockType
import com.tangem.domain.wallets.legacy.unlockIfLockable import com.tangem.domain.wallets.legacy.unlockIfLockable
import com.tangem.tap.* import com.tangem.tap.*
import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter
@ -86,7 +87,7 @@ internal class WelcomeMiddleware {
""".trimIndent(), """.trimIndent(),
) )
userWalletsListManager.unlockIfLockable() userWalletsListManager.unlockIfLockable(type = UnlockType.ANY)
.doOnFailure { error -> .doOnFailure { error ->
Timber.e(error, "Unable to unlock user wallets with biometrics") Timber.e(error, "Unable to unlock user wallets with biometrics")
store.dispatchWithMain(WelcomeAction.ProceedWithBiometrics.Error(error)) store.dispatchWithMain(WelcomeAction.ProceedWithBiometrics.Error(error))

View file

@ -105,16 +105,42 @@ interface UserWalletsListManager {
/** /**
* Receive saved [UserWallet]s, populate [userWallets] flow with it and set [isLocked] as false. * Receive saved [UserWallet]s, populate [userWallets] flow with it and set [isLocked] as false.
* *
* @param throwIfNotAllWalletsUnlocked Indicates that the function must throw * @param type Defines the behavior of the operation.
* [UserWalletsListError.NotAllUserWalletsUnlocked] if not all user wallets are unlocked.
* *
* @return [CompletionResult] of operation, with selected [UserWallet] * @return [CompletionResult] of operation, with selected [UserWallet]
* or null if there is no selected [UserWallet] * or null if there is no selected [UserWallet]
*/ */
suspend fun unlock(throwIfNotAllWalletsUnlocked: Boolean = false): CompletionResult<UserWallet> suspend fun unlock(type: UnlockType): CompletionResult<UserWallet>
/** Remove [UserWallet]s from [userWallets] and set [isLocked] as true */ /** Remove [UserWallet]s from [userWallets] and set [isLocked] as true */
fun lock() fun lock()
/**
* Defines the behavior of the [unlock] operation.
* */
enum class UnlockType {
/**
* Ensures that all stored [UserWallet]s are unlocked,
* or throws [UserWalletsListError.NotAllUserWalletsUnlocked].
*
* In this type [selectedUserWallet] is either a previously selected [UserWallet] or the first stored
* [UserWallet].
* */
ALL,
/**
* Ensures that at least one stored [UserWallet] is unlocked,
* or throws [UserWalletsListError.NoUserWalletSelected].
*
* In this type [selectedUserWallet] is the first stored and unlocked [UserWallet].
* */
ANY,
/**
* Same as [ALL] type, but this type can not change [selectedUserWallet] while unlocking.
* */
ALL_WITHOUT_SELECT,
}
} }
// For provider // For provider

View file

@ -1,6 +1,7 @@
package com.tangem.domain.wallets.legacy package com.tangem.domain.wallets.legacy
import com.tangem.common.CompletionResult import com.tangem.common.CompletionResult
import com.tangem.domain.wallets.legacy.UserWalletsListManager.Lockable.UnlockType
import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWallet
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
@ -43,8 +44,8 @@ val UserWalletsListManager.isLockedSync: Boolean
* *
* @see UserWalletsListManager.Lockable.unlock * @see UserWalletsListManager.Lockable.unlock
* */ * */
suspend fun UserWalletsListManager.unlockIfLockable(): CompletionResult<UserWallet> { suspend fun UserWalletsListManager.unlockIfLockable(type: UnlockType = UnlockType.ANY): CompletionResult<UserWallet> {
return asLockable()?.unlock() ?: CompletionResult.Failure(UserWalletsListError.UnableToUnlockUserWallets()) return asLockable()?.unlock(type) ?: CompletionResult.Failure(UserWalletsListError.UnableToUnlockUserWallets())
} }
/** /**

View file

@ -5,6 +5,7 @@ import arrow.core.raise.either
import arrow.core.raise.ensureNotNull import arrow.core.raise.ensureNotNull
import com.tangem.common.doOnFailure import com.tangem.common.doOnFailure
import com.tangem.domain.wallets.legacy.UserWalletsListError import com.tangem.domain.wallets.legacy.UserWalletsListError
import com.tangem.domain.wallets.legacy.UserWalletsListManager.Lockable.UnlockType
import com.tangem.domain.wallets.legacy.WalletsStateHolder import com.tangem.domain.wallets.legacy.WalletsStateHolder
import com.tangem.domain.wallets.legacy.asLockable import com.tangem.domain.wallets.legacy.asLockable
import com.tangem.domain.wallets.models.UnlockWalletsError import com.tangem.domain.wallets.models.UnlockWalletsError
@ -18,27 +19,26 @@ import com.tangem.domain.wallets.models.UnlockWalletsError
*/ */
class UnlockWalletsUseCase(private val walletsStateHolder: WalletsStateHolder) { class UnlockWalletsUseCase(private val walletsStateHolder: WalletsStateHolder) {
suspend operator fun invoke(throwIfNotAllWalletsUnlocked: Boolean = false): Either<UnlockWalletsError, Unit> = suspend operator fun invoke(type: UnlockType = UnlockType.ANY): Either<UnlockWalletsError, Unit> = either {
either { val userWalletsListManager = ensureNotNull(
val userWalletsListManager = ensureNotNull( value = walletsStateHolder.userWalletsListManager?.asLockable(),
value = walletsStateHolder.userWalletsListManager?.asLockable(), raise = {
raise = { UnlockWalletsError.DataError(
UnlockWalletsError.DataError( cause = IllegalStateException("The lockable user wallets list manager could not be found"),
cause = IllegalStateException("The lockable user wallets list manager could not be found"), )
) },
}, )
)
userWalletsListManager.unlock(throwIfNotAllWalletsUnlocked) userWalletsListManager.unlock(type)
.doOnFailure { error -> .doOnFailure { error ->
val e = when (error) { val e = when (error) {
is UserWalletsListError.NoUserWalletSelected -> UnlockWalletsError.NoUserWalletSelected is UserWalletsListError.NoUserWalletSelected -> UnlockWalletsError.NoUserWalletSelected
is UserWalletsListError.NotAllUserWalletsUnlocked -> is UserWalletsListError.NotAllUserWalletsUnlocked ->
UnlockWalletsError.NotAllUserWalletsUnlocked UnlockWalletsError.NotAllUserWalletsUnlocked
else -> UnlockWalletsError.UnableToUnlockWallets else -> UnlockWalletsError.UnableToUnlockWallets
}
raise(e)
} }
}
raise(e)
}
}
} }

View file

@ -13,6 +13,7 @@ import com.tangem.domain.settings.RemindToRateAppLaterUseCase
import com.tangem.domain.settings.ShouldShowSwapPromoWalletUseCase import com.tangem.domain.settings.ShouldShowSwapPromoWalletUseCase
import com.tangem.domain.tokens.FetchTokenListUseCase import com.tangem.domain.tokens.FetchTokenListUseCase
import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.legacy.UserWalletsListManager.Lockable.UnlockType
import com.tangem.domain.wallets.models.UnlockWalletsError import com.tangem.domain.wallets.models.UnlockWalletsError
import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
@ -135,7 +136,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
analyticsEventHandler.send(MainScreen.UnlockAllWithBiometrics) analyticsEventHandler.send(MainScreen.UnlockAllWithBiometrics)
viewModelScope.launch(dispatchers.main) { viewModelScope.launch(dispatchers.main) {
unlockWalletsUseCase(throwIfNotAllWalletsUnlocked = true) unlockWalletsUseCase(type = UnlockType.ALL_WITHOUT_SELECT)
.onRight { stateHolder.update(CloseBottomSheetTransformer(stateHolder.getSelectedWalletId())) } .onRight { stateHolder.update(CloseBottomSheetTransformer(stateHolder.getSelectedWalletId())) }
.onLeft(::handleUnlockWalletsError) .onLeft(::handleUnlockWalletsError)
} }

View file

@ -87,7 +87,7 @@ web3j = "4.10.1"
# region Tangem # region Tangem
tangemBlockchainSdk = "release-app_5.8-533" tangemBlockchainSdk = "release-app_5.8-533"
#tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds
tangemCardSdk = "release-app_5.8-335" tangemCardSdk = "release-app_5.8-336"
#tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^
# endregion Tangem # endregion Tangem