Updated on 2026-08-14
This commit is contained in:
commit
3946e4ebca
359 changed files with 8459 additions and 3324 deletions
|
|
@ -8,14 +8,14 @@ import com.tangem.domain.models.TokensSortType
|
|||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.account.AccountName
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
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 userWalletId the user wallet id associated with the account list
|
||||
* @property accounts a set of accounts belonging to the user wallet
|
||||
* @property totalAccounts the total number of accounts
|
||||
*
|
||||
|
|
@ -23,7 +23,7 @@ import kotlinx.serialization.Serializable
|
|||
*/
|
||||
@Serializable
|
||||
data class AccountList private constructor(
|
||||
val userWallet: UserWallet,
|
||||
val userWalletId: UserWalletId,
|
||||
val accounts: Set<Account>,
|
||||
val totalAccounts: Int,
|
||||
val sortType: TokensSortType,
|
||||
|
|
@ -51,7 +51,7 @@ data class AccountList private constructor(
|
|||
val accounts = this.accounts.addOrReplace(other) { it.accountId == other.accountId }
|
||||
|
||||
return invoke(
|
||||
userWallet = this.userWallet,
|
||||
userWalletId = this.userWalletId,
|
||||
accounts = accounts,
|
||||
totalAccounts = this.totalAccounts + if (isNewAccount) 1 else 0,
|
||||
sortType = this.sortType,
|
||||
|
|
@ -73,7 +73,7 @@ data class AccountList private constructor(
|
|||
}
|
||||
|
||||
return invoke(
|
||||
userWallet = this.userWallet,
|
||||
userWalletId = this.userWalletId,
|
||||
accounts = accounts,
|
||||
totalAccounts = this.totalAccounts - if (isExistingAccount) 1 else 0,
|
||||
sortType = this.sortType,
|
||||
|
|
@ -134,12 +134,12 @@ data class AccountList private constructor(
|
|||
* 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 userWalletId the user wallet id 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,
|
||||
userWalletId: UserWalletId,
|
||||
accounts: Set<Account>,
|
||||
totalAccounts: Int,
|
||||
sortType: TokensSortType = TokensSortType.NONE,
|
||||
|
|
@ -169,7 +169,7 @@ data class AccountList private constructor(
|
|||
}
|
||||
|
||||
AccountList(
|
||||
userWallet = userWallet,
|
||||
userWalletId = userWalletId,
|
||||
accounts = accounts,
|
||||
totalAccounts = totalAccounts,
|
||||
sortType = sortType,
|
||||
|
|
@ -180,19 +180,19 @@ data class AccountList private constructor(
|
|||
/**
|
||||
* Factory method to create an empty [AccountList] with a main crypto portfolio account
|
||||
*
|
||||
* @param userWallet the user wallet associated with the account list
|
||||
* @param userWalletId the user wallet id associated with the account list
|
||||
*/
|
||||
fun empty(
|
||||
userWallet: UserWallet,
|
||||
userWalletId: UserWalletId,
|
||||
cryptoCurrencies: Set<CryptoCurrency> = emptySet(),
|
||||
sortType: TokensSortType = TokensSortType.NONE,
|
||||
groupType: TokensGroupType = TokensGroupType.NONE,
|
||||
): AccountList {
|
||||
return AccountList(
|
||||
userWallet = userWallet,
|
||||
userWalletId = userWalletId,
|
||||
accounts = setOf(
|
||||
Account.CryptoPortfolio.createMainAccount(
|
||||
userWalletId = userWallet.walletId,
|
||||
userWalletId = userWalletId,
|
||||
cryptoCurrencies = cryptoCurrencies,
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -3,13 +3,13 @@ package com.tangem.domain.account.models
|
|||
import com.tangem.domain.models.TotalFiatBalance
|
||||
import com.tangem.domain.models.account.AccountStatus
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
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 userWalletId the user wallet id 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 (including archived ones)
|
||||
* @property totalFiatBalance the total fiat balance across all accounts
|
||||
|
|
@ -18,7 +18,7 @@ import kotlinx.serialization.Serializable
|
|||
*/
|
||||
@Serializable
|
||||
data class AccountStatusList(
|
||||
val userWallet: UserWallet,
|
||||
val userWalletId: UserWalletId,
|
||||
val accountStatuses: Set<AccountStatus>,
|
||||
val totalAccounts: Int,
|
||||
val totalFiatBalance: TotalFiatBalance,
|
||||
|
|
|
|||
|
|
@ -68,6 +68,13 @@ interface AccountsCRUDRepository {
|
|||
*/
|
||||
suspend fun saveAccounts(accountList: AccountList)
|
||||
|
||||
/**
|
||||
* Save account
|
||||
*
|
||||
* @param account account to be saved
|
||||
*/
|
||||
suspend fun saveAccount(account: Account.CryptoPortfolio)
|
||||
|
||||
/**
|
||||
* Retrieves the total count of accounts associated with a specific user wallet including archived accounts
|
||||
*
|
||||
|
|
|
|||
|
|
@ -5,9 +5,11 @@ 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.models.ArchivedAccount
|
||||
import com.tangem.domain.account.repository.AccountsCRUDRepository
|
||||
import com.tangem.domain.account.tokens.MainAccountTokensMigration
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.account.AccountId
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
|
@ -16,11 +18,13 @@ 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
|
||||
* @property mainAccountTokensMigration handles the migration of tokens from the main account to the recovered account
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class RecoverCryptoPortfolioUseCase(
|
||||
private val crudRepository: AccountsCRUDRepository,
|
||||
private val mainAccountTokensMigration: MainAccountTokensMigration,
|
||||
) {
|
||||
|
||||
/**
|
||||
|
|
@ -30,15 +34,25 @@ class RecoverCryptoPortfolioUseCase(
|
|||
*/
|
||||
suspend operator fun invoke(accountId: AccountId): Either<Error, Account.CryptoPortfolio> = either {
|
||||
val accountList = getAccountList(userWalletId = accountId.userWalletId)
|
||||
|
||||
ensure(accountList.canAddMoreAccounts) {
|
||||
raise(Error.AccountListRequirementsNotMet(cause = AccountList.Error.ExceedsMaxAccountsCount))
|
||||
}
|
||||
|
||||
val archivedAccount = getArchivedAccount(accountId = accountId)
|
||||
|
||||
val recoveredAccount = archivedAccount.recover()
|
||||
|
||||
val updatedAccountList = (accountList + recoveredAccount)
|
||||
.getOrElse { raise(Error.CriticalTechError.AccountListRequirementsNotMet(cause = it)) }
|
||||
.getOrElse { raise(Error.AccountListRequirementsNotMet(cause = it)) }
|
||||
|
||||
saveAccounts(updatedAccountList)
|
||||
|
||||
mainAccountTokensMigration.migrate(
|
||||
userWalletId = accountId.userWalletId,
|
||||
derivationIndex = recoveredAccount.derivationIndex,
|
||||
)
|
||||
|
||||
recoveredAccount
|
||||
}
|
||||
|
||||
|
|
@ -47,7 +61,9 @@ class RecoverCryptoPortfolioUseCase(
|
|||
block = { crudRepository.getAccountListSync(userWalletId = userWalletId) },
|
||||
catch = { raise(Error.DataOperationFailed(cause = it)) },
|
||||
)
|
||||
.getOrElse { raise(Error.CriticalTechError.AccountsNotCreated(userWalletId = userWalletId)) }
|
||||
.getOrElse {
|
||||
raise(Error.DataOperationFailed(message = "Account list not found for wallet $userWalletId"))
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun Raise<Error>.getArchivedAccount(accountId: AccountId): ArchivedAccount {
|
||||
|
|
@ -56,7 +72,7 @@ class RecoverCryptoPortfolioUseCase(
|
|||
catch = { raise(Error.DataOperationFailed(cause = it)) },
|
||||
)
|
||||
.getOrElse {
|
||||
raise(Error.CriticalTechError.AccountNotFound(accountId = accountId))
|
||||
raise(Error.DataOperationFailed(message = "Account not found: $accountId"))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -66,7 +82,6 @@ class RecoverCryptoPortfolioUseCase(
|
|||
accountName = this.name,
|
||||
icon = this.icon,
|
||||
derivationIndex = this.derivationIndex,
|
||||
// TODO: [REDACTED_JIRA]
|
||||
cryptoCurrencies = emptySet(),
|
||||
)
|
||||
}
|
||||
|
|
@ -87,37 +102,18 @@ class RecoverCryptoPortfolioUseCase(
|
|||
get() = this::class.simpleName ?: "RecoverCryptoPortfolioUseCase.Error"
|
||||
|
||||
/**
|
||||
* Critical technical errors that can occur during the recovery operation
|
||||
* Error indicating that the account list requirements were not met.
|
||||
*
|
||||
* @property cause the underlying cause of the error
|
||||
*/
|
||||
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"
|
||||
}
|
||||
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"}"
|
||||
|
||||
constructor(message: String) : this(cause = IllegalStateException(message))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -8,11 +8,7 @@ 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
|
||||
|
|
@ -31,7 +27,7 @@ class AccountListTest {
|
|||
val mainAccount = Account.CryptoPortfolio.createMainAccount(userWalletId = userWalletId)
|
||||
|
||||
val accountList = AccountList(
|
||||
userWallet = mockk(),
|
||||
userWalletId = userWalletId,
|
||||
accounts = setOf(mainAccount),
|
||||
totalAccounts = 1,
|
||||
)
|
||||
|
|
@ -49,13 +45,13 @@ class AccountListTest {
|
|||
fun canAddMoreAccounts() {
|
||||
// Arrange
|
||||
val accountList = AccountList(
|
||||
userWallet = mockk(),
|
||||
userWalletId = userWalletId,
|
||||
accounts = createAccounts(userWalletId = userWalletId, count = 2),
|
||||
totalAccounts = 2,
|
||||
).getOrNull()!!
|
||||
|
||||
val fullAccountList = AccountList(
|
||||
userWallet = mockk(),
|
||||
userWalletId = userWalletId,
|
||||
accounts = createAccounts(userWalletId = userWalletId, count = 20),
|
||||
totalAccounts = 20,
|
||||
).getOrNull()!!
|
||||
|
|
@ -67,16 +63,13 @@ class AccountListTest {
|
|||
|
||||
@Test
|
||||
fun empty() {
|
||||
// Arrange
|
||||
val userWallet = mockk<UserWallet>(relaxed = true)
|
||||
|
||||
// Act
|
||||
val actual = AccountList.empty(userWallet)
|
||||
val actual = AccountList.empty(userWalletId)
|
||||
|
||||
// Assert
|
||||
val expected = AccountList(
|
||||
userWallet = userWallet,
|
||||
accounts = setOf(Account.CryptoPortfolio.createMainAccount(userWalletId = userWallet.walletId)),
|
||||
userWalletId = userWalletId,
|
||||
accounts = setOf(Account.CryptoPortfolio.createMainAccount(userWalletId = userWalletId)),
|
||||
totalAccounts = 1,
|
||||
).getOrNull()!!
|
||||
|
||||
|
|
@ -87,19 +80,12 @@ class AccountListTest {
|
|||
@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,
|
||||
userWalletId = userWalletId,
|
||||
accounts = model.accounts,
|
||||
totalAccounts = model.accounts.size,
|
||||
)
|
||||
|
|
@ -131,13 +117,13 @@ class AccountListTest {
|
|||
createAccounts(userWalletId = userWalletId, count = 1).let {
|
||||
CreateTestModel(
|
||||
accounts = it,
|
||||
expected = AccountList(userWallet = userWallet, accounts = it, totalAccounts = 1),
|
||||
expected = AccountList(userWalletId = userWalletId, accounts = it, totalAccounts = 1),
|
||||
)
|
||||
},
|
||||
createAccounts(userWalletId = userWalletId, count = 20).let {
|
||||
CreateTestModel(
|
||||
accounts = it,
|
||||
expected = AccountList(userWallet = userWallet, accounts = it, totalAccounts = 20),
|
||||
expected = AccountList(userWalletId = userWalletId, accounts = it, totalAccounts = 20),
|
||||
)
|
||||
},
|
||||
CreateTestModel(
|
||||
|
|
@ -171,8 +157,6 @@ class AccountListTest {
|
|||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class Plus {
|
||||
|
||||
private val userWallet = mockk<UserWallet>()
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("provideTestModels")
|
||||
fun invoke(model: PlusTestModel) {
|
||||
|
|
@ -191,13 +175,13 @@ class AccountListTest {
|
|||
|
||||
PlusTestModel(
|
||||
initial = AccountList(
|
||||
userWallet = userWallet,
|
||||
userWalletId = userWalletId,
|
||||
accounts = setOf(mainAccount),
|
||||
totalAccounts = 1,
|
||||
).getOrNull()!!,
|
||||
toAdd = newAccount,
|
||||
expected = AccountList(
|
||||
userWallet = userWallet,
|
||||
userWalletId = userWalletId,
|
||||
accounts = setOf(mainAccount, newAccount),
|
||||
totalAccounts = 2,
|
||||
),
|
||||
|
|
@ -211,13 +195,13 @@ class AccountListTest {
|
|||
|
||||
PlusTestModel(
|
||||
initial = AccountList(
|
||||
userWallet = userWallet,
|
||||
userWalletId = userWalletId,
|
||||
accounts = setOf(mainAccount),
|
||||
totalAccounts = 1,
|
||||
).getOrNull()!!,
|
||||
toAdd = newAccount,
|
||||
expected = AccountList(
|
||||
userWallet = userWallet,
|
||||
userWalletId = userWalletId,
|
||||
accounts = setOf(newAccount),
|
||||
totalAccounts = 1,
|
||||
),
|
||||
|
|
@ -226,7 +210,7 @@ class AccountListTest {
|
|||
// endregion
|
||||
PlusTestModel(
|
||||
initial = AccountList(
|
||||
userWallet = userWallet,
|
||||
userWalletId = userWalletId,
|
||||
accounts = createAccounts(userWalletId = userWalletId, count = 20),
|
||||
totalAccounts = 20,
|
||||
).getOrNull()!!,
|
||||
|
|
@ -246,8 +230,6 @@ class AccountListTest {
|
|||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class Minus {
|
||||
|
||||
private val userWallet = mockk<UserWallet>()
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("provideTestModels")
|
||||
fun invoke(model: MinusTestModel) {
|
||||
|
|
@ -266,13 +248,13 @@ class AccountListTest {
|
|||
|
||||
MinusTestModel(
|
||||
initial = AccountList(
|
||||
userWallet = userWallet,
|
||||
userWalletId = userWalletId,
|
||||
accounts = setOf(mainAccount, secondaryAccount),
|
||||
totalAccounts = 2,
|
||||
).getOrNull()!!,
|
||||
toRemove = secondaryAccount,
|
||||
expected = AccountList(
|
||||
userWallet = userWallet,
|
||||
userWalletId = userWalletId,
|
||||
accounts = setOf(mainAccount),
|
||||
totalAccounts = 1,
|
||||
),
|
||||
|
|
@ -286,13 +268,13 @@ class AccountListTest {
|
|||
|
||||
MinusTestModel(
|
||||
initial = AccountList(
|
||||
userWallet = userWallet,
|
||||
userWalletId = userWalletId,
|
||||
accounts = setOf(mainAccount),
|
||||
totalAccounts = 1,
|
||||
).getOrNull()!!,
|
||||
toRemove = notInList,
|
||||
expected = AccountList(
|
||||
userWallet = userWallet,
|
||||
userWalletId = userWalletId,
|
||||
accounts = setOf(mainAccount),
|
||||
totalAccounts = 1,
|
||||
),
|
||||
|
|
@ -305,7 +287,7 @@ class AccountListTest {
|
|||
|
||||
MinusTestModel(
|
||||
initial = AccountList(
|
||||
userWallet = userWallet,
|
||||
userWalletId = userWalletId,
|
||||
accounts = setOf(mainAccount),
|
||||
totalAccounts = 1,
|
||||
).getOrNull()!!,
|
||||
|
|
@ -321,7 +303,7 @@ class AccountListTest {
|
|||
|
||||
MinusTestModel(
|
||||
initial = AccountList(
|
||||
userWallet = userWallet,
|
||||
userWalletId = userWalletId,
|
||||
accounts = setOf(mainAccount, secondaryAccount),
|
||||
totalAccounts = 2,
|
||||
).getOrNull()!!,
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ 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
|
||||
|
|
@ -35,20 +34,16 @@ class AddCryptoPortfolioUseCaseTest {
|
|||
mainAccountTokensMigration = mainAccountTokensMigration,
|
||||
)
|
||||
|
||||
private val userWallet = mockk<UserWallet>()
|
||||
|
||||
@BeforeEach
|
||||
fun resetMocks() {
|
||||
clearMocks(crudRepository, singleAccountListFetcher, mainAccountTokensMigration, userWallet)
|
||||
|
||||
every { userWallet.walletId } returns userWalletId
|
||||
clearMocks(crudRepository, singleAccountListFetcher, mainAccountTokensMigration)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke should add new crypto portfolio account to existing list`() = runTest {
|
||||
// Arrange
|
||||
val newAccount = createNewAccount()
|
||||
val accountList = AccountList.empty(userWallet)
|
||||
val accountList = AccountList.empty(userWalletId)
|
||||
val updatedAccountList = (accountList + newAccount).getOrNull()!!
|
||||
|
||||
coEvery {
|
||||
|
|
@ -152,7 +147,7 @@ class AddCryptoPortfolioUseCaseTest {
|
|||
fun `invoke should return error if account list requirements not met`() = runTest {
|
||||
// Arrange
|
||||
val accountList = AccountList(
|
||||
userWallet = userWallet,
|
||||
userWalletId = userWalletId,
|
||||
accounts = createAccounts(userWalletId = userWalletId, count = 20),
|
||||
totalAccounts = 20,
|
||||
).getOrNull()!!
|
||||
|
|
@ -228,7 +223,7 @@ class AddCryptoPortfolioUseCaseTest {
|
|||
fun `invoke should return error if saveAccounts throws exception`() = runTest {
|
||||
// Arrange
|
||||
val newAccount = createNewAccount()
|
||||
val accountList = AccountList.empty(userWallet)
|
||||
val accountList = AccountList.empty(userWalletId)
|
||||
val updatedAccountList = (accountList + newAccount).getOrNull()!!
|
||||
|
||||
val exception = IllegalStateException("Test error")
|
||||
|
|
@ -266,7 +261,7 @@ class AddCryptoPortfolioUseCaseTest {
|
|||
fun `invoke should return new account if migrate returns error`() = runTest {
|
||||
// Arrange
|
||||
val newAccount = createNewAccount()
|
||||
val accountList = AccountList.empty(userWallet)
|
||||
val accountList = AccountList.empty(userWalletId)
|
||||
val updatedAccountList = (accountList + newAccount).getOrNull()!!
|
||||
|
||||
val exception = Exception("Migration error")
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ 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
|
||||
|
|
@ -24,19 +23,17 @@ 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
|
||||
clearMocks(crudRepository)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke should archive existing crypto portfolio account`() = runTest {
|
||||
// Arrange
|
||||
val account = createAccount(userWalletId)
|
||||
val accountList = (AccountList.empty(userWallet) + account).getOrNull()!!
|
||||
val accountList = (AccountList.empty(userWalletId) + account).getOrNull()!!
|
||||
val accountId = account.accountId
|
||||
|
||||
val updatedAccountList = (accountList - account).getOrNull()!!
|
||||
|
|
@ -103,7 +100,7 @@ class ArchiveCryptoPortfolioUseCaseTest {
|
|||
@Test
|
||||
fun `invoke should return error if account not found`() = runTest {
|
||||
// Arrange
|
||||
val accountList = AccountList.empty(userWallet)
|
||||
val accountList = AccountList.empty(userWalletId)
|
||||
val accountId = AccountId.forCryptoPortfolio(
|
||||
userWalletId = userWalletId,
|
||||
derivationIndex = DerivationIndex(1).getOrNull()!!,
|
||||
|
|
@ -126,7 +123,7 @@ class ArchiveCryptoPortfolioUseCaseTest {
|
|||
fun `invoke should return error if saveAccounts throws exception`() = runTest {
|
||||
// Arrange
|
||||
val account = createAccount(userWalletId)
|
||||
val accountList = (AccountList.empty(userWallet) + account).getOrNull()!!
|
||||
val accountList = (AccountList.empty(userWalletId) + account).getOrNull()!!
|
||||
val accountId = account.accountId
|
||||
|
||||
val updatedAccountList = (accountList - account).getOrNull()!!
|
||||
|
|
|
|||
|
|
@ -8,11 +8,11 @@ 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.tokens.MainAccountTokensMigration
|
||||
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
|
||||
|
|
@ -27,20 +27,22 @@ import org.junit.jupiter.api.TestInstance
|
|||
class RecoverCryptoPortfolioUseCaseTest {
|
||||
|
||||
private val crudRepository: AccountsCRUDRepository = mockk(relaxUnitFun = true)
|
||||
private val useCase = RecoverCryptoPortfolioUseCase(crudRepository)
|
||||
private val userWallet = mockk<UserWallet>()
|
||||
private val mainAccountTokensMigration: MainAccountTokensMigration = mockk()
|
||||
private val useCase = RecoverCryptoPortfolioUseCase(
|
||||
crudRepository = crudRepository,
|
||||
mainAccountTokensMigration = mainAccountTokensMigration,
|
||||
)
|
||||
|
||||
@BeforeEach
|
||||
fun resetMocks() {
|
||||
clearMocks(crudRepository, userWallet)
|
||||
every { userWallet.walletId } returns userWalletId
|
||||
clearMocks(crudRepository)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke should recover archived crypto portfolio account`() = runTest {
|
||||
// Arrange
|
||||
val account = createAccount(userWalletId)
|
||||
val accountList = AccountList.empty(userWallet)
|
||||
val accountList = AccountList.empty(userWalletId)
|
||||
val archivedAccount = ArchivedAccount(
|
||||
accountId = account.accountId,
|
||||
name = account.accountName,
|
||||
|
|
@ -54,6 +56,7 @@ class RecoverCryptoPortfolioUseCaseTest {
|
|||
|
||||
coEvery { crudRepository.getAccountListSync(userWalletId) } returns accountList.toOption()
|
||||
coEvery { crudRepository.getArchivedAccountSync(account.accountId) } returns archivedAccount.toOption()
|
||||
coEvery { mainAccountTokensMigration.migrate(userWalletId, account.derivationIndex) } returns Unit.right()
|
||||
|
||||
// Act
|
||||
val actual = useCase(account.accountId)
|
||||
|
|
@ -62,7 +65,7 @@ class RecoverCryptoPortfolioUseCaseTest {
|
|||
val expected = account.right()
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
|
||||
coVerifyOrder {
|
||||
coVerifySequence {
|
||||
crudRepository.getAccountListSync(userWalletId)
|
||||
crudRepository.getArchivedAccountSync(account.accountId)
|
||||
crudRepository.saveAccounts(updatedAccountList)
|
||||
|
|
@ -80,13 +83,14 @@ class RecoverCryptoPortfolioUseCaseTest {
|
|||
coEvery { crudRepository.getAccountListSync(userWalletId) } returns None
|
||||
|
||||
// Act
|
||||
val actual = useCase(accountId)
|
||||
val actual = useCase(accountId).leftOrNull() as Error.DataOperationFailed
|
||||
|
||||
// Assert
|
||||
val expected = Error.CriticalTechError.AccountsNotCreated(userWalletId).left()
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
val expected = IllegalStateException("Account list not found for wallet $userWalletId")
|
||||
Truth.assertThat(actual.cause).isInstanceOf(expected::class.java)
|
||||
Truth.assertThat(actual.cause).hasMessageThat().isEqualTo(expected.message)
|
||||
|
||||
coVerifyOrder { crudRepository.getAccountListSync(userWalletId) }
|
||||
coVerifySequence { crudRepository.getAccountListSync(userWalletId) }
|
||||
coVerify(inverse = true) {
|
||||
crudRepository.getArchivedAccountSync(any())
|
||||
crudRepository.saveAccounts(any())
|
||||
|
|
@ -111,7 +115,7 @@ class RecoverCryptoPortfolioUseCaseTest {
|
|||
val expected = Error.DataOperationFailed(exception).left()
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
|
||||
coVerifyOrder { crudRepository.getAccountListSync(userWalletId) }
|
||||
coVerifySequence { crudRepository.getAccountListSync(userWalletId) }
|
||||
coVerify(inverse = true) {
|
||||
crudRepository.getArchivedAccountSync(any())
|
||||
crudRepository.saveAccounts(any())
|
||||
|
|
@ -122,7 +126,7 @@ class RecoverCryptoPortfolioUseCaseTest {
|
|||
fun `invoke should return error if getArchivedAccount throws exception`() = runTest {
|
||||
// Arrange
|
||||
val account = createAccount(userWalletId)
|
||||
val accountList = AccountList.empty(userWallet)
|
||||
val accountList = AccountList.empty(userWalletId)
|
||||
val exception = IllegalStateException("Test error")
|
||||
|
||||
coEvery { crudRepository.getAccountListSync(userWalletId) } returns accountList.toOption()
|
||||
|
|
@ -135,7 +139,7 @@ class RecoverCryptoPortfolioUseCaseTest {
|
|||
val expected = Error.DataOperationFailed(exception).left()
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
|
||||
coVerifyOrder {
|
||||
coVerifySequence {
|
||||
crudRepository.getAccountListSync(userWalletId)
|
||||
crudRepository.getArchivedAccountSync(account.accountId)
|
||||
}
|
||||
|
|
@ -146,19 +150,20 @@ class RecoverCryptoPortfolioUseCaseTest {
|
|||
fun `invoke should return error if getArchivedAccount returns null`() = runTest {
|
||||
// Arrange
|
||||
val account = createAccount(userWalletId)
|
||||
val accountList = AccountList.empty(userWallet)
|
||||
val accountList = AccountList.empty(userWalletId)
|
||||
|
||||
coEvery { crudRepository.getAccountListSync(userWalletId) } returns accountList.toOption()
|
||||
coEvery { crudRepository.getArchivedAccountSync(account.accountId) } returns None
|
||||
|
||||
// Act
|
||||
val actual = useCase(account.accountId)
|
||||
val actual = useCase(account.accountId).leftOrNull() as Error.DataOperationFailed
|
||||
|
||||
// Assert
|
||||
val expected = Error.CriticalTechError.AccountNotFound(account.accountId).left()
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
val expected = IllegalStateException("Account not found: ${account.accountId}")
|
||||
Truth.assertThat(actual.cause).isInstanceOf(expected::class.java)
|
||||
Truth.assertThat(actual.cause).hasMessageThat().isEqualTo(expected.message)
|
||||
|
||||
coVerifyOrder {
|
||||
coVerifySequence {
|
||||
crudRepository.getAccountListSync(userWalletId)
|
||||
crudRepository.getArchivedAccountSync(account.accountId)
|
||||
}
|
||||
|
|
@ -169,7 +174,7 @@ class RecoverCryptoPortfolioUseCaseTest {
|
|||
fun `invoke should return error if saveAccounts throws exception`() = runTest {
|
||||
// Arrange
|
||||
val account = createAccount(userWalletId)
|
||||
val accountList = AccountList.empty(userWallet)
|
||||
val accountList = AccountList.empty(userWalletId)
|
||||
val archivedAccount = ArchivedAccount(
|
||||
accountId = account.accountId,
|
||||
name = account.accountName,
|
||||
|
|
@ -193,7 +198,7 @@ class RecoverCryptoPortfolioUseCaseTest {
|
|||
val expected = Error.DataOperationFailed(exception).left()
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
|
||||
coVerifyOrder {
|
||||
coVerifySequence {
|
||||
crudRepository.getAccountListSync(userWalletId)
|
||||
crudRepository.getArchivedAccountSync(account.accountId)
|
||||
crudRepository.saveAccounts(updatedAccountList)
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ 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
|
||||
|
|
@ -29,19 +28,15 @@ 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
|
||||
clearMocks(crudRepository)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke should update crypto portfolio account with new name`() = runTest {
|
||||
// Arrange
|
||||
val accountList = AccountList.empty(userWallet = userWallet)
|
||||
val accountList = AccountList.empty(userWalletId = userWalletId)
|
||||
val accountId = accountList.mainAccount.accountId
|
||||
|
||||
val newAccountName = AccountName("New name").getOrNull()!!
|
||||
|
|
@ -66,7 +61,7 @@ class UpdateCryptoPortfolioUseCaseTest {
|
|||
@Test
|
||||
fun `invoke should update crypto portfolio account with new icon`() = runTest {
|
||||
// Arrange
|
||||
val accountList = AccountList.empty(userWallet = userWallet)
|
||||
val accountList = AccountList.empty(userWalletId = userWalletId)
|
||||
val accountId = accountList.mainAccount.accountId
|
||||
|
||||
val newAccountIcon = CryptoPortfolioIcon.ofCustomAccount(
|
||||
|
|
@ -94,7 +89,7 @@ class UpdateCryptoPortfolioUseCaseTest {
|
|||
@Test
|
||||
fun `invoke should update crypto portfolio account with new name and icon`() = runTest {
|
||||
// Arrange
|
||||
val accountList = AccountList.empty(userWallet = userWallet)
|
||||
val accountList = AccountList.empty(userWalletId = userWalletId)
|
||||
val accountId = accountList.mainAccount.accountId
|
||||
|
||||
val newAccountName = AccountName("New name").getOrNull()!!
|
||||
|
|
@ -123,7 +118,7 @@ class UpdateCryptoPortfolioUseCaseTest {
|
|||
@Test
|
||||
fun `invoke if name and icon are null`() = runTest {
|
||||
// Arrange
|
||||
val accountList = AccountList.empty(userWallet = userWallet)
|
||||
val accountList = AccountList.empty(userWalletId = userWalletId)
|
||||
val accountId = accountList.mainAccount.accountId
|
||||
|
||||
coEvery { crudRepository.getAccountListSync(userWalletId = userWalletId) } returns accountList.toOption()
|
||||
|
|
@ -144,7 +139,7 @@ class UpdateCryptoPortfolioUseCaseTest {
|
|||
@Test
|
||||
fun `invoke if getAccounts throws exception`() = runTest {
|
||||
// Arrange
|
||||
val accountList = AccountList.empty(userWallet = userWallet)
|
||||
val accountList = AccountList.empty(userWalletId = userWalletId)
|
||||
val accountId = accountList.mainAccount.accountId
|
||||
|
||||
val newAccountName = AccountName("New name").getOrNull()!!
|
||||
|
|
@ -192,7 +187,7 @@ class UpdateCryptoPortfolioUseCaseTest {
|
|||
@Test
|
||||
fun `invoke if getAccounts does not contain accountId`() = runTest {
|
||||
// Arrange
|
||||
val accountList = AccountList.empty(userWallet = userWallet)
|
||||
val accountList = AccountList.empty(userWalletId = userWalletId)
|
||||
val accountId = AccountId.forCryptoPortfolio(
|
||||
userWalletId = userWalletId,
|
||||
derivationIndex = DerivationIndex(1).getOrNull()!!,
|
||||
|
|
@ -217,7 +212,7 @@ class UpdateCryptoPortfolioUseCaseTest {
|
|||
@Test
|
||||
fun `invoke if saveAccounts throws exception`() = runTest {
|
||||
// Arrange
|
||||
val accountList = AccountList.empty(userWallet = userWallet)
|
||||
val accountList = AccountList.empty(userWalletId = userWalletId)
|
||||
val accountId = accountList.mainAccount.accountId
|
||||
|
||||
val newAccountName = AccountName("New name").getOrNull()!!
|
||||
|
|
|
|||
|
|
@ -24,13 +24,17 @@ dependencies {
|
|||
api(projects.domain.networks)
|
||||
api(projects.domain.staking)
|
||||
api(projects.domain.tokens)
|
||||
api(projects.domain.wallets)
|
||||
|
||||
implementation(projects.libs.blockchainSdk)
|
||||
implementation(projects.libs.crypto)
|
||||
|
||||
implementation(deps.kotlin.datetime)
|
||||
implementation(deps.kotlin.serialization)
|
||||
implementation(deps.timber)
|
||||
|
||||
implementation(tangemDeps.blockchain)
|
||||
|
||||
// region DI
|
||||
implementation(deps.hilt.android)
|
||||
kapt(deps.hilt.kapt)
|
||||
|
|
|
|||
|
|
@ -1,10 +1,22 @@
|
|||
package com.tangem.domain.account.status.di
|
||||
|
||||
import com.tangem.domain.account.repository.AccountsCRUDRepository
|
||||
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
|
||||
import com.tangem.domain.account.status.usecase.GetAccountCurrencyByAddressUseCase
|
||||
import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase
|
||||
import com.tangem.domain.account.status.usecase.SaveCryptoCurrenciesUseCase
|
||||
import com.tangem.domain.account.supplier.SingleAccountListSupplier
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher
|
||||
import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier
|
||||
import com.tangem.domain.networks.utils.NetworksCleaner
|
||||
import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher
|
||||
import com.tangem.domain.staking.StakingIdFactory
|
||||
import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher
|
||||
import com.tangem.domain.staking.utils.StakingCleaner
|
||||
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
||||
import com.tangem.domain.wallets.derivations.DerivationsRepository
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -31,7 +43,39 @@ internal object AccountStatusUseCaseModule {
|
|||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGetAccountCurrencyStatusUseCase(): GetAccountCurrencyStatusUseCase {
|
||||
return GetAccountCurrencyStatusUseCase()
|
||||
fun provideGetAccountCurrencyStatusUseCase(
|
||||
singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
|
||||
): GetAccountCurrencyStatusUseCase {
|
||||
return GetAccountCurrencyStatusUseCase(singleAccountStatusListSupplier = singleAccountStatusListSupplier)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSaveCryptoCurrenciesUseCase(
|
||||
singleAccountListSupplier: SingleAccountListSupplier,
|
||||
accountsCRUDRepository: AccountsCRUDRepository,
|
||||
currenciesRepository: CurrenciesRepository,
|
||||
derivationsRepository: DerivationsRepository,
|
||||
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
|
||||
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
|
||||
multiYieldBalanceFetcher: MultiYieldBalanceFetcher,
|
||||
stakingIdFactory: StakingIdFactory,
|
||||
networksCleaner: NetworksCleaner,
|
||||
stakingCleaner: StakingCleaner,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): SaveCryptoCurrenciesUseCase {
|
||||
return SaveCryptoCurrenciesUseCase(
|
||||
singleAccountListSupplier = singleAccountListSupplier,
|
||||
accountsCRUDRepository = accountsCRUDRepository,
|
||||
currenciesRepository = currenciesRepository,
|
||||
derivationsRepository = derivationsRepository,
|
||||
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
|
||||
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
|
||||
multiYieldBalanceFetcher = multiYieldBalanceFetcher,
|
||||
stakingIdFactory = stakingIdFactory,
|
||||
networksCleaner = networksCleaner,
|
||||
stakingCleaner = stakingCleaner,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ import com.tangem.domain.account.models.AccountStatusList
|
|||
import com.tangem.domain.account.producer.SingleAccountListProducer
|
||||
import com.tangem.domain.account.status.utils.CryptoCurrencyStatusesFlowFactory
|
||||
import com.tangem.domain.account.supplier.SingleAccountListSupplier
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.core.utils.lceContent
|
||||
import com.tangem.domain.models.StatusSource
|
||||
import com.tangem.domain.models.TokensGroupType
|
||||
|
|
@ -39,6 +40,7 @@ import java.math.BigDecimal
|
|||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
internal class DefaultSingleAccountStatusListProducer @AssistedInject constructor(
|
||||
@Assisted private val params: SingleAccountStatusListProducer.Params,
|
||||
private val userWalletsListRepository: UserWalletsListRepository,
|
||||
private val singleAccountListSupplier: SingleAccountListSupplier,
|
||||
private val cryptoCurrencyStatusesFlowFactory: CryptoCurrencyStatusesFlowFactory,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
|
|
@ -58,8 +60,12 @@ internal class DefaultSingleAccountStatusListProducer @AssistedInject constructo
|
|||
if (account.cryptoCurrencies.isEmpty()) {
|
||||
createEmptyAccountStatusFlow(account)
|
||||
} else {
|
||||
val userWallet = userWalletsListRepository.userWalletsSync().first {
|
||||
it.walletId == params.userWalletId
|
||||
}
|
||||
|
||||
getAccountStatusFlow(
|
||||
userWallet = accountList.userWallet,
|
||||
userWallet = userWallet,
|
||||
account = account,
|
||||
groupType = accountList.groupType,
|
||||
sortType = accountList.sortType,
|
||||
|
|
@ -71,7 +77,7 @@ internal class DefaultSingleAccountStatusListProducer @AssistedInject constructo
|
|||
val balances = accountStatuses.map { it.tokenList.totalFiatBalance }
|
||||
|
||||
AccountStatusList(
|
||||
userWallet = accountList.userWallet,
|
||||
userWalletId = accountList.userWalletId,
|
||||
accountStatuses = accountStatuses.toSet(),
|
||||
totalAccounts = accountList.totalAccounts,
|
||||
totalFiatBalance = TotalFiatBalanceCalculator.calculate(balances),
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ package com.tangem.domain.account.status.supplier
|
|||
import com.tangem.domain.account.models.AccountStatusList
|
||||
import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer
|
||||
import com.tangem.domain.core.flow.FlowCachingSupplier
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
* Supplier that provides a single [AccountStatusList] for a specific user wallet.
|
||||
|
|
@ -12,4 +14,10 @@ import com.tangem.domain.core.flow.FlowCachingSupplier
|
|||
abstract class SingleAccountStatusListSupplier(
|
||||
override val factory: SingleAccountStatusListProducer.Factory,
|
||||
override val keyCreator: (SingleAccountStatusListProducer.Params) -> String,
|
||||
) : FlowCachingSupplier<SingleAccountStatusListProducer, SingleAccountStatusListProducer.Params, AccountStatusList>()
|
||||
) : FlowCachingSupplier<SingleAccountStatusListProducer, SingleAccountStatusListProducer.Params, AccountStatusList>() {
|
||||
|
||||
operator fun invoke(userWalletId: UserWalletId): Flow<AccountStatusList> {
|
||||
val params = SingleAccountStatusListProducer.Params(userWalletId)
|
||||
return this.invoke(params)
|
||||
}
|
||||
}
|
||||
|
|
@ -117,7 +117,7 @@ class GetAccountCurrencyByAddressUseCase(
|
|||
.firstOrNull()
|
||||
|
||||
return ensureNotNull(result) {
|
||||
"No account found for network: $networkId in walletId: ${accountList.userWallet.walletId}"
|
||||
"No account found for network: $networkId in walletId: ${accountList.userWalletId}"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,24 +2,149 @@ package com.tangem.domain.account.status.usecase
|
|||
|
||||
import arrow.core.Option
|
||||
import arrow.core.none
|
||||
import arrow.core.toOption
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchainsdk.utils.fromNetworkId
|
||||
import com.tangem.domain.account.models.AccountStatusList
|
||||
import com.tangem.domain.account.status.model.AccountCryptoCurrencyStatus
|
||||
import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer
|
||||
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.account.AccountStatus
|
||||
import com.tangem.domain.models.account.DerivationIndex
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.lib.crypto.derivation.AccountNodeRecognizer
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
|
||||
/**
|
||||
* Use case to retrieve the status of a specific cryptocurrency associated with an account.
|
||||
*
|
||||
* @property singleAccountStatusListSupplier supplier to get the list of account statuses.
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
// TODO: Implement [REDACTED_JIRA]
|
||||
class GetAccountCurrencyStatusUseCase {
|
||||
class GetAccountCurrencyStatusUseCase(
|
||||
private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
|
||||
) {
|
||||
|
||||
/**
|
||||
* Invokes the use case to get the [AccountCryptoCurrencyStatus] for the given [currencyId].
|
||||
* Invokes the use case to get the status of a specific cryptocurrency for a given user wallet.
|
||||
*
|
||||
* @param currencyId The ID of the cryptocurrency to look up.
|
||||
*
|
||||
* @return An [Option] containing the [AccountCryptoCurrencyStatus] if found,
|
||||
* or [arrow.core.None] if not found or if any validation fails.
|
||||
* @param userWalletId the ID of the user wallet.
|
||||
* @param currency the cryptocurrency for which the status is to be retrieved.
|
||||
* @return an [Option] containing [AccountCryptoCurrencyStatus] if found, otherwise None.
|
||||
*/
|
||||
suspend operator fun invoke(currencyId: CryptoCurrency.ID): Option<AccountCryptoCurrencyStatus> = none()
|
||||
suspend fun invokeSync(userWalletId: UserWalletId, currency: CryptoCurrency): Option<AccountCryptoCurrencyStatus> {
|
||||
return invokeSync(userWalletId = userWalletId, currencyId = currency.id, network = currency.network)
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes the use case to get the status of a specific cryptocurrency by its ID for a given user wallet and network.
|
||||
* If the [network] is null, it searches across all accounts for the cryptocurrency.
|
||||
*
|
||||
* @param userWalletId the ID of the user wallet.
|
||||
* @param currencyId the ID of the cryptocurrency.
|
||||
* @param network the network associated with the cryptocurrency, can be null.
|
||||
* @return an [Option] containing [AccountCryptoCurrencyStatus] if found, otherwise None.
|
||||
*/
|
||||
suspend fun invokeSync(
|
||||
userWalletId: UserWalletId,
|
||||
currencyId: CryptoCurrency.ID,
|
||||
network: Network?,
|
||||
): Option<AccountCryptoCurrencyStatus> {
|
||||
val accountStatusList = singleAccountStatusListSupplier.getSyncOrNull(
|
||||
params = SingleAccountStatusListProducer.Params(userWalletId),
|
||||
) ?: return none()
|
||||
|
||||
return accountStatusList
|
||||
.toAccountCryptoCurrencyStatus(currencyId, network)
|
||||
.toOption()
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the status of a specific cryptocurrency for a given user wallet as a [Flow].
|
||||
*
|
||||
* @param userWalletId The ID of the user wallet.
|
||||
* @param currency The cryptocurrency for which the status is to be retrieved.
|
||||
* @return A [Flow] emitting [AccountCryptoCurrencyStatus] if found.
|
||||
*/
|
||||
operator fun invoke(userWalletId: UserWalletId, currency: CryptoCurrency): Flow<AccountCryptoCurrencyStatus> {
|
||||
return invoke(userWalletId = userWalletId, currencyId = currency.id, network = currency.network)
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the status of a specific cryptocurrency by its ID for a given user wallet and network as a [Flow].
|
||||
*
|
||||
* @param userWalletId The ID of the user wallet.
|
||||
* @param currencyId The ID of the cryptocurrency.
|
||||
* @param network The network associated with the cryptocurrency, can be null.
|
||||
* @return A [Flow] emitting [AccountCryptoCurrencyStatus] if found.
|
||||
*/
|
||||
operator fun invoke(
|
||||
userWalletId: UserWalletId,
|
||||
currencyId: CryptoCurrency.ID,
|
||||
network: Network?,
|
||||
): Flow<AccountCryptoCurrencyStatus> {
|
||||
return singleAccountStatusListSupplier(
|
||||
params = SingleAccountStatusListProducer.Params(userWalletId),
|
||||
)
|
||||
.mapNotNull { accountStatusList ->
|
||||
accountStatusList.toAccountCryptoCurrencyStatus(currencyId, network)
|
||||
}
|
||||
}
|
||||
|
||||
private fun AccountStatusList.toAccountCryptoCurrencyStatus(
|
||||
currencyId: CryptoCurrency.ID,
|
||||
network: Network?,
|
||||
): AccountCryptoCurrencyStatus? {
|
||||
return getExpectedAccountStatuses(network)
|
||||
.asSequence()
|
||||
.filterIsInstance<AccountStatus.CryptoPortfolio>()
|
||||
.mapNotNull { accountStatus ->
|
||||
val status = accountStatus.flattenCurrencies().firstOrNull { it.currency.id == currencyId }
|
||||
?: return@mapNotNull null
|
||||
|
||||
AccountCryptoCurrencyStatus(account = accountStatus.account, status = status)
|
||||
}
|
||||
.firstOrNull()
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the expected account statuses based on the provided [network].
|
||||
* If the [network] is null, all account statuses are returned.
|
||||
* If the network has a specific derivation index, it filters the accounts accordingly.
|
||||
*
|
||||
* @param network the network to filter accounts by, can be null.
|
||||
* @return a set of [AccountStatus] that match the expected criteria.
|
||||
*/
|
||||
private fun AccountStatusList.getExpectedAccountStatuses(network: Network?): Set<AccountStatus> {
|
||||
val possibleAccountIndex = network?.getAccountIndexOrNull()
|
||||
|
||||
return when (possibleAccountIndex) {
|
||||
// currency can be in any account
|
||||
null -> accountStatuses
|
||||
// currency only in the main account
|
||||
DerivationIndex.Main.value -> setOf(mainAccount)
|
||||
// currency only in the account with specific derivation index or in the main account
|
||||
else -> {
|
||||
val accountStatus = accountStatuses.firstOrNull {
|
||||
val cryptoPortfolio = it.account as? Account.CryptoPortfolio ?: return@firstOrNull false
|
||||
|
||||
cryptoPortfolio.derivationIndex.value == possibleAccountIndex
|
||||
}
|
||||
|
||||
setOfNotNull(accountStatus, mainAccount)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Network.getAccountIndexOrNull(): Int? {
|
||||
val blockchain = Blockchain.fromNetworkId(networkId = rawId) ?: return null
|
||||
val recognizer = AccountNodeRecognizer(blockchain)
|
||||
|
||||
return recognizer.recognize(derivationPath)?.toInt()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,269 @@
|
|||
package com.tangem.domain.account.status.usecase
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.raise.Raise
|
||||
import arrow.core.raise.catch
|
||||
import com.tangem.domain.account.producer.SingleAccountListProducer
|
||||
import com.tangem.domain.account.repository.AccountsCRUDRepository
|
||||
import com.tangem.domain.account.supplier.SingleAccountListSupplier
|
||||
import com.tangem.domain.core.utils.eitherOn
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.account.AccountId
|
||||
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.networks.utils.NetworksCleaner
|
||||
import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher
|
||||
import com.tangem.domain.staking.StakingIdFactory
|
||||
import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher
|
||||
import com.tangem.domain.staking.utils.StakingCleaner
|
||||
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
||||
import com.tangem.domain.wallets.derivations.DerivationsRepository
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.*
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Use case for saving crypto currencies to a specific account.
|
||||
*
|
||||
* @property singleAccountListSupplier Supplier to get account details.
|
||||
* @property currenciesRepository Repository for managing currencies.
|
||||
* @property derivationsRepository Repository for deriving public keys.
|
||||
* @property multiNetworkStatusFetcher Fetcher for updating network statuses.
|
||||
* @property multiQuoteStatusFetcher Fetcher for updating quote statuses.
|
||||
* @property multiYieldBalanceFetcher Fetcher for updating yield balances.
|
||||
* @property stakingIdFactory Factory for creating staking IDs.
|
||||
* @property networksCleaner Cleaner for removing obsolete network data.
|
||||
* @property stakingCleaner Cleaner for removing obsolete staking data.
|
||||
* @property dispatchers Coroutine dispatchers for managing threading.
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
class SaveCryptoCurrenciesUseCase(
|
||||
private val singleAccountListSupplier: SingleAccountListSupplier,
|
||||
private val accountsCRUDRepository: AccountsCRUDRepository,
|
||||
private val currenciesRepository: CurrenciesRepository,
|
||||
private val derivationsRepository: DerivationsRepository,
|
||||
private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
|
||||
private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
|
||||
private val multiYieldBalanceFetcher: MultiYieldBalanceFetcher,
|
||||
private val stakingIdFactory: StakingIdFactory,
|
||||
private val networksCleaner: NetworksCleaner,
|
||||
private val stakingCleaner: StakingCleaner,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(
|
||||
accountId: AccountId,
|
||||
add: List<CryptoCurrency>,
|
||||
remove: List<CryptoCurrency>,
|
||||
): Either<Throwable, Unit> = eitherOn(dispatchers.default) {
|
||||
if (add.isEmpty() && remove.isEmpty()) {
|
||||
Timber.d("No currencies to add or remove, skipping")
|
||||
return@eitherOn
|
||||
}
|
||||
|
||||
val userWalletId = accountId.userWalletId
|
||||
withContext(NonCancellable) {
|
||||
val account = getAccount(accountId = accountId)
|
||||
|
||||
val modifiedCurrencyList = account.cryptoCurrencies.modify(add = add, remove = remove)
|
||||
|
||||
saveAccount(
|
||||
account = account.copy(cryptoCurrencies = modifiedCurrencyList.total.toSet()),
|
||||
)
|
||||
|
||||
derivePublicKeys(userWalletId = userWalletId, currencies = modifiedCurrencyList.added)
|
||||
|
||||
val jobs = refreshBalances(userWalletId = userWalletId, currencies = modifiedCurrencyList.added) +
|
||||
clearMetadata(userWalletId = userWalletId, currencies = modifiedCurrencyList.removed)
|
||||
|
||||
jobs.joinAll()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun Raise<Throwable>.getAccount(accountId: AccountId): Account.CryptoPortfolio {
|
||||
val accountList = singleAccountListSupplier.getSyncOrNull(
|
||||
params = SingleAccountListProducer.Params(userWalletId = accountId.userWalletId),
|
||||
) ?: raise(IllegalStateException("No accounts for wallet ${accountId.userWalletId}"))
|
||||
|
||||
return accountList.accounts.firstOrNull { it.accountId == accountId } as? Account.CryptoPortfolio
|
||||
?: raise(IllegalStateException("No account with id $accountId"))
|
||||
}
|
||||
|
||||
private fun Set<CryptoCurrency>.modify(
|
||||
add: List<CryptoCurrency>,
|
||||
remove: List<CryptoCurrency>,
|
||||
): ModifiedCurrencyList {
|
||||
val mutableCurrencies = this.toMutableList()
|
||||
val added = mutableListOf<CryptoCurrency>()
|
||||
val removed = mutableListOf<CryptoCurrency>()
|
||||
|
||||
val existingCurrenciesById = mutableCurrencies.associateBy(::TempID)
|
||||
|
||||
add.groupByNetwork { !existingCurrenciesById.containsKey(it) }
|
||||
.forEach { (network, currenciesById) ->
|
||||
val coinTempId = TempID(network)
|
||||
|
||||
if (!existingCurrenciesById.containsKey(coinTempId)) {
|
||||
val coin = currenciesById[coinTempId]
|
||||
|
||||
if (coin != null) {
|
||||
mutableCurrencies.add(coin)
|
||||
added.add(coin)
|
||||
|
||||
currenciesById.remove(coinTempId)
|
||||
} else {
|
||||
val createdCoin = currenciesRepository.createCoinCurrency(network)
|
||||
mutableCurrencies.add(createdCoin)
|
||||
added.add(createdCoin)
|
||||
}
|
||||
}
|
||||
|
||||
mutableCurrencies.addAll(currenciesById.values)
|
||||
added.addAll(currenciesById.values)
|
||||
}
|
||||
|
||||
remove.groupByNetwork(valuePredicate = existingCurrenciesById::containsKey)
|
||||
.forEach { (network, currenciesById) ->
|
||||
val coinTempId = TempID(network)
|
||||
|
||||
if (currenciesById.containsKey(coinTempId)) {
|
||||
val existingNetworkCurrenciesCount = mutableCurrencies.count { it.network == network }
|
||||
|
||||
if (existingNetworkCurrenciesCount != currenciesById.size) {
|
||||
return@forEach
|
||||
}
|
||||
}
|
||||
|
||||
mutableCurrencies.removeAll(currenciesById.values)
|
||||
removed.addAll(currenciesById.values)
|
||||
}
|
||||
|
||||
return ModifiedCurrencyList(added = added, removed = removed, total = mutableCurrencies)
|
||||
}
|
||||
|
||||
private suspend fun Raise<Throwable>.saveAccount(account: Account.CryptoPortfolio) {
|
||||
catch(
|
||||
block = { accountsCRUDRepository.saveAccount(account) },
|
||||
catch = ::raise,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun Raise<Throwable>.derivePublicKeys(
|
||||
userWalletId: UserWalletId,
|
||||
currencies: List<CryptoCurrency>,
|
||||
) {
|
||||
catch(
|
||||
block = { derivationsRepository.derivePublicKeys(userWalletId = userWalletId, currencies = currencies) },
|
||||
catch = ::raise,
|
||||
)
|
||||
}
|
||||
|
||||
private fun List<CryptoCurrency>.groupByNetwork(
|
||||
valuePredicate: (TempID) -> Boolean,
|
||||
): LinkedHashMap<Network, MutableMap<TempID, CryptoCurrency>> {
|
||||
val destination = LinkedHashMap<Network, MutableMap<TempID, CryptoCurrency>>()
|
||||
|
||||
for (currency in this) {
|
||||
val key = currency.network
|
||||
val mutableMap = destination.getOrPut(key) { mutableMapOf() }
|
||||
|
||||
val id = TempID(currency)
|
||||
|
||||
if (valuePredicate(id)) {
|
||||
mutableMap.put(id, currency)
|
||||
}
|
||||
}
|
||||
|
||||
return destination
|
||||
}
|
||||
|
||||
private suspend fun refreshBalances(userWalletId: UserWalletId, currencies: List<CryptoCurrency>): List<Job> {
|
||||
if (currencies.isEmpty()) return emptyList()
|
||||
|
||||
return coroutineScope {
|
||||
listOf(
|
||||
launch { refreshNetworks(userWalletId = userWalletId, currencies = currencies) },
|
||||
launch { refreshYieldBalances(userWalletId = userWalletId, currencies = currencies) },
|
||||
launch { refreshQuotes(currencies = currencies) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun refreshNetworks(userWalletId: UserWalletId, currencies: List<CryptoCurrency>) {
|
||||
multiNetworkStatusFetcher(
|
||||
params = MultiNetworkStatusFetcher.Params(
|
||||
userWalletId = userWalletId,
|
||||
networks = currencies.mapTo(hashSetOf(), CryptoCurrency::network),
|
||||
),
|
||||
)
|
||||
|
||||
currenciesRepository.syncTokens(userWalletId)
|
||||
}
|
||||
|
||||
private suspend fun refreshYieldBalances(userWalletId: UserWalletId, currencies: List<CryptoCurrency>) {
|
||||
val stakingIds = currencies.mapNotNullTo(hashSetOf()) {
|
||||
stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = it).getOrNull()
|
||||
}
|
||||
|
||||
multiYieldBalanceFetcher(
|
||||
params = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = stakingIds),
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun refreshQuotes(currencies: List<CryptoCurrency>) {
|
||||
multiQuoteStatusFetcher(
|
||||
params = MultiQuoteStatusFetcher.Params(
|
||||
currenciesIds = currencies.mapNotNullTo(hashSetOf()) { it.id.rawCurrencyId },
|
||||
appCurrencyId = null,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun clearMetadata(userWalletId: UserWalletId, currencies: List<CryptoCurrency>): List<Job> {
|
||||
if (currencies.isEmpty()) return emptyList()
|
||||
|
||||
return coroutineScope {
|
||||
listOf(
|
||||
launch { networksCleaner(userWalletId = userWalletId, currencies = currencies) },
|
||||
launch { clearStaking(userWalletId = userWalletId, currencies = currencies) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun clearStaking(userWalletId: UserWalletId, currencies: List<CryptoCurrency>) {
|
||||
val stakingIds = currencies.mapNotNullTo(hashSetOf()) {
|
||||
stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = it).getOrNull()
|
||||
}
|
||||
|
||||
stakingCleaner(userWalletId = userWalletId, stakingIds = stakingIds)
|
||||
}
|
||||
|
||||
private data class TempID(
|
||||
val networkId: String,
|
||||
val derivationPath: Network.DerivationPath,
|
||||
val contractAddress: String?,
|
||||
) {
|
||||
|
||||
constructor(network: Network) : this(
|
||||
networkId = network.backendId,
|
||||
derivationPath = network.derivationPath,
|
||||
contractAddress = null,
|
||||
)
|
||||
|
||||
constructor(currency: CryptoCurrency) : this(
|
||||
networkId = currency.network.backendId,
|
||||
derivationPath = currency.network.derivationPath,
|
||||
contractAddress = (currency as? CryptoCurrency.Token)?.contractAddress,
|
||||
)
|
||||
}
|
||||
|
||||
private data class ModifiedCurrencyList(
|
||||
val added: List<CryptoCurrency>,
|
||||
val removed: List<CryptoCurrency>,
|
||||
val total: List<CryptoCurrency>,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,198 @@
|
|||
package com.tangem.domain.account.status.producer
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.common.test.utils.getEmittedValues
|
||||
import com.tangem.domain.account.models.AccountStatusList
|
||||
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import io.mockk.*
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
|
||||
@Suppress("UnusedFlow")
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
class DefaultMultiAccountStatusListProducerTest {
|
||||
|
||||
private val userWalletsListRepository: UserWalletsListRepository = mockk()
|
||||
private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier = mockk()
|
||||
private val dispatchers = TestingCoroutineDispatcherProvider()
|
||||
|
||||
private val userWalletId1 = UserWalletId("001")
|
||||
private val userWallet1 = mockk<UserWallet> {
|
||||
every { walletId } returns userWalletId1
|
||||
}
|
||||
|
||||
private val userWalletId2 = UserWalletId("002")
|
||||
private val userWallet2 = mockk<UserWallet> {
|
||||
every { walletId } returns userWalletId2
|
||||
}
|
||||
|
||||
private val producer = DefaultMultiAccountStatusListProducer(
|
||||
params = Unit,
|
||||
userWalletsListRepository = userWalletsListRepository,
|
||||
singleAccountStatusListSupplier = singleAccountStatusListSupplier,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
clearMocks(userWalletsListRepository, singleAccountStatusListSupplier)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `produce returns status lists for all user wallets`() = runTest {
|
||||
// Arrange
|
||||
val wallets = listOf(userWallet1, userWallet2)
|
||||
val walletsFlow = MutableStateFlow(wallets)
|
||||
|
||||
every { userWalletsListRepository.userWallets } returns walletsFlow
|
||||
val accountStatusList1 = mockk<AccountStatusList>()
|
||||
val accountStatusList2 = mockk<AccountStatusList>()
|
||||
|
||||
every {
|
||||
singleAccountStatusListSupplier(
|
||||
params = SingleAccountStatusListProducer.Params(userWalletId1),
|
||||
)
|
||||
} returns flowOf(accountStatusList1)
|
||||
|
||||
every {
|
||||
singleAccountStatusListSupplier(
|
||||
params = SingleAccountStatusListProducer.Params(userWalletId2),
|
||||
)
|
||||
} returns flowOf(accountStatusList2)
|
||||
|
||||
// Act
|
||||
val actual = producer.produce().let(::getEmittedValues)
|
||||
|
||||
// Assert
|
||||
val expected = listOf(accountStatusList1, accountStatusList2)
|
||||
Truth.assertThat(actual).containsExactly(expected)
|
||||
|
||||
coVerify(ordering = Ordering.SEQUENCE) {
|
||||
userWalletsListRepository.userWallets
|
||||
singleAccountStatusListSupplier(
|
||||
params = SingleAccountStatusListProducer.Params(userWalletId1),
|
||||
)
|
||||
singleAccountStatusListSupplier(
|
||||
params = SingleAccountStatusListProducer.Params(userWalletId2),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `produce returns empty flow if userWallets is empty list`() = runTest {
|
||||
// Arrange
|
||||
val walletsFlow = MutableStateFlow<List<UserWallet>>(emptyList())
|
||||
every { userWalletsListRepository.userWallets } returns walletsFlow
|
||||
|
||||
// Act
|
||||
val actual = producer.produce().let(::getEmittedValues)
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).isEmpty()
|
||||
|
||||
coVerify(ordering = Ordering.SEQUENCE) {
|
||||
userWalletsListRepository.userWallets
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `produce returns empty flow if userWallets is null`() = runTest {
|
||||
// Arrange
|
||||
val walletsFlow = MutableStateFlow<List<UserWallet>?>(null)
|
||||
every { userWalletsListRepository.userWallets } returns walletsFlow
|
||||
|
||||
// Act
|
||||
val actual = producer.produce().let(::getEmittedValues)
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).isEmpty()
|
||||
|
||||
coVerify(ordering = Ordering.SEQUENCE) {
|
||||
userWalletsListRepository.userWallets
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `flow will updated if userWallets are updated`() = runTest {
|
||||
// Arrange
|
||||
val userWalletId3 = UserWalletId("003")
|
||||
val userWallet3 = mockk<UserWallet> { every { walletId } returns userWalletId3 }
|
||||
|
||||
val walletsFlow = MutableStateFlow(listOf(userWallet1, userWallet2))
|
||||
every { userWalletsListRepository.userWallets } returns walletsFlow
|
||||
|
||||
val accountStatusList1 = mockk<AccountStatusList>()
|
||||
val accountStatusList2 = mockk<AccountStatusList>()
|
||||
val accountStatusList3 = mockk<AccountStatusList>()
|
||||
|
||||
every {
|
||||
singleAccountStatusListSupplier(
|
||||
params = SingleAccountStatusListProducer.Params(userWalletId1),
|
||||
)
|
||||
} returns flowOf(accountStatusList1)
|
||||
|
||||
every {
|
||||
singleAccountStatusListSupplier(
|
||||
params = SingleAccountStatusListProducer.Params(userWalletId2),
|
||||
)
|
||||
} returns flowOf(accountStatusList2)
|
||||
|
||||
every {
|
||||
singleAccountStatusListSupplier(
|
||||
params = SingleAccountStatusListProducer.Params(userWalletId3),
|
||||
)
|
||||
} returns flowOf(accountStatusList3)
|
||||
|
||||
// Act (first emission)
|
||||
val actual1 = producer.produce().let(::getEmittedValues)
|
||||
|
||||
// Assert (first emission)
|
||||
val expected1 = listOf(accountStatusList1, accountStatusList2)
|
||||
Truth.assertThat(actual1).containsExactly(expected1)
|
||||
|
||||
// Act (second emission)
|
||||
walletsFlow.value = listOf(userWallet1, userWallet2, userWallet3)
|
||||
val actual2 = producer.produce().let(::getEmittedValues)
|
||||
|
||||
// Assert (second emission)
|
||||
val expected2 = listOf(accountStatusList1, accountStatusList2, accountStatusList3)
|
||||
Truth.assertThat(actual2).containsExactly(expected2)
|
||||
|
||||
coVerify(ordering = Ordering.SEQUENCE) {
|
||||
userWalletsListRepository.userWallets
|
||||
singleAccountStatusListSupplier(
|
||||
params = SingleAccountStatusListProducer.Params(userWalletId1),
|
||||
)
|
||||
singleAccountStatusListSupplier(
|
||||
params = SingleAccountStatusListProducer.Params(userWalletId2),
|
||||
)
|
||||
singleAccountStatusListSupplier(
|
||||
params = SingleAccountStatusListProducer.Params(userWalletId1),
|
||||
)
|
||||
singleAccountStatusListSupplier(
|
||||
params = SingleAccountStatusListProducer.Params(userWalletId2),
|
||||
)
|
||||
singleAccountStatusListSupplier(
|
||||
params = SingleAccountStatusListProducer.Params(userWalletId3),
|
||||
)
|
||||
userWalletsListRepository.userWallets
|
||||
singleAccountStatusListSupplier(
|
||||
params = SingleAccountStatusListProducer.Params(userWalletId1),
|
||||
)
|
||||
singleAccountStatusListSupplier(
|
||||
params = SingleAccountStatusListProducer.Params(userWalletId2),
|
||||
)
|
||||
singleAccountStatusListSupplier(
|
||||
params = SingleAccountStatusListProducer.Params(userWalletId3),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,249 @@
|
|||
package com.tangem.domain.account.status.producer
|
||||
|
||||
import arrow.core.nonEmptyListOf
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
|
||||
import com.tangem.common.test.utils.getEmittedValues
|
||||
import com.tangem.domain.account.models.AccountList
|
||||
import com.tangem.domain.account.models.AccountStatusList
|
||||
import com.tangem.domain.account.producer.SingleAccountListProducer
|
||||
import com.tangem.domain.account.status.utils.CryptoCurrencyStatusesFlowFactory
|
||||
import com.tangem.domain.account.supplier.SingleAccountListSupplier
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.core.utils.lceContent
|
||||
import com.tangem.domain.core.utils.lceLoading
|
||||
import com.tangem.domain.models.StatusSource
|
||||
import com.tangem.domain.models.TokensSortType
|
||||
import com.tangem.domain.models.TotalFiatBalance
|
||||
import com.tangem.domain.models.account.AccountStatus
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.quote.PriceChange
|
||||
import com.tangem.domain.models.tokenlist.TokenList
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import io.mockk.*
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Suppress("UnusedFlow")
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
class DefaultSingleAccountStatusListProducerTest {
|
||||
|
||||
private val userWalletsListRepository: UserWalletsListRepository = mockk()
|
||||
private val singleAccountListSupplier: SingleAccountListSupplier = mockk()
|
||||
private val cryptoCurrencyStatusesFlowFactory: CryptoCurrencyStatusesFlowFactory = mockk()
|
||||
|
||||
private val userWalletId = UserWalletId("011")
|
||||
private val userWallet = mockk<UserWallet> {
|
||||
every { this@mockk.walletId } returns userWalletId
|
||||
}
|
||||
|
||||
private val producer = DefaultSingleAccountStatusListProducer(
|
||||
params = SingleAccountStatusListProducer.Params(userWalletId),
|
||||
userWalletsListRepository = userWalletsListRepository,
|
||||
singleAccountListSupplier = singleAccountListSupplier,
|
||||
cryptoCurrencyStatusesFlowFactory = cryptoCurrencyStatusesFlowFactory,
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
)
|
||||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
clearMocks(singleAccountListSupplier, cryptoCurrencyStatusesFlowFactory)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `flow is mapped for user wallet id from params`() = runTest {
|
||||
// Arrange
|
||||
val accountList = AccountList.empty(userWalletId = userWalletId)
|
||||
|
||||
every {
|
||||
singleAccountListSupplier(params = SingleAccountListProducer.Params(userWalletId))
|
||||
} returns flowOf(accountList)
|
||||
|
||||
// Act
|
||||
val actual = producer.produce().let(::getEmittedValues)
|
||||
|
||||
// Assert
|
||||
val expected = AccountStatusList(
|
||||
userWalletId = userWalletId,
|
||||
accountStatuses = setOf(
|
||||
AccountStatus.CryptoPortfolio(
|
||||
account = accountList.mainAccount,
|
||||
tokenList = TokenList.Empty,
|
||||
priceChangeLce = PriceChange(value = BigDecimal("0.00"), source = StatusSource.ACTUAL).lceContent(),
|
||||
),
|
||||
),
|
||||
totalAccounts = 1,
|
||||
totalFiatBalance = TotalFiatBalance.Loaded(amount = BigDecimal.ZERO, source = StatusSource.ACTUAL),
|
||||
)
|
||||
Truth.assertThat(actual).containsExactly(expected)
|
||||
|
||||
coVerify(ordering = Ordering.SEQUENCE) {
|
||||
singleAccountListSupplier(params = SingleAccountListProducer.Params(userWalletId))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `flow will updated if balances are updated`() = runTest {
|
||||
// Arrange
|
||||
val accountList = AccountList.empty(userWalletId)
|
||||
val updatedAccountList = AccountList.empty(userWalletId = userWalletId, sortType = TokensSortType.BALANCE)
|
||||
|
||||
val accountListFlow = MutableStateFlow(value = accountList)
|
||||
|
||||
every {
|
||||
singleAccountListSupplier(params = SingleAccountListProducer.Params(userWalletId))
|
||||
} returns accountListFlow
|
||||
|
||||
// Act (first emission)
|
||||
val actual1 = producer.produce().let(::getEmittedValues)
|
||||
|
||||
// Assert (first emission)
|
||||
val expected = AccountStatusList(
|
||||
userWalletId = userWalletId,
|
||||
accountStatuses = setOf(
|
||||
AccountStatus.CryptoPortfolio(
|
||||
account = accountList.mainAccount,
|
||||
tokenList = TokenList.Empty,
|
||||
priceChangeLce = PriceChange(value = BigDecimal("0.00"), source = StatusSource.ACTUAL).lceContent(),
|
||||
),
|
||||
),
|
||||
totalAccounts = 1,
|
||||
totalFiatBalance = TotalFiatBalance.Loaded(amount = BigDecimal.ZERO, source = StatusSource.ACTUAL),
|
||||
)
|
||||
Truth.assertThat(actual1).containsExactly(expected)
|
||||
|
||||
// Act (second emission)
|
||||
accountListFlow.value = updatedAccountList
|
||||
val actual2 = producer.produce().let(::getEmittedValues)
|
||||
|
||||
// Assert (second emission)
|
||||
val expected2 = AccountStatusList(
|
||||
userWalletId = userWalletId,
|
||||
accountStatuses = setOf(
|
||||
AccountStatus.CryptoPortfolio(
|
||||
account = updatedAccountList.mainAccount,
|
||||
tokenList = TokenList.Empty,
|
||||
priceChangeLce = PriceChange(value = BigDecimal("0.00"), source = StatusSource.ACTUAL).lceContent(),
|
||||
),
|
||||
),
|
||||
totalAccounts = 1,
|
||||
totalFiatBalance = TotalFiatBalance.Loaded(amount = BigDecimal.ZERO, source = StatusSource.ACTUAL),
|
||||
)
|
||||
Truth.assertThat(actual2).containsExactly(expected2)
|
||||
|
||||
coVerify(ordering = Ordering.SEQUENCE) {
|
||||
singleAccountListSupplier(params = SingleAccountListProducer.Params(userWalletId))
|
||||
singleAccountListSupplier(params = SingleAccountListProducer.Params(userWalletId))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `flow is filtered the same balance`() = runTest {
|
||||
// Arrange
|
||||
val accountList = AccountList.empty(userWalletId)
|
||||
val accountListFlow = MutableStateFlow(value = accountList)
|
||||
|
||||
every {
|
||||
singleAccountListSupplier(params = SingleAccountListProducer.Params(userWalletId))
|
||||
} returns accountListFlow
|
||||
|
||||
val expected = AccountStatusList(
|
||||
userWalletId = userWalletId,
|
||||
accountStatuses = setOf(
|
||||
AccountStatus.CryptoPortfolio(
|
||||
account = accountList.mainAccount,
|
||||
tokenList = TokenList.Empty,
|
||||
priceChangeLce = PriceChange(value = BigDecimal("0.00"), source = StatusSource.ACTUAL).lceContent(),
|
||||
),
|
||||
),
|
||||
totalAccounts = 1,
|
||||
totalFiatBalance = TotalFiatBalance.Loaded(amount = BigDecimal.ZERO, source = StatusSource.ACTUAL),
|
||||
)
|
||||
|
||||
// Act (first emission)
|
||||
val actual1 = producer.produce().let(::getEmittedValues)
|
||||
|
||||
// Assert (first emission)
|
||||
Truth.assertThat(actual1).containsExactly(expected)
|
||||
|
||||
// Act (second emission)
|
||||
accountListFlow.value = accountList
|
||||
val actual2 = producer.produce().let(::getEmittedValues)
|
||||
|
||||
// Assert (second emission)
|
||||
Truth.assertThat(actual2).containsExactly(expected)
|
||||
|
||||
coVerify(ordering = Ordering.SEQUENCE) {
|
||||
singleAccountListSupplier(params = SingleAccountListProducer.Params(userWalletId))
|
||||
singleAccountListSupplier(params = SingleAccountListProducer.Params(userWalletId))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `flow is produced for account with non empty crypto currencies`() = runTest {
|
||||
// Arrange
|
||||
val cryptoCurrencyFactory = MockCryptoCurrencyFactory()
|
||||
val accountList = AccountList.empty(
|
||||
userWalletId = userWalletId,
|
||||
cryptoCurrencies = cryptoCurrencyFactory.ethereumAndStellar.toSet(),
|
||||
)
|
||||
|
||||
coEvery { userWalletsListRepository.userWalletsSync() } returns listOf(userWallet)
|
||||
|
||||
every {
|
||||
singleAccountListSupplier(params = SingleAccountListProducer.Params(userWalletId))
|
||||
} returns flowOf(accountList)
|
||||
|
||||
val ethereumStatus = CryptoCurrencyStatus(
|
||||
currency = cryptoCurrencyFactory.ethereum,
|
||||
value = CryptoCurrencyStatus.Loading,
|
||||
)
|
||||
every {
|
||||
cryptoCurrencyStatusesFlowFactory.create(userWallet = userWallet, currency = cryptoCurrencyFactory.ethereum)
|
||||
} returns flowOf(ethereumStatus)
|
||||
|
||||
val stellarStatus = CryptoCurrencyStatus(
|
||||
currency = cryptoCurrencyFactory.stellar,
|
||||
value = CryptoCurrencyStatus.MissedDerivation(priceChange = null, fiatRate = null),
|
||||
)
|
||||
every {
|
||||
cryptoCurrencyStatusesFlowFactory.create(userWallet = userWallet, currency = cryptoCurrencyFactory.stellar)
|
||||
} returns flowOf(stellarStatus)
|
||||
|
||||
// Act
|
||||
val actual = producer.produce().let(::getEmittedValues)
|
||||
|
||||
// Assert
|
||||
val expected = AccountStatusList(
|
||||
userWalletId = userWalletId,
|
||||
accountStatuses = setOf(
|
||||
AccountStatus.CryptoPortfolio(
|
||||
account = accountList.mainAccount,
|
||||
tokenList = TokenList.Ungrouped(
|
||||
totalFiatBalance = TotalFiatBalance.Loading,
|
||||
sortedBy = TokensSortType.NONE,
|
||||
currencies = nonEmptyListOf(ethereumStatus, stellarStatus),
|
||||
),
|
||||
priceChangeLce = lceLoading(),
|
||||
),
|
||||
),
|
||||
totalAccounts = 1,
|
||||
totalFiatBalance = TotalFiatBalance.Loading,
|
||||
)
|
||||
Truth.assertThat(actual).containsExactly(expected)
|
||||
|
||||
coVerify(ordering = Ordering.SEQUENCE) {
|
||||
singleAccountListSupplier(params = SingleAccountListProducer.Params(userWalletId))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -218,7 +218,7 @@ class GetAccountCurrencyByAddressUseCaseTest {
|
|||
},
|
||||
value = NetworkStatus.Unreachable(address = validNetworkAddress),
|
||||
)
|
||||
val accountList = AccountList.empty(multiUserWallet)
|
||||
val accountList = AccountList.empty(userWalletId)
|
||||
|
||||
every { userWalletsListRepository.userWallets } returns MutableStateFlow(listOf(multiUserWallet))
|
||||
coEvery {
|
||||
|
|
@ -253,7 +253,7 @@ class GetAccountCurrencyByAddressUseCaseTest {
|
|||
network = currency.network,
|
||||
value = NetworkStatus.Unreachable(address = validNetworkAddress),
|
||||
)
|
||||
val accountList = AccountList.empty(userWallet = multiUserWallet, cryptoCurrencies = setOf(currency))
|
||||
val accountList = AccountList.empty(userWalletId = userWalletId, cryptoCurrencies = setOf(currency))
|
||||
|
||||
every { userWalletsListRepository.userWallets } returns MutableStateFlow(listOf(multiUserWallet))
|
||||
coEvery {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,289 @@
|
|||
package com.tangem.domain.account.status.usecase
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
|
||||
import com.tangem.common.test.utils.assertNone
|
||||
import com.tangem.common.test.utils.assertSome
|
||||
import com.tangem.common.test.utils.getEmittedValues
|
||||
import com.tangem.domain.account.models.AccountStatusList
|
||||
import com.tangem.domain.account.status.model.AccountCryptoCurrencyStatus
|
||||
import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer
|
||||
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
|
||||
import com.tangem.domain.core.utils.lceLoading
|
||||
import com.tangem.domain.models.TokensSortType
|
||||
import com.tangem.domain.models.TotalFiatBalance
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.account.AccountStatus
|
||||
import com.tangem.domain.models.account.DerivationIndex
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.tokenlist.TokenList
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import io.mockk.*
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Nested
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
class GetAccountCurrencyStatusUseCaseTest {
|
||||
|
||||
private val supplier = mockk<SingleAccountStatusListSupplier>()
|
||||
private val useCase = GetAccountCurrencyStatusUseCase(singleAccountStatusListSupplier = supplier)
|
||||
|
||||
private val userWalletId = UserWalletId("011")
|
||||
private val supplierParams = SingleAccountStatusListProducer.Params(userWalletId)
|
||||
private val currency = MockCryptoCurrencyFactory().ethereum.let {
|
||||
val derivationPath = Network.DerivationPath.Card("m/44'/60'/0'/0/1")
|
||||
|
||||
it.copy(
|
||||
network = it.network.copy(
|
||||
id = Network.ID(value = "ethereum", derivationPath = derivationPath),
|
||||
derivationPath = derivationPath,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
clearMocks(supplier)
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class InvokeSync {
|
||||
|
||||
@Test
|
||||
fun `invokeSync returns None when supplier returns null`() = runTest {
|
||||
// Arrange
|
||||
coEvery { supplier.getSyncOrNull(supplierParams) } returns null
|
||||
|
||||
// Act
|
||||
val actual = useCase.invokeSync(userWalletId = userWalletId, currencyId = currency.id, network = null)
|
||||
|
||||
// Assert
|
||||
assertNone(actual)
|
||||
coVerifyOrder { supplier.getSyncOrNull(supplierParams) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invokeSync returns None when AccountList does not contain required currency id`() = runTest {
|
||||
// Arrange
|
||||
val accountStatus = AccountStatus.CryptoPortfolio(
|
||||
account = Account.CryptoPortfolio.createMainAccount(userWalletId),
|
||||
tokenList = TokenList.Empty,
|
||||
priceChangeLce = lceLoading(),
|
||||
)
|
||||
|
||||
val accountStatusList = mockk<AccountStatusList>(relaxed = true) {
|
||||
every { this@mockk.accountStatuses } returns setOf(accountStatus)
|
||||
}
|
||||
|
||||
coEvery { supplier.getSyncOrNull(supplierParams) } returns accountStatusList
|
||||
|
||||
// Act
|
||||
val actual = useCase.invokeSync(userWalletId = userWalletId, currencyId = currency.id, network = null)
|
||||
|
||||
// Assert
|
||||
assertNone(actual)
|
||||
coVerifyOrder { supplier.getSyncOrNull(supplierParams) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invokeSync returns Some if network is not null`() = runTest {
|
||||
// Arrange
|
||||
val mainAccountStatus = AccountStatus.CryptoPortfolio(
|
||||
account = Account.CryptoPortfolio.createMainAccount(userWalletId),
|
||||
tokenList = TokenList.Empty,
|
||||
priceChangeLce = lceLoading(),
|
||||
)
|
||||
|
||||
val account = mockk<Account.CryptoPortfolio>(relaxed = true) {
|
||||
every { this@mockk.derivationIndex } returns DerivationIndex(1).getOrNull()!!
|
||||
every { this@mockk.cryptoCurrencies } returns setOf(currency)
|
||||
}
|
||||
val currencyStatus = CryptoCurrencyStatus(currency = currency, value = CryptoCurrencyStatus.Loading)
|
||||
val accountStatus = AccountStatus.CryptoPortfolio(
|
||||
account = account,
|
||||
tokenList = TokenList.Ungrouped(
|
||||
totalFiatBalance = TotalFiatBalance.Loading,
|
||||
sortedBy = TokensSortType.NONE,
|
||||
currencies = listOf(currencyStatus),
|
||||
),
|
||||
priceChangeLce = lceLoading(),
|
||||
)
|
||||
|
||||
val accountStatusList = mockk<AccountStatusList>(relaxed = true) {
|
||||
every { this@mockk.accountStatuses } returns setOf(mainAccountStatus, accountStatus, mockk())
|
||||
}
|
||||
|
||||
coEvery { supplier.getSyncOrNull(supplierParams) } returns accountStatusList
|
||||
|
||||
// Act
|
||||
val actual = useCase.invokeSync(
|
||||
userWalletId = userWalletId,
|
||||
currencyId = currency.id,
|
||||
network = currency.network,
|
||||
)
|
||||
|
||||
// Assert
|
||||
val expected = AccountCryptoCurrencyStatus(account = accountStatus.account, status = currencyStatus)
|
||||
assertSome(actual, expected)
|
||||
|
||||
coVerifyOrder { supplier.getSyncOrNull(supplierParams) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invokeSync returns Some if network is null`() = runTest {
|
||||
// Arrange
|
||||
val account = mockk<Account.CryptoPortfolio>(relaxed = true) {
|
||||
every { this@mockk.cryptoCurrencies } returns setOf(currency)
|
||||
}
|
||||
val currencyStatus = CryptoCurrencyStatus(currency = currency, value = CryptoCurrencyStatus.Loading)
|
||||
val accountStatus = AccountStatus.CryptoPortfolio(
|
||||
account = account,
|
||||
tokenList = TokenList.Ungrouped(
|
||||
totalFiatBalance = TotalFiatBalance.Loading,
|
||||
sortedBy = TokensSortType.NONE,
|
||||
currencies = listOf(currencyStatus),
|
||||
),
|
||||
priceChangeLce = lceLoading(),
|
||||
)
|
||||
|
||||
val accountStatusList = mockk<AccountStatusList>(relaxed = true) {
|
||||
every { this@mockk.accountStatuses } returns setOf(accountStatus)
|
||||
}
|
||||
|
||||
coEvery { supplier.getSyncOrNull(supplierParams) } returns accountStatusList
|
||||
|
||||
// Act
|
||||
val actual = useCase.invokeSync(userWalletId = userWalletId, currencyId = currency.id, network = null)
|
||||
|
||||
// Assert
|
||||
val expected = AccountCryptoCurrencyStatus(account = accountStatus.account, status = currencyStatus)
|
||||
assertSome(actual, expected)
|
||||
coVerifyOrder { supplier.getSyncOrNull(supplierParams) }
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UnusedFlow")
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class Invoke {
|
||||
|
||||
@Test
|
||||
fun `invoke returns empty flow when supplier returns empty flow`() = runTest {
|
||||
// Arrange
|
||||
coEvery { supplier(supplierParams) } returns emptyFlow()
|
||||
|
||||
// Act
|
||||
val actual = useCase(userWalletId = userWalletId, currencyId = currency.id, network = null)
|
||||
.let(::getEmittedValues)
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).isEmpty()
|
||||
coVerifyOrder { supplier(supplierParams) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke returns empty flow when AccountList does not contain required currency id`() = runTest {
|
||||
// Arrange
|
||||
val accountStatus = AccountStatus.CryptoPortfolio(
|
||||
account = Account.CryptoPortfolio.createMainAccount(userWalletId),
|
||||
tokenList = TokenList.Empty,
|
||||
priceChangeLce = lceLoading(),
|
||||
)
|
||||
|
||||
val accountStatusList = mockk<AccountStatusList>(relaxed = true) {
|
||||
every { this@mockk.accountStatuses } returns setOf(accountStatus)
|
||||
}
|
||||
|
||||
coEvery { supplier(supplierParams) } returns flowOf(accountStatusList)
|
||||
|
||||
// Act
|
||||
val actual = useCase(userWalletId = userWalletId, currencyId = currency.id, network = null)
|
||||
.let(::getEmittedValues)
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).isEmpty()
|
||||
coVerifyOrder { supplier(supplierParams) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke returns data if network is not null`() = runTest {
|
||||
// Arrange
|
||||
val mainAccountStatus = AccountStatus.CryptoPortfolio(
|
||||
account = Account.CryptoPortfolio.createMainAccount(userWalletId),
|
||||
tokenList = TokenList.Empty,
|
||||
priceChangeLce = lceLoading(),
|
||||
)
|
||||
|
||||
val account = mockk<Account.CryptoPortfolio>(relaxed = true) {
|
||||
every { this@mockk.derivationIndex } returns DerivationIndex(1).getOrNull()!!
|
||||
every { this@mockk.cryptoCurrencies } returns setOf(currency)
|
||||
}
|
||||
val currencyStatus = CryptoCurrencyStatus(currency = currency, value = CryptoCurrencyStatus.Loading)
|
||||
val accountStatus = AccountStatus.CryptoPortfolio(
|
||||
account = account,
|
||||
tokenList = TokenList.Ungrouped(
|
||||
totalFiatBalance = TotalFiatBalance.Loading,
|
||||
sortedBy = TokensSortType.NONE,
|
||||
currencies = listOf(currencyStatus),
|
||||
),
|
||||
priceChangeLce = lceLoading(),
|
||||
)
|
||||
|
||||
val accountStatusList = mockk<AccountStatusList>(relaxed = true) {
|
||||
every { this@mockk.accountStatuses } returns setOf(mainAccountStatus, accountStatus, mockk())
|
||||
}
|
||||
|
||||
coEvery { supplier(supplierParams) } returns flowOf(accountStatusList)
|
||||
|
||||
// Act
|
||||
val actual = useCase(userWalletId = userWalletId, currencyId = currency.id, network = null)
|
||||
.let(::getEmittedValues)
|
||||
|
||||
// Assert
|
||||
val expected = AccountCryptoCurrencyStatus(account = accountStatus.account, status = currencyStatus)
|
||||
Truth.assertThat(actual).containsExactly(expected)
|
||||
|
||||
coVerifyOrder { supplier(supplierParams) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke returns data if network is null`() = runTest {
|
||||
// Arrange
|
||||
val account = mockk<Account.CryptoPortfolio>(relaxed = true) {
|
||||
every { this@mockk.cryptoCurrencies } returns setOf(currency)
|
||||
}
|
||||
val currencyStatus = CryptoCurrencyStatus(currency = currency, value = CryptoCurrencyStatus.Loading)
|
||||
val accountStatus = AccountStatus.CryptoPortfolio(
|
||||
account = account,
|
||||
tokenList = TokenList.Ungrouped(
|
||||
totalFiatBalance = TotalFiatBalance.Loading,
|
||||
sortedBy = TokensSortType.NONE,
|
||||
currencies = listOf(currencyStatus),
|
||||
),
|
||||
priceChangeLce = lceLoading(),
|
||||
)
|
||||
|
||||
val accountStatusList = mockk<AccountStatusList>(relaxed = true) {
|
||||
every { this@mockk.accountStatuses } returns setOf(accountStatus)
|
||||
}
|
||||
|
||||
coEvery { supplier(supplierParams) } returns flowOf(accountStatusList)
|
||||
|
||||
// Act
|
||||
val actual = useCase(userWalletId = userWalletId, currencyId = currency.id, network = null)
|
||||
.let(::getEmittedValues)
|
||||
|
||||
// Assert
|
||||
val expected = AccountCryptoCurrencyStatus(account = accountStatus.account, status = currencyStatus)
|
||||
Truth.assertThat(actual).containsExactly(expected)
|
||||
coVerifyOrder { supplier(supplierParams) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,308 @@
|
|||
package com.tangem.domain.account.status.utils
|
||||
|
||||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
|
||||
import com.tangem.common.test.utils.getEmittedValues
|
||||
import com.tangem.domain.models.StatusSource
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.network.NetworkAddress
|
||||
import com.tangem.domain.models.network.NetworkStatus
|
||||
import com.tangem.domain.models.quote.QuoteStatus
|
||||
import com.tangem.domain.models.staking.StakingID
|
||||
import com.tangem.domain.models.staking.YieldBalance
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.models.wallet.isMultiCurrency
|
||||
import com.tangem.domain.networks.single.SingleNetworkStatusProducer
|
||||
import com.tangem.domain.networks.single.SingleNetworkStatusSupplier
|
||||
import com.tangem.domain.quotes.single.SingleQuoteStatusProducer
|
||||
import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier
|
||||
import com.tangem.domain.staking.StakingIdFactory
|
||||
import com.tangem.domain.staking.single.SingleYieldBalanceProducer
|
||||
import com.tangem.domain.staking.single.SingleYieldBalanceSupplier
|
||||
import io.mockk.*
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Suppress("UnusedFlow")
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
class CryptoCurrencyStatusesFlowFactoryTest {
|
||||
|
||||
private val singleNetworkStatusSupplier: SingleNetworkStatusSupplier = mockk()
|
||||
private val singleQuoteStatusSupplier: SingleQuoteStatusSupplier = mockk()
|
||||
private val singleYieldBalanceSupplier: SingleYieldBalanceSupplier = mockk()
|
||||
private val stakingIdFactory: StakingIdFactory = mockk()
|
||||
|
||||
private val factory = CryptoCurrencyStatusesFlowFactory(
|
||||
singleNetworkStatusSupplier = singleNetworkStatusSupplier,
|
||||
singleQuoteStatusSupplier = singleQuoteStatusSupplier,
|
||||
singleYieldBalanceSupplier = singleYieldBalanceSupplier,
|
||||
stakingIdFactory = stakingIdFactory,
|
||||
)
|
||||
|
||||
private val userWalletId = UserWalletId("011")
|
||||
private val cryptoCurrencyFactory = MockCryptoCurrencyFactory()
|
||||
|
||||
private val networkAddress = NetworkAddress.Single(
|
||||
defaultAddress = NetworkAddress.Address(value = "0x1", type = NetworkAddress.Address.Type.Primary),
|
||||
)
|
||||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
clearMocks(
|
||||
singleNetworkStatusSupplier,
|
||||
singleQuoteStatusSupplier,
|
||||
singleYieldBalanceSupplier,
|
||||
stakingIdFactory,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `if rawCurrencyId is null, there will be no subscription to the quote status`() = runTest {
|
||||
// Arrange
|
||||
val userWallet = mockk<UserWallet> {
|
||||
every { this@mockk.walletId } returns userWalletId
|
||||
every { this@mockk.isMultiCurrency } returns true
|
||||
}
|
||||
|
||||
val currency = cryptoCurrencyFactory.ethereum.copy(
|
||||
id = cryptoCurrencyFactory.ethereum.id.copy(
|
||||
suffix = CryptoCurrency.ID.Suffix.ContractAddress(contractAddress = "0x12345"),
|
||||
),
|
||||
)
|
||||
|
||||
val networkStatus = NetworkStatus(
|
||||
network = currency.network,
|
||||
value = NetworkStatus.Unreachable(address = networkAddress),
|
||||
)
|
||||
val networkStatusFlow = flowOf(networkStatus)
|
||||
every {
|
||||
singleNetworkStatusSupplier(params = SingleNetworkStatusProducer.Params(userWalletId, currency.network))
|
||||
} returns networkStatusFlow
|
||||
|
||||
val stakingId = StakingID(integrationId = "id", address = networkAddress.defaultAddress.value)
|
||||
coEvery {
|
||||
stakingIdFactory.create(currencyId = currency.id, defaultAddress = networkAddress.defaultAddress.value)
|
||||
} returns stakingId.right()
|
||||
|
||||
val yieldBalance = YieldBalance.Empty(stakingId = stakingId, source = StatusSource.ACTUAL)
|
||||
val yieldBalanceFlow = flowOf(yieldBalance)
|
||||
every {
|
||||
singleYieldBalanceSupplier(
|
||||
params = SingleYieldBalanceProducer.Params(userWalletId = userWalletId, stakingId = stakingId),
|
||||
)
|
||||
} returns yieldBalanceFlow
|
||||
|
||||
// Act
|
||||
val actual = factory.create(userWallet = userWallet, currency = currency).let(::getEmittedValues)
|
||||
|
||||
// Assert
|
||||
val expected = CryptoCurrencyStatus(
|
||||
currency = currency,
|
||||
value = CryptoCurrencyStatus.Unreachable(
|
||||
priceChange = null,
|
||||
fiatRate = null,
|
||||
networkAddress = networkAddress,
|
||||
),
|
||||
)
|
||||
Truth.assertThat(actual).containsExactly(expected)
|
||||
|
||||
coVerify(ordering = Ordering.SEQUENCE) {
|
||||
singleNetworkStatusSupplier(params = SingleNetworkStatusProducer.Params(userWalletId, currency.network))
|
||||
stakingIdFactory.create(currencyId = currency.id, defaultAddress = networkAddress.defaultAddress.value)
|
||||
singleYieldBalanceSupplier(params = SingleYieldBalanceProducer.Params(userWalletId, stakingId))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `if userWallet is not multi-currency, there will be no subscription to the yield balance`() = runTest {
|
||||
// Arrange
|
||||
val userWallet = mockk<UserWallet> {
|
||||
every { this@mockk.walletId } returns userWalletId
|
||||
every { this@mockk.isMultiCurrency } returns false
|
||||
}
|
||||
|
||||
val currency = cryptoCurrencyFactory.ethereum
|
||||
|
||||
val networkStatus = NetworkStatus(
|
||||
network = currency.network,
|
||||
value = NetworkStatus.Unreachable(address = networkAddress),
|
||||
)
|
||||
val networkStatusFlow = flowOf(networkStatus)
|
||||
every {
|
||||
singleNetworkStatusSupplier(params = SingleNetworkStatusProducer.Params(userWalletId, currency.network))
|
||||
} returns networkStatusFlow
|
||||
|
||||
val quoteStatus = QuoteStatus(
|
||||
rawCurrencyId = currency.id.rawCurrencyId!!,
|
||||
value = QuoteStatus.Data(
|
||||
source = StatusSource.ACTUAL,
|
||||
fiatRate = BigDecimal.ONE,
|
||||
priceChange = BigDecimal.ONE,
|
||||
),
|
||||
)
|
||||
val quoteStatusFlow = flowOf(quoteStatus)
|
||||
every {
|
||||
singleQuoteStatusSupplier(params = SingleQuoteStatusProducer.Params(currency.id.rawCurrencyId!!))
|
||||
} returns quoteStatusFlow
|
||||
|
||||
// Act
|
||||
val actual = factory.create(userWallet = userWallet, currency = currency).let(::getEmittedValues)
|
||||
|
||||
// Assert
|
||||
val expected = CryptoCurrencyStatus(
|
||||
currency = currency,
|
||||
value = CryptoCurrencyStatus.Unreachable(
|
||||
priceChange = BigDecimal.ONE,
|
||||
fiatRate = BigDecimal.ONE,
|
||||
networkAddress = networkAddress,
|
||||
),
|
||||
)
|
||||
Truth.assertThat(actual).containsExactly(expected)
|
||||
|
||||
coVerify(ordering = Ordering.SEQUENCE) {
|
||||
singleNetworkStatusSupplier(params = SingleNetworkStatusProducer.Params(userWalletId, currency.network))
|
||||
singleQuoteStatusSupplier(params = SingleQuoteStatusProducer.Params(currency.id.rawCurrencyId!!))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `no subscription to the quote status and yield balance`() = runTest {
|
||||
// Arrange
|
||||
val userWallet = mockk<UserWallet> {
|
||||
every { this@mockk.walletId } returns userWalletId
|
||||
every { this@mockk.isMultiCurrency } returns false
|
||||
}
|
||||
|
||||
val currency = cryptoCurrencyFactory.ethereum.copy(
|
||||
id = cryptoCurrencyFactory.ethereum.id.copy(
|
||||
suffix = CryptoCurrency.ID.Suffix.ContractAddress(contractAddress = "0x12345"),
|
||||
),
|
||||
)
|
||||
|
||||
val networkStatus = NetworkStatus(
|
||||
network = currency.network,
|
||||
value = NetworkStatus.Unreachable(address = networkAddress),
|
||||
)
|
||||
val networkStatusFlow = flowOf(networkStatus)
|
||||
every {
|
||||
singleNetworkStatusSupplier(params = SingleNetworkStatusProducer.Params(userWalletId, currency.network))
|
||||
} returns networkStatusFlow
|
||||
|
||||
// Act
|
||||
val actual = factory.create(userWallet = userWallet, currency = currency).let(::getEmittedValues)
|
||||
|
||||
// Assert
|
||||
val expected = CryptoCurrencyStatus(
|
||||
currency = currency,
|
||||
value = CryptoCurrencyStatus.Unreachable(
|
||||
priceChange = null,
|
||||
fiatRate = null,
|
||||
networkAddress = networkAddress,
|
||||
),
|
||||
)
|
||||
Truth.assertThat(actual).containsExactly(expected)
|
||||
|
||||
coVerify(ordering = Ordering.SEQUENCE) {
|
||||
singleNetworkStatusSupplier(params = SingleNetworkStatusProducer.Params(userWalletId, currency.network))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `if stakingId is not supported, yield balance will be null`() = runTest {
|
||||
// Arrange
|
||||
val userWallet = mockk<UserWallet> {
|
||||
every { this@mockk.walletId } returns userWalletId
|
||||
every { this@mockk.isMultiCurrency } returns true
|
||||
}
|
||||
|
||||
val currency = cryptoCurrencyFactory.ethereum
|
||||
|
||||
val networkStatus = NetworkStatus(
|
||||
network = currency.network,
|
||||
value = NetworkStatus.Unreachable(address = networkAddress),
|
||||
)
|
||||
val networkStatusFlow = flowOf(networkStatus)
|
||||
every {
|
||||
singleNetworkStatusSupplier(params = SingleNetworkStatusProducer.Params(userWalletId, currency.network))
|
||||
} returns networkStatusFlow
|
||||
|
||||
val quoteStatus = QuoteStatus(
|
||||
rawCurrencyId = currency.id.rawCurrencyId!!,
|
||||
value = QuoteStatus.Data(
|
||||
source = StatusSource.ACTUAL,
|
||||
fiatRate = BigDecimal.ONE,
|
||||
priceChange = BigDecimal.ONE,
|
||||
),
|
||||
)
|
||||
val quoteStatusFlow = flowOf(quoteStatus)
|
||||
every {
|
||||
singleQuoteStatusSupplier(params = SingleQuoteStatusProducer.Params(currency.id.rawCurrencyId!!))
|
||||
} returns quoteStatusFlow
|
||||
|
||||
coEvery {
|
||||
stakingIdFactory.create(currencyId = currency.id, defaultAddress = networkAddress.defaultAddress.value)
|
||||
} returns StakingIdFactory.Error.UnsupportedCurrency.left()
|
||||
|
||||
// Act
|
||||
val actual = factory.create(userWallet = userWallet, currency = currency).let(::getEmittedValues)
|
||||
|
||||
// Assert
|
||||
val expected = CryptoCurrencyStatus(
|
||||
currency = currency,
|
||||
value = CryptoCurrencyStatus.Unreachable(
|
||||
priceChange = BigDecimal.ONE,
|
||||
fiatRate = BigDecimal.ONE,
|
||||
networkAddress = networkAddress,
|
||||
),
|
||||
)
|
||||
Truth.assertThat(actual).containsExactly(expected)
|
||||
|
||||
coVerify(ordering = Ordering.SEQUENCE) {
|
||||
singleNetworkStatusSupplier(params = SingleNetworkStatusProducer.Params(userWalletId, currency.network))
|
||||
singleQuoteStatusSupplier(params = SingleQuoteStatusProducer.Params(currency.id.rawCurrencyId!!))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `all sources are empty`() = runTest {
|
||||
// Arrange
|
||||
val userWallet = mockk<UserWallet> {
|
||||
every { this@mockk.walletId } returns userWalletId
|
||||
every { this@mockk.isMultiCurrency } returns true
|
||||
}
|
||||
|
||||
val currency = cryptoCurrencyFactory.ethereum
|
||||
|
||||
every {
|
||||
singleNetworkStatusSupplier(params = SingleNetworkStatusProducer.Params(userWalletId, currency.network))
|
||||
} returns emptyFlow()
|
||||
|
||||
every {
|
||||
singleQuoteStatusSupplier(params = SingleQuoteStatusProducer.Params(currency.id.rawCurrencyId!!))
|
||||
} returns emptyFlow()
|
||||
|
||||
// Act
|
||||
val actual = factory.create(userWallet = userWallet, currency = currency).let(::getEmittedValues)
|
||||
|
||||
// Assert
|
||||
val expected = CryptoCurrencyStatus(currency = currency, value = CryptoCurrencyStatus.Loading)
|
||||
Truth.assertThat(actual).containsExactly(expected)
|
||||
|
||||
coVerify(ordering = Ordering.SEQUENCE) {
|
||||
singleNetworkStatusSupplier(params = SingleNetworkStatusProducer.Params(userWalletId, currency.network))
|
||||
singleQuoteStatusSupplier(params = SingleQuoteStatusProducer.Params(currency.id.rawCurrencyId!!))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -212,6 +212,9 @@ data object Wallet2CardConfig : CardConfig {
|
|||
Blockchain.HyperliquidTestnet -> EllipticCurve.Secp256k1
|
||||
Blockchain.Quai -> EllipticCurve.Secp256k1
|
||||
Blockchain.QuaiTestnet -> EllipticCurve.Secp256k1
|
||||
Blockchain.Linea -> EllipticCurve.Secp256k1
|
||||
Blockchain.LineaTestnet -> EllipticCurve.Secp256k1
|
||||
Blockchain.ArbitrumNova -> EllipticCurve.Secp256k1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -168,6 +168,9 @@ class Wallet2CardConfigTest {
|
|||
Blockchain.HyperliquidTestnet to EllipticCurve.Secp256k1,
|
||||
Blockchain.Quai to EllipticCurve.Secp256k1,
|
||||
Blockchain.QuaiTestnet to EllipticCurve.Secp256k1,
|
||||
Blockchain.Linea to EllipticCurve.Secp256k1,
|
||||
Blockchain.LineaTestnet to EllipticCurve.Secp256k1,
|
||||
Blockchain.ArbitrumNova to EllipticCurve.Secp256k1,
|
||||
)
|
||||
|
||||
@Test
|
||||
|
|
|
|||
|
|
@ -1,16 +0,0 @@
|
|||
package com.tangem.domain.tokens
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.Token
|
||||
import org.rekotlin.Action
|
||||
|
||||
sealed interface TokensAction : Action {
|
||||
|
||||
/** Single way to pass data to the screen */
|
||||
sealed interface SetArgs : TokensAction {
|
||||
object ManageAccess : SetArgs
|
||||
object ReadAccess : SetArgs
|
||||
}
|
||||
}
|
||||
|
||||
data class TokenWithBlockchain(val token: Token, val blockchain: Blockchain)
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.domain.markets
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.joda.time.DateTime
|
||||
import java.math.BigDecimal
|
||||
|
||||
|
|
@ -18,6 +19,8 @@ data class TokenMarketInfo(
|
|||
val pricePerformance: PricePerformance?,
|
||||
val exchangesAmount: Int?,
|
||||
) {
|
||||
|
||||
@Serializable
|
||||
data class Network(
|
||||
val networkId: String,
|
||||
val exchangeable: Boolean,
|
||||
|
|
|
|||
|
|
@ -57,13 +57,17 @@ sealed interface Account {
|
|||
val networksCount: Int
|
||||
get() = cryptoCurrencies.map(CryptoCurrency::network).distinct().size
|
||||
|
||||
fun copy(accountName: AccountName = this.accountName, icon: CryptoPortfolioIcon = this.icon): CryptoPortfolio {
|
||||
fun copy(
|
||||
accountName: AccountName = this.accountName,
|
||||
icon: CryptoPortfolioIcon = this.icon,
|
||||
cryptoCurrencies: Set<CryptoCurrency> = this.cryptoCurrencies,
|
||||
): CryptoPortfolio {
|
||||
return CryptoPortfolio(
|
||||
accountId = this.accountId,
|
||||
accountName = accountName,
|
||||
icon = icon,
|
||||
derivationIndex = this.derivationIndex,
|
||||
cryptoCurrencies = this.cryptoCurrencies,
|
||||
cryptoCurrencies = cryptoCurrencies,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
package com.tangem.domain.models.currency
|
||||
|
||||
fun CryptoCurrency.Token.yieldSupplyKey(): String {
|
||||
return "${network.backendId}_$contractAddress"
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.domain.networks.utils
|
||||
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
||||
/**
|
||||
* Cleans up network-related data for a specific user wallet and a list of cryptocurrencies.
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
interface NetworksCleaner {
|
||||
|
||||
/**
|
||||
* Cleans up network-related data for the given [userWalletId] and list of [currencies].
|
||||
*
|
||||
* @param userWalletId The ID of the user wallet for which to clean up data.
|
||||
* @param currencies The list of cryptocurrencies whose associated network data should be cleaned.
|
||||
*/
|
||||
suspend operator fun invoke(userWalletId: UserWalletId, currencies: List<CryptoCurrency>)
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.domain.staking.utils
|
||||
|
||||
import com.tangem.domain.models.staking.StakingID
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
||||
/**
|
||||
* Cleans up staking-related data for a specific user wallet and a set of staking IDs.
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
interface StakingCleaner {
|
||||
|
||||
/**
|
||||
* Cleans up staking-related data for the given [userWalletId] and set of [stakingIds].
|
||||
*
|
||||
* @param userWalletId The ID of the user wallet for which to clean up data.
|
||||
* @param stakingIds The set of staking IDs whose associated data should be cleaned.
|
||||
*/
|
||||
suspend operator fun invoke(userWalletId: UserWalletId, stakingIds: Set<StakingID>)
|
||||
}
|
||||
|
|
@ -40,26 +40,7 @@ interface CurrenciesRepository {
|
|||
* @param userWalletId The unique identifier of the user wallet.
|
||||
* @param currencies The list of cryptocurrencies to be saved.
|
||||
*/
|
||||
suspend fun saveNewCurrenciesList(userWalletId: UserWalletId, currencies: List<CryptoCurrency>)
|
||||
|
||||
/**
|
||||
* Add currencies to a specific user wallet.
|
||||
*
|
||||
* @param userWalletId The unique identifier of the user wallet.
|
||||
* @param currencies The currencies which must be added.
|
||||
* @throws DataError.UserWalletError.WrongUserWallet If single-currency user wallet
|
||||
* ID provided.
|
||||
*/
|
||||
suspend fun addCurrencies(userWalletId: UserWalletId, currencies: List<CryptoCurrency>): List<CryptoCurrency>
|
||||
|
||||
/**
|
||||
* Saves the given list of cryptocurrencies for a specific multi-currency user wallet.
|
||||
*
|
||||
* @param userWalletId The unique identifier of the user wallet.
|
||||
* @param currencies The list of cryptocurrencies to be saved.
|
||||
*/
|
||||
@Deprecated("Tech debt")
|
||||
suspend fun saveNewCurrenciesListCache(userWalletId: UserWalletId, currencies: List<CryptoCurrency>)
|
||||
suspend fun saveCurrenciesLocal(userWalletId: UserWalletId, currencies: List<CryptoCurrency>)
|
||||
|
||||
/**
|
||||
* Add currencies to a specific user wallet.
|
||||
|
|
@ -256,6 +237,8 @@ interface CurrenciesRepository {
|
|||
*/
|
||||
suspend fun getFeePaidCurrency(userWalletId: UserWalletId, network: Network): FeePaidCurrency
|
||||
|
||||
fun createCoinCurrency(network: Network): CryptoCurrency.Coin
|
||||
|
||||
/**
|
||||
* Creates token [cryptoCurrency] based on current token and [network] it`s will be added
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -46,14 +46,7 @@ internal class MockCurrenciesRepository(
|
|||
isTokensSortedByBalanceAfterSortingApply = isSortedByBalance
|
||||
}
|
||||
|
||||
override suspend fun saveNewCurrenciesList(userWalletId: UserWalletId, currencies: List<CryptoCurrency>) = Unit
|
||||
|
||||
override suspend fun addCurrencies(
|
||||
userWalletId: UserWalletId,
|
||||
currencies: List<CryptoCurrency>,
|
||||
): List<CryptoCurrency> = emptyList()
|
||||
|
||||
override suspend fun saveNewCurrenciesListCache(userWalletId: UserWalletId, currencies: List<CryptoCurrency>) = Unit
|
||||
override suspend fun saveCurrenciesLocal(userWalletId: UserWalletId, currencies: List<CryptoCurrency>) = Unit
|
||||
|
||||
override suspend fun addCurrenciesCache(
|
||||
userWalletId: UserWalletId,
|
||||
|
|
@ -152,6 +145,10 @@ internal class MockCurrenciesRepository(
|
|||
return FeePaidCurrency.Coin
|
||||
}
|
||||
|
||||
override fun createCoinCurrency(network: Network): CryptoCurrency.Coin {
|
||||
error("not implemented")
|
||||
}
|
||||
|
||||
override fun createTokenCurrency(cryptoCurrency: CryptoCurrency.Token, network: Network): CryptoCurrency.Token {
|
||||
return cryptoCurrency
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ sealed class TangemPayTxHistoryItem {
|
|||
val merchantName: String,
|
||||
val enrichedMerchantCategory: String?,
|
||||
val merchantCategory: String,
|
||||
val status: String,
|
||||
val status: Status,
|
||||
val enrichedMerchantIconUrl: String?,
|
||||
) : TangemPayTxHistoryItem()
|
||||
|
||||
|
|
@ -36,4 +36,12 @@ sealed class TangemPayTxHistoryItem {
|
|||
override val amount: BigDecimal,
|
||||
override val currency: Currency,
|
||||
) : TangemPayTxHistoryItem()
|
||||
|
||||
enum class Status {
|
||||
PENDING,
|
||||
RESERVED,
|
||||
COMPLETED,
|
||||
DECLINED,
|
||||
UNKNOWN,
|
||||
}
|
||||
}
|
||||
|
|
@ -2,8 +2,6 @@ package com.tangem.domain.pay.model
|
|||
|
||||
import java.math.BigDecimal
|
||||
|
||||
private const val APPROVED_KYC_STATUS = "APPROVED"
|
||||
|
||||
data class MainScreenCustomerInfo(
|
||||
val info: CustomerInfo,
|
||||
val orderStatus: OrderStatus,
|
||||
|
|
@ -11,7 +9,7 @@ data class MainScreenCustomerInfo(
|
|||
|
||||
data class CustomerInfo(
|
||||
val productInstance: ProductInstance?,
|
||||
val kycStatus: String?,
|
||||
val isKycApproved: Boolean,
|
||||
val cardInfo: CardInfo?,
|
||||
) {
|
||||
|
||||
|
|
@ -26,6 +24,4 @@ data class CustomerInfo(
|
|||
val currencyCode: String,
|
||||
val customerWalletAddress: String,
|
||||
)
|
||||
|
||||
fun isKycApproved() = kycStatus == APPROVED_KYC_STATUS
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
package com.tangem.domain.pay.model
|
||||
|
||||
enum class OrderStatus(val apiName: String) {
|
||||
NOT_ISSUED(""),
|
||||
UNKNOWN(""),
|
||||
NEW("NEW"),
|
||||
PROCESSING("PROCESSING"),
|
||||
COMPLETED("COMPLETED"),
|
||||
|
|
|
|||
|
|
@ -36,7 +36,8 @@ interface HotWalletPasswordRequester {
|
|||
* @param hotWalletId The ID of the hot wallet to authenticate with.
|
||||
* @param authMode Indicates whether the request is for authentication mode.
|
||||
* In auth mode user can be deleted after failed attempts.
|
||||
* @param hasBiometry Indicates whether to show biometric authentication option.
|
||||
* @param hasBiometry Indicates whether to show biometric authentication option to the user.
|
||||
* Will be ignored if the device does not support biometry at the moment of the request.
|
||||
*/
|
||||
data class AttemptRequest(
|
||||
val hotWalletId: HotWalletId,
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ data class YieldMarketToken(
|
|||
val chainId: Int,
|
||||
val apy: SerializedBigDecimal,
|
||||
val isActive: Boolean,
|
||||
val maxFeeNative: String,
|
||||
val maxFeeUSD: String,
|
||||
val backendId: String? = null,
|
||||
) {
|
||||
|
||||
|
|
|
|||
|
|
@ -1,19 +0,0 @@
|
|||
package com.tangem.domain.yield.supply.models
|
||||
|
||||
import com.tangem.domain.models.serialization.SerializedBigDecimal
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Domain model representing a token entry in the Yield Markets list.
|
||||
*/
|
||||
@Serializable
|
||||
data class YieldMarketTokenStatus(
|
||||
val tokenAddress: String,
|
||||
val tokenSymbol: String,
|
||||
val tokenName: String,
|
||||
val chainId: Int,
|
||||
val apy: SerializedBigDecimal,
|
||||
val isActive: Boolean,
|
||||
val maxFeeNative: String,
|
||||
val maxFeeUSD: String,
|
||||
)
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
package com.tangem.domain.yield.supply
|
||||
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.yield.supply.models.YieldMarketToken
|
||||
import com.tangem.domain.yield.supply.models.YieldMarketTokenStatus
|
||||
import com.tangem.domain.yield.supply.models.YieldSupplyMarketChartData
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface YieldSupplyMarketRepository {
|
||||
|
||||
/**
|
||||
* Get cached yield markets or null if nothing cached yet.
|
||||
*/
|
||||
suspend fun getCachedMarkets(): List<YieldMarketToken>?
|
||||
|
||||
/**
|
||||
* Update markets by fetching from network and cache the result. Returns latest markets.
|
||||
*/
|
||||
@Throws
|
||||
suspend fun updateMarkets(): List<YieldMarketToken>
|
||||
|
||||
/**
|
||||
* Observe runtime markets updates.
|
||||
*/
|
||||
fun getMarketsFlow(): Flow<List<YieldMarketToken>>
|
||||
|
||||
/**
|
||||
* Get yield token status by contract address.
|
||||
*/
|
||||
@Throws
|
||||
suspend fun getTokenStatus(cryptoCurrencyToken: CryptoCurrency.Token): YieldMarketTokenStatus
|
||||
|
||||
/**
|
||||
* Get yield token APY chart by contract address.
|
||||
*/
|
||||
@Throws
|
||||
suspend fun getTokenChart(cryptoCurrencyToken: CryptoCurrency.Token): YieldSupplyMarketChartData
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
package com.tangem.domain.yield.supply
|
||||
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.yield.supply.models.YieldMarketToken
|
||||
import com.tangem.domain.yield.supply.models.YieldSupplyMarketChartData
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface YieldSupplyRepository {
|
||||
|
||||
/**
|
||||
* Get cached yield markets or null if nothing cached yet.
|
||||
*/
|
||||
suspend fun getCachedMarkets(): List<YieldMarketToken>?
|
||||
|
||||
/**
|
||||
* Update markets by fetching from network and cache the result. Returns latest markets.
|
||||
*/
|
||||
@Throws
|
||||
suspend fun updateMarkets(): List<YieldMarketToken>
|
||||
|
||||
/**
|
||||
* Observe runtime markets updates.
|
||||
*/
|
||||
fun getMarketsFlow(): Flow<List<YieldMarketToken>>
|
||||
|
||||
/**
|
||||
* Get yield token status by contract address from cache
|
||||
*/
|
||||
@Throws
|
||||
suspend fun getTokenStatus(cryptoCurrencyToken: CryptoCurrency.Token): YieldMarketToken
|
||||
|
||||
/**
|
||||
* Get yield token APY chart by contract address.
|
||||
*/
|
||||
@Throws
|
||||
suspend fun getTokenChart(cryptoCurrencyToken: CryptoCurrency.Token): YieldSupplyMarketChartData
|
||||
|
||||
suspend fun isYieldSupplySupported(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Boolean
|
||||
|
||||
/**
|
||||
* Activate yield protocol for the specified token.
|
||||
*
|
||||
* Returns whether the token is active after the operation completes.
|
||||
* May throw on network/backend errors or if required chain id cannot be resolved.
|
||||
*/
|
||||
@Throws
|
||||
suspend fun activateProtocol(cryptoCurrencyToken: CryptoCurrency.Token): Boolean
|
||||
|
||||
/**
|
||||
* Deactivate yield protocol for the specified token.
|
||||
*
|
||||
* Returns whether the token is active after the operation completes
|
||||
* (expected to be false when deactivation succeeds). May throw on
|
||||
* network/backend errors or if required chain id cannot be resolved.
|
||||
*/
|
||||
@Throws
|
||||
suspend fun deactivateProtocol(cryptoCurrencyToken: CryptoCurrency.Token): Boolean
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.domain.yield.supply.usecase
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.yield.supply.YieldSupplyRepository
|
||||
|
||||
class YieldSupplyActivateUseCase(
|
||||
private val yieldSupplyRepository: YieldSupplyRepository,
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(cryptoCurrency: CryptoCurrency): Either<Throwable, Boolean> = Either.catch {
|
||||
val token = cryptoCurrency as? CryptoCurrency.Token ?: error("Token expected")
|
||||
yieldSupplyRepository.activateProtocol(token)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
package com.tangem.domain.yield.supply.usecase
|
||||
|
||||
import com.tangem.domain.yield.supply.YieldSupplyMarketRepository
|
||||
import com.tangem.domain.yield.supply.YieldSupplyRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
|
|
@ -12,11 +12,11 @@ import kotlinx.coroutines.flow.map
|
|||
* - value: APY as string
|
||||
*/
|
||||
class YieldSupplyApyFlowUseCase(
|
||||
private val yieldSupplyMarketRepository: YieldSupplyMarketRepository,
|
||||
private val yieldSupplyRepository: YieldSupplyRepository,
|
||||
) {
|
||||
|
||||
operator fun invoke(): Flow<Map<String, String>> {
|
||||
return yieldSupplyMarketRepository.getMarketsFlow()
|
||||
return yieldSupplyRepository.getMarketsFlow()
|
||||
.map { yieldMarketTokenList ->
|
||||
yieldMarketTokenList.filter { it.isActive }.associate { token ->
|
||||
token.yieldSupplyKey to token.apy.toString()
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package com.tangem.domain.yield.supply.usecase
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.domain.yield.supply.YieldSupplyMarketRepository
|
||||
import com.tangem.domain.yield.supply.YieldSupplyRepository
|
||||
import kotlin.collections.filter
|
||||
|
||||
/**
|
||||
|
|
@ -12,11 +12,11 @@ import kotlin.collections.filter
|
|||
* - value: APY as string
|
||||
*/
|
||||
class YieldSupplyApyUpdateUseCase(
|
||||
private val yieldSupplyMarketRepository: YieldSupplyMarketRepository,
|
||||
private val yieldSupplyRepository: YieldSupplyRepository,
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(): Either<Throwable, Map<String, String>> = Either.catch {
|
||||
yieldSupplyMarketRepository.updateMarkets()
|
||||
yieldSupplyRepository.updateMarkets()
|
||||
.filter { it.isActive }
|
||||
.associate {
|
||||
it.tokenAddress to it.apy.toString()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.domain.yield.supply.usecase
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.yield.supply.YieldSupplyRepository
|
||||
|
||||
class YieldSupplyDeactivateUseCase(
|
||||
private val yieldSupplyRepository: YieldSupplyRepository,
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(cryptoCurrency: CryptoCurrency): Either<Throwable, Boolean> = Either.catch {
|
||||
val token = cryptoCurrency as? CryptoCurrency.Token ?: error("Token expected")
|
||||
yieldSupplyRepository.deactivateProtocol(token)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +1,14 @@
|
|||
package com.tangem.domain.yield.supply.usecase
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.domain.yield.supply.YieldSupplyMarketRepository
|
||||
import com.tangem.domain.yield.supply.YieldSupplyRepository
|
||||
|
||||
class YieldSupplyGetApyUseCase(
|
||||
private val yieldSupplyMarketRepository: YieldSupplyMarketRepository,
|
||||
private val yieldSupplyRepository: YieldSupplyRepository,
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(tokenAddress: String): Either<Throwable, String> = Either.catch {
|
||||
val apys = yieldSupplyMarketRepository.getCachedMarkets() ?: yieldSupplyMarketRepository.updateMarkets()
|
||||
val apys = yieldSupplyRepository.getCachedMarkets() ?: yieldSupplyRepository.updateMarkets()
|
||||
apys.first { it.tokenAddress == tokenAddress }.apy.toString()
|
||||
}
|
||||
}
|
||||
|
|
@ -2,15 +2,15 @@ package com.tangem.domain.yield.supply.usecase
|
|||
|
||||
import arrow.core.Either
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.yield.supply.YieldSupplyMarketRepository
|
||||
import com.tangem.domain.yield.supply.YieldSupplyRepository
|
||||
import com.tangem.domain.yield.supply.models.YieldSupplyMarketChartData
|
||||
|
||||
class YieldSupplyGetChartUseCase(
|
||||
private val yieldSupplyMarketRepository: YieldSupplyMarketRepository,
|
||||
private val yieldSupplyRepository: YieldSupplyRepository,
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(cryptoCurrency: CryptoCurrency.Token): Either<Throwable, YieldSupplyMarketChartData> =
|
||||
Either.catch {
|
||||
yieldSupplyMarketRepository.getTokenChart(cryptoCurrency)
|
||||
yieldSupplyRepository.getTokenChart(cryptoCurrency)
|
||||
}
|
||||
}
|
||||
|
|
@ -2,14 +2,17 @@ package com.tangem.domain.yield.supply.usecase
|
|||
|
||||
import arrow.core.Either
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.yield.supply.YieldSupplyMarketRepository
|
||||
import com.tangem.domain.yield.supply.models.YieldMarketTokenStatus
|
||||
import com.tangem.domain.models.currency.yieldSupplyKey
|
||||
import com.tangem.domain.yield.supply.YieldSupplyRepository
|
||||
import com.tangem.domain.yield.supply.models.YieldMarketToken
|
||||
|
||||
class YieldSupplyGetTokenStatusUseCase(
|
||||
private val yieldSupplyMarketRepository: YieldSupplyMarketRepository,
|
||||
private val yieldSupplyRepository: YieldSupplyRepository,
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(token: CryptoCurrency.Token): Either<Throwable, YieldMarketTokenStatus> = Either.catch {
|
||||
yieldSupplyMarketRepository.getTokenStatus(token)
|
||||
suspend operator fun invoke(token: CryptoCurrency.Token): Either<Throwable, YieldMarketToken> = Either.catch {
|
||||
val tokens = yieldSupplyRepository.getCachedMarkets().orEmpty()
|
||||
val cachedStatus = tokens.firstOrNull { it.yieldSupplyKey == token.yieldSupplyKey() }
|
||||
cachedStatus ?: error("YieldMarketToken not found")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.tangem.domain.yield.supply.usecase
|
||||
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.yield.supply.YieldSupplyRepository
|
||||
|
||||
class YieldSupplyIsAvailableUseCase(
|
||||
private val yieldSupplyRepository: YieldSupplyRepository,
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Boolean {
|
||||
return yieldSupplyRepository.isYieldSupplySupported(userWalletId, cryptoCurrency)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue