Updated on 2026-08-14
This commit is contained in:
parent
21f3ea0d1a
commit
6171aa8097
11 changed files with 310 additions and 12 deletions
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
package com.tangem.datasource.local.datastore.model
|
||||
|
||||
internal typealias WriteTrigger = Unit
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
* */
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
*/
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue