Updated on 2026-08-14

This commit is contained in:
Tangem 2023-07-27 11:44:04 +03:00
parent ce57171f3b
commit ec07825276
22 changed files with 273 additions and 362 deletions

View file

@ -25,7 +25,6 @@ dependencies {
/** Project - Utils */
implementation(projects.core.utils)
// FIXME: For blockchain extensions, remove after refactoring
implementation(projects.domain.legacy)
/** Tangem SDKs */

View file

@ -1,8 +1,8 @@
package com.tangem.data.tokens.di
import com.tangem.data.common.cache.CacheRegistry
import com.tangem.data.tokens.repository.DefaultNetworksRepository
import com.tangem.data.tokens.repository.DefaultTokensRepository
import com.tangem.data.tokens.repository.MockNetworksRepository
import com.tangem.data.tokens.repository.MockQuotesRepository
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.local.token.UserTokensStore
@ -10,6 +10,7 @@ import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.tokens.repository.QuotesRepository
import com.tangem.domain.tokens.repository.TokensRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
@ -39,5 +40,19 @@ internal object TokensDataModule {
@Provides
@Singleton
fun provideNetworksRepository(): NetworksRepository = MockNetworksRepository()
fun provideNetworksRepository(
walletManagersFacade: WalletManagersFacade,
userWalletsStore: UserWalletsStore,
userTokensStore: UserTokensStore,
cacheRegistry: CacheRegistry,
dispatchers: CoroutineDispatcherProvider,
): NetworksRepository {
return DefaultNetworksRepository(
walletManagersFacade,
userWalletsStore,
userTokensStore,
cacheRegistry,
dispatchers,
)
}
}

View file

@ -1,58 +0,0 @@
package com.tangem.data.tokens.mock
import com.tangem.domain.tokens.model.NetworkStatus
import com.tangem.domain.tokens.models.Network
import java.math.BigDecimal
@Suppress("MemberVisibilityCanBePrivate")
internal object MockNetworks {
val network1 = Network(
id = Network.ID("network1"),
name = "Network One",
)
val network2 = Network(
id = Network.ID("network2"),
name = "Network Two",
)
val network3 = Network(
id = Network.ID("network3"),
name = "Network Three",
)
val networks = setOf(network1, network2, network3)
val networkStatus1 = NetworkStatus(
networkId = network1.id,
value = NetworkStatus.Verified(
amounts = mapOf(
MockTokens.token1.id to BigDecimal("123.1234556789"),
MockTokens.token2.id to BigDecimal("42.2"),
MockTokens.token3.id to BigDecimal("1000000000.5"),
),
hasTransactionsInProgress = false,
),
)
val networkStatus2 = NetworkStatus(
networkId = network2.id,
value = NetworkStatus.MissedDerivation,
)
val networkStatus3 = NetworkStatus(
networkId = network3.id,
value = NetworkStatus.Verified(
amounts = mapOf(
MockTokens.token7.id to BigDecimal.ZERO,
MockTokens.token8.id to BigDecimal.TEN,
MockTokens.token9.id to BigDecimal.TEN,
MockTokens.token10.id to BigDecimal.TEN,
),
hasTransactionsInProgress = false,
),
)
val networksStatuses = setOf(networkStatus1, networkStatus2, networkStatus3)
}

View file

@ -1,70 +0,0 @@
package com.tangem.data.tokens.mock
import com.tangem.domain.tokens.model.Quote
import java.math.BigDecimal
@Suppress("MemberVisibilityCanBePrivate")
internal object MockQuotes {
val quote1 = Quote(
currencyId = MockTokens.token1.id,
fiatRate = BigDecimal("1.23"),
priceChange = BigDecimal("0.01"),
)
val quote2 = Quote(
currencyId = MockTokens.token2.id,
fiatRate = BigDecimal("2.34"),
priceChange = BigDecimal("-0.02"),
)
val quote3 = Quote(
currencyId = MockTokens.token3.id,
fiatRate = BigDecimal("3.45"),
priceChange = BigDecimal("0.03"),
)
val quote4 = Quote(
currencyId = MockTokens.token4.id,
fiatRate = BigDecimal("4.56"),
priceChange = BigDecimal("-0.04"),
)
val quote5 = Quote(
currencyId = MockTokens.token5.id,
fiatRate = BigDecimal("5.67"),
priceChange = BigDecimal("0.05"),
)
val quote6 = Quote(
currencyId = MockTokens.token6.id,
fiatRate = BigDecimal("6.78"),
priceChange = BigDecimal("-0.06"),
)
val quote7 = Quote(
currencyId = MockTokens.token7.id,
fiatRate = BigDecimal("7.89"),
priceChange = BigDecimal("0.07"),
)
val quote8 = Quote(
currencyId = MockTokens.token8.id,
fiatRate = BigDecimal("8.90"),
priceChange = BigDecimal("-0.08"),
)
val quote9 = Quote(
currencyId = MockTokens.token9.id,
fiatRate = BigDecimal("9.01"),
priceChange = BigDecimal("0.09"),
)
val quote10 = Quote(
currencyId = MockTokens.token10.id,
fiatRate = BigDecimal("10.12"),
priceChange = BigDecimal("-0.10"),
)
val quotes = setOf(quote1, quote2, quote3, quote4, quote5, quote6, quote7, quote8, quote9, quote10)
}

View file

@ -1,133 +0,0 @@
package com.tangem.data.tokens.mock
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.models.UserWalletId
internal object MockTokens {
val token1
get() = CryptoCurrency.Coin(
id = CryptoCurrency.ID("token1"),
networkId = MockNetworks.network1.id,
name = "Token 1",
symbol = "T1",
decimals = 8,
iconUrl = null,
derivationPath = null,
)
val token2
get() = CryptoCurrency.Token(
id = CryptoCurrency.ID("token2"),
networkId = MockNetworks.network1.id,
name = "Token 2",
symbol = "T2",
isCustom = false,
decimals = 8,
iconUrl = null,
contractAddress = "address",
derivationPath = null,
)
val token3
get() = CryptoCurrency.Token(
id = CryptoCurrency.ID("token3"),
networkId = MockNetworks.network1.id,
name = "Token 3",
symbol = "T3",
isCustom = false,
decimals = 8,
iconUrl = null,
contractAddress = "address",
derivationPath = null,
)
val token4
get() = CryptoCurrency.Coin(
id = CryptoCurrency.ID("token4"),
networkId = MockNetworks.network2.id,
name = "Token 4",
symbol = "T4",
decimals = 8,
iconUrl = null,
derivationPath = null,
)
val token5
get() = CryptoCurrency.Token(
id = CryptoCurrency.ID("token5"),
networkId = MockNetworks.network2.id,
name = "Token 5",
symbol = "T5",
isCustom = false,
decimals = 8,
iconUrl = null,
contractAddress = "address",
derivationPath = null,
)
val token6
get() = CryptoCurrency.Token(
id = CryptoCurrency.ID("token6"),
networkId = MockNetworks.network2.id,
name = "Token 6",
symbol = "T6",
isCustom = false,
decimals = 8,
iconUrl = null,
contractAddress = "address",
derivationPath = null,
)
val token7
get() = CryptoCurrency.Coin(
id = CryptoCurrency.ID("token7"),
networkId = MockNetworks.network3.id,
name = "Token 7",
symbol = "T7",
decimals = 8,
iconUrl = null,
derivationPath = null,
)
val token8
get() = CryptoCurrency.Token(
id = CryptoCurrency.ID("token8"),
networkId = MockNetworks.network3.id,
name = "Token 8",
symbol = "T8",
isCustom = false,
decimals = 8,
iconUrl = null,
contractAddress = "address",
derivationPath = null,
)
val token9
get() = CryptoCurrency.Token(
id = CryptoCurrency.ID("token9"),
networkId = MockNetworks.network3.id,
name = "Token 9",
symbol = "T9",
isCustom = false,
decimals = 8,
iconUrl = null,
contractAddress = "address",
derivationPath = null,
)
val token10
get() = CryptoCurrency.Token(
id = CryptoCurrency.ID("token10"),
networkId = MockNetworks.network3.id,
name = "Token 10",
symbol = "T10",
isCustom = false,
decimals = 8,
iconUrl = null,
contractAddress = "address",
derivationPath = null,
)
val tokens
get() = mapOf(
UserWalletId(stringValue = "123") to setOf(
token1, token2, token3, token4, token5,
token6, token7, token8, token9, token10,
),
UserWalletId(stringValue = "321") to setOf(token1, token2, token3),
UserWalletId(stringValue = "42") to setOf(token7, token8, token9, token10),
UserWalletId(stringValue = "24") to setOf(token4, token5, token6),
)
}

View file

@ -0,0 +1,106 @@
package com.tangem.data.tokens.repository
import com.tangem.data.common.cache.CacheRegistry
import com.tangem.data.tokens.utils.NetworkConverter
import com.tangem.data.tokens.utils.NetworkStatusFactory
import com.tangem.data.tokens.utils.ResponseCurrenciesFactory
import com.tangem.datasource.local.token.UserTokensStore
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.demo.DemoConfig
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.NetworkStatus
import com.tangem.domain.tokens.models.Network
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.extensions.addOrReplace
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
internal class DefaultNetworksRepository(
private val walletManagersFacade: WalletManagersFacade,
private val userWalletsStore: UserWalletsStore,
private val userTokensStore: UserTokensStore,
private val cacheRegistry: CacheRegistry,
private val dispatchers: CoroutineDispatcherProvider,
) : NetworksRepository {
private val networkConverter by lazy { NetworkConverter() }
private val responseCurrenciesFactory by lazy { ResponseCurrenciesFactory(DemoConfig()) }
private val networkStatusFactory by lazy { NetworkStatusFactory() }
private val networksStatuses: MutableStateFlow<HashSet<NetworkStatus>> = MutableStateFlow(hashSetOf())
override fun getNetworks(networksIds: Set<Network.ID>): Set<Network> {
return networkConverter.convertSet(networksIds)
}
override fun getNetworkStatuses(
userWalletId: UserWalletId,
networks: Set<Network.ID>,
refresh: Boolean,
): Flow<Set<NetworkStatus>> = channelFlow {
networksStatuses.collectLatest(::send)
launch(dispatchers.io) {
fetchNetworksStatusesIfCacheExpired(userWalletId, networks, refresh)
}
}
private suspend fun fetchNetworksStatusesIfCacheExpired(
userWalletId: UserWalletId,
networks: Set<Network.ID>,
refresh: Boolean,
) {
cacheRegistry.invokeOnExpire(
key = getNetworksStatusesCacheKey(userWalletId),
skipCache = refresh,
block = { fetchNetworksStatuses(userWalletId, networks) },
)
}
private suspend fun fetchNetworksStatuses(userWalletId: UserWalletId, networks: Set<Network.ID>) {
coroutineScope {
networks
.map { networkId ->
async {
fetchNetworkStatus(userWalletId, networkId)
}
}
.awaitAll()
}
}
private suspend fun fetchNetworkStatus(userWalletId: UserWalletId, networkId: Network.ID) {
val currencies = getCurrencies(userWalletId)
val result = walletManagersFacade.update(
userWalletId = userWalletId,
networkId = networkId,
extraTokens = currencies.filterIsInstanceTo(hashSetOf()),
)
val networkStatus = networkStatusFactory.createNetworkStatus(networkId, result, currencies)
networksStatuses.update { statuses ->
statuses.apply {
addOrReplace(networkStatus) { it.networkId == networkStatus.networkId }
}
}
}
private suspend fun getCurrencies(userWalletId: UserWalletId): Set<CryptoCurrency> {
val userWallet = requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) {
"Unable to find user wallet with provided ID: $userWalletId"
}
val response = requireNotNull(userTokensStore.getSyncOrNull(userWalletId)) {
"Unable to find tokens response for user wallet with provided ID: $userWalletId"
}
return responseCurrenciesFactory.createTokens(response, userWallet.scanResponse.card)
}
private fun getNetworksStatusesCacheKey(userWalletId: UserWalletId): String = "network_status_$userWalletId"
}

