Updated on 2026-08-14

This commit is contained in:
Tangem 2025-08-22 10:09:11 +02:00
commit e4754fed3b
483 changed files with 9315 additions and 4084 deletions

View file

@ -10,10 +10,12 @@ tasks.withType<Test>().configureEach {
dependencies {
api(projects.domain.core)
api(projects.domain.models)
api(projects.domain.wallets.models)
implementation(deps.arrow.core)
implementation(deps.kotlin.coroutines)
implementation(deps.kotlin.serialization)
testImplementation(deps.test.coroutine)

View file

@ -3,7 +3,10 @@ package com.tangem.domain.account.models
import arrow.core.Either
import arrow.core.raise.either
import arrow.core.raise.ensure
import com.tangem.domain.models.TokensGroupType
import com.tangem.domain.models.TokensSortType
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.utils.extensions.addOrReplace
import kotlinx.serialization.Serializable
@ -22,6 +25,8 @@ data class AccountList private constructor(
val userWallet: UserWallet,
val accounts: Set<Account>,
val totalAccounts: Int,
val sortType: TokensSortType,
val groupType: TokensGroupType,
) {
/** Retrieves the main crypto portfolio account from the list of accounts */
@ -48,6 +53,8 @@ data class AccountList private constructor(
userWallet = this.userWallet,
accounts = accounts,
totalAccounts = this.totalAccounts + if (isNewAccount) 1 else 0,
sortType = this.sortType,
groupType = this.groupType,
)
}
@ -68,6 +75,8 @@ data class AccountList private constructor(
userWallet = this.userWallet,
accounts = accounts,
totalAccounts = this.totalAccounts - if (isExistingAccount) 1 else 0,
sortType = this.sortType,
groupType = this.groupType,
)
}
@ -132,6 +141,8 @@ data class AccountList private constructor(
userWallet: UserWallet,
accounts: Set<Account>,
totalAccounts: Int,
sortType: TokensSortType = TokensSortType.NONE,
groupType: TokensGroupType = TokensGroupType.NONE,
): Either<Error, AccountList> = either {
ensure(accounts.isNotEmpty()) { Error.EmptyAccountsList }
@ -149,10 +160,16 @@ data class AccountList private constructor(
val uniqueAccountIdsCount = accounts.map { it.accountId.value }.distinct().size
ensure(accounts.size == uniqueAccountIdsCount) { Error.DuplicateAccountIds }
val uniqueAccountNameCount = accounts.map { it.name.value }.distinct().size
val uniqueAccountNameCount = accounts.map { it.accountName.value }.distinct().size
ensure(accounts.size == uniqueAccountNameCount) { Error.DuplicateAccountNames }
AccountList(userWallet = userWallet, accounts = accounts, totalAccounts = totalAccounts)
AccountList(
userWallet = userWallet,
accounts = accounts,
totalAccounts = totalAccounts,
sortType = sortType,
groupType = groupType,
)
}
/**
@ -160,13 +177,23 @@ data class AccountList private constructor(
*
* @param userWallet the user wallet associated with the account list
*/
fun empty(userWallet: UserWallet): AccountList {
fun empty(
userWallet: UserWallet,
cryptoCurrencies: Set<CryptoCurrency> = emptySet(),
sortType: TokensSortType = TokensSortType.NONE,
groupType: TokensGroupType = TokensGroupType.NONE,
): AccountList {
return AccountList(
userWallet = userWallet,
accounts = setOf(
Account.CryptoPortfolio.createMainAccount(userWalletId = userWallet.walletId),
Account.CryptoPortfolio.createMainAccount(
userWalletId = userWallet.walletId,
cryptoCurrencies = cryptoCurrencies,
),
),
totalAccounts = 1,
sortType = sortType,
groupType = groupType,
)
}

View file

@ -7,6 +7,7 @@ import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import kotlinx.coroutines.flow.Flow
/**
* Repository interface for performing CRUD operations on accounts
@ -38,20 +39,47 @@ interface AccountsCRUDRepository {
*/
suspend fun getArchivedAccount(accountId: AccountId): Option<ArchivedAccount>
/**
* Retrieves a list of archived accounts associated with a specific user wallet
*
* @param userWalletId the unique identifier of the user wallet
* @return an [Option] containing a list of [ArchivedAccount] if found, or `Option.None` if not
*/
suspend fun getArchivedAccountsSync(userWalletId: UserWalletId): Option<List<ArchivedAccount>>
/**
* Provides a flow of archived accounts associated with a specific user wallet
*
* @param userWalletId the unique identifier of the user wallet
*/
fun getArchivedAccounts(userWalletId: UserWalletId): Flow<List<ArchivedAccount>>
/**
* Fetches archived accounts for a specific user wallet and updates the repository
*
* @param userWalletId the unique identifier of the user wallet
*/
suspend fun fetchArchivedAccounts(userWalletId: UserWalletId)
/**
* Saves a list of accounts to the repository
*
* @param accountList the list of accounts to be saved.
*/
@Throws
suspend fun saveAccounts(accountList: AccountList)
/**
* Retrieves the total count of accounts associated with a specific user wallet including archived accounts
*
* @param userWalletId the unique identifier of the user wallet
*/
suspend fun getTotalAccountsCount(userWalletId: UserWalletId): Int
/**
* Retrieves a user wallet by its unique identifier
*
* @param userWalletId the unique identifier of the user wallet
* @return the [UserWallet] associated with the given identifier
*/
@Throws
fun getUserWallet(userWalletId: UserWalletId): UserWallet
}

View file

@ -64,14 +64,9 @@ class AddCryptoPortfolioUseCase(
return Account.CryptoPortfolio(
accountId = AccountId.forCryptoPortfolio(userWalletId = userWalletId, derivationIndex = derivationIndex),
accountName = accountName,
accountIcon = icon,
icon = icon,
derivationIndex = derivationIndex,
isArchived = false,
cryptoCurrencyList = Account.CryptoPortfolio.CryptoCurrencyList(
currencies = emptySet(),
sortType = TokensSortType.NONE,
groupType = TokensGroupType.NONE,
),
cryptoCurrencies = emptySet(),
)
}
@ -88,7 +83,13 @@ class AddCryptoPortfolioUseCase(
catch = { raise(Error.DataOperationFailed(cause = it)) },
)
return AccountList.empty(userWallet = userWallet)
// TODO: [REDACTED_JIRA]
return AccountList.empty(
userWallet = userWallet,
cryptoCurrencies = emptySet(),
sortType = TokensSortType.NONE,
groupType = TokensGroupType.NONE,
)
}
private suspend fun Raise<Error>.saveAccounts(accountList: AccountList) {

View file

@ -0,0 +1,86 @@
package com.tangem.domain.account.usecase
import arrow.core.Either
import arrow.core.getOrElse
import com.tangem.domain.account.models.ArchivedAccount
import com.tangem.domain.account.repository.AccountsCRUDRepository
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.core.lce.LceFlow
import com.tangem.domain.core.utils.lceContent
import com.tangem.domain.core.utils.lceError
import com.tangem.domain.core.utils.lceLoading
import com.tangem.domain.models.wallet.UserWalletId
import kotlinx.coroutines.channels.ProducerScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.retryWhen
import kotlinx.coroutines.launch
typealias ArchivedAccountList = List<ArchivedAccount>
/**
* Use case for retrieving archived accounts for a specific user wallet
*
* @property crudRepository the repository for performing CRUD operations on accounts
*
[REDACTED_AUTHOR]
*/
class GetArchivedAccountsUseCase(
private val crudRepository: AccountsCRUDRepository,
) {
/**
* Executes the use case to retrieve archived accounts for the given user wallet
*
* @param userWalletId the unique identifier of the user wallet
*/
operator fun invoke(userWalletId: UserWalletId): LceFlow<Throwable, ArchivedAccountList> = channelFlow {
val archivedAccounts = getArchivedAccounts(userWalletId = userWalletId)
archivedAccounts
.onRight { send(it.lceContent()) }
.onLeft {
send(lceLoading())
launch {
fetchArchivedAccounts(userWalletId).getOrElse {
send(it.lceError())
}
}
}
subscribeOnArchivedAccounts(userWalletId)
}
.distinctUntilChanged()
private suspend fun getArchivedAccounts(userWalletId: UserWalletId): Either<Throwable, ArchivedAccountList> {
return Either.catch {
crudRepository.getArchivedAccountsSync(userWalletId = userWalletId).getOrElse {
error("Archived accounts not found for user wallet: $userWalletId")
}
}
}
private suspend fun fetchArchivedAccounts(userWalletId: UserWalletId): Either<Throwable, Unit> {
return Either.catch { crudRepository.fetchArchivedAccounts(userWalletId) }
}
private suspend fun ProducerScope<Lce<Throwable, ArchivedAccountList>>.subscribeOnArchivedAccounts(
userWalletId: UserWalletId,
) {
crudRepository.getArchivedAccounts(userWalletId)
.distinctUntilChanged()
.retryWhen { cause, _ ->
send(cause.lceError())
delay(timeMillis = 2000)
true
}
.collectLatest { archivedAccounts ->
send(archivedAccounts.lceContent())
}
}
}

View file

@ -0,0 +1,61 @@
package com.tangem.domain.account.usecase
import arrow.core.Either
import arrow.core.getOrElse
import arrow.core.raise.Raise
import arrow.core.raise.catch
import arrow.core.raise.either
import com.tangem.domain.account.repository.AccountsCRUDRepository
import com.tangem.domain.models.account.DerivationIndex
import com.tangem.domain.models.wallet.UserWalletId
/**
* Use case for retrieving the next unoccupied account index
*
* @property crudRepository repository for performing CRUD operations on accounts
*
[REDACTED_AUTHOR]
*/
class GetUnoccupiedAccountIndexUseCase(
private val crudRepository: AccountsCRUDRepository,
) {
/**
* Invokes the use case to calculate the next unoccupied account index
*
* @param userWalletId the unique identifier of the user wallet
*/
suspend operator fun invoke(userWalletId: UserWalletId): Either<Error, DerivationIndex> = either {
val totalAccountsCount = getTotalAccountsCount(userWalletId = userWalletId)
DerivationIndex(totalAccountsCount + 1).getOrElse {
raise(Error.InvalidDerivationIndex(it))
}
}
private suspend fun Raise<Error>.getTotalAccountsCount(userWalletId: UserWalletId): Int {
return catch(
block = { crudRepository.getTotalAccountsCount(userWalletId = userWalletId) },
catch = { raise(Error.DataOperationFailed(cause = it)) },
)
}
/**
* Represents possible errors that can occur in the use case
*/
sealed interface Error {
val tag: String
get() = this::class.simpleName ?: "GetUnoccupiedAccountIndexUseCase.Error"
/** Error indicating that the derivation index is invalid */
data class InvalidDerivationIndex(val cause: DerivationIndex.Error) : Error {
override fun toString(): String = "$tag: Invalid derivation index: $cause"
}
/** Error indicating that a data operation failed */
data class DataOperationFailed(val cause: Throwable) : Error {
override fun toString(): String = "$tag: Data operation failed: ${cause.message ?: "Unknown error"}"
}
}
}

View file

@ -8,8 +8,6 @@ import arrow.core.raise.either
import com.tangem.domain.account.models.AccountList
import com.tangem.domain.account.models.ArchivedAccount
import com.tangem.domain.account.repository.AccountsCRUDRepository
import com.tangem.domain.models.TokensGroupType
import com.tangem.domain.models.TokensSortType
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.wallet.UserWalletId
@ -66,14 +64,10 @@ class RecoverCryptoPortfolioUseCase(
return Account.CryptoPortfolio(
accountId = this.accountId,
accountName = this.name,
accountIcon = this.icon,
icon = this.icon,
derivationIndex = this.derivationIndex,
isArchived = false,
cryptoCurrencyList = Account.CryptoPortfolio.CryptoCurrencyList(
currencies = emptySet(),
sortType = TokensSortType.NONE,
groupType = TokensGroupType.NONE,
),
// TODO: [REDACTED_JIRA]
cryptoCurrencies = emptySet(),
)
}

View file

@ -79,7 +79,7 @@ class UpdateCryptoPortfolioUseCase(
}
private fun Account.CryptoPortfolio.setIcon(icon: CryptoPortfolioIcon?): Account.CryptoPortfolio {
return if (icon != null) this.copy(accountIcon = icon) else this
return if (icon != null) this.copy(icon = icon) else this
}
/**

View file

@ -123,7 +123,7 @@ class AccountListTest {
accounts = setOf(
Account.CryptoPortfolio.createMainAccount(userWalletId),
Account.CryptoPortfolio.createMainAccount(userWalletId).copy(
accountIcon = CryptoPortfolioIcon.ofDefaultCustomAccount(),
icon = CryptoPortfolioIcon.ofDefaultCustomAccount(),
),
),
expected = AccountList.Error.ExceedsMaxMainAccountsCount.left(),

View file

@ -46,7 +46,7 @@ class AddCryptoPortfolioUseCaseTest {
// Act
val actual = useCase(
userWalletId = userWalletId,
accountName = newAccount.name,
accountName = newAccount.accountName,
icon = newAccount.icon,
derivationIndex = newAccount.derivationIndex,
)
@ -75,7 +75,7 @@ class AddCryptoPortfolioUseCaseTest {
// Act
val actual = useCase(
userWalletId = userWalletId,
accountName = newAccount.name,
accountName = newAccount.accountName,
icon = newAccount.icon,
derivationIndex = newAccount.derivationIndex,
)
@ -107,7 +107,7 @@ class AddCryptoPortfolioUseCaseTest {
// Act
val actual = useCase(
userWalletId = userWalletId,
accountName = newAccount.name,
accountName = newAccount.accountName,
icon = newAccount.icon,
derivationIndex = newAccount.derivationIndex,
)
@ -138,7 +138,7 @@ class AddCryptoPortfolioUseCaseTest {
// Act
val actual = useCase(
userWalletId = userWalletId,
accountName = newAccount.name,
accountName = newAccount.accountName,
icon = newAccount.icon,
derivationIndex = newAccount.derivationIndex,
)
@ -170,7 +170,7 @@ class AddCryptoPortfolioUseCaseTest {
// Act
val actual = useCase(
userWalletId = userWalletId,
accountName = newAccount.name,
accountName = newAccount.accountName,
icon = newAccount.icon,
derivationIndex = newAccount.derivationIndex,
)

View file

@ -39,8 +39,7 @@ class ArchiveCryptoPortfolioUseCaseTest {
val accountList = (AccountList.empty(userWallet) + account).getOrNull()!!
val accountId = account.accountId
val archivedAccount = account.copy(isArchived = true)
val updatedAccountList = (accountList - archivedAccount).getOrNull()!!
val updatedAccountList = (accountList - account).getOrNull()!!
coEvery { crudRepository.getAccounts(userWalletId) } returns accountList.toOption()
@ -130,8 +129,7 @@ class ArchiveCryptoPortfolioUseCaseTest {
val accountList = (AccountList.empty(userWallet) + account).getOrNull()!!
val accountId = account.accountId
val archivedAccount = account.copy(isArchived = true)
val updatedAccountList = (accountList - archivedAccount).getOrNull()!!
val updatedAccountList = (accountList - account).getOrNull()!!
val exception = IllegalStateException("Save failed")

View file

@ -0,0 +1,158 @@
package com.tangem.domain.account.usecase
import arrow.core.None
import arrow.core.toOption
import com.google.common.truth.Truth
import com.tangem.domain.account.models.ArchivedAccount
import com.tangem.domain.account.repository.AccountsCRUDRepository
import com.tangem.domain.core.utils.lceContent
import com.tangem.domain.core.utils.lceError
import com.tangem.domain.core.utils.lceLoading
import com.tangem.domain.models.wallet.UserWalletId
import io.mockk.*
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
@OptIn(ExperimentalCoroutinesApi::class)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class GetArchivedAccountsUseCaseTest {
private val crudRepository: AccountsCRUDRepository = mockk(relaxUnitFun = true)
private val useCase = GetArchivedAccountsUseCase(crudRepository)
private val userWalletId = UserWalletId("011")
@BeforeEach
fun resetMocks() {
clearMocks(crudRepository)
}
@Test
fun `invoke should emit archived accounts when repository returns data`() = runTest {
// Arrange
val archivedAccounts = listOf(
mockk<ArchivedAccount>(),
mockk<ArchivedAccount>(),
)
coEvery { crudRepository.getArchivedAccountsSync(userWalletId) } returns archivedAccounts.toOption()
every { crudRepository.getArchivedAccounts(userWalletId) } returns flowOf(archivedAccounts)
// Act
val actual = getEmittedValues(useCase(userWalletId))
// Assert
val expected = listOf(archivedAccounts.lceContent())
Truth.assertThat(actual).isEqualTo(expected)
coVerifyOrder {
crudRepository.getArchivedAccountsSync(userWalletId)
crudRepository.getArchivedAccounts(userWalletId)
}
coVerify(exactly = 0) { crudRepository.fetchArchivedAccounts(any()) }
}
@Test
fun `invoke should emit loading and fetch when accounts not found`() = runTest {
// Arrange
val archivedAccounts = listOf(
mockk<ArchivedAccount>(),
mockk<ArchivedAccount>(),
)
coEvery { crudRepository.getArchivedAccountsSync(userWalletId) } returns None
every { crudRepository.getArchivedAccounts(userWalletId) } returns flowOf(archivedAccounts)
// Act
val actual = getEmittedValues(useCase(userWalletId))
// Assert
val expected = listOf(
lceLoading(),
archivedAccounts.lceContent(),
)
Truth.assertThat(actual).isEqualTo(expected)
coVerify(exactly = 1) {
crudRepository.getArchivedAccountsSync(userWalletId)
crudRepository.fetchArchivedAccounts(userWalletId)
crudRepository.getArchivedAccounts(userWalletId)
}
}
@Test
fun `invoke should emit error if getArchivedAccountsSync throws exception`() = runTest {
// Arrange
val exception = IllegalStateException("Test error")
val archivedAccounts = listOf(
mockk<ArchivedAccount>(),
mockk<ArchivedAccount>(),
)
coEvery { crudRepository.getArchivedAccountsSync(userWalletId) } throws exception
every { crudRepository.getArchivedAccounts(userWalletId) } returns flowOf(archivedAccounts)
// Act
val actual = getEmittedValues(useCase(userWalletId))
// Assert
val expected = listOf(
lceLoading(),
archivedAccounts.lceContent(),
)
Truth.assertThat(actual).isEqualTo(expected)
coVerify(exactly = 1) {
crudRepository.getArchivedAccountsSync(userWalletId)
crudRepository.fetchArchivedAccounts(userWalletId)
crudRepository.getArchivedAccounts(userWalletId)
}
}
@Test
fun `invoke should emit error if fetchArchivedAccounts throws exception`() = runTest {
// Arrange
val exception = IllegalStateException("Fetch error")
coEvery { crudRepository.getArchivedAccountsSync(userWalletId) } returns None
every { crudRepository.getArchivedAccounts(userWalletId) } returns emptyFlow()
coEvery { crudRepository.fetchArchivedAccounts(userWalletId) } throws exception
// Act
val actual = getEmittedValues(useCase(userWalletId))
// Assert
val expected = listOf(
lceLoading(),
exception.lceError(),
)
Truth.assertThat(actual).isEqualTo(expected)
coVerify(exactly = 1) {
crudRepository.getArchivedAccountsSync(userWalletId)
crudRepository.fetchArchivedAccounts(userWalletId)
crudRepository.getArchivedAccounts(userWalletId)
}
}
@OptIn(ExperimentalCoroutinesApi::class)
fun <T> TestScope.getEmittedValues(flow: Flow<T>): List<T> {
val values = mutableListOf<T>()
backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) {
flow.toList(values)
}
return values
}
}

View file

@ -0,0 +1,59 @@
package com.tangem.domain.account.usecase
import arrow.core.left
import com.google.common.truth.Truth
import com.tangem.domain.account.repository.AccountsCRUDRepository
import com.tangem.domain.models.account.DerivationIndex
import com.tangem.domain.models.wallet.UserWalletId
import io.mockk.clearMocks
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.mockk
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class GetUnoccupiedAccountIndexUseCaseTest {
private val crudRepository: AccountsCRUDRepository = mockk(relaxUnitFun = true)
private val useCase = GetUnoccupiedAccountIndexUseCase(crudRepository)
private val userWalletId = UserWalletId("011")
@BeforeEach
fun resetMocks() {
clearMocks(crudRepository)
}
@Test
fun `invoke should return next unoccupied index when repository returns count`() = runTest {
// Arrange
coEvery { crudRepository.getTotalAccountsCount(userWalletId) } returns 3
// Act
val actual = useCase(userWalletId = userWalletId)
// Assert
val expected = DerivationIndex(4)
Truth.assertThat(actual).isEqualTo(expected)
coVerify { crudRepository.getTotalAccountsCount(userWalletId) }
}
@Test
fun `invoke should return error if repository throws exception`() = runTest {
// Arrange
val exception = IllegalStateException("Test error")
coEvery { crudRepository.getTotalAccountsCount(userWalletId) } throws exception
// Act
val actual = useCase(userWalletId = userWalletId)
// Assert
val expected = GetUnoccupiedAccountIndexUseCase.Error.DataOperationFailed(exception).left()
Truth.assertThat(actual).isEqualTo(expected)
coVerify { crudRepository.getTotalAccountsCount(userWalletId) }
}
}

View file

@ -43,15 +43,14 @@ class RecoverCryptoPortfolioUseCaseTest {
val accountList = AccountList.empty(userWallet)
val archivedAccount = ArchivedAccount(
accountId = account.accountId,
name = account.name,
name = account.accountName,
icon = account.icon,
derivationIndex = account.derivationIndex,
tokensCount = 1,
networksCount = 1,
)
val recoveredAccount = account.copy(isArchived = false)
val updatedAccountList = (accountList + recoveredAccount).getOrNull()!!
val updatedAccountList = (accountList + account).getOrNull()!!
coEvery { crudRepository.getAccounts(userWalletId) } returns accountList.toOption()
coEvery { crudRepository.getArchivedAccount(account.accountId) } returns archivedAccount.toOption()
@ -60,7 +59,7 @@ class RecoverCryptoPortfolioUseCaseTest {
val actual = useCase(account.accountId)
// Assert
val expected = recoveredAccount.right()
val expected = account.right()
Truth.assertThat(actual).isEqualTo(expected)
coVerifyOrder {
@ -173,15 +172,14 @@ class RecoverCryptoPortfolioUseCaseTest {
val accountList = AccountList.empty(userWallet)
val archivedAccount = ArchivedAccount(
accountId = account.accountId,
name = account.name,
name = account.accountName,
icon = account.icon,
derivationIndex = account.derivationIndex,
tokensCount = 1,
networksCount = 1,
)
val recoveredAccount = account.copy(isArchived = false)
val updatedAccountList = (accountList + recoveredAccount).getOrNull()!!
val updatedAccountList = (accountList + account).getOrNull()!!
val exception = IllegalStateException("Save failed")
coEvery { crudRepository.getAccounts(userWalletId) } returns accountList.toOption()

View file

@ -73,7 +73,7 @@ class UpdateCryptoPortfolioUseCaseTest {
value = CryptoPortfolioIcon.Icon.Star,
color = CryptoPortfolioIcon.Color.CaribbeanBlue,
)
val updatedAccount = accountList.mainAccount.copy(accountIcon = newAccountIcon)
val updatedAccount = accountList.mainAccount.copy(icon = newAccountIcon)
val updatedAccountList = (accountList + updatedAccount).getOrNull()!!
coEvery { crudRepository.getAccounts(userWalletId = userWalletId) } returns accountList.toOption()
@ -102,7 +102,7 @@ class UpdateCryptoPortfolioUseCaseTest {
value = CryptoPortfolioIcon.Icon.Star,
color = CryptoPortfolioIcon.Color.CaribbeanBlue,
)
val updatedAccount = accountList.mainAccount.copy(accountName = newAccountName, accountIcon = newAccountIcon)
val updatedAccount = accountList.mainAccount.copy(accountName = newAccountName, icon = newAccountIcon)
val updatedAccountList = (accountList + updatedAccount).getOrNull()!!
coEvery { crudRepository.getAccounts(userWalletId = userWalletId) } returns accountList.toOption()

View file

@ -1,7 +1,5 @@
package com.tangem.domain.account.utils
import com.tangem.domain.models.TokensGroupType
import com.tangem.domain.models.TokensSortType
import com.tangem.domain.models.account.*
import com.tangem.domain.models.wallet.UserWalletId
import kotlin.random.Random
@ -33,13 +31,8 @@ fun createAccount(
return Account.CryptoPortfolio(
accountId = AccountId.forCryptoPortfolio(userWalletId = userWalletId, derivationIndex = derivationIndex),
accountName = AccountName(name).getOrNull()!!,
accountIcon = icon,
icon = icon,
derivationIndex = derivationIndex,
isArchived = false,
cryptoCurrencyList = Account.CryptoPortfolio.CryptoCurrencyList(
currencies = emptySet(),
sortType = TokensSortType.NONE,
groupType = TokensGroupType.NONE,
),
cryptoCurrencies = emptySet(),
)
}

View file

@ -0,0 +1,13 @@
package com.domain.blockaid.models.transaction.simultation
import java.math.BigDecimal
sealed class ApproveInfo {
data class Amount(
val approvedAmount: BigDecimal,
val isUnlimited: Boolean,
val tokenInfo: TokenInfo,
) : ApproveInfo()
data class NonFungibleToken(val name: String, val logoUrl: String?) : ApproveInfo()
}

View file

@ -1,9 +0,0 @@
package com.domain.blockaid.models.transaction.simultation
import java.math.BigDecimal
data class ApprovedAmount(
val approvedAmount: BigDecimal,
val isUnlimited: Boolean,
val tokenInfo: TokenInfo,
)

View file

@ -16,9 +16,7 @@ sealed class SimulationData {
/**
* Represents an approve operation with the specified amount (can be multiple amounts for NFT)
*/
data class Approve(
val approvedAmounts: List<ApprovedAmount>,
) : SimulationData()
data class Approve(val items: List<ApproveInfo>) : SimulationData()
/**
* Simulation was successfully performed and no changes detected

View file

@ -0,0 +1,9 @@
package com.tangem.domain.card.analytics
internal sealed class AnalyticsParam {
sealed class CurrencyType(val value: String) {
class Blockchain(blockchain: com.tangem.blockchain.common.Blockchain) : CurrencyType(blockchain.currency)
class Token(token: com.tangem.blockchain.common.Token) : CurrencyType(token.symbol)
}
}

View file

@ -0,0 +1,14 @@
package com.tangem.domain.card.analytics
import com.tangem.core.analytics.models.AnalyticsEvent
sealed class IntroductionProcess(
event: String,
params: Map<String, String> = mapOf(),
) : AnalyticsEvent("Introduction Process", event, params) {
object ScreenOpened : IntroductionProcess("Introduction Process Screen Opened")
object ButtonTokensList : IntroductionProcess("Button - Tokens List")
object ButtonBuyCards : IntroductionProcess("Button - Buy Cards")
object ButtonScanCard : IntroductionProcess("Button - Scan Card")
}

View file

@ -0,0 +1,23 @@
package com.tangem.domain.card.analytics
import com.tangem.blockchain.common.Blockchain
import com.tangem.core.analytics.models.AnalyticsParam.WalletType
import com.tangem.domain.card.CardTypesResolver
import com.tangem.utils.converter.Converter
class ParamCardCurrencyConverter : Converter<CardTypesResolver, WalletType?> {
override fun convert(value: CardTypesResolver): WalletType? {
if (value.isMultiwalletAllowed()) return WalletType.MultiCurrency
val type = when {
value.isTangemNote() -> AnalyticsParam.CurrencyType.Blockchain(value.getBlockchain())
value.isTangemTwins() -> AnalyticsParam.CurrencyType.Blockchain(Blockchain.Bitcoin)
value.getBlockchain() != Blockchain.Unknown -> AnalyticsParam.CurrencyType.Blockchain(value.getBlockchain())
value.getPrimaryToken() != null -> AnalyticsParam.CurrencyType.Token(value.getPrimaryToken()!!)
else -> null
} ?: return null
return WalletType.SingleCurrency(type.value)
}
}

View file

@ -0,0 +1,11 @@
package com.tangem.domain.card.analytics
import com.tangem.core.analytics.models.AnalyticsEvent
sealed class Shop(
event: String,
params: Map<String, String> = mapOf(),
) : AnalyticsEvent("Shop", event, params) {
object ScreenOpened : Shop("Shop Screen Opened")
}

View file

@ -73,8 +73,21 @@ interface UserWalletsListRepository {
* If the wallet is not found, it returns [SetLockError.UserWalletNotFound]
* If the wallet is locked, it returns [SetLockError.UserWalletLocked]
* If the lock method is not supported, it returns [SetLockError.UnableToSetLock].
*
* @param userWalletId The ID of the user wallet to set the lock for.
* @param lockMethod The method to use for locking the wallet.
* @param changeUnsecured If false, the method will have no effect on unsecured wallets.
*/
suspend fun setLock(userWalletId: UserWalletId, lockMethod: LockMethod): Either<SetLockError, Unit>
suspend fun setLock(
userWalletId: UserWalletId,
lockMethod: LockMethod,
changeUnsecured: Boolean = true,
): Either<SetLockError, Unit>
/**
* Removes biometric lock for user wallet if it is set.
*/
suspend fun removeBiometricLock(userWalletId: UserWalletId)
/**
* Deletes user wallets by ids.

View file

@ -0,0 +1,7 @@
package com.tangem.domain.core.wallets.error
sealed interface SaveFirstColdWalletError {
data object CreateWalletError : SaveFirstColdWalletError
data class SaveError(val error: SaveWalletError) : SaveFirstColdWalletError
data class SelectError(val error: SelectWalletError) : SaveFirstColdWalletError
}

View file

@ -7,11 +7,13 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason
import com.tangem.domain.transaction.models.AssetRequirementsCondition
import kotlinx.coroutines.flow.Flow
/**
* Manager that holds info about available actions as Sell and Buy
*/
@Deprecated("Move to express domain layer")
interface RampStateManager {
suspend fun availableForBuy(userWallet: UserWallet, cryptoCurrency: CryptoCurrency): ScenarioUnavailabilityReason
@ -50,4 +52,9 @@ interface RampStateManager {
userWalletId: UserWalletId,
cryptoCurrencyStatus: CryptoCurrencyStatus,
): ScenarioUnavailabilityReason
/**
* Returns whether asset requirements are full filled to be able use express services
*/
fun checkAssetRequirements(requirements: AssetRequirementsCondition?): Boolean
}

View file

@ -10,6 +10,7 @@ class GetManagedTokensUseCase(
operator fun invoke(
context: ManageTokensListBatchingContext,
// only for onboarding case, change carefully and check repository implementation
loadUserTokensFromRemote: Boolean,
batchSize: Int = 40,
): ManageTokensListBatchFlow {

View file

@ -6,9 +6,13 @@ import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.domain.card.common.extensions.supportedBlockchains
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.domain.core.wallets.requireUserWalletsSync
class FilterAvailableNetworksForWalletUseCase(
private val userWalletsListManager: UserWalletsListManager,
private val userWalletsListRepository: UserWalletsListRepository,
private val useNewRepository: Boolean,
private val excludedBlockchains: ExcludedBlockchains,
) {
@ -20,7 +24,7 @@ class FilterAvailableNetworksForWalletUseCase(
userWalletId: UserWalletId,
networks: Set<TokenMarketInfo.Network>,
): Set<TokenMarketInfo.Network> {
val userWallet = userWalletsListManager.userWalletsSync.firstOrNull {
val userWallet = getWallets().firstOrNull {
it.walletId == userWalletId
} ?: return networks.toSet()
@ -33,4 +37,10 @@ class FilterAvailableNetworksForWalletUseCase(
supportedBlockchains.contains(blockchain)
}.toSet()
}
private fun getWallets() = if (useNewRepository) {
userWalletsListRepository.requireUserWalletsSync()
} else {
userWalletsListManager.userWalletsSync
}
}

View file

@ -1,9 +1,8 @@
package com.tangem.domain.models.account
import arrow.core.Either
import arrow.core.getOrElse
import arrow.core.raise.either
import com.tangem.domain.models.TokensGroupType
import com.tangem.domain.models.TokensSortType
import com.tangem.domain.models.account.Account.CryptoPortfolio.Error.AccountNameError
import com.tangem.domain.models.account.Account.CryptoPortfolio.Error.DerivationIndexError
import com.tangem.domain.models.currency.CryptoCurrency
@ -22,7 +21,7 @@ sealed interface Account {
val accountId: AccountId
/** Name of the account */
val name: AccountName
val accountName: AccountName
/** The identifier of the user wallet associated with the account */
val userWalletId: UserWalletId
@ -31,21 +30,19 @@ sealed interface Account {
/**
* Represents a crypto portfolio account
*
* @property accountId unique identifier of the account
* @property name name of the account
* @property icon icon representing the account
* @property derivationIndex index used for derivation of the account
* @property isArchived indicates whether the account is archived
* @property cryptoCurrencyList list of tokens associated with the account
* @property accountId unique identifier of the account
* @property accountName name of the account
* @property icon icon representing the account
* @property derivationIndex index used for derivation of the account
* @property cryptoCurrencies set of tokens associated with the account
*/
@Serializable
data class CryptoPortfolio private constructor(
override val accountId: AccountId,
override val name: AccountName,
override val accountName: AccountName,
val icon: CryptoPortfolioIcon,
val derivationIndex: DerivationIndex,
val isArchived: Boolean,
val cryptoCurrencyList: CryptoCurrencyList,
val cryptoCurrencies: Set<CryptoCurrency>,
) : Account {
/** Indicates if the account is the main account */
@ -54,41 +51,22 @@ sealed interface Account {
/** Number of tokens in the account */
val tokensCount: Int
get() = cryptoCurrencyList.currencies.size
get() = cryptoCurrencies.size
/** Number of distinct networks in the account */
val networksCount: Int
get() = cryptoCurrencyList.currencies.map(CryptoCurrency::network).distinct().size
get() = cryptoCurrencies.map(CryptoCurrency::network).distinct().size
fun copy(
accountName: AccountName = this.name,
accountIcon: CryptoPortfolioIcon = this.icon,
isArchived: Boolean = this.isArchived,
): CryptoPortfolio {
fun copy(accountName: AccountName = this.accountName, icon: CryptoPortfolioIcon = this.icon): CryptoPortfolio {
return CryptoPortfolio(
accountId = this.accountId,
name = accountName,
icon = accountIcon,
accountName = accountName,
icon = icon,
derivationIndex = this.derivationIndex,
isArchived = isArchived,
cryptoCurrencyList = this.cryptoCurrencyList,
cryptoCurrencies = this.cryptoCurrencies,
)
}
/**
* Represents a list of tokens in the account
*
* @property currencies set of cryptocurrencies in the account
* @property sortType sorting type for the tokens
* @property groupType grouping type for the tokens
*/
@Serializable
data class CryptoCurrencyList(
val currencies: Set<CryptoCurrency>,
val sortType: TokensSortType,
val groupType: TokensGroupType,
)
/**
* Represents possible errors when creating a crypto portfolio account
*/
@ -109,33 +87,34 @@ sealed interface Account {
/**
* Constructor for creating a [CryptoPortfolio] instance
*
* @param accountId unique identifier of the account
* @param name name of the account
* @param accountIcon icon representing the account
* @param derivationIndex index used for derivation of the account
* @param isArchived indicates whether the account is archived
* @param cryptoCurrencyList list of tokens associated with the account
* @param accountId unique identifier of the account
* @param name name of the account
* @param icon icon representing the account
* @param derivationIndex index used for derivation of the account
* @param cryptoCurrencies set of tokens associated with the account
*/
@Suppress("LongParameterList")
operator fun invoke(
accountId: AccountId,
name: String,
accountIcon: CryptoPortfolioIcon,
icon: CryptoPortfolioIcon,
derivationIndex: Int,
isArchived: Boolean,
cryptoCurrencyList: CryptoCurrencyList,
cryptoCurrencies: Set<CryptoCurrency> = emptySet(),
): Either<Error, CryptoPortfolio> {
return either {
val accountName = AccountName(value = name).mapLeft(::AccountNameError).bind()
val derivationIndex = DerivationIndex(derivationIndex).mapLeft(::DerivationIndexError).bind()
val accountName = AccountName(value = name).getOrElse {
raise(AccountNameError(cause = it))
}
val derivationIndex = DerivationIndex(value = derivationIndex).getOrElse {
raise(DerivationIndexError(cause = it))
}
invoke(
accountId = accountId,
accountName = accountName,
accountIcon = accountIcon,
icon = icon,
derivationIndex = derivationIndex,
isArchived = isArchived,
cryptoCurrencyList = cryptoCurrencyList,
cryptoCurrencies = cryptoCurrencies,
)
}
}
@ -143,38 +122,39 @@ sealed interface Account {
/**
* Constructor for creating a [CryptoPortfolio] instance
*
* @param accountId unique identifier of the account
* @param accountName name of the account
* @param accountIcon icon representing the account
* @param derivationIndex index used for derivation of the account
* @param isArchived indicates whether the account is archived
* @param cryptoCurrencyList list of tokens associated with the account
* @param accountId unique identifier of the account
* @param accountName name of the account
* @param icon icon representing the account
* @param derivationIndex index used for derivation of the account
* @param cryptoCurrencies set of tokens associated with the account
*/
@Suppress("LongParameterList")
operator fun invoke(
accountId: AccountId,
accountName: AccountName,
accountIcon: CryptoPortfolioIcon,
icon: CryptoPortfolioIcon,
derivationIndex: DerivationIndex,
isArchived: Boolean,
cryptoCurrencyList: CryptoCurrencyList,
cryptoCurrencies: Set<CryptoCurrency> = emptySet(),
): CryptoPortfolio {
return CryptoPortfolio(
accountId = accountId,
name = accountName,
icon = accountIcon,
accountName = accountName,
icon = icon,
derivationIndex = derivationIndex,
isArchived = isArchived,
cryptoCurrencyList = cryptoCurrencyList,
cryptoCurrencies = cryptoCurrencies,
)
}
/**
* Creates a main account for the given user wallet ID
*
* @param userWalletId the ID of the user wallet
* @param userWalletId the ID of the user wallet
* @param cryptoCurrencies set of tokens associated with the account
*/
fun createMainAccount(userWalletId: UserWalletId): CryptoPortfolio {
fun createMainAccount(
userWalletId: UserWalletId,
cryptoCurrencies: Set<CryptoCurrency> = emptySet(),
): CryptoPortfolio {
val derivationIndex = DerivationIndex.Main
return CryptoPortfolio(
@ -182,15 +162,10 @@ sealed interface Account {
userWalletId = userWalletId,
derivationIndex = derivationIndex,
),
name = AccountName.Main,
accountName = AccountName.Main,
icon = CryptoPortfolioIcon.ofMainAccount(userWalletId),
derivationIndex = derivationIndex,
isArchived = false,
cryptoCurrencyList = CryptoCurrencyList(
currencies = emptySet(),
sortType = TokensSortType.NONE,
groupType = TokensGroupType.NONE,
),
cryptoCurrencies = cryptoCurrencies,
)
}
}

View file

@ -1,10 +1,7 @@
package com.tangem.domain.models.account
import com.google.common.truth.Truth
import com.tangem.domain.models.TokensGroupType
import com.tangem.domain.models.TokensSortType
import com.tangem.domain.models.account.Account.CryptoPortfolio
import com.tangem.domain.models.account.Account.CryptoPortfolio.CryptoCurrencyList
import com.tangem.domain.models.account.Account.CryptoPortfolio.Error.AccountNameError
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWalletId
@ -100,13 +97,12 @@ class AccountTest {
val name = ""
// Act
val actual = CryptoPortfolio(
val actual = CryptoPortfolio.invoke(
accountId = mockk(),
name = name,
accountIcon = mockk(),
icon = mockk(),
derivationIndex = 0,
isArchived = false,
cryptoCurrencyList = mockk(),
cryptoCurrencies = emptySet(),
)
.leftOrNull()!!
@ -125,14 +121,9 @@ class AccountTest {
derivationIndex = derivationIndex,
),
name = "Test Account",
accountIcon = CryptoPortfolioIcon.ofMainAccount(userWalletId = UserWalletId("011")),
icon = CryptoPortfolioIcon.ofMainAccount(userWalletId = UserWalletId("011")),
derivationIndex = derivationIndex.value,
isArchived = false,
cryptoCurrencyList = CryptoCurrencyList(
currencies = emptySet(),
sortType = TokensSortType.NONE,
groupType = TokensGroupType.NONE,
),
cryptoCurrencies = emptySet(),
)
.getOrNull()!!
@ -157,14 +148,9 @@ class AccountTest {
derivationIndex = derivationIndex,
),
accountName = AccountName.Main,
accountIcon = CryptoPortfolioIcon.ofMainAccount(userWalletId),
icon = CryptoPortfolioIcon.ofMainAccount(userWalletId),
derivationIndex = derivationIndex,
isArchived = false,
cryptoCurrencyList = CryptoCurrencyList(
currencies = emptySet(),
sortType = TokensSortType.NONE,
groupType = TokensGroupType.NONE,
),
cryptoCurrencies = emptySet(),
)
Truth.assertThat(actual).isEqualTo(expected)
@ -182,14 +168,9 @@ class AccountTest {
return CryptoPortfolio.invoke(
accountId = AccountId.forCryptoPortfolio(userWalletId = userWalletId, derivationIndex = accountIndex),
name = name,
accountIcon = CryptoPortfolioIcon.ofMainAccount(userWalletId),
icon = CryptoPortfolioIcon.ofMainAccount(userWalletId),
derivationIndex = derivationIndex,
isArchived = false,
cryptoCurrencyList = CryptoCurrencyList(
currencies = currencies,
sortType = TokensSortType.NONE,
groupType = TokensGroupType.NONE,
),
cryptoCurrencies = currencies,
)
.getOrNull()!!
}

View file

@ -24,8 +24,10 @@ interface SettingsRepository {
suspend fun setShouldOpenWelcomeScreenOnResume(value: Boolean)
@Deprecated("Use walletsRepository.requireAccessCode instead")
suspend fun shouldSaveAccessCodes(): Boolean
@Deprecated("Use walletsRepository.requireAccessCode instead")
suspend fun setShouldSaveAccessCodes(value: Boolean)
suspend fun incrementAppLaunchCounter()

View file

@ -9,7 +9,7 @@ import com.tangem.domain.models.tokenlist.TokenList
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.tokens.error.mapper.mapToTokenListError
import com.tangem.domain.tokens.operations.BaseCurrenciesStatusesOperations
import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations
import com.tangem.domain.tokens.operations.TokenListOperations
import com.tangem.domain.tokens.repository.CurrenciesRepository
import kotlinx.coroutines.ExperimentalCoroutinesApi
@ -19,7 +19,7 @@ import kotlinx.coroutines.flow.transformLatest
class GetTokenListUseCase(
private val currenciesRepository: CurrenciesRepository,
private val currenciesStatusesOperations: BaseCurrenciesStatusesOperations,
private val currenciesStatusesOperations: BaseCurrencyStatusOperations,
) {
@OptIn(ExperimentalCoroutinesApi::class)

View file

@ -12,7 +12,7 @@ import com.tangem.domain.models.TotalFiatBalance
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.tokens.operations.BaseCurrenciesStatusesOperations
import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations
import com.tangem.domain.tokens.operations.TokenListFiatBalanceOperations
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.*
@ -20,7 +20,7 @@ import timber.log.Timber
import java.util.concurrent.ConcurrentHashMap
class GetWalletTotalBalanceUseCase(
private val currenciesStatusesOperations: BaseCurrenciesStatusesOperations,
private val currenciesStatusesOperations: BaseCurrencyStatusOperations,
) {
private val walletBalanceCache = ConcurrentHashMap<UserWalletId, TotalFiatBalance.Loaded>()

View file

@ -3,6 +3,7 @@ package com.tangem.domain.tokens.operations
import arrow.core.*
import arrow.core.raise.*
import com.tangem.blockchainsdk.utils.toBlockchain
import com.tangem.domain.core.lce.LceFlow
import com.tangem.domain.core.utils.EitherFlow
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
@ -28,6 +29,7 @@ import com.tangem.domain.staking.single.SingleYieldBalanceSupplier
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier
import com.tangem.domain.tokens.TokensFeatureToggles
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations.Error
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.utils.CurrencyStatusProxyCreator
@ -56,6 +58,8 @@ abstract class BaseCurrencyStatusOperations(
protected val currencyStatusProxyCreator = CurrencyStatusProxyCreator()
abstract fun getCurrenciesStatuses(userWalletId: UserWalletId): LceFlow<TokenListError, List<CryptoCurrencyStatus>>
protected abstract fun getQuotes(id: CryptoCurrency.RawID): Flow<Either<Error, Set<QuoteStatus>>>
protected abstract suspend fun fetchComponents(
@ -382,8 +386,9 @@ abstract class BaseCurrencyStatusOperations(
multiWalletCryptoCurrenciesSupplier.getSyncOrNull(
params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId),
)
?.firstOrNull { it.network.id == networkId && it.network.derivationPath == derivationPath }
?: error("Unable to create network coin with ID: $networkId and derivation path: $derivationPath")
?.filterIsInstance<CryptoCurrency.Coin>()
?.firstOrNull { it.network.id == networkId }
?: error("Unable to create network coin with ID: $networkId")
} else {
currenciesRepository.getNetworkCoin(userWalletId, networkId, derivationPath)
}

View file

@ -60,19 +60,18 @@ class CachedCurrenciesStatusesOperations(
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
private val stakingIdFactory: StakingIdFactory,
private val tokensFeatureToggles: TokensFeatureToggles,
) : BaseCurrenciesStatusesOperations,
BaseCurrencyStatusOperations(
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
multiNetworkStatusSupplier = multiNetworkStatusSupplier,
singleNetworkStatusSupplier = singleNetworkStatusSupplier,
singleQuoteStatusSupplier = singleQuoteStatusSupplier,
singleYieldBalanceSupplier = singleYieldBalanceSupplier,
multiYieldBalanceSupplier = multiYieldBalanceSupplier,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
stakingIdFactory = stakingIdFactory,
tokensFeatureToggles = tokensFeatureToggles,
) {
) : BaseCurrencyStatusOperations(
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
multiNetworkStatusSupplier = multiNetworkStatusSupplier,
singleNetworkStatusSupplier = singleNetworkStatusSupplier,
singleQuoteStatusSupplier = singleQuoteStatusSupplier,
singleYieldBalanceSupplier = singleYieldBalanceSupplier,
multiYieldBalanceSupplier = multiYieldBalanceSupplier,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
stakingIdFactory = stakingIdFactory,
tokensFeatureToggles = tokensFeatureToggles,
) {
override fun getCurrenciesStatuses(
userWalletId: UserWalletId,
@ -83,6 +82,7 @@ class CachedCurrenciesStatusesOperations(
)
}
@Suppress("LongMethod")
@OptIn(ExperimentalCoroutinesApi::class)
private fun transformToCurrenciesStatuses(
userWalletId: UserWalletId,
@ -163,10 +163,26 @@ class CachedCurrenciesStatusesOperations(
.invokeOnCompletion { setFetchFinished(userWalletId) }
}
val networksStatusesUpdates = getNetworkStatusesUpdates(userWalletId, networks)
combine(
flow = getQuotes(currenciesIds),
flow2 = getNetworkStatusesUpdates(userWalletId, networks),
flow3 = getYieldsBalancesUpdates(userWalletId, currencies),
flow2 = networksStatusesUpdates,
flow3 = networksStatusesUpdates.flatMapLatest { maybeNetworksStatuses ->
val networksStatuses = maybeNetworksStatuses.getOrNull()
val currenciesAddresses = if (networksStatuses == null) {
emptyMap()
} else {
currencies.associate { currency ->
val networkStatus = networksStatuses.firstOrNull { it.network == currency.network }
currency.id to extractAddress(networkStatus)
}
}
getYieldsBalancesUpdates(userWalletId, currenciesAddresses)
},
flow4 = fetchingState.map {
val state = it[userWalletId] ?: return@map false
@ -379,24 +395,27 @@ class CachedCurrenciesStatusesOperations(
// temporary code because token list is built using networks list
private fun getYieldsBalancesUpdates(
userWalletId: UserWalletId,
cryptoCurrencies: List<CryptoCurrency>,
cryptoCurrencies: Map<CryptoCurrency.ID, String?>,
): EitherFlow<TokenListError, List<YieldBalance>> {
return channelFlow {
val state = MutableStateFlow(emptyList<YieldBalance>())
val stakingIds = cryptoCurrencies.mapNotNullTo(hashSetOf()) {
stakingIdFactory.create(userWalletId = userWalletId, currencyId = it.id, network = it.network)
val stakingIds = cryptoCurrencies.mapNotNullTo(hashSetOf()) { currencyWithAddress ->
stakingIdFactory.create(
currencyId = currencyWithAddress.key,
defaultAddress = currencyWithAddress.value,
)
.getOrNull()
}
stakingIds.onEach {
stakingIds.onEach { stakingId ->
launch {
singleYieldBalanceSupplier(
params = SingleYieldBalanceProducer.Params(userWalletId = userWalletId, stakingId = it),
params = SingleYieldBalanceProducer.Params(userWalletId = userWalletId, stakingId = stakingId),
)
.onEach { balance ->
state.update { loadedBalances ->
loadedBalances.addOrReplace(balance) { balance.stakingId == it }
loadedBalances.addOrReplace(balance) { balance.stakingId == it.stakingId }
}
}
.launchIn(scope = this)

View file

@ -109,4 +109,18 @@ interface TransactionRepository {
userWalletId: UserWalletId,
network: Network,
): com.tangem.blockchain.extensions.Result<List<ByteArray>>
suspend fun prepareAndSign(
transactionData: TransactionData,
signer: TransactionSigner,
userWalletId: UserWalletId,
network: Network,
): com.tangem.blockchain.extensions.Result<ByteArray>
suspend fun prepareAndSignMultiple(
transactionData: List<TransactionData>,
signer: TransactionSigner,
userWalletId: UserWalletId,
network: Network,
): com.tangem.blockchain.extensions.Result<List<ByteArray>>
}

View file

@ -0,0 +1,70 @@
package com.tangem.domain.transaction.usecase
import arrow.core.Either
import arrow.core.left
import arrow.core.right
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.TransactionSigner
import com.tangem.blockchain.extensions.Result
import com.tangem.domain.card.common.TapWorkarounds.isTangemTwins
import com.tangem.domain.card.models.TwinKey
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.requireColdWallet
import com.tangem.domain.transaction.TransactionRepository
import com.tangem.domain.transaction.error.SendTransactionError
class PrepareAndSignUseCase(
private val transactionRepository: TransactionRepository,
private val cardSdkConfigRepository: CardSdkConfigRepository,
) {
suspend operator fun invoke(
transactionData: TransactionData,
userWallet: UserWallet,
network: Network,
): Either<SendTransactionError, ByteArray> {
val signer = createSigner(userWallet)
val result = transactionRepository.prepareAndSign(
transactionData = transactionData,
userWalletId = userWallet.walletId,
network = network,
signer = signer,
)
return when (result) {
is Result.Failure -> SendTransactionUseCase.handleError(result).left()
is Result.Success -> result.data.right()
}
}
suspend operator fun invoke(
transactionData: List<TransactionData>,
userWallet: UserWallet,
network: Network,
): Either<SendTransactionError, List<ByteArray>> {
val signer = createSigner(userWallet)
val result = transactionRepository.prepareAndSignMultiple(
transactionData = transactionData,
userWalletId = userWallet.walletId,
network = network,
signer = signer,
)
return when (result) {
is Result.Failure -> SendTransactionUseCase.handleError(result).left()
is Result.Success -> result.data.right()
}
}
private fun createSigner(userWallet: UserWallet): TransactionSigner {
userWallet.requireColdWallet() // TODO [REDACTED_TASK_KEY]
val card = userWallet.scanResponse.card
val isCardNotBackedUp = card.backupStatus?.isActive != true && !card.isTangemTwins
val signer = cardSdkConfigRepository.getCommonSigner(
cardId = card.cardId.takeIf { isCardNotBackedUp },
twinKey = TwinKey.getOrNull(scanResponse = userWallet.scanResponse),
)
return signer
}
}

View file

@ -4,8 +4,8 @@ import kotlinx.serialization.Serializable
@Serializable
data class VisaDataToSignByCustomerWallet(
val request: VisaCustomerWalletDataToSignRequest,
val hashToSign: String,
val request: VisaCustomerWalletDataToSignRequest? = null,
)
fun VisaDataToSignByCustomerWallet.sign(signature: String, customerWalletAddress: String) =

View file

@ -0,0 +1,6 @@
package com.tangem.domain.visa.model
data class VisaSignedChallengeByCustomerWallet(
val challenge: String,
val signature: String,
)

View file

@ -3,13 +3,12 @@ package com.tangem.domain.pay.repository
import arrow.core.Either
import com.tangem.core.error.UniversalError
import com.tangem.domain.pay.KycStartInfo
import com.tangem.domain.models.wallet.UserWalletId
interface KycRepository {
suspend fun getKycStartInfo(): Either<UniversalError, KycStartInfo>
suspend fun getKycStartInfo(address: String, cardId: String): Either<UniversalError, KycStartInfo>
interface Factory {
fun create(userWalletId: UserWalletId): KycRepository
fun create(): KycRepository
}
}

View file

@ -18,6 +18,16 @@ interface VisaAuthRepository {
cardWalletAddress: String,
): Either<VisaApiError, VisaAuthChallenge.Wallet>
suspend fun getCustomerWalletAuthChallenge(
customerWalletAddress: String,
): Either<VisaApiError, VisaAuthChallenge.Wallet>
suspend fun getTokenWithCustomerWallet(
sessionId: String,
signature: String,
nonce: String,
): Either<VisaApiError, String>
suspend fun getAccessTokens(signedChallenge: VisaAuthSignedChallenge): Either<VisaApiError, VisaAuthTokens>
suspend fun refreshAccessTokens(refreshToken: VisaAuthTokens.RefreshToken): Either<VisaApiError, VisaAuthTokens>

View file

@ -5,6 +5,11 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class WcEthAddChain(
/**
* chainId are identified by EIP-155 integers expressed in hexadecimal notation,
* with 0x prefix and no leading zeroes for the chainId value.
* For more information https://eips.ethereum.org/EIPS/eip-5792#atomicbatch-capability
*/
@Json(name = "chainId")
val chainId: String,
)

View file

@ -1,7 +1,5 @@
package com.tangem.domain.walletconnect.model
import com.tangem.domain.models.network.Network
sealed interface WcEthMethod : WcMethod {
data class MessageSign(
@ -15,7 +13,7 @@ sealed interface WcEthMethod : WcMethod {
val account: String,
val dataForSign: String,
) : WcEthMethod {
val humanMsg: String = params.message.contents.orEmpty()
val humanMsg: String = params.message?.contents.orEmpty()
}
data class SendTransaction(
@ -28,6 +26,9 @@ sealed interface WcEthMethod : WcMethod {
data class AddEthereumChain(
val rawChain: WcEthAddChain,
val network: Network,
) : WcEthMethod
data class SwitchEthereumChain(
val rawChain: WcEthAddChain,
) : WcEthMethod
}

View file

@ -6,24 +6,24 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class WcEthSignTypedDataParams(
@Json(name = "domain")
val domain: Domain,
val domain: Domain?,
@Json(name = "message")
val message: Message,
val message: Message?,
@Json(name = "primaryType")
val primaryType: String,
val primaryType: String?,
@Json(name = "types")
val types: Map<String, List<Types.Type>>,
) {
@JsonClass(generateAdapter = true)
data class Domain(
@Json(name = "chainId")
val chainId: Int,
val chainId: Int?,
@Json(name = "name")
val name: String,
val name: String?,
@Json(name = "verifyingContract")
val verifyingContract: String,
val verifyingContract: String?,
@Json(name = "version")
val version: String,
val version: String?,
)
@JsonClass(generateAdapter = true)

View file

@ -14,6 +14,7 @@ enum class WcEthMethodName(override val raw: String) : WcMethodName {
SignTransaction("eth_signTransaction"),
SendTransaction("eth_sendTransaction"),
AddEthereumChain("wallet_addEthereumChain"),
SwitchEthereumChain("wallet_switchEthereumChain"),
}
enum class WcSolanaMethodName(override val raw: String) : WcMethodName {

View file

@ -16,4 +16,5 @@ sealed class WcPairError(
data class ApprovalFailed(override val message: String) : WcPairError("107 002 003")
data object RejectionFailed : WcPairError("107 002 004")
data class Unknown(override val message: String) : WcPairError(message)
data class TimeoutException(override val message: String) : WcPairError(message)
}

View file

@ -69,4 +69,9 @@ sealed class HandleMethodError(
data object UnknownSession : HandleMethodError(message = "WalletConnect session was disconnected")
data class UnknownError(override val message: String) : HandleMethodError(message)
data class TangemUnsupportedNetwork(val unsupportedNetwork: String) :
HandleMethodError("TangemUnsupportedNetwork $unsupportedNetwork")
data class NotAddedNetwork(val networkName: String) : HandleMethodError("NotAddedNetwork $networkName")
data class RequiredNetwork(val networkName: String) : HandleMethodError("RequiredNetwork $networkName")
}

View file

@ -2,18 +2,27 @@ package com.tangem.domain.walletconnect.model
sealed interface WcSolanaMethod : WcMethod {
val methodName: String
val trimmedPrefixMethodName: String get() = methodName.substringAfter("_")
data class SignMessage(
val pubKey: String,
val rawMessage: String,
val humanMsg: String,
) : WcSolanaMethod
) : WcSolanaMethod {
override val methodName: String = WcSolanaMethodName.SignMessage.raw
}
data class SignTransaction(
val transaction: String,
val address: String?,
) : WcSolanaMethod
) : WcSolanaMethod {
override val methodName: String = WcSolanaMethodName.SignTransaction.raw
}
data class SignAllTransaction(
val transaction: List<String>,
) : WcSolanaMethod
) : WcSolanaMethod {
override val methodName: String = WcSolanaMethodName.SendAllTransaction.raw
}
}

View file

@ -6,4 +6,12 @@ package com.tangem.domain.walletconnect.model.sdkcopy
data class WcSdkSession(
val topic: String,
val appMetaData: WcAppMetaData,
)
val namespaces: Map<String, Session>,
) {
data class Session(
val chains: List<String>,
val accounts: List<String>,
val methods: List<String>,
val events: List<String>,
)
}

View file

@ -11,6 +11,7 @@ import com.tangem.domain.walletconnect.model.WcSessionApprove
import com.tangem.domain.walletconnect.model.WcSessionProposal
import com.tangem.domain.walletconnect.model.sdkcopy.WcAppMetaData
import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest
import com.tangem.utils.extensions.mapNotNullValues
sealed class WcAnalyticEvents(
event: String,
@ -103,7 +104,7 @@ sealed class WcAnalyticEvents(
class SignatureRequestReceived(
rawRequest: WcSdkSessionRequest,
network: Network,
emulationStatus: EmulationStatus,
emulationStatus: EmulationStatus?,
) : WcAnalyticEvents(
event = "Signature Request Received",
params = mapOf(
@ -111,8 +112,8 @@ sealed class WcAnalyticEvents(
AnalyticsParam.Key.DAPP_URL to rawRequest.dAppMetaData.url,
AnalyticsParam.Key.METHOD_NAME to rawRequest.request.method,
AnalyticsParam.Key.BLOCKCHAIN to network.name,
AnalyticsParam.Key.EMULATION_STATUS to emulationStatus.status,
),
AnalyticsParam.Key.EMULATION_STATUS to emulationStatus?.status,
).mapNotNullValues { it.value },
) {
enum class EmulationStatus(val status: String) {
Emulated("Emulated"),

View file

@ -1,12 +1,20 @@
package com.tangem.domain.walletconnect.usecase.method
import arrow.core.Either
import com.tangem.domain.models.network.Network
import com.tangem.domain.walletconnect.model.HandleMethodError
import com.tangem.domain.walletconnect.model.WcRequestError
interface WcAddNetworkUseCase :
WcMethodUseCase,
WcMethodContext {
suspend operator fun invoke(): Either<HandleMethodError, AddNetwork>
suspend fun approve(): Either<WcRequestError, String>
fun reject()
data class AddNetwork(
val network: Network,
val isExistInWcSession: Boolean,
)
}

View file

@ -0,0 +1,18 @@
package com.tangem.domain.walletconnect.usecase.method
import arrow.core.Either
import com.tangem.domain.models.network.Network
import com.tangem.domain.walletconnect.model.HandleMethodError
interface WcSwitchNetworkUseCase :
WcMethodUseCase,
WcMethodContext {
suspend operator fun invoke(): Either<HandleMethodError, SwitchNetwork>
fun reject()
data class SwitchNetwork(
val network: Network,
val isExistInWcSession: Boolean,
)
}

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 // TODO [REDACTED_TASK_KEY] [Hot Wallet] Derivation config for hot wallet
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

@ -4,17 +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 -> {
// TODO [REDACTED_TASK_KEY] [Hot Wallet] Derivation config for hot wallet
val primaryCurve = Wallet2CardConfig.primaryCurve(blockchain)
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

@ -1,17 +1,49 @@
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()
suspend fun requestPassword(hasBiometry: Boolean): Result
/**
* 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()

View file

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

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

@ -6,6 +6,7 @@ import com.tangem.common.doOnFailure
import com.tangem.domain.wallets.legacy.UserWalletsListManager
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,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

@ -1,11 +0,0 @@
package com.tangem.domain.wallets.usecase
import com.tangem.sdk.api.TangemSdkManager
import javax.inject.Inject
class GetIsBiometricsEnabledUseCase @Inject constructor(
private val tangemSdkManager: TangemSdkManager,
) {
operator fun invoke(): Boolean = runCatching(tangemSdkManager::needEnrollBiometrics).getOrNull() ?: false
}

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

@ -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.wallets.legacy.UserWalletsListError
import com.tangem.domain.wallets.legacy.UserWalletsListManager
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.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")
}