Updated on 2026-08-14

This commit is contained in:
Tangem 2025-05-07 15:50:08 +04:00
parent 9cda183a5b
commit 3171058cb6
16 changed files with 796 additions and 222 deletions

View file

@ -0,0 +1,46 @@
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.UserWalletId
/**
* Factory for creating list of [CryptoCurrency] for selected card
*
[REDACTED_AUTHOR]
*/
interface CardCryptoCurrencyFactory {
/**
* Universal method for creating list of [CryptoCurrency] in [network] for any card
*
* @param userWalletId user wallet id that determines type of card
* @param network network
*/
@Throws
suspend fun create(userWalletId: UserWalletId, network: Network): List<CryptoCurrency>
/**
* Create default coins for multi currency card
*
* @param scanResponse scan response
*/
fun createDefaultCoinsForMultiCurrencyCard(scanResponse: ScanResponse): List<CryptoCurrency.Coin>
/**
* Create primary currency for single currency card
*
* @param scanResponse scan response
*/
@Throws
fun createPrimaryCurrencyForSingleCurrencyCard(scanResponse: ScanResponse): CryptoCurrency
/**
* Create currencies for single currency card with token (like, NODL)
*
* @param scanResponse scan response
*/
@Throws
fun createCurrenciesForSingleCurrencyCardWithToken(scanResponse: ScanResponse): List<CryptoCurrency>
}

View file

@ -0,0 +1,128 @@
package com.tangem.data.common.currency
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.datasource.local.token.UserTokensResponseStore
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.common.TapWorkarounds.isTestCard
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.demo.DemoConfig
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
/**
* Default implementation of factory for creating list of [CryptoCurrency] for selected card
*
* @property demoConfig demo config
* @property excludedBlockchains excluded blockchains
* @property userWalletsStore user wallets store
* @property userTokensResponseStore user tokens response store
*/
internal class DefaultCardCryptoCurrencyFactory(
private val demoConfig: DemoConfig,
private val excludedBlockchains: ExcludedBlockchains,
private val userWalletsStore: UserWalletsStore,
private val userTokensResponseStore: UserTokensResponseStore,
) : CardCryptoCurrencyFactory {
private val cryptoCurrencyFactory by lazy { CryptoCurrencyFactory(excludedBlockchains) }
override suspend fun create(userWalletId: UserWalletId, network: Network): List<CryptoCurrency> {
val userWallet = userWalletsStore.getSyncStrict(key = userWalletId)
val blockchain = Blockchain.fromNetworkId(networkId = network.backendId)
// multi-currency wallet
if (userWallet.isMultiCurrency) return getMultiWalletCurrencies(userWallet = userWallet, network = network)
// check if the blockchain of single-currency wallet is the same as network
val cardBlockchain = userWallet.scanResponse.cardTypesResolver.getBlockchain()
if (cardBlockchain != blockchain) return emptyList()
// single-currency wallet with token (NODL)
if (userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken()) {
return createCurrenciesForSingleCurrencyCardWithToken(userWallet.scanResponse)
}
// single-currency wallet
return createPrimaryCurrencyForSingleCurrencyCard(userWallet.scanResponse).let(::listOf)
}
override fun createDefaultCoinsForMultiCurrencyCard(scanResponse: ScanResponse): List<CryptoCurrency.Coin> {
val card = scanResponse.card
var blockchains = if (demoConfig.isDemoCardId(card.cardId)) {
demoConfig.demoBlockchains
} else {
listOf(Blockchain.Bitcoin, Blockchain.Ethereum)
}
if (card.isTestCard) {
blockchains = blockchains.mapNotNull { it.getTestnetVersion() }
}
return blockchains.mapNotNull {
cryptoCurrencyFactory.createCoin(
blockchain = it,
extraDerivationPath = null,
scanResponse = scanResponse,
)
}
}
override fun createPrimaryCurrencyForSingleCurrencyCard(scanResponse: ScanResponse): CryptoCurrency {
return with(getSingleWalletCurrencies(scanResponse)) {
primaryToken ?: coin
}
}
override fun createCurrenciesForSingleCurrencyCardWithToken(scanResponse: ScanResponse): List<CryptoCurrency> {
return with(getSingleWalletCurrencies(scanResponse)) {
listOfNotNull(coin, primaryToken)
}
}
private suspend fun getMultiWalletCurrencies(userWallet: UserWallet, network: Network): List<CryptoCurrency> {
val response = userTokensResponseStore.getSyncOrNull(userWalletId = userWallet.walletId)
?: return emptyList()
val responseCurrenciesFactory = ResponseCryptoCurrenciesFactory(excludedBlockchains)
return responseCurrenciesFactory.createCurrencies(
tokens = response.tokens.filter {
it.networkId == network.backendId && it.derivationPath == network.derivationPath.value
},
scanResponse = userWallet.scanResponse,
)
}
private fun getSingleWalletCurrencies(scanResponse: ScanResponse): SingleWalletCurrencies {
val resolver = scanResponse.cardTypesResolver
val blockchain = resolver.getBlockchain()
val coin = cryptoCurrencyFactory.createCoin(
blockchain = blockchain,
extraDerivationPath = null,
scanResponse = scanResponse,
)
requireNotNull(coin) { "Coin for the single currency card cannot be null" }
val primaryToken = resolver.getPrimaryToken()?.let { token ->
cryptoCurrencyFactory.createToken(
sdkToken = token,
blockchain = blockchain,
extraDerivationPath = null,
scanResponse = scanResponse,
)
}
return SingleWalletCurrencies(coin = coin, primaryToken = primaryToken)
}
private data class SingleWalletCurrencies(val coin: CryptoCurrency, val primaryToken: CryptoCurrency?)
}

