Updated on 2026-08-14

This commit is contained in:
Tangem 2024-05-21 12:02:55 +02:00
parent 78996521c6
commit ab5fac9196
41 changed files with 366 additions and 73 deletions

View file

@ -0,0 +1,59 @@
package com.tangem.domain.wallets.builder
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.usecase.GetCardImageUseCase
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase
class UserWalletBuilder(
private val scanResponse: ScanResponse,
private val generateWalletNameUseCase: GenerateWalletNameUseCase,
private val getCardImageUseCase: GetCardImageUseCase = GetCardImageUseCase(),
) {
private var backupCardsIds: Set<String> = emptySet()
private var hasBackupError: Boolean = false
private val CardDTO.isBackupNotAllowed: Boolean
get() = !settings.isBackupAllowed
/**
* DANGEROUS!!!
* [backupCardsIds] will be non-empty list if card is backed up on current device.
*/
fun backupCardsIds(backupCardsIds: Set<String>?) = this.apply {
if (backupCardsIds != null) {
this.backupCardsIds = backupCardsIds
}
}
/**
* Sets if UserWallet has any backup errors (wrong curves etc). Use in onboarding
*/
fun hasBackupError(hasBackupError: Boolean) = this.apply {
this.hasBackupError = hasBackupError
}
suspend fun build(): UserWallet? {
return with(scanResponse) {
UserWalletIdBuilder.scanResponse(scanResponse)
.build()
?.let {
UserWallet(
walletId = it,
name = generateWalletNameUseCase(
productType = productType,
isBackupNotAllowed = card.isBackupNotAllowed,
isStartToCoin = cardTypesResolver.isStart2Coin(),
),
artworkUrl = getCardImageUseCase.invoke(card.cardId, card.cardPublicKey),
cardsInWallet = backupCardsIds.plus(card.cardId),
scanResponse = this,
isMultiCurrency = cardTypesResolver.isMultiwalletAllowed(),
hasBackupError = hasBackupError,
)
}
}
}
}

View file

@ -0,0 +1,83 @@
package com.tangem.domain.wallets.builder
import com.tangem.common.extensions.calculateSha256
import com.tangem.common.extensions.hexToBytes
import com.tangem.crypto.Secp256k1
import com.tangem.domain.common.TapWorkarounds.isTangemTwins
import com.tangem.domain.common.extensions.calculateHmacSha256
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ProductType
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.models.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.Wallet2,
ProductType.Ring,
ProductType.Start2Coin,
ProductType.Visa,
-> null
},
)
}
fun walletPublicKey(publicKey: ByteArray): UserWalletId {
return UserWalletIdBuilder(
publicKey = publicKey,
).build()!!
}
private fun findPublicKey(wallets: List<CardDTO.Wallet>): ByteArray? {
return wallets.firstOrNull()
?.publicKey
}
}
}

View file

