diff --git a/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt index 22e627efca..5c303041a0 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt @@ -1,5 +1,6 @@ package com.tangem.tap.di.domain +import com.tangem.domain.account.repository.AccountsCRUDRepository import com.tangem.domain.account.usecase.AddCryptoPortfolioUseCase import com.tangem.domain.account.usecase.ArchiveCryptoPortfolioUseCase import com.tangem.domain.account.usecase.RecoverCryptoPortfolioUseCase @@ -22,8 +23,10 @@ internal object AccountDomainModule { @Provides @Singleton - fun provideUpdateCryptoPortfolioUseCase(): UpdateCryptoPortfolioUseCase { - return UpdateCryptoPortfolioUseCase() + fun provideUpdateCryptoPortfolioUseCase( + accountsCRUDRepository: AccountsCRUDRepository, + ): UpdateCryptoPortfolioUseCase { + return UpdateCryptoPortfolioUseCase(crudRepository = accountsCRUDRepository) } @Provides diff --git a/domain/account/build.gradle.kts b/domain/account/build.gradle.kts index d21a2a628e..cf1bc96831 100644 --- a/domain/account/build.gradle.kts +++ b/domain/account/build.gradle.kts @@ -16,8 +16,9 @@ dependencies { implementation(deps.arrow.core) implementation(deps.kotlin.serialization) + testImplementation(deps.test.coroutine) testImplementation(deps.test.junit5) testRuntimeOnly(deps.test.junit5.engine) - testImplementation(deps.test.truth) testImplementation(deps.test.mockk) + testImplementation(deps.test.truth) } \ No newline at end of file diff --git a/domain/account/src/main/java/com/tangem/domain/account/models/AccountList.kt b/domain/account/src/main/java/com/tangem/domain/account/models/AccountList.kt index fdc9f393b0..e782ece215 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/models/AccountList.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/models/AccountList.kt @@ -5,6 +5,7 @@ import arrow.core.raise.either import arrow.core.raise.ensure import com.tangem.domain.models.account.Account import com.tangem.domain.models.wallet.UserWallet +import com.tangem.utils.extensions.addOrReplace import kotlinx.serialization.Serializable /** @@ -27,6 +28,42 @@ data class AccountList private constructor( val mainAccount: Account.CryptoPortfolio get() = accounts.first { it is Account.CryptoPortfolio && it.isMainAccount } as Account.CryptoPortfolio + /** + * Adds an account to the account list. + * If an account with the same identifier already exists, it will be replaced. + * Returns a new [AccountList] instance with the updated accounts set, or a validation error if constraints are + * violated (e.g., maximum number of accounts exceeded). + * + * @param other the account to add or replace + */ + operator fun plus(other: Account): Either { + val isNewAccount = this.accounts.none { it.accountId == other.accountId } + val accounts = this.accounts.addOrReplace(other) { it.accountId == other.accountId } + + return invoke( + userWallet = this.userWallet, + accounts = accounts, + totalAccounts = this.totalAccounts + if (isNewAccount) 1 else 0, + ) + } + + /** + * Removes the specified account from the account list. + * Returns a new [AccountList] instance with the updated accounts set, or a validation error if constraints are + * violated (e.g., the list becomes empty). + * + * @param other the account to remove + */ + operator fun minus(other: Account): Either { + return invoke( + userWallet = this.userWallet, + accounts = this.accounts.toMutableSet().apply { + removeIf { it.accountId == other.accountId } + }, + totalAccounts = this.totalAccounts - 1, + ) + } + /** * Represents possible errors that can occur when creating an `AccountList` */ @@ -54,10 +91,23 @@ data class AccountList private constructor( return "$tag: There should be at most one main crypto portfolio in the account list" } } + + @Serializable + data object ExceedsMaxAccountsCount : Error { + override fun toString(): String = "$tag: The number of accounts must not exceed 20" + } + + @Serializable + data object DuplicateAccountIds : Error { + override fun toString(): String = "$tag: Account list contains duplicate account IDs" + } } companion object { + private const val MAX_ACCOUNTS_COUNT = 20 + private const val MAX_MAIN_ACCOUNTS_COUNT = 1 + /** * Factory method to create an `AccountList` instance. * Validates the input to ensure the accounts list is not empty and contains exactly one main account. @@ -73,8 +123,10 @@ data class AccountList private constructor( ): Either = either { ensure(accounts.isNotEmpty()) { Error.EmptyAccountsList } + ensure(accounts.size <= MAX_ACCOUNTS_COUNT) { Error.ExceedsMaxAccountsCount } + val mainAccountsCount = accounts.mainAccountsCount() - ensure(mainAccountsCount == 1) { + ensure(mainAccountsCount == MAX_MAIN_ACCOUNTS_COUNT) { if (mainAccountsCount == 0) { Error.MainAccountNotFound } else { @@ -82,6 +134,9 @@ data class AccountList private constructor( } } + val uniqueAccountIdsCount = accounts.map { it.accountId.value }.distinct().size + ensure(accounts.size == uniqueAccountIdsCount) { Error.DuplicateAccountIds } + AccountList(userWallet = userWallet, accounts = accounts, totalAccounts = totalAccounts) } diff --git a/domain/account/src/main/java/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCase.kt b/domain/account/src/main/java/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCase.kt index cec8cbc039..d73f3e9eaa 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCase.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCase.kt @@ -1,50 +1,129 @@ package com.tangem.domain.account.usecase import arrow.core.Either +import arrow.core.getOrElse +import arrow.core.raise.Raise +import arrow.core.raise.catch import arrow.core.raise.either -import com.tangem.domain.models.TokensGroupType -import com.tangem.domain.models.TokensSortType +import arrow.core.raise.ensure +import com.tangem.domain.account.models.AccountList +import com.tangem.domain.account.repository.AccountsCRUDRepository import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.account.AccountName import com.tangem.domain.models.account.CryptoPortfolioIcon -import kotlin.random.Random +import com.tangem.domain.models.wallet.UserWalletId /** + * Use case for updating a crypto portfolio account. + * + * @property crudRepository the repository used for performing CRUD operations on accounts + * [REDACTED_AUTHOR] */ -class UpdateCryptoPortfolioUseCase { +class UpdateCryptoPortfolioUseCase( + private val crudRepository: AccountsCRUDRepository, +) { + /** + * Updates a crypto portfolio account with the provided name and/or icon + * + * @param accountId the unique identifier of the account to update + * @param accountName the new name for the account (optional) + * @param icon the new icon for the account (optional) + * @return an [Either] containing the updated [Account.CryptoPortfolio] on success, or an [Error] on failure + */ suspend operator fun invoke( accountId: AccountId, - name: AccountName? = null, + accountName: AccountName? = null, icon: CryptoPortfolioIcon? = null, ): Either = either { - Account.CryptoPortfolio( - accountId = accountId, - name = name?.value ?: "Account", - accountIcon = icon ?: CryptoPortfolioIcon.ofDefaultCustomAccount(), - derivationIndex = Random.nextInt(from = 0, until = 21), - isArchived = false, - cryptoCurrencyList = Account.CryptoPortfolio.CryptoCurrencyList( - currencies = emptySet(), - sortType = TokensSortType.NONE, - groupType = TokensGroupType.NONE, - ), - ) - .mapLeft { Error.DataOperationFailed } - .bind() + ensure(accountName != null || icon != null) { Error.NothingToUpdate } - // TODO: [REDACTED_JIRA] - // Create a domain model AccountName - // Get the current account by [accountId] - // Create a new domain model Account from old data considering new parameters - // Save information in local storage - // Save to backend + val accountList = getAccountList(userWalletId = accountId.userWalletId) + + val account = accountList.accounts + .firstOrNull { it.accountId == accountId } as? Account.CryptoPortfolio + ?: raise(Error.CriticalTechError.AccountNotFound(accountId = accountId)) + + val updatedAccount = account + .setName(name = accountName) + .setIcon(icon = icon) + + val updatedAccounts = (accountList + updatedAccount) + .getOrElse { raise(Error.CriticalTechError.AccountListRequirementsNotMet(it)) } + + saveAccounts(updatedAccounts) + + updatedAccount } + private suspend fun Raise.getAccountList(userWalletId: UserWalletId): AccountList { + return catch( + block = { crudRepository.getAccounts(userWalletId = userWalletId) }, + catch = { raise(Error.DataOperationFailed(cause = it)) }, + ) + .getOrElse { raise(Error.CriticalTechError.AccountsNotCreated(userWalletId = userWalletId)) } + } + + private suspend fun Raise.saveAccounts(accountList: AccountList) { + catch( + block = { crudRepository.saveAccounts(accountList) }, + catch = { raise(Error.DataOperationFailed(cause = it)) }, + ) + } + + private fun Account.CryptoPortfolio.setName(name: AccountName?): Account.CryptoPortfolio { + return if (name != null) this.copy(accountName = name) else this + } + + private fun Account.CryptoPortfolio.setIcon(icon: CryptoPortfolioIcon?): Account.CryptoPortfolio { + return if (icon != null) this.copy(accountIcon = icon) else this + } + + /** + * Represents possible errors that can occur during the update operation + */ sealed interface Error { - data object DataOperationFailed : Error + /** Error indicating that there is nothing to update */ + data object NothingToUpdate : Error { + override fun toString(): String = "Nothing to update: both account name and icon are null" + } + + /** Error indicating that a data operation failed */ + data class DataOperationFailed(val cause: Throwable) : Error { + override fun toString(): String = "Data operation failed: ${cause.message ?: "Unknown error"}" + } + + /** + * Represents critical technical errors that can occur during the update operation. + * These errors are a consequence of an inconsistent state. + */ + sealed interface CriticalTechError : Error { + + /** + + * + * @property userWalletId the unique identifier of the user wallet + */ + data class AccountsNotCreated(val userWalletId: UserWalletId) : CriticalTechError { + override fun toString(): String = "Accounts for $userWalletId are not created" + } + + /** Error indicating that the account with [accountId] was not found */ + data class AccountNotFound(val accountId: AccountId) : CriticalTechError { + override fun toString(): String = "Account with ID $accountId not found" + } + + /** + * Error indicating that the account list requirements were not met. + * + * @property cause the underlying cause of the error + */ + data class AccountListRequirementsNotMet(val cause: AccountList.Error) : CriticalTechError { + override fun toString(): String = "Account list requirements not met: $cause" + } + } } } \ No newline at end of file diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/models/AccountListTest.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/models/AccountListTest.kt index 2abafd8934..04bb22f7b6 100644 --- a/domain/account/src/test/kotlin/com/tangem/domain/account/models/AccountListTest.kt +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/models/AccountListTest.kt @@ -3,7 +3,9 @@ package com.tangem.domain.account.models import arrow.core.Either import arrow.core.left import com.google.common.truth.Truth +import com.tangem.domain.account.utils.randomAccountId import com.tangem.domain.models.account.Account +import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.wallet.UserWallet import io.mockk.clearMocks import io.mockk.every @@ -92,6 +94,33 @@ class AccountListTest { ), ) }, + createAccounts(count = 20).let { + CreateTestModel( + accounts = it, + expected = AccountList( + userWallet = userWallet, + accounts = it, + totalAccounts = 20, + ), + ) + }, + CreateTestModel( + accounts = createAccounts(21), + expected = AccountList.Error.ExceedsMaxAccountsCount.left(), + ), + CreateTestModel( + accounts = setOf( + createAccount( + accountId = AccountId(value = "1", userWalletId = mockk()), + isMain = true, + ), + createAccount( + accountId = AccountId(value = "1", userWalletId = mockk()), + isMain = false, + ), + ), + expected = AccountList.Error.DuplicateAccountIds.left(), + ), ) } @@ -100,9 +129,22 @@ class AccountListTest { val expected: Either, ) - private fun createAccount(isMain: Boolean = false): Account.CryptoPortfolio { + private fun createAccounts(count: Int): Set { + return buildSet { + add(createAccount(isMain = true)) + repeat(count - 1) { + add(createAccount(isMain = false)) + } + } + } + + private fun createAccount( + accountId: AccountId = AccountId(value = randomAccountId(5), userWalletId = mockk()), + isMain: Boolean = false, + ): Account.CryptoPortfolio { return mockk { - every { isMainAccount } returns isMain + every { this@mockk.accountId } returns accountId + every { this@mockk.isMainAccount } returns isMain } } } \ No newline at end of file diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCaseTest.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCaseTest.kt new file mode 100644 index 0000000000..6588216bc8 --- /dev/null +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCaseTest.kt @@ -0,0 +1,298 @@ +package com.tangem.domain.account.usecase + +import arrow.core.None +import arrow.core.left +import arrow.core.right +import arrow.core.toOption +import com.google.common.truth.Truth +import com.tangem.domain.account.models.AccountList +import com.tangem.domain.account.repository.AccountsCRUDRepository +import com.tangem.domain.account.usecase.UpdateCryptoPortfolioUseCase.Error +import com.tangem.domain.account.utils.randomAccountId +import com.tangem.domain.models.TokensGroupType +import com.tangem.domain.models.TokensSortType +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.account.AccountId +import com.tangem.domain.models.account.AccountName +import com.tangem.domain.models.account.CryptoPortfolioIcon +import io.mockk.* +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import kotlin.random.Random + +/** +[REDACTED_AUTHOR] + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class UpdateCryptoPortfolioUseCaseTest { + + private val crudRepository: AccountsCRUDRepository = mockk(relaxUnitFun = true) + private val useCase = UpdateCryptoPortfolioUseCase(crudRepository = crudRepository) + + @BeforeEach + fun resetMocks() { + clearMocks(crudRepository) + } + + @Test + fun `invoke should update crypto portfolio account with new name`() = runTest { + // Arrange + val accountId = AccountId(value = "test-account-id", userWalletId = mockk()) + val userWalletId = accountId.userWalletId + val account = createAccount(accountId = accountId, isMain = true) + val accountList = AccountList( + userWallet = mockk(), + accounts = setOf(account), + totalAccounts = 1, + ) + .getOrNull()!! + + val newAccountName = AccountName("New name").getOrNull()!! + + coEvery { crudRepository.getAccounts(userWalletId = userWalletId) } returns accountList.toOption() + + // Act + val actual = useCase(accountId = accountId, accountName = newAccountName) + + // Assert + val updatedAccount = account.copy(accountName = newAccountName) + val expected = updatedAccount.right() + Truth.assertThat(actual).isEqualTo(expected) + + val updatedAccountList = (accountList + updatedAccount).getOrNull()!! + coVerifyOrder { + crudRepository.getAccounts(userWalletId = userWalletId) + crudRepository.saveAccounts(accountList = updatedAccountList) + } + } + + @Test + fun `invoke should update crypto portfolio account with new icon`() = runTest { + // Arrange + val accountId = AccountId(value = "test-account-id", userWalletId = mockk()) + val userWalletId = accountId.userWalletId + val account = createAccount(accountId = accountId, isMain = true) + val accountList = AccountList( + userWallet = mockk(), + accounts = setOf(account), + totalAccounts = 1, + ) + .getOrNull()!! + + val newAccountIcon = CryptoPortfolioIcon.ofCustomAccount( + value = CryptoPortfolioIcon.Icon.Star, + color = CryptoPortfolioIcon.Color.CaribbeanBlue, + ) + + coEvery { crudRepository.getAccounts(userWalletId = userWalletId) } returns accountList.toOption() + + // Act + val actual = useCase(accountId = accountId, icon = newAccountIcon) + + // Assert + val updatedAccount = account.copy(accountIcon = newAccountIcon) + val expected = updatedAccount.right() + Truth.assertThat(actual).isEqualTo(expected) + + val updatedAccountList = (accountList + updatedAccount).getOrNull()!! + coVerifyOrder { + crudRepository.getAccounts(userWalletId = userWalletId) + crudRepository.saveAccounts(accountList = updatedAccountList) + } + } + + @Test + fun `invoke should update crypto portfolio account with new name and icon`() = runTest { + // Arrange + val accountId = AccountId(value = "test-account-id", userWalletId = mockk()) + val userWalletId = accountId.userWalletId + val account = createAccount(accountId = accountId, isMain = true) + val accountList = AccountList( + userWallet = mockk(), + accounts = setOf(account), + totalAccounts = 1, + ) + .getOrNull()!! + + val newAccountName = AccountName("New name").getOrNull()!! + val newAccountIcon = CryptoPortfolioIcon.ofCustomAccount( + value = CryptoPortfolioIcon.Icon.Star, + color = CryptoPortfolioIcon.Color.CaribbeanBlue, + ) + + coEvery { crudRepository.getAccounts(userWalletId = userWalletId) } returns accountList.toOption() + + // Act + val actual = useCase(accountId = accountId, accountName = newAccountName, icon = newAccountIcon) + + // Assert + val updatedAccount = account.copy(accountName = newAccountName, accountIcon = newAccountIcon) + val expected = updatedAccount.right() + Truth.assertThat(actual).isEqualTo(expected) + + val updatedAccountList = (accountList + updatedAccount).getOrNull()!! + coVerifyOrder { + crudRepository.getAccounts(userWalletId = userWalletId) + crudRepository.saveAccounts(accountList = updatedAccountList) + } + } + + @Test + fun `invoke if name and icon are null`() = runTest { + // Arrange + val accountId = AccountId(value = "test-account-id", userWalletId = mockk()) + val userWalletId = accountId.userWalletId + val account = createAccount(accountId = accountId, isMain = true) + val accountList = AccountList( + userWallet = mockk(), + accounts = setOf(account), + totalAccounts = 1, + ) + .getOrNull()!! + + coEvery { crudRepository.getAccounts(userWalletId = userWalletId) } returns accountList.toOption() + + // Act + val actual = useCase(accountId = accountId) + + // Assert + val expected = Error.NothingToUpdate.left() + Truth.assertThat(actual).isEqualTo(expected) + + coVerify(inverse = true) { + crudRepository.getAccounts(userWalletId = any()) + crudRepository.saveAccounts(accountList = any()) + } + } + + @Test + fun `invoke if getAccounts throws exception`() = runTest { + // Arrange + val accountId = AccountId(value = "test-account-id", userWalletId = mockk()) + val userWalletId = accountId.userWalletId + + val newAccountName = AccountName("New name").getOrNull()!! + val exception = IllegalStateException("Test exception") + + coEvery { crudRepository.getAccounts(userWalletId = userWalletId) } throws exception + + // Act + val actual = useCase(accountId = accountId, accountName = newAccountName) + + // Assert + val expected = Error.DataOperationFailed(cause = exception).left() + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { crudRepository.getAccounts(userWalletId = userWalletId) } + coVerify(inverse = true) { crudRepository.saveAccounts(accountList = any()) } + } + + @Test + fun `invoke if getAccounts returns None`() = runTest { + // Arrange + val accountId = AccountId(value = "test-account-id", userWalletId = mockk()) + val userWalletId = accountId.userWalletId + val accountList = None + + val newAccountName = AccountName("New name").getOrNull()!! + + coEvery { crudRepository.getAccounts(userWalletId = userWalletId) } returns accountList + + // Act + val actual = useCase(accountId = accountId, accountName = newAccountName) + + // Assert + val expected = Error.CriticalTechError.AccountsNotCreated(userWalletId = userWalletId).left() + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { crudRepository.getAccounts(userWalletId = userWalletId) } + coVerify(inverse = true) { crudRepository.saveAccounts(accountList = any()) } + } + + @Test + fun `invoke if getAccounts does not contain accountId`() = runTest { + // Arrange + val accountId = AccountId(value = "test-account-id", userWalletId = mockk()) + val userWalletId = accountId.userWalletId + val account = createAccount( + accountId = AccountId(value = "another-account-id", userWalletId = mockk()), + isMain = true, + ) + val accountList = AccountList( + userWallet = mockk(), + accounts = setOf(account), + totalAccounts = 1, + ) + .getOrNull()!! + + val newAccountName = AccountName("New name").getOrNull()!! + + coEvery { crudRepository.getAccounts(userWalletId = userWalletId) } returns accountList.toOption() + + // Act + val actual = useCase(accountId = accountId, accountName = newAccountName) + + // Assert + val expected = Error.CriticalTechError.AccountNotFound(accountId = accountId).left() + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { crudRepository.getAccounts(userWalletId = userWalletId) } + coVerify(inverse = true) { crudRepository.saveAccounts(accountList = any()) } + } + + @Test + fun `invoke if saveAccounts throws exception`() = runTest { + // Arrange + val accountId = AccountId(value = "test-account-id", userWalletId = mockk()) + val userWalletId = accountId.userWalletId + val account = createAccount(accountId = accountId, isMain = true) + val accountList = AccountList( + userWallet = mockk(), + accounts = setOf(account), + totalAccounts = 1, + ).getOrNull()!! + + val newAccountName = AccountName("New name").getOrNull()!! + val updatedAccount = account.copy(accountName = newAccountName) + val updatedAccountList = (accountList + updatedAccount).getOrNull()!! + + val exception = IllegalStateException("Save failed") + + coEvery { crudRepository.getAccounts(userWalletId = userWalletId) } returns accountList.toOption() + coEvery { crudRepository.saveAccounts(accountList = updatedAccountList) } throws exception + + // Act + val actual = useCase(accountId = accountId, accountName = newAccountName) + + // Assert + val expected = Error.DataOperationFailed(cause = exception).left() + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { + crudRepository.getAccounts(userWalletId = userWalletId) + crudRepository.saveAccounts(accountList = updatedAccountList) + } + } + + private fun createAccount( + accountId: AccountId = AccountId(value = randomAccountId(length = 5), userWalletId = mockk()), + accountIcon: CryptoPortfolioIcon = CryptoPortfolioIcon.ofDefaultCustomAccount(), + isMain: Boolean, + ): Account.CryptoPortfolio { + return Account.CryptoPortfolio( + accountId = accountId, + name = "Test Account", + accountIcon = accountIcon, + derivationIndex = if (isMain) 0 else Random.nextInt(1, 21), + isArchived = false, + cryptoCurrencyList = Account.CryptoPortfolio.CryptoCurrencyList( + currencies = emptySet(), + sortType = TokensSortType.NONE, + groupType = TokensGroupType.NONE, + ), + ) + .getOrNull()!! + } +} \ No newline at end of file diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/utils/AccountExt.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/utils/AccountExt.kt new file mode 100644 index 0000000000..1186faab17 --- /dev/null +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/utils/AccountExt.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.account.utils + +fun randomAccountId(length: Int): String { + val chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" + return (1..length) + .map { chars.random() } + .joinToString("") +} \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/account/Account.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/account/Account.kt index bbea116526..8d7b786cca 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/account/Account.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/account/Account.kt @@ -60,6 +60,21 @@ sealed interface Account { val networksCount: Int get() = cryptoCurrencyList.currencies.map(CryptoCurrency::network).distinct().size + fun copy( + accountName: AccountName = this.name, + accountIcon: CryptoPortfolioIcon = this.icon, + isArchived: Boolean = this.isArchived, + ): CryptoPortfolio { + return CryptoPortfolio( + accountId = this.accountId, + name = accountName, + icon = accountIcon, + derivationIndex = this.derivationIndex, + isArchived = isArchived, + cryptoCurrencyList = this.cryptoCurrencyList, + ) + } + /** * Represents a list of tokens in the account * @@ -117,6 +132,38 @@ sealed interface Account { return either { val accountName = AccountName(name).mapLeft(::AccountNameError).bind() + invoke( + accountId = accountId, + accountName = accountName, + accountIcon = accountIcon, + derivationIndex = derivationIndex, + isArchived = isArchived, + cryptoCurrencyList = cryptoCurrencyList, + ) + .bind() + } + } + + /** + * Constructor for creating a [CryptoPortfolio] instance + * + * @param accountId unique identifier of the account + * @param accountName name of the account + * @param accountIcon icon representing the account + * @param derivationIndex index used for derivation of the account + * @param isArchived indicates whether the account is archived + * @param cryptoCurrencyList list of tokens associated with the account + */ + @Suppress("LongParameterList") + operator fun invoke( + accountId: AccountId, + accountName: AccountName, + accountIcon: CryptoPortfolioIcon, + derivationIndex: Int, + isArchived: Boolean, + cryptoCurrencyList: CryptoCurrencyList, + ): Either { + return either { ensure(derivationIndex >= 0) { Error.NegativeDerivationIndex } CryptoPortfolio(