Updated on 2026-08-14
This commit is contained in:
parent
8da4dc8205
commit
f70d183814
12 changed files with 156 additions and 37 deletions
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.tap.features.customtoken.impl.data
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.datasource.api.common.response.getOrThrow
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.domain.common.extensions.supportedBlockchains
|
||||
import com.tangem.domain.common.extensions.toNetworkId
|
||||
|
|
@ -39,6 +40,7 @@ class DefaultCustomTokenRepository(
|
|||
contractAddress = address,
|
||||
networkIds = networkId ?: supportedTokenNetworkIds.joinToString(separator = ","),
|
||||
)
|
||||
.getOrThrow()
|
||||
.coins.firstNotNullOfOrNull { coin ->
|
||||
val networksWithTheSameAddress = coin.networks.filter { network ->
|
||||
(network.contractAddress != null || network.decimalCount != null) &&
|
||||
|
|
|
|||
|
|
@ -9,6 +9,10 @@ import com.tangem.tap.features.customtoken.impl.domain.models.FoundToken
|
|||
*/
|
||||
interface CustomTokenRepository {
|
||||
|
||||
/** Find token by [address] and [networkId] */
|
||||
/**
|
||||
* Find token by [address] and [networkId]
|
||||
*
|
||||
* @throws com.tangem.datasource.api.common.response.ApiResponseError
|
||||
* */
|
||||
suspend fun findToken(address: String, networkId: String?): FoundToken
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ package com.tangem.tap.features.tokens.impl.data
|
|||
import androidx.paging.PagingSource
|
||||
import androidx.paging.PagingState
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.datasource.api.common.response.getOrThrow
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.domain.common.extensions.supportedBlockchains
|
||||
import com.tangem.domain.common.extensions.toNetworkId
|
||||
|
|
@ -50,7 +51,7 @@ internal class TangemApiTokensPagingSource(
|
|||
searchText = searchText,
|
||||
offset = page * params.loadSize,
|
||||
limit = params.loadSize,
|
||||
)
|
||||
).getOrThrow()
|
||||
}.fold(
|
||||
onSuccess = { response ->
|
||||
LoadResult.Page(
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ internal interface TokensListRepository {
|
|||
* Get available tokens list
|
||||
*
|
||||
* @param searchText search text
|
||||
*
|
||||
* @throws com.tangem.datasource.api.common.response.ApiResponseError
|
||||
*/
|
||||
fun getAvailableTokens(searchText: String?): Flow<PagingData<Token>>
|
||||
}
|
||||
|
|
@ -20,7 +20,7 @@ interface TangemTechApi {
|
|||
@Query("searchText") searchText: String? = null,
|
||||
@Query("offset") offset: Int? = null,
|
||||
@Query("limit") limit: Int? = null,
|
||||
): CoinsResponse
|
||||
): ApiResponse<CoinsResponse>
|
||||
|
||||
@GET("rates")
|
||||
suspend fun getRates(@Query("currencyId") currencyId: String, @Query("coinIds") coinIds: String): RatesResponse
|
||||
|
|
@ -35,7 +35,10 @@ interface TangemTechApi {
|
|||
suspend fun getUserTokens(@Path(value = "user-id") userId: String): ApiResponse<UserTokensResponse>
|
||||
|
||||
@PUT("user-tokens/{user-id}")
|
||||
suspend fun saveUserTokens(@Path(value = "user-id") userId: String, @Body userTokens: UserTokensResponse)
|
||||
suspend fun saveUserTokens(
|
||||
@Path(value = "user-id") userId: String,
|
||||
@Body userTokens: UserTokensResponse,
|
||||
): ApiResponse<Unit>
|
||||
|
||||
/** Returns referral status by [walletId] */
|
||||
@GET("referral/{walletId}")
|
||||
|
|
|
|||
|
|
@ -31,12 +31,16 @@ value class ApiResponseRaise(
|
|||
/**
|
||||
* Attempts to execute an API call safely, providing error handling.
|
||||
*
|
||||
* @param T The return type of the API call and the function.
|
||||
* @param call The API call block to execute.
|
||||
* @param onError A function to handle errors and return a fallback value of type [T].
|
||||
*
|
||||
* @return The result of the API call or the fallback value provided by [onError] if an error occurs.
|
||||
*/
|
||||
inline fun <T> safeApiCall(call: ApiResponseRaise.() -> T, onError: (ApiResponseError) -> T): T {
|
||||
suspend inline fun <T> safeApiCall(
|
||||
crossinline call: suspend ApiResponseRaise.() -> T,
|
||||
crossinline onError: suspend (ApiResponseError) -> T,
|
||||
): T {
|
||||
return recover(
|
||||
block = { call(ApiResponseRaise(raise = this)) },
|
||||
recover = {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.tangem.data.tokens.paging
|
|||
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.paging.PagingState
|
||||
import com.tangem.datasource.api.common.response.getOrThrow
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.api.tangemTech.models.CoinsResponse
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
|
|
@ -41,7 +42,7 @@ internal class CoinsPagingSource(
|
|||
searchText = searchText,
|
||||
offset = page * params.loadSize,
|
||||
limit = params.loadSize,
|
||||
)
|
||||
).getOrThrow()
|
||||
}.fold(
|
||||
onSuccess = { response ->
|
||||
val coinsIds = response.coins.map { coin ->
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ internal class DefaultCurrenciesRepository(
|
|||
private val cardCurrenciesFactory = CardCryptoCurrenciesFactory(demoConfig)
|
||||
private val userTokensResponseFactory = UserTokensResponseFactory()
|
||||
private val userTokensBackwardCompatibility = UserTokensBackwardCompatibility()
|
||||
private val customTokensMerger = CustomTokensMerger(tangemTechApi, dispatchers)
|
||||
|
||||
override suspend fun saveTokens(
|
||||
userWalletId: UserWalletId,
|
||||
|
|
@ -313,6 +314,7 @@ internal class DefaultCurrenciesRepository(
|
|||
): Boolean {
|
||||
val blockchain = Blockchain.fromId(cryptoCurrencyStatus.currency.network.id.value)
|
||||
val isBitcoinBlockchain = blockchain == Blockchain.Bitcoin || blockchain == Blockchain.BitcoinTestnet
|
||||
|
||||
return if (cryptoCurrencyStatus.currency is CryptoCurrency.Coin && isBitcoinBlockchain) {
|
||||
val outgoingTransactions = cryptoCurrencyStatus.value.pendingTransactions.filter { it.isOutgoing }
|
||||
outgoingTransactions.isNotEmpty()
|
||||
|
|
@ -341,40 +343,32 @@ internal class DefaultCurrenciesRepository(
|
|||
private suspend fun fetchTokens(userWallet: UserWallet) {
|
||||
val userWalletId = userWallet.walletId
|
||||
|
||||
if (demoConfig.isDemoCardId(userWallet.cardId) && userTokensStore.getSyncOrNull(key = userWalletId) == null) {
|
||||
userTokensStore.store(
|
||||
key = userWalletId,
|
||||
value = userTokensResponseFactory.createUserTokensResponse(
|
||||
currencies = cardCurrenciesFactory.createDefaultCoinsForMultiCurrencyCard(userWallet.scanResponse),
|
||||
isGroupedByNetwork = false,
|
||||
isSortedByBalance = false,
|
||||
),
|
||||
)
|
||||
return
|
||||
val response = if (checkIsEmptyDemoWallet(userWallet)) {
|
||||
createDefaultUserTokensResponse(userWallet)
|
||||
} else {
|
||||
safeApiCall({ tangemTechApi.getUserTokens(userWalletId.stringValue).bind() }) {
|
||||
handleFetchTokensError(userWallet, it)
|
||||
}
|
||||
}
|
||||
|
||||
val response = safeApiCall(
|
||||
call = {
|
||||
tangemTechApi.getUserTokens(userWalletId.stringValue).bind().let {
|
||||
it.copy(tokens = it.tokens.distinct())
|
||||
}
|
||||
},
|
||||
onError = { handleFetchTokensError(userWallet, it) },
|
||||
)
|
||||
val compatibleUserTokensResponse = response
|
||||
.let { it.copy(tokens = it.tokens.distinct()) }
|
||||
.let { customTokensMerger.mergeIfPresented(userWalletId, response) }
|
||||
.let(userTokensBackwardCompatibility::applyCompatibilityAndGetUpdated)
|
||||
|
||||
val compatibleUserTokensResponse = userTokensBackwardCompatibility.applyCompatibilityAndGetUpdated(response)
|
||||
userTokensStore.store(userWallet.walletId, compatibleUserTokensResponse)
|
||||
fetchExchangeableUserMarketCoinsByIds(userWalletId, compatibleUserTokensResponse)
|
||||
}
|
||||
|
||||
private suspend fun checkIsEmptyDemoWallet(userWallet: UserWallet): Boolean {
|
||||
return demoConfig.isDemoCardId(userWallet.cardId) && userTokensStore.getSyncOrNull(userWallet.walletId) == null
|
||||
}
|
||||
|
||||
private suspend fun storeAndPushTokens(userWalletId: UserWalletId, response: UserTokensResponse) {
|
||||
val compatibleUserTokensResponse = userTokensBackwardCompatibility.applyCompatibilityAndGetUpdated(response)
|
||||
userTokensStore.store(userWalletId, compatibleUserTokensResponse)
|
||||
try {
|
||||
tangemTechApi.saveUserTokens(userWalletId.stringValue, response)
|
||||
} catch (e: Throwable) {
|
||||
Timber.e("Unable to save user tokens for: ${userWalletId.stringValue}")
|
||||
}
|
||||
|
||||
pushTokens(userWalletId, response)
|
||||
}
|
||||
|
||||
private suspend fun fetchExchangeableUserMarketCoinsByIds(
|
||||
|
|
@ -407,16 +401,12 @@ internal class DefaultCurrenciesRepository(
|
|||
private suspend fun handleFetchTokensError(userWallet: UserWallet, e: ApiResponseError): UserTokensResponse {
|
||||
val userWalletId = userWallet.walletId
|
||||
val response = userTokensStore.getSyncOrNull(userWalletId)
|
||||
?: userTokensResponseFactory.createUserTokensResponse(
|
||||
currencies = cardCurrenciesFactory.createDefaultCoinsForMultiCurrencyCard(userWallet.scanResponse),
|
||||
isGroupedByNetwork = false,
|
||||
isSortedByBalance = false,
|
||||
)
|
||||
?: createDefaultUserTokensResponse(userWallet)
|
||||
|
||||
if (e is ApiResponseError.HttpException && e.code == ApiResponseError.HttpException.Code.NOT_FOUND) {
|
||||
Timber.w(e, "Requested currencies could not be found in the remote store for: $userWalletId")
|
||||
|
||||
tangemTechApi.saveUserTokens(userWalletId.stringValue, response)
|
||||
pushTokens(userWalletId, response)
|
||||
} else {
|
||||
cacheRegistry.invalidate(getTokensCacheKey(userWalletId))
|
||||
}
|
||||
|
|
@ -424,6 +414,19 @@ internal class DefaultCurrenciesRepository(
|
|||
return response
|
||||
}
|
||||
|
||||
private suspend fun pushTokens(userWalletId: UserWalletId, response: UserTokensResponse) {
|
||||
safeApiCall({ tangemTechApi.saveUserTokens(userWalletId.stringValue, response).bind() }) {
|
||||
Timber.e(it, "Unable to save user tokens for: ${userWalletId.stringValue}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun createDefaultUserTokensResponse(userWallet: UserWallet) =
|
||||
userTokensResponseFactory.createUserTokensResponse(
|
||||
currencies = cardCurrenciesFactory.createDefaultCoinsForMultiCurrencyCard(userWallet.scanResponse),
|
||||
isGroupedByNetwork = false,
|
||||
isSortedByBalance = false,
|
||||
)
|
||||
|
||||
private suspend fun getUserWallet(userWalletId: UserWalletId): UserWallet {
|
||||
return requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) {
|
||||
"Unable to find a user wallet with provided ID: $userWalletId"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,95 @@
|
|||
package com.tangem.data.tokens.utils
|
||||
|
||||
import com.tangem.data.common.api.safeApiCall
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Responsible for merging custom tokens into a user's token response.
|
||||
* It handles the logic to update tokens with additional details if necessary.
|
||||
*/
|
||||
internal class CustomTokensMerger(
|
||||
private val tangemTechApi: TangemTechApi,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) {
|
||||
|
||||
/**
|
||||
* Merges custom tokens into the user's token response if needed.
|
||||
*
|
||||
* This function processes each token in the response asynchronously, checking if an update
|
||||
* is needed, and if so, updating the token from [TangemTechApi.getCoins] response. It then pushes to the backend
|
||||
* and returns an updated UserTokensResponse.
|
||||
*
|
||||
* @param userWalletId The identifier for the user's wallet, used when pushing updates.
|
||||
* @param response The original user tokens response that may need to be updated.
|
||||
* @return A potentially updated UserTokensResponse, with custom tokens merged if necessary.
|
||||
*/
|
||||
suspend fun mergeIfPresented(userWalletId: UserWalletId, response: UserTokensResponse): UserTokensResponse {
|
||||
val mergedTokens = withContext(dispatchers.default) {
|
||||
response.tokens
|
||||
.map { token ->
|
||||
async { mergeIfPresented(token) }
|
||||
}
|
||||
.awaitAll()
|
||||
}
|
||||
val updatedResponse = response.copy(tokens = mergedTokens)
|
||||
|
||||
if (response.tokens != updatedResponse.tokens) {
|
||||
pushTokens(userWalletId, updatedResponse)
|
||||
}
|
||||
|
||||
return updatedResponse
|
||||
}
|
||||
|
||||
private suspend fun mergeIfPresented(token: UserTokensResponse.Token): UserTokensResponse.Token {
|
||||
if (isCoinOrNonCustomToken(token)) return token
|
||||
|
||||
return merge(token)
|
||||
}
|
||||
|
||||
private suspend fun merge(customToken: UserTokensResponse.Token): UserTokensResponse.Token {
|
||||
val foundToken = fetchToken(customToken)
|
||||
|
||||
return foundToken ?: customToken
|
||||
}
|
||||
|
||||
private fun isCoinOrNonCustomToken(token: UserTokensResponse.Token): Boolean {
|
||||
return token.contractAddress.isNullOrEmpty() || token.id != null
|
||||
}
|
||||
|
||||
private suspend fun fetchToken(token: UserTokensResponse.Token): UserTokensResponse.Token? {
|
||||
val response = withContext(dispatchers.io) {
|
||||
safeApiCall(
|
||||
call = {
|
||||
tangemTechApi.getCoins(
|
||||
contractAddress = token.contractAddress,
|
||||
networkIds = token.networkId,
|
||||
).bind()
|
||||
},
|
||||
onError = {
|
||||
Timber.w(it, "Unable to fetch token")
|
||||
null
|
||||
},
|
||||
)
|
||||
}
|
||||
val foundToken = response?.coins?.firstOrNull() ?: return null
|
||||
|
||||
return token.copy(
|
||||
id = foundToken.id,
|
||||
name = foundToken.name,
|
||||
symbol = foundToken.symbol,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun pushTokens(userWalletId: UserWalletId, response: UserTokensResponse) {
|
||||
safeApiCall({ tangemTechApi.saveUserTokens(userWalletId.stringValue, response).bind() }) {
|
||||
Timber.e(it, "Unable to save user tokens for: $userWalletId")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -15,6 +15,7 @@ interface TokensListRepository {
|
|||
*
|
||||
* @param searchText The search text used to filter tokens.
|
||||
* @return A [Flow] emitting [PagingData] containing the tokens with quotes matching the search criteria.
|
||||
* @throws com.tangem.datasource.api.common.response.ApiResponseError
|
||||
*/
|
||||
fun getTokens(searchText: String?): Flow<PagingData<Token>>
|
||||
}
|
||||
|
|
@ -156,7 +156,7 @@ internal class SwapRepositoryImpl @Inject constructor(
|
|||
exchangeable = true,
|
||||
active = true,
|
||||
networkIds = networkId,
|
||||
).coins,
|
||||
).getOrThrow().coins,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@ interface SwapRepository {
|
|||
|
||||
suspend fun getRates(currencyId: String, tokenIds: List<String>): Map<String, Double>
|
||||
|
||||
/**
|
||||
* @throws com.tangem.datasource.api.common.response.ApiResponseError
|
||||
* */
|
||||
suspend fun getExchangeableTokens(networkId: String): List<Currency>
|
||||
|
||||
suspend fun getExchangeStatus(txId: String): Either<UnknownError, ExchangeStatusModel>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue