Updated on 2026-08-14
This commit is contained in:
commit
8bba8e1afa
825 changed files with 9837 additions and 22210 deletions
|
|
@ -53,6 +53,7 @@ internal class DefaultCardSdkConfigRepository(
|
|||
ProductType.Wallet2,
|
||||
ProductType.Start2Coin,
|
||||
ProductType.Ring,
|
||||
ProductType.Visa,
|
||||
-> CardIdDisplayFormat.Full
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -1,73 +0,0 @@
|
|||
package com.tangem.data.source.preferences
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import androidx.core.content.edit
|
||||
import java.util.*
|
||||
|
||||
@Deprecated("Create repository instead")
|
||||
class AppRatingLaunchObserver internal constructor(
|
||||
private val preferences: SharedPreferences,
|
||||
private val launchCounts: Int,
|
||||
) {
|
||||
|
||||
private val deferShowing = 20
|
||||
private val firstShowing = 3
|
||||
private var fundsFoundDate: Calendar? = null
|
||||
|
||||
init {
|
||||
val msWhenFundsWasFound = preferences.getLong(K_FUNDS_FOUND_DATE, FUNDS_FOUND_DATE_UNDEFINED)
|
||||
if (msWhenFundsWasFound != FUNDS_FOUND_DATE_UNDEFINED) {
|
||||
fundsFoundDate = Calendar.getInstance().apply { timeInMillis = msWhenFundsWasFound }
|
||||
}
|
||||
}
|
||||
|
||||
fun foundWalletWithFunds() {
|
||||
if (fundsFoundDate != null) return
|
||||
|
||||
fundsFoundDate = Calendar.getInstance()
|
||||
preferences.edit(true) {
|
||||
putLong(K_FUNDS_FOUND_DATE, fundsFoundDate!!.timeInMillis).apply()
|
||||
putInt(K_SHOW_RATING_AT_LAUNCH_COUNT, launchCounts + firstShowing)
|
||||
}
|
||||
}
|
||||
|
||||
fun isReadyToShow(): Boolean {
|
||||
val fundsDate = fundsFoundDate ?: return false
|
||||
|
||||
if (!userWasInteractWithRating()) {
|
||||
val diff = Calendar.getInstance().timeInMillis - fundsDate.timeInMillis
|
||||
val diffInDays = diff / DAY_MILLIS
|
||||
return launchCounts >= getCounterOfNextShowing() && diffInDays >= firstShowing
|
||||
}
|
||||
|
||||
val nextShowing = getCounterOfNextShowing()
|
||||
return launchCounts >= nextShowing
|
||||
}
|
||||
|
||||
fun applyDelayedShowing() {
|
||||
updateNextShowing(launchCounts + deferShowing)
|
||||
}
|
||||
|
||||
fun setNeverToShow() {
|
||||
updateNextShowing(Int.MAX_VALUE)
|
||||
}
|
||||
|
||||
private fun updateNextShowing(at: Int) {
|
||||
val editor = preferences.edit()
|
||||
editor.putInt(K_SHOW_RATING_AT_LAUNCH_COUNT, at)
|
||||
editor.putBoolean(K_USER_WAS_INTERACT_WITH_RATING, true)
|
||||
editor.apply()
|
||||
}
|
||||
|
||||
private fun userWasInteractWithRating(): Boolean = preferences.getBoolean(K_USER_WAS_INTERACT_WITH_RATING, false)
|
||||
|
||||
private fun getCounterOfNextShowing(): Int = preferences.getInt(K_SHOW_RATING_AT_LAUNCH_COUNT, firstShowing)
|
||||
|
||||
companion object {
|
||||
private const val K_SHOW_RATING_AT_LAUNCH_COUNT = "showRatingDialogAtLaunchCount"
|
||||
private const val K_FUNDS_FOUND_DATE = "fundsFoundDate"
|
||||
private const val K_USER_WAS_INTERACT_WITH_RATING = "userWasInteractWithRating"
|
||||
private const val FUNDS_FOUND_DATE_UNDEFINED = -1L
|
||||
private const val DAY_MILLIS = 1000 * 60 * 60 * 24
|
||||
}
|
||||
}
|
||||
|
|
@ -5,10 +5,7 @@ import android.content.SharedPreferences
|
|||
import androidx.core.content.edit
|
||||
import com.tangem.common.json.MoshiJsonConverter
|
||||
import com.tangem.data.source.preferences.adapters.BigDecimalAdapter
|
||||
import com.tangem.data.source.preferences.adapters.CardBalanceStateAdapter
|
||||
import com.tangem.data.source.preferences.storage.DisclaimerPrefStorage
|
||||
import com.tangem.data.source.preferences.storage.FiatCurrenciesPrefStorage
|
||||
import com.tangem.data.source.preferences.storage.ToppedUpWalletStorage
|
||||
import com.tangem.data.source.preferences.storage.UsedCardsPrefStorage
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -17,35 +14,24 @@ import javax.inject.Inject
|
|||
@Deprecated("Create repository instead")
|
||||
class PreferencesDataSource @Inject internal constructor(applicationContext: Context) {
|
||||
|
||||
val appRatingLaunchObserver: AppRatingLaunchObserver
|
||||
val usedCardsPrefStorage: UsedCardsPrefStorage
|
||||
val fiatCurrenciesPrefStorage: FiatCurrenciesPrefStorage
|
||||
val disclaimerPrefStorage: DisclaimerPrefStorage
|
||||
val toppedUpWalletStorage: ToppedUpWalletStorage
|
||||
|
||||
private val preferences: SharedPreferences =
|
||||
applicationContext.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE)
|
||||
|
||||
private val moshiConverter = MoshiJsonConverter(
|
||||
adapters = listOf(BigDecimalAdapter(), CardBalanceStateAdapter()) + MoshiJsonConverter.getTangemSdkAdapters(),
|
||||
adapters = listOf(BigDecimalAdapter()) + MoshiJsonConverter.getTangemSdkAdapters(),
|
||||
typedAdapters = MoshiJsonConverter.getTangemSdkTypedAdapters(),
|
||||
)
|
||||
|
||||
init {
|
||||
incrementLaunchCounter()
|
||||
appRatingLaunchObserver = AppRatingLaunchObserver(preferences, getCountOfLaunches())
|
||||
usedCardsPrefStorage = UsedCardsPrefStorage(preferences, moshiConverter)
|
||||
usedCardsPrefStorage.migrate()
|
||||
fiatCurrenciesPrefStorage = FiatCurrenciesPrefStorage(preferences, moshiConverter)
|
||||
fiatCurrenciesPrefStorage.migrate()
|
||||
disclaimerPrefStorage = DisclaimerPrefStorage(preferences)
|
||||
toppedUpWalletStorage = ToppedUpWalletStorage(preferences, moshiConverter)
|
||||
}
|
||||
|
||||
var sprinklrFirstLaunchTime: Long?
|
||||
get() = preferences.getLong(SPRINKLR_FIRST_LAUNCH_KEY, 0).takeIf { it != 0L }
|
||||
set(value) = preferences.edit { putLong(SPRINKLR_FIRST_LAUNCH_KEY, value ?: 0) }
|
||||
|
||||
var shouldShowSaveUserWalletScreen: Boolean
|
||||
get() = preferences.getBoolean(SAVE_WALLET_DIALOG_SHOWN_KEY, true)
|
||||
set(value) = preferences.edit {
|
||||
|
|
@ -78,8 +64,6 @@ class PreferencesDataSource @Inject internal constructor(applicationContext: Con
|
|||
return preferences.getBoolean(TWINS_ONBOARDING_SHOWN_KEY, false)
|
||||
}
|
||||
|
||||
private fun getCountOfLaunches(): Int = preferences.getInt(APP_LAUNCH_COUNT_KEY, 1)
|
||||
|
||||
private fun incrementLaunchCounter() {
|
||||
var count = preferences.getInt(APP_LAUNCH_COUNT_KEY, 0)
|
||||
preferences.edit { putInt(APP_LAUNCH_COUNT_KEY, ++count) }
|
||||
|
|
@ -89,7 +73,6 @@ class PreferencesDataSource @Inject internal constructor(applicationContext: Con
|
|||
private const val PREFERENCES_NAME = "tapPrefs"
|
||||
private const val TWINS_ONBOARDING_SHOWN_KEY = "twinsOnboardingShown"
|
||||
private const val APP_LAUNCH_COUNT_KEY = "launchCount"
|
||||
private const val SPRINKLR_FIRST_LAUNCH_KEY = "sprinklrFirstLaunch"
|
||||
private const val SAVE_WALLET_DIALOG_SHOWN_KEY = "saveUserWalletShown"
|
||||
private const val SAVE_ACCESS_CODES_KEY = "saveAccessCodes"
|
||||
private const val APPLICATION_STOPPED_KEY = "applicationStopped"
|
||||
|
|
|
|||
|
|
@ -1,20 +0,0 @@
|
|||
package com.tangem.data.source.preferences.adapters
|
||||
|
||||
import com.squareup.moshi.FromJson
|
||||
import com.squareup.moshi.ToJson
|
||||
import com.tangem.data.source.preferences.model.DataSourceTopupInfo
|
||||
|
||||
class CardBalanceStateAdapter {
|
||||
|
||||
@ToJson
|
||||
fun toJson(src: DataSourceTopupInfo.CardBalanceState): String = src.serializedName
|
||||
|
||||
@FromJson
|
||||
fun fromJson(json: String): DataSourceTopupInfo.CardBalanceState {
|
||||
return when (json) {
|
||||
DataSourceTopupInfo.CardBalanceState.Empty.serializedName -> DataSourceTopupInfo.CardBalanceState.Empty
|
||||
DataSourceTopupInfo.CardBalanceState.Full.serializedName -> DataSourceTopupInfo.CardBalanceState.Full
|
||||
else -> error("CardBalanceState not found")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
package com.tangem.data.source.preferences.model
|
||||
|
||||
data class DataSourceCurrency(
|
||||
val id: String,
|
||||
val code: String,
|
||||
val name: String,
|
||||
val rateBTC: String,
|
||||
val unit: String,
|
||||
val type: String,
|
||||
)
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
package com.tangem.data.source.preferences.model
|
||||
|
||||
data class DataSourceFiatCurrency(
|
||||
val code: String,
|
||||
val name: String,
|
||||
val symbol: String,
|
||||
)
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
package com.tangem.data.source.preferences.model
|
||||
|
||||
data class DataSourceTopupInfo(
|
||||
val walletId: String,
|
||||
val cardBalanceState: CardBalanceState,
|
||||
) {
|
||||
enum class CardBalanceState(val serializedName: String) {
|
||||
Empty(serializedName = "Empty"),
|
||||
Full(serializedName = "Full"),
|
||||
CustomToken(serializedName = "Custom token"),
|
||||
BlockchainError(serializedName = "Blockchain error"),
|
||||
NoRate(serializedName = "No rate"),
|
||||
}
|
||||
}
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
package com.tangem.data.source.preferences.storage
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import androidx.core.content.edit
|
||||
import com.tangem.common.json.MoshiJsonConverter
|
||||
import com.tangem.data.source.preferences.model.DataSourceCurrency
|
||||
import com.tangem.data.source.preferences.model.DataSourceFiatCurrency
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Deprecated("Create repository instead")
|
||||
class FiatCurrenciesPrefStorage internal constructor(
|
||||
private val preferences: SharedPreferences,
|
||||
private val converter: MoshiJsonConverter,
|
||||
) {
|
||||
fun migrate() {
|
||||
preferences.edit(true) {
|
||||
remove(FIAT_CURRENCIES_KEY_OLD)
|
||||
remove(APP_CURRENCY_KEY_OLD)
|
||||
}
|
||||
}
|
||||
|
||||
fun getAppCurrency(): DataSourceFiatCurrency? {
|
||||
val json = preferences.getString(APP_CURRENCY_KEY, "")
|
||||
if (json.isNullOrBlank()) return null
|
||||
|
||||
return converter.fromJson(json)
|
||||
}
|
||||
|
||||
fun saveAppCurrency(fiatCurrency: DataSourceFiatCurrency) {
|
||||
val json = converter.toJson(fiatCurrency)
|
||||
preferences.edit { putString(APP_CURRENCY_KEY, json) }
|
||||
}
|
||||
|
||||
fun save(currencies: List<DataSourceCurrency>) {
|
||||
val json: String = converter.toJson(currencies)
|
||||
return preferences.edit().putString(FIAT_CURRENCIES_KEY, json).apply()
|
||||
}
|
||||
|
||||
fun restore(): List<DataSourceCurrency> {
|
||||
val json = preferences.getString(FIAT_CURRENCIES_KEY, "")
|
||||
val type = converter.typedList(DataSourceCurrency::class.java)
|
||||
if (json.isNullOrBlank()) return emptyList()
|
||||
|
||||
return converter.fromJson(json, type) ?: emptyList()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val FIAT_CURRENCIES_KEY_OLD = "fiatCurrencies"
|
||||
private const val APP_CURRENCY_KEY_OLD = "appCurrency"
|
||||
|
||||
private const val FIAT_CURRENCIES_KEY = "fiatCurrencies_v2"
|
||||
private const val APP_CURRENCY_KEY = "appCurrency_v2"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
package com.tangem.data.source.preferences.storage
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import androidx.core.content.edit
|
||||
import com.tangem.common.json.MoshiJsonConverter
|
||||
import com.tangem.data.source.preferences.model.DataSourceTopupInfo
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Deprecated("Create repository instead")
|
||||
class ToppedUpWalletStorage internal constructor(
|
||||
private val preferences: SharedPreferences,
|
||||
private val jsonConverter: MoshiJsonConverter,
|
||||
) {
|
||||
|
||||
private val walletList: MutableSet<DataSourceTopupInfo> = mutableSetOf()
|
||||
|
||||
init {
|
||||
walletList.addAll(restore())
|
||||
}
|
||||
|
||||
fun save(userWalletInfo: DataSourceTopupInfo): Boolean {
|
||||
walletList.removeAll { it.walletId == userWalletInfo.walletId }
|
||||
walletList.add(userWalletInfo)
|
||||
return save(walletList)
|
||||
}
|
||||
|
||||
fun restore(walletId: String): DataSourceTopupInfo? {
|
||||
return walletList.firstOrNull { it.walletId == walletId }
|
||||
}
|
||||
|
||||
private fun save(userWallets: MutableSet<DataSourceTopupInfo>): Boolean {
|
||||
return try {
|
||||
val json = jsonConverter.toJson(userWallets)
|
||||
preferences.edit(true) { putString(KEY, json) }
|
||||
true
|
||||
} catch (ex: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun restore(): MutableSet<DataSourceTopupInfo> {
|
||||
val json = preferences.getString(KEY, null) ?: return mutableSetOf()
|
||||
return try {
|
||||
val typedList = jsonConverter.typedList(DataSourceTopupInfo::class.java)
|
||||
val listData = jsonConverter.fromJson<List<DataSourceTopupInfo>>(json, typedList)!!
|
||||
listData.toMutableSet()
|
||||
} catch (ex: Exception) {
|
||||
preferences.edit(true) { remove(KEY) }
|
||||
mutableSetOf()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val KEY = "userWalletsInfo"
|
||||
}
|
||||
}
|
||||
|
|
@ -102,4 +102,13 @@ internal object TokensDataModule {
|
|||
quotesRepository = quotesRepository,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideCardNetworksRepository(
|
||||
userWalletsStore: UserWalletsStore,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): NetworksCompatibilityRepository {
|
||||
return DefaultNetworksCompatibilityRepository(userWalletsStore = userWalletsStore, dispatchers = dispatchers)
|
||||
}
|
||||
}
|
||||
|
|
@ -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 ->
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ internal object CoinsResponseConverter : Converter<CoinsData, List<Token>> {
|
|||
Token.Network(
|
||||
networkId = network.networkId,
|
||||
standardType = getNetworkStandardType(blockchain).name,
|
||||
name = blockchain.fullName,
|
||||
address = network.contractAddress,
|
||||
iconUrl = getIconUrl(network.networkId, value.imageHost),
|
||||
decimalCount = network.decimalCount?.toInt(),
|
||||
|
|
|
|||
|
|
@ -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,89 @@
|
|||
package com.tangem.data.tokens.repository
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.data.tokens.utils.getNetwork
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.common.extensions.*
|
||||
import com.tangem.domain.common.util.cardTypesResolver
|
||||
import com.tangem.domain.common.util.derivationStyleProvider
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.tokens.repository.NetworksCompatibilityRepository
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
internal class DefaultNetworksCompatibilityRepository(
|
||||
private val userWalletsStore: UserWalletsStore,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : NetworksCompatibilityRepository {
|
||||
|
||||
/**
|
||||
* @return returns true if either the network is not Solana (check is not relevant) or if it is Solana and
|
||||
* UserWallet supports tokens on Solana Network
|
||||
*/
|
||||
@Throws(IllegalArgumentException::class)
|
||||
override suspend fun areSolanaTokensSupportedIfRelevant(networkId: String, userWalletId: UserWalletId): Boolean {
|
||||
return withContext(dispatchers.io) {
|
||||
val scanResponse = getWalletOrThrow(userWalletId).scanResponse
|
||||
val blockchain = getBlockchainOrThrow(networkId)
|
||||
val blockchainsSupportingTokens = scanResponse.card.supportedTokens(scanResponse.cardTypesResolver)
|
||||
blockchain != Blockchain.Solana || blockchainsSupportingTokens.contains(Blockchain.Solana)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IllegalArgumentException::class)
|
||||
override suspend fun areTokensSupportedByNetwork(networkId: String, userWalletId: UserWalletId): Boolean {
|
||||
return withContext(dispatchers.io) {
|
||||
val scanResponse = getWalletOrThrow(userWalletId).scanResponse
|
||||
val blockchain = getBlockchainOrThrow(networkId)
|
||||
val blockchainsSupportingTokens = scanResponse.card.supportedTokens(scanResponse.cardTypesResolver)
|
||||
scanResponse.card.canHandleToken(
|
||||
supportedTokens = blockchainsSupportingTokens,
|
||||
blockchain = blockchain,
|
||||
cardTypesResolver = scanResponse.cardTypesResolver,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IllegalArgumentException::class)
|
||||
override suspend fun isNetworkSupported(networkId: String, userWalletId: UserWalletId): Boolean {
|
||||
return withContext(dispatchers.io) {
|
||||
val scanResponse = getWalletOrThrow(userWalletId).scanResponse
|
||||
val blockchain = getBlockchainOrThrow(networkId)
|
||||
scanResponse.card.canHandleBlockchain(
|
||||
blockchain = blockchain,
|
||||
cardTypesResolver = scanResponse.cardTypesResolver,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IllegalArgumentException::class)
|
||||
override suspend fun getSupportedNetworks(userWalletId: UserWalletId): List<Network> {
|
||||
val scanResponse = getWalletOrThrow(userWalletId).scanResponse
|
||||
return Blockchain.values()
|
||||
.filter { blockchain ->
|
||||
scanResponse.card.supportedBlockchains(scanResponse.cardTypesResolver).contains(blockchain)
|
||||
}
|
||||
.sortedBy(Blockchain::fullName)
|
||||
.mapNotNull { blockchain ->
|
||||
getNetwork(blockchain, null, scanResponse.derivationStyleProvider)
|
||||
}
|
||||
}
|
||||
|
||||
override fun areTokensSupportedByNetwork(networkId: String): Boolean {
|
||||
return Blockchain.fromNetworkId(networkId)?.canHandleTokens() ?: false
|
||||
}
|
||||
|
||||
private suspend fun getWalletOrThrow(userWalletId: UserWalletId): UserWallet {
|
||||
return requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) {
|
||||
"Requested UserWallet not found"
|
||||
}
|
||||
}
|
||||
|
||||
private fun getBlockchainOrThrow(networkId: String): Blockchain {
|
||||
return requireNotNull(Blockchain.fromNetworkId(networkId)) {
|
||||
"Requested network not found"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -186,6 +186,7 @@ internal class DefaultNetworksRepository(
|
|||
is UpdateWalletManagerResult.NoAccount,
|
||||
-> Unit
|
||||
is UpdateWalletManagerResult.Unreachable,
|
||||
is UpdateWalletManagerResult.UnreachableWithoutAddresses,
|
||||
is UpdateWalletManagerResult.MissedDerivation,
|
||||
-> {
|
||||
Timber.w(
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import com.tangem.data.tokens.utils.QuotesConverter
|
|||
import com.tangem.data.tokens.utils.QuotesUnsupportedCurrenciesIdAdapter
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse
|
||||
import com.tangem.datasource.local.*
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys
|
||||
import com.tangem.datasource.local.preferences.utils.getObject
|
||||
|
|
|
|||
|
|
@ -3,13 +3,19 @@ package com.tangem.data.tokens.repository
|
|||
import androidx.paging.Pager
|
||||
import androidx.paging.PagingConfig
|
||||
import androidx.paging.PagingData
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.data.tokens.paging.CoinsPagingSource
|
||||
import com.tangem.data.tokens.utils.FoundTokenConverter
|
||||
import com.tangem.datasource.api.common.response.getOrThrow
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.domain.common.extensions.fromNetworkId
|
||||
import com.tangem.domain.tokens.model.FoundToken
|
||||
import com.tangem.domain.tokens.model.Token
|
||||
import com.tangem.domain.tokens.repository.QuotesRepository
|
||||
import com.tangem.domain.tokens.repository.TokensListRepository
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
* Default repository implementation for managing operations related to a complete set of tokens
|
||||
|
|
@ -42,4 +48,32 @@ internal class DefaultTokensListRepository(
|
|||
},
|
||||
).flow
|
||||
}
|
||||
|
||||
override suspend fun findToken(contractAddress: String, networkId: String): FoundToken? {
|
||||
return withContext(dispatchers.io) {
|
||||
val foundCoin = tangemTechApi.getCoins(
|
||||
contractAddress = contractAddress,
|
||||
networkIds = networkId,
|
||||
).getOrThrow().coins.firstNotNullOfOrNull { coin ->
|
||||
val tokenNetwork = coin.networks.filter { network ->
|
||||
network.contractAddress != null && network.decimalCount != null &&
|
||||
network.contractAddress?.equals(contractAddress, ignoreCase = true) == true &&
|
||||
networkId == network.networkId
|
||||
}
|
||||
if (tokenNetwork.isNotEmpty()) {
|
||||
coin.copy(networks = tokenNetwork)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
foundCoin?.let { FoundTokenConverter.convert(foundCoin) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun validateAddress(contractAddress: String, networkId: String): Boolean {
|
||||
return when (val blockchain = Blockchain.fromNetworkId(networkId) ?: Blockchain.Unknown) {
|
||||
Blockchain.Unknown, Blockchain.Binance, Blockchain.BinanceTestnet -> true
|
||||
else -> blockchain.validateAddress(contractAddress)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package com.tangem.data.tokens.utils
|
|||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.domain.common.DerivationStyleProvider
|
||||
import com.tangem.domain.common.extensions.fromNetworkId
|
||||
import com.tangem.domain.common.extensions.toCoinId
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import timber.log.Timber
|
||||
|
|
@ -57,4 +58,43 @@ class CryptoCurrencyFactory {
|
|||
isCustom = isCustomCoin(network),
|
||||
)
|
||||
}
|
||||
|
||||
fun createCoin(
|
||||
networkId: String,
|
||||
extraDerivationPath: String?,
|
||||
derivationStyleProvider: DerivationStyleProvider,
|
||||
): CryptoCurrency.Coin? {
|
||||
val blockchain = Blockchain.fromNetworkId(networkId) ?: Blockchain.Unknown
|
||||
return createCoin(blockchain, extraDerivationPath, derivationStyleProvider)
|
||||
}
|
||||
|
||||
fun createToken(
|
||||
token: Token,
|
||||
networkId: String,
|
||||
extraDerivationPath: String?,
|
||||
derivationStyleProvider: DerivationStyleProvider,
|
||||
): CryptoCurrency.Token? {
|
||||
val sdkToken = SdkToken(
|
||||
name = token.name,
|
||||
symbol = token.symbol,
|
||||
contractAddress = token.contractAddress,
|
||||
decimals = token.decimals,
|
||||
id = token.id,
|
||||
)
|
||||
val blockchain = Blockchain.fromNetworkId(networkId) ?: Blockchain.Unknown
|
||||
return createToken(
|
||||
sdkToken = sdkToken,
|
||||
blockchain = blockchain,
|
||||
extraDerivationPath = extraDerivationPath,
|
||||
derivationStyleProvider = derivationStyleProvider,
|
||||
)
|
||||
}
|
||||
|
||||
data class Token(
|
||||
val name: String,
|
||||
val symbol: String,
|
||||
val contractAddress: String,
|
||||
val decimals: Int,
|
||||
val id: String? = null,
|
||||
)
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.tangem.data.tokens.utils
|
||||
|
||||
import com.tangem.datasource.api.tangemTech.models.CoinsResponse
|
||||
import com.tangem.domain.tokens.model.FoundToken
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
internal object FoundTokenConverter : Converter<CoinsResponse.Coin, FoundToken> {
|
||||
|
||||
override fun convert(value: CoinsResponse.Coin): FoundToken {
|
||||
return FoundToken(
|
||||
id = value.id,
|
||||
name = value.name,
|
||||
symbol = value.symbol,
|
||||
contractAddress = requireNotNull(value.networks.first().contractAddress),
|
||||
decimals = requireNotNull(value.networks.first().decimalCount).intValueExact(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -19,7 +19,10 @@ internal class NetworkStatusFactory {
|
|||
network = network,
|
||||
value = when (result) {
|
||||
is UpdateWalletManagerResult.MissedDerivation -> NetworkStatus.MissedDerivation
|
||||
is UpdateWalletManagerResult.Unreachable -> NetworkStatus.Unreachable
|
||||
is UpdateWalletManagerResult.UnreachableWithoutAddresses -> NetworkStatus.UnreachableWithoutAddresses
|
||||
is UpdateWalletManagerResult.Unreachable -> NetworkStatus.Unreachable(
|
||||
address = getNetworkAddress(result.selectedAddress, result.addresses),
|
||||
)
|
||||
is UpdateWalletManagerResult.NoAccount -> NetworkStatus.NoAccount(
|
||||
address = getNetworkAddress(result.selectedAddress, result.addresses),
|
||||
amountToCreateAccount = result.amountToCreateAccount,
|
||||
|
|
|
|||
1
data/transaction/.gitignore
vendored
Normal file
1
data/transaction/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
30
data/transaction/build.gradle.kts
Normal file
30
data/transaction/build.gradle.kts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
alias(deps.plugins.kotlin.kapt)
|
||||
alias(deps.plugins.hilt.android)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.tangem.data.transaction"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
/** Tangem SDKs */
|
||||
implementation(deps.tangem.blockchain)
|
||||
|
||||
/** Core */
|
||||
implementation(projects.core.utils)
|
||||
|
||||
/** Domain */
|
||||
implementation(projects.domain.transaction)
|
||||
implementation(projects.domain.legacy)
|
||||
implementation(projects.domain.wallets.models)
|
||||
implementation(projects.domain.tokens.models)
|
||||
|
||||
/** DI */
|
||||
implementation(deps.hilt.android)
|
||||
kapt(deps.hilt.kapt)
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
package com.tangem.data.transaction
|
||||
|
||||
import androidx.core.text.isDigitsOnly
|
||||
import com.tangem.blockchain.blockchains.binance.BinanceTransactionExtras
|
||||
import com.tangem.blockchain.blockchains.cosmos.CosmosTransactionExtras
|
||||
import com.tangem.blockchain.blockchains.stellar.StellarMemo
|
||||
import com.tangem.blockchain.blockchains.stellar.StellarTransactionExtras
|
||||
import com.tangem.blockchain.blockchains.ton.TonTransactionExtras
|
||||
import com.tangem.blockchain.blockchains.xrp.XrpTransactionBuilder
|
||||
import com.tangem.blockchain.common.Amount
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.TransactionData
|
||||
import com.tangem.blockchain.common.TransactionExtras
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.transaction.TransactionRepository
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
internal class DefaultTransactionRepository(
|
||||
private val walletManagersFacade: WalletManagersFacade,
|
||||
private val coroutineDispatcherProvider: CoroutineDispatcherProvider,
|
||||
) : TransactionRepository {
|
||||
|
||||
override suspend fun createTransaction(
|
||||
amount: Amount,
|
||||
fee: Fee,
|
||||
memo: String?,
|
||||
destination: String,
|
||||
userWalletId: UserWalletId,
|
||||
network: Network,
|
||||
): TransactionData? = withContext(coroutineDispatcherProvider.io) {
|
||||
val blockchain = Blockchain.fromId(network.id.value)
|
||||
val walletManager = walletManagersFacade.getOrCreateWalletManager(
|
||||
userWalletId = userWalletId,
|
||||
blockchain = blockchain,
|
||||
derivationPath = network.derivationPath.value,
|
||||
)
|
||||
|
||||
return@withContext walletManager?.createTransaction(amount, fee, destination)?.copy(
|
||||
extras = getMemoExtras(network.id.value, memo),
|
||||
)
|
||||
}
|
||||
|
||||
private fun getMemoExtras(networkId: String, memo: String?): TransactionExtras? {
|
||||
val blockchain = Blockchain.fromId(networkId)
|
||||
if (memo == null) return null
|
||||
return when (blockchain) {
|
||||
Blockchain.Stellar -> {
|
||||
val xmlMemo = when {
|
||||
memo.isNotEmpty() && memo.isDigitsOnly() -> StellarMemo.Id(memo.toBigInteger())
|
||||
else -> StellarMemo.Text(memo)
|
||||
}
|
||||
StellarTransactionExtras(xmlMemo)
|
||||
}
|
||||
Blockchain.Binance -> BinanceTransactionExtras(memo)
|
||||
Blockchain.XRP -> memo.toLongOrNull()?.let { XrpTransactionBuilder.XrpTransactionExtras(it) }
|
||||
Blockchain.Cosmos -> CosmosTransactionExtras(memo)
|
||||
Blockchain.TON -> TonTransactionExtras(memo)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package com.tangem.data.transaction.di
|
||||
|
||||
import com.tangem.data.transaction.DefaultTransactionRepository
|
||||
import com.tangem.domain.transaction.TransactionRepository
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
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 TransactionDataModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesTransactionRepository(
|
||||
walletManagersFacade: WalletManagersFacade,
|
||||
coroutineDispatcherProvider: CoroutineDispatcherProvider,
|
||||
): TransactionRepository {
|
||||
return DefaultTransactionRepository(
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
coroutineDispatcherProvider = coroutineDispatcherProvider,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -66,7 +66,12 @@ class DefaultTxHistoryRepository(
|
|||
|
||||
override fun getTxExploreUrl(txHash: String, networkId: Network.ID): String {
|
||||
val blockchain = Blockchain.fromId(networkId.value)
|
||||
return blockchain.getExploreTxUrl(txHash)
|
||||
// TODO: Fix ton tx urls [REDACTED_TASK_KEY]
|
||||
return if (blockchain == Blockchain.TON || blockchain == Blockchain.TONTestnet) {
|
||||
""
|
||||
} else {
|
||||
blockchain.getExploreTxUrl(txHash)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getUserWallet(userWalletId: UserWalletId): UserWallet {
|
||||
|
|
|
|||
|
|
@ -2,8 +2,10 @@ package com.tangem.data.txhistory.repository.paging
|
|||
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.paging.PagingState
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.data.common.cache.CacheRegistry
|
||||
import com.tangem.datasource.local.txhistory.TxHistoryItemsStore
|
||||
import com.tangem.domain.common.extensions.fromNetworkId
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.txhistory.models.PaginationWrapper
|
||||
import com.tangem.domain.txhistory.models.TxHistoryItem
|
||||
|
|
@ -20,6 +22,8 @@ internal class TxHistoryPagingSource(
|
|||
|
||||
private val storeKey = TxHistoryItemsStore.Key(sourceParams.userWalletId, sourceParams.currency)
|
||||
|
||||
override val keyReuseSupported: Boolean get() = true
|
||||
|
||||
override fun getRefreshKey(state: PagingState<Int, TxHistoryItem>): Int? {
|
||||
return state.anchorPosition?.let { anchorPosition ->
|
||||
val anchorPage = state.closestPageToPosition(anchorPosition)
|
||||
|
|
@ -70,14 +74,54 @@ internal class TxHistoryPagingSource(
|
|||
}
|
||||
|
||||
private suspend fun fetch(pageToLoad: Int, pageSize: Int) {
|
||||
val wrappedItems = walletManagersFacade.getTxHistoryItems(
|
||||
var wrappedItems = walletManagersFacade.getTxHistoryItems(
|
||||
userWalletId = sourceParams.userWalletId,
|
||||
currency = sourceParams.currency,
|
||||
page = pageToLoad,
|
||||
pageSize = pageSize,
|
||||
)
|
||||
|
||||
txHistoryItemsStore.store(storeKey, wrappedItems)
|
||||
if (pageToLoad == 1) {
|
||||
wrappedItems = wrappedItems.addRecentTransactions()
|
||||
}
|
||||
|
||||
txHistoryItemsStore.store(key = storeKey, value = wrappedItems)
|
||||
}
|
||||
|
||||
private suspend fun PaginationWrapper<TxHistoryItem>.addRecentTransactions(): PaginationWrapper<TxHistoryItem> {
|
||||
val recentTxHistoryItems = Blockchain.fromNetworkId(sourceParams.currency.network.backendId)?.let {
|
||||
walletManagersFacade.getRecentTransactions(
|
||||
userWalletId = sourceParams.userWalletId,
|
||||
blockchain = it,
|
||||
derivationPath = sourceParams.currency.network.derivationPath.value,
|
||||
)
|
||||
.filterUnconfirmedTransaction()
|
||||
.filterIfApiKnowsAboutTx(apiItems = items)
|
||||
} ?: emptyList()
|
||||
|
||||
return if (recentTxHistoryItems.isEmpty()) {
|
||||
Timber.d("Nothing to add to TxHistory")
|
||||
this
|
||||
} else {
|
||||
Timber.d(
|
||||
"Recent transactions were added to TxHistory: %s",
|
||||
recentTxHistoryItems.joinToString(
|
||||
prefix = "[",
|
||||
postfix = "]",
|
||||
transform = TxHistoryItem::txHash,
|
||||
),
|
||||
)
|
||||
|
||||
return copy(items = recentTxHistoryItems + items)
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<TxHistoryItem>.filterUnconfirmedTransaction(): List<TxHistoryItem> {
|
||||
return filter { it.status == TxHistoryItem.TransactionStatus.Unconfirmed }
|
||||
}
|
||||
|
||||
private fun List<TxHistoryItem>.filterIfApiKnowsAboutTx(apiItems: List<TxHistoryItem>): List<TxHistoryItem> {
|
||||
return filter { item -> apiItems.none { it.txHash == item.txHash } }
|
||||
}
|
||||
|
||||
private fun getTxHistoryPageKey(page: Int): String {
|
||||
|
|
|
|||
|
|
@ -11,14 +11,27 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
/** Tangem libraries */
|
||||
implementation(deps.tangem.blockchain) // android-library
|
||||
|
||||
/** Core */
|
||||
implementation(projects.core.datasource)
|
||||
implementation(projects.core.utils)
|
||||
|
||||
/** Domain */
|
||||
implementation(projects.domain.wallets)
|
||||
|
||||
/** Domain models */
|
||||
implementation(projects.domain.wallets.models)
|
||||
implementation(projects.domain.tokens.models)
|
||||
|
||||
/** DI */
|
||||
implementation(deps.hilt.android)
|
||||
implementation(project(":domain:legacy"))
|
||||
kapt(deps.hilt.kapt)
|
||||
|
||||
/** Local storages */
|
||||
/** Other deps */
|
||||
implementation(deps.androidx.datastore)
|
||||
implementation(deps.arrow.core)
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
package com.tangem.data.wallets
|
||||
|
||||
import androidx.core.text.isDigitsOnly
|
||||
import com.tangem.blockchain.blockchains.near.NearWalletManager
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.domain.wallets.repository.WalletAddressServiceRepository
|
||||
import java.math.BigInteger
|
||||
|
||||
class DefaultWalletAddressServiceRepository(
|
||||
private val walletManagersFacade: WalletManagersFacade,
|
||||
) : WalletAddressServiceRepository {
|
||||
|
||||
override suspend fun validateAddress(userWalletId: UserWalletId, network: Network, address: String): Boolean {
|
||||
val blockchain = Blockchain.fromId(network.id.value)
|
||||
|
||||
return if (blockchain.isNear()) {
|
||||
val walletManager = walletManagersFacade.getOrCreateWalletManager(
|
||||
userWalletId = userWalletId,
|
||||
blockchain = blockchain,
|
||||
derivationPath = network.derivationPath.value,
|
||||
) ?: return false
|
||||
(walletManager as? NearWalletManager)?.validateAddress(address) ?: false
|
||||
} else {
|
||||
blockchain.validateAddress(address)
|
||||
}
|
||||
}
|
||||
|
||||
override fun validateMemo(network: Network, memo: String): Boolean {
|
||||
if (memo.isEmpty()) return true
|
||||
return when (network.id.value) {
|
||||
Blockchain.XRP.id -> {
|
||||
val tag = memo.toLongOrNull()
|
||||
tag != null && tag <= XRP_TAG_MAX_NUMBER
|
||||
}
|
||||
Blockchain.Stellar.id -> {
|
||||
isAssignableXlmValue(memo)
|
||||
}
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
|
||||
private fun Blockchain.isNear(): Boolean {
|
||||
return this == Blockchain.Near || this == Blockchain.NearTestnet
|
||||
}
|
||||
|
||||
private fun isAssignableXlmValue(value: String): Boolean {
|
||||
return when {
|
||||
value.isNotEmpty() && value.isDigitsOnly() -> {
|
||||
try {
|
||||
// from com.tangem.blockchain.blockchains.stellar.StellarMemo.toStellarSdkMemo
|
||||
value.toBigInteger() in BigInteger.ZERO..Long.MAX_VALUE.toBigInteger() * 2.toBigInteger()
|
||||
} catch (ex: NumberFormatException) {
|
||||
false
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
// from org.stellar.sdk.MemoText
|
||||
value.toByteArray().size <= XLM_MEMO_MAX_LENGTH
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val XLM_MEMO_MAX_LENGTH = 28
|
||||
private const val XRP_TAG_MAX_NUMBER = 4294967295
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,10 @@
|
|||
package com.tangem.data.wallets.di
|
||||
|
||||
import com.tangem.data.wallets.DefaultWalletAddressServiceRepository
|
||||
import com.tangem.data.wallets.DefaultWalletsRepository
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.domain.wallets.repository.WalletAddressServiceRepository
|
||||
import com.tangem.domain.wallets.repository.WalletsRepository
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
|
|
@ -18,4 +21,12 @@ internal object WalletsDataModule {
|
|||
fun providesWalletsRepository(appPreferencesStore: AppPreferencesStore): WalletsRepository {
|
||||
return DefaultWalletsRepository(appPreferencesStore = appPreferencesStore)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesWalletAddressServiceRepository(
|
||||
walletManagersFacade: WalletManagersFacade,
|
||||
): WalletAddressServiceRepository {
|
||||
return DefaultWalletAddressServiceRepository(walletManagersFacade)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue