Updated on 2026-08-14

This commit is contained in:
Tangem 2025-08-21 12:43:59 +03:00
commit fe2cfac54d
1140 changed files with 23493 additions and 8579 deletions

View file

@ -37,6 +37,11 @@ dependencies {
implementation(tangemDeps.hot.core)
// endregion
/** Other libraries */
implementation(platform(deps.firebase.bom))
implementation(deps.firebase.analytics)
implementation(deps.timber)
// region DI
implementation(deps.hilt.android)
kapt(deps.hilt.kapt)

View file

@ -25,7 +25,7 @@ class HotUserWalletBuilder @AssistedInject constructor(
) {
suspend fun build(): UserWallet.Hot = withContext(dispatcherProvider.default) {
val allNetworks = Blockchain.entries
val allNetworks = Blockchain.entries.filter { it.isTestnet().not() }
val curves = allNetworks.map { it.getSupportedCurves() }.flatten().toSet()
val requests = curves.sortedBy { it.ordinal }.map { curve ->
val derivationPaths = allNetworks.filter { curve in it.getSupportedCurves() }

View file

@ -0,0 +1,18 @@
package com.tangem.domain.wallets.config
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.card.EllipticCurve
import com.tangem.domain.card.configs.CardConfig
import com.tangem.domain.models.scan.CardDTO
class ColdCurvesConfig(cardDTO: CardDTO) : CurvesConfig {
val cardConfig = CardConfig.createConfig(cardDTO)
override val mandatoryCurves: List<EllipticCurve>
get() = cardConfig.mandatoryCurves
override fun primaryCurve(blockchain: Blockchain): EllipticCurve? {
return cardConfig.primaryCurve(blockchain)
}
}

View file

@ -0,0 +1,18 @@
package com.tangem.domain.wallets.config
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.card.EllipticCurve
import com.tangem.domain.models.wallet.UserWallet
interface CurvesConfig {
val mandatoryCurves: List<EllipticCurve>
fun primaryCurve(blockchain: Blockchain): EllipticCurve?
}
val UserWallet.curvesConfig: CurvesConfig
get() = when (this) {
is UserWallet.Cold -> ColdCurvesConfig(this.scanResponse.card)
is UserWallet.Hot -> HotCurvesConfig
}

View file

@ -0,0 +1,15 @@
package com.tangem.domain.wallets.config
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.card.EllipticCurve
import com.tangem.domain.card.configs.Wallet2CardConfig
data object HotCurvesConfig : CurvesConfig {
override val mandatoryCurves: List<EllipticCurve>
get() = Wallet2CardConfig.mandatoryCurves
override fun primaryCurve(blockchain: Blockchain): EllipticCurve? {
return Wallet2CardConfig.primaryCurve(blockchain)
}
}

View file

@ -10,11 +10,14 @@ import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.wallets.models.UserWalletRemoteInfo
import com.tangem.domain.models.wallet.copy
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext
class DefaultUserWalletsSyncDelegate(
private val userWalletsListManager: UserWalletsListManager,
private val userWalletsListRepository: UserWalletsListRepository,
private val useNewRepository: Boolean,
private val dispatchers: CoroutineDispatcherProvider,
) : UserWalletsSyncDelegate {
@ -28,10 +31,43 @@ class DefaultUserWalletsSyncDelegate(
}
}
// TODO remove dispatchers whnen UserWalletsListManager will be main safe
private suspend fun renameUserWallet(
userWalletId: UserWalletId,
name: String,
): Either<UpdateWalletError, UserWallet> = if (useNewRepository) {
renameUserWalletInNewRepository(userWalletId, name)
} else {
renameUserWalletInLegacyRepository(userWalletId, name)
}
private suspend fun renameUserWalletInNewRepository(
userWalletId: UserWalletId,
name: String,
): Either<UpdateWalletError, UserWallet> = either {
val userWallets = userWalletsListRepository.userWalletsSync()
val userWallet = userWallets.find { it.walletId == userWalletId }
?: raise(UpdateWalletError.DataError(IllegalStateException("User wallet with id $userWalletId not found")))
ensure(userWallets.none { it.name == name && it.walletId != userWalletId }) {
UpdateWalletError.NameAlreadyExists
}
ensure(name != userWallet.name) {
UpdateWalletError.NameAlreadyExists
}
val updatedWallet = userWallet.copy(name = name)
userWalletsListRepository.saveWithoutLock(updatedWallet, canOverride = true)
.map { updatedWallet }
.mapLeft { error -> UpdateWalletError.DataError(IllegalStateException("")) }
.bind()
}
// TODO remove dispatchers whnen UserWalletsListManager will be main safe
private suspend fun renameUserWalletInLegacyRepository(
userWalletId: UserWalletId,
name: String,
): Either<UpdateWalletError, UserWallet> = withContext(dispatchers.io) {
either {
val existingNames = userWalletsListManager.userWalletsSync

View file

@ -0,0 +1,35 @@
package com.tangem.domain.wallets.derivations
import com.tangem.common.extensions.ByteArrayKey
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.wallets.usecase.BackendId
import com.tangem.operations.derivation.ExtendedPublicKeysMap
interface ColdMapDerivationsRepository {
@Throws
suspend fun derivePublicKeys(userWallet: UserWallet.Cold, currencies: List<CryptoCurrency>): UserWallet.Cold
suspend fun derivePublicKeysByNetworkIds(
userWallet: UserWallet.Cold,
networkIds: List<Network.RawID>,
): UserWallet.Cold
@Throws
suspend fun derivePublicKeysByNetworks(userWallet: UserWallet.Cold, networks: List<Network>): UserWallet.Cold
@Throws
suspend fun derivePublicKeys(
userWallet: UserWallet.Cold,
derivations: Map<ByteArrayKey, List<DerivationPath>>,
): Pair<UserWallet.Cold, Map<ByteArrayKey, ExtendedPublicKeysMap>>
/** Check if user [userWallet] has missed derivations using map of [Network.ID] with extraDerivationPath */
suspend fun hasMissedDerivations(
userWallet: UserWallet.Cold,
networksWithDerivationPath: Map<BackendId, String?>,
): Boolean
}

View file

@ -0,0 +1,30 @@
package com.tangem.domain.wallets.derivations
import com.tangem.blockchain.common.derivation.DerivationStyle
import com.tangem.domain.card.common.TapWorkarounds.isWallet2
import com.tangem.domain.models.scan.CardDTO
interface DerivationStyleProvider {
fun getDerivationStyle(): DerivationStyle?
}
internal class TangemDerivationStyleProvider(
private val card: CardDTO,
) : DerivationStyleProvider {
override fun getDerivationStyle(): DerivationStyle? {
return when {
!card.settings.isHDWalletAllowed -> null
firstBatchesOfWallet1(card) -> DerivationStyle.V1
card.isWallet2 -> DerivationStyle.V3
else -> DerivationStyle.V2
}
}
private fun firstBatchesOfWallet1(card: CardDTO): Boolean {
return card.batchId == "AC01" || card.batchId == "AC02" || card.batchId == "CB95"
}
}
internal class TangemHotDerivationStyleProvider : DerivationStyleProvider {
override fun getDerivationStyle(): DerivationStyle? = DerivationStyle.V3
}

View file

@ -0,0 +1,17 @@
package com.tangem.domain.wallets.derivations
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.wallet.UserWallet
val UserWallet.derivationStyleProvider: DerivationStyleProvider
get() = when (this) {
is UserWallet.Cold -> scanResponse.derivationStyleProvider
is UserWallet.Hot -> TangemHotDerivationStyleProvider()
}
val ScanResponse.derivationStyleProvider: DerivationStyleProvider
get() = card.derivationStyleProvider
val CardDTO.derivationStyleProvider: DerivationStyleProvider
get() = TangemDerivationStyleProvider(this)

View file

@ -0,0 +1,32 @@
package com.tangem.domain.wallets.derivations
import com.tangem.common.extensions.ByteArrayKey
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.wallets.usecase.BackendId
import com.tangem.operations.derivation.ExtendedPublicKeysMap
interface DerivationsRepository {
@Throws
suspend fun derivePublicKeys(userWalletId: UserWalletId, currencies: List<CryptoCurrency>)
suspend fun derivePublicKeysByNetworkIds(userWalletId: UserWalletId, networkIds: List<Network.RawID>)
@Throws
suspend fun derivePublicKeysByNetworks(userWalletId: UserWalletId, networks: List<Network>)
@Throws
suspend fun derivePublicKeys(
userWalletId: UserWalletId,
derivations: Map<ByteArrayKey, List<DerivationPath>>,
): Map<ByteArrayKey, ExtendedPublicKeysMap>
/** Check if user [userWalletId] has missed derivations using map of [Network.ID] with extraDerivationPath */
suspend fun hasMissedDerivations(
userWalletId: UserWalletId,
networksWithDerivationPath: Map<BackendId, String?>,
): Boolean
}

View file

@ -0,0 +1,35 @@
package com.tangem.domain.wallets.derivations
import com.tangem.common.extensions.ByteArrayKey
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.wallets.usecase.BackendId
import com.tangem.operations.derivation.ExtendedPublicKeysMap
interface HotMapDerivationsRepository {
@Throws
suspend fun derivePublicKeys(userWallet: UserWallet.Hot, currencies: List<CryptoCurrency>): UserWallet.Hot
suspend fun derivePublicKeysByNetworkIds(
userWallet: UserWallet.Hot,
networkIds: List<Network.RawID>,
): UserWallet.Hot
@Throws
suspend fun derivePublicKeysByNetworks(userWallet: UserWallet.Hot, networks: List<Network>): UserWallet.Hot
@Throws
suspend fun derivePublicKeys(
userWallet: UserWallet.Hot,
derivations: Map<ByteArrayKey, List<DerivationPath>>,
): Pair<UserWallet.Hot, Map<ByteArrayKey, ExtendedPublicKeysMap>>
/** Check if user [userWallet] has missed derivations using map of [Network.ID] with extraDerivationPath */
suspend fun hasMissedDerivations(
userWallet: UserWallet.Hot,
networksWithDerivationPath: Map<BackendId, String?>,
): Boolean
}

View file

@ -4,16 +4,14 @@ import com.tangem.blockchain.blockchains.cardano.CardanoUtils
import com.tangem.blockchain.common.Blockchain
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.domain.card.common.util.hasDerivation
import com.tangem.domain.card.configs.Wallet2CardConfig
import com.tangem.domain.models.wallet.UserWallet
import kotlin.collections.first
import kotlin.collections.orEmpty
import com.tangem.domain.wallets.config.curvesConfig
fun UserWallet.hasDerivation(blockchain: Blockchain, derivationPath: String): Boolean {
return when (this) {
is UserWallet.Cold -> scanResponse.hasDerivation(blockchain, derivationPath)
is UserWallet.Hot -> {
val primaryCurve = Wallet2CardConfig.primaryCurve(blockchain) // TODO [REDACTED_TASK_KEY]: handle hot wallet config
val primaryCurve = curvesConfig.primaryCurve(blockchain)
val list = if (blockchain == Blockchain.Cardano) {
listOf(
CardanoUtils.extendedDerivationPath(DerivationPath(derivationPath)),

View file

@ -0,0 +1,71 @@
package com.tangem.domain.wallets.hot
import com.tangem.hot.sdk.model.HotWalletId
import kotlinx.coroutines.flow.Flow
/**
* Repository for managing access code attempts for hot wallets.
* It tracks the number of attempts made to access a hot wallet and applies cooldowns or deletion
* based on the number of attempts.
*/
interface HotWalletAccessCodeAttemptsRepository {
/**
* Increments the number of attempts for the given [AttemptId].
* If the number of attempts exceeds [MAX_FAST_FORWARD_ATTEMPTS], a cooldown period is initiated.
*/
suspend fun incrementAttempts(id: AttemptId)
/**
* Resets the attempts for the given [HotWalletId].
* This is typically called when the user successfully authenticates or when the wallet is deleted.
*/
suspend fun resetAttempts(hotWalletId: HotWalletId)
/**
* Retrieves the current attempts for the given [AttemptId].
* The result is a flow that emits the current state of attempts.
*/
fun getAttempts(id: AttemptId): Flow<Attempts>
/**
* Synchronously retrieves the current attempts for the given [AttemptId].
* This is useful when you need to get the attempts without using a flow.
*/
suspend fun getAttemptsSync(id: AttemptId): Attempts
data class AttemptId(
val hotWalletId: HotWalletId,
val auth: Boolean,
)
sealed interface Attempts {
val count: Int
data class FastForward(
override val count: Int,
) : Attempts
data class WithDelay(
override val count: Int,
val remainingSeconds: Int,
) : Attempts
data class BeforeDeletion(
override val count: Int,
val remainingSeconds: Int,
val remainingAttemptsCountBeforeDeletion: Int,
) : Attempts
data object Deletion : Attempts {
override val count: Int = MAX_ATTEMPTS_BEFORE_DELETION
}
}
companion object {
const val COOLDOWN_SECONDS = 60
const val MAX_FAST_FORWARD_ATTEMPTS = 5
const val ATTEMPTS_BEFORE_DELETION = 20
const val MAX_ATTEMPTS_BEFORE_DELETION = 30
}
}

View file

@ -0,0 +1,52 @@
package com.tangem.domain.wallets.hot
import com.tangem.hot.sdk.model.HotAuth
import com.tangem.hot.sdk.model.HotWalletId
/**
* Interface for requesting the password for a hot wallet.
* It provides methods to handle password requests, authentication states, and user interactions.
*/
interface HotWalletPasswordRequester {
/**
* Sets state to show wrong password state.
*/
suspend fun wrongPassword()
/**
* Sets state to show successful authentication state.
*/
suspend fun successfulAuthentication()
/**
* Requests the user to enter the password for the hot wallet.
* @param attemptRequest Contains information about the hot wallet and authentication mode.
* @return Result of the password request, which can be either a password entry, biometric use, or dismissal.
*/
suspend fun requestPassword(attemptRequest: AttemptRequest): Result
/**
* Dismisses the password request dialog.
*/
suspend fun dismiss()
/**
* Represents a request to authenticate with a hot wallet.
* @param hotWalletId The ID of the hot wallet to authenticate with.
* @param authMode Indicates whether the request is for authentication mode.
* In auth mode user can be deleted after failed attempts.
* @param hasBiometry Indicates whether to show biometric authentication option.
*/
data class AttemptRequest(
val hotWalletId: HotWalletId,
val authMode: Boolean,
val hasBiometry: Boolean,
)
sealed class Result {
data object UseBiometry : Result()
data object Dismiss : Result()
data class EnteredPassword(val password: HotAuth.Password) : Result()
}
}

View file

@ -1,6 +0,0 @@
package com.tangem.domain.wallets.models
sealed interface DeleteWalletError {
data object UnableToDelete : DeleteWalletError
}

View file

@ -1,13 +0,0 @@
package com.tangem.domain.wallets.models
/**
[REDACTED_AUTHOR]
*/
sealed interface SaveWalletError {
val messageId: Int?
data class DataError(override val messageId: Int?) : SaveWalletError
data class WalletAlreadySaved(override val messageId: Int) : SaveWalletError
}

View file

@ -1,6 +0,0 @@
package com.tangem.domain.wallets.models
sealed interface SelectWalletError {
object UnableToSelectUserWallet : SelectWalletError
}

View file

@ -1,8 +0,0 @@
package com.tangem.domain.wallets.repository
import com.tangem.domain.models.network.Network
interface HotDerivationsRepository {
fun getAllSupportedNetworks(): Set<Network>
}

View file

@ -11,10 +11,20 @@ interface WalletsRepository {
suspend fun shouldSaveUserWalletsSync(): Boolean
@Deprecated("Hot wallet make always save user wallets. Do not use this method")
fun shouldSaveUserWallets(): Flow<Boolean>
@Deprecated("Hot wallet make always save user wallets. Do not use this method")
suspend fun saveShouldSaveUserWallets(item: Boolean)
suspend fun useBiometricAuthentication(): Boolean
suspend fun setUseBiometricAuthentication(value: Boolean)
suspend fun requireAccessCode(): Boolean
suspend fun setRequireAccessCode(value: Boolean)
suspend fun isWalletWithRing(userWalletId: UserWalletId): Boolean
suspend fun setHasWalletsWithRing(userWalletId: UserWalletId)

View file

@ -4,8 +4,9 @@ import arrow.core.Either
import arrow.core.raise.either
import com.tangem.common.doOnFailure
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.models.DeleteWalletError
import com.tangem.domain.core.wallets.error.DeleteWalletError
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.core.wallets.UserWalletsListRepository
/**
* Use case for deleting user wallet
@ -14,7 +15,11 @@ import com.tangem.domain.models.wallet.UserWalletId
*
[REDACTED_AUTHOR]
*/
class DeleteWalletUseCase(private val userWalletsListManager: UserWalletsListManager) {
class DeleteWalletUseCase(
private val userWalletsListManager: UserWalletsListManager,
private val userWalletsListRepository: UserWalletsListRepository,
private val useNewRepository: Boolean,
) {
/**
* Deletes user wallet with provided ID.
@ -24,6 +29,12 @@ class DeleteWalletUseCase(private val userWalletsListManager: UserWalletsListMan
* @return [Either] with [DeleteWalletError] or [Boolean] which indicates that there are still saved wallets.
* */
suspend operator fun invoke(userWalletId: UserWalletId): Either<DeleteWalletError, Boolean> {
if (useNewRepository) {
return userWalletsListRepository.delete(userWalletIds = listOf(userWalletId)).map {
userWalletsListRepository.selectedUserWallet.value != null
}
}
return either {
userWalletsListManager.delete(userWalletIds = listOf(userWalletId))
.doOnFailure {

View file

@ -0,0 +1,18 @@
package com.tangem.domain.wallets.usecase
import arrow.core.Either
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.wallets.derivations.DerivationsRepository
class DerivePublicKeysUseCase(
private val derivationsRepository: DerivationsRepository,
) {
suspend operator fun invoke(userWalletId: UserWalletId, currencies: List<CryptoCurrency>): Either<Throwable, Unit> {
return Either.Companion.catch {
derivationsRepository.derivePublicKeys(userWalletId, currencies)
}
}
}

View file

@ -0,0 +1,23 @@
package com.tangem.domain.wallets.usecase
import com.google.firebase.analytics.ktx.analytics
import com.google.firebase.ktx.Firebase
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
class GenerateBuyTangemCardLinkUseCase {
suspend operator fun invoke(): String = suspendCoroutine { cont ->
Firebase.analytics.appInstanceId
.addOnSuccessListener { id ->
cont.resume("$NEW_BUY_WALLET_URL&app_instance_id=$id")
}
.addOnFailureListener {
cont.resume(NEW_BUY_WALLET_URL)
}
}
companion object {
private const val NEW_BUY_WALLET_URL = "https://buy.tangem.com/?utm_source=tangem-app&utm_medium=app"
}
}

View file

@ -2,12 +2,16 @@ package com.tangem.domain.wallets.usecase
import com.tangem.domain.models.scan.ProductType
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.domain.core.wallets.requireUserWalletsSync
/**
* Use case for user wallet name generation
*/
class GenerateWalletNameUseCase(
private val userWalletsListManager: UserWalletsListManager,
private val userWalletsListRepository: UserWalletsListRepository,
private val useNewRepository: Boolean,
) {
operator fun invoke(productType: ProductType, isBackupNotAllowed: Boolean, isStartToCoin: Boolean): String {
@ -17,16 +21,24 @@ class GenerateWalletNameUseCase(
isStartToCoin = isStartToCoin,
)
val existingNames = userWalletsListManager.userWalletsSync.map { it.name }.toSet()
val existingNames = getNamesSet()
return suggestedWalletName(defaultName, existingNames)
}
fun invokeForHot(): String {
val defaultName = "Wallet"
val existingNames = userWalletsListManager.userWalletsSync.map { it.name }.toSet()
val existingNames = getNamesSet()
return suggestedWalletName(defaultName, existingNames)
}
private fun getNamesSet(): Set<String> {
return if (useNewRepository) {
userWalletsListRepository.requireUserWalletsSync().map { it.name }.toSet()
} else {
userWalletsListManager.userWalletsSync.map { it.name }.toSet()
}
}
private fun suggestedWalletName(defaultName: String, existingNames: Set<String>): String {
val startIndex = 2
if (!existingNames.contains(defaultName)) {

View file

@ -0,0 +1,159 @@
package com.tangem.domain.wallets.usecase
import arrow.core.Either
import arrow.core.right
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.toBlockchain
import com.tangem.common.extensions.ByteArrayKey
import com.tangem.common.extensions.calculateRipemd160
import com.tangem.common.extensions.calculateSha256
import com.tangem.crypto.NetworkType
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.derivations.DerivationsRepository
import com.tangem.operations.derivation.ExtendedPublicKeysMap
/**
* Derivates an exteneded public key (xpub) based on blockchain hardened derivation
*/
class GetExtendedPublicKeyForCurrencyUseCase(
private val derivationsRepository: DerivationsRepository,
private val walletManagersFacade: WalletManagersFacade,
) {
suspend operator fun invoke(userWalletId: UserWalletId, network: Network): Either<Throwable, String> {
return Either.catch {
val walletManager = walletManagersFacade.getOrCreateWalletManager(userWalletId, network)
?: error("Wallet not found for userWalletId=$userWalletId and network=$network")
val blockchain = network.toBlockchain()
val isSecp256k1Blockchain = Blockchain.secp256k1Blockchains(network.isTestnet).contains(blockchain)
val hdKey = if (isSecp256k1Blockchain) {
walletManager.wallet.publicKey.derivationType?.hdKey ?: error("No derivation found")
} else {
error("No derivation found")
}
var childKey = makeChildKey(
isBip44DerivationStyleXPUB = blockchain.isBip44DerivationStyleXPUB(),
extendedPublicKey = hdKey.extendedPublicKey,
derivationPath = hdKey.path,
)
var parentKey = Key(
derivationPath = childKey.derivationPath.dropLastNodes(1),
extendedPublicKey = null,
)
val pendingDerivations = getPendingDerivations(childKey, parentKey)
val derivedKeys = deriveKeys(
userWalletId = userWalletId,
seedKey = walletManager.wallet.publicKey.seedKey,
paths = pendingDerivations,
)
if (childKey.extendedPublicKey == null) {
childKey = childKey.copy(
extendedPublicKey = derivedKeys[childKey.derivationPath] ?: error("Failed to derive child key"),
)
}
if (parentKey.extendedPublicKey == null) {
parentKey = parentKey.copy(
extendedPublicKey = derivedKeys[parentKey.derivationPath] ?: error("Failed to derive parent key"),
)
}
makeExtendedKey(childKey, parentKey, network.isTestnet)
}
}
/**
* @return true if xpub generation is supported, false otherwise
*/
suspend fun isSupported(userWalletId: UserWalletId, network: Network): Either<Throwable, Boolean> = Either.catch {
val walletManager = walletManagersFacade.getOrCreateWalletManager(userWalletId, network)
?: error("Wallet not found for user wallet $userWalletId and network ${network.id}")
val blockchain = network.toBlockchain()
val isSecp256k1Blockchain = Blockchain.secp256k1Blockchains(network.isTestnet).contains(blockchain)
val isHdKey = walletManager.wallet.publicKey.derivationType?.hdKey
val isSupported = isSecp256k1Blockchain && isHdKey != null
return isSupported.right()
}
private suspend fun deriveKeys(
userWalletId: UserWalletId,
seedKey: ByteArray,
paths: MutableList<DerivationPath>,
): ExtendedPublicKeysMap {
val result = derivationsRepository.derivePublicKeys(userWalletId, mapOf(ByteArrayKey(seedKey) to paths))
return result.getValue(ByteArrayKey(seedKey))
}
private fun makeExtendedKey(childKey: Key, parentKey: Key, isTestnet: Boolean): String {
val publicKey = childKey.extendedPublicKey?.publicKey ?: error("No public key found")
val chainCode = childKey.extendedPublicKey.chainCode
val lastChildNode = childKey.derivationPath.nodes.last()
val parentPublicKey = parentKey.extendedPublicKey?.publicKey
val depth = childKey.derivationPath.nodes.size
val childNumber = lastChildNode.index
val parentFingerprint = parentPublicKey
?.calculateSha256()?.calculateRipemd160()
?.take(PARENT_FINGERPRINT_SIZE)?.toByteArray()
?: error("No parent fingerprint found")
val net = if (isTestnet) NetworkType.Testnet else NetworkType.Mainnet
return ExtendedPublicKey(
publicKey = publicKey,
chainCode = chainCode,
depth = depth,
parentFingerprint = parentFingerprint,
childNumber = childNumber,
).serialize(net)
}
private fun getPendingDerivations(childKey: Key, parentKey: Key): MutableList<DerivationPath> {
val pendingDerivations = mutableListOf<DerivationPath>()
if (childKey.extendedPublicKey == null) {
pendingDerivations.add(childKey.derivationPath)
}
if (parentKey.extendedPublicKey == null) {
pendingDerivations.add(parentKey.derivationPath)
}
return pendingDerivations
}
private fun makeChildKey(
isBip44DerivationStyleXPUB: Boolean,
extendedPublicKey: ExtendedPublicKey,
derivationPath: DerivationPath,
): Key = if (isBip44DerivationStyleXPUB) {
Key(derivationPath.dropLastNodes(2), null)
} else {
Key(derivationPath, extendedPublicKey)
}
private fun DerivationPath.dropLastNodes(count: Int): DerivationPath {
return DerivationPath(nodes.dropLast(count))
}
private data class Key(
val derivationPath: DerivationPath,
val extendedPublicKey: ExtendedPublicKey?,
)
private companion object {
const val PARENT_FINGERPRINT_SIZE = 4
}
}

View file

@ -4,13 +4,20 @@ import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.legacy.asLockable
import com.tangem.domain.wallets.legacy.isLockedSync
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.core.wallets.UserWalletsListRepository
import kotlinx.coroutines.flow.*
class GetSavedWalletsCountUseCase(
private val userWalletsListManager: UserWalletsListManager,
private val userWalletsListRepository: UserWalletsListRepository,
private val useNewRepository: Boolean,
) {
operator fun invoke(): Flow<List<UserWallet>> {
if (useNewRepository) {
return userWalletsListRepository.userWallets.map { requireNotNull(it) }
}
return userWalletsListManager.savedWalletsCount
.filter { count ->
if (count == 0) return@filter true

View file

@ -6,6 +6,7 @@ import arrow.core.raise.ensureNotNull
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.models.GetUserWalletError
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.core.wallets.UserWalletsListRepository
/**
* Use case for getting selected wallet.
@ -15,10 +16,20 @@ import com.tangem.domain.models.wallet.UserWallet
*
[REDACTED_AUTHOR]
*/
class GetSelectedWalletSyncUseCase(private val userWalletsListManager: UserWalletsListManager) {
class GetSelectedWalletSyncUseCase(
private val userWalletsListManager: UserWalletsListManager,
private val userWalletsListRepository: UserWalletsListRepository,
private val useNewRepository: Boolean = false,
) {
@Deprecated("You should provide the selected wallet via routing parameters due to the scalability of the features")
operator fun invoke(): Either<GetUserWalletError, UserWallet> {
if (useNewRepository) {
return either {
userWalletsListRepository.selectedUserWallet.value ?: raise(GetUserWalletError.UserWalletNotFound)
}
}
return either {
ensureNotNull(
value = userWalletsListManager.selectedUserWalletSync,

View file

@ -5,7 +5,9 @@ import arrow.core.raise.either
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.models.GetUserWalletError
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.core.wallets.UserWalletsListRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filterNotNull
/**
* Use case for getting flow of selected wallet.
@ -14,12 +16,32 @@ import kotlinx.coroutines.flow.Flow
*
[REDACTED_AUTHOR]
*/
class GetSelectedWalletUseCase(private val userWalletsListManager: UserWalletsListManager) {
@Deprecated("You should provide the selected wallet via routing parameters due to the scalability of the features")
class GetSelectedWalletUseCase(
private val userWalletsListManager: UserWalletsListManager,
private val userWalletsListRepository: UserWalletsListRepository,
private val useNewRepository: Boolean = false,
) {
@Deprecated("You should provide the selected wallet via routing parameters due to the scalability of the features")
operator fun invoke(): Either<GetUserWalletError, Flow<UserWallet>> {
return either {
userWalletsListManager.selectedUserWallet
if (useNewRepository) {
userWalletsListRepository.selectedUserWallet.filterNotNull()
} else {
userWalletsListManager.selectedUserWallet
}
}
}
@Deprecated("You should provide the selected wallet via routing parameters due to the scalability of the features")
fun sync(): Either<GetUserWalletError, UserWallet?> {
return either {
if (useNewRepository) {
userWalletsListRepository.selectedUserWallet.value
} else {
userWalletsListManager.selectedUserWalletSync
}
}
}
}

View file

@ -10,13 +10,24 @@ import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.models.GetUserWalletError
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.domain.core.wallets.requireUserWalletsSync
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.transformLatest
class GetUserWalletUseCase(private val userWalletsListManager: UserWalletsListManager) {
class GetUserWalletUseCase(
private val userWalletsListManager: UserWalletsListManager,
private val userWalletsListRepository: UserWalletsListRepository,
private val useNewListRepository: Boolean,
) {
operator fun invoke(userWalletId: UserWalletId): Either<GetUserWalletError, UserWallet> = either {
val userWallets = userWalletsListManager.userWalletsSync
val userWallets = if (useNewListRepository) {
userWalletsListRepository.requireUserWalletsSync()
} else {
userWalletsListManager.userWalletsSync
}
ensureNotNull(userWallets.firstOrNull { it.walletId == userWalletId }) {
raise(GetUserWalletError.UserWalletNotFound)
@ -25,7 +36,13 @@ class GetUserWalletUseCase(private val userWalletsListManager: UserWalletsListMa
@OptIn(ExperimentalCoroutinesApi::class)
fun invokeFlow(userWalletId: UserWalletId): EitherFlow<GetUserWalletError, UserWallet> {
return userWalletsListManager.userWallets.transformLatest { userWallets ->
val flow = if (useNewListRepository) {
userWalletsListRepository.userWallets.map { requireNotNull(it) }
} else {
userWalletsListManager.userWallets
}
return flow.transformLatest { userWallets ->
userWallets.firstOrNull { it.walletId == userWalletId }
?.let { emit(it.right()) }
?: emit(GetUserWalletError.UserWalletNotFound.left())

View file

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

View file

@ -1,8 +1,10 @@
package com.tangem.domain.wallets.usecase
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.core.wallets.UserWalletsListRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
/**
* Use case for getting list of user wallets
@ -11,11 +13,23 @@ import kotlinx.coroutines.flow.Flow
*
[REDACTED_AUTHOR]
*/
class GetWalletsUseCase(private val userWalletsListManager: UserWalletsListManager) {
class GetWalletsUseCase(
private val userWalletsListManager: UserWalletsListManager,
private val userWalletsListRepository: UserWalletsListRepository,
private val useNewListRepository: Boolean,
) {
@Throws(IllegalArgumentException::class)
operator fun invoke(): Flow<List<UserWallet>> = userWalletsListManager.userWallets
operator fun invoke(): Flow<List<UserWallet>> = if (useNewListRepository) {
userWalletsListRepository.userWallets.map { requireNotNull(it) }
} else {
userWalletsListManager.userWallets
}
@Throws(IllegalArgumentException::class)
fun invokeSync(): List<UserWallet> = userWalletsListManager.userWalletsSync
fun invokeSync(): List<UserWallet> = if (useNewListRepository) {
userWalletsListRepository.userWallets.value!!
} else {
userWalletsListManager.userWalletsSync
}
}

View file

@ -0,0 +1,24 @@
package com.tangem.domain.wallets.usecase
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.wallets.derivations.DerivationsRepository
typealias BackendId = String
/**
* Use case to check if user has missed derivations
*
[REDACTED_AUTHOR]
*/
class HasMissedDerivationsUseCase(
private val derivationsRepository: DerivationsRepository,
) {
/** Check if user [userWalletId] has missed derivations using map of [com.tangem.domain.models.network.Network.ID] with extraDerivationPath */
suspend operator fun invoke(
userWalletId: UserWalletId,
networksWithDerivationPath: Map<BackendId, String?>,
): Boolean {
return derivationsRepository.hasMissedDerivations(userWalletId, networksWithDerivationPath)
}
}

View file

@ -4,6 +4,7 @@ import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.core.wallets.UserWalletsListRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
@ -12,12 +13,22 @@ import kotlinx.coroutines.flow.map
*
* @property userWalletsListManager user wallets list manager
*/
class IsNeedToBackupUseCase(private val userWalletsListManager: UserWalletsListManager) {
class IsNeedToBackupUseCase(
private val userWalletsListManager: UserWalletsListManager,
private val userWalletsListRepository: UserWalletsListRepository,
private val useNewRepository: Boolean,
) {
operator fun invoke(id: UserWalletId): Flow<Boolean> {
return userWalletsListManager.userWallets
val userWalletsFlow = if (useNewRepository) {
userWalletsListRepository.userWallets
} else {
userWalletsListManager.userWallets
}
return userWalletsFlow
.map { wallets ->
val wallet = wallets.firstOrNull { it.walletId == id }
val wallet = wallets?.firstOrNull { it.walletId == id }
if (wallet == null) {
false
} else {

View file

@ -6,10 +6,12 @@ import arrow.core.raise.either
import arrow.core.right
import com.tangem.common.doOnFailure
import com.tangem.common.doOnSuccess
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.domain.core.wallets.error.SaveWalletError
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.wallets.legacy.UserWalletsListError
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.models.SaveWalletError
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.wallets.repository.WalletsRepository
/**
* Use case for saving user wallet
@ -18,22 +20,60 @@ import com.tangem.domain.models.wallet.UserWallet
*
[REDACTED_AUTHOR]
*/
class SaveWalletUseCase(private val userWalletsListManager: UserWalletsListManager) {
class SaveWalletUseCase(
private val userWalletsListManager: UserWalletsListManager,
private val userWalletsListRepository: UserWalletsListRepository,
private val walletsRepository: WalletsRepository,
private val useNewRepository: Boolean,
) {
suspend operator fun invoke(userWallet: UserWallet, canOverride: Boolean = false): Either<SaveWalletError, Unit> {
return either {
userWalletsListManager.save(userWallet, canOverride)
.doOnSuccess { return Unit.right() }
.doOnFailure {
return when (it) {
is UserWalletsListError.WalletAlreadySaved -> SaveWalletError.WalletAlreadySaved(
it.messageResId,
)
else -> SaveWalletError.DataError(it.messageResId)
}.left()
}
return if (useNewRepository) {
either {
val newUserWallet =
userWalletsListRepository.userWalletsSync().none { it.walletId == userWallet.walletId }
val userWallet = userWalletsListRepository.saveWithoutLock(userWallet, canOverride).bind()
return Unit.right()
if (newUserWallet) {
when (userWallet) {
is UserWallet.Cold -> {
if (walletsRepository.useBiometricAuthentication()) {
userWalletsListRepository.setLock(
userWallet.walletId,
UserWalletsListRepository.LockMethod.Biometric,
)
} else {
Unit.right()
}
}
is UserWallet.Hot -> {
userWalletsListRepository.setLock(
userWallet.walletId,
UserWalletsListRepository.LockMethod.NoLock,
)
}
}.mapLeft {
SaveWalletError.DataError(null)
}.map {
userWalletsListRepository.select(userWallet.walletId)
}.bind()
}
}
} else {
either {
userWalletsListManager.save(userWallet, canOverride)
.doOnSuccess { return Unit.right() }
.doOnFailure {
return when (it) {
is UserWalletsListError.WalletAlreadySaved -> SaveWalletError.WalletAlreadySaved(
it.messageResId,
)
else -> SaveWalletError.DataError(it.messageResId)
}.left()
}
return Unit.right()
}
}
}
}

View file

@ -4,11 +4,12 @@ import arrow.core.Either
import arrow.core.raise.either
import arrow.core.right
import com.tangem.common.CompletionResult
import com.tangem.domain.core.wallets.error.SelectWalletError
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.models.SelectWalletError
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.core.wallets.UserWalletsListRepository
/**
* Use case for selecting wallet
@ -20,10 +21,19 @@ import com.tangem.domain.models.wallet.UserWalletId
*/
class SelectWalletUseCase(
private val userWalletsListManager: UserWalletsListManager,
private val userWalletsListRepository: UserWalletsListRepository,
private val useNewRepository: Boolean,
private val reduxStateHolder: ReduxStateHolder,
) {
suspend operator fun invoke(userWalletId: UserWalletId): Either<SelectWalletError, UserWallet> {
if (useNewRepository) {
return userWalletsListRepository.select(userWalletId).map {
reduxStateHolder.onUserWalletSelected(it)
it
}
}
return either {
return when (val result = userWalletsListManager.select(userWalletId)) {
is CompletionResult.Failure -> raise(SelectWalletError.UnableToSelectUserWallet)

View file

@ -7,6 +7,9 @@ import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.models.UpdateWalletError
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.core.wallets.error.SaveWalletError
import com.tangem.domain.wallets.models.UpdateWalletError.*
import com.tangem.domain.core.wallets.UserWalletsListRepository
/**
* Use case for updating user wallet
@ -15,15 +18,38 @@ import com.tangem.domain.models.wallet.UserWalletId
*
[REDACTED_AUTHOR]
*/
class UpdateWalletUseCase(private val userWalletsListManager: UserWalletsListManager) {
class UpdateWalletUseCase(
private val userWalletsListManager: UserWalletsListManager,
private val userWalletsListRepository: UserWalletsListRepository,
private val useNewRepository: Boolean,
) {
suspend operator fun invoke(
userWalletId: UserWalletId,
update: suspend (UserWallet) -> UserWallet,
): Either<UpdateWalletError, UserWallet> = either {
when (val result = userWalletsListManager.update(userWalletId, update)) {
is CompletionResult.Failure -> raise(UpdateWalletError.DataError(result.error))
is CompletionResult.Success -> result.data
): Either<UpdateWalletError, UserWallet> {
if (useNewRepository) {
val userWallet = userWalletsListRepository.userWallets.value?.find { it.walletId == userWalletId }
?: return Either.Left(
UpdateWalletError.DataError(IllegalStateException("User wallet with id $userWalletId not found")),
)
val updatedWallet = update(userWallet)
return userWalletsListRepository.saveWithoutLock(updatedWallet, canOverride = true)
.mapLeft {
when (it) {
is SaveWalletError.DataError -> DataError(
IllegalStateException("Failed to update wallet: ${it.messageId}"),
)
is SaveWalletError.WalletAlreadySaved -> UpdateWalletError.NameAlreadyExists
}
}
}
return either {
when (val result = userWalletsListManager.update(userWalletId, update)) {
is CompletionResult.Failure -> raise(UpdateWalletError.DataError(result.error))
is CompletionResult.Success -> result.data
}
}
}
}

View file

@ -22,7 +22,11 @@ class GetSavedWalletsCountUseCaseTest {
@Before
fun setup() {
userWalletsListManager = mockk()
useCase = GetSavedWalletsCountUseCase(userWalletsListManager)
useCase = GetSavedWalletsCountUseCase(
userWalletsListManager,
userWalletsListRepository = mockk(),
useNewRepository = false,
)
mockkStatic("com.tangem.domain.wallets.legacy.UserWalletsListManagerExtensionsKt")
}