Updated on 2026-08-14

This commit is contained in:
Tangem 2025-08-07 18:58:42 +04:00
parent 0a8e78877d
commit 06263e1193
11 changed files with 360 additions and 436 deletions

View file

@ -2,10 +2,10 @@ package com.tangem.domain.models.account
import arrow.core.Either
import arrow.core.raise.either
import arrow.core.raise.ensure
import com.tangem.domain.models.TokensGroupType
import com.tangem.domain.models.TokensSortType
import com.tangem.domain.models.account.Account.CryptoPortfolio.Error.AccountNameError
import com.tangem.domain.models.account.Account.CryptoPortfolio.Error.DerivationIndexError
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWalletId
import kotlinx.serialization.Serializable
@ -43,14 +43,14 @@ sealed interface Account {
override val accountId: AccountId,
override val name: AccountName,
val icon: CryptoPortfolioIcon,
val derivationIndex: Int,
val derivationIndex: DerivationIndex,
val isArchived: Boolean,
val cryptoCurrencyList: CryptoCurrencyList,
) : Account {
/** Indicates if the account is the main account */
val isMainAccount: Boolean
get() = derivationIndex == 0
get() = derivationIndex.isMain
/** Number of tokens in the account */
val tokensCount: Int
@ -97,15 +97,11 @@ sealed interface Account {
/** Error indicating that the account name is blank */
@Serializable
data class AccountNameError(val cause: AccountName.Error) : Error {
override fun toString(): String = cause.toString()
}
data class AccountNameError(val cause: AccountName.Error) : Error
/** Error indicating that the derivation index is negative */
@Serializable
data object NegativeDerivationIndex : Error {
override fun toString(): String = "${this::class.simpleName}: Derivation index must be non-negative"
}
data class DerivationIndexError(val cause: DerivationIndex.Error) : Error
}
companion object {
@ -130,7 +126,8 @@ sealed interface Account {
cryptoCurrencyList: CryptoCurrencyList,
): Either<Error, CryptoPortfolio> {
return either {
val accountName = AccountName(name).mapLeft(::AccountNameError).bind()
val accountName = AccountName(value = name).mapLeft(::AccountNameError).bind()
val derivationIndex = DerivationIndex(derivationIndex).mapLeft(::DerivationIndexError).bind()
invoke(
accountId = accountId,
@ -140,7 +137,6 @@ sealed interface Account {
isArchived = isArchived,
cryptoCurrencyList = cryptoCurrencyList,
)
.bind()
}
}
@ -159,22 +155,18 @@ sealed interface Account {
accountId: AccountId,
accountName: AccountName,
accountIcon: CryptoPortfolioIcon,
derivationIndex: Int,
derivationIndex: DerivationIndex,
isArchived: Boolean,
cryptoCurrencyList: CryptoCurrencyList,
): Either<Error, CryptoPortfolio> {
return either {
ensure(derivationIndex >= 0) { Error.NegativeDerivationIndex }
CryptoPortfolio(
accountId = accountId,
name = accountName,
icon = accountIcon,
derivationIndex = derivationIndex,
isArchived = isArchived,
cryptoCurrencyList = cryptoCurrencyList,
)
}
): CryptoPortfolio {
return CryptoPortfolio(
accountId = accountId,
name = accountName,
icon = accountIcon,
derivationIndex = derivationIndex,
isArchived = isArchived,
cryptoCurrencyList = cryptoCurrencyList,
)
}
/**
@ -183,12 +175,16 @@ sealed interface Account {
* @param userWalletId the ID of the user wallet
*/
fun createMainAccount(userWalletId: UserWalletId): CryptoPortfolio {
// TODO: [REDACTED_JIRA]
val derivationIndex = DerivationIndex.Main
return CryptoPortfolio(
accountId = AccountId(userWalletId = userWalletId, value = "main_account"),
accountId = AccountId.forCryptoPortfolio(
userWalletId = userWalletId,
derivationIndex = derivationIndex,
),
name = AccountName.Main,
icon = CryptoPortfolioIcon.ofMainAccount(userWalletId),
derivationIndex = 0,
derivationIndex = derivationIndex,
isArchived = false,
cryptoCurrencyList = CryptoCurrencyList(
currencies = emptySet(),

View file

@ -1,7 +1,10 @@
package com.tangem.domain.models.account
import com.tangem.common.extensions.toByteArray
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.utils.extensions.toHexString
import kotlinx.serialization.Serializable
import java.security.MessageDigest
/**
* Represents a unique identifier for an account
@ -10,7 +13,26 @@ import kotlinx.serialization.Serializable
* @property userWalletId the identifier of the user wallet associated with the account
*/
@Serializable
data class AccountId(
data class AccountId private constructor(
val value: String,
val userWalletId: UserWalletId,
)
) {
companion object {
private val sha256Digest: MessageDigest by lazy { MessageDigest.getInstance("SHA-256") }
/**
* Creates a unique account identifier for a crypto portfolio
*
* @param userWalletId the identifier of the user wallet
* @param derivationIndex the derivation index used to generate the identifier
*/
fun forCryptoPortfolio(userWalletId: UserWalletId, derivationIndex: DerivationIndex): AccountId {
val input = userWalletId.value + derivationIndex.value.toByteArray()
val value = sha256Digest.digest(input).toHexString()
return AccountId(value = value, userWalletId = userWalletId)
}
}
}

View file

@ -0,0 +1,59 @@
package com.tangem.domain.models.account
import arrow.core.Either
import arrow.core.raise.either
import arrow.core.raise.ensure
import kotlinx.serialization.Serializable
/**
* Represents a derivation index for accounts, ensuring validity and providing utility methods
*
* @property value the integer value of the derivation index
*
[REDACTED_AUTHOR]
*/
@Serializable
data class DerivationIndex private constructor(
val value: Int,
) {
/** Checks if the derivation index corresponds to the main account */
val isMain: Boolean
get() = value == MAIN_ACCOUNT_DERIVATION_INDEX
/**
* Represents possible errors that can occur when creating a [DerivationIndex]
*/
@Serializable
sealed interface Error {
/** Error indicating that the provided derivation index [derivationIndex] is invalid */
@Serializable
data class NegativeDerivationIndex(val derivationIndex: Int) : Error {
override fun toString(): String {
return "${this::class.simpleName}: Derivation index cannot be negative: $derivationIndex"
}
}
}
companion object {
private const val MAIN_ACCOUNT_DERIVATION_INDEX = 0
/** Predefined instance of [DerivationIndex] for the main account */
val Main: DerivationIndex = DerivationIndex(value = MAIN_ACCOUNT_DERIVATION_INDEX)
/**
* Factory method to create a [DerivationIndex] instance
*
* @param value the integer value of the derivation index
*
* @return Either an error if the value is invalid, or a valid [DerivationIndex] instance
*/
operator fun invoke(value: Int): Either<Error, DerivationIndex> = either {
ensure(value >= 0) { Error.NegativeDerivationIndex(derivationIndex = value) }
DerivationIndex(value)
}
}
}

View file

@ -0,0 +1,44 @@
package com.tangem.domain.models.account
import com.google.common.truth.Truth
import com.tangem.domain.models.wallet.UserWalletId
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.MethodSource
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class AccountIdTest {
@ParameterizedTest
@MethodSource("provideTestModels")
fun forCryptoPortfolio(model: ForCryptoPortfolioModel) {
// Arrange
val userWalletId = UserWalletId("27163F47405CE73110837F24DF82607FF11C7AF9D78C93F409E4FEAFF3400C8F")
// Act
val actual = AccountId.forCryptoPortfolio(userWalletId = userWalletId, derivationIndex = model.derivationIndex)
// Assert
Truth.assertThat(actual.value).isEqualTo(model.expected)
}
private fun provideTestModels() = listOf(
ForCryptoPortfolioModel(
derivationIndex = DerivationIndex.Main,
expected = "4E39B13EA11E3B35339664A10BEF48F4AF752A1CC2200F79D23CB0FB3396C63F",
),
ForCryptoPortfolioModel(
derivationIndex = DerivationIndex(1).getOrNull()!!,
expected = "7F22E71F8106783F0F2DAFCDE525E2F2A2281E864DDBE2FE668FA09329D563A2",
),
ForCryptoPortfolioModel(
derivationIndex = DerivationIndex(42).getOrNull()!!,
expected = "555C1E17A302659446C97393453B7C2B3246AF4DA082C56C28FB6EDD1A6606A4",
),
)
data class ForCryptoPortfolioModel(
val derivationIndex: DerivationIndex,
val expected: String,
)
}

View file

@ -115,38 +115,18 @@ class AccountTest {
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `invoke returns NegativeDerivationIndex`() {
// Arrange
val derivationIndex = -1
// Act
val actual = CryptoPortfolio(
accountId = mockk(),
name = "Test Account",
accountIcon = mockk(),
derivationIndex = derivationIndex,
isArchived = false,
cryptoCurrencyList = mockk(),
)
.leftOrNull()!!
// Assert
val expected = CryptoPortfolio.Error.NegativeDerivationIndex
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `invoke returns CryptoPortfolio`() {
// Act
val derivationIndex = DerivationIndex.Main
val actual = CryptoPortfolio(
accountId = AccountId(
value = "value",
accountId = AccountId.forCryptoPortfolio(
userWalletId = UserWalletId("011"),
derivationIndex = derivationIndex,
),
name = "Test Account",
accountIcon = CryptoPortfolioIcon.ofMainAccount(userWalletId = UserWalletId("011")),
derivationIndex = 0,
derivationIndex = derivationIndex.value,
isArchived = false,
cryptoCurrencyList = CryptoCurrencyList(
currencies = emptySet(),
@ -165,24 +145,27 @@ class AccountTest {
fun createMainAccount() {
// Arrange
val userWalletId = UserWalletId("011")
val derivationIndex = DerivationIndex.Main
// Act
val actual = CryptoPortfolio.createMainAccount(userWalletId = userWalletId)
// Assert
// TODO: [REDACTED_JIRA]
val expected = CryptoPortfolio(
accountId = AccountId(userWalletId = userWalletId, value = "main_account"),
accountId = AccountId.forCryptoPortfolio(
userWalletId = userWalletId,
derivationIndex = derivationIndex,
),
accountName = AccountName.Main,
accountIcon = CryptoPortfolioIcon.ofMainAccount(userWalletId),
derivationIndex = 0,
derivationIndex = derivationIndex,
isArchived = false,
cryptoCurrencyList = CryptoCurrencyList(
currencies = emptySet(),
sortType = TokensSortType.NONE,
groupType = TokensGroupType.NONE,
),
).getOrNull()
)
Truth.assertThat(actual).isEqualTo(expected)
}
@ -194,11 +177,10 @@ class AccountTest {
derivationIndex: Int = 0,
currencies: Set<CryptoCurrency> = emptySet(),
): CryptoPortfolio {
val accountIndex = DerivationIndex(value = derivationIndex).getOrNull()!!
return CryptoPortfolio.invoke(
accountId = AccountId(
value = "value",
userWalletId = userWalletId,
),
accountId = AccountId.forCryptoPortfolio(userWalletId = userWalletId, derivationIndex = accountIndex),
name = name,
accountIcon = CryptoPortfolioIcon.ofMainAccount(userWalletId),
derivationIndex = derivationIndex,

View file

@ -0,0 +1,49 @@
package com.tangem.domain.models.account
import arrow.core.Either
import arrow.core.left
import arrow.core.right
import com.google.common.truth.Truth
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.MethodSource
/**
[REDACTED_AUTHOR]
*/
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class DerivationIndexTest {
@Test
fun `isMain returns true only for main derivation index`() {
// Arrange
val main = DerivationIndex.Main
val notMain = DerivationIndex(1).getOrNull()!!
// Act & Assert
Truth.assertThat(main.isMain).isTrue()
Truth.assertThat(notMain.isMain).isFalse()
}
@ParameterizedTest
@MethodSource("provideTestModels")
fun invoke(model: InvokeTestModel) {
// Act
val actual = DerivationIndex(model.index)
// Assert
Truth.assertThat(actual).isEqualTo(model.expected)
}
private fun provideTestModels() = listOf(
InvokeTestModel(index = 0, expected = DerivationIndex.Main.right()),
InvokeTestModel(index = 5, expected = DerivationIndex(5).getOrNull()!!.right()),
InvokeTestModel(index = -1, expected = DerivationIndex.Error.NegativeDerivationIndex(-1).left()),
)
data class InvokeTestModel(
val index: Int,
val expected: Either<DerivationIndex.Error, DerivationIndex>,
)
}