Updated on 2026-08-14
This commit is contained in:
parent
78996521c6
commit
ab5fac9196
41 changed files with 366 additions and 73 deletions
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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?
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -2,5 +2,7 @@ package com.tangem.domain.wallets.models
|
|||
|
||||
sealed interface UpdateWalletError {
|
||||
|
||||
object DataError : UpdateWalletError
|
||||
data object DataError : UpdateWalletError
|
||||
|
||||
data object NameAlreadyExists : UpdateWalletError
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.tangem.domain.wallets.repository
|
||||
|
||||
/**
|
||||
* Access to migrate names flag
|
||||
*/
|
||||
interface WalletNamesMigrationRepository {
|
||||
|
||||
suspend fun isMigrationDone(): Boolean
|
||||
|
||||
suspend fun setMigrationDone()
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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 }
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue