Updated on 2026-08-14

This commit is contained in:
Tangem 2025-03-11 18:55:37 +03:00
parent 330ae73357
commit e7efe7b76a
4636 changed files with 234864 additions and 63507 deletions

1
data/common/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,35 @@
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
alias(deps.plugins.kotlin.kapt)
id("configuration")
}
android {
namespace = "com.tangem.data.common"
}
dependencies {
/* Core */
implementation(projects.core.datasource)
/* Domain */
implementation(projects.domain.models)
implementation(projects.domain.legacy)
implementation(projects.domain.tokens.models)
/* Libs - SDK */
implementation(tangemDeps.blockchain)
implementation(tangemDeps.card.core)
implementation(projects.libs.blockchainSdk)
/* DI */
implementation(deps.hilt.android)
kapt(deps.hilt.kapt)
/* Libs - Other */
implementation(deps.kotlin.coroutines)
implementation(deps.jodatime)
implementation(deps.timber)
implementation(deps.arrow.core)
}

View file

@ -0,0 +1,73 @@
package com.tangem.data.common.api
import arrow.core.raise.Raise
import arrow.core.raise.recover
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.common.response.ApiResponseError
import kotlinx.coroutines.withTimeoutOrNull
import timber.log.Timber
import kotlin.time.Duration
/**
* A wrapper around the [Raise] interface specific for [ApiResponseError]. It provides utility functions to
* operate on [ApiResponse] instances.
*
* @property raise A [Raise] instance for raising [ApiResponseError].
*/
@JvmInline
value class ApiResponseRaise(
private val raise: Raise<ApiResponseError>,
) : Raise<ApiResponseError> by raise {
/**
* Binds the given [ApiResponse] to its underlying value or raises an error.
*
* @return The underlying data of the response if it's successful.
*/
fun <T : Any> ApiResponse<T>.bind(): T = when (this) {
is ApiResponse.Success -> data
is ApiResponse.Error -> raise.raise(cause)
}
}
/**
* Attempts to execute an API call safely, providing error handling and a timeout.
*
* @param T The return type of the API call and the function.
* @param timeoutMillis The timeout in milliseconds for the API call. Default is 30 seconds.
* @param call The API call block to execute.
* @param onError A function to handle errors and return a fallback value of type [T].
*
* @return The result of the API call or the fallback value provided by [onError] if an error occurs.
*/
suspend inline fun <T> safeApiCallWithTimeout(
timeoutMillis: Duration = with(Duration) { 30.seconds },
crossinline call: suspend ApiResponseRaise.() -> T,
crossinline onError: suspend (ApiResponseError) -> T,
): T = safeApiCall(
call = {
withTimeoutOrNull(timeoutMillis) { call() }
?: raise(ApiResponseError.TimeoutException)
},
onError = onError,
)
/**
* Attempts to execute an API call safely, providing error handling.
*
* @param T The return type of the API call and the function.
* @param call The API call block to execute.
* @param onError A function to handle errors and return a fallback value of type [T].
*
* @return The result of the API call or the fallback value provided by [onError] if an error occurs.
*/
suspend inline fun <T> safeApiCall(
crossinline call: suspend ApiResponseRaise.() -> T,
crossinline onError: suspend (ApiResponseError) -> T,
): T = recover(
block = { call(ApiResponseRaise(raise = this)) },
recover = {
Timber.e(it, "Unable to perform safe API call")
onError(it)
},
)

View file

@ -0,0 +1,61 @@
package com.tangem.data.common.cache
import org.joda.time.Duration
/**
* Represents a registry for managing cache.
*/
interface CacheRegistry {
/**
* Checks whether the cache key is expired.
*
* @param key cache key.
* @return `true` if the cache key is expired, `false` otherwise.
*/
suspend fun isExpired(key: String): Boolean
/**
* Invalidates the cache key in registry.
*
* If the key doesn't exist, or it's already invalidated, this method doesn't have any effect.
*
* @param key cache key.
*/
suspend fun invalidate(key: String)
/**
* Invalidates cache keys in registry.
*
* If the key doesn't exist, or it's already invalidated, this method doesn't have any effect.
*
* @param keys cache keys.
*/
suspend fun invalidate(keys: Collection<String>)
/**
* Invalidates all cache keys in the registry.
*
* After the call, the registry doesn't contain any valid keys.
*/
suspend fun invalidateAll()
/**
* Defines a callback to be invoked when the cache key expires.
*
* @param key cache key.
* @param skipCache if `true`, the callback will be invoked regardless of whether the key has expired or not.
* @param expireIn the duration after which the cache key is considered expired.
* @param block the block of code to be executed when the cache key expires.
*/
suspend fun invokeOnExpire(
key: String,
skipCache: Boolean,
expireIn: Duration = Duration.standardMinutes(DEFAULT_CACHE_KEY_EXPIRE_IN_MINUTES),
block: suspend () -> Unit,
)
private companion object {
const val DEFAULT_CACHE_KEY_EXPIRE_IN_MINUTES = 5L
}
}

View file

@ -0,0 +1,82 @@
package com.tangem.data.common.cache
import com.tangem.datasource.local.cache.CacheKeysStore
import com.tangem.datasource.local.cache.model.CacheKey
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import org.joda.time.Duration
import org.joda.time.LocalDateTime
import timber.log.Timber
import java.util.concurrent.ConcurrentHashMap
internal class DefaultCacheRegistry(
private val cacheKeysStore: CacheKeysStore,
) : CacheRegistry {
private val mutex = Mutex()
private val mutexes = ConcurrentHashMap<String, Mutex>()
override suspend fun isExpired(key: String): Boolean {
val cacheKey = cacheKeysStore.getSyncOrNull(key) ?: return true
return cacheKey.updatedAt
.plus(cacheKey.expiresIn)
.isBefore(LocalDateTime.now())
}
override suspend fun invalidate(key: String) {
Timber.d("Invalidate the cache key: $key")
withContext(NonCancellable) { cacheKeysStore.remove(key) }
}
override suspend fun invalidate(keys: Collection<String>) {
Timber.d("Invalidate cache keys: $keys")
withContext(NonCancellable) { cacheKeysStore.remove(keys) }
}
override suspend fun invalidateAll() {
Timber.d("Invalidate all cache keys")
withContext(NonCancellable) { cacheKeysStore.clear() }
}
override suspend fun invokeOnExpire(
key: String,
skipCache: Boolean,
expireIn: Duration,
block: suspend () -> Unit,
) {
// use a separate mutexForKey for each key to avoid multiple calls block() to the same key
// also used mutex to safe create mutexForKey, otherwise it can lead to multiple calls for the same key
val mutexForKey = mutex.withLock {
mutexes.getOrPut(key) { Mutex() }
}
mutexForKey.withLock {
val isExpired = isExpired(key) || skipCache
if (!isExpired) {
return
}
try {
Timber.d("Invoke the action associated with the cache key: $key")
cacheKeysStore.store(
key = CacheKey(
id = key,
updatedAt = LocalDateTime.now(),
expiresIn = expireIn,
),
)
block()
} catch (e: Throwable) {
Timber.e(e, "The action related to the cache key has failed: $key")
invalidate(key)
throw e
}
}
}
}

View file

@ -0,0 +1,21 @@
package com.tangem.data.common.cache.di
import com.tangem.data.common.cache.CacheRegistry
import com.tangem.data.common.cache.DefaultCacheRegistry
import com.tangem.datasource.local.cache.CacheKeysStore
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 CacheRegistryModule {
@Provides
@Singleton
fun provideCacheRegistry(cacheKeysStore: CacheKeysStore): CacheRegistry {
return DefaultCacheRegistry(cacheKeysStore)
}
}

View file

@ -0,0 +1,151 @@
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.blockchainsdk.utils.toCoinId
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
import timber.log.Timber
import com.tangem.blockchain.common.Token as SdkToken
// FIXME: Make internal
class CryptoCurrencyFactory(
private val excludedBlockchains: ExcludedBlockchains,
) {
@Suppress("LongParameterList") // Yep, it's long
fun createToken(
network: Network,
rawId: CryptoCurrency.RawID?,
name: String,
symbol: String,
decimals: Int,
contractAddress: String,
): CryptoCurrency.Token {
val id = getTokenId(network, rawId, contractAddress)
return CryptoCurrency.Token(
id = id,
network = network,
name = name,
symbol = symbol,
decimals = decimals,
iconUrl = rawId?.let(::getTokenIconUrlFromDefaultHost),
isCustom = isCustomToken(id, network),
contractAddress = contractAddress,
)
}
fun createToken(
sdkToken: SdkToken,
blockchain: Blockchain,
extraDerivationPath: String?,
scanResponse: ScanResponse,
): CryptoCurrency.Token? {
if (blockchain == Blockchain.Unknown) {
Timber.e("Unable to map the SDK token to the domain token with Unknown blockchain")
return null
}
val network = getNetwork(blockchain, extraDerivationPath, scanResponse, excludedBlockchains) ?: return null
val id = getTokenId(network, sdkToken)
return CryptoCurrency.Token(
id = id,
network = network,
name = sdkToken.name,
symbol = sdkToken.symbol,
iconUrl = getTokenIconUrl(blockchain, sdkToken),
decimals = sdkToken.decimals,
isCustom = isCustomToken(id, network),
contractAddress = sdkToken.contractAddress,
)
}
fun createCoin(
blockchain: Blockchain,
extraDerivationPath: String?,
scanResponse: ScanResponse,
): CryptoCurrency.Coin? {
if (blockchain == Blockchain.Unknown) {
Timber.e("Unable to map the SDK token to the domain token with Unknown blockchain")
return null
}
val network = getNetwork(blockchain, extraDerivationPath, scanResponse, excludedBlockchains) ?: return null
return createCoin(network)
}
fun createCoin(networkId: String, extraDerivationPath: String?, scanResponse: ScanResponse): CryptoCurrency.Coin? {
val blockchain = Blockchain.fromNetworkId(networkId) ?: Blockchain.Unknown
return createCoin(blockchain, extraDerivationPath, scanResponse)
}
fun createCoin(network: Network): CryptoCurrency.Coin {
val blockchain = Blockchain.fromId(network.id.value)
return CryptoCurrency.Coin(
id = getCoinId(network, blockchain.toCoinId()),
network = network,
name = blockchain.getCoinName(),
symbol = blockchain.currency,
iconUrl = getCoinIconUrl(blockchain),
decimals = blockchain.decimals(),
isCustom = isCustomCoin(network),
)
}
fun createToken(
token: Token,
networkId: String,
extraDerivationPath: String?,
scanResponse: ScanResponse,
): CryptoCurrency.Token? {
val sdkToken = SdkToken(
name = token.name,
symbol = token.symbol,
contractAddress = token.contractAddress,
decimals = token.decimals,
id = token.id,
)
val blockchain = Blockchain.fromNetworkId(networkId) ?: Blockchain.Unknown
return createToken(
sdkToken = sdkToken,
blockchain = blockchain,
extraDerivationPath = extraDerivationPath,
scanResponse = scanResponse,
)
}
fun createToken(cryptoCurrency: CryptoCurrency.Token, network: Network): CryptoCurrency.Token {
val sdkToken = SdkToken(
name = cryptoCurrency.name,
symbol = cryptoCurrency.symbol,
contractAddress = cryptoCurrency.contractAddress,
decimals = cryptoCurrency.decimals,
id = cryptoCurrency.id.rawCurrencyId?.value,
)
val blockchain = Blockchain.fromNetworkId(cryptoCurrency.network.backendId) ?: Blockchain.Unknown
val id = getTokenId(network, sdkToken)
return CryptoCurrency.Token(
id = id,
network = network,
name = sdkToken.name,
symbol = sdkToken.symbol,
iconUrl = getTokenIconUrl(blockchain, sdkToken),
decimals = sdkToken.decimals,
isCustom = isCustomToken(id, network),
contractAddress = sdkToken.contractAddress,
)
}
data class Token(
val name: String,
val symbol: String,
val contractAddress: String,
val decimals: Int,
val id: String? = null,
)
}

View file

@ -0,0 +1,305 @@
package com.tangem.data.common.currency
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.FeePaidCurrency
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.blockchainsdk.utils.toNetworkId
import com.tangem.domain.common.DerivationStyleProvider
import com.tangem.domain.common.extensions.canHandleToken
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.model.Network
import timber.log.Timber
fun getBlockchain(networkId: Network.ID): Blockchain {
return Blockchain.fromId(networkId.value)
}
fun getNetwork(
blockchain: Blockchain,
extraDerivationPath: String?,
derivationStyleProvider: DerivationStyleProvider?,
excludedBlockchains: ExcludedBlockchains,
canHandleTokens: Boolean,
): Network? {
if (!isBlockchainSupported(blockchain, excludedBlockchains)) {
return null
}
return Network(
id = Network.ID(blockchain.id),
backendId = blockchain.toNetworkId(),
name = blockchain.fullName,
isTestnet = blockchain.isTestnet(),
derivationPath = getNetworkDerivationPath(
blockchain = blockchain,
extraDerivationPath = extraDerivationPath,
cardDerivationStyleProvider = derivationStyleProvider,
),
currencySymbol = blockchain.currency,
standardType = getNetworkStandardType(blockchain),
hasFiatFeeRate = blockchain.feePaidCurrency() !is FeePaidCurrency.FeeResource,
canHandleTokens = canHandleTokens,
transactionExtrasType = blockchain.getSupportedTransactionExtras(),
)
}
fun getNetwork(
networkId: Network.ID,
derivationPath: Network.DerivationPath,
scanResponse: ScanResponse,
excludedBlockchains: ExcludedBlockchains,
): Network? {
val blockchain = getBlockchain(networkId)
if (!isBlockchainSupported(blockchain, excludedBlockchains)) {
return null
}
return Network(
id = networkId,
backendId = blockchain.toNetworkId(),
name = blockchain.fullName,
isTestnet = blockchain.isTestnet(),
derivationPath = derivationPath,
currencySymbol = blockchain.currency,
standardType = getNetworkStandardType(blockchain),
hasFiatFeeRate = blockchain.feePaidCurrency() !is FeePaidCurrency.FeeResource,
canHandleTokens = scanResponse.card.canHandleToken(
blockchain,
scanResponse.cardTypesResolver,
excludedBlockchains,
),
transactionExtrasType = blockchain.getSupportedTransactionExtras(),
)
}
private fun isBlockchainSupported(blockchain: Blockchain, excludedBlockchains: ExcludedBlockchains): Boolean {
if (blockchain == Blockchain.Unknown) {
Timber.w("Unable to convert Unknown blockchain to the domain network model")
return false
}
if (blockchain in excludedBlockchains) {
Timber.w("Unable to convert excluded blockchain to the domain network model")
return false
}
return true
}
fun getNetwork(
blockchain: Blockchain,
extraDerivationPath: String?,
scanResponse: ScanResponse,
excludedBlockchains: ExcludedBlockchains,
): Network? {
return getNetwork(
blockchain = blockchain,
extraDerivationPath = extraDerivationPath,
derivationStyleProvider = scanResponse.derivationStyleProvider,
excludedBlockchains = excludedBlockchains,
canHandleTokens = scanResponse.card.canHandleToken(
blockchain,
scanResponse.cardTypesResolver,
excludedBlockchains,
),
)
}
fun getNetworkDerivationPath(
blockchain: Blockchain,
extraDerivationPath: String?,
cardDerivationStyleProvider: DerivationStyleProvider?,
): Network.DerivationPath {
if (cardDerivationStyleProvider == null) {
return Network.DerivationPath.None
}
val defaultDerivationPath = getDefaultDerivationPath(blockchain, cardDerivationStyleProvider)
return if (extraDerivationPath.isNullOrBlank()) {
if (defaultDerivationPath.isNullOrBlank()) {
Network.DerivationPath.None
} else {
Network.DerivationPath.Card(defaultDerivationPath)
}
} else {
if (extraDerivationPath == defaultDerivationPath) {
Network.DerivationPath.Card(defaultDerivationPath)
} else {
Network.DerivationPath.Custom(extraDerivationPath)
}
}
}
fun getNetworkStandardType(blockchain: Blockchain): Network.StandardType {
return when (blockchain) {
Blockchain.Ethereum, Blockchain.EthereumTestnet -> Network.StandardType.ERC20
Blockchain.BSC, Blockchain.BSCTestnet -> Network.StandardType.BEP20
Blockchain.Binance, Blockchain.BinanceTestnet -> Network.StandardType.BEP2
Blockchain.Tron, Blockchain.TronTestnet -> Network.StandardType.TRC20
else -> Network.StandardType.Unspecified(blockchain.name)
}
}
private fun getDefaultDerivationPath(
blockchain: Blockchain,
derivationStyleProvider: DerivationStyleProvider,
): String? {
return blockchain.derivationPath(derivationStyleProvider.getDerivationStyle())?.rawPath
}
@Suppress("LongMethod")
private fun Blockchain.getSupportedTransactionExtras(): Network.TransactionExtrasType {
return when (this) {
Blockchain.XRP -> Network.TransactionExtrasType.DESTINATION_TAG
Blockchain.Binance,
Blockchain.TON,
Blockchain.Cosmos,
Blockchain.TerraV1,
Blockchain.TerraV2,
Blockchain.Stellar,
Blockchain.Hedera,
Blockchain.Algorand,
Blockchain.Sei,
Blockchain.InternetComputer,
Blockchain.Casper,
-> Network.TransactionExtrasType.MEMO
// region Other blockchains
Blockchain.Unknown,
Blockchain.Alephium,
Blockchain.AlephiumTestnet,
Blockchain.Arbitrum,
Blockchain.ArbitrumTestnet,
Blockchain.Avalanche,
Blockchain.AvalancheTestnet,
Blockchain.BinanceTestnet,
Blockchain.BSC,
Blockchain.BSCTestnet,
Blockchain.Bitcoin,
Blockchain.BitcoinTestnet,
Blockchain.BitcoinCash,
Blockchain.BitcoinCashTestnet,
Blockchain.Cardano,
Blockchain.CosmosTestnet,
Blockchain.Dogecoin,
Blockchain.Ducatus,
Blockchain.Ethereum,
Blockchain.EthereumTestnet,
Blockchain.EthereumClassic,
Blockchain.EthereumClassicTestnet,
Blockchain.Fantom,
Blockchain.FantomTestnet,
Blockchain.Litecoin,
Blockchain.Near,
Blockchain.NearTestnet,
Blockchain.Polkadot,
Blockchain.PolkadotTestnet,
Blockchain.Kava,
Blockchain.KavaTestnet,
Blockchain.Kusama,
Blockchain.Polygon,
Blockchain.PolygonTestnet,
Blockchain.RSK,
Blockchain.SeiTestnet,
Blockchain.StellarTestnet,
Blockchain.Solana,
Blockchain.SolanaTestnet,
Blockchain.Tezos,
Blockchain.Tron,
Blockchain.TronTestnet,
Blockchain.Gnosis,
Blockchain.Dash,
Blockchain.Optimism,
Blockchain.OptimismTestnet,
Blockchain.Dischain,
Blockchain.EthereumPow,
Blockchain.EthereumPowTestnet,
Blockchain.Kaspa,
Blockchain.KaspaTestnet,
Blockchain.Telos,
Blockchain.TelosTestnet,
Blockchain.TONTestnet,
Blockchain.Ravencoin,
Blockchain.Clore,
Blockchain.RavencoinTestnet,
Blockchain.Cronos,
Blockchain.AlephZero,
Blockchain.AlephZeroTestnet,
Blockchain.OctaSpace,
Blockchain.OctaSpaceTestnet,
Blockchain.Chia,
Blockchain.ChiaTestnet,
Blockchain.Decimal,
Blockchain.DecimalTestnet,
Blockchain.XDC,
Blockchain.XDCTestnet,
Blockchain.VeChain,
Blockchain.VeChainTestnet,
Blockchain.Aptos,
Blockchain.AptosTestnet,
Blockchain.Playa3ull,
Blockchain.Shibarium,
Blockchain.ShibariumTestnet,
Blockchain.AlgorandTestnet,
Blockchain.HederaTestnet,
Blockchain.Aurora,
Blockchain.AuroraTestnet,
Blockchain.Areon,
Blockchain.AreonTestnet,
Blockchain.PulseChain,
Blockchain.PulseChainTestnet,
Blockchain.ZkSyncEra,
Blockchain.ZkSyncEraTestnet,
Blockchain.Nexa,
Blockchain.NexaTestnet,
Blockchain.Moonbeam,
Blockchain.MoonbeamTestnet,
Blockchain.Manta,
Blockchain.MantaTestnet,
Blockchain.PolygonZkEVM,
Blockchain.PolygonZkEVMTestnet,
Blockchain.Radiant,
Blockchain.Fact0rn,
Blockchain.Base,
Blockchain.BaseTestnet,
Blockchain.Moonriver,
Blockchain.MoonriverTestnet,
Blockchain.Mantle,
Blockchain.MantleTestnet,
Blockchain.Flare,
Blockchain.FlareTestnet,
Blockchain.Taraxa,
Blockchain.TaraxaTestnet,
Blockchain.Koinos,
Blockchain.KoinosTestnet,
Blockchain.Joystream,
Blockchain.Bittensor,
Blockchain.Filecoin,
Blockchain.Blast,
Blockchain.BlastTestnet,
Blockchain.Cyber,
Blockchain.CyberTestnet,
Blockchain.Sui,
Blockchain.SuiTestnet,
Blockchain.EnergyWebChain,
Blockchain.EnergyWebChainTestnet,
Blockchain.EnergyWebX,
Blockchain.EnergyWebXTestnet,
Blockchain.CasperTestnet,
Blockchain.Core,
Blockchain.CoreTestnet,
Blockchain.Xodex,
Blockchain.Canxium,
Blockchain.Chiliz,
Blockchain.ChilizTestnet,
Blockchain.VanarChain,
Blockchain.VanarChainTestnet,
Blockchain.OdysseyChain, Blockchain.OdysseyChainTestnet,
Blockchain.Bitrock, Blockchain.BitrockTestnet,
Blockchain.Sonic, Blockchain.SonicTestnet,
Blockchain.ApeChain, Blockchain.ApeChainTestnet,
-> Network.TransactionExtrasType.NONE
// endregion
}
}

