Updated on 2026-08-14
This commit is contained in:
parent
3b65bef8a8
commit
75cdf4011c
5 changed files with 479 additions and 7 deletions
|
|
@ -1,6 +1,8 @@
|
|||
package com.tangem.common.test.utils
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.None
|
||||
import arrow.core.Option
|
||||
import com.google.common.truth.Truth
|
||||
|
||||
fun <B> assertEither(actual: Either<Throwable, B>, expected: Either<Throwable, B>) {
|
||||
|
|
@ -29,4 +31,16 @@ fun <B> assertEitherLeft(actual: Either<Throwable, B>, expected: Throwable) {
|
|||
Truth.assertThat(it::class.java).isEqualTo(expected::class.java)
|
||||
Truth.assertThat(it).hasMessageThat().isEqualTo(expected.message)
|
||||
}
|
||||
}
|
||||
|
||||
fun <B> assertNone(actual: Option<B>) {
|
||||
Truth.assertThat(actual).isEqualTo(None)
|
||||
}
|
||||
|
||||
fun <B> assertSome(actual: Option<B>, expected: B) {
|
||||
actual
|
||||
.onNone { error("Actual is None") }
|
||||
.onSome {
|
||||
Truth.assertThat(it).isEqualTo(expected)
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ plugins {
|
|||
alias(deps.plugins.kotlin.android)
|
||||
alias(deps.plugins.kotlin.kapt)
|
||||
alias(deps.plugins.hilt.android)
|
||||
alias(deps.plugins.kotlin.serialization)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
|
|
@ -15,18 +16,20 @@ tasks.withType<Test>().configureEach {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.domain.account)
|
||||
implementation(projects.domain.core)
|
||||
api(projects.domain.account)
|
||||
api(projects.domain.core)
|
||||
api(projects.domain.common)
|
||||
implementation(projects.domain.quotes)
|
||||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.networks)
|
||||
implementation(projects.domain.staking)
|
||||
implementation(projects.domain.tokens)
|
||||
api(projects.domain.quotes)
|
||||
api(projects.domain.models)
|
||||
api(projects.domain.networks)
|
||||
api(projects.domain.staking)
|
||||
api(projects.domain.tokens)
|
||||
|
||||
implementation(projects.libs.crypto)
|
||||
|
||||
implementation(deps.kotlin.datetime)
|
||||
implementation(deps.kotlin.serialization)
|
||||
implementation(deps.timber)
|
||||
|
||||
// region DI
|
||||
implementation(deps.hilt.android)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.domain.account.status.model
|
||||
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Combines an [Account] with its corresponding [CryptoCurrency].
|
||||
*
|
||||
* This data class is useful for representing the relationship between an account and the cryptocurrency it holds.
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Serializable
|
||||
data class AccountCryptoCurrency(val account: Account.CryptoPortfolio, val cryptoCurrency: CryptoCurrency) {
|
||||
|
||||
init {
|
||||
require(account.cryptoCurrencies.contains(cryptoCurrency))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,136 @@
|
|||
package com.tangem.domain.account.status.usecase
|
||||
|
||||
import arrow.core.NonEmptyList
|
||||
import arrow.core.None
|
||||
import arrow.core.Option
|
||||
import arrow.core.raise.OptionRaise
|
||||
import arrow.core.raise.RaiseDSL
|
||||
import arrow.core.raise.option
|
||||
import arrow.core.toNonEmptyListOrNull
|
||||
import com.tangem.domain.account.models.AccountList
|
||||
import com.tangem.domain.account.producer.SingleAccountListProducer
|
||||
import com.tangem.domain.account.status.model.AccountCryptoCurrency
|
||||
import com.tangem.domain.account.supplier.SingleAccountListSupplier
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.network.getAddress
|
||||
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.multi.MultiNetworkStatusProducer
|
||||
import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier
|
||||
import timber.log.Timber
|
||||
import kotlin.contracts.ExperimentalContracts
|
||||
import kotlin.contracts.contract
|
||||
|
||||
private typealias WalletIdWithNetworkId = Pair<UserWalletId, Network.ID>
|
||||
|
||||
/**
|
||||
* Use case to retrieve an [AccountCryptoCurrency] based on a provided address.
|
||||
*
|
||||
* @property userWalletsListRepository Repository to access user wallets.
|
||||
* @property multiNetworkStatusSupplier Supplier to get network status for multiple networks.
|
||||
* @property singleAccountListSupplier Supplier to get account lists for a single wallet.
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class GetAccountCurrencyByAddressUseCase(
|
||||
private val userWalletsListRepository: UserWalletsListRepository,
|
||||
private val multiNetworkStatusSupplier: MultiNetworkStatusSupplier,
|
||||
private val singleAccountListSupplier: SingleAccountListSupplier,
|
||||
) {
|
||||
|
||||
/**
|
||||
* Invokes the use case to get an [AccountCryptoCurrency] for the given address.
|
||||
*
|
||||
* @param address The address to look up.
|
||||
* @return An [Option] containing the [AccountCryptoCurrency] if found, or [None] if not found or if any validation
|
||||
* fails.
|
||||
*/
|
||||
suspend operator fun invoke(address: String): Option<AccountCryptoCurrency> = option {
|
||||
validate(address)
|
||||
|
||||
val userWalletIds = getUserWalletIds()
|
||||
|
||||
val (walletId, networkId) = getWalletIdWithNetworkId(userWalletIds, address)
|
||||
|
||||
val accountList = getAccountList(userWalletId = walletId)
|
||||
|
||||
getAccountCryptoCurrency(accountList, networkId)
|
||||
}
|
||||
|
||||
private fun OptionRaise.validate(address: String) {
|
||||
ensureNotNull(address.takeIf { it.isNotEmpty() }) { "Address is empty" }
|
||||
}
|
||||
|
||||
private fun OptionRaise.getUserWalletIds(): NonEmptyList<UserWalletId> {
|
||||
val userWalletIds = userWalletsListRepository.userWallets.value.orEmpty()
|
||||
.filter(UserWallet::isMultiCurrency)
|
||||
.map(UserWallet::walletId)
|
||||
.toNonEmptyListOrNull()
|
||||
|
||||
return ensureNotNull(userWalletIds) { "No multi-currency wallets found" }
|
||||
}
|
||||
|
||||
private suspend fun OptionRaise.getWalletIdWithNetworkId(
|
||||
userWalletIds: NonEmptyList<UserWalletId>,
|
||||
address: String,
|
||||
): WalletIdWithNetworkId {
|
||||
var pair: WalletIdWithNetworkId? = null
|
||||
|
||||
for (id in userWalletIds) {
|
||||
val networkStatus = multiNetworkStatusSupplier.getSyncOrNull(
|
||||
params = MultiNetworkStatusProducer.Params(userWalletId = id),
|
||||
)
|
||||
?.firstOrNull { it.getAddress() == address }
|
||||
|
||||
if (networkStatus != null) {
|
||||
pair = id to networkStatus.network.id
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return ensureNotNull(value = pair) { "No network status found for address: $address" }
|
||||
}
|
||||
|
||||
private suspend fun OptionRaise.getAccountList(userWalletId: UserWalletId): AccountList {
|
||||
val accountList = singleAccountListSupplier.getSyncOrNull(
|
||||
params = SingleAccountListProducer.Params(userWalletId = userWalletId),
|
||||
)
|
||||
|
||||
return ensureNotNull(accountList) { "No account list found for walletId: $userWalletId" }
|
||||
}
|
||||
|
||||
private fun OptionRaise.getAccountCryptoCurrency(
|
||||
accountList: AccountList,
|
||||
networkId: Network.ID,
|
||||
): AccountCryptoCurrency {
|
||||
val result = accountList.accounts.asSequence()
|
||||
.filterIsInstance<Account.CryptoPortfolio>()
|
||||
.mapNotNull { account ->
|
||||
val currency = account.cryptoCurrencies.firstOrNull { it.network.id == networkId }
|
||||
?: return@mapNotNull null
|
||||
|
||||
AccountCryptoCurrency(account = account, cryptoCurrency = currency)
|
||||
}
|
||||
.firstOrNull()
|
||||
|
||||
return ensureNotNull(result) {
|
||||
"No account found for network: $networkId in walletId: ${accountList.userWallet.walletId}"
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalContracts::class)
|
||||
@RaiseDSL
|
||||
inline fun <B : Any> OptionRaise.ensureNotNull(value: B?, message: () -> String): B {
|
||||
contract {
|
||||
returns() implies (value != null)
|
||||
}
|
||||
|
||||
return value ?: run {
|
||||
Timber.d(message())
|
||||
raise(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,299 @@
|
|||
package com.tangem.domain.account.status.usecase
|
||||
|
||||
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.domain.account.models.AccountList
|
||||
import com.tangem.domain.account.producer.SingleAccountListProducer
|
||||
import com.tangem.domain.account.status.model.AccountCryptoCurrency
|
||||
import com.tangem.domain.account.supplier.SingleAccountListSupplier
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.network.NetworkAddress
|
||||
import com.tangem.domain.models.network.NetworkStatus
|
||||
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.multi.MultiNetworkStatusProducer
|
||||
import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier
|
||||
import io.mockk.*
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
class GetAccountCurrencyByAddressUseCaseTest {
|
||||
|
||||
private val userWalletsListRepository: UserWalletsListRepository = mockk()
|
||||
private val multiNetworkStatusSupplier: MultiNetworkStatusSupplier = mockk()
|
||||
private val singleAccountListSupplier: SingleAccountListSupplier = mockk()
|
||||
|
||||
private val useCase = GetAccountCurrencyByAddressUseCase(
|
||||
userWalletsListRepository = userWalletsListRepository,
|
||||
multiNetworkStatusSupplier = multiNetworkStatusSupplier,
|
||||
singleAccountListSupplier = singleAccountListSupplier,
|
||||
)
|
||||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
clearMocks(userWalletsListRepository, multiNetworkStatusSupplier, singleAccountListSupplier)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns None if address is empty`() = runTest {
|
||||
// Arrange
|
||||
val address = ""
|
||||
|
||||
// Act
|
||||
val actual = useCase(address)
|
||||
|
||||
// Assert
|
||||
assertNone(actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns None if userWalletIds is null`() = runTest {
|
||||
// Arrange
|
||||
every { userWalletsListRepository.userWallets } returns MutableStateFlow(null)
|
||||
|
||||
// Act
|
||||
val actual = useCase(validAddress)
|
||||
|
||||
// Assert
|
||||
assertNone(actual)
|
||||
|
||||
coVerifySequence {
|
||||
userWalletsListRepository.userWallets
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns None if userWalletIds is empty list`() = runTest {
|
||||
// Arrange
|
||||
every { userWalletsListRepository.userWallets } returns MutableStateFlow(emptyList())
|
||||
|
||||
// Act
|
||||
val actual = useCase(validAddress)
|
||||
|
||||
// Assert
|
||||
assertNone(actual)
|
||||
|
||||
coVerifySequence {
|
||||
userWalletsListRepository.userWallets
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns None if userWalletIds contain only single wallet`() = runTest {
|
||||
// Arrange
|
||||
val singleWallet = mockk<UserWallet> {
|
||||
every { isMultiCurrency } returns false
|
||||
}
|
||||
|
||||
every { userWalletsListRepository.userWallets } returns MutableStateFlow(listOf(singleWallet))
|
||||
|
||||
// Act
|
||||
val actual = useCase(validAddress)
|
||||
|
||||
// Assert
|
||||
assertNone(actual)
|
||||
|
||||
coVerifySequence {
|
||||
userWalletsListRepository.userWallets
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns None if multiNetworkStatusSupplier returns null`() = runTest {
|
||||
// Arrange
|
||||
every { userWalletsListRepository.userWallets } returns MutableStateFlow(listOf(multiUserWallet))
|
||||
coEvery {
|
||||
multiNetworkStatusSupplier.getSyncOrNull(params = MultiNetworkStatusProducer.Params(userWalletId))
|
||||
} returns null
|
||||
|
||||
// Act
|
||||
val actual = useCase(validAddress)
|
||||
|
||||
// Assert
|
||||
assertNone(actual)
|
||||
|
||||
coVerifySequence {
|
||||
userWalletsListRepository.userWallets
|
||||
multiNetworkStatusSupplier.getSyncOrNull(params = MultiNetworkStatusProducer.Params(userWalletId))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns None if multiNetworkStatusSupplier returns empty list`() = runTest {
|
||||
// Arrange
|
||||
every { userWalletsListRepository.userWallets } returns MutableStateFlow(listOf(multiUserWallet))
|
||||
coEvery {
|
||||
multiNetworkStatusSupplier.getSyncOrNull(params = MultiNetworkStatusProducer.Params(userWalletId))
|
||||
} returns emptySet()
|
||||
|
||||
// Act
|
||||
val actual = useCase(validAddress)
|
||||
|
||||
// Assert
|
||||
assertNone(actual)
|
||||
|
||||
coVerifySequence {
|
||||
userWalletsListRepository.userWallets
|
||||
multiNetworkStatusSupplier.getSyncOrNull(params = MultiNetworkStatusProducer.Params(userWalletId))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns None if network status not found`() = runTest {
|
||||
// Arrange
|
||||
val networkStatus = NetworkStatus(
|
||||
network = mockk(),
|
||||
value = NetworkStatus.Unreachable(address = null),
|
||||
)
|
||||
|
||||
every { userWalletsListRepository.userWallets } returns MutableStateFlow(listOf(multiUserWallet))
|
||||
coEvery {
|
||||
multiNetworkStatusSupplier.getSyncOrNull(params = MultiNetworkStatusProducer.Params(userWalletId))
|
||||
} returns setOf(networkStatus)
|
||||
|
||||
// Act
|
||||
val actual = useCase(validAddress)
|
||||
|
||||
// Assert
|
||||
assertNone(actual)
|
||||
|
||||
coVerifySequence {
|
||||
userWalletsListRepository.userWallets
|
||||
multiNetworkStatusSupplier.getSyncOrNull(params = MultiNetworkStatusProducer.Params(userWalletId))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns None if singleAccountListSupplier returns null`() = runTest {
|
||||
// Arrange
|
||||
val networkStatus = NetworkStatus(
|
||||
network = mockk {
|
||||
every { id } returns Network.ID(value = "bitcoin", derivationPath = Network.DerivationPath.None)
|
||||
},
|
||||
value = NetworkStatus.Unreachable(address = validNetworkAddress),
|
||||
)
|
||||
|
||||
every { userWalletsListRepository.userWallets } returns MutableStateFlow(listOf(multiUserWallet))
|
||||
coEvery {
|
||||
multiNetworkStatusSupplier.getSyncOrNull(params = MultiNetworkStatusProducer.Params(userWalletId))
|
||||
} returns setOf(networkStatus)
|
||||
coEvery {
|
||||
singleAccountListSupplier.getSyncOrNull(
|
||||
params = SingleAccountListProducer.Params(userWalletId = userWalletId),
|
||||
)
|
||||
} returns null
|
||||
|
||||
// Act
|
||||
val actual = useCase(validAddress)
|
||||
|
||||
// Assert
|
||||
assertNone(actual)
|
||||
|
||||
coVerifySequence {
|
||||
userWalletsListRepository.userWallets
|
||||
multiNetworkStatusSupplier.getSyncOrNull(params = MultiNetworkStatusProducer.Params(userWalletId))
|
||||
singleAccountListSupplier.getSyncOrNull(
|
||||
params = SingleAccountListProducer.Params(userWalletId = userWalletId),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns None if crypto currencies is empty list`() = runTest {
|
||||
// Arrange
|
||||
val networkId = Network.ID(value = "bitcoin", derivationPath = Network.DerivationPath.None)
|
||||
val networkStatus = NetworkStatus(
|
||||
network = mockk {
|
||||
every { id } returns networkId
|
||||
},
|
||||
value = NetworkStatus.Unreachable(address = validNetworkAddress),
|
||||
)
|
||||
val accountList = AccountList.empty(multiUserWallet)
|
||||
|
||||
every { userWalletsListRepository.userWallets } returns MutableStateFlow(listOf(multiUserWallet))
|
||||
coEvery {
|
||||
multiNetworkStatusSupplier.getSyncOrNull(params = MultiNetworkStatusProducer.Params(userWalletId))
|
||||
} returns setOf(networkStatus)
|
||||
coEvery {
|
||||
singleAccountListSupplier.getSyncOrNull(
|
||||
params = SingleAccountListProducer.Params(userWalletId = userWalletId),
|
||||
)
|
||||
} returns accountList
|
||||
|
||||
// Act
|
||||
val actual = useCase(validAddress)
|
||||
|
||||
// Assert
|
||||
assertNone(actual)
|
||||
|
||||
coVerifySequence {
|
||||
userWalletsListRepository.userWallets
|
||||
multiNetworkStatusSupplier.getSyncOrNull(params = MultiNetworkStatusProducer.Params(userWalletId))
|
||||
singleAccountListSupplier.getSyncOrNull(
|
||||
params = SingleAccountListProducer.Params(userWalletId = userWalletId),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns Some if all data is valid`() = runTest {
|
||||
// Arrange
|
||||
val currency = MockCryptoCurrencyFactory().ethereum
|
||||
val networkStatus = NetworkStatus(
|
||||
network = currency.network,
|
||||
value = NetworkStatus.Unreachable(address = validNetworkAddress),
|
||||
)
|
||||
val accountList = AccountList.empty(userWallet = multiUserWallet, cryptoCurrencies = setOf(currency))
|
||||
|
||||
every { userWalletsListRepository.userWallets } returns MutableStateFlow(listOf(multiUserWallet))
|
||||
coEvery {
|
||||
multiNetworkStatusSupplier.getSyncOrNull(params = MultiNetworkStatusProducer.Params(userWalletId))
|
||||
} returns setOf(networkStatus)
|
||||
coEvery {
|
||||
singleAccountListSupplier.getSyncOrNull(
|
||||
params = SingleAccountListProducer.Params(userWalletId = userWalletId),
|
||||
)
|
||||
} returns accountList
|
||||
|
||||
// Act
|
||||
val actual = useCase(validAddress)
|
||||
|
||||
// Assert
|
||||
val expected = AccountCryptoCurrency(account = accountList.mainAccount, cryptoCurrency = currency)
|
||||
assertSome(actual, expected)
|
||||
|
||||
coVerifySequence {
|
||||
userWalletsListRepository.userWallets
|
||||
multiNetworkStatusSupplier.getSyncOrNull(params = MultiNetworkStatusProducer.Params(userWalletId))
|
||||
singleAccountListSupplier.getSyncOrNull(
|
||||
params = SingleAccountListProducer.Params(userWalletId = userWalletId),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private companion object Companion {
|
||||
const val validAddress = "0x1234567890abcdef"
|
||||
val validNetworkAddress = NetworkAddress.Single(
|
||||
defaultAddress = NetworkAddress.Address(
|
||||
value = validAddress,
|
||||
type = NetworkAddress.Address.Type.Primary,
|
||||
),
|
||||
)
|
||||
|
||||
val userWalletId = UserWalletId("011")
|
||||
val multiUserWallet = mockk<UserWallet> {
|
||||
every { this@mockk.walletId } returns userWalletId
|
||||
every { this@mockk.isMultiCurrency } returns true
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue