Updated on 2026-08-14
This commit is contained in:
parent
292d895c4a
commit
8dc8119970
8 changed files with 461 additions and 0 deletions
|
|
@ -104,6 +104,7 @@ dependencies {
|
|||
implementation(projects.data.staking)
|
||||
implementation(projects.data.walletConnect)
|
||||
implementation(projects.data.markets)
|
||||
implementation(projects.data.manageTokens)
|
||||
|
||||
/** Features */
|
||||
implementation(projects.features.onboarding)
|
||||
|
|
|
|||
1
data/manage-tokens/.gitignore
vendored
Normal file
1
data/manage-tokens/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
44
data/manage-tokens/build.gradle.kts
Normal file
44
data/manage-tokens/build.gradle.kts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
alias(deps.plugins.kotlin.kapt)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.example.data.managetokens"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
/** Project - Domain */
|
||||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.manageTokens)
|
||||
implementation(projects.domain.tokens.models)
|
||||
implementation(projects.domain.wallets.models)
|
||||
|
||||
/** Project - Data */
|
||||
implementation(projects.core.datasource)
|
||||
implementation(projects.data.common)
|
||||
|
||||
/** Project - Utils */
|
||||
implementation(projects.core.utils)
|
||||
implementation(projects.core.pagination)
|
||||
implementation(projects.domain.legacy)
|
||||
implementation(projects.libs.blockchainSdk)
|
||||
|
||||
/** Tangem SDKs */
|
||||
implementation(deps.tangem.blockchain)
|
||||
implementation(deps.tangem.card.core)
|
||||
|
||||
/** AndroidX */
|
||||
implementation(deps.androidx.datastore)
|
||||
|
||||
/** DI */
|
||||
implementation(deps.hilt.core)
|
||||
kapt(deps.hilt.kapt)
|
||||
|
||||
/** Other */
|
||||
implementation(deps.moshi.kotlin)
|
||||
implementation(deps.timber)
|
||||
}
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
package com.tangem.data.managetokens
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchainsdk.utils.isSupportedInApp
|
||||
import com.tangem.blockchainsdk.utils.toNetworkId
|
||||
import com.tangem.data.common.utils.retryOnError
|
||||
import com.tangem.data.managetokens.utils.ManageTokensUpdateFetcher
|
||||
import com.tangem.data.managetokens.utils.ManagedCryptoCurrencyFactory
|
||||
import com.tangem.datasource.api.common.response.getOrThrow
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
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.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.common.extensions.supportedBlockchains
|
||||
import com.tangem.domain.common.util.cardTypesResolver
|
||||
import com.tangem.domain.common.util.derivationStyleProvider
|
||||
import com.tangem.domain.managetokens.model.ManageTokensListBatchFlow
|
||||
import com.tangem.domain.managetokens.model.ManageTokensListBatchingContext
|
||||
import com.tangem.domain.managetokens.model.ManageTokensListConfig
|
||||
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency
|
||||
import com.tangem.domain.managetokens.repository.ManageTokensRepository
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
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.toBatchFlow
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
|
||||
internal class DefaultManageTokensRepository(
|
||||
private val tangemTechApi: TangemTechApi,
|
||||
private val userWalletsStore: UserWalletsStore,
|
||||
private val managedCryptoCurrencyFactory: ManagedCryptoCurrencyFactory,
|
||||
private val manageTokensUpdateFetcher: ManageTokensUpdateFetcher,
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : ManageTokensRepository {
|
||||
|
||||
override fun getTokenListBatchFlow(
|
||||
context: ManageTokensListBatchingContext,
|
||||
batchSize: Int,
|
||||
): ManageTokensListBatchFlow {
|
||||
return BatchListSource(
|
||||
fetchDispatcher = dispatchers.io,
|
||||
context = context,
|
||||
generateNewKey = { it.size.inc() },
|
||||
batchFetcher = createFetcher(batchSize),
|
||||
updateFetcher = manageTokensUpdateFetcher,
|
||||
).toBatchFlow()
|
||||
}
|
||||
|
||||
@Suppress("ComplexCondition")
|
||||
private fun createFetcher(
|
||||
batchSize: Int,
|
||||
): LimitOffsetBatchFetcher<ManageTokensListConfig, List<ManagedCryptoCurrency>> = LimitOffsetBatchFetcher(
|
||||
prefetchDistance = batchSize,
|
||||
batchSize = batchSize,
|
||||
subFetcher = { request, _, isFirstBatchFetching ->
|
||||
val userWallet = getUserWallet(request.params.userWalletId)
|
||||
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()
|
||||
} else {
|
||||
retryOnError(call = call)
|
||||
}
|
||||
|
||||
val tokensResponse = getStoredUserTokens(request.params.userWalletId)
|
||||
val items = if (isFirstBatchFetching &&
|
||||
tokensResponse != null &&
|
||||
userWallet != null &&
|
||||
request.params.searchText.isNullOrBlank()
|
||||
) {
|
||||
managedCryptoCurrencyFactory.createWithCustomTokens(
|
||||
coinsResponse = coinsResponse,
|
||||
tokensResponse = tokensResponse,
|
||||
derivationStyleProvider = userWallet.scanResponse.derivationStyleProvider,
|
||||
)
|
||||
} else {
|
||||
managedCryptoCurrencyFactory.create(
|
||||
coinsResponse = coinsResponse,
|
||||
tokensResponse = tokensResponse,
|
||||
derivationStyleProvider = userWallet?.scanResponse?.derivationStyleProvider,
|
||||
)
|
||||
}
|
||||
|
||||
BatchFetchResult.Success(
|
||||
data = items,
|
||||
empty = items.isEmpty(),
|
||||
last = items.size < request.limit,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
private suspend fun getStoredUserTokens(userWalletId: UserWalletId?): UserTokensResponse? {
|
||||
return if (userWalletId != null) {
|
||||
appPreferencesStore.getObjectSyncOrNull(
|
||||
key = PreferencesKeys.getUserTokensKey(userWalletId.stringValue),
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getUserWallet(userWalletId: UserWalletId?): UserWallet? {
|
||||
if (userWalletId == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
return requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) {
|
||||
"User wallet not found"
|
||||
}
|
||||
}
|
||||
|
||||
private fun getSupportedBlockchains(userWallet: UserWallet?): List<Blockchain> {
|
||||
return userWallet?.scanResponse?.let {
|
||||
it.card.supportedBlockchains(it.cardTypesResolver)
|
||||
} ?: Blockchain.entries.filter {
|
||||
!it.isTestnet() && it.isSupportedInApp()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
package com.tangem.data.managetokens.di
|
||||
|
||||
import com.tangem.data.managetokens.DefaultManageTokensRepository
|
||||
import com.tangem.data.managetokens.utils.ManageTokensUpdateFetcher
|
||||
import com.tangem.data.managetokens.utils.ManagedCryptoCurrencyFactory
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.managetokens.repository.ManageTokensRepository
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
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 ManageTokensDataModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideManageTokensRepository(
|
||||
tangemTechApi: TangemTechApi,
|
||||
userWalletsStore: UserWalletsStore,
|
||||
managedCryptoCurrencyFactory: ManagedCryptoCurrencyFactory,
|
||||
manageTokensUpdateFetcher: ManageTokensUpdateFetcher,
|
||||
appPreferencesStore: AppPreferencesStore,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): ManageTokensRepository {
|
||||
return DefaultManageTokensRepository(
|
||||
tangemTechApi,
|
||||
userWalletsStore,
|
||||
managedCryptoCurrencyFactory,
|
||||
manageTokensUpdateFetcher,
|
||||
appPreferencesStore,
|
||||
dispatchers,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
package com.tangem.data.managetokens.utils
|
||||
|
||||
import com.tangem.domain.managetokens.model.ManageTokensUpdateAction
|
||||
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency
|
||||
import com.tangem.pagination.Batch
|
||||
import com.tangem.pagination.BatchUpdateFetcher
|
||||
import com.tangem.pagination.BatchUpdateResult
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
internal class ManageTokensUpdateFetcher @Inject constructor() :
|
||||
BatchUpdateFetcher<Int, List<ManagedCryptoCurrency>, ManageTokensUpdateAction> {
|
||||
|
||||
override suspend fun BatchUpdateFetcher.UpdateContext<Int, List<ManagedCryptoCurrency>>.fetchUpdateAsync(
|
||||
toUpdate: List<Batch<Int, List<ManagedCryptoCurrency>>>,
|
||||
updateRequest: ManageTokensUpdateAction,
|
||||
) {
|
||||
when (updateRequest) {
|
||||
is ManageTokensUpdateAction.AddCurrency -> coroutineScope {
|
||||
val tasks = toUpdate.map { (key, data) ->
|
||||
async {
|
||||
val currencyIndex = data.indexOfFirst { it.id == updateRequest.currencyId }
|
||||
.takeIf { it != -1 }
|
||||
?: error("Currency '${updateRequest.currencyId}' not found in batch #$key")
|
||||
val updatedCurrency = when (val currency = data[currencyIndex]) {
|
||||
is ManagedCryptoCurrency.Custom -> error("Can't add custom currency '${currency.id}'")
|
||||
is ManagedCryptoCurrency.Token -> {
|
||||
currency.copy(
|
||||
addedIn = if (updateRequest.isSelected) {
|
||||
currency.addedIn + updateRequest.networkId
|
||||
} else {
|
||||
currency.addedIn - updateRequest.networkId
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data.toMutableList().apply {
|
||||
set(currencyIndex, updatedCurrency)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.forEachIndexed { index, task ->
|
||||
launch {
|
||||
val updatedItems = task.await()
|
||||
|
||||
update {
|
||||
BatchUpdateResult.Success(
|
||||
data = mapNotNull {
|
||||
if (it.key == toUpdate[index].key) {
|
||||
Batch(it.key, updatedItems)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,168 @@
|
|||
package com.tangem.data.managetokens.utils
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchainsdk.utils.fromNetworkId
|
||||
import com.tangem.blockchainsdk.utils.isSupportedInApp
|
||||
import com.tangem.blockchainsdk.utils.toCoinId
|
||||
import com.tangem.data.common.currency.getCoinId
|
||||
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.domain.common.DerivationStyleProvider
|
||||
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency
|
||||
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency.SourceNetwork
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
internal class ManagedCryptoCurrencyFactory @Inject constructor() {
|
||||
|
||||
fun create(
|
||||
coinsResponse: CoinsResponse,
|
||||
tokensResponse: UserTokensResponse?,
|
||||
derivationStyleProvider: DerivationStyleProvider?,
|
||||
): List<ManagedCryptoCurrency> {
|
||||
return coinsResponse.coins.mapNotNull { coin ->
|
||||
createToken(coin, tokensResponse, coinsResponse.imageHost, derivationStyleProvider)
|
||||
}
|
||||
}
|
||||
|
||||
fun createWithCustomTokens(
|
||||
coinsResponse: CoinsResponse,
|
||||
tokensResponse: UserTokensResponse,
|
||||
derivationStyleProvider: DerivationStyleProvider,
|
||||
): List<ManagedCryptoCurrency> {
|
||||
val customTokens = tokensResponse.tokens
|
||||
.mapNotNull { token ->
|
||||
maybeCreateCustomToken(token, coinsResponse.imageHost, derivationStyleProvider)
|
||||
}
|
||||
|
||||
val tokens = create(coinsResponse, tokensResponse, derivationStyleProvider)
|
||||
|
||||
return customTokens + tokens
|
||||
}
|
||||
|
||||
private fun maybeCreateCustomToken(
|
||||
token: UserTokensResponse.Token,
|
||||
imageHost: String?,
|
||||
derivationStyleProvider: DerivationStyleProvider,
|
||||
): ManagedCryptoCurrency? {
|
||||
val blockchain = Blockchain.fromNetworkId(token.networkId)
|
||||
?.takeIf { it.isSupportedInApp() }
|
||||
?: return null
|
||||
|
||||
if (!checkIsCustomToken(token, blockchain, derivationStyleProvider)) {
|
||||
return null
|
||||
}
|
||||
|
||||
val network = getNetwork(
|
||||
blockchain = blockchain,
|
||||
extraDerivationPath = token.derivationPath,
|
||||
derivationStyleProvider = derivationStyleProvider,
|
||||
) ?: return null
|
||||
val contractAddress = token.contractAddress
|
||||
|
||||
return if (contractAddress.isNullOrBlank()) {
|
||||
ManagedCryptoCurrency.Custom.Coin(
|
||||
currencyId = getCoinId(network, blockchain.toCoinId()),
|
||||
name = token.name,
|
||||
symbol = token.symbol,
|
||||
iconUrl = getIconUrl(blockchain.id, imageHost),
|
||||
network = network,
|
||||
)
|
||||
} else {
|
||||
ManagedCryptoCurrency.Custom.Token(
|
||||
currencyId = getTokenId(network, token.id, contractAddress),
|
||||
name = token.name,
|
||||
symbol = token.symbol,
|
||||
iconUrl = token.id?.let { getIconUrl(it, imageHost) },
|
||||
contractAddress = contractAddress,
|
||||
network = network,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createToken(
|
||||
coinResponse: CoinsResponse.Coin,
|
||||
tokensResponse: UserTokensResponse?,
|
||||
imageHost: String?,
|
||||
derivationStyleProvider: DerivationStyleProvider?,
|
||||
): ManagedCryptoCurrency? {
|
||||
if (coinResponse.networks.isEmpty() || !coinResponse.active) return null
|
||||
|
||||
return ManagedCryptoCurrency.Token(
|
||||
id = ManagedCryptoCurrency.ID(coinResponse.id),
|
||||
name = coinResponse.name,
|
||||
symbol = coinResponse.symbol,
|
||||
iconUrl = getIconUrl(coinResponse.id, imageHost),
|
||||
availableNetworks = coinResponse.networks.mapNotNull { network ->
|
||||
createSource(network, derivationStyleProvider)
|
||||
},
|
||||
addedIn = findAddedInNetworksIds(coinResponse.id, tokensResponse),
|
||||
)
|
||||
}
|
||||
|
||||
private fun createSource(
|
||||
networkResponse: CoinsResponse.Coin.Network,
|
||||
derivationStyleProvider: DerivationStyleProvider?,
|
||||
extraDerivationPath: String? = null,
|
||||
): SourceNetwork? {
|
||||
val blockchain = Blockchain.fromNetworkId(networkResponse.networkId)
|
||||
?.takeIf { it.isSupportedInApp() }
|
||||
?: return null
|
||||
|
||||
val network = getNetwork(blockchain, extraDerivationPath, derivationStyleProvider) ?: return null
|
||||
val contractAddress = networkResponse.contractAddress
|
||||
|
||||
return if (contractAddress.isNullOrBlank()) {
|
||||
SourceNetwork.Main(
|
||||
network = network,
|
||||
)
|
||||
} else {
|
||||
SourceNetwork.Default(
|
||||
network = network,
|
||||
contractAddress = contractAddress,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun findAddedInNetworksIds(currencyId: String, tokensResponse: UserTokensResponse?): Set<Network.ID> {
|
||||
if (tokensResponse == null) return emptySet()
|
||||
|
||||
return tokensResponse.tokens
|
||||
.filter { it.id == currencyId }
|
||||
.map { it.networkId }
|
||||
.mapNotNullTo(mutableSetOf()) { networkId ->
|
||||
val blockchain = Blockchain.fromNetworkId(networkId)
|
||||
|
||||
if (blockchain != null && blockchain.isSupportedInApp()) {
|
||||
Network.ID(blockchain.id)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getIconUrl(id: String, imageHost: String?): String {
|
||||
return "${imageHost ?: DEFAULT_IMAGE_HOST}large/$id.png"
|
||||
}
|
||||
|
||||
private fun checkIsCustomToken(
|
||||
token: UserTokensResponse.Token,
|
||||
blockchain: Blockchain,
|
||||
derivationStyleProvider: DerivationStyleProvider,
|
||||
): Boolean = token.id.isNullOrBlank() ||
|
||||
checkIsCustomDerivationPath(token.derivationPath, blockchain, derivationStyleProvider)
|
||||
|
||||
private fun checkIsCustomDerivationPath(
|
||||
derivationPath: String?,
|
||||
blockchain: Blockchain,
|
||||
derivationStyleProvider: DerivationStyleProvider,
|
||||
): Boolean = derivationPath != blockchain.derivationPath(derivationStyleProvider.getDerivationStyle())?.rawPath
|
||||
|
||||
private companion object {
|
||||
const val DEFAULT_IMAGE_HOST = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/"
|
||||
}
|
||||
}
|
||||
|
|
@ -250,4 +250,5 @@ include(":data:qr-scanning")
|
|||
include(":data:staking")
|
||||
include(":data:wallet-connect")
|
||||
include(":data:markets")
|
||||
include(":data:manage-tokens")
|
||||
// endregion Data modules
|
||||
Loading…
Add table
Add a link
Reference in a new issue