View file

@ -0,0 +1,126 @@
package com.tangem.data.common.currency
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.blockchainsdk.utils.toCoinId
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.model.CryptoCurrency
import timber.log.Timber
import com.tangem.blockchain.common.Token as SdkToken
class ResponseCryptoCurrenciesFactory(
private val excludedBlockchains: ExcludedBlockchains,
) {
fun createCurrency(currencyId: String, response: UserTokensResponse, scanResponse: ScanResponse): CryptoCurrency {
return response.tokens
.asSequence()
.mapNotNull { createCurrency(it, scanResponse) }
.first { it.id.value == currencyId }
}
fun createCurrencies(response: UserTokensResponse, scanResponse: ScanResponse): List<CryptoCurrency> {
return response.tokens
.asSequence()
.mapNotNull { createCurrency(it, scanResponse) }
.distinctBy { it.id }
.toList()
}
fun createCurrency(responseToken: UserTokensResponse.Token, scanResponse: ScanResponse): CryptoCurrency? {
var blockchain = Blockchain.fromNetworkId(responseToken.networkId)
if (blockchain == null || blockchain == Blockchain.Unknown) {
Timber.e("Unable to find a blockchain with the network ID: ${responseToken.networkId}")
return null
}
if (scanResponse.cardTypesResolver.isTestCard()) {
blockchain = blockchain.getTestnetVersion() ?: blockchain
}
val sdkToken = createSdkToken(responseToken)
return if (sdkToken == null) {
createCoin(blockchain, responseToken, scanResponse)
} else {
createToken(blockchain, sdkToken, responseToken.derivationPath, scanResponse)
}
}
private fun createSdkToken(token: UserTokensResponse.Token): SdkToken? {
return token.contractAddress?.let { contractAddress ->
SdkToken(
name = token.name,
symbol = token.symbol,
contractAddress = contractAddress,
decimals = token.decimals,
id = token.id,
)
}
}
private fun createCoin(
blockchain: Blockchain,
responseToken: UserTokensResponse.Token,
scanResponse: ScanResponse,
): CryptoCurrency.Coin? {
val network = getNetwork(
blockchain,
responseToken.derivationPath,
scanResponse,
excludedBlockchains,
) ?: return null
return CryptoCurrency.Coin(
id = getCoinId(network, blockchain.toCoinId()),
network = network,
name = blockchain.getCoinName(),
symbol = blockchain.getSymbolForCoin(responseToken),
decimals = responseToken.decimals,
iconUrl = getCoinIconUrl(blockchain),
isCustom = isCustomCoin(network),
)
}
private fun Blockchain.getSymbolForCoin(responseToken: UserTokensResponse.Token): String {
return when (this) {
// workaround: Dischain was renamed but backend still returns the old name,
// get name and symbol from enum Blockchain until backend renamed
// [REDACTED_JIRA]
Blockchain.Dischain,
Blockchain.Polygon,
-> this.currency
else -> responseToken.symbol
}
}
private fun createToken(
blockchain: Blockchain,
sdkToken: Token,
responseDerivationPath: String?,
scanResponse: ScanResponse,
): CryptoCurrency.Token? {
val network = getNetwork(
blockchain,
responseDerivationPath,
scanResponse,
excludedBlockchains,
) ?: return null
val id = getTokenId(network, sdkToken)
return CryptoCurrency.Token(
id = id,
network = network,
name = sdkToken.name,
symbol = sdkToken.symbol,
decimals = sdkToken.decimals,
iconUrl = getTokenIconUrl(blockchain, sdkToken),
contractAddress = sdkToken.contractAddress,
isCustom = isCustomToken(id, network),
)
}
}

View file

@ -0,0 +1,100 @@
package com.tangem.data.common.currency
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.IconsUtil
import com.tangem.blockchainsdk.utils.toCoinId
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrency.ID
import com.tangem.domain.tokens.model.Network
import com.tangem.blockchain.common.Token as SdkToken
import com.tangem.domain.tokens.model.CryptoCurrency.ID.Body as CurrencyIdBody
import com.tangem.domain.tokens.model.CryptoCurrency.ID.Prefix.COIN_PREFIX as COIN_ID_PREFIX
import com.tangem.domain.tokens.model.CryptoCurrency.ID.Prefix.TOKEN_PREFIX as TOKEN_ID_PREFIX
import com.tangem.domain.tokens.model.CryptoCurrency.ID.Suffix.ContractAddress as CustomCurrencyIdSuffix
import com.tangem.domain.tokens.model.CryptoCurrency.ID.Suffix.RawID as CurrencyIdSuffix
private const val DEFAULT_TOKENS_ICONS_HOST = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins"
private const val TOKEN_ICON_SIZE = "large"
private const val TOKEN_ICON_EXT = "png"
fun isCustomToken(tokenId: ID, network: Network): Boolean {
return network.derivationPath is Network.DerivationPath.Custom || tokenId.rawCurrencyId == null
}
fun isCustomCoin(network: Network): Boolean {
return network.derivationPath is Network.DerivationPath.Custom
}
fun getCoinId(network: Network, coinId: String): ID {
return ID(COIN_ID_PREFIX, getCurrencyIdBody(network), CurrencyIdSuffix(rawId = coinId))
}
fun getTokenId(network: Network, sdkToken: SdkToken): ID {
val sdkTokenId = sdkToken.id
val tokenId = sdkTokenId?.let { CryptoCurrency.RawID(it) }
return getTokenId(network, tokenId, sdkToken.contractAddress)
}
fun getTokenId(network: Network, rawTokenId: CryptoCurrency.RawID?, contractAddress: String): ID {
val suffix = if (rawTokenId == null) {
CustomCurrencyIdSuffix(contractAddress)
} else {
CurrencyIdSuffix(rawTokenId.value, contractAddress)
}
return ID(TOKEN_ID_PREFIX, getCurrencyIdBody(network), suffix)
}
fun getTokenIconUrl(blockchain: Blockchain, token: SdkToken): String? {
val tokenId = token.id
val rawID = tokenId?.let { CryptoCurrency.RawID(it) }
return if (rawID == null) {
IconsUtil.getTokenIconUri(blockchain, token)?.toString()
} else {
getTokenIconUrlFromDefaultHost(rawID)
}
}
fun getCoinIconUrl(blockchain: Blockchain): String? {
val coinId = when (blockchain) {
Blockchain.Unknown -> null
else -> blockchain.toCoinId()
}?.let { CryptoCurrency.RawID(it) }
return coinId?.let(::getTokenIconUrlFromDefaultHost)
}
fun List<UserTokensResponse.Token>.hasCoinForToken(network: Network): Boolean {
return any {
val blockchain = getBlockchain(networkId = network.id)
val tokenDerivation = network.derivationPath.value
it.id == blockchain.toCoinId() && it.derivationPath == tokenDerivation
}
}
private fun getCurrencyIdBody(network: Network): CurrencyIdBody {
return when (val path = network.derivationPath) {
is Network.DerivationPath.Custom -> CurrencyIdBody.NetworkIdWithDerivationPath(
rawId = network.id.value,
derivationPath = path.value,
)
is Network.DerivationPath.Card,
is Network.DerivationPath.None,
-> CurrencyIdBody.NetworkId(network.id.value)
}
}
fun getTokenIconUrlFromDefaultHost(tokenId: CryptoCurrency.RawID): String {
return buildString {
append(DEFAULT_TOKENS_ICONS_HOST)
append('/')
append(TOKEN_ICON_SIZE)
append('/')
append(tokenId.value)
append('.')
append(TOKEN_ICON_EXT)
}
}