View file

@ -0,0 +1,33 @@
package com.tangem.data.common.di
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.data.common.currency.CardCryptoCurrencyFactory
import com.tangem.data.common.currency.DefaultCardCryptoCurrencyFactory
import com.tangem.datasource.local.token.UserTokensResponseStore
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.demo.DemoConfig
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object DataCommonModule {
@Provides
@Singleton
fun provideCardCryptoCurrencyFactory(
excludedBlockchains: ExcludedBlockchains,
userWalletsStore: UserWalletsStore,
userTokensResponseStore: UserTokensResponseStore,
): CardCryptoCurrencyFactory {
return DefaultCardCryptoCurrencyFactory(
demoConfig = DemoConfig(),
excludedBlockchains = excludedBlockchains,
userWalletsStore = userWalletsStore,
userTokensResponseStore = userTokensResponseStore,
)
}
}

View file

@ -0,0 +1,430 @@
package com.tangem.data.common.currency
import android.net.Uri
import com.google.common.truth.Truth
import com.tangem.blockchain.common.Blockchain
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.datasource.local.token.UserTokensResponseStore
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.common.configs.GenericCardConfig
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.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
/**
[REDACTED_AUTHOR]
*/
internal class DefaultCardCryptoCurrencyFactoryTest {
private val userWalletsStore: UserWalletsStore = mockk()
private val userTokensResponseStore: UserTokensResponseStore = mockk()
private val factory = DefaultCardCryptoCurrencyFactory(
demoConfig = DemoConfig(),
excludedBlockchains = ExcludedBlockchains(),
userWalletsStore = userWalletsStore,
userTokensResponseStore = userTokensResponseStore,
)
@Before
fun setup() {
mockkStatic(Uri::class)
every { Uri.parse(any()) } returns mockk()
}
@Test
fun `test create if userTokensResponse is not empty`() = runTest {
val multiWallet = createMultiWallet()
val userTokensResponse = UserTokensResponseFactory().createUserTokensResponse(
currencies = listOf(ethereum),
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 = 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"),
),
)
}
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"),
),
)
}
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(),
),
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()
val actual = factory.createCurrenciesForSingleCurrencyCardWithToken(
scanResponse = singleWalletWithToken.scanResponse,
)
val expected = listOf(ethereum)
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `test createCurrenciesForSingleCurrencyCardWithToken if primaryToken is not null`() = runTest {
val singleWalletWithToken = createSingleWalletWithToken()
val actual = factory.createCurrenciesForSingleCurrencyCardWithToken(
scanResponse = singleWalletWithToken.scanResponse,
)
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 {
return UserWallet(
name = "Wallet 1",
walletId = UserWalletId("011"),
cardsInWallet = setOf(),
isMultiCurrency = true,
scanResponse = MockScanResponseFactory.create(
cardConfig = GenericCardConfig(maxWalletCount = 2),
derivedKeys = emptyMap(),
),
hasBackupError = false,
)
}
private fun createSingleWallet(): UserWallet {
return UserWallet(
name = "Note",
walletId = UserWalletId("011"),
cardsInWallet = setOf(),
isMultiCurrency = false,
scanResponse = MockScanResponseFactory.create(
cardConfig = GenericCardConfig(maxWalletCount = 2),
derivedKeys = emptyMap(),
).let {
it.copy(
card = it.card.copy(batchId = "AB10"),
productType = ProductType.Note,
)
},
hasBackupError = false,
)
}
private fun createSingleWalletWithToken(): UserWallet {
return 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 = "ETH",
token = WalletData.Token(
name = "Ethereum",
symbol = "ETH",
contractAddress = "0x",
decimals = 8,
),
),
),
hasBackupError = 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))
is CryptoCurrency.Token -> copy(network = network.copy(canHandleTokens = value))
}
}
}
}