Updated on 2026-08-14

This commit is contained in:
Tangem 2024-10-01 19:42:21 +04:00
commit 36d499539b
8 changed files with 183 additions and 61 deletions

View file

@ -75,6 +75,7 @@ fun getActiveIconRes(blockchainId: String): Int {
"cyber", "cyber/test" -> R.drawable.img_cyber_22
"sei", "sei/test" -> R.drawable.img_sei_22
"internet-computer" -> R.drawable.img_icp_22
"sui", "sui/test" -> R.drawable.img_sui_22
else -> R.drawable.ic_alert_24
}
}
@ -151,6 +152,7 @@ fun getActiveIconResByNetworkId(networkId: String): Int {
"cyber", "cyber/test" -> R.drawable.img_cyber_22
"sei", "sei/test" -> R.drawable.img_sei_22
"internet-computer" -> R.drawable.img_icp_22
"sui", "sui/test" -> R.drawable.img_sui_22
else -> R.drawable.ic_alert_24
}
}
@ -224,6 +226,7 @@ fun getActiveIconResByCoinId(coinId: String): Int {
"cyber", "cyber/test" -> R.drawable.img_cyber_22
"sei", "sei/test" -> R.drawable.img_sei_22
"internet-computer" -> R.drawable.img_icp_22
"sui", "sui/test" -> R.drawable.img_sui_22
else -> R.drawable.ic_alert_24
}
}
@ -300,6 +303,7 @@ fun getGreyedOutIconRes(blockchainId: String): Int {
"cyber", "cyber/test" -> R.drawable.ic_cyber_22
"sei", "sei/test" -> R.drawable.ic_sei_22
"internet-computer" -> R.drawable.ic_icp_22
"sui", "sui/test" -> R.drawable.ic_sui_22
else -> R.drawable.ic_alert_24
}
}
@ -376,6 +380,7 @@ fun getGreyedOutIconResByNetworkId(networkId: String): Int {
"cyber", "cyber/test" -> R.drawable.ic_cyber_22
"sei", "sei/test" -> R.drawable.ic_sei_22
"internet-computer" -> R.drawable.ic_icp_22
"sui", "sui/test" -> R.drawable.ic_sui_22
else -> R.drawable.ic_alert_24
}
}

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="22dp"
android:height="22dp"
android:viewportWidth="22"
android:viewportHeight="22">
<path
android:pathData="M14.8,9.7C15.4,10.5 15.8,11.5 15.8,12.7C15.8,13.8 15.4,14.9 14.7,15.7L14.7,15.8L14.7,15.7C14.6,15.6 14.6,15.5 14.6,15.4C14.3,14 13.2,12.7 11.4,11.7C10.2,11 9.5,10.1 9.3,9.2C9.2,8.6 9.3,8 9.4,7.5C9.6,6.9 9.9,6.5 10.1,6.2L10.8,5.4C10.9,5.2 11.1,5.2 11.2,5.4L14.8,9.7H14.8ZM15.9,8.8L11.2,3.1C11.1,3 10.9,3 10.8,3.1L6.1,8.8L6.1,8.8C5.3,9.9 4.8,11.3 4.8,12.8C4.8,16.2 7.6,19 11,19C14.5,19 17.3,16.2 17.3,12.8C17.3,11.3 16.7,9.9 15.9,8.8L15.9,8.8L15.9,8.8ZM7.3,9.7L7.7,9.1L7.7,9.2C7.7,9.3 7.7,9.4 7.7,9.5C8,10.9 9,12.1 10.6,13C12,13.8 12.8,14.7 13.1,15.7C13.2,16.2 13.2,16.6 13.1,16.9L13.1,17L13.1,17C12.5,17.3 11.8,17.5 11,17.5C8.4,17.5 6.2,15.3 6.2,12.7C6.2,11.5 6.6,10.5 7.3,9.7L7.3,9.7Z"
android:fillColor="#000" />
</vector>

View file

@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="22dp"
android:height="22dp"
android:viewportWidth="22"
android:viewportHeight="22">
<clip-path
android:pathData="M11,0L11,0A11,11 0,0 1,22 11L22,11A11,11 0,0 1,11 22L11,22A11,11 0,0 1,0 11L0,11A11,11 0,0 1,11 0z"/>
<path
android:pathData="M0,0h22v22h-22z"
android:fillColor="#4CA3FF"/>
<path
android:pathData="M14.8,9.7C15.4,10.5 15.8,11.5 15.8,12.7C15.8,13.8 15.4,14.9 14.7,15.7L14.7,15.8L14.7,15.7C14.6,15.6 14.6,15.5 14.6,15.4C14.3,14 13.2,12.7 11.4,11.7C10.2,11 9.5,10.1 9.3,9.2C9.2,8.6 9.3,8 9.4,7.5C9.6,6.9 9.9,6.5 10.1,6.2L10.8,5.4C10.9,5.2 11.1,5.2 11.2,5.4L14.8,9.7H14.8ZM15.9,8.8L11.2,3.1C11.1,3 10.9,3 10.8,3.1L6.1,8.8L6.1,8.8C5.3,9.9 4.8,11.3 4.8,12.8C4.8,16.2 7.6,19 11,19C14.5,19 17.3,16.2 17.3,12.8C17.3,11.3 16.7,9.9 15.9,8.8L15.9,8.8L15.9,8.8ZM7.3,9.7L7.7,9.1L7.7,9.2C7.7,9.3 7.7,9.4 7.7,9.5C8,10.9 9,12.1 10.6,13C12,13.8 12.8,14.7 13.1,15.7C13.2,16.2 13.2,16.6 13.1,16.9L13.1,17L13.1,17C12.5,17.3 11.8,17.5 11,17.5C8.4,17.5 6.2,15.3 6.2,12.7C6.2,11.5 6.6,10.5 7.3,9.7L7.3,9.7Z"
android:fillColor="#ffffff" />
</vector>

View file

@ -1,8 +1,8 @@
package com.tangem.data.managetokens
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.blockchainsdk.compatibility.l2BlockchainsCoinIds
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.blockchainsdk.utils.isSupportedInApp
import com.tangem.blockchainsdk.utils.toNetworkId
import com.tangem.data.common.currency.getBlockchain
@ -15,7 +15,9 @@ import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull
import com.tangem.datasource.local.testnet.TestnetTokensStorage
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.common.TapWorkarounds.isTestCard
import com.tangem.domain.common.extensions.canHandleBlockchain
import com.tangem.domain.common.extensions.canHandleToken
import com.tangem.domain.common.extensions.supportedBlockchains
@ -31,6 +33,7 @@ import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.pagination.BatchFetchResult
import com.tangem.pagination.BatchListSource
import com.tangem.pagination.fetcher.LimitOffsetBatchFetcher
import com.tangem.pagination.fetcher.LimitOffsetBatchFetcher.Request
import com.tangem.pagination.toBatchFlow
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -40,6 +43,7 @@ internal class DefaultManageTokensRepository(
private val userWalletsStore: UserWalletsStore,
private val manageTokensUpdateFetcher: ManageTokensUpdateFetcher,
private val appPreferencesStore: AppPreferencesStore,
private val testnetTokensStorage: TestnetTokensStorage,
private val dispatchers: CoroutineDispatcherProvider,
) : ManageTokensRepository {
@ -59,7 +63,6 @@ internal class DefaultManageTokensRepository(
).toBatchFlow()
}
@Suppress("ComplexCondition")
private fun createFetcher(
batchSize: Int,
): LimitOffsetBatchFetcher<ManageTokensListConfig, List<ManagedCryptoCurrency>> = LimitOffsetBatchFetcher(
@ -67,59 +70,101 @@ internal class DefaultManageTokensRepository(
batchSize = batchSize,
subFetcher = { request, _, isFirstBatchFetching ->
val userWallet = request.params.userWalletId?.let { getUserWallet(it) }
val supportedBlockchains = getSupportedBlockchains(userWallet)
val searchText = request.params.searchText?.takeIf { it.isNotBlank() }
val call = suspend {
tangemTechApi.getCoins(
networkIds = supportedBlockchains.joinToString(
separator = ",",
transform = Blockchain::toNetworkId,
),
active = true,
searchText = searchText,
offset = request.offset * request.limit,
limit = request.limit,
).getOrThrow()
}
val coinsResponse = if (isFirstBatchFetching) {
call()
if (userWallet?.scanResponse?.card?.isTestCard == true) {
fetchTestnetCurrencies(userWallet, request)
} else {
retryOnError(call = call)
fetchCurrencies(userWallet, request, isFirstBatchFetching)
}
// filter l2 coins
val updatedCoinsResponse = coinsResponse.copy(
coins = coinsResponse.coins.filterNot { l2BlockchainsCoinIds.contains(it.id) },
)
val tokensResponse = request.params.userWalletId?.let { getSavedUserTokensResponseSync(it) }
val items = if (isFirstBatchFetching &&
tokensResponse != null &&
userWallet != null &&
request.params.searchText.isNullOrBlank()
) {
managedCryptoCurrencyFactory.createWithCustomTokens(
coinsResponse = updatedCoinsResponse,
tokensResponse = tokensResponse,
derivationStyleProvider = userWallet.scanResponse.derivationStyleProvider,
)
} else {
managedCryptoCurrencyFactory.create(
coinsResponse = updatedCoinsResponse,
tokensResponse = tokensResponse,
derivationStyleProvider = userWallet?.scanResponse?.derivationStyleProvider,
)
}
BatchFetchResult.Success(
data = items,
empty = items.isEmpty(),
last = items.size < request.limit,
)
},
)
@Suppress("ComplexCondition")
private suspend fun fetchCurrencies(
userWallet: UserWallet?,
request: Request<ManageTokensListConfig>,
isFirstBatchFetching: Boolean,
): BatchFetchResult.Success<List<ManagedCryptoCurrency>> {
val supportedBlockchains = getSupportedBlockchains(userWallet)
val call = suspend {
tangemTechApi.getCoins(
networkIds = supportedBlockchains.joinToString(
separator = ",",
transform = Blockchain::toNetworkId,
),
active = true,
searchText = request.params.searchText,
offset = request.offset * request.limit,
limit = request.limit,
).getOrThrow()
}
val coinsResponse = if (isFirstBatchFetching) {
call()
} else {
retryOnError(call = call)
}
// filter l2 coins
val updatedCoinsResponse = coinsResponse.copy(
coins = coinsResponse.coins.filterNot { l2BlockchainsCoinIds.contains(it.id) },
)
val tokensResponse = request.params.userWalletId?.let { getSavedUserTokensResponseSync(it) }
val items = if (isFirstBatchFetching &&
tokensResponse != null &&
userWallet != null &&
request.params.searchText.isNullOrBlank()
) {
managedCryptoCurrencyFactory.createWithCustomTokens(
coinsResponse = updatedCoinsResponse,
tokensResponse = tokensResponse,
derivationStyleProvider = userWallet.scanResponse.derivationStyleProvider,
)
} else {
managedCryptoCurrencyFactory.create(
coinsResponse = updatedCoinsResponse,
tokensResponse = tokensResponse,
derivationStyleProvider = userWallet?.scanResponse?.derivationStyleProvider,
)
}
return BatchFetchResult.Success(
data = items,
empty = items.isEmpty(),
last = items.size < request.limit,
)
}
private suspend fun fetchTestnetCurrencies(
userWallet: UserWallet,
request: Request<ManageTokensListConfig>,
): BatchFetchResult.Success<List<ManagedCryptoCurrency>> {
val searchText = request.params.searchText
val testnetTokensConfig = testnetTokensStorage.getConfig()
val items = managedCryptoCurrencyFactory.createTestnetWithCustomTokens(
testnetTokensConfig = if (!searchText.isNullOrBlank()) {
testnetTokensConfig.copy(
tokens = testnetTokensConfig.tokens.filter { token ->
token.symbol.contains(other = searchText, ignoreCase = true) ||
token.name.contains(other = searchText, ignoreCase = true)
},
)
} else {
testnetTokensConfig
},
tokensResponse = getSavedUserTokensResponseSync(userWallet.walletId),
derivationStyleProvider = userWallet.scanResponse.derivationStyleProvider,
)
return BatchFetchResult.Success(
data = items,
empty = items.isEmpty(),
last = true,
)
}
private suspend fun getUserWallet(userWalletId: UserWalletId): UserWallet {
return requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) {
"Unable to find a user wallet with provided ID: $userWalletId"

View file

@ -5,6 +5,7 @@ import com.tangem.data.managetokens.DefaultManageTokensRepository
import com.tangem.data.managetokens.utils.ManageTokensUpdateFetcher
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.testnet.TestnetTokensStorage
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.managetokens.repository.CustomTokensRepository
import com.tangem.domain.managetokens.repository.ManageTokensRepository
@ -27,6 +28,7 @@ internal object ManageTokensDataModule {
userWalletsStore: UserWalletsStore,
manageTokensUpdateFetcher: ManageTokensUpdateFetcher,
appPreferencesStore: AppPreferencesStore,
testnetTokensStorage: TestnetTokensStorage,
dispatchers: CoroutineDispatcherProvider,
): ManageTokensRepository {
return DefaultManageTokensRepository(
@ -34,6 +36,7 @@ internal object ManageTokensDataModule {
userWalletsStore,
manageTokensUpdateFetcher,
appPreferencesStore,
testnetTokensStorage,
dispatchers,
)
}

View file

@ -12,6 +12,7 @@ import com.tangem.data.common.currency.getNetwork
import com.tangem.data.common.currency.getTokenId
import com.tangem.datasource.api.tangemTech.models.CoinsResponse
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.local.testnet.models.TestnetTokensConfig
import com.tangem.domain.common.DerivationStyleProvider
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency.SourceNetwork
@ -34,16 +35,49 @@ internal class ManagedCryptoCurrencyFactory {
tokensResponse: UserTokensResponse,
derivationStyleProvider: DerivationStyleProvider,
): List<ManagedCryptoCurrency> {
val customTokens = tokensResponse.tokens
.mapNotNull { token ->
maybeCreateCustomToken(token, coinsResponse.imageHost, derivationStyleProvider)
}
val customTokens = createCustomTokens(tokensResponse, derivationStyleProvider)
val tokens = create(coinsResponse, tokensResponse, derivationStyleProvider)
return customTokens + tokens
}
fun createTestnetWithCustomTokens(
testnetTokensConfig: TestnetTokensConfig,
tokensResponse: UserTokensResponse?,
derivationStyleProvider: DerivationStyleProvider,
): List<ManagedCryptoCurrency> {
val customTokens = tokensResponse
?.let { createCustomTokens(it, derivationStyleProvider) }
?: emptyList()
val testnetTokens = testnetTokensConfig.tokens.map { testnetToken ->
ManagedCryptoCurrency.Token(
id = ManagedCryptoCurrency.ID(testnetToken.id),
name = testnetToken.name,
symbol = testnetToken.symbol,
iconUrl = getIconUrl(testnetToken.id),
availableNetworks = testnetToken.networks?.mapNotNull { network ->
createSource(
networkId = network.id,
contractAddress = network.address,
decimals = network.decimalCount ?: return@mapNotNull null,
derivationStyleProvider = derivationStyleProvider,
)
} ?: emptyList(),
addedIn = findAddedInNetworks(testnetToken.id, tokensResponse, derivationStyleProvider),
)
}
return customTokens + testnetTokens
}
private fun createCustomTokens(
tokensResponse: UserTokensResponse,
derivationStyleProvider: DerivationStyleProvider,
): List<ManagedCryptoCurrency> = tokensResponse.tokens
.mapNotNull { token ->
maybeCreateCustomToken(token, null, derivationStyleProvider)
}
private fun maybeCreateCustomToken(
token: UserTokensResponse.Token,
imageHost: String?,
@ -100,23 +134,29 @@ internal class ManagedCryptoCurrencyFactory {
symbol = coinResponse.symbol,
iconUrl = getIconUrl(coinResponse.id, imageHost),
availableNetworks = updatedNetworks.mapNotNull { network ->
createSource(network, derivationStyleProvider)
createSource(
networkId = network.networkId,
contractAddress = network.contractAddress,
decimals = network.decimalCount?.toInt() ?: return@mapNotNull null,
derivationStyleProvider = derivationStyleProvider,
)
},
addedIn = findAddedInNetworks(coinResponse.id, tokensResponse, derivationStyleProvider),
)
}
private fun createSource(
networkResponse: CoinsResponse.Coin.Network,
networkId: String,
contractAddress: String?,
decimals: Int,
derivationStyleProvider: DerivationStyleProvider?,
extraDerivationPath: String? = null,
): SourceNetwork? {
val blockchain = Blockchain.fromNetworkId(networkResponse.networkId)
val blockchain = Blockchain.fromNetworkId(networkId)
?.takeIf { it.isSupportedInApp() }
?: return null
val network = getNetwork(blockchain, extraDerivationPath, derivationStyleProvider) ?: return null
val contractAddress = networkResponse.contractAddress
return if (contractAddress.isNullOrBlank()) {
SourceNetwork.Main(
@ -127,7 +167,7 @@ internal class ManagedCryptoCurrencyFactory {
} else {
SourceNetwork.Default(
network = network,
decimals = requireNotNull(networkResponse.decimalCount?.toInt()),
decimals = decimals,
contractAddress = contractAddress,
)
}
@ -157,7 +197,7 @@ internal class ManagedCryptoCurrencyFactory {
}
}
private fun getIconUrl(id: String, imageHost: String?): String {
private fun getIconUrl(id: String, imageHost: String? = DEFAULT_IMAGE_HOST): String {
return "${imageHost ?: DEFAULT_IMAGE_HOST}large/$id.png"
}

View file

@ -89,7 +89,7 @@ markdownComposeView = "0.5.4"
# endregion Other libraries
# region Tangem
tangemBlockchainSdk = "release-app_5.16-803"
tangemBlockchainSdk = "release-app_5.16-804"
#tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds
tangemCardSdk = "release-app_5.16-386"
#tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^

View file

@ -126,6 +126,8 @@ fun Blockchain.Companion.fromNetworkId(networkId: String): Blockchain? {
"sei-network" -> Blockchain.Sei
"sei-network/test" -> Blockchain.SeiTestnet
"internet-computer" -> Blockchain.InternetComputer
"sui" -> Blockchain.Sui
"sui/test" -> Blockchain.SuiTestnet
else -> null
}
}
@ -253,6 +255,8 @@ fun Blockchain.toNetworkId(): String {
Blockchain.Sei -> "sei-network"
Blockchain.SeiTestnet -> "sei-network/test"
Blockchain.InternetComputer -> "internet-computer"
Blockchain.Sui -> "sui"
Blockchain.SuiTestnet -> "sui/test"
}
}
@ -337,6 +341,7 @@ fun Blockchain.toCoinId(): String {
Blockchain.Cyber, Blockchain.CyberTestnet -> "cyber-ethereum"
Blockchain.Sei, Blockchain.SeiTestnet -> "sei-network"
Blockchain.InternetComputer -> "internet-computer"
Blockchain.Sui, Blockchain.SuiTestnet -> "sui"
}
}