View file

@ -0,0 +1,42 @@
package com.tangem.data.common.currency
import com.tangem.blockchainsdk.utils.toNetworkId
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.domain.tokens.model.CryptoCurrency
class UserTokensResponseFactory {
fun createUserTokensResponse(
currencies: List<CryptoCurrency>,
isGroupedByNetwork: Boolean,
isSortedByBalance: Boolean,
): UserTokensResponse {
return UserTokensResponse(
tokens = currencies.map(::createResponseToken),
group = if (isGroupedByNetwork) {
UserTokensResponse.GroupType.NETWORK
} else {
UserTokensResponse.GroupType.NONE
},
sort = if (isSortedByBalance) {
UserTokensResponse.SortType.BALANCE
} else {
UserTokensResponse.SortType.MANUAL
},
)
}
fun createResponseToken(currency: CryptoCurrency): UserTokensResponse.Token {
val blockchain = getBlockchain(currency.network.id)
return UserTokensResponse.Token(
id = currency.id.rawCurrencyId?.value,
networkId = blockchain.toNetworkId(),
derivationPath = currency.network.derivationPath.value,
name = currency.name,
symbol = currency.symbol,
decimals = currency.decimals,
contractAddress = (currency as? CryptoCurrency.Token)?.contractAddress,
)
}
}

