Updated on 2026-08-14

This commit is contained in:
Tangem 2022-12-29 14:00:54 +03:00
parent 696eea5fb6
commit f65b631c3a
13 changed files with 104 additions and 130 deletions

View file

@ -19,7 +19,7 @@ interface UserWalletsListManager {
suspend fun selectWallet(userWalletId: UserWalletId): CompletionResult<UserWallet>
/**
* Save user wallet
* Save provided user wallet and set it as selected
* @param userWallet [UserWallet] to save
* @param canOverride If false, then terminate with [UserWalletListError.WalletAlreadySaved] when user tries to save an
* already saved card
@ -27,6 +27,17 @@ interface UserWalletsListManager {
* */
suspend fun save(userWallet: UserWallet, canOverride: Boolean = false): CompletionResult<Unit>
/**
* Same as [save] but not change selected user wallet ID
* and not terminate with [UserWalletListError.WalletAlreadySaved] if [UserWallet] already saved
*
* 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]
* */
suspend fun update(userWalletId: UserWalletId, update: (UserWallet) -> UserWallet): CompletionResult<UserWallet>
suspend fun delete(userWalletIds: List<UserWalletId>): CompletionResult<Unit>
suspend fun clear(): CompletionResult<Unit>

View file

@ -10,6 +10,7 @@ import com.tangem.tap.domain.userWalletList.repository.SelectedUserWalletReposit
import com.tangem.tap.domain.userWalletList.repository.UserWalletsKeysRepository
import com.tangem.tap.domain.userWalletList.repository.UserWalletsPublicInformationRepository
import com.tangem.tap.domain.userWalletList.repository.UserWalletsSensitiveInformationRepository
import com.tangem.tap.domain.userWalletList.utils.encryptionKey
import com.tangem.tap.domain.userWalletList.utils.toUserWallets
import com.tangem.tap.domain.userWalletList.utils.updateWith
import kotlinx.coroutines.ExperimentalCoroutinesApi
@ -84,7 +85,7 @@ internal class BiometricUserWalletsListManager(
override suspend fun save(userWallet: UserWallet, canOverride: Boolean): CompletionResult<Unit> {
return if (canOverride) {
saveInternal(userWallet)
saveInternal(userWallet, changeSelectedUserWallet = true)
} else {
val isWalletSaved = state.value.userWallets
.any {
@ -94,11 +95,25 @@ internal class BiometricUserWalletsListManager(
if (isWalletSaved) {
CompletionResult.Failure(UserWalletListError.WalletAlreadySaved)
} else {
saveInternal(userWallet)
saveInternal(userWallet, changeSelectedUserWallet = true)
}
}
}
override suspend fun update(
userWalletId: UserWalletId,
update: (UserWallet) -> UserWallet,
): CompletionResult<UserWallet> {
return get(userWalletId)
.map { storedUserWallet ->
update(storedUserWallet)
}
.flatMap { updatedUserWallet ->
saveInternal(updatedUserWallet, changeSelectedUserWallet = false)
.map { updatedUserWallet }
}
}
override suspend fun delete(userWalletIds: List<UserWalletId>): CompletionResult<Unit> {
if (userWalletIds.isEmpty()) {
return CompletionResult.Success(Unit)
@ -111,9 +126,12 @@ internal class BiometricUserWalletsListManager(
.flatMap { keysRepository.delete(userWalletIds) }
.map {
state.update { prevState ->
val newUserWallets = prevState.userWallets.filter { it.walletId !in userWalletIds }
prevState.copy(
encryptionKeys = prevState.encryptionKeys.filter { it.walletId !in userWalletIds },
userWallets = prevState.userWallets.filter { it.walletId !in userWalletIds },
userWallets = newUserWallets,
isLocked = newUserWallets.any { it.isLocked },
)
}
}
@ -135,27 +153,23 @@ internal class BiometricUserWalletsListManager(
}
}
private suspend fun saveInternal(userWallet: UserWallet): CompletionResult<Unit> {
val newEncryptionKeys = state.value.encryptionKeys
.plus(UserWalletEncryptionKey(userWallet))
.distinctBy { it.walletId }
return keysRepository.store(newEncryptionKeys)
.doOnSuccess {
state.update { prevState ->
prevState.copy(
encryptionKeys = newEncryptionKeys,
selectedUserWalletId = userWallet.walletId,
)
}
}
private suspend fun saveInternal(
userWallet: UserWallet,
changeSelectedUserWallet: Boolean,
): CompletionResult<Unit> {
return saveEncryptionKeyIfNotNull(userWallet)
.flatMap { sensitiveInformationRepository.save(userWallet, encryptionKey = it) }
.flatMap { publicInformationRepository.save(userWallet) }
.flatMap { sensitiveInformationRepository.save(userWallet) }
.map { 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 },
)
}
@ -181,6 +195,27 @@ internal class BiometricUserWalletsListManager(
}
}
private suspend fun saveEncryptionKeyIfNotNull(userWallet: UserWallet): CompletionResult<ByteArray?> {
val encryptionKey = userWallet.scanResponse.card.encryptionKey
?.let { UserWalletEncryptionKey(userWallet.walletId, it) }
return if (encryptionKey != null) {
keysRepository.save(encryptionKey)
.doOnSuccess {
state.update { prevState ->
prevState.copy(
encryptionKeys = prevState.encryptionKeys
.plus(encryptionKey)
.distinctBy { it.walletId },
)
}
}
.map { encryptionKey.encryptionKey }
} else {
CompletionResult.Success(data = null)
}
}
private suspend fun loadModels(): CompletionResult<Unit> {
return getSavedUserWallets()
.map { userWallets ->

View file

@ -1,56 +0,0 @@
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.UserWalletsListManager
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
class DummyUserWalletsListManager : UserWalletsListManager {
override val userWallets: Flow<List<UserWallet>>
get() = flowOf(emptyList())
override val selectedUserWallet: Flow<UserWallet>
get() = flowOf()
override val selectedUserWalletSync: UserWallet?
get() = null
override val isLocked: Flow<Boolean>
get() = flowOf(true)
override val isLockedSync: Boolean
get() = true
override val hasSavedUserWallets: Boolean
get() = false
override suspend fun unlockWithBiometry(): CompletionResult<UserWallet?> {
return CompletionResult.Success(null)
}
override fun lock() {
/* no-op */
}
override suspend fun selectWallet(userWalletId: UserWalletId): CompletionResult<UserWallet> {
return catching {
error("Not implemented")
}
}
override suspend fun save(userWallet: UserWallet, canOverride: Boolean): CompletionResult<Unit> {
return CompletionResult.Success(Unit)
}
override suspend fun delete(userWalletIds: List<UserWalletId>): CompletionResult<Unit> {
return CompletionResult.Success(Unit)
}
override suspend fun clear(): CompletionResult<Unit> {
return CompletionResult.Success(Unit)
}
override suspend fun get(userWalletId: UserWalletId): CompletionResult<UserWallet> {
return catching {
error("Not implemented")
}
}
}

View file

@ -2,19 +2,12 @@ package com.tangem.tap.domain.userWalletList.model
import com.squareup.moshi.JsonClass
import com.tangem.domain.common.util.UserWalletId
import com.tangem.tap.domain.model.UserWallet
import com.tangem.tap.domain.userWalletList.utils.encryptionKey
@JsonClass(generateAdapter = true)
internal data class UserWalletEncryptionKey(
val walletId: UserWalletId,
val encryptionKey: ByteArray,
) {
constructor(userWallet: UserWallet) : this(
walletId = userWallet.walletId,
encryptionKey = userWallet.scanResponse.card.encryptionKey,
)
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is UserWalletEncryptionKey) return false

View file

@ -14,11 +14,11 @@ internal interface UserWalletsKeysRepository {
suspend fun getAll(): CompletionResult<List<UserWalletEncryptionKey>>
/**
* Store the encryption keys for user wallets. Biometric authentication not required
* @param encryptionKeys List of encryption keys for user wallets
* Save the encryption key for user wallet. Biometric authentication not required
* @param encryptionKey [UserWalletEncryptionKey] to save
* @return [CompletionResult] of operation
* */
suspend fun store(encryptionKeys: List<UserWalletEncryptionKey>): CompletionResult<Unit>
suspend fun save(encryptionKey: UserWalletEncryptionKey): CompletionResult<Unit>
/**
* Delete encryption keys for user wallets. Biometric authentication not required

View file

@ -7,7 +7,7 @@ import com.tangem.tap.domain.userWalletList.model.UserWalletEncryptionKey
import com.tangem.tap.domain.userWalletList.model.UserWalletSensitiveInformation
internal interface UserWalletsSensitiveInformationRepository {
suspend fun save(userWallet: UserWallet): CompletionResult<Unit>
suspend fun save(userWallet: UserWallet, encryptionKey: ByteArray?): CompletionResult<Unit>
suspend fun getAll(
encryptionKeys: List<UserWalletEncryptionKey>,
): CompletionResult<Map<UserWalletId, UserWalletSensitiveInformation>>

View file

@ -45,10 +45,9 @@ internal class BiometricUserWalletsKeysRepository(
}
}
override suspend fun store(encryptionKeys: List<UserWalletEncryptionKey>): CompletionResult<Unit> {
override suspend fun save(encryptionKey: UserWalletEncryptionKey): CompletionResult<Unit> {
return withContext(Dispatchers.IO) {
encryptionKeys.map { storeEncryptionKey(it) }
.fold()
storeEncryptionKey(encryptionKey)
.mapFailure { error ->
UserWalletListError.SaveEncryptionKeysError(error.cause ?: error)
}

View file

@ -38,11 +38,12 @@ internal class DefaultUserWalletsSensitiveInformationRepository(
Cipher.getInstance("$algorithm/$blockMode/$encryptionPadding")
}
override suspend fun save(userWallet: UserWallet): CompletionResult<Unit> {
override suspend fun save(userWallet: UserWallet, encryptionKey: ByteArray?): CompletionResult<Unit> {
if (encryptionKey == null) return CompletionResult.Success(Unit) // Encryption key is null, do nothing
return catching {
val encryptedSensitiveInformation = userWallet.sensitiveInformation
.encode()
.encryptAndStoreIv(userWallet.walletId.stringValue, userWallet.scanResponse.card.encryptionKey)
.encryptAndStoreIv(userWallet.walletId.stringValue, encryptionKey)
getAllEncrypted().toMutableMap()
.apply { set(userWallet.walletId.stringValue, encryptedSensitiveInformation) }

View file

@ -4,15 +4,8 @@ import com.tangem.common.extensions.calculateSha256
import com.tangem.domain.common.CardDTO
import com.tangem.domain.common.extensions.calculateHmacSha256
internal val CardDTO.encryptionKey: ByteArray
@Throws(IllegalArgumentException::class)
get() {
val walletPublicKey = requireNotNull(findPublicKey(wallets)) {
"Wallet public key must not be null"
}
return calculateEncryptionKey(walletPublicKey)
}
internal val CardDTO.encryptionKey: ByteArray?
get() = findPublicKey(wallets)?.let { calculateEncryptionKey(it) }
private fun calculateEncryptionKey(publicKey: ByteArray): ByteArray {
val message = MESSAGE_FOR_ENCRYPTION_KEY.toByteArray()

View file

@ -281,13 +281,16 @@ class TokensMiddleware {
) {
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync
if (selectedUserWallet != null) {
val updatedUserWallet = selectedUserWallet.copy(
scanResponse = scanResponse,
)
scope.launch {
userWalletsListManager.save(updatedUserWallet, canOverride = true)
.flatMap {
userWalletsListManager.update(
userWalletId = selectedUserWallet.walletId,
update = { userWallet ->
userWallet.copy(
scanResponse = scanResponse,
)
},
)
.flatMap { updatedUserWallet ->
walletCurrenciesManager.addCurrencies(
userWallet = updatedUserWallet,
currenciesToAdd = currencyList,

View file

@ -180,21 +180,24 @@ class MultiWalletMiddleware {
}
private fun scanAndUpdateCard(
selectedWallet: UserWallet,
selectedUserWallet: UserWallet,
state: WalletState?,
) = scope.launch {
Analytics.send(MainScreen.CardWasScanned())
ScanCardProcessor.scan(
cardId = selectedWallet.cardId,
cardId = selectedUserWallet.cardId,
additionalBlockchainsToDerive = state?.missingDerivations?.map { it.blockchain },
) { scanResponse ->
val userWallet = selectedWallet.copy(
scanResponse = scanResponse,
userWalletsListManager.update(
userWalletId = selectedUserWallet.walletId,
update = { userWallet ->
userWallet.copy(
scanResponse = scanResponse,
)
},
)
userWalletsListManager.save(userWallet, canOverride = true)
.doOnSuccess {
store.state.globalState.tapWalletManager.loadData(userWallet, refresh = true)
.doOnSuccess { updatedUserWallet ->
store.state.globalState.tapWalletManager.loadData(updatedUserWallet, refresh = true)
}
}
}

View file

@ -239,9 +239,7 @@ internal class WalletSelectorMiddleware {
Analytics.send(MyWallets.Button.EditWalletTapped)
scope.launch {
userWalletsListManager.get(userWalletId)
.map { it.copy(name = newName) }
.flatMap { userWalletsListManager.save(it, canOverride = true) }
userWalletsListManager.update(userWalletId) { it.copy(name = newName) }
.doOnFailure { error ->
store.dispatchOnMain(WalletSelectorAction.HandleError(error))
}

View file

@ -38,7 +38,6 @@ internal class WalletSelectorViewModel : ViewModel(), StoreSubscriber<WalletSele
fun walletClicked(userWalletId: UserWalletId) = with(state.value) {
when {
editingUserWalletsIds.isNotEmpty() && isWalletLocked(userWalletId, this) -> Unit
editingUserWalletsIds.isNotEmpty() && !editingUserWalletsIds.contains(userWalletId) -> {
editWallet(userWalletId)
}
@ -52,7 +51,7 @@ internal class WalletSelectorViewModel : ViewModel(), StoreSubscriber<WalletSele
}
fun walletLongClicked(userWalletId: UserWalletId) = with(state.value) {
if (!isWalletLocked(userWalletId, this) && editingUserWalletsIds.isEmpty()) {
if (editingUserWalletsIds.isEmpty()) {
editWallet(userWalletId)
}
}
@ -67,15 +66,15 @@ internal class WalletSelectorViewModel : ViewModel(), StoreSubscriber<WalletSele
fun renameWallet() = with(state.value) {
if (editingUserWalletsIds.isNotEmpty() && renameWalletDialog == null) {
val editedWalletId = editingUserWalletsIds.first()
val editedWallet = (multiCurrencyWallets + singleCurrencyWallets)
.find { it.id == editedWalletId }
val editedUserWalletId = editingUserWalletsIds.first()
val editedUserWallet = (multiCurrencyWallets + singleCurrencyWallets)
.find { it.id == editedUserWalletId }
if (editedWallet != null) {
if (editedUserWallet != null) {
val dialog = RenameWalletDialog(
currentName = editedWallet.name,
currentName = editedUserWallet.name,
onApply = { newName ->
store.dispatch(WalletSelectorAction.RenameWallet(editedWalletId, newName))
store.dispatch(WalletSelectorAction.RenameWallet(editedUserWalletId, newName))
stateInternal.update { prevState ->
prevState.copy(
renameWalletDialog = null,
@ -137,11 +136,6 @@ internal class WalletSelectorViewModel : ViewModel(), StoreSubscriber<WalletSele
}
}
private fun isWalletLocked(userWalletId: UserWalletId, state: WalletSelectorScreenState): Boolean = with(state) {
multiCurrencyWallets.find { it.id == userWalletId }?.isLocked
?: singleCurrencyWallets.find { it.id == userWalletId }?.isLocked ?: isLocked
}
private fun subscribeToStoreChanges() {
store.subscribe(this) { appState ->
appState.skip { old, new -> old.walletSelectorState == new.walletSelectorState }