Updated on 2026-08-14
This commit is contained in:
commit
fe2cfac54d
1140 changed files with 23493 additions and 8579 deletions
1
domain/account/.gitignore
vendored
Normal file
1
domain/account/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
26
domain/account/build.gradle.kts
Normal file
26
domain/account/build.gradle.kts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
plugins {
|
||||
alias(deps.plugins.kotlin.jvm)
|
||||
alias(deps.plugins.kotlin.serialization)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
tasks.withType<Test>().configureEach {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
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)
|
||||
testImplementation(deps.test.junit5)
|
||||
testRuntimeOnly(deps.test.junit5.engine)
|
||||
testImplementation(deps.test.mockk)
|
||||
testImplementation(deps.test.truth)
|
||||
}
|
||||
|
|
@ -0,0 +1,177 @@
|
|||
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.account.Account
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.utils.extensions.addOrReplace
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Represents a list of accounts associated with a user wallet
|
||||
*
|
||||
* @property userWallet the user wallet associated with the account list
|
||||
* @property accounts a set of accounts belonging to the user wallet
|
||||
* @property totalAccounts the total number of accounts
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Serializable
|
||||
data class AccountList private constructor(
|
||||
val userWallet: UserWallet,
|
||||
val accounts: Set<Account>,
|
||||
val totalAccounts: Int,
|
||||
) {
|
||||
|
||||
/** Retrieves the main crypto portfolio account from the list of accounts */
|
||||
val mainAccount: Account.CryptoPortfolio
|
||||
get() = accounts.first { it is Account.CryptoPortfolio && it.isMainAccount } as Account.CryptoPortfolio
|
||||
|
||||
/** Returns true if more accounts can be added (the maximum number of accounts has not been reached) */
|
||||
val canAddMoreAccounts: Boolean
|
||||
get() = accounts.size < MAX_ACCOUNTS_COUNT
|
||||
|
||||
/**
|
||||
* Adds an account to the account list.
|
||||
* If an account with the same identifier already exists, it will be replaced.
|
||||
* Returns a new [AccountList] instance with the updated accounts set, or a validation error if constraints are
|
||||
* violated (e.g., maximum number of accounts exceeded).
|
||||
*
|
||||
* @param other the account to add or replace
|
||||
*/
|
||||
operator fun plus(other: Account): Either<Error, AccountList> {
|
||||
val isNewAccount = this.accounts.none { it.accountId == other.accountId }
|
||||
val accounts = this.accounts.addOrReplace(other) { it.accountId == other.accountId }
|
||||
|
||||
return invoke(
|
||||
userWallet = this.userWallet,
|
||||
accounts = accounts,
|
||||
totalAccounts = this.totalAccounts + if (isNewAccount) 1 else 0,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the specified account from the account list.
|
||||
* Returns a new [AccountList] instance with the updated accounts set, or a validation error if constraints are
|
||||
* violated (e.g., the list becomes empty).
|
||||
*
|
||||
* @param other the account to remove
|
||||
*/
|
||||
operator fun minus(other: Account): Either<Error, AccountList> {
|
||||
val isExistingAccount = this.accounts.any { it.accountId == other.accountId }
|
||||
val accounts = this.accounts.toMutableSet().apply {
|
||||
removeIf { it.accountId == other.accountId }
|
||||
}
|
||||
|
||||
return invoke(
|
||||
userWallet = this.userWallet,
|
||||
accounts = accounts,
|
||||
totalAccounts = this.totalAccounts - if (isExistingAccount) 1 else 0,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents possible errors that can occur when creating an `AccountList`
|
||||
*/
|
||||
@Serializable
|
||||
sealed interface Error {
|
||||
|
||||
val tag: String
|
||||
get() = this::class.simpleName ?: "AccountListError"
|
||||
|
||||
@Serializable
|
||||
data object EmptyAccountsList : Error {
|
||||
override fun toString(): String = "$tag: The accounts list cannot be empty"
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data object MainAccountNotFound : Error {
|
||||
override fun toString(): String {
|
||||
return "$tag: Account list does not contain a main crypto portfolio account"
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data object ExceedsMaxMainAccountsCount : Error {
|
||||
override fun toString(): String {
|
||||
return "$tag: There should be at most one main crypto portfolio in the account list"
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data object ExceedsMaxAccountsCount : Error {
|
||||
override fun toString(): String = "$tag: The number of accounts must not exceed 20"
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data object DuplicateAccountIds : Error {
|
||||
override fun toString(): String = "$tag: Account list contains duplicate account IDs"
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data object DuplicateAccountNames : Error {
|
||||
override fun toString(): String = "$tag: Account list contains duplicate account names"
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val MAX_ACCOUNTS_COUNT = 20
|
||||
private const val MAX_MAIN_ACCOUNTS_COUNT = 1
|
||||
|
||||
/**
|
||||
* Factory method to create an `AccountList` instance.
|
||||
* Validates the input to ensure the accounts list is not empty and contains exactly one main account.
|
||||
*
|
||||
* @param userWallet the user wallet associated with the account list
|
||||
* @param accounts a set of accounts belonging to the user wallet
|
||||
* @param totalAccounts the total number of accounts
|
||||
*/
|
||||
operator fun invoke(
|
||||
userWallet: UserWallet,
|
||||
accounts: Set<Account>,
|
||||
totalAccounts: Int,
|
||||
): Either<Error, AccountList> = either {
|
||||
ensure(accounts.isNotEmpty()) { Error.EmptyAccountsList }
|
||||
|
||||
ensure(accounts.size <= MAX_ACCOUNTS_COUNT) { Error.ExceedsMaxAccountsCount }
|
||||
|
||||
val mainAccountsCount = accounts.mainAccountsCount()
|
||||
ensure(mainAccountsCount == MAX_MAIN_ACCOUNTS_COUNT) {
|
||||
if (mainAccountsCount == 0) {
|
||||
Error.MainAccountNotFound
|
||||
} else {
|
||||
Error.ExceedsMaxMainAccountsCount
|
||||
}
|
||||
}
|
||||
|
||||
val uniqueAccountIdsCount = accounts.map { it.accountId.value }.distinct().size
|
||||
ensure(accounts.size == uniqueAccountIdsCount) { Error.DuplicateAccountIds }
|
||||
|
||||
val uniqueAccountNameCount = accounts.map { it.name.value }.distinct().size
|
||||
ensure(accounts.size == uniqueAccountNameCount) { Error.DuplicateAccountNames }
|
||||
|
||||
AccountList(userWallet = userWallet, accounts = accounts, totalAccounts = totalAccounts)
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method to create an empty [AccountList] with a main crypto portfolio account
|
||||
*
|
||||
* @param userWallet the user wallet associated with the account list
|
||||
*/
|
||||
fun empty(userWallet: UserWallet): AccountList {
|
||||
return AccountList(
|
||||
userWallet = userWallet,
|
||||
accounts = setOf(
|
||||
Account.CryptoPortfolio.createMainAccount(userWalletId = userWallet.walletId),
|
||||
),
|
||||
totalAccounts = 1,
|
||||
)
|
||||
}
|
||||
|
||||
private fun Set<Account>.mainAccountsCount(): Int {
|
||||
return count { (it as? Account.CryptoPortfolio)?.isMainAccount == true }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package com.tangem.domain.account.models
|
||||
|
||||
import com.tangem.domain.models.account.AccountStatus
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Represents a list of account statuses associated with a user wallet
|
||||
*
|
||||
* @property userWallet the user wallet to which the account statuses belong
|
||||
* @property accountStatuses a set of account statuses associated with the user wallet
|
||||
* @property totalAccounts the total number of accounts
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Serializable
|
||||
data class AccountStatusList(
|
||||
val userWallet: UserWallet,
|
||||
val accountStatuses: Set<AccountStatus>,
|
||||
val totalAccounts: Int,
|
||||
)
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package com.tangem.domain.account.models
|
||||
|
||||
import com.tangem.domain.models.account.AccountId
|
||||
import com.tangem.domain.models.account.AccountName
|
||||
import com.tangem.domain.models.account.CryptoPortfolioIcon
|
||||
import com.tangem.domain.models.account.DerivationIndex
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Represents an archived crypto portfolio account
|
||||
*
|
||||
* @property accountId the unique identifier of the archived account
|
||||
* @property name the name of the archived account
|
||||
* @property icon the icon representing the archived account
|
||||
* @property derivationIndex the derivation index for the archived account
|
||||
* @property tokensCount the number of tokens in the archived account
|
||||
* @property networksCount the number of networks associated with the archived account
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Serializable
|
||||
data class ArchivedAccount(
|
||||
val accountId: AccountId,
|
||||
val name: AccountName,
|
||||
val icon: CryptoPortfolioIcon,
|
||||
val derivationIndex: DerivationIndex,
|
||||
val tokensCount: Int,
|
||||
val networksCount: Int,
|
||||
)
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
package com.tangem.domain.account.repository
|
||||
|
||||
import arrow.core.Option
|
||||
import com.tangem.domain.account.models.AccountList
|
||||
import com.tangem.domain.account.models.ArchivedAccount
|
||||
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
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
interface AccountsCRUDRepository {
|
||||
|
||||
/**
|
||||
* Retrieves a list of accounts associated with a specific user wallet
|
||||
*
|
||||
* @param userWalletId the unique identifier of the user wallet
|
||||
* @return an [Option] containing the [AccountList] if found, or `Option.None` if not
|
||||
*/
|
||||
suspend fun getAccounts(userWalletId: UserWalletId): Option<AccountList>
|
||||
|
||||
/**
|
||||
* Retrieves a specific account by its unique identifier
|
||||
*
|
||||
* @param accountId the unique identifier of the account
|
||||
* @return an [Option] containing the [Account.CryptoPortfolio] if found, or `Option.None` if not
|
||||
*/
|
||||
suspend fun getAccount(accountId: AccountId): Option<Account.CryptoPortfolio>
|
||||
|
||||
/**
|
||||
* Retrieves a archived account by its unique identifier
|
||||
*
|
||||
* @param accountId the unique identifier of the account
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
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
|
||||
*/
|
||||
fun getUserWallet(userWalletId: UserWalletId): UserWallet
|
||||
}
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
package com.tangem.domain.account.usecase
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.Option
|
||||
import arrow.core.getOrElse
|
||||
import arrow.core.raise.Raise
|
||||
import arrow.core.raise.catch
|
||||
import arrow.core.raise.either
|
||||
import com.tangem.domain.account.models.AccountList
|
||||
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.*
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
||||
/**
|
||||
* Use case for adding a new crypto portfolio account
|
||||
*
|
||||
* @property crudRepository the repository used for performing CRUD operations on accounts
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class AddCryptoPortfolioUseCase(
|
||||
private val crudRepository: AccountsCRUDRepository,
|
||||
) {
|
||||
|
||||
/**
|
||||
* Adds a new crypto portfolio account to the repository
|
||||
*
|
||||
* @param userWalletId the unique identifier of the user wallet
|
||||
* @param accountName the name of the new account
|
||||
* @param icon the icon representing the new account
|
||||
* @param derivationIndex the derivation index for the account
|
||||
*
|
||||
|
||||
*/
|
||||
suspend operator fun invoke(
|
||||
userWalletId: UserWalletId,
|
||||
accountName: AccountName,
|
||||
icon: CryptoPortfolioIcon,
|
||||
derivationIndex: DerivationIndex,
|
||||
): Either<Error, Account.CryptoPortfolio> = either {
|
||||
val newAccount = createAccount(userWalletId, accountName, icon, derivationIndex)
|
||||
|
||||
val accountList = getAccountList(userWalletId = userWalletId).getOrElse {
|
||||
createNewAccountList(userWalletId = userWalletId)
|
||||
}
|
||||
|
||||
val updatedAccounts = (accountList + newAccount).getOrElse {
|
||||
raise(Error.AccountListRequirementsNotMet(it))
|
||||
}
|
||||
|
||||
saveAccounts(updatedAccounts)
|
||||
|
||||
newAccount
|
||||
}
|
||||
|
||||
private fun Raise<Error>.createAccount(
|
||||
userWalletId: UserWalletId,
|
||||
accountName: AccountName,
|
||||
icon: CryptoPortfolioIcon,
|
||||
derivationIndex: DerivationIndex,
|
||||
): Account.CryptoPortfolio {
|
||||
return Account.CryptoPortfolio(
|
||||
accountId = AccountId.forCryptoPortfolio(userWalletId = userWalletId, derivationIndex = derivationIndex),
|
||||
accountName = accountName,
|
||||
accountIcon = icon,
|
||||
derivationIndex = derivationIndex,
|
||||
isArchived = false,
|
||||
cryptoCurrencyList = Account.CryptoPortfolio.CryptoCurrencyList(
|
||||
currencies = emptySet(),
|
||||
sortType = TokensSortType.NONE,
|
||||
groupType = TokensGroupType.NONE,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun Raise<Error>.getAccountList(userWalletId: UserWalletId): Option<AccountList> {
|
||||
return catch(
|
||||
block = { crudRepository.getAccounts(userWalletId = userWalletId) },
|
||||
catch = { raise(Error.DataOperationFailed(cause = it)) },
|
||||
)
|
||||
}
|
||||
|
||||
private fun Raise<Error>.createNewAccountList(userWalletId: UserWalletId): AccountList {
|
||||
val userWallet = catch(
|
||||
block = { crudRepository.getUserWallet(userWalletId = userWalletId) },
|
||||
catch = { raise(Error.DataOperationFailed(cause = it)) },
|
||||
)
|
||||
|
||||
return AccountList.empty(userWallet = userWallet)
|
||||
}
|
||||
|
||||
private suspend fun Raise<Error>.saveAccounts(accountList: AccountList) {
|
||||
catch(
|
||||
block = { crudRepository.saveAccounts(accountList) },
|
||||
catch = { raise(Error.DataOperationFailed(cause = it)) },
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents possible errors that can occur during the add operation
|
||||
*/
|
||||
sealed interface Error {
|
||||
|
||||
/**
|
||||
* Error indicating that the account list requirements were not met.
|
||||
*
|
||||
* @property cause the underlying cause of the error
|
||||
*/
|
||||
data class AccountListRequirementsNotMet(val cause: AccountList.Error) : Error {
|
||||
override fun toString(): String = "Account list requirements not met: $cause"
|
||||
}
|
||||
|
||||
/** Error indicating that a data operation failed */
|
||||
data class DataOperationFailed(val cause: Throwable) : Error {
|
||||
override fun toString(): String = "Data operation failed: ${cause.message ?: "Unknown error"}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
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.models.AccountList
|
||||
import com.tangem.domain.account.repository.AccountsCRUDRepository
|
||||
import com.tangem.domain.models.account.AccountId
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
||||
/**
|
||||
* Use case for archiving a crypto portfolio.
|
||||
* This class provides functionality to archive a specific account within a user's crypto portfolio.
|
||||
* It ensures that the account exists and meets the necessary requirements before performing the operation.
|
||||
*
|
||||
* @property crudRepository repository for performing CRUD operations on accounts
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class ArchiveCryptoPortfolioUseCase(
|
||||
private val crudRepository: AccountsCRUDRepository,
|
||||
) {
|
||||
|
||||
/** Archives the specified account by its [accountId] */
|
||||
suspend operator fun invoke(accountId: AccountId): Either<Error, Unit> = either {
|
||||
val accountList = getAccountList(userWalletId = accountId.userWalletId)
|
||||
|
||||
val archivingAccount = accountList.accounts
|
||||
.firstOrNull { it.accountId == accountId }
|
||||
?: raise(Error.CriticalTechError.AccountNotFound(accountId = accountId))
|
||||
|
||||
val updatedAccounts = (accountList - archivingAccount).getOrElse {
|
||||
raise(Error.CriticalTechError.AccountListRequirementsNotMet(cause = it))
|
||||
}
|
||||
|
||||
saveAccounts(updatedAccounts)
|
||||
}
|
||||
|
||||
private suspend fun Raise<Error>.getAccountList(userWalletId: UserWalletId): AccountList {
|
||||
return catch(
|
||||
block = { crudRepository.getAccounts(userWalletId = userWalletId) },
|
||||
catch = { raise(Error.DataOperationFailed(cause = it)) },
|
||||
)
|
||||
.getOrElse { raise(Error.CriticalTechError.AccountsNotCreated(userWalletId = userWalletId)) }
|
||||
}
|
||||
|
||||
private suspend fun Raise<Error>.saveAccounts(accountList: AccountList) {
|
||||
catch(
|
||||
block = { crudRepository.saveAccounts(accountList) },
|
||||
catch = { raise(Error.DataOperationFailed(cause = it)) },
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents possible errors that can occur during the archiving process
|
||||
*/
|
||||
sealed interface Error {
|
||||
|
||||
/** Error indicating that a data operation failed */
|
||||
data class DataOperationFailed(val cause: Throwable) : Error {
|
||||
override fun toString(): String = "$this: Data operation failed: ${cause.message ?: "Unknown error"}"
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents critical technical errors that can occur during the update operation.
|
||||
* These errors are a consequence of an inconsistent state.
|
||||
*/
|
||||
sealed interface CriticalTechError : Error {
|
||||
|
||||
/**
|
||||
|
||||
*
|
||||
* @property userWalletId the unique identifier of the user wallet
|
||||
*/
|
||||
data class AccountsNotCreated(val userWalletId: UserWalletId) : CriticalTechError {
|
||||
|
||||
override fun toString(): String {
|
||||
return "${this.javaClass.simpleName}: Accounts for $userWalletId are not created"
|
||||
}
|
||||
}
|
||||
|
||||
/** Error indicating that the account with [accountId] was not found */
|
||||
data class AccountNotFound(val accountId: AccountId) : CriticalTechError {
|
||||
override fun toString(): String = "${this.javaClass.simpleName}: Account with ID $accountId not found"
|
||||
}
|
||||
|
||||
/**
|
||||
* Error indicating that the account list requirements were not met.
|
||||
*
|
||||
* @property cause the underlying cause of the error
|
||||
*/
|
||||
data class AccountListRequirementsNotMet(val cause: AccountList.Error) : CriticalTechError {
|
||||
|
||||
override fun toString(): String {
|
||||
return "${this.javaClass.simpleName}: Account list requirements not met: $cause"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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"}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
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.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
|
||||
|
||||
/**
|
||||
* Use case for recovering a crypto portfolio account from archived accounts
|
||||
*
|
||||
* @property crudRepository repository for performing CRUD operations on accounts
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class RecoverCryptoPortfolioUseCase(
|
||||
private val crudRepository: AccountsCRUDRepository,
|
||||
) {
|
||||
|
||||
/**
|
||||
* Recovers a crypto portfolio account by moving it from archived accounts to active accounts
|
||||
*
|
||||
* @param accountId the unique identifier of the account to recover
|
||||
*/
|
||||
suspend operator fun invoke(accountId: AccountId): Either<Error, Account.CryptoPortfolio> = either {
|
||||
val accountList = getAccountList(userWalletId = accountId.userWalletId)
|
||||
val archivedAccount = getArchivedAccount(accountId = accountId)
|
||||
|
||||
val recoveredAccount = archivedAccount.recover()
|
||||
|
||||
val updatedAccountList = (accountList + recoveredAccount)
|
||||
.getOrElse { raise(Error.CriticalTechError.AccountListRequirementsNotMet(cause = it)) }
|
||||
|
||||
saveAccounts(updatedAccountList)
|
||||
|
||||
recoveredAccount
|
||||
}
|
||||
|
||||
private suspend fun Raise<Error>.getAccountList(userWalletId: UserWalletId): AccountList {
|
||||
return catch(
|
||||
block = { crudRepository.getAccounts(userWalletId = userWalletId) },
|
||||
catch = { raise(Error.DataOperationFailed(cause = it)) },
|
||||
)
|
||||
.getOrElse { raise(Error.CriticalTechError.AccountsNotCreated(userWalletId = userWalletId)) }
|
||||
}
|
||||
|
||||
private suspend fun Raise<Error>.getArchivedAccount(accountId: AccountId): ArchivedAccount {
|
||||
return catch(
|
||||
block = { crudRepository.getArchivedAccount(accountId = accountId) },
|
||||
catch = { raise(Error.DataOperationFailed(cause = it)) },
|
||||
)
|
||||
.getOrElse {
|
||||
raise(Error.CriticalTechError.AccountNotFound(accountId = accountId))
|
||||
}
|
||||
}
|
||||
|
||||
private fun ArchivedAccount.recover(): Account.CryptoPortfolio {
|
||||
return Account.CryptoPortfolio(
|
||||
accountId = this.accountId,
|
||||
accountName = this.name,
|
||||
accountIcon = this.icon,
|
||||
derivationIndex = this.derivationIndex,
|
||||
isArchived = false,
|
||||
cryptoCurrencyList = Account.CryptoPortfolio.CryptoCurrencyList(
|
||||
currencies = emptySet(),
|
||||
sortType = TokensSortType.NONE,
|
||||
groupType = TokensGroupType.NONE,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun Raise<Error>.saveAccounts(accountList: AccountList) {
|
||||
catch(
|
||||
block = { crudRepository.saveAccounts(accountList) },
|
||||
catch = { raise(Error.DataOperationFailed(cause = it)) },
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents possible errors that can occur during the add operation
|
||||
*/
|
||||
sealed interface Error {
|
||||
|
||||
val tag: String
|
||||
get() = this::class.simpleName ?: "RecoverCryptoPortfolioUseCase.Error"
|
||||
|
||||
/**
|
||||
* Critical technical errors that can occur during the recovery operation
|
||||
*/
|
||||
sealed interface CriticalTechError : Error {
|
||||
|
||||
/**
|
||||
|
||||
*
|
||||
* @property userWalletId the unique identifier of the user wallet
|
||||
*/
|
||||
data class AccountsNotCreated(val userWalletId: UserWalletId) : CriticalTechError {
|
||||
override fun toString(): String = "$tag: Accounts for $userWalletId are not created"
|
||||
}
|
||||
|
||||
/** Error indicating that the account with [accountId] was not found */
|
||||
data class AccountNotFound(val accountId: AccountId) : CriticalTechError {
|
||||
override fun toString(): String = "$tag: Account with ID $accountId not found"
|
||||
}
|
||||
|
||||
/**
|
||||
* Error indicating that the account list requirements were not met.
|
||||
*
|
||||
* @property cause the underlying cause of the error
|
||||
*/
|
||||
data class AccountListRequirementsNotMet(val cause: AccountList.Error) : Error {
|
||||
override fun toString(): String = "$tag: Account list requirements not met: $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"}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
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 arrow.core.raise.ensure
|
||||
import com.tangem.domain.account.models.AccountList
|
||||
import com.tangem.domain.account.repository.AccountsCRUDRepository
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.account.AccountId
|
||||
import com.tangem.domain.models.account.AccountName
|
||||
import com.tangem.domain.models.account.CryptoPortfolioIcon
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
||||
/**
|
||||
* Use case for updating a crypto portfolio account.
|
||||
*
|
||||
* @property crudRepository the repository used for performing CRUD operations on accounts
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class UpdateCryptoPortfolioUseCase(
|
||||
private val crudRepository: AccountsCRUDRepository,
|
||||
) {
|
||||
|
||||
/**
|
||||
* Updates a crypto portfolio account with the provided name and/or icon
|
||||
*
|
||||
* @param accountId the unique identifier of the account to update
|
||||
* @param accountName the new name for the account (optional)
|
||||
* @param icon the new icon for the account (optional)
|
||||
* @return an [Either] containing the updated [Account.CryptoPortfolio] on success, or an [Error] on failure
|
||||
*/
|
||||
suspend operator fun invoke(
|
||||
accountId: AccountId,
|
||||
accountName: AccountName? = null,
|
||||
icon: CryptoPortfolioIcon? = null,
|
||||
): Either<Error, Account.CryptoPortfolio> = either {
|
||||
ensure(accountName != null || icon != null) { Error.NothingToUpdate }
|
||||
|
||||
val accountList = getAccountList(userWalletId = accountId.userWalletId)
|
||||
|
||||
val account = accountList.accounts
|
||||
.firstOrNull { it.accountId == accountId } as? Account.CryptoPortfolio
|
||||
?: raise(Error.CriticalTechError.AccountNotFound(accountId = accountId))
|
||||
|
||||
val updatedAccount = account
|
||||
.setName(name = accountName)
|
||||
.setIcon(icon = icon)
|
||||
|
||||
val updatedAccounts = (accountList + updatedAccount).getOrElse {
|
||||
raise(Error.CriticalTechError.AccountListRequirementsNotMet(it))
|
||||
}
|
||||
|
||||
saveAccounts(updatedAccounts)
|
||||
|
||||
updatedAccount
|
||||
}
|
||||
|
||||
private suspend fun Raise<Error>.getAccountList(userWalletId: UserWalletId): AccountList {
|
||||
return catch(
|
||||
block = { crudRepository.getAccounts(userWalletId = userWalletId) },
|
||||
catch = { raise(Error.DataOperationFailed(cause = it)) },
|
||||
)
|
||||
.getOrElse { raise(Error.CriticalTechError.AccountsNotCreated(userWalletId = userWalletId)) }
|
||||
}
|
||||
|
||||
private suspend fun Raise<Error>.saveAccounts(accountList: AccountList) {
|
||||
catch(
|
||||
block = { crudRepository.saveAccounts(accountList) },
|
||||
catch = { raise(Error.DataOperationFailed(cause = it)) },
|
||||
)
|
||||
}
|
||||
|
||||
private fun Account.CryptoPortfolio.setName(name: AccountName?): Account.CryptoPortfolio {
|
||||
return if (name != null) this.copy(accountName = name) else this
|
||||
}
|
||||
|
||||
private fun Account.CryptoPortfolio.setIcon(icon: CryptoPortfolioIcon?): Account.CryptoPortfolio {
|
||||
return if (icon != null) this.copy(accountIcon = icon) else this
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents possible errors that can occur during the update operation
|
||||
*/
|
||||
sealed interface Error {
|
||||
|
||||
/** Error indicating that there is nothing to update */
|
||||
data object NothingToUpdate : Error {
|
||||
override fun toString(): String = "Nothing to update: both account name and icon are null"
|
||||
}
|
||||
|
||||
/** Error indicating that a data operation failed */
|
||||
data class DataOperationFailed(val cause: Throwable) : Error {
|
||||
override fun toString(): String = "Data operation failed: ${cause.message ?: "Unknown error"}"
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents critical technical errors that can occur during the update operation.
|
||||
* These errors are a consequence of an inconsistent state.
|
||||
*/
|
||||
sealed interface CriticalTechError : Error {
|
||||
|
||||
/**
|
||||
|
||||
*
|
||||
* @property userWalletId the unique identifier of the user wallet
|
||||
*/
|
||||
data class AccountsNotCreated(val userWalletId: UserWalletId) : CriticalTechError {
|
||||
override fun toString(): String = "Accounts for $userWalletId are not created"
|
||||
}
|
||||
|
||||
/** Error indicating that the account with [accountId] was not found */
|
||||
data class AccountNotFound(val accountId: AccountId) : CriticalTechError {
|
||||
override fun toString(): String = "Account with ID $accountId not found"
|
||||
}
|
||||
|
||||
/**
|
||||
* Error indicating that the account list requirements were not met.
|
||||
*
|
||||
* @property cause the underlying cause of the error
|
||||
*/
|
||||
data class AccountListRequirementsNotMet(val cause: AccountList.Error) : CriticalTechError {
|
||||
override fun toString(): String = "Account list requirements not met: $cause"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,346 @@
|
|||
package com.tangem.domain.account.models
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.left
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.domain.account.utils.createAccount
|
||||
import com.tangem.domain.account.utils.createAccounts
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.account.AccountName
|
||||
import com.tangem.domain.models.account.CryptoPortfolioIcon
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import io.mockk.clearMocks
|
||||
import io.mockk.mockk
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Nested
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
import org.junit.jupiter.params.provider.MethodSource
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
class AccountListTest {
|
||||
|
||||
@Test
|
||||
fun mainAccount() {
|
||||
// Arrange
|
||||
val mainAccount = Account.CryptoPortfolio.createMainAccount(userWalletId = userWalletId)
|
||||
|
||||
val accountList = AccountList(
|
||||
userWallet = mockk(),
|
||||
accounts = setOf(mainAccount),
|
||||
totalAccounts = 1,
|
||||
)
|
||||
.getOrNull()!!
|
||||
|
||||
// Act
|
||||
val actual = accountList.mainAccount
|
||||
|
||||
// Assert
|
||||
val expected = mainAccount
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun canAddMoreAccounts() {
|
||||
// Arrange
|
||||
val accountList = AccountList(
|
||||
userWallet = mockk(),
|
||||
accounts = createAccounts(userWalletId = userWalletId, count = 2),
|
||||
totalAccounts = 2,
|
||||
).getOrNull()!!
|
||||
|
||||
val fullAccountList = AccountList(
|
||||
userWallet = mockk(),
|
||||
accounts = createAccounts(userWalletId = userWalletId, count = 20),
|
||||
totalAccounts = 20,
|
||||
).getOrNull()!!
|
||||
|
||||
// Act & Assert
|
||||
Truth.assertThat(accountList.canAddMoreAccounts).isTrue()
|
||||
Truth.assertThat(fullAccountList.canAddMoreAccounts).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun empty() {
|
||||
// Arrange
|
||||
val userWallet = mockk<UserWallet>(relaxed = true)
|
||||
|
||||
// Act
|
||||
val actual = AccountList.empty(userWallet)
|
||||
|
||||
// Assert
|
||||
val expected = AccountList(
|
||||
userWallet = userWallet,
|
||||
accounts = setOf(Account.CryptoPortfolio.createMainAccount(userWalletId = userWallet.walletId)),
|
||||
totalAccounts = 1,
|
||||
).getOrNull()!!
|
||||
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class Create {
|
||||
|
||||
private val userWallet = mockk<UserWallet>()
|
||||
|
||||
@BeforeEach
|
||||
fun resetMocks() {
|
||||
clearMocks(userWallet)
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("provideTestModels")
|
||||
fun invoke(model: CreateTestModel) {
|
||||
// Act
|
||||
val actual = AccountList(
|
||||
userWallet = userWallet,
|
||||
accounts = model.accounts,
|
||||
totalAccounts = model.accounts.size,
|
||||
)
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).isEqualTo(model.expected)
|
||||
}
|
||||
|
||||
private fun provideTestModels() = listOf(
|
||||
CreateTestModel(
|
||||
accounts = emptySet(),
|
||||
expected = AccountList.Error.EmptyAccountsList.left(),
|
||||
),
|
||||
CreateTestModel(
|
||||
accounts = setOf(
|
||||
createAccount(userWalletId = userWalletId, derivationIndex = 1),
|
||||
),
|
||||
expected = AccountList.Error.MainAccountNotFound.left(),
|
||||
),
|
||||
CreateTestModel(
|
||||
accounts = setOf(
|
||||
Account.CryptoPortfolio.createMainAccount(userWalletId),
|
||||
Account.CryptoPortfolio.createMainAccount(userWalletId).copy(
|
||||
accountIcon = CryptoPortfolioIcon.ofDefaultCustomAccount(),
|
||||
),
|
||||
),
|
||||
expected = AccountList.Error.ExceedsMaxMainAccountsCount.left(),
|
||||
),
|
||||
createAccounts(userWalletId = userWalletId, count = 1).let {
|
||||
CreateTestModel(
|
||||
accounts = it,
|
||||
expected = AccountList(userWallet = userWallet, accounts = it, totalAccounts = 1),
|
||||
)
|
||||
},
|
||||
createAccounts(userWalletId = userWalletId, count = 20).let {
|
||||
CreateTestModel(
|
||||
accounts = it,
|
||||
expected = AccountList(userWallet = userWallet, accounts = it, totalAccounts = 20),
|
||||
)
|
||||
},
|
||||
CreateTestModel(
|
||||
accounts = createAccounts(userWalletId = userWalletId, count = 21),
|
||||
expected = AccountList.Error.ExceedsMaxAccountsCount.left(),
|
||||
),
|
||||
CreateTestModel(
|
||||
accounts = setOf(
|
||||
createAccount(userWalletId = userWalletId, derivationIndex = 0),
|
||||
createAccount(userWalletId = userWalletId, derivationIndex = 1),
|
||||
createAccount(userWalletId = userWalletId, derivationIndex = 1),
|
||||
),
|
||||
expected = AccountList.Error.DuplicateAccountIds.left(),
|
||||
),
|
||||
CreateTestModel(
|
||||
accounts = setOf(
|
||||
createAccount(userWalletId = userWalletId, name = "Name", derivationIndex = 0),
|
||||
createAccount(userWalletId = userWalletId, name = "Name", derivationIndex = 1),
|
||||
),
|
||||
expected = AccountList.Error.DuplicateAccountNames.left(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
data class CreateTestModel(
|
||||
val accounts: Set<Account>,
|
||||
val expected: Either<AccountList.Error, AccountList>,
|
||||
)
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class Plus {
|
||||
|
||||
private val userWallet = mockk<UserWallet>()
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("provideTestModels")
|
||||
fun invoke(model: PlusTestModel) {
|
||||
// Act
|
||||
val actual = model.initial.plus(other = model.toAdd)
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).isEqualTo(model.expected)
|
||||
}
|
||||
|
||||
private fun provideTestModels() = listOf(
|
||||
// region Add new account
|
||||
run {
|
||||
val mainAccount = Account.CryptoPortfolio.createMainAccount(userWalletId)
|
||||
val newAccount = createAccount(userWalletId = userWalletId, derivationIndex = 1)
|
||||
|
||||
PlusTestModel(
|
||||
initial = AccountList(
|
||||
userWallet = userWallet,
|
||||
accounts = setOf(mainAccount),
|
||||
totalAccounts = 1,
|
||||
).getOrNull()!!,
|
||||
toAdd = newAccount,
|
||||
expected = AccountList(
|
||||
userWallet = userWallet,
|
||||
accounts = setOf(mainAccount, newAccount),
|
||||
totalAccounts = 2,
|
||||
),
|
||||
)
|
||||
},
|
||||
// endregion
|
||||
// region Replace existing account
|
||||
run {
|
||||
val mainAccount = Account.CryptoPortfolio.createMainAccount(userWalletId)
|
||||
val newAccount = mainAccount.copy(accountName = AccountName("New Name").getOrNull()!!)
|
||||
|
||||
PlusTestModel(
|
||||
initial = AccountList(
|
||||
userWallet = userWallet,
|
||||
accounts = setOf(mainAccount),
|
||||
totalAccounts = 1,
|
||||
).getOrNull()!!,
|
||||
toAdd = newAccount,
|
||||
expected = AccountList(
|
||||
userWallet = userWallet,
|
||||
accounts = setOf(newAccount),
|
||||
totalAccounts = 1,
|
||||
),
|
||||
)
|
||||
},
|
||||
// endregion
|
||||
PlusTestModel(
|
||||
initial = AccountList(
|
||||
userWallet = userWallet,
|
||||
accounts = createAccounts(userWalletId = userWalletId, count = 20),
|
||||
totalAccounts = 20,
|
||||
).getOrNull()!!,
|
||||
toAdd = createAccount(userWalletId = userWalletId, derivationIndex = 21),
|
||||
expected = AccountList.Error.ExceedsMaxAccountsCount.left(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
data class PlusTestModel(
|
||||
val initial: AccountList,
|
||||
val toAdd: Account,
|
||||
val expected: Either<AccountList.Error, AccountList>,
|
||||
)
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class Minus {
|
||||
|
||||
private val userWallet = mockk<UserWallet>()
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("provideTestModels")
|
||||
fun invoke(model: MinusTestModel) {
|
||||
// Act
|
||||
val actual = model.initial.minus(model.toRemove)
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).isEqualTo(model.expected)
|
||||
}
|
||||
|
||||
private fun provideTestModels() = listOf(
|
||||
// region Remove existing account
|
||||
run {
|
||||
val mainAccount = Account.CryptoPortfolio.createMainAccount(userWalletId)
|
||||
val secondaryAccount = createAccount(userWalletId = userWalletId, derivationIndex = 2)
|
||||
|
||||
MinusTestModel(
|
||||
initial = AccountList(
|
||||
userWallet = userWallet,
|
||||
accounts = setOf(mainAccount, secondaryAccount),
|
||||
totalAccounts = 2,
|
||||
).getOrNull()!!,
|
||||
toRemove = secondaryAccount,
|
||||
expected = AccountList(
|
||||
userWallet = userWallet,
|
||||
accounts = setOf(mainAccount),
|
||||
totalAccounts = 1,
|
||||
),
|
||||
)
|
||||
},
|
||||
// endregion
|
||||
// region Remove unexisting account
|
||||
run {
|
||||
val mainAccount = Account.CryptoPortfolio.createMainAccount(userWalletId)
|
||||
val notInList = createAccount(userWalletId = userWalletId, derivationIndex = 3)
|
||||
|
||||
MinusTestModel(
|
||||
initial = AccountList(
|
||||
userWallet = userWallet,
|
||||
accounts = setOf(mainAccount),
|
||||
totalAccounts = 1,
|
||||
).getOrNull()!!,
|
||||
toRemove = notInList,
|
||||
expected = AccountList(
|
||||
userWallet = userWallet,
|
||||
accounts = setOf(mainAccount),
|
||||
totalAccounts = 1,
|
||||
),
|
||||
)
|
||||
},
|
||||
// endregion
|
||||
// region EmptyAccountsList
|
||||
run {
|
||||
val mainAccount = Account.CryptoPortfolio.createMainAccount(userWalletId)
|
||||
|
||||
MinusTestModel(
|
||||
initial = AccountList(
|
||||
userWallet = userWallet,
|
||||
accounts = setOf(mainAccount),
|
||||
totalAccounts = 1,
|
||||
).getOrNull()!!,
|
||||
toRemove = mainAccount,
|
||||
expected = AccountList.Error.EmptyAccountsList.left(),
|
||||
)
|
||||
},
|
||||
// endregion
|
||||
// region MainAccountNotFound
|
||||
run {
|
||||
val mainAccount = Account.CryptoPortfolio.createMainAccount(userWalletId)
|
||||
val secondaryAccount = createAccount(userWalletId = userWalletId, derivationIndex = 2)
|
||||
|
||||
MinusTestModel(
|
||||
initial = AccountList(
|
||||
userWallet = userWallet,
|
||||
accounts = setOf(mainAccount, secondaryAccount),
|
||||
totalAccounts = 2,
|
||||
).getOrNull()!!,
|
||||
toRemove = mainAccount,
|
||||
expected = AccountList.Error.MainAccountNotFound.left(),
|
||||
)
|
||||
},
|
||||
// endregion
|
||||
)
|
||||
}
|
||||
|
||||
data class MinusTestModel(
|
||||
val initial: AccountList,
|
||||
val toRemove: Account,
|
||||
val expected: Either<AccountList.Error, AccountList>,
|
||||
)
|
||||
|
||||
private companion object {
|
||||
|
||||
val userWalletId = UserWalletId("011")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,203 @@
|
|||
package com.tangem.domain.account.usecase
|
||||
|
||||
import arrow.core.None
|
||||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
import arrow.core.toOption
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.domain.account.models.AccountList
|
||||
import com.tangem.domain.account.repository.AccountsCRUDRepository
|
||||
import com.tangem.domain.account.utils.createAccount
|
||||
import com.tangem.domain.account.utils.createAccounts
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.account.CryptoPortfolioIcon
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import io.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 AddCryptoPortfolioUseCaseTest {
|
||||
|
||||
private val crudRepository: AccountsCRUDRepository = mockk(relaxUnitFun = true)
|
||||
private val useCase = AddCryptoPortfolioUseCase(crudRepository)
|
||||
|
||||
private val userWallet = mockk<UserWallet>()
|
||||
|
||||
@BeforeEach
|
||||
fun resetMocks() {
|
||||
clearMocks(crudRepository, userWallet)
|
||||
|
||||
every { userWallet.walletId } returns userWalletId
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke should add new crypto portfolio account to existing list`() = runTest {
|
||||
// Arrange
|
||||
val newAccount = createNewAccount()
|
||||
val accountList = AccountList.empty(userWallet)
|
||||
val updatedAccountList = (accountList + newAccount).getOrNull()!!
|
||||
|
||||
coEvery { crudRepository.getAccounts(userWalletId) } returns accountList.toOption()
|
||||
|
||||
// Act
|
||||
val actual = useCase(
|
||||
userWalletId = userWalletId,
|
||||
accountName = newAccount.name,
|
||||
icon = newAccount.icon,
|
||||
derivationIndex = newAccount.derivationIndex,
|
||||
)
|
||||
|
||||
// Assert
|
||||
val expected = newAccount.right()
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
|
||||
coVerifyOrder {
|
||||
crudRepository.getAccounts(userWalletId)
|
||||
crudRepository.saveAccounts(updatedAccountList)
|
||||
}
|
||||
|
||||
coVerify(inverse = true) { crudRepository.getUserWallet(userWalletId) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke should create new account list if none exists`() = runTest {
|
||||
// Arrange
|
||||
val newAccount = createNewAccount()
|
||||
val newAccountList = (AccountList.empty(userWallet) + newAccount).getOrNull()!!
|
||||
|
||||
coEvery { crudRepository.getAccounts(userWalletId) } returns None
|
||||
coEvery { crudRepository.getUserWallet(userWalletId) } returns userWallet
|
||||
|
||||
// Act
|
||||
val actual = useCase(
|
||||
userWalletId = userWalletId,
|
||||
accountName = newAccount.name,
|
||||
icon = newAccount.icon,
|
||||
derivationIndex = newAccount.derivationIndex,
|
||||
)
|
||||
|
||||
// Assert
|
||||
val expected = newAccount.right()
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
|
||||
coVerifyOrder {
|
||||
crudRepository.getAccounts(userWalletId)
|
||||
crudRepository.getUserWallet(userWalletId)
|
||||
crudRepository.saveAccounts(newAccountList)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke should return error if account list requirements not met`() = runTest {
|
||||
// Arrange
|
||||
val accountList = AccountList(
|
||||
userWallet = userWallet,
|
||||
accounts = createAccounts(userWalletId = userWalletId, count = 20),
|
||||
totalAccounts = 20,
|
||||
).getOrNull()!!
|
||||
|
||||
val newAccount = createNewAccount(derivationIndex = 21)
|
||||
|
||||
coEvery { crudRepository.getAccounts(userWalletId) } returns accountList.toOption()
|
||||
|
||||
// Act
|
||||
val actual = useCase(
|
||||
userWalletId = userWalletId,
|
||||
accountName = newAccount.name,
|
||||
icon = newAccount.icon,
|
||||
derivationIndex = newAccount.derivationIndex,
|
||||
)
|
||||
|
||||
// Assert
|
||||
val expected = AddCryptoPortfolioUseCase.Error.AccountListRequirementsNotMet(
|
||||
cause = AccountList.Error.ExceedsMaxAccountsCount,
|
||||
).left()
|
||||
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
|
||||
coVerifyOrder { crudRepository.getAccounts(userWalletId) }
|
||||
|
||||
coVerify(inverse = true) {
|
||||
crudRepository.getUserWallet(any())
|
||||
crudRepository.saveAccounts(any())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke should return error if getAccounts throws exception`() = runTest {
|
||||
// Arrange
|
||||
val newAccount = createNewAccount()
|
||||
val exception = IllegalStateException("Test error")
|
||||
|
||||
coEvery { crudRepository.getAccounts(userWalletId) } throws exception
|
||||
|
||||
// Act
|
||||
val actual = useCase(
|
||||
userWalletId = userWalletId,
|
||||
accountName = newAccount.name,
|
||||
icon = newAccount.icon,
|
||||
derivationIndex = newAccount.derivationIndex,
|
||||
)
|
||||
|
||||
// Assert
|
||||
val expected = AddCryptoPortfolioUseCase.Error.DataOperationFailed(cause = exception).left()
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
|
||||
coVerifyOrder { crudRepository.getAccounts(userWalletId) }
|
||||
|
||||
coVerify(inverse = true) {
|
||||
crudRepository.getUserWallet(any())
|
||||
crudRepository.saveAccounts(any())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke should return error if saveAccounts throws exception`() = runTest {
|
||||
// Arrange
|
||||
val newAccount = createNewAccount()
|
||||
val accountList = AccountList.empty(userWallet)
|
||||
val updatedAccountList = (accountList + newAccount).getOrNull()!!
|
||||
|
||||
val exception = IllegalStateException("Test error")
|
||||
|
||||
coEvery { crudRepository.getAccounts(userWalletId) } returns accountList.toOption()
|
||||
coEvery { crudRepository.saveAccounts(updatedAccountList) } throws exception
|
||||
|
||||
// Act
|
||||
val actual = useCase(
|
||||
userWalletId = userWalletId,
|
||||
accountName = newAccount.name,
|
||||
icon = newAccount.icon,
|
||||
derivationIndex = newAccount.derivationIndex,
|
||||
)
|
||||
|
||||
// Assert
|
||||
val expected = AddCryptoPortfolioUseCase.Error.DataOperationFailed(cause = exception).left()
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
|
||||
coVerifyOrder {
|
||||
crudRepository.getAccounts(userWalletId)
|
||||
crudRepository.saveAccounts(updatedAccountList)
|
||||
}
|
||||
|
||||
coVerify(inverse = true) { crudRepository.getUserWallet(userWalletId) }
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
||||
val userWalletId = UserWalletId("011")
|
||||
|
||||
fun createNewAccount(derivationIndex: Int = 1): Account.CryptoPortfolio {
|
||||
return createAccount(
|
||||
userWalletId = userWalletId,
|
||||
name = "New Account",
|
||||
icon = CryptoPortfolioIcon.ofDefaultCustomAccount(),
|
||||
derivationIndex = derivationIndex,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,157 @@
|
|||
package com.tangem.domain.account.usecase
|
||||
|
||||
import arrow.core.None
|
||||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
import arrow.core.toOption
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.domain.account.models.AccountList
|
||||
import com.tangem.domain.account.repository.AccountsCRUDRepository
|
||||
import com.tangem.domain.account.usecase.ArchiveCryptoPortfolioUseCase.Error
|
||||
import com.tangem.domain.account.utils.createAccount
|
||||
import com.tangem.domain.models.account.AccountId
|
||||
import com.tangem.domain.models.account.DerivationIndex
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import io.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 ArchiveCryptoPortfolioUseCaseTest {
|
||||
|
||||
private val crudRepository: AccountsCRUDRepository = mockk(relaxUnitFun = true)
|
||||
private val useCase = ArchiveCryptoPortfolioUseCase(crudRepository)
|
||||
private val userWallet = mockk<UserWallet>()
|
||||
|
||||
@BeforeEach
|
||||
fun resetMocks() {
|
||||
clearMocks(crudRepository, userWallet)
|
||||
every { userWallet.walletId } returns userWalletId
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke should archive existing crypto portfolio account`() = runTest {
|
||||
// Arrange
|
||||
val account = createAccount(userWalletId)
|
||||
val accountList = (AccountList.empty(userWallet) + account).getOrNull()!!
|
||||
val accountId = account.accountId
|
||||
|
||||
val archivedAccount = account.copy(isArchived = true)
|
||||
val updatedAccountList = (accountList - archivedAccount).getOrNull()!!
|
||||
|
||||
coEvery { crudRepository.getAccounts(userWalletId) } returns accountList.toOption()
|
||||
|
||||
// Act
|
||||
val actual = useCase(accountId)
|
||||
|
||||
// Assert
|
||||
val expected = Unit.right()
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
|
||||
coVerifyOrder {
|
||||
crudRepository.getAccounts(userWalletId)
|
||||
crudRepository.saveAccounts(updatedAccountList)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke should return error if getAccounts returns None`() = runTest {
|
||||
// Arrange
|
||||
val accountId = AccountId.forCryptoPortfolio(
|
||||
userWalletId = userWalletId,
|
||||
derivationIndex = DerivationIndex.Main,
|
||||
)
|
||||
|
||||
coEvery { crudRepository.getAccounts(userWalletId) } returns None
|
||||
|
||||
// Act
|
||||
val actual = useCase(accountId)
|
||||
|
||||
// Assert
|
||||
val expected = Error.CriticalTechError.AccountsNotCreated(userWalletId).left()
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
|
||||
coVerifyOrder { crudRepository.getAccounts(userWalletId) }
|
||||
coVerify(inverse = true) { crudRepository.saveAccounts(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke should return error if getAccounts throws exception`() = runTest {
|
||||
// Arrange
|
||||
val accountId = AccountId.forCryptoPortfolio(
|
||||
userWalletId = userWalletId,
|
||||
derivationIndex = DerivationIndex.Main,
|
||||
)
|
||||
|
||||
val exception = IllegalStateException("Test error")
|
||||
|
||||
coEvery { crudRepository.getAccounts(userWalletId) } throws exception
|
||||
|
||||
// Act
|
||||
val actual = useCase(accountId)
|
||||
|
||||
// Assert
|
||||
val expected = Error.DataOperationFailed(exception).left()
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
|
||||
coVerifyOrder { crudRepository.getAccounts(userWalletId) }
|
||||
coVerify(inverse = true) { crudRepository.saveAccounts(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke should return error if account not found`() = runTest {
|
||||
// Arrange
|
||||
val accountList = AccountList.empty(userWallet)
|
||||
val accountId = AccountId.forCryptoPortfolio(
|
||||
userWalletId = userWalletId,
|
||||
derivationIndex = DerivationIndex(1).getOrNull()!!,
|
||||
)
|
||||
|
||||
coEvery { crudRepository.getAccounts(userWalletId) } returns accountList.toOption()
|
||||
|
||||
// Act
|
||||
val actual = useCase(accountId)
|
||||
|
||||
// Assert
|
||||
val expected = Error.CriticalTechError.AccountNotFound(accountId).left()
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
|
||||
coVerifyOrder { crudRepository.getAccounts(userWalletId) }
|
||||
coVerify(inverse = true) { crudRepository.saveAccounts(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke should return error if saveAccounts throws exception`() = runTest {
|
||||
// Arrange
|
||||
val account = createAccount(userWalletId)
|
||||
val accountList = (AccountList.empty(userWallet) + account).getOrNull()!!
|
||||
val accountId = account.accountId
|
||||
|
||||
val archivedAccount = account.copy(isArchived = true)
|
||||
val updatedAccountList = (accountList - archivedAccount).getOrNull()!!
|
||||
|
||||
val exception = IllegalStateException("Save failed")
|
||||
|
||||
coEvery { crudRepository.getAccounts(userWalletId) } returns accountList.toOption()
|
||||
coEvery { crudRepository.saveAccounts(updatedAccountList) } throws exception
|
||||
|
||||
// Act
|
||||
val actual = useCase(accountId)
|
||||
|
||||
// Assert
|
||||
val expected = Error.DataOperationFailed(exception).left()
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
|
||||
coVerifyOrder {
|
||||
crudRepository.getAccounts(userWalletId)
|
||||
crudRepository.saveAccounts(updatedAccountList)
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
val userWalletId = UserWalletId("011")
|
||||
}
|
||||
}
|
||||
|
|
@ -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 arrow.core.right
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.domain.account.repository.AccountsCRUDRepository
|
||||
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 = 4.right()
|
||||
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) }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,208 @@
|
|||
package com.tangem.domain.account.usecase
|
||||
|
||||
import arrow.core.None
|
||||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
import arrow.core.toOption
|
||||
import com.google.common.truth.Truth
|
||||
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.account.usecase.RecoverCryptoPortfolioUseCase.Error
|
||||
import com.tangem.domain.account.utils.createAccount
|
||||
import com.tangem.domain.models.account.AccountId
|
||||
import com.tangem.domain.models.account.DerivationIndex
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import io.mockk.*
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
class RecoverCryptoPortfolioUseCaseTest {
|
||||
|
||||
private val crudRepository: AccountsCRUDRepository = mockk(relaxUnitFun = true)
|
||||
private val useCase = RecoverCryptoPortfolioUseCase(crudRepository)
|
||||
private val userWallet = mockk<UserWallet>()
|
||||
|
||||
@BeforeEach
|
||||
fun resetMocks() {
|
||||
clearMocks(crudRepository, userWallet)
|
||||
every { userWallet.walletId } returns userWalletId
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke should recover archived crypto portfolio account`() = runTest {
|
||||
// Arrange
|
||||
val account = createAccount(userWalletId)
|
||||
val accountList = AccountList.empty(userWallet)
|
||||
val archivedAccount = ArchivedAccount(
|
||||
accountId = account.accountId,
|
||||
name = account.name,
|
||||
icon = account.icon,
|
||||
derivationIndex = account.derivationIndex,
|
||||
tokensCount = 1,
|
||||
networksCount = 1,
|
||||
)
|
||||
|
||||
val recoveredAccount = account.copy(isArchived = false)
|
||||
val updatedAccountList = (accountList + recoveredAccount).getOrNull()!!
|
||||
|
||||
coEvery { crudRepository.getAccounts(userWalletId) } returns accountList.toOption()
|
||||
coEvery { crudRepository.getArchivedAccount(account.accountId) } returns archivedAccount.toOption()
|
||||
|
||||
// Act
|
||||
val actual = useCase(account.accountId)
|
||||
|
||||
// Assert
|
||||
val expected = recoveredAccount.right()
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
|
||||
coVerifyOrder {
|
||||
crudRepository.getAccounts(userWalletId)
|
||||
crudRepository.getArchivedAccount(account.accountId)
|
||||
crudRepository.saveAccounts(updatedAccountList)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke should return error if getAccounts returns None`() = runTest {
|
||||
// Arrange
|
||||
val accountId = AccountId.forCryptoPortfolio(
|
||||
userWalletId = userWalletId,
|
||||
derivationIndex = DerivationIndex.Main,
|
||||
)
|
||||
|
||||
coEvery { crudRepository.getAccounts(userWalletId) } returns None
|
||||
|
||||
// Act
|
||||
val actual = useCase(accountId)
|
||||
|
||||
// Assert
|
||||
val expected = Error.CriticalTechError.AccountsNotCreated(userWalletId).left()
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
|
||||
coVerifyOrder { crudRepository.getAccounts(userWalletId) }
|
||||
coVerify(inverse = true) {
|
||||
crudRepository.getArchivedAccount(any())
|
||||
crudRepository.saveAccounts(any())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke should return error if getAccounts throws exception`() = runTest {
|
||||
// Arrange
|
||||
val accountId = AccountId.forCryptoPortfolio(
|
||||
userWalletId = userWalletId,
|
||||
derivationIndex = DerivationIndex.Main,
|
||||
)
|
||||
val exception = IllegalStateException("Test error")
|
||||
|
||||
coEvery { crudRepository.getAccounts(userWalletId) } throws exception
|
||||
|
||||
// Act
|
||||
val actual = useCase(accountId)
|
||||
|
||||
// Assert
|
||||
val expected = Error.DataOperationFailed(exception).left()
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
|
||||
coVerifyOrder { crudRepository.getAccounts(userWalletId) }
|
||||
coVerify(inverse = true) {
|
||||
crudRepository.getArchivedAccount(any())
|
||||
crudRepository.saveAccounts(any())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke should return error if getArchivedAccount throws exception`() = runTest {
|
||||
// Arrange
|
||||
val account = createAccount(userWalletId)
|
||||
val accountList = AccountList.empty(userWallet)
|
||||
val exception = IllegalStateException("Test error")
|
||||
|
||||
coEvery { crudRepository.getAccounts(userWalletId) } returns accountList.toOption()
|
||||
coEvery { crudRepository.getArchivedAccount(account.accountId) } throws exception
|
||||
|
||||
// Act
|
||||
val actual = useCase(account.accountId)
|
||||
|
||||
// Assert
|
||||
val expected = Error.DataOperationFailed(exception).left()
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
|
||||
coVerifyOrder {
|
||||
crudRepository.getAccounts(userWalletId)
|
||||
crudRepository.getArchivedAccount(account.accountId)
|
||||
}
|
||||
coVerify(inverse = true) { crudRepository.saveAccounts(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke should return error if getArchivedAccount returns null`() = runTest {
|
||||
// Arrange
|
||||
val account = createAccount(userWalletId)
|
||||
val accountList = AccountList.empty(userWallet)
|
||||
|
||||
coEvery { crudRepository.getAccounts(userWalletId) } returns accountList.toOption()
|
||||
coEvery { crudRepository.getArchivedAccount(account.accountId) } returns None
|
||||
|
||||
// Act
|
||||
val actual = useCase(account.accountId)
|
||||
|
||||
// Assert
|
||||
val expected = Error.CriticalTechError.AccountNotFound(account.accountId).left()
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
|
||||
coVerifyOrder {
|
||||
crudRepository.getAccounts(userWalletId)
|
||||
crudRepository.getArchivedAccount(account.accountId)
|
||||
}
|
||||
coVerify(inverse = true) { crudRepository.saveAccounts(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke should return error if saveAccounts throws exception`() = runTest {
|
||||
// Arrange
|
||||
val account = createAccount(userWalletId)
|
||||
val accountList = AccountList.empty(userWallet)
|
||||
val archivedAccount = ArchivedAccount(
|
||||
accountId = account.accountId,
|
||||
name = account.name,
|
||||
icon = account.icon,
|
||||
derivationIndex = account.derivationIndex,
|
||||
tokensCount = 1,
|
||||
networksCount = 1,
|
||||
)
|
||||
|
||||
val recoveredAccount = account.copy(isArchived = false)
|
||||
val updatedAccountList = (accountList + recoveredAccount).getOrNull()!!
|
||||
val exception = IllegalStateException("Save failed")
|
||||
|
||||
coEvery { crudRepository.getAccounts(userWalletId) } returns accountList.toOption()
|
||||
coEvery { crudRepository.getArchivedAccount(account.accountId) } returns archivedAccount.toOption()
|
||||
coEvery { crudRepository.saveAccounts(updatedAccountList) } throws exception
|
||||
|
||||
// Act
|
||||
val actual = useCase(account.accountId)
|
||||
|
||||
// Assert
|
||||
val expected = Error.DataOperationFailed(exception).left()
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
|
||||
coVerifyOrder {
|
||||
crudRepository.getAccounts(userWalletId)
|
||||
crudRepository.getArchivedAccount(account.accountId)
|
||||
crudRepository.saveAccounts(updatedAccountList)
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
val userWalletId = UserWalletId("011")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,247 @@
|
|||
package com.tangem.domain.account.usecase
|
||||
|
||||
import arrow.core.None
|
||||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
import arrow.core.toOption
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.domain.account.models.AccountList
|
||||
import com.tangem.domain.account.repository.AccountsCRUDRepository
|
||||
import com.tangem.domain.account.usecase.UpdateCryptoPortfolioUseCase.Error
|
||||
import com.tangem.domain.models.account.AccountId
|
||||
import com.tangem.domain.models.account.AccountName
|
||||
import com.tangem.domain.models.account.CryptoPortfolioIcon
|
||||
import com.tangem.domain.models.account.DerivationIndex
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import io.mockk.*
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
class UpdateCryptoPortfolioUseCaseTest {
|
||||
|
||||
private val crudRepository: AccountsCRUDRepository = mockk(relaxUnitFun = true)
|
||||
private val useCase = UpdateCryptoPortfolioUseCase(crudRepository = crudRepository)
|
||||
|
||||
private val userWallet = mockk<UserWallet>()
|
||||
|
||||
@BeforeEach
|
||||
fun resetMocks() {
|
||||
clearMocks(crudRepository, userWallet)
|
||||
|
||||
every { userWallet.walletId } returns userWalletId
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke should update crypto portfolio account with new name`() = runTest {
|
||||
// Arrange
|
||||
val accountList = AccountList.empty(userWallet = userWallet)
|
||||
val accountId = accountList.mainAccount.accountId
|
||||
|
||||
val newAccountName = AccountName("New name").getOrNull()!!
|
||||
val updatedAccount = accountList.mainAccount.copy(accountName = newAccountName)
|
||||
val updatedAccountList = (accountList + updatedAccount).getOrNull()!!
|
||||
|
||||
coEvery { crudRepository.getAccounts(userWalletId = userWalletId) } returns accountList.toOption()
|
||||
|
||||
// Act
|
||||
val actual = useCase(accountId = accountId, accountName = newAccountName)
|
||||
|
||||
// Assert
|
||||
val expected = updatedAccount.right()
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
|
||||
coVerifyOrder {
|
||||
crudRepository.getAccounts(userWalletId = userWalletId)
|
||||
crudRepository.saveAccounts(accountList = updatedAccountList)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke should update crypto portfolio account with new icon`() = runTest {
|
||||
// Arrange
|
||||
val accountList = AccountList.empty(userWallet = userWallet)
|
||||
val accountId = accountList.mainAccount.accountId
|
||||
|
||||
val newAccountIcon = CryptoPortfolioIcon.ofCustomAccount(
|
||||
value = CryptoPortfolioIcon.Icon.Star,
|
||||
color = CryptoPortfolioIcon.Color.CaribbeanBlue,
|
||||
)
|
||||
val updatedAccount = accountList.mainAccount.copy(accountIcon = newAccountIcon)
|
||||
val updatedAccountList = (accountList + updatedAccount).getOrNull()!!
|
||||
|
||||
coEvery { crudRepository.getAccounts(userWalletId = userWalletId) } returns accountList.toOption()
|
||||
|
||||
// Act
|
||||
val actual = useCase(accountId = accountId, icon = newAccountIcon)
|
||||
|
||||
// Assert
|
||||
val expected = updatedAccount.right()
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
|
||||
coVerifyOrder {
|
||||
crudRepository.getAccounts(userWalletId = userWalletId)
|
||||
crudRepository.saveAccounts(accountList = updatedAccountList)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke should update crypto portfolio account with new name and icon`() = runTest {
|
||||
// Arrange
|
||||
val accountList = AccountList.empty(userWallet = userWallet)
|
||||
val accountId = accountList.mainAccount.accountId
|
||||
|
||||
val newAccountName = AccountName("New name").getOrNull()!!
|
||||
val newAccountIcon = CryptoPortfolioIcon.ofCustomAccount(
|
||||
value = CryptoPortfolioIcon.Icon.Star,
|
||||
color = CryptoPortfolioIcon.Color.CaribbeanBlue,
|
||||
)
|
||||
val updatedAccount = accountList.mainAccount.copy(accountName = newAccountName, accountIcon = newAccountIcon)
|
||||
val updatedAccountList = (accountList + updatedAccount).getOrNull()!!
|
||||
|
||||
coEvery { crudRepository.getAccounts(userWalletId = userWalletId) } returns accountList.toOption()
|
||||
|
||||
// Act
|
||||
val actual = useCase(accountId = accountId, accountName = newAccountName, icon = newAccountIcon)
|
||||
|
||||
// Assert
|
||||
val expected = updatedAccount.right()
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
|
||||
coVerifyOrder {
|
||||
crudRepository.getAccounts(userWalletId = userWalletId)
|
||||
crudRepository.saveAccounts(accountList = updatedAccountList)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke if name and icon are null`() = runTest {
|
||||
// Arrange
|
||||
val accountList = AccountList.empty(userWallet = userWallet)
|
||||
val accountId = accountList.mainAccount.accountId
|
||||
|
||||
coEvery { crudRepository.getAccounts(userWalletId = userWalletId) } returns accountList.toOption()
|
||||
|
||||
// Act
|
||||
val actual = useCase(accountId = accountId)
|
||||
|
||||
// Assert
|
||||
val expected = Error.NothingToUpdate.left()
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
|
||||
coVerify(inverse = true) {
|
||||
crudRepository.getAccounts(userWalletId = any())
|
||||
crudRepository.saveAccounts(accountList = any())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke if getAccounts throws exception`() = runTest {
|
||||
// Arrange
|
||||
val accountList = AccountList.empty(userWallet = userWallet)
|
||||
val accountId = accountList.mainAccount.accountId
|
||||
|
||||
val newAccountName = AccountName("New name").getOrNull()!!
|
||||
|
||||
val exception = IllegalStateException("Test exception")
|
||||
|
||||
coEvery { crudRepository.getAccounts(userWalletId = userWalletId) } throws exception
|
||||
|
||||
// Act
|
||||
val actual = useCase(accountId = accountId, accountName = newAccountName)
|
||||
|
||||
// Assert
|
||||
val expected = Error.DataOperationFailed(cause = exception).left()
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
|
||||
coVerifyOrder { crudRepository.getAccounts(userWalletId = userWalletId) }
|
||||
coVerify(inverse = true) { crudRepository.saveAccounts(accountList = any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke if getAccounts returns None`() = runTest {
|
||||
// Arrange
|
||||
val accountId = AccountId.forCryptoPortfolio(
|
||||
userWalletId = userWalletId,
|
||||
derivationIndex = DerivationIndex.Main,
|
||||
)
|
||||
val accountList = None
|
||||
|
||||
val newAccountName = AccountName("New name").getOrNull()!!
|
||||
|
||||
coEvery { crudRepository.getAccounts(userWalletId = userWalletId) } returns accountList
|
||||
|
||||
// Act
|
||||
val actual = useCase(accountId = accountId, accountName = newAccountName)
|
||||
|
||||
// Assert
|
||||
val expected = Error.CriticalTechError.AccountsNotCreated(userWalletId = userWalletId).left()
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
|
||||
coVerifyOrder { crudRepository.getAccounts(userWalletId = userWalletId) }
|
||||
coVerify(inverse = true) { crudRepository.saveAccounts(accountList = any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke if getAccounts does not contain accountId`() = runTest {
|
||||
// Arrange
|
||||
val accountList = AccountList.empty(userWallet = userWallet)
|
||||
val accountId = AccountId.forCryptoPortfolio(
|
||||
userWalletId = userWalletId,
|
||||
derivationIndex = DerivationIndex(1).getOrNull()!!,
|
||||
)
|
||||
|
||||
val newAccountName = AccountName("New name").getOrNull()!!
|
||||
|
||||
coEvery { crudRepository.getAccounts(userWalletId = userWalletId) } returns accountList.toOption()
|
||||
|
||||
// Act
|
||||
val actual = useCase(accountId = accountId, accountName = newAccountName)
|
||||
|
||||
// Assert
|
||||
val expected = Error.CriticalTechError.AccountNotFound(accountId = accountId).left()
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
|
||||
coVerifyOrder { crudRepository.getAccounts(userWalletId = userWalletId) }
|
||||
coVerify(inverse = true) { crudRepository.saveAccounts(accountList = any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke if saveAccounts throws exception`() = runTest {
|
||||
// Arrange
|
||||
val accountList = AccountList.empty(userWallet = userWallet)
|
||||
val accountId = accountList.mainAccount.accountId
|
||||
|
||||
val newAccountName = AccountName("New name").getOrNull()!!
|
||||
val updatedAccount = accountList.mainAccount.copy(accountName = newAccountName)
|
||||
val updatedAccountList = (accountList + updatedAccount).getOrNull()!!
|
||||
|
||||
val exception = IllegalStateException("Save failed")
|
||||
|
||||
coEvery { crudRepository.getAccounts(userWalletId = userWalletId) } returns accountList.toOption()
|
||||
coEvery { crudRepository.saveAccounts(accountList = updatedAccountList) } throws exception
|
||||
|
||||
// Act
|
||||
val actual = useCase(accountId = accountId, accountName = newAccountName)
|
||||
|
||||
// Assert
|
||||
val expected = Error.DataOperationFailed(cause = exception).left()
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
|
||||
coVerifyOrder {
|
||||
crudRepository.getAccounts(userWalletId = userWalletId)
|
||||
crudRepository.saveAccounts(accountList = updatedAccountList)
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
||||
val userWalletId = UserWalletId("011")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
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
|
||||
|
||||
fun createAccounts(userWalletId: UserWalletId, count: Int): Set<Account.CryptoPortfolio> {
|
||||
return buildSet {
|
||||
add(Account.CryptoPortfolio.createMainAccount(userWalletId))
|
||||
|
||||
repeat(count - 1) {
|
||||
val account = createAccount(
|
||||
userWalletId = userWalletId,
|
||||
name = "Test Account ${it + 1}",
|
||||
derivationIndex = it + 1,
|
||||
)
|
||||
|
||||
add(account)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun createAccount(
|
||||
userWalletId: UserWalletId,
|
||||
name: String = "Test Account",
|
||||
icon: CryptoPortfolioIcon = CryptoPortfolioIcon.ofDefaultCustomAccount(),
|
||||
derivationIndex: Int = Random.nextInt(1, 21),
|
||||
): Account.CryptoPortfolio {
|
||||
val derivationIndex = DerivationIndex(derivationIndex).getOrNull()!!
|
||||
|
||||
return Account.CryptoPortfolio(
|
||||
accountId = AccountId.forCryptoPortfolio(userWalletId = userWalletId, derivationIndex = derivationIndex),
|
||||
accountName = AccountName(name).getOrNull()!!,
|
||||
accountIcon = icon,
|
||||
derivationIndex = derivationIndex,
|
||||
isArchived = false,
|
||||
cryptoCurrencyList = Account.CryptoPortfolio.CryptoCurrencyList(
|
||||
currencies = emptySet(),
|
||||
sortType = TokensSortType.NONE,
|
||||
groupType = TokensGroupType.NONE,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.domain.card
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.Blockchain.Companion.fromId
|
||||
import com.tangem.blockchain.common.Token
|
||||
import com.tangem.common.card.EllipticCurve
|
||||
import com.tangem.common.card.FirmwareVersion
|
||||
|
|
@ -76,7 +77,7 @@ internal class TangemCardTypesResolver(
|
|||
} else {
|
||||
return Blockchain.Unknown
|
||||
}
|
||||
Blockchain.Companion.fromBlockchainName(blockchainName)
|
||||
Blockchain.fromBlockchainName(blockchainName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -100,6 +100,20 @@ fun UserWallet.canHandleToken(blockchain: Blockchain, excludedBlockchains: Exclu
|
|||
}
|
||||
}
|
||||
|
||||
fun UserWallet.canHandleBlockchain(blockchain: Blockchain, excludedBlockchains: ExcludedBlockchains): Boolean {
|
||||
return when (this) {
|
||||
is UserWallet.Cold -> {
|
||||
scanResponse.card.canHandleBlockchain(
|
||||
blockchain = blockchain,
|
||||
excludedBlockchains = excludedBlockchains,
|
||||
cardTypesResolver = scanResponse.cardTypesResolver,
|
||||
)
|
||||
}
|
||||
is UserWallet.Hot -> blockchain.isTestnet().not() &&
|
||||
blockchain !in excludedBlockchains
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The same as [CardDTO.supportedTokens] but with supportedTokens input, if previously calculated
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -6,10 +6,7 @@ import com.tangem.common.card.EllipticCurve
|
|||
import com.tangem.common.extensions.toMapKey
|
||||
import com.tangem.crypto.hdWallet.DerivationPath
|
||||
import com.tangem.domain.card.CardTypesResolver
|
||||
import com.tangem.domain.card.DerivationStyleProvider
|
||||
import com.tangem.domain.card.TangemCardTypesResolver
|
||||
import com.tangem.domain.card.TangemDerivationStyleProvider
|
||||
import com.tangem.domain.card.TangemHotDerivationStyleProvider
|
||||
import com.tangem.domain.card.common.TapWorkarounds.isTangemTwins
|
||||
import com.tangem.domain.card.common.TapWorkarounds.isTestCard
|
||||
import com.tangem.domain.card.configs.CardConfig
|
||||
|
|
@ -25,18 +22,6 @@ val ScanResponse.cardTypesResolver: CardTypesResolver
|
|||
walletData = walletData,
|
||||
)
|
||||
|
||||
val UserWallet.derivationStyleProvider: DerivationStyleProvider
|
||||
get() = when (this) {
|
||||
is UserWallet.Cold -> this.scanResponse.derivationStyleProvider
|
||||
is UserWallet.Hot -> TangemHotDerivationStyleProvider()
|
||||
}
|
||||
|
||||
val ScanResponse.derivationStyleProvider: DerivationStyleProvider
|
||||
get() = card.derivationStyleProvider
|
||||
|
||||
val CardDTO.derivationStyleProvider: DerivationStyleProvider
|
||||
get() = TangemDerivationStyleProvider(this)
|
||||
|
||||
val UserWallet.Cold.cardTypesResolver: CardTypesResolver
|
||||
get() = scanResponse.cardTypesResolver
|
||||
|
||||
|
|
|
|||
|
|
@ -207,6 +207,8 @@ data object Wallet2CardConfig : CardConfig {
|
|||
Blockchain.ZkLinkNovaTestnet -> EllipticCurve.Secp256k1
|
||||
Blockchain.Pepecoin -> EllipticCurve.Secp256k1
|
||||
Blockchain.PepecoinTestnet -> EllipticCurve.Secp256k1
|
||||
Blockchain.Hyperliquid -> EllipticCurve.Secp256k1
|
||||
Blockchain.HyperliquidTestnet -> EllipticCurve.Secp256k1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -164,6 +164,8 @@ class Wallet2CardConfigTest {
|
|||
Blockchain.ZkLinkNovaTestnet to EllipticCurve.Secp256k1,
|
||||
Blockchain.Pepecoin to EllipticCurve.Secp256k1,
|
||||
Blockchain.PepecoinTestnet to EllipticCurve.Secp256k1,
|
||||
Blockchain.Hyperliquid to EllipticCurve.Secp256k1,
|
||||
Blockchain.HyperliquidTestnet to EllipticCurve.Secp256k1,
|
||||
)
|
||||
|
||||
@Test
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ dependencies {
|
|||
api(deps.kotlin.coroutines)
|
||||
api(deps.arrow.core)
|
||||
api(deps.arrow.fx)
|
||||
api(projects.domain.models)
|
||||
|
||||
implementation(deps.kotlin.serialization)
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,145 @@
|
|||
package com.tangem.domain.core.wallets
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.domain.core.wallets.error.DeleteWalletError
|
||||
import com.tangem.domain.core.wallets.error.LockWalletsError
|
||||
import com.tangem.domain.core.wallets.error.SaveWalletError
|
||||
import com.tangem.domain.core.wallets.error.SelectWalletError
|
||||
import com.tangem.domain.core.wallets.error.SetLockError
|
||||
import com.tangem.domain.core.wallets.error.UnlockWalletError
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
/**
|
||||
* Repository for managing user wallets list.
|
||||
* It provides methods to load, select, save, lock, unlock, and delete user wallets.
|
||||
*
|
||||
* TODO tests [REDACTED_TASK_KEY]
|
||||
*
|
||||
* @see com.tangem.domain.models.wallet.UserWallet
|
||||
* @see com.tangem.domain.models.wallet.UserWalletId
|
||||
*/
|
||||
interface UserWalletsListRepository {
|
||||
|
||||
/**
|
||||
* List of user wallets.
|
||||
* It can be null if the list is not loaded yet.
|
||||
*/
|
||||
val userWallets: StateFlow<List<UserWallet>?>
|
||||
|
||||
/**
|
||||
* Currently selected user wallet.
|
||||
* It can be null if wallets list is not loaded yet or wallets list is empty.
|
||||
*/
|
||||
val selectedUserWallet: StateFlow<UserWallet?>
|
||||
|
||||
/**
|
||||
* Loads user wallets list and selected wallet.
|
||||
* If the list is already loaded, it does nothing.
|
||||
*/
|
||||
suspend fun load()
|
||||
|
||||
/**
|
||||
* Gets and if necessary loads user wallets list and selected wallet.
|
||||
*/
|
||||
suspend fun userWalletsSync(): List<UserWallet>
|
||||
|
||||
/**
|
||||
* Gets and if necessary loads selected user wallet.
|
||||
*/
|
||||
suspend fun selectedUserWalletSync(): UserWallet?
|
||||
|
||||
/**
|
||||
* Selects user wallet by id.
|
||||
* If the wallet is not found, it returns [SelectWalletError.UnableToSelectUserWallet].
|
||||
*/
|
||||
suspend fun select(userWalletId: UserWalletId): Either<SelectWalletError, UserWallet>
|
||||
|
||||
/**
|
||||
* Saves user wallet.
|
||||
* If the wallet already exists and [canOverride] is false, it returns [SaveWalletError.WalletAlreadySaved].
|
||||
* If the wallet already exists and [canOverride] is true, it overrides the existing wallet.
|
||||
*
|
||||
* Does not lock the wallet after saving, it should be done manually using [setLock] method.
|
||||
*/
|
||||
suspend fun saveWithoutLock(
|
||||
userWallet: UserWallet,
|
||||
canOverride: Boolean = true,
|
||||
): Either<SaveWalletError, UserWallet>
|
||||
|
||||
/**
|
||||
* Sets lock for **unlocked** user wallet.
|
||||
* 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,
|
||||
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.
|
||||
* If the wallet is not found, it returns [DeleteWalletError.UnableToDelete]
|
||||
*/
|
||||
suspend fun delete(userWalletIds: List<UserWalletId>): Either<DeleteWalletError, Unit>
|
||||
|
||||
/**
|
||||
* Unlocks specific user wallet.
|
||||
* If the wallet is already unlocked, returns [UnlockWalletError.AlreadyUnlocked].
|
||||
* If the wallet is not found, returns [UnlockWalletError.UserWalletNotFound].
|
||||
* If the unlock method is not supported, returns [UnlockWalletError.UnableToUnlock]
|
||||
* If the user cancels the unlock operation (ex. dismisses dialogs), returns [UnlockWalletError.UserCancelled].
|
||||
* If the scanned card does not match the wallet, returns [UnlockWalletError.ScannedCardWalletNotMatched].
|
||||
*/
|
||||
suspend fun unlock(userWalletId: UserWalletId, unlockMethod: UnlockMethod): Either<UnlockWalletError, Unit>
|
||||
|
||||
/**
|
||||
* Unlocks all user wallets using biometric authentication.
|
||||
* If all the wallets was are already unlocked, returns [UnlockWalletError.AlreadyUnlocked].
|
||||
* Success if at least one wallet was unlocked.
|
||||
* If the biometric method is not supported for some of user wallets, returns [UnlockWalletError.UnableToUnlock]
|
||||
*/
|
||||
suspend fun unlockAllWallets(): Either<UnlockWalletError, Unit>
|
||||
|
||||
/**
|
||||
* Locks all secured user wallets (wallets that are not locked with [LockMethod.NoLock]).
|
||||
* If all the wallets are already locked or unsecured, returns [LockWalletsError.NothingToLock].
|
||||
* Success if at least one wallet was locked.
|
||||
*/
|
||||
suspend fun lockAllWallets(): Either<LockWalletsError, Unit>
|
||||
|
||||
/**
|
||||
* Clears all persistent data related to user wallets.
|
||||
* This includes removing all user wallets, selected wallet, and any other related data.
|
||||
* User wallets will stay in the cache, but will be reloaded on next repository initialization.
|
||||
*/
|
||||
suspend fun clearPersistentData()
|
||||
|
||||
sealed class LockMethod {
|
||||
data object Biometric : LockMethod()
|
||||
class AccessCode(val accessCode: CharArray) : LockMethod()
|
||||
data object NoLock : LockMethod()
|
||||
}
|
||||
|
||||
enum class UnlockMethod {
|
||||
Biometric,
|
||||
AccessCode,
|
||||
Scan,
|
||||
}
|
||||
}
|
||||
|
||||
fun UserWalletsListRepository.requireUserWalletsSync(): List<UserWallet> {
|
||||
return userWallets.value ?: error("User wallets list is not loaded")
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.domain.wallets.models
|
||||
package com.tangem.domain.core.wallets.error
|
||||
|
||||
sealed interface DeleteWalletError {
|
||||
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem.domain.core.wallets.error
|
||||
|
||||
interface LockWalletsError {
|
||||
|
||||
data object NothingToLock : LockWalletsError
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.domain.wallets.models
|
||||
package com.tangem.domain.core.wallets.error
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem.domain.core.wallets.error
|
||||
|
||||
sealed interface SelectWalletError {
|
||||
|
||||
data object UnableToSelectUserWallet : SelectWalletError
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.domain.core.wallets.error
|
||||
|
||||
sealed interface SetLockError {
|
||||
|
||||
data object UserWalletNotFound : SetLockError
|
||||
|
||||
data object UserWalletLocked : SetLockError
|
||||
|
||||
data class UnableToSetLock(val cause: Throwable) : SetLockError
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.tangem.domain.core.wallets.error
|
||||
|
||||
sealed interface UnlockWalletError {
|
||||
|
||||
data object AlreadyUnlocked : UnlockWalletError
|
||||
|
||||
data object UserWalletNotFound : UnlockWalletError
|
||||
|
||||
data object UnableToUnlock : UnlockWalletError
|
||||
|
||||
data object UserCancelled : UnlockWalletError
|
||||
|
||||
data object ScannedCardWalletNotMatched : UnlockWalletError
|
||||
}
|
||||
|
|
@ -3,9 +3,9 @@ package com.tangem.domain.exchange
|
|||
import arrow.core.Either
|
||||
import com.tangem.domain.core.lce.Lce
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
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.CryptoCurrencyStatus
|
||||
import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason
|
||||
import com.tangem.domain.transaction.models.AssetRequirementsCondition
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ dependencies {
|
|||
implementation(projects.domain.staking)
|
||||
implementation(projects.domain.tokens)
|
||||
implementation(projects.domain.card)
|
||||
implementation(projects.domain.wallets)
|
||||
implementation(projects.domain.legacy)
|
||||
|
||||
/* Core */
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ package com.tangem.domain.managetokens
|
|||
|
||||
import arrow.core.Either
|
||||
import arrow.core.flatten
|
||||
import com.tangem.domain.card.repository.DerivationsRepository
|
||||
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency
|
||||
import com.tangem.domain.managetokens.repository.CustomTokensRepository
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
|
|
@ -10,9 +9,11 @@ import com.tangem.domain.models.network.Network
|
|||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher
|
||||
import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher
|
||||
import com.tangem.domain.staking.StakingIdFactory
|
||||
import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher
|
||||
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.domain.wallets.derivations.DerivationsRepository
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
class SaveManagedTokensUseCase(
|
||||
|
|
@ -23,6 +24,7 @@ class SaveManagedTokensUseCase(
|
|||
private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
|
||||
private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
|
||||
private val multiYieldBalanceFetcher: MultiYieldBalanceFetcher,
|
||||
private val stakingIdFactory: StakingIdFactory,
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(
|
||||
|
|
@ -91,11 +93,12 @@ class SaveManagedTokensUseCase(
|
|||
userWalletId: UserWalletId,
|
||||
addedCurrencies: List<CryptoCurrency>,
|
||||
) {
|
||||
val stakingIds = addedCurrencies.mapNotNullTo(hashSetOf()) {
|
||||
stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = it).getOrNull()
|
||||
}
|
||||
|
||||
multiYieldBalanceFetcher(
|
||||
params = MultiYieldBalanceFetcher.Params(
|
||||
userWalletId = userWalletId,
|
||||
currencyIdWithNetworkMap = addedCurrencies.associateTo(hashMapOf()) { it.id to it.network },
|
||||
),
|
||||
params = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = stakingIds),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package com.tangem.domain.markets
|
||||
|
||||
import com.tangem.domain.core.serialization.SerializedBigDecimal
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.serialization.SerializedBigDecimal
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
|
|
|
|||
|
|
@ -4,13 +4,15 @@ import com.tangem.blockchain.common.Blockchain
|
|||
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
|
||||
import com.tangem.blockchainsdk.utils.fromNetworkId
|
||||
import com.tangem.domain.card.common.extensions.supportedBlockchains
|
||||
import com.tangem.domain.card.common.util.cardTypesResolver
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
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,
|
||||
) {
|
||||
|
||||
|
|
@ -22,25 +24,23 @@ 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()
|
||||
|
||||
return when (userWallet) {
|
||||
is UserWallet.Cold -> {
|
||||
val supportedBlockchains = userWallet.scanResponse.card.supportedBlockchains(
|
||||
cardTypesResolver = userWallet.scanResponse.cardTypesResolver,
|
||||
excludedBlockchains = excludedBlockchains,
|
||||
)
|
||||
val supportedBlockchains = userWallet.supportedBlockchains(
|
||||
excludedBlockchains = excludedBlockchains,
|
||||
)
|
||||
|
||||
networks.filter {
|
||||
val blockchain = Blockchain.fromNetworkId(it.networkId)
|
||||
supportedBlockchains.contains(blockchain)
|
||||
}.toSet()
|
||||
}
|
||||
is UserWallet.Hot -> {
|
||||
networks.toSet() // TODO [REDACTED_TASK_KEY]
|
||||
}
|
||||
}
|
||||
return networks.filter {
|
||||
val blockchain = Blockchain.fromNetworkId(it.networkId)
|
||||
supportedBlockchains.contains(blockchain)
|
||||
}.toSet()
|
||||
}
|
||||
|
||||
private fun getWallets() = if (useNewRepository) {
|
||||
userWalletsListRepository.requireUserWalletsSync()
|
||||
} else {
|
||||
userWalletsListManager.userWalletsSync
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +1,14 @@
|
|||
package com.tangem.domain.markets
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.domain.card.repository.DerivationsRepository
|
||||
import com.tangem.domain.wallets.derivations.DerivationsRepository
|
||||
import com.tangem.domain.markets.repositories.MarketsTokenRepository
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher
|
||||
import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher
|
||||
import com.tangem.domain.staking.StakingIdFactory
|
||||
import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher
|
||||
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
||||
|
||||
|
|
@ -28,6 +29,7 @@ class SaveMarketTokensUseCase(
|
|||
private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
|
||||
private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
|
||||
private val multiYieldBalanceFetcher: MultiYieldBalanceFetcher,
|
||||
private val stakingIdFactory: StakingIdFactory,
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(
|
||||
|
|
@ -89,11 +91,12 @@ class SaveMarketTokensUseCase(
|
|||
userWalletId: UserWalletId,
|
||||
existingCurrencies: List<CryptoCurrency>,
|
||||
) {
|
||||
val stakingIds = existingCurrencies.mapNotNullTo(hashSetOf()) {
|
||||
stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = it).getOrNull()
|
||||
}
|
||||
|
||||
multiYieldBalanceFetcher(
|
||||
params = MultiYieldBalanceFetcher.Params(
|
||||
userWalletId = userWalletId,
|
||||
currencyIdWithNetworkMap = existingCurrencies.associateTo(hashMapOf()) { it.id to it.network },
|
||||
),
|
||||
params = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = stakingIds),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ dependencies {
|
|||
implementation(tangemDeps.hot.core)
|
||||
implementation(deps.moshi.kotlin)
|
||||
implementation(deps.moshi.adapters)
|
||||
implementation(deps.kotlin.datetime)
|
||||
implementation(deps.kotlin.serialization)
|
||||
ksp(deps.moshi.kotlin.codegen)
|
||||
implementation(deps.arrow.core)
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
package com.tangem.domain.models
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Source of the status of any loaded data
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Serializable
|
||||
enum class StatusSource {
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,22 +1,26 @@
|
|||
package com.tangem.domain.models
|
||||
|
||||
import java.math.BigDecimal
|
||||
import com.tangem.domain.models.serialization.SerializedBigDecimal
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Represents the possible states of the fiat balance, including loading, failure, or a loaded amount
|
||||
*/
|
||||
@Serializable
|
||||
sealed interface TotalFiatBalance {
|
||||
|
||||
/**
|
||||
* Represents the loading state of the fiat balance.
|
||||
* This state indicates that the fiat balance is currently being retrieved or calculated.
|
||||
*/
|
||||
@Serializable
|
||||
data object Loading : TotalFiatBalance
|
||||
|
||||
/**
|
||||
* Represents the failure state of the fiat balance.
|
||||
* This state indicates that an attempt to retrieve or calculate the fiat balance has failed.
|
||||
*/
|
||||
@Serializable
|
||||
data object Failed : TotalFiatBalance
|
||||
|
||||
/**
|
||||
|
|
@ -25,8 +29,9 @@ sealed interface TotalFiatBalance {
|
|||
* @property amount the loaded fiat balance amount
|
||||
* @property isAllAmountsSummarized indicates whether the amount includes a summary of all underlying amounts
|
||||
*/
|
||||
@Serializable
|
||||
data class Loaded(
|
||||
val amount: BigDecimal,
|
||||
val amount: SerializedBigDecimal,
|
||||
val isAllAmountsSummarized: Boolean,
|
||||
val source: StatusSource,
|
||||
) : TotalFiatBalance
|
||||
|
|
|
|||
|
|
@ -0,0 +1,198 @@
|
|||
package com.tangem.domain.models.account
|
||||
|
||||
import arrow.core.Either
|
||||
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
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Represents an account
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Serializable
|
||||
sealed interface Account {
|
||||
|
||||
/** Unique identifier of the account */
|
||||
val accountId: AccountId
|
||||
|
||||
/** Name of the account */
|
||||
val name: AccountName
|
||||
|
||||
/** The identifier of the user wallet associated with the account */
|
||||
val userWalletId: UserWalletId
|
||||
get() = accountId.userWalletId
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
@Serializable
|
||||
data class CryptoPortfolio private constructor(
|
||||
override val accountId: AccountId,
|
||||
override val name: AccountName,
|
||||
val icon: CryptoPortfolioIcon,
|
||||
val derivationIndex: DerivationIndex,
|
||||
val isArchived: Boolean,
|
||||
val cryptoCurrencyList: CryptoCurrencyList,
|
||||
) : Account {
|
||||
|
||||
/** Indicates if the account is the main account */
|
||||
val isMainAccount: Boolean
|
||||
get() = derivationIndex.isMain
|
||||
|
||||
/** Number of tokens in the account */
|
||||
val tokensCount: Int
|
||||
get() = cryptoCurrencyList.currencies.size
|
||||
|
||||
/** Number of distinct networks in the account */
|
||||
val networksCount: Int
|
||||
get() = cryptoCurrencyList.currencies.map(CryptoCurrency::network).distinct().size
|
||||
|
||||
fun copy(
|
||||
accountName: AccountName = this.name,
|
||||
accountIcon: CryptoPortfolioIcon = this.icon,
|
||||
isArchived: Boolean = this.isArchived,
|
||||
): CryptoPortfolio {
|
||||
return CryptoPortfolio(
|
||||
accountId = this.accountId,
|
||||
name = accountName,
|
||||
icon = accountIcon,
|
||||
derivationIndex = this.derivationIndex,
|
||||
isArchived = isArchived,
|
||||
cryptoCurrencyList = this.cryptoCurrencyList,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
@Serializable
|
||||
sealed interface Error {
|
||||
|
||||
/** Error indicating that the account name is blank */
|
||||
@Serializable
|
||||
data class AccountNameError(val cause: AccountName.Error) : Error
|
||||
|
||||
/** Error indicating that the derivation index is negative */
|
||||
@Serializable
|
||||
data class DerivationIndexError(val cause: DerivationIndex.Error) : Error
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
operator fun invoke(
|
||||
accountId: AccountId,
|
||||
name: String,
|
||||
accountIcon: CryptoPortfolioIcon,
|
||||
derivationIndex: Int,
|
||||
isArchived: Boolean,
|
||||
cryptoCurrencyList: CryptoCurrencyList,
|
||||
): Either<Error, CryptoPortfolio> {
|
||||
return either {
|
||||
val accountName = AccountName(value = name).mapLeft(::AccountNameError).bind()
|
||||
val derivationIndex = DerivationIndex(derivationIndex).mapLeft(::DerivationIndexError).bind()
|
||||
|
||||
invoke(
|
||||
accountId = accountId,
|
||||
accountName = accountName,
|
||||
accountIcon = accountIcon,
|
||||
derivationIndex = derivationIndex,
|
||||
isArchived = isArchived,
|
||||
cryptoCurrencyList = cryptoCurrencyList,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
operator fun invoke(
|
||||
accountId: AccountId,
|
||||
accountName: AccountName,
|
||||
accountIcon: CryptoPortfolioIcon,
|
||||
derivationIndex: DerivationIndex,
|
||||
isArchived: Boolean,
|
||||
cryptoCurrencyList: CryptoCurrencyList,
|
||||
): CryptoPortfolio {
|
||||
return CryptoPortfolio(
|
||||
accountId = accountId,
|
||||
name = accountName,
|
||||
icon = accountIcon,
|
||||
derivationIndex = derivationIndex,
|
||||
isArchived = isArchived,
|
||||
cryptoCurrencyList = cryptoCurrencyList,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a main account for the given user wallet ID
|
||||
*
|
||||
* @param userWalletId the ID of the user wallet
|
||||
*/
|
||||
fun createMainAccount(userWalletId: UserWalletId): CryptoPortfolio {
|
||||
val derivationIndex = DerivationIndex.Main
|
||||
|
||||
return CryptoPortfolio(
|
||||
accountId = AccountId.forCryptoPortfolio(
|
||||
userWalletId = userWalletId,
|
||||
derivationIndex = derivationIndex,
|
||||
),
|
||||
name = AccountName.Main,
|
||||
icon = CryptoPortfolioIcon.ofMainAccount(userWalletId),
|
||||
derivationIndex = derivationIndex,
|
||||
isArchived = false,
|
||||
cryptoCurrencyList = CryptoCurrencyList(
|
||||
currencies = emptySet(),
|
||||
sortType = TokensSortType.NONE,
|
||||
groupType = TokensGroupType.NONE,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
package com.tangem.domain.models.account
|
||||
|
||||
import com.tangem.common.extensions.toByteArray
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.utils.extensions.toHexString
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.security.MessageDigest
|
||||
|
||||
/**
|
||||
* Represents a unique identifier for an account
|
||||
*
|
||||
* @property value a unique string value that distinguishes this account
|
||||
* @property userWalletId the identifier of the user wallet associated with the account
|
||||
*/
|
||||
@Serializable
|
||||
data class AccountId private constructor(
|
||||
val value: String,
|
||||
val userWalletId: UserWalletId,
|
||||
) {
|
||||
|
||||
companion object {
|
||||
|
||||
private val sha256Digest: MessageDigest by lazy { MessageDigest.getInstance("SHA-256") }
|
||||
|
||||
/**
|
||||
* Creates a unique account identifier for a crypto portfolio
|
||||
*
|
||||
* @param userWalletId the identifier of the user wallet
|
||||
* @param derivationIndex the derivation index used to generate the identifier
|
||||
*/
|
||||
fun forCryptoPortfolio(userWalletId: UserWalletId, derivationIndex: DerivationIndex): AccountId {
|
||||
val input = userWalletId.value + derivationIndex.value.toByteArray()
|
||||
val value = sha256Digest.digest(input).toHexString()
|
||||
|
||||
return AccountId(value = value, userWalletId = userWalletId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
package com.tangem.domain.models.account
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.raise.either
|
||||
import arrow.core.raise.ensure
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Represents an account name
|
||||
*
|
||||
* @property value the validated account name as a string
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Serializable
|
||||
data class AccountName private constructor(
|
||||
val value: String,
|
||||
) {
|
||||
|
||||
/**
|
||||
* Represents possible validation errors
|
||||
*/
|
||||
@Serializable
|
||||
sealed interface Error {
|
||||
|
||||
/**
|
||||
* Error indicating that the account name is blank
|
||||
*/
|
||||
@Serializable
|
||||
data object Empty : Error {
|
||||
override fun toString(): String = "${Empty::class.simpleName}: Account name cannot be blank"
|
||||
}
|
||||
|
||||
/**
|
||||
* Error indicating that the account name exceeds the maximum allowed length
|
||||
*/
|
||||
@Serializable
|
||||
data object ExceedsMaxLength : Error {
|
||||
override fun toString(): String {
|
||||
return "${ExceedsMaxLength::class.simpleName}: Account name cannot exceed $MAX_LENGTH characters"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val MAIN_ACCOUNT_NAME = "Main Account"
|
||||
private const val MAX_LENGTH = 20
|
||||
|
||||
/** Default name for the main account */
|
||||
val Main: AccountName
|
||||
get() = AccountName(value = MAIN_ACCOUNT_NAME)
|
||||
|
||||
/**
|
||||
* Factory method to create an `AccountName` instance.
|
||||
* Validates the input string to ensure it is not blank and does not exceed the maximum length.
|
||||
*
|
||||
* @param value the input string representing the account name
|
||||
*/
|
||||
operator fun invoke(value: String): Either<Error, AccountName> = either {
|
||||
val trimmedValue = value.trim()
|
||||
|
||||
ensure(trimmedValue.isNotBlank()) { Error.Empty }
|
||||
ensure(trimmedValue.length <= MAX_LENGTH) { Error.ExceedsMaxLength }
|
||||
|
||||
AccountName(value = trimmedValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package com.tangem.domain.models.account
|
||||
|
||||
import com.tangem.domain.models.tokenlist.TokenList
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Represents the status of an account
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Serializable
|
||||
sealed interface AccountStatus {
|
||||
|
||||
/** The account associated with this status */
|
||||
val account: Account
|
||||
|
||||
/**
|
||||
* Represents the status of a crypto portfolio account
|
||||
*
|
||||
* @property account the crypto portfolio account
|
||||
* @property tokenList the list of tokens associated with the account
|
||||
*/
|
||||
@Serializable
|
||||
data class CryptoPortfolio(
|
||||
override val account: Account.CryptoPortfolio,
|
||||
val tokenList: TokenList,
|
||||
) : AccountStatus
|
||||
}
|
||||
|
|
@ -1,48 +1,26 @@
|
|||
package com.tangem.domain.models.account
|
||||
|
||||
import com.tangem.domain.models.account.CryptoPortfolioIcon.Companion.ofCustomAccount
|
||||
import com.tangem.domain.models.account.CryptoPortfolioIcon.Companion.ofDefaultCustomAccount
|
||||
import com.tangem.domain.models.account.CryptoPortfolioIcon.Companion.ofMainAccount
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Represents an icon for an [Account.CryptoPortfolio] account
|
||||
*
|
||||
* @property type the type of the account icon
|
||||
* @property value the type of the account icon
|
||||
* @property color the color of the account icon
|
||||
*
|
||||
* @constructor [ofMainAccount], [ofCustomAccount]
|
||||
* @constructor [ofMainAccount], [ofDefaultCustomAccount]
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Serializable
|
||||
data class CryptoPortfolioIcon private constructor(
|
||||
val type: Type,
|
||||
val value: Icon,
|
||||
val color: Color,
|
||||
) {
|
||||
|
||||
/**
|
||||
* Represents the type of an account icon. Can either be a specific [Icon] or a [Symbol]
|
||||
*/
|
||||
@Serializable
|
||||
sealed interface Type {
|
||||
|
||||
/**
|
||||
* Represents a specific predefined icon type
|
||||
*
|
||||
* @property value the predefined [Icon] of the icon
|
||||
*/
|
||||
@Serializable
|
||||
data class Icon(val value: CryptoPortfolioIcon.Icon) : Type
|
||||
|
||||
/**
|
||||
* Represents an icon with a letter
|
||||
*
|
||||
* @property value the letter used as the icon
|
||||
*/
|
||||
@Serializable
|
||||
data class Symbol(val value: Char) : Type
|
||||
}
|
||||
|
||||
/**
|
||||
* Enum class representing the icons of accounts
|
||||
*/
|
||||
|
|
@ -91,59 +69,44 @@ data class CryptoPortfolioIcon private constructor(
|
|||
|
||||
companion object {
|
||||
|
||||
private val defaultMainAccountType: Icon = Icon.Star
|
||||
private val defaultMainAccountColor: Color = Color.Azure
|
||||
private val defaultMainAccountIcon: Icon = Icon.Star
|
||||
private val excludedCustomAccountIcons: Set<Icon> = setOf(Icon.Letter, Icon.Star)
|
||||
private const val HASH_MULTIPLIER = 31
|
||||
|
||||
/**
|
||||
* Creates an [CryptoPortfolioIcon] for the Main account, ensuring the color is not in the excluded set.
|
||||
* Creating a [CryptoPortfolioIcon] for the Main account with default values.
|
||||
* The color is derived from the [UserWalletId].
|
||||
*
|
||||
* @param exclude excluded colors that are already used for main accounts
|
||||
* @param userWalletId the ID of the user wallet
|
||||
*/
|
||||
fun ofMainAccount(exclude: Set<Color>): CryptoPortfolioIcon {
|
||||
val isDefaultColorBusy = defaultMainAccountColor in exclude
|
||||
fun ofMainAccount(userWalletId: UserWalletId): CryptoPortfolioIcon {
|
||||
val colors = Color.entries
|
||||
val hash = userWalletId.value.fold(0) { acc, byte -> acc * HASH_MULTIPLIER + byte }
|
||||
|
||||
val color = if (isDefaultColorBusy) {
|
||||
val colorsWithExcluded = Color.entries - exclude
|
||||
val index = (hash and Int.MAX_VALUE) % colors.size
|
||||
val color = colors[index]
|
||||
|
||||
val availableColors = if (colorsWithExcluded.isNotEmpty()) {
|
||||
colorsWithExcluded
|
||||
} else {
|
||||
Color.entries
|
||||
}
|
||||
|
||||
availableColors.random()
|
||||
} else {
|
||||
defaultMainAccountColor
|
||||
}
|
||||
|
||||
return CryptoPortfolioIcon(
|
||||
type = Type.Icon(value = defaultMainAccountType),
|
||||
color = color,
|
||||
)
|
||||
return CryptoPortfolioIcon(value = defaultMainAccountIcon, color = color)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an [CryptoPortfolioIcon] for a user account based on the account name
|
||||
*
|
||||
* @param accountName the name of the account, used to determine the letter for the icon
|
||||
*/
|
||||
fun ofCustomAccount(accountName: String): CryptoPortfolioIcon {
|
||||
fun ofDefaultCustomAccount(): CryptoPortfolioIcon {
|
||||
val icon = (Icon.entries - excludedCustomAccountIcons).random()
|
||||
val color = Color.entries.random()
|
||||
|
||||
return CryptoPortfolioIcon(
|
||||
type = Type.Symbol(value = accountName.first()),
|
||||
color = color,
|
||||
)
|
||||
return CryptoPortfolioIcon(value = icon, color = color)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a [CryptoPortfolioIcon] for a user account with a specific type and color
|
||||
*
|
||||
* @param type the type of the account icon
|
||||
* @param value the icon of the account
|
||||
* @param color the color of the account icon
|
||||
*/
|
||||
fun ofCustomAccount(type: Type, color: Color): CryptoPortfolioIcon {
|
||||
return CryptoPortfolioIcon(type = type, color = color)
|
||||
fun ofCustomAccount(value: Icon, color: Color): CryptoPortfolioIcon {
|
||||
return CryptoPortfolioIcon(value = value, color = color)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
package com.tangem.domain.models.account
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.raise.either
|
||||
import arrow.core.raise.ensure
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Represents a derivation index for accounts, ensuring validity and providing utility methods
|
||||
*
|
||||
* @property value the integer value of the derivation index
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Serializable
|
||||
data class DerivationIndex private constructor(
|
||||
val value: Int,
|
||||
) {
|
||||
|
||||
/** Checks if the derivation index corresponds to the main account */
|
||||
val isMain: Boolean
|
||||
get() = value == MAIN_ACCOUNT_DERIVATION_INDEX
|
||||
|
||||
/**
|
||||
* Represents possible errors that can occur when creating a [DerivationIndex]
|
||||
*/
|
||||
@Serializable
|
||||
sealed interface Error {
|
||||
|
||||
/** Error indicating that the provided derivation index [derivationIndex] is invalid */
|
||||
@Serializable
|
||||
data class NegativeDerivationIndex(val derivationIndex: Int) : Error {
|
||||
override fun toString(): String {
|
||||
return "${this::class.simpleName}: Derivation index cannot be negative: $derivationIndex"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val MAIN_ACCOUNT_DERIVATION_INDEX = 0
|
||||
|
||||
/** Predefined instance of [DerivationIndex] for the main account */
|
||||
val Main: DerivationIndex = DerivationIndex(value = MAIN_ACCOUNT_DERIVATION_INDEX)
|
||||
|
||||
/**
|
||||
* Factory method to create a [DerivationIndex] instance
|
||||
*
|
||||
* @param value the integer value of the derivation index
|
||||
*
|
||||
* @return Either an error if the value is invalid, or a valid [DerivationIndex] instance
|
||||
*/
|
||||
operator fun invoke(value: Int): Either<Error, DerivationIndex> = either {
|
||||
ensure(value >= 0) { Error.NegativeDerivationIndex(derivationIndex = value) }
|
||||
|
||||
DerivationIndex(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
package com.tangem.domain.tokens.model
|
||||
package com.tangem.domain.models.currency
|
||||
|
||||
import com.tangem.domain.models.StatusSource
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.getResultStatusSource
|
||||
import com.tangem.domain.models.network.NetworkAddress
|
||||
import com.tangem.domain.models.network.TxInfo
|
||||
import com.tangem.domain.staking.model.stakekit.YieldBalance
|
||||
import java.math.BigDecimal
|
||||
import com.tangem.domain.models.serialization.SerializedBigDecimal
|
||||
import com.tangem.domain.models.staking.YieldBalance
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Represents the status of a cryptocurrency asset within a network.
|
||||
|
|
@ -18,6 +18,7 @@ import java.math.BigDecimal
|
|||
* @property currency The details of the cryptocurrency asset, including its type, name, symbol, and other properties.
|
||||
* @property value The current status of the cryptocurrency, reflecting its state within the network.
|
||||
*/
|
||||
@Serializable
|
||||
data class CryptoCurrencyStatus(
|
||||
val currency: CryptoCurrency,
|
||||
val value: Value,
|
||||
|
|
@ -28,36 +29,40 @@ data class CryptoCurrencyStatus(
|
|||
*
|
||||
* @property isError Indicates whether this status represents an error status.
|
||||
*/
|
||||
sealed class Value(val isError: Boolean) {
|
||||
@Serializable
|
||||
sealed interface Value {
|
||||
|
||||
val isError: Boolean
|
||||
|
||||
/** The amount of the cryptocurrency. */
|
||||
open val amount: BigDecimal? = null
|
||||
val amount: SerializedBigDecimal? get() = null
|
||||
|
||||
/** The fiat equivalent of the cryptocurrency's amount. */
|
||||
open val fiatAmount: BigDecimal? = null
|
||||
val fiatAmount: SerializedBigDecimal? get() = null
|
||||
|
||||
/** The exchange rate used for converting the cryptocurrency amount to fiat. */
|
||||
open val fiatRate: BigDecimal? = null
|
||||
val fiatRate: SerializedBigDecimal? get() = null
|
||||
|
||||
/** The change in price of the cryptocurrency. */
|
||||
open val priceChange: BigDecimal? = null
|
||||
val priceChange: SerializedBigDecimal? get() = null
|
||||
|
||||
/** Indicates if there are any transactions in progress related to the cryptocurrency network. */
|
||||
open val hasCurrentNetworkTransactions: Boolean = false
|
||||
val hasCurrentNetworkTransactions: Boolean get() = false
|
||||
|
||||
/** The pending cryptocurrency transactions. */
|
||||
open val pendingTransactions: Set<TxInfo> = emptySet()
|
||||
val pendingTransactions: Set<TxInfo> get() = emptySet()
|
||||
|
||||
/** The network address */
|
||||
open val networkAddress: NetworkAddress? = null
|
||||
val networkAddress: NetworkAddress? get() = null
|
||||
|
||||
/** Staking yield balance */
|
||||
open val yieldBalance: YieldBalance? = null
|
||||
val yieldBalance: YieldBalance? get() = null
|
||||
|
||||
/** Sources */
|
||||
open val sources: Sources = Sources()
|
||||
val sources: Sources get() = Sources()
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Sources(
|
||||
val networkSource: StatusSource = StatusSource.ACTUAL,
|
||||
val quoteSource: StatusSource = StatusSource.ACTUAL,
|
||||
|
|
@ -70,7 +75,11 @@ data class CryptoCurrencyStatus(
|
|||
}
|
||||
|
||||
/** Represents the Loading state of a cryptocurrency, typically while fetching its details. */
|
||||
data object Loading : Value(isError = false)
|
||||
@Serializable
|
||||
data object Loading : Value {
|
||||
|
||||
override val isError: Boolean = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a state where the cryptocurrency is not reachable.
|
||||
|
|
@ -79,23 +88,35 @@ data class CryptoCurrencyStatus(
|
|||
* @property fiatRate The exchange rate used for converting the cryptocurrency amount to fiat.
|
||||
* @property networkAddress The network address
|
||||
*/
|
||||
@Serializable
|
||||
data class Unreachable(
|
||||
override val priceChange: BigDecimal?,
|
||||
override val fiatRate: BigDecimal?,
|
||||
override val priceChange: SerializedBigDecimal?,
|
||||
override val fiatRate: SerializedBigDecimal?,
|
||||
override val networkAddress: NetworkAddress?,
|
||||
) : Value(isError = true)
|
||||
) : Value {
|
||||
|
||||
override val isError: Boolean = true
|
||||
}
|
||||
|
||||
/** Represents a state where the cryptocurrency's network amount not found. */
|
||||
@Serializable
|
||||
data class NoAmount(
|
||||
override val priceChange: BigDecimal?,
|
||||
override val fiatRate: BigDecimal?,
|
||||
) : Value(isError = true)
|
||||
override val priceChange: SerializedBigDecimal?,
|
||||
override val fiatRate: SerializedBigDecimal?,
|
||||
) : Value {
|
||||
|
||||
override val isError: Boolean = true
|
||||
}
|
||||
|
||||
/** Represents a state where the cryptocurrency's derivation is missed. */
|
||||
@Serializable
|
||||
data class MissedDerivation(
|
||||
override val priceChange: BigDecimal?,
|
||||
override val fiatRate: BigDecimal?,
|
||||
) : Value(isError = true)
|
||||
override val priceChange: SerializedBigDecimal?,
|
||||
override val fiatRate: SerializedBigDecimal?,
|
||||
) : Value {
|
||||
|
||||
override val isError: Boolean = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a state where there is no account associated with the cryptocurrency
|
||||
|
|
@ -103,16 +124,18 @@ data class CryptoCurrencyStatus(
|
|||
* @property amountToCreateAccount base reserve amount for account creation
|
||||
* @property sources sources of data
|
||||
*/
|
||||
@Serializable
|
||||
data class NoAccount(
|
||||
val amountToCreateAccount: BigDecimal,
|
||||
override val fiatAmount: BigDecimal?,
|
||||
override val priceChange: BigDecimal?,
|
||||
override val fiatRate: BigDecimal?,
|
||||
val amountToCreateAccount: SerializedBigDecimal,
|
||||
override val fiatAmount: SerializedBigDecimal?,
|
||||
override val priceChange: SerializedBigDecimal?,
|
||||
override val fiatRate: SerializedBigDecimal?,
|
||||
override val networkAddress: NetworkAddress,
|
||||
override val sources: Sources,
|
||||
) : Value(isError = false) {
|
||||
) : Value {
|
||||
|
||||
override val amount: BigDecimal = BigDecimal.ZERO
|
||||
override val isError: Boolean = false
|
||||
override val amount: SerializedBigDecimal? = SerializedBigDecimal.ZERO
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -127,17 +150,21 @@ data class CryptoCurrencyStatus(
|
|||
* @property pendingTransactions The current cryptocurrency transactions.
|
||||
* @property sources sources of data
|
||||
*/
|
||||
@Serializable
|
||||
data class Loaded(
|
||||
override val amount: BigDecimal,
|
||||
override val fiatAmount: BigDecimal,
|
||||
override val fiatRate: BigDecimal,
|
||||
override val priceChange: BigDecimal,
|
||||
override val amount: SerializedBigDecimal,
|
||||
override val fiatAmount: SerializedBigDecimal,
|
||||
override val fiatRate: SerializedBigDecimal,
|
||||
override val priceChange: SerializedBigDecimal,
|
||||
override val yieldBalance: YieldBalance?,
|
||||
override val hasCurrentNetworkTransactions: Boolean,
|
||||
override val pendingTransactions: Set<TxInfo>,
|
||||
override val networkAddress: NetworkAddress,
|
||||
override val sources: Sources,
|
||||
) : Value(isError = false)
|
||||
) : Value {
|
||||
|
||||
override val isError: Boolean = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a Custom state of a cryptocurrency, typically used for user-defined tokens.
|
||||
|
|
@ -150,17 +177,21 @@ data class CryptoCurrencyStatus(
|
|||
* cryptocurrency network.
|
||||
* @property pendingTransactions The current cryptocurrency transactions.
|
||||
*/
|
||||
@Serializable
|
||||
data class Custom(
|
||||
override val amount: BigDecimal,
|
||||
override val fiatAmount: BigDecimal?,
|
||||
override val fiatRate: BigDecimal?,
|
||||
override val priceChange: BigDecimal?,
|
||||
override val amount: SerializedBigDecimal,
|
||||
override val fiatAmount: SerializedBigDecimal?,
|
||||
override val fiatRate: SerializedBigDecimal?,
|
||||
override val priceChange: SerializedBigDecimal?,
|
||||
override val yieldBalance: YieldBalance?,
|
||||
override val hasCurrentNetworkTransactions: Boolean,
|
||||
override val pendingTransactions: Set<TxInfo>,
|
||||
override val networkAddress: NetworkAddress,
|
||||
override val sources: Sources,
|
||||
) : Value(isError = false)
|
||||
) : Value {
|
||||
|
||||
override val isError: Boolean = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a state where the cryptocurrency is available, but there is no current quote available for it.
|
||||
|
|
@ -170,12 +201,16 @@ data class CryptoCurrencyStatus(
|
|||
* cryptocurrency network.
|
||||
* @property pendingTransactions The current cryptocurrency transactions.
|
||||
*/
|
||||
@Serializable
|
||||
data class NoQuote(
|
||||
override val amount: BigDecimal,
|
||||
override val amount: SerializedBigDecimal,
|
||||
override val yieldBalance: YieldBalance?,
|
||||
override val hasCurrentNetworkTransactions: Boolean,
|
||||
override val pendingTransactions: Set<TxInfo>,
|
||||
override val networkAddress: NetworkAddress,
|
||||
override val sources: Sources,
|
||||
) : Value(isError = false)
|
||||
) : Value {
|
||||
|
||||
override val isError: Boolean = false
|
||||
}
|
||||
}
|
||||
|
|
@ -20,6 +20,7 @@ import kotlinx.serialization.Serializable
|
|||
* currency (for those blockchains that have FeeResource instead of a standard type of fee)
|
||||
* @property canHandleTokens indicates whether the network can handle tokens
|
||||
* @property transactionExtrasType the type of extras supported for sending a transaction
|
||||
* @property nameResolvingType the type of on-chain name resolution supported by the network (e.g., ENS, SNS etc)
|
||||
*/
|
||||
@Serializable
|
||||
data class Network(
|
||||
|
|
@ -33,6 +34,7 @@ data class Network(
|
|||
val hasFiatFeeRate: Boolean,
|
||||
val canHandleTokens: Boolean,
|
||||
val transactionExtrasType: TransactionExtrasType,
|
||||
val nameResolvingType: NameResolvingType,
|
||||
) {
|
||||
|
||||
/** Raw ID */
|
||||
|
|
@ -161,4 +163,16 @@ data class Network(
|
|||
-> true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the type of on-chain name resolution supported by the network.
|
||||
*/
|
||||
enum class NameResolvingType {
|
||||
|
||||
/** No name resolution supported */
|
||||
NONE,
|
||||
|
||||
/** Ethereum Name Service (ENS) */
|
||||
ENS,
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,9 @@
|
|||
package com.tangem.domain.models.network
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/** Represents a network address */
|
||||
@Serializable
|
||||
sealed class NetworkAddress {
|
||||
|
||||
/** The default or currently selected network address */
|
||||
|
|
@ -14,6 +17,7 @@ sealed class NetworkAddress {
|
|||
*
|
||||
* @property defaultAddress the static network address
|
||||
*/
|
||||
@Serializable
|
||||
data class Single(override val defaultAddress: Address) : NetworkAddress() {
|
||||
|
||||
override val availableAddresses: Set<Address> = setOf(defaultAddress)
|
||||
|
|
@ -25,6 +29,7 @@ sealed class NetworkAddress {
|
|||
* @property defaultAddress the currently selected or default network address
|
||||
* @property availableAddresses the set of available network addresses to choose from
|
||||
*/
|
||||
@Serializable
|
||||
data class Selectable(
|
||||
override val defaultAddress: Address,
|
||||
override val availableAddresses: Set<Address>,
|
||||
|
|
@ -41,6 +46,7 @@ sealed class NetworkAddress {
|
|||
* @property value string representation of the address
|
||||
* @property type address type
|
||||
*/
|
||||
@Serializable
|
||||
data class Address(
|
||||
val value: String,
|
||||
val type: Type,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.domain.models.network
|
||||
|
||||
import java.math.BigDecimal
|
||||
import com.tangem.domain.models.serialization.SerializedBigDecimal
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Represents information about a transaction. Do not use it for sending transactions.
|
||||
|
|
@ -15,6 +16,7 @@ import java.math.BigDecimal
|
|||
* @property type transaction type
|
||||
* @property amount transaction amount
|
||||
*/
|
||||
@Serializable
|
||||
data class TxInfo(
|
||||
val txHash: String,
|
||||
val timestampInMillis: Long,
|
||||
|
|
@ -24,10 +26,11 @@ data class TxInfo(
|
|||
val interactionAddressType: InteractionAddressType?,
|
||||
val status: TransactionStatus,
|
||||
val type: TransactionType,
|
||||
val amount: BigDecimal,
|
||||
val amount: SerializedBigDecimal,
|
||||
) {
|
||||
|
||||
/** Destination type*/
|
||||
@Serializable
|
||||
sealed class DestinationType {
|
||||
|
||||
/**
|
||||
|
|
@ -35,6 +38,7 @@ data class TxInfo(
|
|||
*
|
||||
* @property addressType address type
|
||||
*/
|
||||
@Serializable
|
||||
data class Single(val addressType: AddressType) : DestinationType()
|
||||
|
||||
/**
|
||||
|
|
@ -42,21 +46,29 @@ data class TxInfo(
|
|||
*
|
||||
* @property addressTypes addresses types
|
||||
*/
|
||||
@Serializable
|
||||
data class Multiple(val addressTypes: List<AddressType>) : DestinationType()
|
||||
}
|
||||
|
||||
/** Address type */
|
||||
@Serializable
|
||||
sealed class AddressType {
|
||||
|
||||
/** Address value */
|
||||
abstract val address: String
|
||||
|
||||
@Serializable
|
||||
data class User(override val address: String) : AddressType()
|
||||
|
||||
@Serializable
|
||||
data class Contract(override val address: String) : AddressType()
|
||||
|
||||
@Serializable
|
||||
data class Validator(override val address: String) : AddressType()
|
||||
}
|
||||
|
||||
/** Source type */
|
||||
@Serializable
|
||||
sealed class SourceType {
|
||||
|
||||
/**
|
||||
|
|
@ -64,6 +76,7 @@ data class TxInfo(
|
|||
*
|
||||
* @property address address
|
||||
*/
|
||||
@Serializable
|
||||
data class Single(val address: String) : SourceType()
|
||||
|
||||
/**
|
||||
|
|
@ -71,38 +84,79 @@ data class TxInfo(
|
|||
*
|
||||
* @property addresses addresses
|
||||
*/
|
||||
@Serializable
|
||||
data class Multiple(val addresses: List<String>) : SourceType()
|
||||
}
|
||||
|
||||
/** Transaction type */
|
||||
@Serializable
|
||||
sealed interface TransactionType {
|
||||
|
||||
@Serializable
|
||||
data object Transfer : TransactionType
|
||||
|
||||
@Serializable
|
||||
data object Approve : TransactionType
|
||||
|
||||
@Serializable
|
||||
data object Swap : TransactionType
|
||||
|
||||
@Serializable
|
||||
data object UnknownOperation : TransactionType
|
||||
|
||||
@Serializable
|
||||
data class Operation(val name: String) : TransactionType
|
||||
|
||||
@Serializable
|
||||
sealed interface Staking : TransactionType {
|
||||
|
||||
@Serializable
|
||||
data class Vote(val validatorAddress: String) : Staking
|
||||
|
||||
@Serializable
|
||||
data object ClaimRewards : Staking
|
||||
|
||||
@Serializable
|
||||
data object Stake : Staking
|
||||
|
||||
@Serializable
|
||||
data object Unstake : Staking
|
||||
|
||||
@Serializable
|
||||
data object Withdraw : Staking
|
||||
|
||||
@Serializable
|
||||
data object Restake : Staking
|
||||
}
|
||||
}
|
||||
|
||||
/** Transaction status */
|
||||
@Serializable
|
||||
sealed class TransactionStatus {
|
||||
|
||||
@Serializable
|
||||
data object Failed : TransactionStatus()
|
||||
|
||||
@Serializable
|
||||
data object Unconfirmed : TransactionStatus()
|
||||
|
||||
@Serializable
|
||||
data object Confirmed : TransactionStatus()
|
||||
}
|
||||
|
||||
@Serializable
|
||||
sealed class InteractionAddressType {
|
||||
|
||||
@Serializable
|
||||
data class Validator(val address: String) : InteractionAddressType()
|
||||
|
||||
@Serializable
|
||||
data class User(val address: String) : InteractionAddressType()
|
||||
|
||||
@Serializable
|
||||
data class Contract(val address: String) : InteractionAddressType()
|
||||
|
||||
@Serializable
|
||||
data class Multiple(val addresses: List<String>) : InteractionAddressType()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.domain.core.serialization
|
||||
package com.tangem.domain.models.serialization
|
||||
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.descriptors.PrimitiveKind
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.domain.core.serialization
|
||||
package com.tangem.domain.models.serialization
|
||||
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.descriptors.PrimitiveKind
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.domain.core.serialization
|
||||
package com.tangem.domain.models.serialization
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.math.BigDecimal
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.domain.core.serialization
|
||||
package com.tangem.domain.models.serialization
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.math.BigInteger
|
||||
|
|
@ -1,5 +1,8 @@
|
|||
package com.tangem.domain.staking.model.stakekit
|
||||
package com.tangem.domain.models.staking
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
enum class NetworkType {
|
||||
AVALANCHE_C,
|
||||
AVALANCHE_ATOMIC,
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem.domain.models.staking
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class StakingID(val integrationId: String, val address: String)
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
package com.tangem.domain.models.staking
|
||||
|
||||
import com.tangem.domain.models.StatusSource
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Represents a yield balance in the staking system
|
||||
*/
|
||||
@Serializable
|
||||
sealed interface YieldBalance {
|
||||
|
||||
/** The unique identifier of the staking operation */
|
||||
val stakingId: StakingID
|
||||
|
||||
/** The source of the status information */
|
||||
val source: StatusSource
|
||||
|
||||
/**
|
||||
* Represents a yield balance with actual data
|
||||
*
|
||||
* @property stakingId the unique identifier of the staking operation
|
||||
* @property source the source of the status information
|
||||
* @property balance the balance details of the yield
|
||||
*/
|
||||
@Serializable
|
||||
data class Data(
|
||||
override val stakingId: StakingID,
|
||||
override val source: StatusSource,
|
||||
val balance: YieldBalanceItem,
|
||||
) : YieldBalance
|
||||
|
||||
/**
|
||||
* Represents an empty yield balance
|
||||
*
|
||||
* @property stakingId the unique identifier of the staking operation
|
||||
* @property source the source of the status information
|
||||
*/
|
||||
@Serializable
|
||||
data class Empty(
|
||||
override val stakingId: StakingID,
|
||||
override val source: StatusSource,
|
||||
) : YieldBalance
|
||||
|
||||
/**
|
||||
* Represents an error state for the yield balance
|
||||
*
|
||||
* @property stakingId the unique identifier of the staking operation
|
||||
*/
|
||||
@Serializable
|
||||
data class Error(override val stakingId: StakingID) : YieldBalance {
|
||||
override val source: StatusSource = StatusSource.ACTUAL
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a copy of the current yield balance with a new status source
|
||||
*
|
||||
* @param source the new source of the status information
|
||||
*/
|
||||
fun copySealed(source: StatusSource): YieldBalance {
|
||||
return when (this) {
|
||||
is Data -> copy(source = source)
|
||||
is Empty -> copy(source = source)
|
||||
is Error,
|
||||
-> this
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,88 +1,44 @@
|
|||
package com.tangem.domain.staking.model.stakekit
|
||||
package com.tangem.domain.models.staking
|
||||
|
||||
import com.tangem.domain.models.StatusSource
|
||||
import com.tangem.domain.staking.model.StakingID
|
||||
import com.tangem.domain.staking.model.stakekit.action.StakingActionType
|
||||
import org.joda.time.DateTime
|
||||
import java.math.BigDecimal
|
||||
|
||||
sealed class YieldBalance {
|
||||
|
||||
abstract val integrationId: String?
|
||||
abstract val address: String?
|
||||
abstract val source: StatusSource
|
||||
|
||||
fun copySealed(source: StatusSource): YieldBalance {
|
||||
return when (this) {
|
||||
is Data -> copy(source = source)
|
||||
is Empty -> copy(source = source)
|
||||
is Error,
|
||||
is Unsupported,
|
||||
-> this
|
||||
}
|
||||
}
|
||||
|
||||
fun getStakingId(): StakingID? {
|
||||
val integrationId = integrationId
|
||||
val address = address
|
||||
|
||||
if (integrationId == null || address == null) return null
|
||||
|
||||
return StakingID(integrationId = integrationId, address = address)
|
||||
}
|
||||
|
||||
data class Data(
|
||||
override val integrationId: String?,
|
||||
override val address: String,
|
||||
override val source: StatusSource,
|
||||
val balance: YieldBalanceItem,
|
||||
) : YieldBalance()
|
||||
|
||||
data class Empty(
|
||||
override val integrationId: String?,
|
||||
override val address: String,
|
||||
override val source: StatusSource,
|
||||
) : YieldBalance()
|
||||
|
||||
data object Unsupported : YieldBalance() {
|
||||
override val integrationId: String? = null
|
||||
override val address: String? = null
|
||||
override val source: StatusSource = StatusSource.ACTUAL
|
||||
}
|
||||
|
||||
data class Error(override val integrationId: String?, override val address: String?) : YieldBalance() {
|
||||
override val source: StatusSource = StatusSource.ACTUAL
|
||||
}
|
||||
}
|
||||
import com.tangem.domain.models.serialization.SerializedBigDecimal
|
||||
import com.tangem.domain.models.staking.action.StakingActionType
|
||||
import kotlinx.datetime.Instant
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class YieldBalanceItem(
|
||||
val items: List<BalanceItem>,
|
||||
val integrationId: String?,
|
||||
val integrationId: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class BalanceItem(
|
||||
val groupId: String,
|
||||
val token: Token,
|
||||
val token: YieldToken,
|
||||
val type: BalanceType,
|
||||
val amount: BigDecimal,
|
||||
val amount: SerializedBigDecimal,
|
||||
val rawCurrencyId: String?,
|
||||
val validatorAddress: String?,
|
||||
val date: DateTime?,
|
||||
val date: Instant?,
|
||||
val pendingActions: List<PendingAction>,
|
||||
val pendingActionsConstraints: List<PendingActionConstraints>,
|
||||
val isPending: Boolean,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PendingActionConstraints(
|
||||
val type: StakingActionType,
|
||||
val amountArg: PendingAction.PendingActionArgs.Amount?,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PendingAction(
|
||||
val type: StakingActionType,
|
||||
val passthrough: String,
|
||||
val args: PendingActionArgs?,
|
||||
) {
|
||||
|
||||
@Serializable
|
||||
data class PendingActionArgs(
|
||||
val amount: Amount?,
|
||||
val duration: Duration?,
|
||||
|
|
@ -91,18 +47,22 @@ data class PendingAction(
|
|||
val tronResource: TronResource?,
|
||||
val signatureVerification: Boolean?,
|
||||
) {
|
||||
|
||||
@Serializable
|
||||
data class Amount(
|
||||
val required: Boolean,
|
||||
val minimum: BigDecimal?,
|
||||
val maximum: BigDecimal?,
|
||||
val minimum: SerializedBigDecimal?,
|
||||
val maximum: SerializedBigDecimal?,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Duration(
|
||||
val required: Boolean,
|
||||
val minimum: Int?,
|
||||
val maximum: Int?,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class TronResource(
|
||||
val required: Boolean,
|
||||
val options: List<String>,
|
||||
|
|
@ -114,6 +74,7 @@ data class PendingAction(
|
|||
* IMPORTANT!!!
|
||||
* Order is used to sort balances.
|
||||
*/
|
||||
@Serializable
|
||||
@Suppress("MagicNumber")
|
||||
enum class BalanceType(val order: Int) {
|
||||
AVAILABLE(1),
|
||||
|
|
@ -128,6 +89,7 @@ enum class BalanceType(val order: Int) {
|
|||
;
|
||||
|
||||
companion object {
|
||||
|
||||
fun BalanceType.isClickable() = when (this) {
|
||||
STAKED,
|
||||
UNSTAKED,
|
||||
|
|
@ -144,6 +106,7 @@ enum class BalanceType(val order: Int) {
|
|||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
enum class RewardBlockType {
|
||||
NoRewards,
|
||||
Rewards,
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.domain.models.staking
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class YieldToken(
|
||||
val name: String,
|
||||
val network: NetworkType,
|
||||
val symbol: String,
|
||||
val decimals: Int,
|
||||
val address: String?,
|
||||
val coinGeckoId: String?,
|
||||
val logoURI: String?,
|
||||
val isPoints: Boolean?,
|
||||
)
|
||||
|
|
@ -1,5 +1,8 @@
|
|||
package com.tangem.domain.staking.model.stakekit.action
|
||||
package com.tangem.domain.models.staking.action
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
enum class StakingActionType {
|
||||
STAKE,
|
||||
UNSTAKE,
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
package com.tangem.domain.models.tokenlist
|
||||
|
||||
import com.tangem.domain.models.StatusSource
|
||||
import com.tangem.domain.models.TokensSortType
|
||||
import com.tangem.domain.models.TotalFiatBalance
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.serialization.SerializedBigDecimal
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Represents a list of cryptocurrency tokens, which can be grouped by network or ungrouped.
|
||||
*
|
||||
* The tokens can be represented in two forms: either grouped by the network or as an ungrouped collection.
|
||||
* Additional details like the total fiat balance and the sorting type can be associated with the list.
|
||||
*/
|
||||
@Serializable
|
||||
sealed interface TokenList {
|
||||
|
||||
/** The total fiat balance across all tokens */
|
||||
val totalFiatBalance: TotalFiatBalance
|
||||
|
||||
/** The criteria used for sorting the tokens */
|
||||
val sortedBy: TokensSortType
|
||||
|
||||
/**
|
||||
* Represents tokens that are grouped by their network
|
||||
*
|
||||
* @property totalFiatBalance the total fiat balance across all groups
|
||||
* @property sortedBy the criteria used for sorting the tokens within the groups
|
||||
* @property groups a list of network groups containing tokens
|
||||
*/
|
||||
@Serializable
|
||||
data class GroupedByNetwork(
|
||||
override val totalFiatBalance: TotalFiatBalance,
|
||||
override val sortedBy: TokensSortType,
|
||||
val groups: List<NetworkGroup>,
|
||||
) : TokenList {
|
||||
|
||||
/**
|
||||
* Represents a group of cryptocurrencies associated with a specific network
|
||||
*
|
||||
* @property network the blockchain network associated with the group
|
||||
* @property currencies a list of cryptocurrency statuses that belong to the network
|
||||
*/
|
||||
@Serializable
|
||||
data class NetworkGroup(
|
||||
val network: Network,
|
||||
val currencies: List<CryptoCurrencyStatus>,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents tokens that are not grouped by any specific criteria.
|
||||
*
|
||||
* @property totalFiatBalance the total fiat balance across all groups
|
||||
* @property sortedBy the criteria used for sorting the tokens within the groups
|
||||
* @property currencies a list of cryptocurrency statuses
|
||||
*/
|
||||
@Serializable
|
||||
data class Ungrouped(
|
||||
override val totalFiatBalance: TotalFiatBalance,
|
||||
override val sortedBy: TokensSortType,
|
||||
val currencies: List<CryptoCurrencyStatus>,
|
||||
) : TokenList
|
||||
|
||||
/** Represents a state where the token list is empty */
|
||||
@Serializable
|
||||
data object Empty : TokenList {
|
||||
|
||||
override val totalFiatBalance: TotalFiatBalance = TotalFiatBalance.Loaded(
|
||||
amount = SerializedBigDecimal.ZERO,
|
||||
isAllAmountsSummarized = true,
|
||||
source = StatusSource.ACTUAL,
|
||||
)
|
||||
|
||||
override val sortedBy: TokensSortType = TokensSortType.NONE
|
||||
}
|
||||
|
||||
/** Get flatten list of cryptocurrency status [CryptoCurrencyStatus] */
|
||||
fun flattenCurrencies(): List<CryptoCurrencyStatus> {
|
||||
return when (this) {
|
||||
is GroupedByNetwork -> groups.flatMap(GroupedByNetwork.NetworkGroup::currencies)
|
||||
is Ungrouped -> currencies
|
||||
is Empty -> emptyList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
package com.tangem.domain.models.account
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
import org.junit.jupiter.params.provider.MethodSource
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
class AccountIdTest {
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("provideTestModels")
|
||||
fun forCryptoPortfolio(model: ForCryptoPortfolioModel) {
|
||||
// Arrange
|
||||
val userWalletId = UserWalletId("27163F47405CE73110837F24DF82607FF11C7AF9D78C93F409E4FEAFF3400C8F")
|
||||
|
||||
// Act
|
||||
val actual = AccountId.forCryptoPortfolio(userWalletId = userWalletId, derivationIndex = model.derivationIndex)
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual.value).isEqualTo(model.expected)
|
||||
}
|
||||
|
||||
private fun provideTestModels() = listOf(
|
||||
ForCryptoPortfolioModel(
|
||||
derivationIndex = DerivationIndex.Main,
|
||||
expected = "4E39B13EA11E3B35339664A10BEF48F4AF752A1CC2200F79D23CB0FB3396C63F",
|
||||
),
|
||||
ForCryptoPortfolioModel(
|
||||
derivationIndex = DerivationIndex(1).getOrNull()!!,
|
||||
expected = "7F22E71F8106783F0F2DAFCDE525E2F2A2281E864DDBE2FE668FA09329D563A2",
|
||||
),
|
||||
ForCryptoPortfolioModel(
|
||||
derivationIndex = DerivationIndex(42).getOrNull()!!,
|
||||
expected = "555C1E17A302659446C97393453B7C2B3246AF4DA082C56C28FB6EDD1A6606A4",
|
||||
),
|
||||
)
|
||||
|
||||
data class ForCryptoPortfolioModel(
|
||||
val derivationIndex: DerivationIndex,
|
||||
val expected: String,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
package com.tangem.domain.models.account
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.left
|
||||
import com.google.common.truth.Truth
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
import org.junit.jupiter.params.provider.MethodSource
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
class AccountNameTest {
|
||||
|
||||
@Test
|
||||
fun main_returnsMainAccountName() {
|
||||
// Act
|
||||
val main = AccountName.Main.value
|
||||
|
||||
// Assert
|
||||
val expected = "Main Account"
|
||||
Truth.assertThat(main).isEqualTo(expected)
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("provideTestModels")
|
||||
fun invoke(model: InvokeTestModel) {
|
||||
// Act
|
||||
val actual = AccountName(value = model.value)
|
||||
|
||||
// Assert
|
||||
actual
|
||||
.onRight {
|
||||
val expected = model.expected.getOrNull()!!
|
||||
Truth.assertThat(it).isEqualTo(expected)
|
||||
}
|
||||
.onLeft {
|
||||
val expected = model.expected.leftOrNull()!!
|
||||
Truth.assertThat(it).isEqualTo(expected)
|
||||
}
|
||||
}
|
||||
|
||||
private fun provideTestModels() = listOf(
|
||||
InvokeTestModel(
|
||||
value = "",
|
||||
expected = AccountName.Error.Empty.left(),
|
||||
),
|
||||
InvokeTestModel(
|
||||
value = " ",
|
||||
expected = AccountName.Error.Empty.left(),
|
||||
),
|
||||
InvokeTestModel(
|
||||
value = "a".repeat(21),
|
||||
expected = AccountName.Error.ExceedsMaxLength.left(),
|
||||
),
|
||||
"a".repeat(20).let { value ->
|
||||
InvokeTestModel(
|
||||
value = value,
|
||||
expected = AccountName(value = value),
|
||||
)
|
||||
},
|
||||
InvokeTestModel(
|
||||
value = " name ",
|
||||
expected = AccountName(value = "name"),
|
||||
),
|
||||
InvokeTestModel(
|
||||
value = "Main Account",
|
||||
expected = AccountName(value = "Main Account"),
|
||||
),
|
||||
)
|
||||
|
||||
data class InvokeTestModel(
|
||||
val value: String,
|
||||
val expected: Either<AccountName.Error, AccountName>,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,196 @@
|
|||
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
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import org.junit.jupiter.api.Nested
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
class AccountTest {
|
||||
|
||||
@Test
|
||||
fun `Account userWalletId`() {
|
||||
// Arrange
|
||||
val userWalletId = UserWalletId("011")
|
||||
|
||||
// Act
|
||||
val actual = createCryptoPortfolioStub(userWalletId = userWalletId).userWalletId
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).isEqualTo(userWalletId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `CryptoPortfolio isMainAccount`() {
|
||||
// Arrange
|
||||
val derivationIndex0 = 0
|
||||
val derivationIndex1 = 1
|
||||
|
||||
// Act
|
||||
val actual1 = createCryptoPortfolioStub(derivationIndex = derivationIndex0)
|
||||
.isMainAccount
|
||||
|
||||
val actual2 = createCryptoPortfolioStub(derivationIndex = derivationIndex1)
|
||||
.isMainAccount
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual1).isTrue()
|
||||
Truth.assertThat(actual2).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `CryptoPortfolio tokensCount`() {
|
||||
// Arrange
|
||||
val emptyCurrencies = emptySet<CryptoCurrency>()
|
||||
val filledCurrencies = setOf(mockk<CryptoCurrency>())
|
||||
|
||||
// Act
|
||||
val actual1 = createCryptoPortfolioStub(currencies = emptyCurrencies)
|
||||
.tokensCount
|
||||
|
||||
val actual2 = createCryptoPortfolioStub(currencies = filledCurrencies)
|
||||
.tokensCount
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual1).isEqualTo(0)
|
||||
Truth.assertThat(actual2).isEqualTo(1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `CryptoPortfolio networksCount`() {
|
||||
// Arrange
|
||||
val emptyCurrencies = emptySet<CryptoCurrency>()
|
||||
val filledCurrencies = setOf(
|
||||
mockk<CryptoCurrency> {
|
||||
every { network } returns mockk()
|
||||
},
|
||||
)
|
||||
|
||||
// Act
|
||||
val actual1 = createCryptoPortfolioStub(currencies = emptyCurrencies)
|
||||
.networksCount
|
||||
|
||||
val actual2 = createCryptoPortfolioStub(currencies = filledCurrencies)
|
||||
.networksCount
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual1).isEqualTo(0)
|
||||
Truth.assertThat(actual2).isEqualTo(1)
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class CreateCryptoPortfolio {
|
||||
|
||||
@Test
|
||||
fun `invoke returns AccountNameError`() {
|
||||
// Arrange
|
||||
val name = ""
|
||||
|
||||
// Act
|
||||
val actual = CryptoPortfolio(
|
||||
accountId = mockk(),
|
||||
name = name,
|
||||
accountIcon = mockk(),
|
||||
derivationIndex = 0,
|
||||
isArchived = false,
|
||||
cryptoCurrencyList = mockk(),
|
||||
)
|
||||
.leftOrNull()!!
|
||||
|
||||
// Assert
|
||||
val expected = AccountNameError(cause = AccountName.Error.Empty)
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke returns CryptoPortfolio`() {
|
||||
// Act
|
||||
val derivationIndex = DerivationIndex.Main
|
||||
val actual = CryptoPortfolio(
|
||||
accountId = AccountId.forCryptoPortfolio(
|
||||
userWalletId = UserWalletId("011"),
|
||||
derivationIndex = derivationIndex,
|
||||
),
|
||||
name = "Test Account",
|
||||
accountIcon = CryptoPortfolioIcon.ofMainAccount(userWalletId = UserWalletId("011")),
|
||||
derivationIndex = derivationIndex.value,
|
||||
isArchived = false,
|
||||
cryptoCurrencyList = CryptoCurrencyList(
|
||||
currencies = emptySet(),
|
||||
sortType = TokensSortType.NONE,
|
||||
groupType = TokensGroupType.NONE,
|
||||
),
|
||||
)
|
||||
.getOrNull()!!
|
||||
|
||||
// Assert
|
||||
val expected = createCryptoPortfolioStub()
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun createMainAccount() {
|
||||
// Arrange
|
||||
val userWalletId = UserWalletId("011")
|
||||
val derivationIndex = DerivationIndex.Main
|
||||
|
||||
// Act
|
||||
val actual = CryptoPortfolio.createMainAccount(userWalletId = userWalletId)
|
||||
|
||||
// Assert
|
||||
val expected = CryptoPortfolio(
|
||||
accountId = AccountId.forCryptoPortfolio(
|
||||
userWalletId = userWalletId,
|
||||
derivationIndex = derivationIndex,
|
||||
),
|
||||
accountName = AccountName.Main,
|
||||
accountIcon = CryptoPortfolioIcon.ofMainAccount(userWalletId),
|
||||
derivationIndex = derivationIndex,
|
||||
isArchived = false,
|
||||
cryptoCurrencyList = CryptoCurrencyList(
|
||||
currencies = emptySet(),
|
||||
sortType = TokensSortType.NONE,
|
||||
groupType = TokensGroupType.NONE,
|
||||
),
|
||||
)
|
||||
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createCryptoPortfolioStub(
|
||||
userWalletId: UserWalletId = UserWalletId("011"),
|
||||
name: String = "Test Account",
|
||||
derivationIndex: Int = 0,
|
||||
currencies: Set<CryptoCurrency> = emptySet(),
|
||||
): CryptoPortfolio {
|
||||
val accountIndex = DerivationIndex(value = derivationIndex).getOrNull()!!
|
||||
|
||||
return CryptoPortfolio.invoke(
|
||||
accountId = AccountId.forCryptoPortfolio(userWalletId = userWalletId, derivationIndex = accountIndex),
|
||||
name = name,
|
||||
accountIcon = CryptoPortfolioIcon.ofMainAccount(userWalletId),
|
||||
derivationIndex = derivationIndex,
|
||||
isArchived = false,
|
||||
cryptoCurrencyList = CryptoCurrencyList(
|
||||
currencies = currencies,
|
||||
sortType = TokensSortType.NONE,
|
||||
groupType = TokensGroupType.NONE,
|
||||
),
|
||||
)
|
||||
.getOrNull()!!
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +1,14 @@
|
|||
package com.tangem.domain.models.account
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.domain.models.account.CryptoPortfolioIcon.*
|
||||
import com.tangem.domain.models.account.CryptoPortfolioIcon.Color
|
||||
import com.tangem.domain.models.account.CryptoPortfolioIcon.Icon
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import io.mockk.every
|
||||
import io.mockk.mockkObject
|
||||
import io.mockk.unmockkObject
|
||||
import io.mockk.verify
|
||||
import io.mockk.verifyOrder
|
||||
import org.junit.jupiter.api.Nested
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
import org.junit.jupiter.params.provider.MethodSource
|
||||
|
|
@ -23,159 +24,160 @@ class CryptoPortfolioIconTest {
|
|||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class OfMainAccount {
|
||||
|
||||
@Test
|
||||
fun `ofMainAccount with empty exclude`() {
|
||||
// Act
|
||||
val actual = CryptoPortfolioIcon.ofMainAccount(exclude = emptySet())
|
||||
|
||||
// Assert
|
||||
val expectedColor = Color.Azure
|
||||
Truth.assertThat(actual.color).isEqualTo(expectedColor)
|
||||
|
||||
val expectedType = Type.Icon(value = Icon.Star)
|
||||
Truth.assertThat(actual.type).isEqualTo(expectedType)
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("provideTestModels")
|
||||
fun ofMainAccount(model: OfMainAccountModel) {
|
||||
// Arrange
|
||||
mockkObject(Random.Default)
|
||||
|
||||
val size = (Color.entries.size - model.exclude.size).takeIf { it > 0 } ?: Color.entries.size
|
||||
every { Random.nextInt(size) } returns model.randomNextInt
|
||||
|
||||
// Act
|
||||
val actual = CryptoPortfolioIcon.ofMainAccount(exclude = model.exclude)
|
||||
val actual = CryptoPortfolioIcon.ofMainAccount(userWalletId = model.userWalletId)
|
||||
|
||||
// Assert
|
||||
val expectedColor = model.expectedColor
|
||||
Truth.assertThat(actual.color).isEqualTo(expectedColor)
|
||||
|
||||
val expectedType = Type.Icon(value = Icon.Star)
|
||||
Truth.assertThat(actual.type).isEqualTo(expectedType)
|
||||
|
||||
verify(exactly = 1) { Random.nextInt(size) }
|
||||
|
||||
unmockkObject(Random.Default)
|
||||
val expectedIcon = Icon.Star
|
||||
Truth.assertThat(actual.value).isEqualTo(expectedIcon)
|
||||
}
|
||||
|
||||
private fun provideTestModels() = listOf(
|
||||
// If the default color is already occupied (present in the exclude set), a random color from the
|
||||
// remaining available colors will be selected for the main account icon.
|
||||
OfMainAccountModel(
|
||||
exclude = setOf(Color.Azure),
|
||||
randomNextInt = 0,
|
||||
expectedColor = Color.entries[1],
|
||||
userWalletId = UserWalletId("1234567890abcdef"),
|
||||
expectedColor = Color.Pattypan,
|
||||
),
|
||||
OfMainAccountModel(
|
||||
exclude = setOf(Color.Azure, Color.CaribbeanBlue),
|
||||
randomNextInt = 0,
|
||||
expectedColor = Color.entries[2],
|
||||
userWalletId = UserWalletId("27163F47405CE73110837F24DF82607FF11C7AF9D78C93F409E4FEAFF3400C8F"),
|
||||
expectedColor = Color.CandyGrapeFizz,
|
||||
),
|
||||
// If all colors are already occupied, a random one will be selected.
|
||||
OfMainAccountModel(
|
||||
exclude = Color.entries.toSet(),
|
||||
randomNextInt = 1,
|
||||
expectedColor = Color.entries[1],
|
||||
userWalletId = UserWalletId("64A3791C180584C700EBECD6EAB36CBC34643BB449BC87761104C09F41DBCF3D"),
|
||||
expectedColor = Color.PalatinateBlue,
|
||||
),
|
||||
OfMainAccountModel(
|
||||
userWalletId = UserWalletId("01C061A99FCCEDA87933267EBAB3513592F83AD2E27BDA6EE5546BA96009D21F"),
|
||||
expectedColor = Color.Pelati,
|
||||
),
|
||||
OfMainAccountModel(
|
||||
userWalletId = UserWalletId("6D387A8FA5D2AF95F601EBCA8736D73D2ED53159835D8C407FBD4BBB10290C8B"),
|
||||
expectedColor = Color.CaribbeanBlue,
|
||||
),
|
||||
OfMainAccountModel(
|
||||
userWalletId = UserWalletId("33FCD9B9982C31648C235AE55A29212D567ECD3BA24BE4227D1A01897ADBC959"),
|
||||
expectedColor = Color.SweetDesire,
|
||||
),
|
||||
OfMainAccountModel(
|
||||
userWalletId = UserWalletId("197C8C5AA59270F3E9E1F30799A007D193DA596E6DC24C37D002C2EC203C2A0B"),
|
||||
expectedColor = Color.VitalGreen,
|
||||
),
|
||||
OfMainAccountModel(
|
||||
userWalletId = UserWalletId("ACF90C18393828958B5E795771F0692A00D3D7ADC092F726AB4A7E3116DD6E6E"),
|
||||
expectedColor = Color.Pattypan,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
data class OfMainAccountModel(
|
||||
val exclude: Set<Color>,
|
||||
val randomNextInt: Int,
|
||||
val userWalletId: UserWalletId,
|
||||
val expectedColor: Color,
|
||||
)
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class OfCustomAccountBasedOnName {
|
||||
inner class OfDefaultCustomAccount {
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("provideTestModels")
|
||||
fun ofCustomAccount(model: OfCustomAccountModel.BasedOnName) {
|
||||
fun ofCustomAccount(model: OfDefaultCustomAccountModel) {
|
||||
// Arrange
|
||||
val availableIcons = Icon.entries - setOf(Icon.Letter, Icon.Star)
|
||||
|
||||
mockkObject(Random.Default)
|
||||
|
||||
every { Random.nextInt(until = Color.entries.size) } returns model.randomNextInt
|
||||
every { Random.nextInt(until = availableIcons.size) } returns model.randomIconIndex
|
||||
every { Random.nextInt(until = Color.entries.size) } returns model.randomColorIndex
|
||||
|
||||
// Act
|
||||
val actual = CryptoPortfolioIcon.ofCustomAccount(accountName = model.accountName)
|
||||
val actual = CryptoPortfolioIcon.ofDefaultCustomAccount()
|
||||
|
||||
// Assert
|
||||
val expectedColor = model.expectedColor
|
||||
Truth.assertThat(actual.color).isEqualTo(expectedColor)
|
||||
val expected = model.expected
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
|
||||
val expectedType = Type.Symbol(value = model.accountName.first())
|
||||
Truth.assertThat(actual.type).isEqualTo(expectedType)
|
||||
|
||||
verify(exactly = 1) { Random.nextInt(until = Color.entries.size) }
|
||||
verifyOrder {
|
||||
Random.nextInt(until = availableIcons.size)
|
||||
Random.nextInt(until = Color.entries.size)
|
||||
}
|
||||
|
||||
unmockkObject(Random.Default)
|
||||
}
|
||||
|
||||
private fun provideTestModels() = listOf(
|
||||
OfCustomAccountModel.BasedOnName(
|
||||
accountName = "New account",
|
||||
randomNextInt = 0,
|
||||
expectedColor = Color.entries[0],
|
||||
OfDefaultCustomAccountModel(
|
||||
randomIconIndex = 0,
|
||||
randomColorIndex = 0,
|
||||
expected = CryptoPortfolioIcon.ofCustomAccount(value = Icon.User, color = Color.Azure),
|
||||
),
|
||||
OfCustomAccountModel.BasedOnName(
|
||||
accountName = "Awesome",
|
||||
randomNextInt = Color.entries.lastIndex,
|
||||
expectedColor = Color.entries.last(),
|
||||
OfDefaultCustomAccountModel(
|
||||
randomIconIndex = 1,
|
||||
randomColorIndex = 1,
|
||||
expected = CryptoPortfolioIcon.ofCustomAccount(value = Icon.Family, color = Color.CaribbeanBlue),
|
||||
),
|
||||
OfDefaultCustomAccountModel(
|
||||
randomIconIndex = Icon.entries.lastIndex - 2,
|
||||
randomColorIndex = Color.entries.lastIndex,
|
||||
expected = CryptoPortfolioIcon.ofCustomAccount(value = Icon.Gift, color = Color.VitalGreen),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
data class OfDefaultCustomAccountModel(
|
||||
val randomIconIndex: Int,
|
||||
val randomColorIndex: Int,
|
||||
val expected: CryptoPortfolioIcon,
|
||||
)
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class OfCustomAccountWithTypeAndColor {
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("provideTestModels")
|
||||
fun ofCustomAccount(model: OfCustomAccountModel.WithTypeAndColor) {
|
||||
fun ofCustomAccount(model: OfCustomAccountModel) {
|
||||
// Act
|
||||
val actual = CryptoPortfolioIcon.ofCustomAccount(type = model.type, color = model.color)
|
||||
val actual = CryptoPortfolioIcon.ofCustomAccount(value = model.icon, color = model.color)
|
||||
|
||||
// Assert
|
||||
val expectedColor = model.expectedColor
|
||||
Truth.assertThat(actual.color).isEqualTo(expectedColor)
|
||||
|
||||
val expectedType = model.expectedType
|
||||
Truth.assertThat(actual.type).isEqualTo(expectedType)
|
||||
Truth.assertThat(actual.value).isEqualTo(expectedType)
|
||||
}
|
||||
|
||||
private fun provideTestModels() = listOf(
|
||||
OfCustomAccountModel.WithTypeAndColor(
|
||||
type = Type.Icon(value = Icon.User),
|
||||
OfCustomAccountModel(
|
||||
icon = Icon.User,
|
||||
color = Color.CaribbeanBlue,
|
||||
expectedType = Type.Icon(value = Icon.User),
|
||||
expectedType = Icon.User,
|
||||
expectedColor = Color.CaribbeanBlue,
|
||||
),
|
||||
OfCustomAccountModel.WithTypeAndColor(
|
||||
type = Type.Symbol(value = 'A'),
|
||||
OfCustomAccountModel(
|
||||
icon = Icon.Letter,
|
||||
color = Color.DullLavender,
|
||||
expectedType = Type.Symbol(value = 'A'),
|
||||
expectedType = Icon.Letter,
|
||||
expectedColor = Color.DullLavender,
|
||||
),
|
||||
OfCustomAccountModel(
|
||||
icon = Icon.Star,
|
||||
color = Color.DullLavender,
|
||||
expectedType = Icon.Star,
|
||||
expectedColor = Color.DullLavender,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
sealed interface OfCustomAccountModel {
|
||||
|
||||
data class BasedOnName(
|
||||
val accountName: String,
|
||||
val randomNextInt: Int,
|
||||
val expectedColor: Color,
|
||||
) : OfCustomAccountModel
|
||||
|
||||
data class WithTypeAndColor(
|
||||
val type: Type,
|
||||
val color: Color,
|
||||
val expectedType: Type,
|
||||
val expectedColor: Color,
|
||||
) : OfCustomAccountModel
|
||||
}
|
||||
data class OfCustomAccountModel(
|
||||
val icon: Icon,
|
||||
val color: Color,
|
||||
val expectedType: Icon,
|
||||
val expectedColor: Color,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
package com.tangem.domain.models.account
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
import com.google.common.truth.Truth
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
import org.junit.jupiter.params.provider.MethodSource
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
class DerivationIndexTest {
|
||||
|
||||
@Test
|
||||
fun `isMain returns true only for main derivation index`() {
|
||||
// Arrange
|
||||
val main = DerivationIndex.Main
|
||||
val notMain = DerivationIndex(1).getOrNull()!!
|
||||
|
||||
// Act & Assert
|
||||
Truth.assertThat(main.isMain).isTrue()
|
||||
Truth.assertThat(notMain.isMain).isFalse()
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("provideTestModels")
|
||||
fun invoke(model: InvokeTestModel) {
|
||||
// Act
|
||||
val actual = DerivationIndex(model.index)
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).isEqualTo(model.expected)
|
||||
}
|
||||
|
||||
private fun provideTestModels() = listOf(
|
||||
InvokeTestModel(index = 0, expected = DerivationIndex.Main.right()),
|
||||
InvokeTestModel(index = 5, expected = DerivationIndex(5).getOrNull()!!.right()),
|
||||
InvokeTestModel(index = -1, expected = DerivationIndex.Error.NegativeDerivationIndex(-1).left()),
|
||||
)
|
||||
|
||||
data class InvokeTestModel(
|
||||
val index: Int,
|
||||
val expected: Either<DerivationIndex.Error, DerivationIndex>,
|
||||
)
|
||||
}
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
package com.tangem.domain.nft.models
|
||||
|
||||
import com.tangem.domain.core.serialization.SerializedBigInteger
|
||||
import com.tangem.domain.models.StatusSource
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.serialization.SerializedBigInteger
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
package com.tangem.domain.nft.models
|
||||
|
||||
import com.tangem.domain.core.serialization.SerializedBigDecimal
|
||||
import com.tangem.domain.models.serialization.SerializedBigDecimal
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
|
|
|
|||
|
|
@ -2,25 +2,25 @@ package com.tangem.domain.notifications
|
|||
|
||||
import arrow.core.Either
|
||||
import com.tangem.domain.notifications.models.ApplicationId
|
||||
import com.tangem.domain.notifications.repository.NotificationsRepository
|
||||
import com.tangem.domain.notifications.repository.PushNotificationsRepository
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
||||
class GetApplicationIdUseCase(
|
||||
private val notificationsRepository: NotificationsRepository,
|
||||
private val pushNotificationsRepository: PushNotificationsRepository,
|
||||
) {
|
||||
private val mutex = Mutex()
|
||||
|
||||
suspend operator fun invoke(): Either<Throwable, ApplicationId> = Either.catch {
|
||||
val localApplicationId = notificationsRepository.getApplicationId()
|
||||
val localApplicationId = pushNotificationsRepository.getApplicationId()
|
||||
if (localApplicationId != null) return@catch localApplicationId
|
||||
|
||||
mutex.withLock {
|
||||
val doubleCheckedId = notificationsRepository.getApplicationId()
|
||||
val doubleCheckedId = pushNotificationsRepository.getApplicationId()
|
||||
if (doubleCheckedId != null) return@withLock doubleCheckedId
|
||||
|
||||
val newApplicationId = notificationsRepository.createApplicationId()
|
||||
notificationsRepository.saveApplicationId(newApplicationId)
|
||||
val newApplicationId = pushNotificationsRepository.createApplicationId()
|
||||
pushNotificationsRepository.saveApplicationId(newApplicationId)
|
||||
newApplicationId
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,13 +2,13 @@ package com.tangem.domain.notifications
|
|||
|
||||
import arrow.core.Either
|
||||
import com.tangem.domain.notifications.models.NotificationsEligibleNetwork
|
||||
import com.tangem.domain.notifications.repository.NotificationsRepository
|
||||
import com.tangem.domain.notifications.repository.PushNotificationsRepository
|
||||
|
||||
class GetNetworksAvailableForNotificationsUseCase(
|
||||
private val notificationsRepository: NotificationsRepository,
|
||||
private val pushNotificationsRepository: PushNotificationsRepository,
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(): Either<Throwable, List<NotificationsEligibleNetwork>> = Either.catch {
|
||||
notificationsRepository.getEligibleNetworks()
|
||||
pushNotificationsRepository.getEligibleNetworks()
|
||||
}
|
||||
}
|
||||
|
|
@ -2,16 +2,16 @@ package com.tangem.domain.notifications
|
|||
|
||||
import arrow.core.Either
|
||||
import com.tangem.domain.notifications.models.ApplicationId
|
||||
import com.tangem.domain.notifications.repository.NotificationsRepository
|
||||
import com.tangem.domain.notifications.repository.PushNotificationsRepository
|
||||
import com.tangem.utils.notifications.PushNotificationsTokenProvider
|
||||
|
||||
class SendPushTokenUseCase(
|
||||
private val notificationsRepository: NotificationsRepository,
|
||||
private val pushNotificationsRepository: PushNotificationsRepository,
|
||||
private val pushNotificationsTokenProvider: PushNotificationsTokenProvider,
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(applicationId: ApplicationId): Either<Throwable, Unit> = Either.catch {
|
||||
val token = pushNotificationsTokenProvider.getToken()
|
||||
notificationsRepository.sendPushToken(applicationId, token)
|
||||
pushNotificationsRepository.sendPushToken(applicationId, token)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.tangem.domain.notifications
|
||||
|
||||
import com.tangem.domain.notifications.repository.NotificationsRepository
|
||||
|
||||
class SetShouldShowNotificationUseCase(
|
||||
private val notificationsRepository: NotificationsRepository,
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(key: String, value: Boolean) {
|
||||
notificationsRepository.setShouldShowNotifications(key, value)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.tangem.domain.notifications
|
||||
|
||||
import com.tangem.domain.notifications.repository.NotificationsRepository
|
||||
|
||||
class ShouldShowNotificationUseCase(
|
||||
private val notificationsRepository: NotificationsRepository,
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(key: String): Boolean {
|
||||
return notificationsRepository.shouldShowNotification(key)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,27 +1,43 @@
|
|||
package com.tangem.domain.notifications.repository
|
||||
|
||||
import com.tangem.domain.notifications.models.ApplicationId
|
||||
import com.tangem.domain.notifications.models.NotificationsEligibleNetwork
|
||||
|
||||
/**
|
||||
* Repository interface for managing local notification logic and state.
|
||||
*
|
||||
* This interface provides methods to check and update whether specific notifications should be shown,
|
||||
* as well as to track the display count for certain notifications (e.g., Tron token fee).
|
||||
*
|
||||
* Note: This repository is responsible only for the local logic and state (such as preferences and counters)
|
||||
* regarding notifications. It does **not** directly show or hide notifications to the user.
|
||||
* The actual display and hiding of notifications in the UI is handled by [NotificationsUM],
|
||||
* which uses this repository to determine the appropriate behavior.
|
||||
*/
|
||||
interface NotificationsRepository {
|
||||
|
||||
@Throws
|
||||
suspend fun createApplicationId(pushToken: String? = null): ApplicationId
|
||||
/**
|
||||
* Checks whether a notification with the given [key] should be shown to the user.
|
||||
* @param key The unique identifier for the notification.
|
||||
* @return true if the notification should be shown, false otherwise.
|
||||
*/
|
||||
suspend fun shouldShowNotification(key: String): Boolean
|
||||
|
||||
suspend fun saveApplicationId(appId: ApplicationId)
|
||||
|
||||
suspend fun getApplicationId(): ApplicationId?
|
||||
/**
|
||||
* Sets whether a notification with the given [key] should be shown to the user.
|
||||
* @param key The unique identifier for the notification.
|
||||
* @param value true if the notification should be shown, false otherwise.
|
||||
*/
|
||||
suspend fun setShouldShowNotifications(key: String, value: Boolean)
|
||||
|
||||
/**
|
||||
* Gets the number of times the Tron token fee notification has been shown.
|
||||
* @return The current show counter for the Tron token fee notification.
|
||||
*/
|
||||
suspend fun getTronTokenFeeNotificationShowCounter(): Int
|
||||
|
||||
/**
|
||||
* Increments the counter tracking how many times the Tron token fee notification has been shown.
|
||||
*/
|
||||
suspend fun incrementTronTokenFeeNotificationShowCounter()
|
||||
|
||||
@Throws
|
||||
suspend fun sendPushToken(appId: ApplicationId, pushToken: String)
|
||||
|
||||
@Throws
|
||||
suspend fun getEligibleNetworks(): List<NotificationsEligibleNetwork>
|
||||
|
||||
suspend fun shouldShowSubscribeOnNotificationsAfterUpdate(): Boolean
|
||||
|
||||
suspend fun isUserAllowToSubscribeOnPushNotifications(): Boolean
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.domain.notifications.repository
|
||||
|
||||
import com.tangem.domain.notifications.models.ApplicationId
|
||||
import com.tangem.domain.notifications.models.NotificationsEligibleNetwork
|
||||
|
||||
interface PushNotificationsRepository {
|
||||
|
||||
@Throws
|
||||
suspend fun createApplicationId(pushToken: String? = null): ApplicationId
|
||||
|
||||
suspend fun saveApplicationId(appId: ApplicationId)
|
||||
|
||||
suspend fun getApplicationId(): ApplicationId?
|
||||
|
||||
@Throws
|
||||
suspend fun sendPushToken(appId: ApplicationId, pushToken: String)
|
||||
|
||||
@Throws
|
||||
suspend fun getEligibleNetworks(): List<NotificationsEligibleNetwork>
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@ package com.tangem.domain.notifications
|
|||
import arrow.core.Either
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.domain.notifications.models.ApplicationId
|
||||
import com.tangem.domain.notifications.repository.NotificationsRepository
|
||||
import com.tangem.domain.notifications.repository.PushNotificationsRepository
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.coVerifyOrder
|
||||
|
|
@ -15,14 +15,14 @@ import java.net.SocketTimeoutException
|
|||
|
||||
class GetApplicationIdUseCaseTest {
|
||||
|
||||
private val notificationsRepository: NotificationsRepository = mockk()
|
||||
private val useCase = GetApplicationIdUseCase(notificationsRepository)
|
||||
private val pushNotificationsRepository: PushNotificationsRepository = mockk()
|
||||
private val useCase = GetApplicationIdUseCase(pushNotificationsRepository)
|
||||
|
||||
@Test
|
||||
fun `GIVEN local application ID exists WHEN invoke THEN return local application ID`() = runTest {
|
||||
// GIVEN
|
||||
val expectedApplicationId = ApplicationId("test-app-id")
|
||||
coEvery { notificationsRepository.getApplicationId() } returns expectedApplicationId
|
||||
coEvery { pushNotificationsRepository.getApplicationId() } returns expectedApplicationId
|
||||
|
||||
// WHEN
|
||||
val result = useCase()
|
||||
|
|
@ -30,10 +30,10 @@ class GetApplicationIdUseCaseTest {
|
|||
// THEN
|
||||
assertThat(result).isInstanceOf(Either.Right::class.java)
|
||||
assertThat((result as Either.Right).value).isEqualTo(expectedApplicationId)
|
||||
coVerify(exactly = 1) { notificationsRepository.getApplicationId() }
|
||||
coVerify(exactly = 1) { pushNotificationsRepository.getApplicationId() }
|
||||
coVerify(inverse = true) {
|
||||
notificationsRepository.createApplicationId()
|
||||
notificationsRepository.saveApplicationId(any())
|
||||
pushNotificationsRepository.createApplicationId()
|
||||
pushNotificationsRepository.saveApplicationId(any())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -41,9 +41,9 @@ class GetApplicationIdUseCaseTest {
|
|||
fun `GIVEN local application ID does not exist WHEN invoke THEN create and save new application ID`() = runTest {
|
||||
// GIVEN
|
||||
val newApplicationId = ApplicationId("new-app-id")
|
||||
coEvery { notificationsRepository.getApplicationId() } returns null
|
||||
coEvery { notificationsRepository.createApplicationId() } returns newApplicationId
|
||||
coEvery { notificationsRepository.saveApplicationId(newApplicationId) } returns Unit
|
||||
coEvery { pushNotificationsRepository.getApplicationId() } returns null
|
||||
coEvery { pushNotificationsRepository.createApplicationId() } returns newApplicationId
|
||||
coEvery { pushNotificationsRepository.saveApplicationId(newApplicationId) } returns Unit
|
||||
|
||||
// WHEN
|
||||
val result = useCase()
|
||||
|
|
@ -52,10 +52,10 @@ class GetApplicationIdUseCaseTest {
|
|||
assertThat(result).isInstanceOf(Either.Right::class.java)
|
||||
assertThat((result as Either.Right).value).isEqualTo(newApplicationId)
|
||||
coVerifyOrder {
|
||||
notificationsRepository.getApplicationId()
|
||||
notificationsRepository.getApplicationId()
|
||||
notificationsRepository.createApplicationId()
|
||||
notificationsRepository.saveApplicationId(newApplicationId)
|
||||
pushNotificationsRepository.getApplicationId()
|
||||
pushNotificationsRepository.getApplicationId()
|
||||
pushNotificationsRepository.createApplicationId()
|
||||
pushNotificationsRepository.saveApplicationId(newApplicationId)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -63,7 +63,7 @@ class GetApplicationIdUseCaseTest {
|
|||
fun `GIVEN repository throws exception WHEN invoke THEN return Either Left with error`() = runTest {
|
||||
// GIVEN
|
||||
val expectedError = SocketTimeoutException("Test error")
|
||||
coEvery { notificationsRepository.getApplicationId() } throws expectedError
|
||||
coEvery { pushNotificationsRepository.getApplicationId() } throws expectedError
|
||||
|
||||
// WHEN
|
||||
val result = useCase()
|
||||
|
|
@ -71,10 +71,10 @@ class GetApplicationIdUseCaseTest {
|
|||
// THEN
|
||||
assertThat(result).isInstanceOf(Either.Left::class.java)
|
||||
assertThat((result as Either.Left).value).isEqualTo(expectedError)
|
||||
coVerify(exactly = 1) { notificationsRepository.getApplicationId() }
|
||||
coVerify(exactly = 1) { pushNotificationsRepository.getApplicationId() }
|
||||
coVerify(inverse = true) {
|
||||
notificationsRepository.createApplicationId()
|
||||
notificationsRepository.saveApplicationId(any())
|
||||
pushNotificationsRepository.createApplicationId()
|
||||
pushNotificationsRepository.saveApplicationId(any())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -85,14 +85,14 @@ class GetApplicationIdUseCaseTest {
|
|||
val newApplicationId = ApplicationId("new-app-id")
|
||||
var isIdCreated = false
|
||||
|
||||
coEvery { notificationsRepository.getApplicationId() } answers {
|
||||
coEvery { pushNotificationsRepository.getApplicationId() } answers {
|
||||
if (!isIdCreated) null else newApplicationId
|
||||
}
|
||||
coEvery { notificationsRepository.createApplicationId() } answers {
|
||||
coEvery { pushNotificationsRepository.createApplicationId() } answers {
|
||||
isIdCreated = true
|
||||
newApplicationId
|
||||
}
|
||||
coEvery { notificationsRepository.saveApplicationId(newApplicationId) } returns Unit
|
||||
coEvery { pushNotificationsRepository.saveApplicationId(newApplicationId) } returns Unit
|
||||
|
||||
// WHEN
|
||||
val results = coroutineScope {
|
||||
|
|
@ -108,9 +108,9 @@ class GetApplicationIdUseCaseTest {
|
|||
assertThat(result).isInstanceOf(Either.Right::class.java)
|
||||
assertThat((result as Either.Right).value).isEqualTo(newApplicationId)
|
||||
}
|
||||
coVerify(exactly = PARALLEL_COUNT + 1) { notificationsRepository.getApplicationId() }
|
||||
coVerify(exactly = 1) { notificationsRepository.createApplicationId() }
|
||||
coVerify(exactly = 1) { notificationsRepository.saveApplicationId(newApplicationId) }
|
||||
coVerify(exactly = PARALLEL_COUNT + 1) { pushNotificationsRepository.getApplicationId() }
|
||||
coVerify(exactly = 1) { pushNotificationsRepository.createApplicationId() }
|
||||
coVerify(exactly = 1) { pushNotificationsRepository.saveApplicationId(newApplicationId) }
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -119,14 +119,14 @@ class GetApplicationIdUseCaseTest {
|
|||
val newApplicationId = ApplicationId("new-app-id")
|
||||
var isIdCreated = false
|
||||
|
||||
coEvery { notificationsRepository.getApplicationId() } answers {
|
||||
coEvery { pushNotificationsRepository.getApplicationId() } answers {
|
||||
if (!isIdCreated) null else newApplicationId
|
||||
}
|
||||
coEvery { notificationsRepository.createApplicationId() } answers {
|
||||
coEvery { pushNotificationsRepository.createApplicationId() } answers {
|
||||
isIdCreated = true
|
||||
newApplicationId
|
||||
}
|
||||
coEvery { notificationsRepository.saveApplicationId(newApplicationId) } returns Unit
|
||||
coEvery { pushNotificationsRepository.saveApplicationId(newApplicationId) } returns Unit
|
||||
|
||||
// WHEN
|
||||
val results = coroutineScope {
|
||||
|
|
@ -143,9 +143,9 @@ class GetApplicationIdUseCaseTest {
|
|||
assertThat(result).isInstanceOf(Either.Right::class.java)
|
||||
assertThat((result as Either.Right).value).isEqualTo(newApplicationId)
|
||||
}
|
||||
coVerify(exactly = PARALLEL_COUNT + 1) { notificationsRepository.getApplicationId() }
|
||||
coVerify(exactly = 1) { notificationsRepository.createApplicationId() }
|
||||
coVerify(exactly = 1) { notificationsRepository.saveApplicationId(newApplicationId) }
|
||||
coVerify(exactly = PARALLEL_COUNT + 1) { pushNotificationsRepository.getApplicationId() }
|
||||
coVerify(exactly = 1) { pushNotificationsRepository.createApplicationId() }
|
||||
coVerify(exactly = 1) { pushNotificationsRepository.saveApplicationId(newApplicationId) }
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ package com.tangem.domain.notifications
|
|||
import arrow.core.Either
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.domain.notifications.models.ApplicationId
|
||||
import com.tangem.domain.notifications.repository.NotificationsRepository
|
||||
import com.tangem.domain.notifications.repository.PushNotificationsRepository
|
||||
import com.tangem.utils.notifications.PushNotificationsTokenProvider
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
|
|
@ -14,16 +14,16 @@ import org.junit.Test
|
|||
|
||||
class SendPushTokenUseCaseTest {
|
||||
|
||||
private lateinit var notificationsRepository: NotificationsRepository
|
||||
private lateinit var pushNotificationsRepository: PushNotificationsRepository
|
||||
private lateinit var pushNotificationsTokenProvider: PushNotificationsTokenProvider
|
||||
private lateinit var sendPushTokenUseCase: SendPushTokenUseCase
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
notificationsRepository = mockk()
|
||||
pushNotificationsRepository = mockk()
|
||||
pushNotificationsTokenProvider = mockk()
|
||||
sendPushTokenUseCase = SendPushTokenUseCase(
|
||||
notificationsRepository = notificationsRepository,
|
||||
pushNotificationsRepository = pushNotificationsRepository,
|
||||
pushNotificationsTokenProvider = pushNotificationsTokenProvider,
|
||||
)
|
||||
}
|
||||
|
|
@ -34,14 +34,14 @@ class SendPushTokenUseCaseTest {
|
|||
val applicationId = ApplicationId("test-app-id")
|
||||
val token = "test-token"
|
||||
coEvery { pushNotificationsTokenProvider.getToken() } returns token
|
||||
coEvery { notificationsRepository.sendPushToken(applicationId, token) } returns Unit
|
||||
coEvery { pushNotificationsRepository.sendPushToken(applicationId, token) } returns Unit
|
||||
|
||||
// WHEN
|
||||
val result = sendPushTokenUseCase(applicationId)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isEqualTo(Either.Right(Unit))
|
||||
coVerify(exactly = 1) { notificationsRepository.sendPushToken(applicationId, token) }
|
||||
coVerify(exactly = 1) { pushNotificationsRepository.sendPushToken(applicationId, token) }
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -51,13 +51,13 @@ class SendPushTokenUseCaseTest {
|
|||
val token = "test-token"
|
||||
val expectedError = RuntimeException("Network error")
|
||||
coEvery { pushNotificationsTokenProvider.getToken() } returns token
|
||||
coEvery { notificationsRepository.sendPushToken(applicationId, token) } throws expectedError
|
||||
coEvery { pushNotificationsRepository.sendPushToken(applicationId, token) } throws expectedError
|
||||
|
||||
// WHEN
|
||||
val result = sendPushTokenUseCase(applicationId)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isEqualTo(Either.Left(expectedError))
|
||||
coVerify(exactly = 1) { notificationsRepository.sendPushToken(applicationId, token) }
|
||||
coVerify(exactly = 1) { pushNotificationsRepository.sendPushToken(applicationId, token) }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
package com.tangem.domain.onramp.model
|
||||
|
||||
import com.tangem.domain.core.serialization.SerializedBigDecimal
|
||||
import com.tangem.domain.models.serialization.SerializedBigDecimal
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
|
|
|
|||
|
|
@ -2,10 +2,10 @@ package com.tangem.domain.onramp.model.cache
|
|||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.tangem.domain.core.serialization.SerializedBigDecimal
|
||||
import com.tangem.domain.models.serialization.SerializedBigDecimal
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.onramp.model.OnrampCurrency
|
||||
import com.tangem.domain.onramp.model.OnrampStatus
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
||||
/**
|
||||
* Model for local storing onramp transaction
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@ package com.tangem.domain.onramp
|
|||
|
||||
import arrow.core.Either
|
||||
import arrow.core.raise.either
|
||||
import com.tangem.domain.onramp.repositories.LegacyTopUpRepository
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.onramp.model.error.OnrampError
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.onramp.repositories.LegacyTopUpRepository
|
||||
|
||||
class GetLegacyTopUpUrlUseCase(
|
||||
private val legacyTopUpRepository: LegacyTopUpRepository,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ dependencies {
|
|||
api(projects.core.analytics)
|
||||
api(projects.core.utils)
|
||||
|
||||
implementation(deps.kotlin.datetime)
|
||||
implementation(deps.kotlin.serialization)
|
||||
implementation(deps.jodatime)
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ dependencies {
|
|||
implementation(projects.domain.core)
|
||||
implementation(projects.domain.models)
|
||||
|
||||
implementation(deps.kotlin.datetime)
|
||||
implementation(deps.kotlin.serialization)
|
||||
implementation(deps.jodatime)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +0,0 @@
|
|||
package com.tangem.domain.staking.model
|
||||
|
||||
// TODO: make part of YieldBalance in the future
|
||||
data class StakingID(val integrationId: String, val address: String)
|
||||
|
|
@ -1,13 +1,14 @@
|
|||
package com.tangem.domain.staking.model.stakekit
|
||||
|
||||
import com.tangem.domain.core.serialization.SerializedBigDecimal
|
||||
import com.tangem.domain.models.serialization.SerializedBigDecimal
|
||||
import com.tangem.domain.models.staking.YieldToken
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class Yield(
|
||||
val id: String,
|
||||
val token: Token,
|
||||
val tokens: List<Token>,
|
||||
val token: YieldToken,
|
||||
val tokens: List<YieldToken>,
|
||||
val args: Args,
|
||||
val status: Status,
|
||||
val apy: SerializedBigDecimal,
|
||||
|
|
@ -93,9 +94,9 @@ data class Yield(
|
|||
val logoUri: String,
|
||||
val description: String,
|
||||
val documentation: String?,
|
||||
val gasFeeToken: Token,
|
||||
val token: Token,
|
||||
val tokens: List<Token>,
|
||||
val gasFeeToken: YieldToken,
|
||||
val token: YieldToken,
|
||||
val tokens: List<YieldToken>,
|
||||
val type: String,
|
||||
val rewardSchedule: RewardSchedule,
|
||||
val cooldownPeriod: Period?,
|
||||
|
|
@ -145,18 +146,6 @@ data class Yield(
|
|||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Token(
|
||||
val name: String,
|
||||
val network: NetworkType,
|
||||
val symbol: String,
|
||||
val decimals: Int,
|
||||
val address: String?,
|
||||
val coinGeckoId: String?,
|
||||
val logoURI: String?,
|
||||
val isPoints: Boolean?,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class AddressArgument(
|
||||
val required: Boolean,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.domain.staking.model.stakekit.action
|
||||
|
||||
import com.tangem.domain.models.staking.action.StakingActionType
|
||||
import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction
|
||||
import org.joda.time.DateTime
|
||||
import java.math.BigDecimal
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
package com.tangem.domain.staking.model.stakekit.transaction
|
||||
|
||||
import com.tangem.domain.staking.model.stakekit.Token
|
||||
import com.tangem.domain.models.staking.YieldToken
|
||||
import com.tangem.domain.models.staking.action.StakingActionType
|
||||
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
|
||||
import com.tangem.domain.staking.model.stakekit.action.StakingActionType
|
||||
import java.math.BigDecimal
|
||||
|
||||
data class ActionParams(
|
||||
|
|
@ -11,7 +11,7 @@ data class ActionParams(
|
|||
val amount: BigDecimal,
|
||||
val address: String,
|
||||
val validatorAddress: String,
|
||||
val token: Token,
|
||||
val token: YieldToken,
|
||||
val publicKey: String? = null,
|
||||
val passthrough: String? = null,
|
||||
val type: StakingActionType? = null,
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
package com.tangem.domain.staking.model.stakekit.transaction
|
||||
|
||||
import com.tangem.domain.staking.model.stakekit.Token
|
||||
import com.tangem.domain.models.staking.YieldToken
|
||||
import java.math.BigDecimal
|
||||
|
||||
data class StakingGasEstimate(
|
||||
val amount: BigDecimal,
|
||||
val token: Token,
|
||||
val token: YieldToken,
|
||||
val gasLimit: String?,
|
||||
)
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
package com.tangem.domain.staking.model.stakekit.transaction
|
||||
|
||||
import com.tangem.domain.staking.model.stakekit.NetworkType
|
||||
import com.tangem.domain.models.staking.NetworkType
|
||||
|
||||
data class StakingTransaction(
|
||||
val id: String,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ package com.tangem.domain.staking
|
|||
|
||||
import arrow.core.Either
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.staking.model.stakekit.NetworkType
|
||||
import com.tangem.domain.models.staking.NetworkType
|
||||
import com.tangem.domain.staking.model.stakekit.StakingError
|
||||
import com.tangem.domain.staking.model.stakekit.action.StakingActionStatus
|
||||
import com.tangem.domain.staking.repositories.StakingActionRepository
|
||||
|
|
|
|||
|
|
@ -1,36 +1,41 @@
|
|||
package com.tangem.domain.staking
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.raise.catch
|
||||
import arrow.core.getOrElse
|
||||
import arrow.core.raise.either
|
||||
import arrow.core.right
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.staking.model.stakekit.StakingError
|
||||
import com.tangem.domain.staking.repositories.StakingErrorResolver
|
||||
import com.tangem.domain.staking.single.SingleYieldBalanceFetcher
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.staking.model.stakekit.StakingError
|
||||
import com.tangem.domain.staking.single.SingleYieldBalanceFetcher
|
||||
|
||||
class FetchStakingYieldBalanceUseCase(
|
||||
private val stakingErrorResolver: StakingErrorResolver,
|
||||
private val singleYieldBalanceFetcher: SingleYieldBalanceFetcher,
|
||||
private val stakingIdFactory: StakingIdFactory,
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(
|
||||
userWalletId: UserWalletId,
|
||||
cryptoCurrency: CryptoCurrency,
|
||||
): Either<StakingError, Unit> {
|
||||
return either {
|
||||
catch(
|
||||
block = {
|
||||
singleYieldBalanceFetcher(
|
||||
params = SingleYieldBalanceFetcher.Params(
|
||||
userWalletId = userWalletId,
|
||||
currencyId = cryptoCurrency.id,
|
||||
network = cryptoCurrency.network,
|
||||
),
|
||||
)
|
||||
},
|
||||
catch = { stakingErrorResolver.resolve(it) },
|
||||
)
|
||||
}
|
||||
): Either<StakingError, Unit> = either {
|
||||
val stakingId = stakingIdFactory.create(
|
||||
userWalletId = userWalletId,
|
||||
currencyId = cryptoCurrency.id,
|
||||
network = cryptoCurrency.network,
|
||||
)
|
||||
.getOrElse {
|
||||
when (it) {
|
||||
is StakingIdFactory.Error.UnableToGetAddress -> raise(StakingError.DomainError("$it"))
|
||||
StakingIdFactory.Error.UnsupportedCurrency -> Unit.right()
|
||||
}
|
||||
|
||||
return@either
|
||||
}
|
||||
|
||||
singleYieldBalanceFetcher(
|
||||
params = SingleYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingId = stakingId),
|
||||
)
|
||||
.mapLeft { StakingError.DomainError("$it") }
|
||||
.bind()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +1,18 @@
|
|||
package com.tangem.domain.staking
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.domain.staking.model.stakekit.action.StakingActionType
|
||||
import com.tangem.domain.staking.repositories.StakingRepository
|
||||
import com.tangem.domain.staking.model.StakingIntegrationID
|
||||
import com.tangem.domain.models.staking.action.StakingActionType
|
||||
import java.math.BigDecimal
|
||||
|
||||
class GetActionRequirementAmountUseCase(
|
||||
private val stakingRepository: StakingRepository,
|
||||
) {
|
||||
class GetActionRequirementAmountUseCase {
|
||||
|
||||
operator fun invoke(integrationId: String, actionType: StakingActionType): Either<Throwable, BigDecimal?> =
|
||||
Either.catch {
|
||||
stakingRepository.getActionRequirementAmount(integrationId, actionType)
|
||||
operator fun invoke(integrationId: String, actionType: StakingActionType): BigDecimal? {
|
||||
return if (StakingIntegrationID.EthereumToken.Polygon.value == integrationId &&
|
||||
actionType == StakingActionType.CLAIM_REWARDS
|
||||
) {
|
||||
BigDecimal.ONE
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
package com.tangem.domain.staking
|
||||
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.staking.repositories.StakingRepository
|
||||
|
||||
class GetStakingIntegrationIdUseCase(
|
||||
private val stakingRepository: StakingRepository,
|
||||
) {
|
||||
|
||||
operator fun invoke(cryptoCurrencyId: CryptoCurrency.ID) =
|
||||
stakingRepository.getSupportedIntegrationId(cryptoCurrencyId)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue