Updated on 2026-08-14

This commit is contained in:
Tangem 2023-07-25 10:06:30 +03:00
parent 21f3ea0d1a
commit 6171aa8097
11 changed files with 310 additions and 12 deletions

View file

@ -5,13 +5,18 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
class AndroidFileReader @Inject constructor(@ApplicationContext private val context: Context) : FileReader {
override fun readFile(fileName: String): String {
return context.openFileInput(fileName).bufferedReader().readText()
return context.openFileInput(fileName).use { stream ->
stream.bufferedReader().use { reader ->
reader.readText()
}
}
}
override fun rewriteFile(content: String, fileName: String) {
context.openFileOutput(fileName, Context.MODE_PRIVATE).use {
it.write(content.toByteArray(), 0, content.length)
context.openFileOutput(fileName, Context.MODE_PRIVATE).use { stream ->
stream.write(content.toByteArray(), 0, content.length)
}
}
}

View file

@ -2,12 +2,11 @@ package com.tangem.datasource.local.datastore
import com.squareup.moshi.JsonAdapter
import com.tangem.datasource.files.FileReader
import com.tangem.datasource.local.datastore.model.WriteTrigger
import com.tangem.domain.core.error.DataError
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.*
import java.io.FileNotFoundException
internal class FileDataStore<Data>(
private val fileNameProvider: (key: String) -> String,
@ -15,12 +14,14 @@ internal class FileDataStore<Data>(
private val adapter: JsonAdapter<Data>,
) {
private val writeTrigger = MutableSharedFlow<Unit>(
private val writeTrigger = MutableSharedFlow<WriteTrigger>(
replay = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST,
)
fun get(key: String): Flow<Data> {
return writeTrigger
.onEmpty { emit(WriteTrigger) }
.map { getInternal(fileNameProvider(key)) }
.filterNotNull()
}
@ -34,7 +35,7 @@ internal class FileDataStore<Data>(
val json = adapter.toJson(content)
fileReader.rewriteFile(json, fileNameProvider(key))
writeTrigger.tryEmit(Unit)
writeTrigger.tryEmit(WriteTrigger)
} catch (e: Throwable) {
throw DataError.PersistenceError.UnableToWriteFile(e)
}
@ -42,7 +43,11 @@ internal class FileDataStore<Data>(
private fun getInternal(fileName: String): Data? {
return try {
val json = fileReader.readFile(fileName)
val json = try {
fileReader.readFile(fileName)
} catch (e: FileNotFoundException) {
return null
}
adapter.fromJson(json)
} catch (e: Throwable) {

View file

@ -0,0 +1,3 @@
package com.tangem.datasource.local.datastore.model
internal typealias WriteTrigger = Unit

View file

@ -1,18 +1,34 @@
plugins {
alias(deps.plugins.kotlin.jvm)
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
alias(deps.plugins.kotlin.kapt)
id("configuration")
}
android {
namespace = "com.tangem.data.tokens"
}
dependencies {
implementation(projects.domain.core)
implementation(projects.domain.models)
implementation(projects.domain.tokens)
implementation(projects.domain.demo)
implementation(projects.domain.wallets.models)
implementation(deps.kotlin.coroutines)
implementation(projects.core.datasource)
// FIXME: For blockchain extensions, remove after refactoring
implementation(projects.domain.legacy)
implementation(deps.kotlin.coroutines)
implementation(deps.arrow.core)
implementation(deps.hilt.core)
kapt(deps.hilt.kapt)
implementation(deps.timber)
implementation(deps.tangem.blockchain)
implementation(deps.tangem.card.core)
}

View file

@ -0,0 +1,89 @@
package com.tangem.data.tokens.utils
import com.tangem.blockchain.common.Blockchain
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.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.model.Network
import timber.log.Timber
import com.tangem.blockchain.common.Token as SdkToken
import com.tangem.domain.tokens.model.Token as DomainToken
@Suppress("unused") // TODO: Will be used in next MR
internal class CardTokensFactory(private val demoConfig: DemoConfig) {
fun createDefaultTokensForMultiCurrencyCard(card: CardDTO): Set<DomainToken> {
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 { createCoin(it, card) }.toSet()
}
fun createTokensForSingleCurrencyCard(scanResponse: ScanResponse): Set<DomainToken> {
val card = scanResponse.card
val resolver = scanResponse.cardTypesResolver
val blockchain = resolver.getBlockchain()
val coin = requireNotNull(createCoin(blockchain, card)) {
"Coin for the single currency card cannot be null"
}
val primaryToken = resolver.getPrimaryToken()?.let { token ->
createToken(token, blockchain, card)
}
return buildSet {
add(coin)
if (primaryToken != null) {
add(primaryToken)
}
}
}
private fun createToken(sdkToken: SdkToken, blockchain: Blockchain, card: CardDTO): DomainToken? {
if (blockchain != Blockchain.Unknown) {
Timber.e("Unable to map the SDK token to the domain token with Unknown blockchain")
return null
}
return DomainToken(
id = getTokenId(sdkToken, blockchain),
networkId = Network.ID(blockchain.id),
name = sdkToken.name,
symbol = sdkToken.symbol,
iconUrl = getCoinOrTokenIconUrl(sdkToken, blockchain),
decimals = sdkToken.decimals,
isCustom = false,
contractAddress = sdkToken.contractAddress,
derivationPath = getDerivationPath(blockchain, card),
)
}
private fun createCoin(blockchain: Blockchain, card: CardDTO): DomainToken? {
if (blockchain != Blockchain.Unknown) {
Timber.e("Unable to map the SDK token to the domain token with Unknown blockchain")
return null
}
return DomainToken(
id = getCoinId(blockchain),
networkId = Network.ID(blockchain.id),
name = blockchain.fullName,
symbol = blockchain.currency,
iconUrl = getCoinIconId(blockchain),
decimals = blockchain.decimals(),
isCustom = false,
contractAddress = null,
derivationPath = getDerivationPath(blockchain, card),
)
}
}

View file

@ -0,0 +1,58 @@
package com.tangem.data.tokens.utils
import com.tangem.blockchain.common.Blockchain
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.domain.common.TapWorkarounds.derivationStyle
import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.domain.demo.DemoConfig
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.tokens.model.Token
import timber.log.Timber
import com.tangem.blockchain.common.Token as SdkToken
@Suppress("unused") // TODO: Will be used in next MR
internal class ResponseTokensFactory(private val demoConfig: DemoConfig) {
fun createTokens(response: UserTokensResponse, card: CardDTO): Set<Token> {
return response.tokens.mapNotNull { createToken(it, card) }.toSet()
}
private fun createToken(token: UserTokensResponse.Token, card: CardDTO): Token? {
var blockchain = Blockchain.fromNetworkId(token.networkId)
if (blockchain == null) {
Timber.e("Unable to find a blockchain with the network ID: ${token.networkId}")
return null
}
if (demoConfig.isDemoCardId(card.cardId)) {
blockchain = blockchain.getTestnetVersion() ?: blockchain
}
val sdkToken = createSdkToken(token)
return Token(
id = getTokenId(sdkToken, blockchain),
networkId = Network.ID(blockchain.id),
name = token.name,
symbol = token.symbol,
decimals = token.decimals,
iconUrl = getCoinOrTokenIconUrl(sdkToken, blockchain),
contractAddress = token.contractAddress,
derivationPath = token.derivationPath,
isCustom = isCustomToken(token.id, token.derivationPath, card.derivationStyle, blockchain),
)
}
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,
)
}
}
}

View file

@ -0,0 +1,75 @@
package com.tangem.data.tokens.utils
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.DerivationStyle
import com.tangem.blockchain.common.IconsUtil
import com.tangem.domain.common.TapWorkarounds.derivationStyle
import com.tangem.domain.common.extensions.toCoinId
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.tokens.model.Token
import com.tangem.blockchain.common.Token as SdkToken
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"
internal fun isCustomToken(
tokenId: String?,
tokenDerivationPath: String?,
cardDerivationStyle: DerivationStyle?,
blockchain: Blockchain,
): Boolean {
if (tokenId == null) return true
if (tokenDerivationPath == null || cardDerivationStyle == null) return false
return tokenDerivationPath != blockchain.derivationPath(cardDerivationStyle)?.rawPath
}
internal fun getTokenId(token: SdkToken?, blockchain: Blockchain): Token.ID {
val tokenId = token?.id
return when {
token == null -> getCoinId(blockchain)
tokenId == null -> getCustomTokenId(token.contractAddress, blockchain)
else -> Token.ID(tokenId)
}
}
internal fun getCoinId(blockchain: Blockchain): Token.ID {
val value = blockchain.toCoinId()
return Token.ID(value)
}
internal fun getCoinOrTokenIconUrl(token: SdkToken?, blockchain: Blockchain): String? {
val tokenId = token?.id
return when {
token == null -> getCoinIconId(blockchain)
tokenId == null -> IconsUtil.getTokenIconUri(blockchain, token)?.toString()
else -> getTokenIconUrlFromDefaultHost(tokenId)
}
}
internal fun getCoinIconId(blockchain: Blockchain): String {
return getTokenIconUrlFromDefaultHost(blockchain.toCoinId())
}
internal fun getDerivationPath(blockchain: Blockchain, card: CardDTO): String? {
return if (card.settings.isHDWalletAllowed) {
blockchain.derivationPath(card.derivationStyle)?.rawPath
} else {
null
}
}
private fun getCustomTokenId(contractAddress: String, blockchain: Blockchain): Token.ID {
val value = "custom_${blockchain.id}_$contractAddress"
return Token.ID(value)
}
private fun getTokenIconUrlFromDefaultHost(tokenId: String): String {
return "$DEFAULT_TOKENS_ICONS_HOST/$TOKEN_ICON_SIZE/$tokenId.$TOKEN_ICON_EXT"
}

View file

@ -0,0 +1,44 @@
package com.tangem.data.tokens.utils
import com.tangem.blockchain.common.Blockchain
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.domain.common.extensions.toNetworkId
import com.tangem.domain.tokens.model.Token
@Suppress("unused") // TODO: Will be used in next MR
internal class UserTokensResponseFactory {
fun createUserTokensResponse(
tokens: Set<Token>,
isGroupedByNetwork: Boolean,
isSortedByBalance: Boolean,
): UserTokensResponse {
return UserTokensResponse(
tokens = tokens.map(::createResponseToken),
group = if (isGroupedByNetwork) {
UserTokensResponse.GroupType.NETWORK
} else {
UserTokensResponse.GroupType.NONE
},
sort = if (isSortedByBalance) {
UserTokensResponse.SortType.BALANCE
} else {
UserTokensResponse.SortType.MANUAL
},
)
}
private fun createResponseToken(domainToken: Token): UserTokensResponse.Token {
val blockchain = Blockchain.fromId(domainToken.networkId.value)
return UserTokensResponse.Token(
id = domainToken.id.value.takeUnless { domainToken.isCustom },
networkId = blockchain.toNetworkId(),
derivationPath = domainToken.derivationPath,
name = domainToken.name,
symbol = domainToken.symbol,
decimals = domainToken.decimals,
contractAddress = domainToken.contractAddress,
)
}
}

View file

@ -5,6 +5,7 @@ import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.Blockchain
import java.math.BigDecimal
// FIXME: Move to :domain:demo:models
@Suppress("LargeClass")
class DemoConfig {

View file

@ -11,6 +11,7 @@ import com.tangem.operations.attestation.Attestation
import java.util.*
import com.tangem.common.card.FirmwareVersion as SdkFirmwareVersion
// TODO: Move to :domain:card:models
/**
* [Card] copy
* */

View file

@ -5,6 +5,7 @@ import com.tangem.common.extensions.ByteArrayKey
import com.tangem.operations.backup.PrimaryCard
import com.tangem.operations.derivation.ExtendedPublicKeysMap
// TODO: Move to :domain:card:models
/**
[REDACTED_AUTHOR]
*/