View file

@ -0,0 +1,28 @@
package com.tangem.data.common.locale
import java.util.Locale
/**
[REDACTED_AUTHOR]
*/
internal class DefaultLocaleProvider : LocaleProvider {
override fun getLocale(): Locale {
return Locale.getDefault()
}
override fun getWebUriLocaleLanguage(): String {
val language = getLocale().language
return if (LOCALE_LANG_RU.equals(language, true) || LOCALE_LANG_BY.equals(language, true)) {
LOCALE_LANG_RU
} else {
LOCALE_LANG_EN
}
}
companion object {
const val LOCALE_LANG_RU = "ru"
const val LOCALE_LANG_BY = "by"
const val LOCALE_LANG_EN = "en"
}
}

View file

@ -0,0 +1,13 @@
package com.tangem.data.common.locale
import java.util.Locale
/**
[REDACTED_AUTHOR]
*/
interface LocaleProvider {
fun getLocale(): Locale
fun getWebUriLocaleLanguage(): String
}

View file

@ -0,0 +1,20 @@
package com.tangem.data.common.locale.di
import com.tangem.data.common.locale.DefaultLocaleProvider
import com.tangem.data.common.locale.LocaleProvider
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 LocaleProviderModule {
@Provides
@Singleton
fun provideCacheRegistry(): LocaleProvider {
return DefaultLocaleProvider()
}
}

View file

@ -0,0 +1,35 @@
package com.tangem.data.common.utils
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.delay
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.yield
import timber.log.Timber
import kotlin.coroutines.cancellation.CancellationException
@Suppress("UnconditionalJumpStatementInLoop", "MagicNumber")
suspend fun <T> retryOnError(priority: Boolean = false, startRetryDelay: Int = 500, call: suspend () -> T): T {
var currentDelay = startRetryDelay
var priorityCounter = 5
while (true) {
return try {
call()
} catch (e: Exception) {
if (e is CancellationException) {
currentCoroutineContext().ensureActive()
}
Timber.e(e, "Error occurred during retryOnError block")
if (priority && priorityCounter > 0) {
--priorityCounter
} else {
yield()
delay(timeMillis = currentDelay.toLong())
currentDelay *= 2
}
continue
}
}
}