View file

@ -1,31 +0,0 @@
package com.tangem.data.tokens.repository
import com.tangem.data.tokens.mock.MockNetworks
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.NetworkStatus
import com.tangem.domain.tokens.models.Network
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
internal class MockNetworksRepository : NetworksRepository {
override fun getNetworks(networksIds: Set<Network.ID>): Set<Network> {
return MockNetworks.networks
.filter { it.id in networksIds }
.toSet()
}
override fun getNetworkStatuses(
userWalletId: UserWalletId,
networks: Map<Network.ID, Set<CryptoCurrency.ID>>,
refresh: Boolean,
): Flow<Set<NetworkStatus>> {
return flowOf(
MockNetworks.networksStatuses
.filter { it.networkId in networks.keys }
.toSet(),
)
}
}

View file

@ -1,19 +1,23 @@
package com.tangem.data.tokens.repository
import com.tangem.data.tokens.mock.MockQuotes
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Quote
import com.tangem.domain.tokens.repository.QuotesRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import java.math.BigDecimal
internal class MockQuotesRepository : QuotesRepository {
override fun getQuotes(tokensIds: Set<CryptoCurrency.ID>, refresh: Boolean): Flow<Set<Quote>> {
override fun getQuotes(currenciesIds: Set<CryptoCurrency.ID>, refresh: Boolean): Flow<Set<Quote>> {
return flowOf(
MockQuotes.quotes
.filter { it.currencyId in tokensIds }
.toSet(),
currenciesIds.map {
Quote(
currencyId = it,
fiatRate = BigDecimal.ZERO,
priceChange = BigDecimal.ZERO,
)
}.toSet(),
)
}
}

View file

@ -0,0 +1,31 @@
package com.tangem.data.tokens.utils
import com.tangem.blockchain.common.Blockchain
import com.tangem.domain.tokens.models.Network
import com.tangem.utils.converter.Converter
import timber.log.Timber
internal class NetworkConverter : Converter<Network.ID, Network?> {
override fun convert(value: Network.ID): Network? {
val blockchain = Blockchain.fromId(value.value)
if (blockchain == Blockchain.Unknown) {
Timber.e("Unable to convert Unknown blockchain to the domain network model")
return null
}
return Network(
id = value,
name = blockchain.fullName,
)
}
override fun convertList(input: Collection<Network.ID>): List<Network> {
return input.mapNotNull(::convert)
}
override fun convertSet(input: Collection<Network.ID>): Set<Network> {
return input.mapNotNullTo(hashSetOf(), ::convert)
}
}

View file

@ -0,0 +1,57 @@
package com.tangem.data.tokens.utils
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.NetworkStatus
import com.tangem.domain.tokens.models.Network
import com.tangem.domain.walletmanager.model.CryptoCurrencyAmount
import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult
import timber.log.Timber
import java.math.BigDecimal
internal class NetworkStatusFactory {
fun createNetworkStatus(
networkId: Network.ID,
result: UpdateWalletManagerResult,
currencies: Set<CryptoCurrency>,
): NetworkStatus {
return NetworkStatus(
networkId = networkId,
value = when (result) {
is UpdateWalletManagerResult.MissedDerivation -> NetworkStatus.MissedDerivation
is UpdateWalletManagerResult.Unreachable -> NetworkStatus.Unreachable
is UpdateWalletManagerResult.NoAccount -> NetworkStatus.NoAccount(result.amountToCreateAccount)
is UpdateWalletManagerResult.Verified -> NetworkStatus.Verified(
amounts = formatAmounts(result.tokensAmounts, currencies),
hasTransactionsInProgress = result.hasTransactionsInProgress,
)
},
)
}
private fun formatAmounts(
amounts: Set<CryptoCurrencyAmount>,
currencies: Set<CryptoCurrency>,
): Map<CryptoCurrency.ID, BigDecimal> {
val formattedAmounts = hashMapOf<CryptoCurrency.ID, BigDecimal>()
currencies.forEach { currency ->
val amount = when (currency) {
is CryptoCurrency.Coin -> amounts.singleOrNull { it is CryptoCurrencyAmount.Coin }
is CryptoCurrency.Token -> amounts.singleOrNull {
it is CryptoCurrencyAmount.Token &&
it.id == getTokenIdString(currency) &&
it.tokenContractAddress == currency.contractAddress
}
}?.value
if (amount == null) {
Timber.e("Unable to find a token amount for: ${currency.name}")
} else {
formattedAmounts[currency.id] = amount
}
}
return formattedAmounts
}
}

View file

@ -49,7 +49,7 @@ internal fun getTokenId(blockchain: Blockchain, token: SdkToken): CryptoCurrency
return getTokenOrCoinId(blockchain, token)
}
internal fun getResponseTokenId(currency: CryptoCurrency): String? {
internal fun getTokenIdString(currency: CryptoCurrency): String? {
return currency.id.value.substringAfter(TOKEN_ID_DELIMITER)
.takeUnless { currency is CryptoCurrency.Token && currency.isCustom }
}

View file

@ -30,7 +30,7 @@ internal class UserTokensResponseFactory {
val blockchain = getBlockchain(currency.networkId)
return UserTokensResponse.Token(
id = getResponseTokenId(currency),
id = getTokenIdString(currency),
networkId = blockchain.toNetworkId(),
derivationPath = currency.derivationPath,
name = currency.name,