Updated on 2026-08-14
This commit is contained in:
commit
e4754fed3b
483 changed files with 9315 additions and 4084 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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"}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()!!
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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>()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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>>
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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) =
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem.domain.visa.model
|
||||
|
||||
data class VisaSignedChallengeByCustomerWallet(
|
||||
val challenge: String,
|
||||
val signature: String,
|
||||
)
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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>,
|
||||
)
|
||||
}
|
||||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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() }
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
package com.tangem.domain.wallets.models
|
||||
|
||||
sealed interface SelectWalletError {
|
||||
|
||||
object UnableToSelectUserWallet : SelectWalletError
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue