Updated on 2026-08-14

This commit is contained in:
Tangem 2022-12-27 11:10:34 +03:00
parent fa41036fca
commit de6b0af228
18 changed files with 147 additions and 90 deletions

View file

@ -3,11 +3,11 @@ package com.tangem.tap.common.analytics.converters
import com.tangem.common.Converter
import com.tangem.common.extensions.isZero
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.util.userWalletId
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Basic
import com.tangem.tap.common.analytics.filters.BasicTopUpFilter
import com.tangem.tap.domain.extensions.isMultiwalletAllowed
import com.tangem.tap.domain.model.builders.UserWalletIdBuilder
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.tap.features.wallet.redux.WalletData
import com.tangem.tap.features.wallet.redux.WalletState
@ -29,7 +29,9 @@ class BasicSignInEventConverter(
currency = cardCurrency,
batch = scanResponse.card.batchId,
).apply {
filterData = scanResponse.card.userWalletId.stringValue
filterData = UserWalletIdBuilder.scanResponse(scanResponse)
.build()
?.stringValue
}
}
}
@ -43,7 +45,7 @@ class BasicTopUpEventConverter(
val cardCurrency = ParamCardCurrencyConverter().convert(scanResponse) ?: return null
val data = BasicTopUpFilter.Data(
walletId = scanResponse.card.userWalletId.stringValue,
walletId = UserWalletIdBuilder.scanResponse(scanResponse).build()?.stringValue ?: "",
cardBalanceState = AnalyticsParam.CardBalanceState.from(value.walletsDataFromStores),
)

View file

@ -10,8 +10,8 @@ import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.address.Address
import com.tangem.domain.common.CardDTO
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.util.userWalletId
import com.tangem.tap.common.extensions.stripZeroPlainString
import com.tangem.tap.domain.model.builders.UserWalletIdBuilder
class AdditionalFeedbackInfo {
class EmailWalletInfo(
@ -54,7 +54,7 @@ class AdditionalFeedbackInfo {
cardFirmwareVersion = data.card.firmwareVersion.stringValue
cardIssuer = data.card.issuer.name
signedHashesCount = formatSignedHashes(data.card.wallets)
userWalletId = data.card.userWalletId.stringValue
userWalletId = UserWalletIdBuilder.scanResponse(data).build()?.stringValue ?: ""
}
fun setWalletsInfo(walletManagers: List<WalletManager>) {

View file

@ -1,6 +1,5 @@
package com.tangem.tap.domain.model
import com.tangem.common.extensions.toHexString
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.util.UserWalletId
@ -33,23 +32,4 @@ data class UserWallet(
val isLocked: Boolean
get() = scanResponse.card.wallets.isEmpty()
}
/**
* !!! Workaround !!!
*
* Calculate same [UserWalletId] for twins instead
*
* TODO: Remove after [REDACTED_JIRA]
* */
fun UserWallet.isTwinnedWith(other: UserWallet): Boolean {
if (!scanResponse.isTangemTwins() || !other.scanResponse.isTangemTwins()) return false
if (scanResponse.secondTwinPublicKey == null || other.scanResponse.secondTwinPublicKey == null) return false
if (other.scanResponse.secondTwinPublicKey == scanResponse.card.wallets.firstOrNull()?.publicKey?.toHexString()) {
return true
}
if (scanResponse.secondTwinPublicKey == other.scanResponse.card.wallets.firstOrNull()?.publicKey?.toHexString()) {
return true
}
return false
}

View file

@ -8,7 +8,6 @@ import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.TapWorkarounds.isStart2Coin
import com.tangem.domain.common.TwinCardNumber
import com.tangem.domain.common.TwinsHelper
import com.tangem.domain.common.util.userWalletId
import com.tangem.operations.attestation.OnlineCardVerifier
import com.tangem.operations.attestation.TangemApi
import com.tangem.tap.domain.model.UserWallet
@ -49,16 +48,20 @@ class UserWalletBuilder(
}
}
suspend fun build(): UserWallet {
suspend fun build(): UserWallet? {
return with(scanResponse) {
UserWallet(
walletId = card.userWalletId,
name = userWalletName,
artworkUrl = loadArtworkUrl(card.cardId, card.cardPublicKey),
cardsInWallet = backupCardsIds.plus(card.cardId),
scanResponse = this,
isMultiCurrency = isMultiCurrency,
)
UserWalletIdBuilder.scanResponse(scanResponse)
.build()
?.let {
UserWallet(
walletId = it,
name = userWalletName,
artworkUrl = loadArtworkUrl(card.cardId, card.cardPublicKey),
cardsInWallet = backupCardsIds.plus(card.cardId),
scanResponse = this,
isMultiCurrency = isMultiCurrency,
)
}
}
}

View file

@ -0,0 +1,70 @@
package com.tangem.tap.domain.model.builders
import com.tangem.common.extensions.calculateSha256
import com.tangem.common.extensions.hexToBytes
import com.tangem.crypto.Secp256k1
import com.tangem.domain.common.CardDTO
import com.tangem.domain.common.ProductType
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.TapWorkarounds.isTangemTwins
import com.tangem.domain.common.extensions.calculateHmacSha256
import com.tangem.domain.common.util.UserWalletId
class UserWalletIdBuilder private constructor(
private val publicKey: ByteArray?,
private val pairTwinPublicKey: ByteArray? = null,
) {
fun build(): UserWalletId? {
val seed = if (publicKey != null) {
if (pairTwinPublicKey != null) {
Secp256k1.sum(publicKey, pairTwinPublicKey)
} else {
publicKey
}
} else null
return seed?.let {
UserWalletId(value = calculateUserWalletId(it))
}
}
private fun calculateUserWalletId(seed: ByteArray?): ByteArray? {
val message = MESSAGE_FOR_WALLET_ID.toByteArray()
val keyHash = seed?.calculateSha256()
return if (keyHash != null) {
message.calculateHmacSha256(keyHash)
} else null
}
companion object {
private const val MESSAGE_FOR_WALLET_ID = "UserWalletID"
@Throws(IllegalArgumentException::class)
fun card(card: CardDTO): UserWalletIdBuilder {
require(!card.isTangemTwins) {
"For twin cards use scanResponse to ID calculation"
}
return UserWalletIdBuilder(findPublicKey(card.wallets))
}
fun scanResponse(scanResponse: ScanResponse): UserWalletIdBuilder {
return UserWalletIdBuilder(
publicKey = findPublicKey(scanResponse.card.wallets),
pairTwinPublicKey = when (scanResponse.productType) {
ProductType.Twins -> scanResponse.secondTwinPublicKey?.hexToBytes()
ProductType.Note,
ProductType.Wallet,
ProductType.SaltPay,
-> null
},
)
}
private fun findPublicKey(wallets: List<CardDTO.Wallet>): ByteArray? {
return wallets.firstOrNull()
?.publicKey
}
}
}

View file

@ -6,9 +6,9 @@ import com.tangem.common.services.Result
import com.tangem.datasource.api.tangemTech.TangemTechService
import com.tangem.datasource.api.tangemTech.UserTokensResponse
import com.tangem.domain.common.CardDTO
import com.tangem.domain.common.util.userWalletId
import com.tangem.tap.common.AndroidFileReader
import com.tangem.tap.domain.NoDataError
import com.tangem.tap.domain.model.builders.UserWalletIdBuilder
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.wallet.models.Currency
@ -24,7 +24,7 @@ class UserTokensRepository(
private val networkService: UserTokensNetworkService,
) {
suspend fun getUserTokens(card: CardDTO): List<Currency> {
val userId = card.userWalletId.stringValue
val userId = getUserWalletId(card) ?: return emptyList()
if (DemoHelper.isDemoCardId(card.cardId)) {
return loadTokensOffline(card, userId).ifEmpty { loadDemoCurrencies() }
}
@ -47,14 +47,14 @@ class UserTokensRepository(
}
suspend fun saveUserTokens(card: CardDTO, tokens: List<Currency>) {
val userId = card.userWalletId.stringValue
val userId = getUserWalletId(card) ?: return
val userTokens = tokens.toUserTokensResponse()
networkService.saveUserTokens(userId, userTokens)
storageService.saveUserTokens(userId, userTokens)
}
suspend fun removeUserTokens(card: CardDTO) {
val userId = card.userWalletId.stringValue
val userId = getUserWalletId(card) ?: return
val userTokens = emptyList<Currency>().toUserTokensResponse()
networkService.saveUserTokens(userId, userTokens)
storageService.saveUserTokens(userId, userTokens)
@ -70,7 +70,7 @@ class UserTokensRepository(
}
suspend fun loadBlockchainsToDerive(card: CardDTO): List<BlockchainNetwork> {
val userId = card.userWalletId.stringValue
val userId = getUserWalletId(card) ?: return emptyList()
val blockchainNetworks = loadTokensOffline(card, userId).toBlockchainNetworks()
if (DemoHelper.isDemoCardId(card.cardId)) {
@ -103,6 +103,7 @@ class UserTokensRepository(
coroutineScope { launch { networkService.saveUserTokens(userId = userId, tokens = userTokens) } }
tokens
}
else -> {
val tokens = storageService.getUserTokens(userId) ?: storageService.getUserTokens(card)
tokens.distinct()
@ -114,6 +115,11 @@ class UserTokensRepository(
return storageService.getUserTokens(userId) ?: storageService.getUserTokens(card)
}
private fun getUserWalletId(card: CardDTO): String? {
return UserWalletIdBuilder.card(card).build()
?.stringValue
}
companion object {
const val SORT_DEFAULT_VALUE = "manual"
const val GROUP_DEFAULT_VALUE = "none"

View file

@ -3,7 +3,6 @@ package com.tangem.tap.domain.userWalletList.implementation
import com.tangem.common.*
import com.tangem.domain.common.util.UserWalletId
import com.tangem.tap.domain.model.UserWallet
import com.tangem.tap.domain.model.isTwinnedWith
import com.tangem.tap.domain.userWalletList.UserWalletListError
import com.tangem.tap.domain.userWalletList.UserWalletsListManager
import com.tangem.tap.domain.userWalletList.model.UserWalletEncryptionKey
@ -89,8 +88,7 @@ internal class BiometricUserWalletsListManager(
} else {
val isWalletSaved = state.value.userWallets
.any {
// Workaround, check [UserWallet.isTwinnedWith]
it.cardsInWallet.contains(userWallet.cardId) || it.isTwinnedWith(userWallet)
it.walletId == userWallet.walletId || it.cardsInWallet.contains(userWallet.cardId)
}
if (isWalletSaved) {

View file

@ -2,8 +2,8 @@ package com.tangem.tap.domain.userWalletList.model
import com.squareup.moshi.JsonClass
import com.tangem.domain.common.util.UserWalletId
import com.tangem.domain.common.util.encryptionKey
import com.tangem.tap.domain.model.UserWallet
import com.tangem.tap.domain.userWalletList.utils.encryptionKey
@JsonClass(generateAdapter = true)
internal data class UserWalletEncryptionKey(

View file

@ -9,13 +9,13 @@ import com.tangem.common.catching
import com.tangem.common.mapFailure
import com.tangem.common.services.secure.SecureStorage
import com.tangem.domain.common.util.UserWalletId
import com.tangem.domain.common.util.encryptionKey
import com.tangem.tap.common.extensions.filterNotNull
import com.tangem.tap.domain.model.UserWallet
import com.tangem.tap.domain.userWalletList.UserWalletListError
import com.tangem.tap.domain.userWalletList.model.UserWalletEncryptionKey
import com.tangem.tap.domain.userWalletList.model.UserWalletSensitiveInformation
import com.tangem.tap.domain.userWalletList.repository.UserWalletsSensitiveInformationRepository
import com.tangem.tap.domain.userWalletList.utils.encryptionKey
import com.tangem.tap.domain.userWalletList.utils.sensitiveInformation
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext

View file

@ -1,24 +1,27 @@
package com.tangem.domain.common.util
package com.tangem.tap.domain.userWalletList.utils
import com.tangem.common.extensions.calculateSha256
import com.tangem.domain.common.CardDTO
import com.tangem.domain.common.extensions.calculateHmacSha256
val CardDTO.userWalletId: UserWalletId
get() = UserWalletId(findWalletPublicKey(wallets))
internal val CardDTO.encryptionKey: ByteArray
@Throws(IllegalArgumentException::class)
get() {
val walletPublicKey = requireNotNull(findPublicKey(wallets)) {
"Wallet public key must not be null"
}
val CardDTO.encryptionKey: ByteArray
get() = findWalletPublicKey(wallets)
?.let { calculateEncryptionKey(it) }
?: error("Wallet ID not found")
return calculateEncryptionKey(walletPublicKey)
}
private fun calculateEncryptionKey(publicKey: ByteArray): ByteArray {
val message = MESSAGE_FOR_ENCRYPTION_KEY.toByteArray()
val keyHash = publicKey.calculateSha256()
return message.calculateHmacSha256(keyHash)
}
private fun findWalletPublicKey(wallets: List<CardDTO.Wallet>): ByteArray? {
private fun findPublicKey(wallets: List<CardDTO.Wallet>): ByteArray? {
return wallets.firstOrNull()
?.publicKey
}

View file

@ -4,10 +4,8 @@ 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.toHexString
import com.tangem.common.flatMap
import com.tangem.domain.common.TapWorkarounds.isTangemTwins
import com.tangem.domain.common.util.userWalletId
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Settings
@ -20,6 +18,7 @@ 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.model.builders.UserWalletIdBuilder
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
@ -27,6 +26,7 @@ import com.tangem.tap.preferencesStorage
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import com.tangem.tap.userTokensRepository
import com.tangem.tap.userWalletsListManager
import com.tangem.tap.walletStoresManager
import com.tangem.wallet.R
@ -79,20 +79,16 @@ class DetailsMiddleware {
}
DetailsAction.ScanCard -> {
scope.launch {
tangemSdkManager.scanCard(allowRequestAccessCodeFromRepository = true)
.doOnSuccess { card ->
val isSameWallet = state.scanResponse?.card?.userWalletId
?.equals(card.userWalletId)
?: false
tangemSdkManager.scanProduct(userTokensRepository)
.doOnSuccess { scanResponse ->
val currentUserWalletId = state.scanResponse
?.let { UserWalletIdBuilder.scanResponse(it).build() }
val scannedUserWalletId = UserWalletIdBuilder.scanResponse(scanResponse)
.build()
val isSameWallet = currentUserWalletId == scannedUserWalletId
// !!! Workaround !!!
// TODO: Remove after [REDACTED_JIRA]
val isTwinned = card.wallets.firstOrNull()?.publicKey?.toHexString()
?.equals(state.scanResponse?.secondTwinPublicKey)
?: false
if (isSameWallet || isTwinned) {
store.dispatchOnMain(DetailsAction.PrepareCardSettingsData(card))
if (isSameWallet) {
store.dispatchOnMain(DetailsAction.PrepareCardSettingsData(scanResponse.card))
} else {
store.dispatchDialogShow(
AppDialog.SimpleOkDialogRes(
@ -122,8 +118,10 @@ class DetailsMiddleware {
is DetailsAction.ResetToFactory.Proceed -> {
val card = store.state.detailsState.cardSettingsState?.card ?: return
scope.launch {
val userWalletId = UserWalletIdBuilder.card(card).build()
tangemSdkManager.resetToFactorySettings(card.cardId)
.flatMap { userWalletsListManager.delete(listOf(card.userWalletId)) }
.flatMap { userWalletsListManager.delete(listOfNotNull(userWalletId)) }
.flatMap { tangemSdkManager.deleteSavedUserCodes(setOf(card.cardId)) }
.doOnSuccess {
Analytics.send(Settings.CardSettings.FactoryResetFinished())
@ -275,7 +273,7 @@ class DetailsMiddleware {
private suspend fun saveCurrentWallet(state: DetailsState) {
val scanResponse = state.scanResponse ?: return
val userWallet = UserWalletBuilder(scanResponse).build()
val userWallet = UserWalletBuilder(scanResponse).build() ?: return
userWalletsListManager.save(userWallet)
.doOnFailure { error ->

View file

@ -93,7 +93,7 @@ private fun readCard() = scope.launch {
onSuccess = { scanResponse ->
scope.launch {
if (preferencesStorage.shouldSaveUserWallets) {
val userWallet = UserWalletBuilder(scanResponse).build()
val userWallet = UserWalletBuilder(scanResponse).build() ?: return@launch
userWalletsListManager.save(userWallet)
.doOnFailure { error ->
Timber.e(error, "Unable to save user wallet")

View file

@ -6,7 +6,6 @@ import com.tangem.common.CompletionResult
import com.tangem.common.extensions.guard
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.domain.common.util.userWalletId
import com.tangem.tap.DELAY_SDK_DIALOG_CLOSE
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.events.AnalyticsParam
@ -25,6 +24,7 @@ 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.makePrimaryWalletManager
import com.tangem.tap.domain.model.builders.UserWalletIdBuilder
import com.tangem.tap.domain.twins.TwinCardsManager
import com.tangem.tap.features.onboarding.OnboardingHelper
import com.tangem.tap.features.wallet.models.Currency
@ -156,7 +156,7 @@ private fun handle(action: Action, dispatch: DispatchFunction) {
TwinCardsStep.CreateFirstWallet -> {
scope.launch {
userWalletsListManager.delete(
listOf(getScanResponse().card.userWalletId),
listOfNotNull(UserWalletIdBuilder.scanResponse(getScanResponse()).build()),
)
}
}

View file

@ -88,7 +88,7 @@ internal class SaveWalletMiddleware {
scope.launch {
val userWallet = UserWalletBuilder(scanResponse)
.backupCardsIds(state.backupInfo?.backupCardsIds)
.build()
.build() ?: return@launch
val isFirstSavedWallet = !userWalletsListManager.hasSavedUserWallets

View file

@ -1,6 +1,7 @@
package com.tangem.tap.features.walletSelector.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.flatMap
@ -162,6 +163,7 @@ internal class WalletSelectorMiddleware {
private suspend fun saveUserWalletAndPopBackToWalletScreen(scanResponse: ScanResponse): CompletionResult<Unit> {
val userWallet = UserWalletBuilder(scanResponse).build()
?: return CompletionResult.Failure(TangemSdkError.WalletIsNotCreated())
return userWalletsListManager.save(userWallet)
.doOnSuccess {

View file

@ -71,7 +71,7 @@ internal class WelcomeMiddleware {
private fun proceedWithCard(state: WelcomeState) = scope.launch {
scanCardInternal { scanResponse ->
val userWallet = UserWalletBuilder(scanResponse).build()
val userWallet = UserWalletBuilder(scanResponse).build() ?: return@scanCardInternal
userWalletsListManager.save(userWallet, canOverride = true)
.doOnFailure { error ->

View file

@ -8,12 +8,12 @@ import com.tangem.domain.common.CardDTO
import com.tangem.domain.common.TapWorkarounds.derivationStyle
import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.domain.common.extensions.toNetworkId
import com.tangem.domain.common.util.userWalletId
import com.tangem.lib.crypto.UserWalletManager
import com.tangem.lib.crypto.models.Currency
import com.tangem.lib.crypto.models.NativeToken
import com.tangem.lib.crypto.models.NonNativeToken
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 org.rekotlin.Action
@ -54,7 +54,12 @@ class UserWalletManagerImpl(
}
override fun getWalletId(): String {
return appStateHolder.getActualCard()?.userWalletId?.stringValue ?: ""
return appStateHolder.getActualCard()?.let {
UserWalletIdBuilder.card(it)
.build()
?.stringValue
}
?: ""
}
override suspend fun isTokenAdded(currency: Currency): Boolean {

View file

@ -1,17 +1,15 @@
package com.tangem.domain.common.util
import com.tangem.common.extensions.calculateSha256
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.extensions.toHexString
import com.tangem.domain.common.extensions.calculateHmacSha256
class UserWalletId(
val stringValue: String,
) {
val value = stringValue.hexToBytes()
constructor(walletPublicKey: ByteArray?) : this(
stringValue = walletPublicKey?.let { calculateUserWalletId(it).toHexString() } ?: "",
constructor(value: ByteArray?) : this(
stringValue = value?.toHexString() ?: "",
)
override fun equals(other: Any?): Boolean {
@ -32,12 +30,4 @@ class UserWalletId(
"UserWalletId(${take(3)}...${takeLast(3)})"
}
}
}
private fun calculateUserWalletId(publicKey: ByteArray): ByteArray {
val message = MESSAGE_FOR_WALLET_ID.toByteArray()
val keyHash = publicKey.calculateSha256()
return message.calculateHmacSha256(keyHash)
}
private const val MESSAGE_FOR_WALLET_ID = "UserWalletID"
}