From 6171aa8097ef25010ba92b6cd7776f9a7f60870e Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 25 Jul 2023 10:06:30 +0300 Subject: [PATCH] Updated on 2026-08-14 --- .../datasource/files/AndroidFileReader.kt | 11 ++- .../local/datastore/FileDataStore.kt | 19 ++-- .../local/datastore/model/WriteTrigger.kt | 3 + data/tokens/build.gradle.kts | 20 ++++- .../data/tokens/utils/CardTokensFactory.kt | 89 +++++++++++++++++++ .../tokens/utils/ResponseTokensFactory.kt | 58 ++++++++++++ .../data/tokens/utils/TokensOperations.kt | 75 ++++++++++++++++ .../tokens/utils/UserTokensResponseFactory.kt | 44 +++++++++ .../java/com/tangem/domain/demo/DemoConfig.kt | 1 + .../com/tangem/domain/models/scan/CardDTO.kt | 1 + .../tangem/domain/models/scan/ScanResponse.kt | 1 + 11 files changed, 310 insertions(+), 12 deletions(-) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/datastore/model/WriteTrigger.kt create mode 100644 data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CardTokensFactory.kt create mode 100644 data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseTokensFactory.kt create mode 100644 data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/TokensOperations.kt create mode 100644 data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/UserTokensResponseFactory.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/files/AndroidFileReader.kt b/core/datasource/src/main/java/com/tangem/datasource/files/AndroidFileReader.kt index 8ed8eb243b..01b45b3a15 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/files/AndroidFileReader.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/files/AndroidFileReader.kt @@ -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) } } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/FileDataStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/FileDataStore.kt index 2da02a5df8..aa89c11b91 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/FileDataStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/FileDataStore.kt @@ -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( private val fileNameProvider: (key: String) -> String, @@ -15,12 +14,14 @@ internal class FileDataStore( private val adapter: JsonAdapter, ) { - private val writeTrigger = MutableSharedFlow( + private val writeTrigger = MutableSharedFlow( + replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST, ) fun get(key: String): Flow { return writeTrigger + .onEmpty { emit(WriteTrigger) } .map { getInternal(fileNameProvider(key)) } .filterNotNull() } @@ -34,7 +35,7 @@ internal class FileDataStore( 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( 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) { diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/model/WriteTrigger.kt b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/model/WriteTrigger.kt new file mode 100644 index 0000000000..aaab7f83df --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/model/WriteTrigger.kt @@ -0,0 +1,3 @@ +package com.tangem.datasource.local.datastore.model + +internal typealias WriteTrigger = Unit \ No newline at end of file diff --git a/data/tokens/build.gradle.kts b/data/tokens/build.gradle.kts index 2d8aaaf35d..9fa03694fe 100644 --- a/data/tokens/build.gradle.kts +++ b/data/tokens/build.gradle.kts @@ -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) } \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CardTokensFactory.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CardTokensFactory.kt new file mode 100644 index 0000000000..671810e7e7 --- /dev/null +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CardTokensFactory.kt @@ -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 { + 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 { + 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), + ) + } +} \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseTokensFactory.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseTokensFactory.kt new file mode 100644 index 0000000000..462a115635 --- /dev/null +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseTokensFactory.kt @@ -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 { + 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, + ) + } + } +} \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/TokensOperations.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/TokensOperations.kt new file mode 100644 index 0000000000..72938103a5 --- /dev/null +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/TokensOperations.kt @@ -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" +} \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/UserTokensResponseFactory.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/UserTokensResponseFactory.kt new file mode 100644 index 0000000000..6c895bee65 --- /dev/null +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/UserTokensResponseFactory.kt @@ -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, + 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, + ) + } +} \ No newline at end of file diff --git a/domain/demo/src/main/java/com/tangem/domain/demo/DemoConfig.kt b/domain/demo/src/main/java/com/tangem/domain/demo/DemoConfig.kt index 6c22a84135..ad6e89bba0 100644 --- a/domain/demo/src/main/java/com/tangem/domain/demo/DemoConfig.kt +++ b/domain/demo/src/main/java/com/tangem/domain/demo/DemoConfig.kt @@ -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 { diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/scan/CardDTO.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/scan/CardDTO.kt index a89909b584..1777aa498a 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/scan/CardDTO.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/scan/CardDTO.kt @@ -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 * */ diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/scan/ScanResponse.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/scan/ScanResponse.kt index 352a0564f1..c6f1d9f75c 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/scan/ScanResponse.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/scan/ScanResponse.kt @@ -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] */