@ -18,6 +18,9 @@ interface UserWalletsListManager {
/** [Flow] with selected [UserWallet] updates */
val selectedUserWallet: Flow<UserWallet>
/** All saved [UserWallet]s */
val userWalletsSync: List<UserWallet>
/** Selected [UserWallet] */
val selectedUserWalletSync: UserWallet?

View file

@ -0,0 +1,14 @@
package com.tangem.domain.wallets.models
data class Artwork(val artworkId: String) {
companion object {
const val DEFAULT_IMG_URL = "https://app.tangem.com/cards/card_default.png"
const val SERGIO_CARD_URL = "https://app.tangem.com/cards/card_tg059.png"
const val MARTA_CARD_URL = "https://app.tangem.com/cards/card_tg083.png"
const val TWIN_CARD_1_URL = "https://app.tangem.com/cards/card_tg085.png"
const val TWIN_CARD_2_URL = "https://app.tangem.com/cards/card_tg086.png"
const val SERGIO_CARD_ID = "BC01"
const val MARTA_CARD_ID = "BC02"
}
}

View file

@ -2,5 +2,7 @@ package com.tangem.domain.wallets.models
sealed interface UpdateWalletError {
object DataError : UpdateWalletError
data object DataError : UpdateWalletError
data object NameAlreadyExists : UpdateWalletError
}

View file

@ -0,0 +1,11 @@
package com.tangem.domain.wallets.repository
/**
* Access to migrate names flag
*/
interface WalletNamesMigrationRepository {
suspend fun isMigrationDone(): Boolean
suspend fun setMigrationDone()
}

View file

@ -0,0 +1,60 @@
package com.tangem.domain.wallets.usecase
import com.tangem.domain.models.scan.ProductType
import com.tangem.domain.wallets.legacy.UserWalletsListManager
/**
* Use case for user wallet name generation
*/
class GenerateWalletNameUseCase(
private val userWalletsListManager: UserWalletsListManager,
) {
operator fun invoke(productType: ProductType, isBackupNotAllowed: Boolean, isStartToCoin: Boolean): String {
val defaultName = getDefaultName(
productType = productType,
isBackupNotAllowed = isBackupNotAllowed,
isStartToCoin = isStartToCoin,
)
val existingNames = userWalletsListManager.userWalletsSync.map { it.name }.toSet()
return suggestedWalletName(defaultName, existingNames)
}
private fun suggestedWalletName(defaultName: String, existingNames: Set<String>): String {
val startIndex = 2
if (!existingNames.contains(defaultName)) {
return defaultName
}
for (index in startIndex..MAX_WALLETS_LIMIT) {
val potentialName = "$defaultName $index"
if (!existingNames.contains(potentialName)) {
return potentialName
}
}
return defaultName
}
private fun getDefaultName(productType: ProductType, isBackupNotAllowed: Boolean, isStartToCoin: Boolean): String {
return when (productType) {
ProductType.Note -> "Note"
ProductType.Twins -> "Twin"
ProductType.Start2Coin -> "Start2Coin"
ProductType.Visa -> "Tangem Visa"
ProductType.Wallet,
ProductType.Wallet2,
ProductType.Ring,
-> when {
isBackupNotAllowed -> "Tangem card"
isStartToCoin -> "Start2Coin"
else -> "Wallet"
}
}
}
companion object {
const val MAX_WALLETS_LIMIT = 10000
}
}

View file

@ -0,0 +1,57 @@
package com.tangem.domain.wallets.usecase
import com.tangem.common.extensions.toHexString
import com.tangem.common.services.Result
import com.tangem.domain.common.TwinCardNumber
import com.tangem.domain.common.TwinsHelper
import com.tangem.domain.wallets.models.Artwork
import com.tangem.operations.attestation.OnlineCardVerifier
import com.tangem.operations.attestation.TangemApi
/**
* Use case for getting card image url
*
* @property verifier REST API
*
[REDACTED_AUTHOR]
*/
class GetCardImageUseCase(private val verifier: OnlineCardVerifier = OnlineCardVerifier()) {
/**
* Get card image url
*
* @param cardId card id
* @param cardPublicKey card public key
*/
suspend operator fun invoke(cardId: String, cardPublicKey: ByteArray): String {
return when (val result = verifier.getCardInfo(cardId, cardPublicKey)) {
is Result.Success -> {
val artworkId = result.data.artwork?.id
if (artworkId.isNullOrEmpty()) {
getFallbackArtworkUrl(cardId)
} else {
getUrlForArtwork(cardId, cardPublicKey.toHexString(), artworkId)
}
}
is Result.Failure -> getFallbackArtworkUrl(cardId)
}
}
private fun getFallbackArtworkUrl(cardId: String): String {
return when {
cardId.startsWith(Artwork.SERGIO_CARD_ID) -> Artwork.SERGIO_CARD_URL
cardId.startsWith(Artwork.MARTA_CARD_ID) -> Artwork.MARTA_CARD_URL
else -> when (TwinsHelper.getTwinCardNumber(cardId)) {
TwinCardNumber.First -> Artwork.TWIN_CARD_1_URL
TwinCardNumber.Second -> Artwork.TWIN_CARD_2_URL
else -> Artwork.DEFAULT_IMG_URL
}
}
}
private fun getUrlForArtwork(cardId: String, cardPublicKeyHex: String, artworkId: String): String {
return TangemApi.Companion.BaseUrl.VERIFY.url + TangemApi.ARTWORK +
"?artworkId=$artworkId&CID=$cardId&publicKey=$cardPublicKeyHex"
}
}

View file

@ -0,0 +1,13 @@
package com.tangem.domain.wallets.usecase
import com.tangem.domain.wallets.legacy.UserWalletsListManager
/**
* Use case for getting list of user wallets names.
*
* @property userWalletsListManager user wallets list manager
*/
class GetWalletNamesUseCase(private val userWalletsListManager: UserWalletsListManager) {
operator fun invoke(): List<String> = userWalletsListManager.userWalletsSync.map { it.name }
}

View file

@ -0,0 +1,36 @@
package com.tangem.domain.wallets.usecase
import arrow.core.Either
import arrow.core.left
import arrow.core.raise.either
import arrow.core.right
import com.tangem.common.doOnFailure
import com.tangem.common.doOnSuccess
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.models.UpdateWalletError
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
/**
* Use case for rename user wallet
*
* @property userWalletsListManager user wallets list manager
*/
class RenameWalletUseCase(private val userWalletsListManager: UserWalletsListManager) {
suspend operator fun invoke(userWalletId: UserWalletId, name: String): Either<UpdateWalletError, UserWallet> {
val existingNames = userWalletsListManager.userWalletsSync
if (existingNames.any { it.name == name && it.walletId != userWalletId }) {
return UpdateWalletError.NameAlreadyExists.left()
}
return either {
userWalletsListManager.update(userWalletId) { it.copy(name = name) }
.doOnSuccess { return it.right() }
.doOnFailure { return UpdateWalletError.DataError.left() }
return UpdateWalletError.DataError.left()
}
}
}