Updated on 2026-08-14

This commit is contained in:
Tangem 2025-05-17 17:51:43 +04:00
parent df076f15aa
commit 295f236805
10 changed files with 747 additions and 317 deletions

View file

@ -31,4 +31,6 @@ dependencies {
implementation(tangemDeps.blockchain)
implementation(tangemDeps.card.core)
implementation(deps.test.junit5)
}

View file

@ -14,20 +14,25 @@ object MockNetworkStatusFactory {
private val defaultNetwork = MockCryptoCurrencyFactory().ethereum.network
fun createVerified(network: Network = defaultNetwork, source: StatusSource = StatusSource.ACTUAL): NetworkStatus {
fun createVerified(
network: Network = defaultNetwork,
source: StatusSource = StatusSource.ACTUAL,
transform: (NetworkStatus.Verified) -> NetworkStatus.Verified = { it },
): NetworkStatus {
return NetworkStatus(
network = network,
value = NetworkStatus.Verified(
address = NetworkAddress.Single(
defaultAddress = NetworkAddress.Address(
value = "0x123",
value = "0x1",
type = NetworkAddress.Address.Type.Primary,
),
),
amounts = mapOf(),
pendingTransactions = mapOf(),
source = source,
),
)
.let(transform),
)
}
@ -37,7 +42,7 @@ object MockNetworkStatusFactory {
value = NetworkStatus.NoAccount(
address = NetworkAddress.Single(
defaultAddress = NetworkAddress.Address(
value = "0x123",
value = "0x1",
type = NetworkAddress.Address.Type.Primary,
),
),

View file

@ -27,7 +27,7 @@ class MockUpdateWalletManagerResultFactory {
return UpdateWalletManagerResult.NoAccount(
selectedAddress = "0x1",
addresses = setOf(Address(value = "0x1", type = Address.Type.Primary)),
amountToCreateAccount = BigDecimal.ZERO,
amountToCreateAccount = BigDecimal.ONE,
errorMessage = "",
)
}

View file

@ -0,0 +1,11 @@
package com.tangem.common.test.utils
import org.junit.jupiter.params.provider.MethodSource
/**
[REDACTED_AUTHOR]
*/
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
@MethodSource("provideTestModels")
annotation class ProvideTestModels

View file

@ -9,6 +9,10 @@ android {
namespace = "com.tangem.data.common"
}
tasks.withType<Test>().configureEach {
useJUnitPlatform()
}
dependencies {
/* Core */
implementation(projects.core.datasource)
@ -39,7 +43,8 @@ dependencies {
/* Test */
testImplementation(projects.common.test)
testImplementation(deps.test.coroutine)
testImplementation(deps.test.junit)
testImplementation(deps.test.junit5)
testRuntimeOnly(deps.test.junit5.engine)
testImplementation(deps.test.mockk)
testImplementation(deps.test.truth)
}

View file

@ -3,6 +3,7 @@ package com.tangem.data.common.currency
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
/**
@ -21,6 +22,18 @@ interface CardCryptoCurrencyFactory {
@Throws
suspend fun create(userWalletId: UserWalletId, network: Network): List<CryptoCurrency>
/**
* Create currencies for multi currency card
*
* @param userWallet user wallet
* @param networks networks
*/
@Throws
suspend fun createCurrenciesForMultiCurrencyCard(
userWallet: UserWallet,
networks: Set<Network>,
): Map<Network, List<CryptoCurrency>>
/**
* Create default coins for multi currency card
*

View file

@ -37,7 +37,9 @@ internal class DefaultCardCryptoCurrencyFactory(
val blockchain = Blockchain.fromNetworkId(networkId = network.backendId)
// multi-currency wallet
if (userWallet.isMultiCurrency) return getMultiWalletCurrencies(userWallet = userWallet, network = network)
if (userWallet.isMultiCurrency) {
return getMultiWalletCurrencies(userWallet = userWallet, networks = setOf(network))[network].orEmpty()
}
// check if the blockchain of single-currency wallet is the same as network
val cardBlockchain = userWallet.scanResponse.cardTypesResolver.getBlockchain()
@ -52,7 +54,18 @@ internal class DefaultCardCryptoCurrencyFactory(
return createPrimaryCurrencyForSingleCurrencyCard(userWallet.scanResponse).let(::listOf)
}
override suspend fun createCurrenciesForMultiCurrencyCard(
userWallet: UserWallet,
networks: Set<Network>,
): Map<Network, List<CryptoCurrency>> {
require(userWallet.isMultiCurrency) { "It isn't multi-currency wallet" }
return getMultiWalletCurrencies(userWallet = userWallet, networks = networks)
}
override fun createDefaultCoinsForMultiCurrencyCard(scanResponse: ScanResponse): List<CryptoCurrency.Coin> {
require(scanResponse.cardTypesResolver.isMultiwalletAllowed()) { "It isn't multi-currency wallet" }
val card = scanResponse.card
var blockchains = if (demoConfig.isDemoCardId(card.cardId)) {
@ -75,29 +88,39 @@ internal class DefaultCardCryptoCurrencyFactory(
}
override fun createPrimaryCurrencyForSingleCurrencyCard(scanResponse: ScanResponse): CryptoCurrency {
require(scanResponse.cardTypesResolver.isSingleWallet()) { "It isn't single-currency wallet" }
return with(getSingleWalletCurrencies(scanResponse)) {
primaryToken ?: coin
}
}
override fun createCurrenciesForSingleCurrencyCardWithToken(scanResponse: ScanResponse): List<CryptoCurrency> {
require(scanResponse.cardTypesResolver.isSingleWalletWithToken()) {
"It isn't single-currency wallet with token"
}
return with(getSingleWalletCurrencies(scanResponse)) {
listOfNotNull(coin, primaryToken)
}
}
private suspend fun getMultiWalletCurrencies(userWallet: UserWallet, network: Network): List<CryptoCurrency> {
private suspend fun getMultiWalletCurrencies(
userWallet: UserWallet,
networks: Set<Network>,
): Map<Network, List<CryptoCurrency>> {
val response = userTokensResponseStore.getSyncOrNull(userWalletId = userWallet.walletId)
?: return emptyList()
?: return emptyMap()
val responseCurrenciesFactory = ResponseCryptoCurrenciesFactory(excludedBlockchains)
return responseCurrenciesFactory.createCurrencies(
tokens = response.tokens.filter {
it.networkId == network.backendId && it.derivationPath == network.derivationPath.value
tokens = response.tokens.filter { token ->
networks.any { it.backendId == token.networkId && it.derivationPath.value == token.derivationPath }
},
scanResponse = userWallet.scanResponse,
)
.groupBy(CryptoCurrency::network)
}
private fun getSingleWalletCurrencies(scanResponse: ScanResponse): SingleWalletCurrencies {

View file

@ -7,6 +7,8 @@ import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.common.card.WalletData
import com.tangem.common.test.domain.card.MockScanResponseFactory
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
import com.tangem.common.test.utils.ProvideTestModels
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.local.token.UserTokensResponseStore
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.common.configs.GenericCardConfig
@ -14,16 +16,20 @@ import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.demo.DemoConfig
import com.tangem.domain.models.scan.ProductType
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import io.mockk.*
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.params.ParameterizedTest
/**
[REDACTED_AUTHOR]
*/
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
internal class DefaultCardCryptoCurrencyFactoryTest {
private val userWalletsStore: UserWalletsStore = mockk()
@ -36,325 +42,427 @@ internal class DefaultCardCryptoCurrencyFactoryTest {
userTokensResponseStore = userTokensResponseStore,
)
@Before
fun setup() {
private val cryptoCurrencyFactory = MockCryptoCurrencyFactory()
private val ethereum = cryptoCurrencyFactory.ethereum.setCanHandleTokens(value = true)
private val bitcoin = cryptoCurrencyFactory.createCoin(blockchain = Blockchain.Bitcoin)
private val userTokensResponseFactory = UserTokensResponseFactory()
private val iconUri: Uri = mockk()
@BeforeEach
fun init() {
clearMocks(userWalletsStore, userTokensResponseStore, iconUri)
mockkStatic(Uri::class)
every { Uri.parse(any()) } returns mockk()
every { Uri.parse(any()) } returns iconUri
}
@Test
fun `test create if userTokensResponse is not empty`() = runTest {
val multiWallet = createMultiWallet()
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class CreateMultiWallet {
val userTokensResponse = UserTokensResponseFactory().createUserTokensResponse(
currencies = listOf(ethereum),
isGroupedByNetwork = false,
isSortedByBalance = false,
@ParameterizedTest
@ProvideTestModels
fun `create currencies in ETH for multi-currency wallet`(model: CreateTestModel.MultiWallet) = runTest {
// Arrange
val userWallet = createMultiWallet()
val userTokensResponse = model.userTokensResponse
val network = ethereum.network
coEvery { userWalletsStore.getSyncStrict(key = userWallet.walletId) } returns userWallet
coEvery { userTokensResponseStore.getSyncOrNull(userWallet.walletId) } returns userTokensResponse
// Act
val actual = factory.create(userWalletId = userWallet.walletId, network = network)
// Assert
val expected = model.expected
Truth.assertThat(actual).isEqualTo(expected)
coVerifyOrder {
userWalletsStore.getSyncStrict(key = userWallet.walletId)
userTokensResponseStore.getSyncOrNull(userWalletId = userWallet.walletId)
}
}
private fun provideTestModels() = listOf(
CreateTestModel.MultiWallet(userTokensResponse = null, expected = emptyList()),
CreateTestModel.MultiWallet(userTokensResponse = createUserTokensResponse(), expected = emptyList()),
CreateTestModel.MultiWallet(
userTokensResponse = createUserTokensResponse(currencies = listOf(ethereum)),
expected = listOf(ethereum),
),
CreateTestModel.MultiWallet(
userTokensResponse = createUserTokensResponse(listOf(element = bitcoin)),
expected = emptyList(),
),
)
coEvery { userWalletsStore.getSyncStrict(key = multiWallet.walletId) } returns multiWallet
coEvery { userTokensResponseStore.getSyncOrNull(multiWallet.walletId) } returns userTokensResponse
val actual = factory.create(userWalletId = multiWallet.walletId, network = ethereum.network)
coVerifyOrder {
userWalletsStore.getSyncStrict(key = multiWallet.walletId)
userTokensResponseStore.getSyncOrNull(multiWallet.walletId)
}
val expected = listOf(ethereum)
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `test create if userTokensResponse is empty`() = runTest {
val multiWallet = createMultiWallet()
val userTokensResponse = UserTokensResponseFactory().createUserTokensResponse(
currencies = listOf(),
isGroupedByNetwork = false,
isSortedByBalance = false,
)
coEvery { userWalletsStore.getSyncStrict(key = multiWallet.walletId) } returns multiWallet
coEvery { userTokensResponseStore.getSyncOrNull(multiWallet.walletId) } returns userTokensResponse
val actual = factory.create(userWalletId = multiWallet.walletId, network = ethereum.network)
coVerifyOrder {
userWalletsStore.getSyncStrict(key = multiWallet.walletId)
userTokensResponseStore.getSyncOrNull(multiWallet.walletId)
}
val expected = emptyList<CryptoCurrency>()
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `test create if userTokensResponse is null`() = runTest {
val multiWallet = createMultiWallet()
coEvery { userWalletsStore.getSyncStrict(key = multiWallet.walletId) } returns multiWallet
coEvery { userTokensResponseStore.getSyncOrNull(multiWallet.walletId) } returns null
val actual = factory.create(userWalletId = multiWallet.walletId, network = ethereum.network)
coVerifyOrder {
userWalletsStore.getSyncStrict(key = multiWallet.walletId)
userTokensResponseStore.getSyncOrNull(multiWallet.walletId)
}
val expected = emptyList<CryptoCurrency>()
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `test create if userTokensResponse does not contain currency of selected network`() = runTest {
val multiWallet = createMultiWallet()
val userTokensResponse = UserTokensResponseFactory().createUserTokensResponse(
currencies = listOf(bitcoin),
isGroupedByNetwork = false,
isSortedByBalance = false,
)
coEvery { userWalletsStore.getSyncStrict(key = multiWallet.walletId) } returns multiWallet
coEvery { userTokensResponseStore.getSyncOrNull(multiWallet.walletId) } returns userTokensResponse
val actual = factory.create(userWalletId = multiWallet.walletId, network = ethereum.network)
coVerifyOrder {
userWalletsStore.getSyncStrict(key = multiWallet.walletId)
userTokensResponseStore.getSyncOrNull(multiWallet.walletId)
}
val expected = emptyList<CryptoCurrency>()
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `test create if single wallet has another primary network`() = runTest {
val singleWallet = createSingleWallet()
coEvery { userWalletsStore.getSyncStrict(key = singleWallet.walletId) } returns singleWallet
val actual = factory.create(userWalletId = singleWallet.walletId, network = bitcoin.network)
coVerifyOrder {
userWalletsStore.getSyncStrict(key = singleWallet.walletId)
singleWallet.scanResponse.cardTypesResolver.getBlockchain()
}
val expected = emptyList<CryptoCurrency>()
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `test create if card is single wallet`() = runTest {
val singleWallet = createSingleWallet()
coEvery { userWalletsStore.getSyncStrict(key = singleWallet.walletId) } returns singleWallet
val actual = factory.create(userWalletId = singleWallet.walletId, network = ethereum.network)
coVerifyOrder {
userWalletsStore.getSyncStrict(key = singleWallet.walletId)
singleWallet.scanResponse.cardTypesResolver.getBlockchain()
}
val expected = listOf(ethereum)
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `test create if card is single wallet with token`() = runTest {
val singleWallet = createSingleWalletWithToken()
coEvery { userWalletsStore.getSyncStrict(key = singleWallet.walletId) } returns singleWallet
val actual = factory.create(userWalletId = singleWallet.walletId, network = ethereum.network)
val token = CryptoCurrencyFactory(excludedBlockchains = ExcludedBlockchains()).createToken(
sdkToken = singleWallet.scanResponse.cardTypesResolver.getPrimaryToken()!!,
blockchain = Blockchain.Ethereum,
extraDerivationPath = null,
scanResponse = singleWallet.scanResponse,
)
val expected = listOf(ethereum, token)
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `test createDefaultCoinsForMultiCurrencyCard if card is prod`() = runTest {
val multiWallet = createMultiWallet()
val actual = factory.createDefaultCoinsForMultiCurrencyCard(scanResponse = multiWallet.scanResponse)
val expected = listOf(bitcoin, ethereum)
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `test createDefaultCoinsForMultiCurrencyCard if card is test`() = runTest {
val multiWallet = createMultiWallet().let {
it.copy(
scanResponse = it.scanResponse.copy(
card = it.scanResponse.card.copy(cardId = "FF99", batchId = "99FF"),
),
private fun createUserTokensResponse(currencies: List<CryptoCurrency> = emptyList()): UserTokensResponse {
return userTokensResponseFactory.createUserTokensResponse(
currencies = currencies,
isGroupedByNetwork = false,
isSortedByBalance = false,
)
}
val actual = factory.createDefaultCoinsForMultiCurrencyCard(scanResponse = multiWallet.scanResponse)
val expected = listOf(
cryptoCurrencyFactory.createCoin(blockchain = Blockchain.BitcoinTestnet),
cryptoCurrencyFactory.createCoin(blockchain = Blockchain.EthereumTestnet).setCanHandleTokens(true),
)
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `test createDefaultCoinsForMultiCurrencyCard if card is demo`() = runTest {
val multiWallet = createMultiWallet().let {
it.copy(
scanResponse = it.scanResponse.copy(
card = it.scanResponse.card.copy(cardId = "AC01000000041225"),
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class CreateSingleWallet {
@ParameterizedTest
@ProvideTestModels
fun `create currencies for single-currency wallet (ETH)`(model: CreateTestModel.SingleWallet) = runTest {
// Arrange
val userWallet = createSingleWallet()
coEvery { userWalletsStore.getSyncStrict(key = userWallet.walletId) } returns userWallet
// Act
val actual = factory.create(userWalletId = userWallet.walletId, network = model.network)
// Assert
val expected = model.expected
Truth.assertThat(actual).isEqualTo(expected)
coVerifyOrder {
userWalletsStore.getSyncStrict(key = userWallet.walletId)
userWallet.scanResponse.cardTypesResolver.getBlockchain()
}
coVerify(inverse = true) {
userTokensResponseStore.getSyncOrNull(userWalletId = any())
}
}
private fun provideTestModels() = listOf(
CreateTestModel.SingleWallet(network = ethereum.network, expected = listOf(ethereum)),
CreateTestModel.SingleWallet(network = bitcoin.network, expected = emptyList()),
)
}
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class CreateSingleWalletWithToken {
@ParameterizedTest
@ProvideTestModels
fun `create currencies for single-currency wallet with token (ETH)`(
model: CreateTestModel.SingleWalletWithToken,
) = runTest {
// Arrange
val userWallet = createSingleWalletWithToken()
coEvery { userWalletsStore.getSyncStrict(key = userWallet.walletId) } returns userWallet
// Act
val actual = factory.create(userWalletId = userWallet.walletId, network = model.network)
// Assert
val primaryToken = if (model.isPrimaryTokenExpected) {
createPrimaryToken(blockchain = Blockchain.Ethereum)
} else {
null
}
val expected = listOfNotNull(*model.expected.toTypedArray(), primaryToken)
Truth.assertThat(actual).isEqualTo(expected)
coVerifyOrder {
userWalletsStore.getSyncStrict(key = userWallet.walletId)
userWallet.scanResponse.cardTypesResolver.getBlockchain()
}
coVerify(inverse = true) {
userTokensResponseStore.getSyncOrNull(userWalletId = any())
}
}
private fun provideTestModels() = listOf(
CreateTestModel.SingleWalletWithToken(
network = ethereum.network,
isPrimaryTokenExpected = true,
expected = listOf(ethereum),
),
CreateTestModel.SingleWalletWithToken(
network = bitcoin.network,
isPrimaryTokenExpected = false,
expected = emptyList(),
),
)
}
sealed interface CreateTestModel {
val expected: List<CryptoCurrency>
data class MultiWallet(
val userTokensResponse: UserTokensResponse?,
override val expected: List<CryptoCurrency>,
) : CreateTestModel
data class SingleWallet(
val network: Network,
override val expected: List<CryptoCurrency>,
) : CreateTestModel
data class SingleWalletWithToken(
val network: Network,
val isPrimaryTokenExpected: Boolean,
override val expected: List<CryptoCurrency>,
) : CreateTestModel
}
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class CreateCurrenciesForMultiCurrencyCard {
@ParameterizedTest
@ProvideTestModels
fun `create currencies in ETH and BTC for multi-currency card`(model: CreateCurrenciesForMultiWalletModel) =
runTest {
// Arrange
val userWallet = model.multiWallet
val networks = setOf(ethereum.network, bitcoin.network)
val userTokensResponse = model.userTokensResponse
coEvery { userTokensResponseStore.getSyncOrNull(userWallet.walletId) } returns userTokensResponse
// Act
val actual = runCatching {
factory.createCurrenciesForMultiCurrencyCard(userWallet = userWallet, networks = networks)
}
// Assert
actual
.onSuccess {
val expected = model.expected.getOrNull()!!
Truth.assertThat(it).isEqualTo(expected)
}
.onFailure {
val exception = model.expected.exceptionOrNull()!!
Truth.assertThat(it).isInstanceOf(exception::class.java)
Truth.assertThat(it).hasMessageThat().isEqualTo(exception.message)
}
}
private fun provideTestModels() = listOf(
CreateCurrenciesForMultiWalletModel(
multiWallet = createMultiWallet(),
userTokensResponse = null,
expected = Result.success(emptyMap()),
),
CreateCurrenciesForMultiWalletModel(
multiWallet = createMultiWallet(),
userTokensResponse = createUserTokensResponse(),
expected = Result.success(emptyMap()),
),
CreateCurrenciesForMultiWalletModel(
multiWallet = createMultiWallet(),
userTokensResponse = createUserTokensResponse(currencies = listOf(bitcoin, ethereum)),
expected = mapOf(
bitcoin.network to listOf(bitcoin),
ethereum.network to listOf(ethereum),
).let(Result.Companion::success),
),
CreateCurrenciesForMultiWalletModel(
multiWallet = createSingleWallet(),
userTokensResponse = null,
expected = Result.failure(IllegalArgumentException("It isn't multi-currency wallet")),
),
CreateCurrenciesForMultiWalletModel(
multiWallet = createSingleWalletWithToken(),
userTokensResponse = null,
expected = Result.failure(IllegalArgumentException("It isn't multi-currency wallet")),
),
)
}
data class CreateCurrenciesForMultiWalletModel(
val multiWallet: UserWallet,
val userTokensResponse: UserTokensResponse?,
val expected: Result<Map<Network, List<CryptoCurrency>>>,
)
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class CreateDefaultCoinsForMultiCurrencyCard {
@ParameterizedTest
@ProvideTestModels
fun `create default coins for multi currency card`(model: CreateDefaultForMultiWalletModel) = runTest {
// Arrange
val multiWallet = model.multiWallet
// Act
val actual = factory.createDefaultCoinsForMultiCurrencyCard(scanResponse = multiWallet.scanResponse)
// Assert
val expected = model.expected
Truth.assertThat(actual).isEqualTo(expected)
}
private fun provideTestModels() = listOf(
// PROD card
CreateDefaultForMultiWalletModel(multiWallet = createMultiWallet(), expected = listOf(bitcoin, ethereum)),
// TEST card
CreateDefaultForMultiWalletModel(
multiWallet = createMultiWallet(cardId = "FF99", batchId = "99FF"),
expected = listOf(
cryptoCurrencyFactory.createCoin(blockchain = Blockchain.BitcoinTestnet),
cryptoCurrencyFactory.createCoin(blockchain = Blockchain.EthereumTestnet).setCanHandleTokens(true),
),
)
}
val actual = factory.createDefaultCoinsForMultiCurrencyCard(scanResponse = multiWallet.scanResponse)
val expected = listOf(
bitcoin,
ethereum,
cryptoCurrencyFactory.createCoin(blockchain = Blockchain.Dogecoin),
cryptoCurrencyFactory.createCoin(blockchain = Blockchain.Solana),
)
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `test createPrimaryCurrencyForSingleCurrencyCard if unable to create token`() = runTest {
val singleWallet = UserWallet(
name = "Note",
walletId = UserWalletId("011"),
cardsInWallet = setOf(),
isMultiCurrency = false,
scanResponse = MockScanResponseFactory.create(
cardConfig = GenericCardConfig(maxWalletCount = 2),
derivedKeys = emptyMap(),
),
hasBackupError = false,
)
val actual = runCatching {
factory.createPrimaryCurrencyForSingleCurrencyCard(scanResponse = singleWallet.scanResponse)
}
val exception = IllegalArgumentException("Coin for the single currency card cannot be null")
Truth.assertThat(actual.isFailure).isTrue()
Truth.assertThat(actual.exceptionOrNull()).isInstanceOf(exception::class.java)
Truth.assertThat(actual.exceptionOrNull()).hasMessageThat().isEqualTo(exception.message)
}
@Test
fun `test createPrimaryCurrencyForSingleCurrencyCard if primaryToken is null`() = runTest {
val singleWallet = createSingleWallet()
val actual = factory.createPrimaryCurrencyForSingleCurrencyCard(scanResponse = singleWallet.scanResponse)
val expected = ethereum
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `test createPrimaryCurrencyForSingleCurrencyCard if primaryToken is not null`() = runTest {
val singleWallet = createSingleWalletWithToken()
val actual = factory.createPrimaryCurrencyForSingleCurrencyCard(scanResponse = singleWallet.scanResponse)
val expected = CryptoCurrencyFactory(excludedBlockchains = ExcludedBlockchains()).createToken(
sdkToken = singleWallet.scanResponse.cardTypesResolver.getPrimaryToken()!!,
blockchain = Blockchain.Ethereum,
extraDerivationPath = null,
scanResponse = singleWallet.scanResponse,
)
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `test createCurrenciesForSingleCurrencyCardWithToken if unable to create token`() = runTest {
val singleWalletWithToken = UserWallet(
name = "Note",
walletId = UserWalletId("011"),
cardsInWallet = setOf(),
isMultiCurrency = false,
scanResponse = MockScanResponseFactory.create(
cardConfig = GenericCardConfig(maxWalletCount = 2),
derivedKeys = emptyMap(),
// // DEMO card
CreateDefaultForMultiWalletModel(
multiWallet = createMultiWallet(cardId = "AC01000000041225"),
expected = listOf(
bitcoin,
ethereum,
cryptoCurrencyFactory.createCoin(blockchain = Blockchain.Dogecoin),
cryptoCurrencyFactory.createCoin(blockchain = Blockchain.Solana),
),
),
hasBackupError = false,
)
val actual = runCatching {
factory.createCurrenciesForSingleCurrencyCardWithToken(scanResponse = singleWalletWithToken.scanResponse)
}
val exception = IllegalArgumentException("Coin for the single currency card cannot be null")
Truth.assertThat(actual.isFailure).isTrue()
Truth.assertThat(actual.exceptionOrNull()).isInstanceOf(exception::class.java)
Truth.assertThat(actual.exceptionOrNull()).hasMessageThat().isEqualTo(exception.message)
}
@Test
fun `test createCurrenciesForSingleCurrencyCardWithToken if primaryToken is null`() = runTest {
val singleWalletWithToken = createSingleWallet()
data class CreateDefaultForMultiWalletModel(val multiWallet: UserWallet, val expected: List<CryptoCurrency>)
val actual = factory.createCurrenciesForSingleCurrencyCardWithToken(
scanResponse = singleWalletWithToken.scanResponse,
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class CreatePrimaryCurrencyForSingleCurrencyCard {
@ParameterizedTest
@ProvideTestModels
fun `create primary currency for single currency card`(model: CreatePrimaryCurrencyForSingleWalletModel) =
runTest {
// Arrange
val singleWallet = model.singleWallet
// Act
val actual = runCatching {
factory.createPrimaryCurrencyForSingleCurrencyCard(scanResponse = singleWallet.scanResponse)
}
// Assert
actual
.onSuccess {
Truth.assertThat(actual).isEqualTo(model.expected)
}
.onFailure {
val exception = model.expected.exceptionOrNull()!!
Truth.assertThat(actual.exceptionOrNull()).isInstanceOf(exception::class.java)
Truth.assertThat(actual.exceptionOrNull()).hasMessageThat().isEqualTo(exception.message)
}
}
private fun provideTestModels() = listOf(
CreatePrimaryCurrencyForSingleWalletModel(
singleWallet = createSingleWallet(),
expected = Result.success(ethereum),
),
CreatePrimaryCurrencyForSingleWalletModel(
singleWallet = createSingleWallet(batchId = ""),
expected = Result.failure(IllegalArgumentException("Coin for the single currency card cannot be null")),
),
CreatePrimaryCurrencyForSingleWalletModel(
singleWallet = createSingleWallet(addWalletData = true),
expected = Result.failure(IllegalArgumentException("It isn't single-currency wallet")),
),
)
val expected = listOf(ethereum)
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `test createCurrenciesForSingleCurrencyCardWithToken if primaryToken is not null`() = runTest {
val singleWalletWithToken = createSingleWalletWithToken()
data class CreatePrimaryCurrencyForSingleWalletModel(
val singleWallet: UserWallet,
val expected: Result<CryptoCurrency>,
)
val actual = factory.createCurrenciesForSingleCurrencyCardWithToken(
scanResponse = singleWalletWithToken.scanResponse,
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class CreateCurrenciesForSingleCurrencyCardWithToken {
@ParameterizedTest
@ProvideTestModels
fun `create currencies for single currency card with token`(model: CreateForSingleWalletWithTokenModel) =
runTest {
// Arrange
val userWallet = model.singleWalletWithToken
// Act
val actual = runCatching {
factory.createCurrenciesForSingleCurrencyCardWithToken(scanResponse = userWallet.scanResponse)
}
// Assert
actual
.onSuccess {
val primaryToken = if (model.isPrimaryTokenExpected) {
createPrimaryToken(blockchain = Blockchain.Ethereum)
} else {
null
}
val expected = listOfNotNull(*model.expected.getOrNull()!!.toTypedArray(), primaryToken)
Truth.assertThat(it).isEqualTo(expected)
}
.onFailure {
val exception = model.expected.exceptionOrNull()!!
Truth.assertThat(it).isInstanceOf(exception::class.java)
Truth.assertThat(it).hasMessageThat().isEqualTo(exception.message)
}
}
private fun provideTestModels() = listOf(
CreateForSingleWalletWithTokenModel(
singleWalletWithToken = UserWallet(
name = "NODL",
walletId = UserWalletId("011"),
cardsInWallet = setOf(),
isMultiCurrency = false,
scanResponse = MockScanResponseFactory.create(
cardConfig = GenericCardConfig(maxWalletCount = 2),
derivedKeys = emptyMap(),
).copy(
productType = ProductType.Note,
walletData = WalletData(
blockchain = "",
token = WalletData.Token(
name = "Ethereum",
symbol = "ETH",
contractAddress = "0x",
decimals = 8,
),
),
),
hasBackupError = false,
),
expected = Result.failure(IllegalArgumentException("Coin for the single currency card cannot be null")),
),
CreateForSingleWalletWithTokenModel(
singleWalletWithToken = createSingleWalletWithToken(),
isPrimaryTokenExpected = true,
expected = Result.success(listOf(ethereum)),
),
)
val token = CryptoCurrencyFactory(excludedBlockchains = ExcludedBlockchains()).createToken(
sdkToken = singleWalletWithToken.scanResponse.cardTypesResolver.getPrimaryToken()!!,
blockchain = Blockchain.Ethereum,
extraDerivationPath = null,
scanResponse = singleWalletWithToken.scanResponse,
)
val expected = listOf(ethereum, token)
Truth.assertThat(actual).isEqualTo(expected)
}
private fun createMultiWallet(): UserWallet {
data class CreateForSingleWalletWithTokenModel(
val singleWalletWithToken: UserWallet,
val isPrimaryTokenExpected: Boolean = false,
val expected: Result<List<CryptoCurrency>>,
)
private fun createMultiWallet(cardId: String? = null, batchId: String? = null): UserWallet {
return UserWallet(
name = "Wallet 1",
walletId = UserWalletId("011"),
@ -363,12 +471,19 @@ internal class DefaultCardCryptoCurrencyFactoryTest {
scanResponse = MockScanResponseFactory.create(
cardConfig = GenericCardConfig(maxWalletCount = 2),
derivedKeys = emptyMap(),
),
).let {
it.copy(
card = it.card.copy(
cardId = cardId ?: it.card.cardId,
batchId = batchId ?: it.card.batchId,
),
)
},
hasBackupError = false,
)
}
private fun createSingleWallet(): UserWallet {
private fun createSingleWallet(batchId: String? = null, addWalletData: Boolean = false): UserWallet {
return UserWallet(
name = "Note",
walletId = UserWalletId("011"),
@ -379,8 +494,21 @@ internal class DefaultCardCryptoCurrencyFactoryTest {
derivedKeys = emptyMap(),
).let {
it.copy(
card = it.card.copy(batchId = "AB10"),
card = it.card.copy(batchId = batchId ?: "AB10"),
productType = ProductType.Note,
walletData = if (addWalletData) {
WalletData(
blockchain = "ETH",
token = WalletData.Token(
name = "Ethereum",
symbol = "ETH",
contractAddress = "0x",
decimals = 8,
),
)
} else {
it.walletData
},
)
},
hasBackupError = false,
@ -412,14 +540,27 @@ internal class DefaultCardCryptoCurrencyFactoryTest {
)
}
private fun createPrimaryToken(blockchain: Blockchain): CryptoCurrency.Token {
val userWallet = createSingleWalletWithToken()
return CryptoCurrencyFactory(excludedBlockchains = ExcludedBlockchains()).createToken(
sdkToken = userWallet.scanResponse.cardTypesResolver.getPrimaryToken()!!,
blockchain = blockchain,
extraDerivationPath = null,
scanResponse = userWallet.scanResponse,
)!!
}
private fun createUserTokensResponse(currencies: List<CryptoCurrency> = emptyList()): UserTokensResponse {
return userTokensResponseFactory.createUserTokensResponse(
currencies = currencies,
isGroupedByNetwork = false,
isSortedByBalance = false,
)
}
private companion object {
val cryptoCurrencyFactory = MockCryptoCurrencyFactory()
val ethereum = cryptoCurrencyFactory.ethereum.setCanHandleTokens(value = true)
val bitcoin = cryptoCurrencyFactory.createCoin(blockchain = Blockchain.Bitcoin)
fun CryptoCurrency.setCanHandleTokens(value: Boolean): CryptoCurrency {
return when (this) {
is CryptoCurrency.Coin -> copy(network = network.copy(canHandleTokens = value))

View file

@ -0,0 +1,69 @@
package com.tangem.data.networks.fetcher
import arrow.core.Either
import com.tangem.data.networks.store.NetworksStatusesStoreV2
import com.tangem.data.networks.store.setSourceAsOnlyCache
import com.tangem.data.networks.store.storeStatus
import com.tangem.data.networks.utils.NetworkStatusFactory
import com.tangem.domain.core.utils.catchOn
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext
import timber.log.Timber
import javax.inject.Inject
/**
* Common implementation of network status fetcher
*
* @property walletManagersFacade wallet managers facade
* @property networksStatusesStore networks statuses store
* @property dispatchers dispatchers
*
[REDACTED_AUTHOR]
*/
internal class CommonNetworkStatusFetcher @Inject constructor(
private val walletManagersFacade: WalletManagersFacade,
private val networksStatusesStore: NetworksStatusesStoreV2,
private val dispatchers: CoroutineDispatcherProvider,
) {
/**
* Fetch
*
* @param userWalletId user wallet id
* @param network network
* @param networkCurrencies network currencies
*/
suspend fun fetch(
userWalletId: UserWalletId,
network: Network,
networkCurrencies: Set<CryptoCurrency>,
): Either<Throwable, Unit> {
return Either.catchOn(dispatchers.default) {
val result = withContext(dispatchers.io) {
walletManagersFacade.update(
userWalletId = userWalletId,
network = network,
extraTokens = networkCurrencies
.filterIsInstance<CryptoCurrency.Token>()
.toSet(),
)
}
val status = NetworkStatusFactory.create(
network = network,
updatingResult = result,
addedCurrencies = networkCurrencies,
)
networksStatusesStore.storeStatus(userWalletId = userWalletId, status = status)
}
.onLeft {
Timber.e("Failed to fetch network status for $userWalletId [${network.id.value}]: $it")
networksStatusesStore.setSourceAsOnlyCache(userWalletId = userWalletId, network = network)
}
}
}

View file

@ -0,0 +1,161 @@
package com.tangem.data.networks.fetcher
import arrow.core.Either
import com.google.common.truth.Truth
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.test.domain.network.MockNetworkStatusFactory
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
import com.tangem.common.test.domain.walletmanager.MockUpdateWalletManagerResultFactory
import com.tangem.common.test.utils.ProvideTestModels
import com.tangem.data.networks.store.NetworksStatusesStoreV2
import com.tangem.data.networks.store.setSourceAsOnlyCache
import com.tangem.data.networks.store.storeStatus
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyAmountStatus
import com.tangem.domain.tokens.model.NetworkAddress
import com.tangem.domain.tokens.model.NetworkStatus
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
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 org.junit.jupiter.params.ParameterizedTest
/**
[REDACTED_AUTHOR]
*/
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
internal class CommonNetworkStatusFetcherTest {
private val walletManagersFacade: WalletManagersFacade = mockk()
private val networksStatusesStore: NetworksStatusesStoreV2 = mockk(relaxUnitFun = true)
private val fetcher = CommonNetworkStatusFetcher(
walletManagersFacade = walletManagersFacade,
networksStatusesStore = networksStatusesStore,
dispatchers = TestingCoroutineDispatcherProvider(),
)
private val cryptoCurrencyFactory = MockCryptoCurrencyFactory()
@BeforeEach
fun resetMocks() {
clearMocks(walletManagersFacade, networksStatusesStore)
}
@Test
fun `fetch failure if walletManagersFacade throws exception`() = runTest {
// Arrange
val userWalletId = UserWalletId("011")
val network = cryptoCurrencyFactory.ethereum.network
val extraTokens = setOf(
cryptoCurrencyFactory.createToken(Blockchain.Ethereum) as CryptoCurrency.Token,
)
val updateException = IllegalStateException()
coEvery { walletManagersFacade.update(userWalletId, network, extraTokens) } throws updateException
// Act
val actual = fetcher.fetch(userWalletId = userWalletId, network = network, networkCurrencies = extraTokens)
// Assert
val expected = Either.Left(updateException)
Truth.assertThat(actual.isLeft()).isTrue()
Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected.leftOrNull()!!::class.java)
Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.leftOrNull()!!.message)
coVerifyOrder {
walletManagersFacade.update(userWalletId, network, extraTokens)
}
}
@ParameterizedTest
@ProvideTestModels
fun `fetch successfully for any result of walletManagersFacade`(model: SuccessTestModel) = runTest {
// Arrange
val userWalletId = UserWalletId("011")
val network = cryptoCurrencyFactory.ethereum.network
val extraTokens = setOf(
cryptoCurrencyFactory.createToken(Blockchain.Ethereum) as CryptoCurrency.Token,
)
val updateResult = model.updateResult
val status = model.status
coEvery { walletManagersFacade.update(userWalletId, network, extraTokens) } returns updateResult
coEvery { networksStatusesStore.storeStatus(userWalletId = userWalletId, status = status) } returns Unit
// Act
val actual = fetcher.fetch(userWalletId = userWalletId, network = network, networkCurrencies = extraTokens)
// Assert
val expected = Either.Right(Unit)
Truth.assertThat(actual).isEqualTo(expected)
coVerifyOrder {
walletManagersFacade.update(userWalletId, network, extraTokens)
networksStatusesStore.storeStatus(userWalletId = userWalletId, status = status)
}
coVerify(inverse = true) {
networksStatusesStore.setSourceAsOnlyCache(userWalletId = any(), network = any())
}
}
private fun provideTestModels() = listOf(
SuccessTestModel(
updateResult = UpdateWalletManagerResult.MissedDerivation,
status = MockNetworkStatusFactory.createMissedDerivation(),
),
SuccessTestModel(
updateResult = MockUpdateWalletManagerResultFactory().createUnreachable(),
status = NetworkStatus(
network = cryptoCurrencyFactory.ethereum.network,
value = NetworkStatus.Unreachable(address = null),
),
),
SuccessTestModel(
updateResult = MockUpdateWalletManagerResultFactory().createUnreachableWithAddress(),
status = NetworkStatus(
network = cryptoCurrencyFactory.ethereum.network,
value = NetworkStatus.Unreachable(
address = NetworkAddress.Single(
defaultAddress = NetworkAddress.Address(
value = "0x1",
type = NetworkAddress.Address.Type.Primary,
),
),
),
),
),
SuccessTestModel(
updateResult = MockUpdateWalletManagerResultFactory().createNoAccount(),
status = MockNetworkStatusFactory.createNoAccount(),
),
SuccessTestModel(
updateResult = MockUpdateWalletManagerResultFactory().createVerified(),
status = MockNetworkStatusFactory.createVerified(cryptoCurrencyFactory.ethereum.network) {
it.copy(
amounts = mapOf(
CryptoCurrency.ID.fromValue(
value = "token⟨ETH⟩NEVER-MIND⚓NEVER-MIND",
) to CryptoCurrencyAmountStatus.NotFound,
),
pendingTransactions = mapOf(
CryptoCurrency.ID.fromValue(value = "token⟨ETH⟩NEVER-MIND⚓NEVER-MIND") to emptySet(),
),
)
},
),
)
data class SuccessTestModel(
val updateResult: UpdateWalletManagerResult,
val status: NetworkStatus,
)
}