Updated on 2026-08-14
This commit is contained in:
commit
6bcb15430c
971 changed files with 24716 additions and 13701 deletions
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.data.appcurrency
|
||||
|
||||
import com.tangem.data.appcurrency.utils.AppCurrencyConverter
|
||||
import com.tangem.data.common.api.safeApiCall
|
||||
import com.tangem.data.common.cache.CacheRegistry
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse
|
||||
|
|
@ -15,7 +16,6 @@ import kotlinx.coroutines.flow.map
|
|||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.joda.time.Duration
|
||||
import timber.log.Timber
|
||||
|
||||
internal class DefaultAppCurrencyRepository(
|
||||
private val tangemTechApi: TangemTechApi,
|
||||
|
|
@ -34,7 +34,7 @@ internal class DefaultAppCurrencyRepository(
|
|||
.collect(::send)
|
||||
}
|
||||
|
||||
launch(dispatchers.io) {
|
||||
withContext(dispatchers.io) {
|
||||
if (selectedAppCurrencyStore.isEmpty()) {
|
||||
fetchDefaultAppCurrency()
|
||||
}
|
||||
|
|
@ -47,6 +47,7 @@ internal class DefaultAppCurrencyRepository(
|
|||
|
||||
val currencies = availableAppCurrenciesStore.getAllSyncOrNull()
|
||||
?.map(appCurrencyConverter::convert)
|
||||
?.sortedBy(AppCurrency::name)
|
||||
|
||||
requireNotNull(currencies) {
|
||||
"No available currencies stored"
|
||||
|
|
@ -80,15 +81,15 @@ internal class DefaultAppCurrencyRepository(
|
|||
}
|
||||
|
||||
private suspend fun fetchAvailableCurrencies() {
|
||||
try {
|
||||
val response = tangemTechApi.getCurrencyList()
|
||||
val response = safeApiCall(
|
||||
call = { tangemTechApi.getCurrencyList().bind() },
|
||||
onError = {
|
||||
cacheRegistry.invalidate(AVAILABLE_CURRENCIES_CACHE_KEY)
|
||||
getDefaultCurrenciesResponse()
|
||||
},
|
||||
)
|
||||
|
||||
availableAppCurrenciesStore.store(response)
|
||||
} catch (e: Throwable) {
|
||||
Timber.e(e, "Unable to fetch available currencies")
|
||||
|
||||
availableAppCurrenciesStore.store(getDefaultCurrenciesResponse())
|
||||
}
|
||||
availableAppCurrenciesStore.store(response)
|
||||
}
|
||||
|
||||
private fun getDefaultCurrenciesResponse(): CurrenciesResponse = CurrenciesResponse(
|
||||
|
|
|
|||
|
|
@ -12,13 +12,11 @@ android {
|
|||
dependencies {
|
||||
|
||||
/** Project - Domain */
|
||||
implementation(projects.domain.core)
|
||||
implementation(projects.domain.appTheme)
|
||||
implementation(projects.domain.appTheme.models)
|
||||
|
||||
/** Project - Data */
|
||||
implementation(projects.core.datasource)
|
||||
implementation(projects.data.common)
|
||||
|
||||
/** Project - Utils */
|
||||
implementation(projects.core.utils)
|
||||
|
|
@ -29,6 +27,4 @@ dependencies {
|
|||
|
||||
/** Other */
|
||||
implementation(deps.kotlin.coroutines)
|
||||
implementation(deps.timber)
|
||||
implementation(deps.jodatime)
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package com.tangem.data.apptheme
|
||||
|
||||
import com.tangem.datasource.local.apptheme.AppThemeModeStore
|
||||
import com.tangem.domain.apptheme.model.AppThemeMode
|
||||
import com.tangem.domain.apptheme.repository.AppThemeModeRepository
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
internal class DefaultAppThemeModeRepository(
|
||||
private val appThemeModeStore: AppThemeModeStore,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : AppThemeModeRepository {
|
||||
|
||||
override fun getAppThemeMode(): Flow<AppThemeMode> {
|
||||
return channelFlow {
|
||||
launch(dispatchers.io) {
|
||||
if (appThemeModeStore.isEmpty()) {
|
||||
appThemeModeStore.store(AppThemeMode.DEFAULT)
|
||||
}
|
||||
}
|
||||
|
||||
launch(dispatchers.io) {
|
||||
appThemeModeStore.get().collect(::send)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun changeAppThemeMode(mode: AppThemeMode) {
|
||||
withContext(dispatchers.io) {
|
||||
appThemeModeStore.store(mode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
package com.tangem.data.apptheme
|
||||
|
||||
import com.tangem.domain.apptheme.model.AppThemeMode
|
||||
import com.tangem.domain.apptheme.repository.AppThemeModeRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
internal class MockAppThemeModeRepository : AppThemeModeRepository {
|
||||
|
||||
private val appThemeModeFlow = MutableStateFlow(AppThemeMode.DEFAULT)
|
||||
|
||||
override fun getAppThemeMode(): Flow<AppThemeMode> {
|
||||
return appThemeModeFlow
|
||||
}
|
||||
|
||||
override suspend fun changeAppThemeMode(mode: AppThemeMode) {
|
||||
appThemeModeFlow.value = mode
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,9 @@
|
|||
package com.tangem.data.apptheme.di
|
||||
|
||||
import com.tangem.data.apptheme.MockAppThemeModeRepository
|
||||
import com.tangem.data.apptheme.DefaultAppThemeModeRepository
|
||||
import com.tangem.datasource.local.apptheme.AppThemeModeStore
|
||||
import com.tangem.domain.apptheme.repository.AppThemeModeRepository
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -14,7 +16,10 @@ internal object AppThemeModeDataModule {
|
|||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideAppThemeModeRepository(): AppThemeModeRepository {
|
||||
return MockAppThemeModeRepository()
|
||||
fun provideAppThemeModeRepository(
|
||||
appThemeModeStore: AppThemeModeStore,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): AppThemeModeRepository {
|
||||
return DefaultAppThemeModeRepository(appThemeModeStore, dispatchers)
|
||||
}
|
||||
}
|
||||
1
data/balance-hiding/.gitignore
vendored
Normal file
1
data/balance-hiding/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
27
data/balance-hiding/build.gradle.kts
Normal file
27
data/balance-hiding/build.gradle.kts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
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.balancehiding"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
/** DI */
|
||||
implementation(deps.hilt.android)
|
||||
|
||||
kapt(deps.hilt.kapt)
|
||||
|
||||
implementation(deps.kotlin.coroutines)
|
||||
|
||||
implementation(projects.core.utils)
|
||||
implementation(projects.core.datasource)
|
||||
|
||||
implementation(projects.domain.balanceHiding)
|
||||
implementation(projects.domain.balanceHiding.models)
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
package com.tangem.data.balancehiding
|
||||
|
||||
import com.tangem.datasource.local.appcurrency.BalanceHidingSettingsStore
|
||||
import com.tangem.domain.balancehiding.BalanceHidingSettings
|
||||
import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
internal class DefaultBalanceHidingRepository(
|
||||
private val balanceHidingSettingsStore: BalanceHidingSettingsStore,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : BalanceHidingRepository {
|
||||
|
||||
override fun getBalanceHidingSettingsFlow(): Flow<BalanceHidingSettings> {
|
||||
return balanceHidingSettingsStore.get()
|
||||
.onStart { emit(getBalanceHidingSettings()) }
|
||||
.flowOn(dispatchers.io)
|
||||
.distinctUntilChanged()
|
||||
}
|
||||
|
||||
override suspend fun storeBalanceHidingSettings(balanceHidingSettings: BalanceHidingSettings) {
|
||||
withContext(dispatchers.io) {
|
||||
balanceHidingSettingsStore.store(balanceHidingSettings)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getBalanceHidingSettings(): BalanceHidingSettings {
|
||||
return withContext(dispatchers.io) {
|
||||
balanceHidingSettingsStore.getSyncOrDefault()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package com.tangem.data.balancehiding
|
||||
|
||||
import android.content.Context
|
||||
import android.hardware.Sensor
|
||||
import android.hardware.SensorManager
|
||||
import com.tangem.domain.balancehiding.DeviceFlipDetector
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
|
||||
internal class DefaultDeviceFlipDetector(context: Context) : DeviceFlipDetector {
|
||||
|
||||
private val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
|
||||
private var gravitySensor = sensorManager.getDefaultSensor(Sensor.TYPE_GRAVITY)
|
||||
|
||||
override fun getDeviceFlipFlow(): Flow<Unit> = callbackFlow {
|
||||
val listener = FlipListener { trySend(Unit) }
|
||||
|
||||
gravitySensor?.let {
|
||||
sensorManager.registerListener(listener, it, SensorManager.SENSOR_DELAY_NORMAL)
|
||||
}
|
||||
|
||||
awaitClose { sensorManager.unregisterListener(listener) }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package com.tangem.data.balancehiding
|
||||
|
||||
import android.hardware.Sensor
|
||||
import android.hardware.SensorEvent
|
||||
import android.hardware.SensorEventListener
|
||||
import android.os.SystemClock
|
||||
|
||||
internal class FlipListener(private val action: () -> Unit) : SensorEventListener {
|
||||
|
||||
private val zAxisThreshold = -6
|
||||
private val throttleTimeMs = 3000
|
||||
private var lastTriggerTime = 0L
|
||||
private var isScreenDown = false
|
||||
|
||||
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
|
||||
/* no-op */
|
||||
}
|
||||
|
||||
override fun onSensorChanged(event: SensorEvent?) {
|
||||
event?.let {
|
||||
val currentTime = SystemClock.elapsedRealtime()
|
||||
val zAxisValue = it.values[2]
|
||||
|
||||
if (zAxisValue < zAxisThreshold && !isScreenDown) {
|
||||
isScreenDown = true
|
||||
lastTriggerTime = currentTime
|
||||
// TODO add module logging
|
||||
// Timber.tag("onSensorChanged").d("screen down")
|
||||
} else if (zAxisValue >= zAxisThreshold) {
|
||||
if (isScreenDown && currentTime - lastTriggerTime <= throttleTimeMs) {
|
||||
// Timber.tag("onSensorChanged").d("screen up!")
|
||||
lastTriggerTime = currentTime
|
||||
action.invoke()
|
||||
}
|
||||
isScreenDown = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
package com.tangem.data.balancehiding.di
|
||||
|
||||
import android.content.Context
|
||||
import com.tangem.data.balancehiding.DefaultBalanceHidingRepository
|
||||
import com.tangem.data.balancehiding.DefaultDeviceFlipDetector
|
||||
import com.tangem.datasource.local.appcurrency.BalanceHidingSettingsStore
|
||||
import com.tangem.domain.balancehiding.DeviceFlipDetector
|
||||
import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository
|
||||
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
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object BalanceHidingModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideBalanceHidingRepository(
|
||||
balanceHidingSettingsStore: BalanceHidingSettingsStore,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): BalanceHidingRepository {
|
||||
return DefaultBalanceHidingRepository(
|
||||
balanceHidingSettingsStore = balanceHidingSettingsStore,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideFlipDetector(@ApplicationContext context: Context): DeviceFlipDetector {
|
||||
return DefaultDeviceFlipDetector(context = context)
|
||||
}
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@ dependencies {
|
|||
implementation(deps.tangem.card.android)
|
||||
implementation(deps.tangem.card.core)
|
||||
|
||||
implementation(projects.core.datasource)
|
||||
implementation(projects.core.utils)
|
||||
|
||||
implementation(projects.data.source.preferences)
|
||||
|
|
|
|||
|
|
@ -1,16 +1,49 @@
|
|||
package com.tangem.data.card
|
||||
|
||||
import com.tangem.data.source.preferences.PreferencesDataSource
|
||||
import com.tangem.datasource.local.card.UsedCardInfo
|
||||
import com.tangem.datasource.local.card.UsedCardsStore
|
||||
import com.tangem.domain.card.repository.CardRepository
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.extensions.addOrReplace
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.channelFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
internal class DefaultCardRepository(
|
||||
private val preferencesDataSource: PreferencesDataSource,
|
||||
private val usedCardsStore: UsedCardsStore,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : CardRepository {
|
||||
|
||||
override suspend fun wasCardScanned(cardId: String): Boolean {
|
||||
return withContext(dispatchers.io) { preferencesDataSource.usedCardsPrefStorage.wasScanned(cardId) }
|
||||
override fun wasCardScanned(cardId: String): Flow<Boolean> {
|
||||
return channelFlow {
|
||||
launch(dispatchers.io) {
|
||||
usedCardsStore.get()
|
||||
.collect { savedCards ->
|
||||
send(element = savedCards.any { it.cardId == cardId })
|
||||
}
|
||||
}
|
||||
|
||||
withContext(dispatchers.io) {
|
||||
if (usedCardsStore.getSyncOrNull() == null) {
|
||||
send(element = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun setCardWasScanned(cardId: String) {
|
||||
withContext(dispatchers.io) {
|
||||
usedCardsStore.store(
|
||||
item = usedCardsStore.getSyncOrNull()
|
||||
?.updateCard(cardId)
|
||||
?: listOf(UsedCardInfo(cardId = cardId, isScanned = true)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<UsedCardInfo>.updateCard(cardId: String): List<UsedCardInfo> {
|
||||
val card = find { it.cardId == cardId } ?: UsedCardInfo(cardId = cardId, isScanned = true)
|
||||
return addOrReplace(item = card.copy(isScanned = true), predicate = { it.cardId == cardId })
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import com.tangem.data.card.DefaultCardRepository
|
|||
import com.tangem.data.card.DefaultCardSdkConfigRepository
|
||||
import com.tangem.data.card.sdk.CardSdkProvider
|
||||
import com.tangem.data.source.preferences.PreferencesDataSource
|
||||
import com.tangem.datasource.local.card.UsedCardsStore
|
||||
import com.tangem.domain.card.repository.CardRepository
|
||||
import com.tangem.domain.card.repository.CardSdkConfigRepository
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
|
|
@ -32,9 +33,9 @@ internal object CardDataModule {
|
|||
@Provides
|
||||
@Singleton
|
||||
fun provideCardRepository(
|
||||
preferencesDataSource: PreferencesDataSource,
|
||||
usedCardsStore: UsedCardsStore,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): CardRepository {
|
||||
return DefaultCardRepository(preferencesDataSource = preferencesDataSource, dispatchers = dispatchers)
|
||||
return DefaultCardRepository(usedCardsStore = usedCardsStore, dispatchers = dispatchers)
|
||||
}
|
||||
}
|
||||
|
|
@ -5,11 +5,17 @@ plugins {
|
|||
id("configuration")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.tangem.data.common"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.core.datasource)
|
||||
|
||||
implementation(deps.kotlin.coroutines)
|
||||
implementation(deps.jodatime)
|
||||
implementation(deps.timber)
|
||||
implementation(deps.arrow.core)
|
||||
|
||||
implementation(deps.hilt.android)
|
||||
kapt(deps.hilt.kapt)
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
<manifest package="com.tangem.data.common" />
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
package com.tangem.data.common.api
|
||||
|
||||
import arrow.core.raise.Raise
|
||||
import arrow.core.raise.recover
|
||||
import com.tangem.datasource.api.common.response.ApiResponse
|
||||
import com.tangem.datasource.api.common.response.ApiResponseError
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* A wrapper around the [Raise] interface specific for [ApiResponseError]. It provides utility functions to
|
||||
* operate on [ApiResponse] instances.
|
||||
*
|
||||
* @property raise A [Raise] instance for raising [ApiResponseError].
|
||||
*/
|
||||
@JvmInline
|
||||
value class ApiResponseRaise(
|
||||
private val raise: Raise<ApiResponseError>,
|
||||
) : Raise<ApiResponseError> by raise {
|
||||
|
||||
/**
|
||||
* Binds the given [ApiResponse] to its underlying value or raises an error.
|
||||
*
|
||||
* @return The underlying data of the response if it's successful.
|
||||
*/
|
||||
fun <T : Any> ApiResponse<T>.bind(): T = when (this) {
|
||||
is ApiResponse.Success -> data
|
||||
is ApiResponse.Error -> raise.raise(cause)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to execute an API call safely, providing error handling.
|
||||
*
|
||||
* @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 {
|
||||
return recover(
|
||||
block = { call(ApiResponseRaise(raise = this)) },
|
||||
recover = {
|
||||
Timber.w(it, "Unable to perform safe API call")
|
||||
onError(it)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -24,6 +24,15 @@ interface CacheRegistry {
|
|||
*/
|
||||
suspend fun invalidate(key: String)
|
||||
|
||||
/**
|
||||
* Invalidates cache keys in registry.
|
||||
*
|
||||
* If the key doesn't exist, or it's already invalidated, this method doesn't have any effect.
|
||||
*
|
||||
* @param keys cache keys.
|
||||
*/
|
||||
suspend fun invalidate(keys: Collection<String>)
|
||||
|
||||
/**
|
||||
* Invalidates all cache keys in the registry.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -2,8 +2,11 @@ package com.tangem.data.common.cache
|
|||
|
||||
import com.tangem.datasource.local.cache.CacheKeysStore
|
||||
import com.tangem.datasource.local.cache.model.CacheKey
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.joda.time.Duration
|
||||
import org.joda.time.LocalDateTime
|
||||
import timber.log.Timber
|
||||
|
||||
internal class DefaultCacheRegistry(
|
||||
private val cacheKeysStore: CacheKeysStore,
|
||||
|
|
@ -18,11 +21,18 @@ internal class DefaultCacheRegistry(
|
|||
}
|
||||
|
||||
override suspend fun invalidate(key: String) {
|
||||
cacheKeysStore.remove(key)
|
||||
Timber.d("Invalidate the cache key: $key")
|
||||
withContext(NonCancellable) { cacheKeysStore.remove(key) }
|
||||
}
|
||||
|
||||
override suspend fun invalidate(keys: Collection<String>) {
|
||||
Timber.d("Invalidate cache keys: $keys")
|
||||
withContext(NonCancellable) { cacheKeysStore.remove(keys) }
|
||||
}
|
||||
|
||||
override suspend fun invalidateAll() {
|
||||
cacheKeysStore.clear()
|
||||
Timber.d("Invalidate all cache keys")
|
||||
withContext(NonCancellable) { cacheKeysStore.clear() }
|
||||
}
|
||||
|
||||
override suspend fun invokeOnExpire(
|
||||
|
|
@ -34,18 +44,23 @@ internal class DefaultCacheRegistry(
|
|||
val isExpired = isExpired(key) || skipCache
|
||||
if (!isExpired) return
|
||||
|
||||
cacheKeysStore.store(
|
||||
key = CacheKey(
|
||||
id = key,
|
||||
updatedAt = LocalDateTime.now(),
|
||||
expiresIn = expireIn,
|
||||
),
|
||||
)
|
||||
|
||||
try {
|
||||
Timber.d("Invoke the action associated with the cache key: $key")
|
||||
|
||||
cacheKeysStore.store(
|
||||
key = CacheKey(
|
||||
id = key,
|
||||
updatedAt = LocalDateTime.now(),
|
||||
expiresIn = expireIn,
|
||||
),
|
||||
)
|
||||
|
||||
block()
|
||||
} catch (e: Throwable) {
|
||||
Timber.e(e, "The action related to the cache key has failed: $key")
|
||||
|
||||
invalidate(key)
|
||||
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,13 +14,16 @@ dependencies {
|
|||
|
||||
/** DI */
|
||||
implementation(deps.hilt.android)
|
||||
|
||||
kapt(deps.hilt.kapt)
|
||||
|
||||
implementation(deps.kotlin.coroutines)
|
||||
|
||||
implementation(projects.core.utils)
|
||||
implementation(projects.core.datasource)
|
||||
|
||||
implementation(projects.domain.settings)
|
||||
implementation(projects.domain.balanceHiding.models)
|
||||
|
||||
implementation(projects.data.source.preferences)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,103 @@
|
|||
package com.tangem.data.settings
|
||||
|
||||
import com.tangem.datasource.local.settings.AppLaunchCountStore
|
||||
import com.tangem.datasource.local.settings.AppRatingShowingCountStore
|
||||
import com.tangem.datasource.local.settings.FundsFoundDateInMillisStore
|
||||
import com.tangem.datasource.local.settings.UserInteractingStatusStore
|
||||
import com.tangem.domain.settings.repositories.AppRatingRepository
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.Calendar
|
||||
|
||||
internal class DefaultAppRatingRepository(
|
||||
private val fundsFoundDateInMillisStore: FundsFoundDateInMillisStore,
|
||||
private val appLaunchCountStore: AppLaunchCountStore,
|
||||
private val appRatingShowingCountStore: AppRatingShowingCountStore,
|
||||
private val userInteractingStatusStore: UserInteractingStatusStore,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : AppRatingRepository {
|
||||
|
||||
override suspend fun initialize() {
|
||||
fundsFoundDateInMillisStore.getSyncOrNull()
|
||||
?: fundsFoundDateInMillisStore.store(item = FUNDS_FOUND_DATE_UNDEFINED)
|
||||
|
||||
appLaunchCountStore.getSyncOrNull()
|
||||
?: appLaunchCountStore.store(item = DEFAULT_APP_LAUNCH_COUNT)
|
||||
|
||||
appRatingShowingCountStore.getSyncOrNull()
|
||||
?: appRatingShowingCountStore.store(item = FIRST_SHOWING_COUNT)
|
||||
|
||||
userInteractingStatusStore.getSyncOrNull()
|
||||
?: userInteractingStatusStore.store(item = false)
|
||||
}
|
||||
|
||||
override suspend fun setWalletWithFundsFound() {
|
||||
withContext(dispatchers.io) {
|
||||
val foundDate = fundsFoundDateInMillisStore.getSyncOrNull()
|
||||
if (foundDate != null && foundDate != FUNDS_FOUND_DATE_UNDEFINED) return@withContext
|
||||
|
||||
fundsFoundDateInMillisStore.store(item = Calendar.getInstance().timeInMillis)
|
||||
|
||||
val appLaunchCount = appLaunchCountStore.getSyncOrNull().let { count ->
|
||||
if (count == null) {
|
||||
appLaunchCountStore.store(item = DEFAULT_APP_LAUNCH_COUNT)
|
||||
DEFAULT_APP_LAUNCH_COUNT
|
||||
} else {
|
||||
count
|
||||
}
|
||||
}
|
||||
|
||||
appRatingShowingCountStore.store(item = appLaunchCount + FIRST_SHOWING_COUNT)
|
||||
|
||||
userInteractingStatusStore.getSyncOrNull()
|
||||
?: userInteractingStatusStore.store(item = false)
|
||||
}
|
||||
}
|
||||
|
||||
override fun isReadyToShow(): Flow<Boolean> {
|
||||
// TODO: [REDACTED_JIRA]
|
||||
return combine(
|
||||
userInteractingStatusStore.get(),
|
||||
appRatingShowingCountStore.get(),
|
||||
appLaunchCountStore.get(),
|
||||
fundsFoundDateInMillisStore.get(),
|
||||
) { isInteracting, ratingShowingCount, appLaunchCount, fundsFoundDate ->
|
||||
if (!isInteracting) {
|
||||
val diff = Calendar.getInstance().timeInMillis - fundsFoundDate
|
||||
val diffInDays = diff / DAY_IN_MILLIS
|
||||
val isFundsFound = fundsFoundDate != FUNDS_FOUND_DATE_UNDEFINED
|
||||
|
||||
appLaunchCount >= ratingShowingCount && diffInDays >= FIRST_SHOWING_COUNT && isFundsFound
|
||||
} else {
|
||||
appLaunchCount >= ratingShowingCount
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun remindLater() {
|
||||
appLaunchCountStore.getSyncOrNull()?.let {
|
||||
updateNextShowing(at = it + DEFER_SHOWING_COUNT)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun setNeverToShow() {
|
||||
updateNextShowing(at = Int.MAX_VALUE)
|
||||
}
|
||||
|
||||
private suspend fun updateNextShowing(at: Int) {
|
||||
withContext(dispatchers.io) {
|
||||
appRatingShowingCountStore.store(item = at)
|
||||
userInteractingStatusStore.store(item = true)
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val FUNDS_FOUND_DATE_UNDEFINED = -1L
|
||||
const val DEFAULT_APP_LAUNCH_COUNT = 1
|
||||
const val FIRST_SHOWING_COUNT = 3
|
||||
const val DEFER_SHOWING_COUNT = 20
|
||||
const val DAY_IN_MILLIS = 1000 * 60 * 60 * 24
|
||||
}
|
||||
}
|
||||
|
|
@ -10,12 +10,6 @@ internal class DefaultSettingsRepository(
|
|||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : SettingsRepository {
|
||||
|
||||
override suspend fun isUserAlreadyRateApp(): Boolean {
|
||||
return withContext(dispatchers.io) {
|
||||
preferencesDataSource.appRatingLaunchObserver.isReadyToShow()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun shouldShowSaveUserWalletScreen(): Boolean {
|
||||
return withContext(dispatchers.io) { preferencesDataSource.shouldShowSaveUserWalletScreen }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,13 @@
|
|||
package com.tangem.data.settings.di
|
||||
|
||||
import com.tangem.data.settings.DefaultAppRatingRepository
|
||||
import com.tangem.data.settings.DefaultSettingsRepository
|
||||
import com.tangem.data.source.preferences.PreferencesDataSource
|
||||
import com.tangem.datasource.local.settings.AppLaunchCountStore
|
||||
import com.tangem.datasource.local.settings.AppRatingShowingCountStore
|
||||
import com.tangem.datasource.local.settings.FundsFoundDateInMillisStore
|
||||
import com.tangem.datasource.local.settings.UserInteractingStatusStore
|
||||
import com.tangem.domain.settings.repositories.AppRatingRepository
|
||||
import com.tangem.domain.settings.repositories.SettingsRepository
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
|
|
@ -20,6 +26,27 @@ internal object SettingsDataModule {
|
|||
preferencesDataSource: PreferencesDataSource,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): SettingsRepository {
|
||||
return DefaultSettingsRepository(preferencesDataSource = preferencesDataSource, dispatchers = dispatchers)
|
||||
return DefaultSettingsRepository(
|
||||
preferencesDataSource = preferencesDataSource,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideAppRatingRepository(
|
||||
fundsFoundDateInMillisStore: FundsFoundDateInMillisStore,
|
||||
appLaunchCountStore: AppLaunchCountStore,
|
||||
appRatingShowingCountStore: AppRatingShowingCountStore,
|
||||
userInteractingStatusStore: UserInteractingStatusStore,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): AppRatingRepository {
|
||||
return DefaultAppRatingRepository(
|
||||
fundsFoundDateInMillisStore = fundsFoundDateInMillisStore,
|
||||
appLaunchCountStore = appLaunchCountStore,
|
||||
appRatingShowingCountStore = appRatingShowingCountStore,
|
||||
userInteractingStatusStore = userInteractingStatusStore,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -56,12 +56,6 @@ class PreferencesDataSource @Inject internal constructor(applicationContext: Con
|
|||
putBoolean(SAVE_WALLET_DIALOG_SHOWN_KEY, value)
|
||||
}
|
||||
|
||||
var shouldSaveUserWallets: Boolean
|
||||
get() = preferences.getBoolean(SAVE_USER_WALLETS_KEY, false)
|
||||
set(value) = preferences.edit {
|
||||
putBoolean(SAVE_USER_WALLETS_KEY, value)
|
||||
}
|
||||
|
||||
var shouldSaveAccessCodes: Boolean
|
||||
get() = preferences.getBoolean(SAVE_ACCESS_CODES_KEY, false)
|
||||
set(value) = preferences.edit {
|
||||
|
|
@ -102,7 +96,6 @@ class PreferencesDataSource @Inject internal constructor(applicationContext: Con
|
|||
private const val ZENDESK_FIRST_LAUNCH_KEY = "chatFirstLaunchKey"
|
||||
private const val SPRINKLR_FIRST_LAUNCH_KEY = "sprinklrFirstLaunch"
|
||||
private const val SAVE_WALLET_DIALOG_SHOWN_KEY = "saveUserWalletShown"
|
||||
private const val SAVE_USER_WALLETS_KEY = "saveUserWallets"
|
||||
private const val SAVE_ACCESS_CODES_KEY = "saveAccessCodes"
|
||||
private const val APPLICATION_STOPPED_KEY = "applicationStopped"
|
||||
private const val OPEN_WELCOME_ON_RESUME_KEY = "openWelcomeOnResume"
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ dependencies {
|
|||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.tokens)
|
||||
implementation(projects.domain.tokens.models)
|
||||
implementation(projects.domain.txhistory.models)
|
||||
implementation(projects.domain.wallets.models)
|
||||
|
||||
/** Project - Data */
|
||||
|
|
@ -40,4 +41,5 @@ dependencies {
|
|||
implementation(deps.moshi.kotlin)
|
||||
implementation(deps.jodatime)
|
||||
implementation(deps.timber)
|
||||
implementation(deps.retrofit) // For HttpException
|
||||
}
|
||||
|
|
@ -2,14 +2,18 @@ package com.tangem.data.tokens.di
|
|||
|
||||
import com.tangem.data.common.cache.CacheRegistry
|
||||
import com.tangem.data.tokens.repository.DefaultCurrenciesRepository
|
||||
import com.tangem.data.tokens.repository.DefaultMarketCryptoCurrencyRepository
|
||||
import com.tangem.data.tokens.repository.DefaultNetworksRepository
|
||||
import com.tangem.data.tokens.repository.DefaultQuotesRepository
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.local.appcurrency.SelectedAppCurrencyStore
|
||||
import com.tangem.datasource.local.network.NetworksStatusesStore
|
||||
import com.tangem.datasource.local.quote.QuotesStore
|
||||
import com.tangem.datasource.local.token.UserMarketCoinsStore
|
||||
import com.tangem.datasource.local.token.UserTokensStore
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
||||
import com.tangem.domain.tokens.repository.MarketCryptoCurrencyRepository
|
||||
import com.tangem.domain.tokens.repository.NetworksRepository
|
||||
import com.tangem.domain.tokens.repository.QuotesRepository
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
|
|
@ -30,10 +34,18 @@ internal object TokensDataModule {
|
|||
tangemTechApi: TangemTechApi,
|
||||
userTokensStore: UserTokensStore,
|
||||
userWalletsStore: UserWalletsStore,
|
||||
userMarketCoinsStore: UserMarketCoinsStore,
|
||||
cacheRegistry: CacheRegistry,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): CurrenciesRepository {
|
||||
return DefaultCurrenciesRepository(tangemTechApi, userTokensStore, userWalletsStore, cacheRegistry, dispatchers)
|
||||
return DefaultCurrenciesRepository(
|
||||
tangemTechApi = tangemTechApi,
|
||||
userTokensStore = userTokensStore,
|
||||
userWalletsStore = userWalletsStore,
|
||||
userMarketCoinsStore = userMarketCoinsStore,
|
||||
cacheRegistry = cacheRegistry,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
|
|
@ -46,17 +58,18 @@ internal object TokensDataModule {
|
|||
dispatchers: CoroutineDispatcherProvider,
|
||||
): QuotesRepository {
|
||||
return DefaultQuotesRepository(
|
||||
tangemTechApi,
|
||||
quotesStore,
|
||||
selectedAppCurrencyStore,
|
||||
cacheRegistry,
|
||||
dispatchers,
|
||||
tangemTechApi = tangemTechApi,
|
||||
quotesStore = quotesStore,
|
||||
selectedAppCurrencyStore = selectedAppCurrencyStore,
|
||||
cacheRegistry = cacheRegistry,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideNetworksRepository(
|
||||
networksStatusesStore: NetworksStatusesStore,
|
||||
walletManagersFacade: WalletManagersFacade,
|
||||
userWalletsStore: UserWalletsStore,
|
||||
userTokensStore: UserTokensStore,
|
||||
|
|
@ -64,11 +77,20 @@ internal object TokensDataModule {
|
|||
dispatchers: CoroutineDispatcherProvider,
|
||||
): NetworksRepository {
|
||||
return DefaultNetworksRepository(
|
||||
walletManagersFacade,
|
||||
userWalletsStore,
|
||||
userTokensStore,
|
||||
cacheRegistry,
|
||||
dispatchers,
|
||||
networksStatusesStore = networksStatusesStore,
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
userWalletsStore = userWalletsStore,
|
||||
userTokensStore = userTokensStore,
|
||||
cacheRegistry = cacheRegistry,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideDefaultMarketCoinsRepository(
|
||||
userMarketCoinsStore: UserMarketCoinsStore,
|
||||
): MarketCryptoCurrencyRepository {
|
||||
return DefaultMarketCryptoCurrencyRepository(userMarketCoinsStore)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,40 +1,47 @@
|
|||
package com.tangem.data.tokens.repository
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.data.common.api.safeApiCall
|
||||
import com.tangem.data.common.cache.CacheRegistry
|
||||
import com.tangem.data.tokens.utils.CardCurrenciesFactory
|
||||
import com.tangem.data.tokens.utils.ResponseCurrenciesFactory
|
||||
import com.tangem.data.tokens.utils.UserTokensResponseFactory
|
||||
import com.tangem.data.tokens.utils.*
|
||||
import com.tangem.datasource.api.common.response.ApiResponseError
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
||||
import com.tangem.datasource.local.token.UserMarketCoinsStore
|
||||
import com.tangem.datasource.local.token.UserTokensStore
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.common.extensions.toCoinId
|
||||
import com.tangem.domain.common.extensions.toNetworkId
|
||||
import com.tangem.domain.common.util.derivationStyleProvider
|
||||
import com.tangem.domain.common.util.hasDerivation
|
||||
import com.tangem.domain.core.error.DataError
|
||||
import com.tangem.domain.demo.DemoConfig
|
||||
import com.tangem.domain.tokens.models.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.channelFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
|
||||
@Suppress("LargeClass")
|
||||
internal class DefaultCurrenciesRepository(
|
||||
private val tangemTechApi: TangemTechApi,
|
||||
private val userTokensStore: UserTokensStore,
|
||||
private val userWalletsStore: UserWalletsStore,
|
||||
private val userMarketCoinsStore: UserMarketCoinsStore,
|
||||
private val cacheRegistry: CacheRegistry,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : CurrenciesRepository {
|
||||
|
||||
private val demoConfig = DemoConfig()
|
||||
private val responseCurrenciesFactory = ResponseCurrenciesFactory(demoConfig)
|
||||
private val cardCurrenciesFactory = CardCurrenciesFactory(demoConfig)
|
||||
private val responseCurrenciesFactory = ResponseCryptoCurrenciesFactory()
|
||||
private val cardCurrenciesFactory = CardCryptoCurrenciesFactory(demoConfig)
|
||||
private val userTokensResponseFactory = UserTokensResponseFactory()
|
||||
private val userTokensBackwardCompatibility = UserTokensBackwardCompatibility()
|
||||
|
||||
override suspend fun saveTokens(
|
||||
userWalletId: UserWalletId,
|
||||
|
|
@ -53,6 +60,59 @@ internal class DefaultCurrenciesRepository(
|
|||
storeAndPushTokens(userWalletId, response)
|
||||
}
|
||||
|
||||
override suspend fun addCurrencies(userWalletId: UserWalletId, currencies: List<CryptoCurrency>) {
|
||||
return withContext(dispatchers.io) {
|
||||
val savedCurrencies = requireNotNull(
|
||||
value = userTokensStore.getSyncOrNull(userWalletId),
|
||||
lazyMessage = { "Saved tokens empty. Can not perform add currencies action" },
|
||||
)
|
||||
|
||||
val filteredCurrencies = currencies.toMutableList()
|
||||
filteredCurrencies.filterNot { currency ->
|
||||
val blockchain = getBlockchain(networkId = currency.network.id)
|
||||
val networkId = blockchain.toNetworkId()
|
||||
val contractAddress = (currency as? CryptoCurrency.Token)?.contractAddress
|
||||
savedCurrencies.tokens.firstOrNull { token ->
|
||||
token.contractAddress == contractAddress &&
|
||||
token.networkId == networkId &&
|
||||
token.derivationPath == currency.network.derivationPath.value
|
||||
} != null
|
||||
}
|
||||
|
||||
val newCoins = createCoinsForNewTokens(
|
||||
userWalletId = userWalletId,
|
||||
newTokens = filteredCurrencies.filterIsInstance<CryptoCurrency.Token>(),
|
||||
savedCurrencies = savedCurrencies.tokens,
|
||||
)
|
||||
|
||||
val newCurrencies = newCoins + filteredCurrencies
|
||||
|
||||
storeAndPushTokens(
|
||||
userWalletId = userWalletId,
|
||||
response = savedCurrencies.copy(
|
||||
tokens = savedCurrencies.tokens + newCurrencies.map(userTokensResponseFactory::createResponseToken),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun createCoinsForNewTokens(
|
||||
userWalletId: UserWalletId,
|
||||
newTokens: List<CryptoCurrency.Token>,
|
||||
savedCurrencies: List<UserTokensResponse.Token>,
|
||||
): List<CryptoCurrency.Coin> {
|
||||
return newTokens
|
||||
.filterNot { savedCurrencies.hasCoinForToken(it) } // tokens without coins
|
||||
.mapNotNull {
|
||||
CryptoCurrencyFactory().createCoin(
|
||||
blockchain = getBlockchain(networkId = it.network.id),
|
||||
extraDerivationPath = it.network.derivationPath.value,
|
||||
derivationStyleProvider = getUserWallet(userWalletId).scanResponse.derivationStyleProvider,
|
||||
)
|
||||
}
|
||||
.distinct()
|
||||
}
|
||||
|
||||
override suspend fun removeCurrency(userWalletId: UserWalletId, currency: CryptoCurrency) =
|
||||
withContext(dispatchers.io) {
|
||||
val savedCurrencies = requireNotNull(
|
||||
|
|
@ -69,6 +129,23 @@ internal class DefaultCurrenciesRepository(
|
|||
)
|
||||
}
|
||||
|
||||
override suspend fun removeCurrencies(userWalletId: UserWalletId, currencies: List<CryptoCurrency>) {
|
||||
return withContext(dispatchers.io) {
|
||||
val savedCurrencies = requireNotNull(
|
||||
value = userTokensStore.getSyncOrNull(userWalletId),
|
||||
lazyMessage = { "Saved tokens empty. Can not perform remove currencies action" },
|
||||
)
|
||||
|
||||
val tokens = currencies.map(userTokensResponseFactory::createResponseToken)
|
||||
storeAndPushTokens(
|
||||
userWalletId = userWalletId,
|
||||
response = savedCurrencies.copy(
|
||||
tokens = savedCurrencies.tokens.filterNot(tokens::contains),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getSingleCurrencyWalletPrimaryCurrency(userWalletId: UserWalletId): CryptoCurrency {
|
||||
return withContext(dispatchers.io) {
|
||||
val userWallet = getUserWallet(userWalletId)
|
||||
|
|
@ -78,19 +155,44 @@ internal class DefaultCurrenciesRepository(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun getSingleCurrencyWalletWithCardCurrencies(userWalletId: UserWalletId): List<CryptoCurrency> {
|
||||
return withContext(dispatchers.io) {
|
||||
val userWallet = getUserWallet(userWalletId)
|
||||
ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = false)
|
||||
|
||||
cardCurrenciesFactory.createCurrenciesForSingleCurrencyCardWithToken(userWallet.scanResponse)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getSingleCurrencyWalletWithCardCurrency(
|
||||
userWalletId: UserWalletId,
|
||||
id: CryptoCurrency.ID,
|
||||
): CryptoCurrency {
|
||||
return withContext(dispatchers.io) {
|
||||
val userWallet = getUserWallet(userWalletId)
|
||||
ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = false)
|
||||
|
||||
val currency = cardCurrenciesFactory.createCurrenciesForSingleCurrencyCardWithToken(userWallet.scanResponse)
|
||||
.find { it.id == id }
|
||||
requireNotNull(currency) { "Unable to find currency with provided ID: $id" }
|
||||
}
|
||||
}
|
||||
|
||||
override fun getMultiCurrencyWalletCurrenciesUpdates(userWalletId: UserWalletId): Flow<List<CryptoCurrency>> {
|
||||
return channelFlow {
|
||||
val userWallet = getUserWallet(userWalletId)
|
||||
ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = true)
|
||||
|
||||
launch(dispatchers.io) {
|
||||
getMultiCurrencyWalletCurrencies(userWallet).collect(::send)
|
||||
getMultiCurrencyWalletCurrencies(userWallet)
|
||||
.collectLatest(::send)
|
||||
}
|
||||
|
||||
launch(dispatchers.io) {
|
||||
withContext(dispatchers.io) {
|
||||
fetchTokensIfCacheExpired(userWallet, refresh = false)
|
||||
}
|
||||
}
|
||||
.cancellable()
|
||||
}
|
||||
|
||||
override suspend fun getMultiCurrencyWalletCurrenciesSync(
|
||||
|
|
@ -106,15 +208,13 @@ internal class DefaultCurrenciesRepository(
|
|||
"Unable to find tokens response for user wallet with provided ID: $userWalletId"
|
||||
}
|
||||
|
||||
return responseCurrenciesFactory.createCurrencies(
|
||||
response = storedTokens,
|
||||
card = userWallet.scanResponse.card,
|
||||
)
|
||||
return responseCurrenciesFactory.createCurrencies(storedTokens, userWallet.scanResponse)
|
||||
}
|
||||
|
||||
override suspend fun getMultiCurrencyWalletCurrency(
|
||||
userWalletId: UserWalletId,
|
||||
id: CryptoCurrency.ID,
|
||||
derivationPath: Network.DerivationPath,
|
||||
): CryptoCurrency = withContext(dispatchers.io) {
|
||||
val userWallet = getUserWallet(userWalletId)
|
||||
ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = true)
|
||||
|
|
@ -123,7 +223,34 @@ internal class DefaultCurrenciesRepository(
|
|||
"Unable to find tokens response for user wallet with provided ID: $userWalletId"
|
||||
}
|
||||
|
||||
responseCurrenciesFactory.createCurrency(id, response, userWallet.scanResponse.card)
|
||||
responseCurrenciesFactory.createCurrency(id, response, userWallet.scanResponse, derivationPath.value)
|
||||
}
|
||||
|
||||
override suspend fun getNetworkCoin(
|
||||
userWalletId: UserWalletId,
|
||||
networkId: Network.ID,
|
||||
derivationPath: Network.DerivationPath,
|
||||
): CryptoCurrency.Coin {
|
||||
val userWallet = getUserWallet(userWalletId)
|
||||
ensureIsCorrectUserWallet(userWallet = userWallet, isMultiCurrencyWalletExpected = true)
|
||||
|
||||
fetchTokensIfCacheExpired(userWallet = userWallet, refresh = false)
|
||||
|
||||
val storedTokens = requireNotNull(userTokensStore.getSyncOrNull(userWallet.walletId)) {
|
||||
"Unable to find tokens response for user wallet with provided ID: $userWalletId"
|
||||
}
|
||||
val blockchain = Blockchain.fromId(networkId.value)
|
||||
val blockchainNetworkId = blockchain.toNetworkId()
|
||||
val coinId = blockchain.toCoinId()
|
||||
|
||||
val storedCoin = storedTokens.tokens
|
||||
.find {
|
||||
it.networkId == blockchainNetworkId && it.id == coinId && it.derivationPath == derivationPath.value
|
||||
} ?: error("Coin in this network $networkId not found")
|
||||
|
||||
val coin = responseCurrenciesFactory.createCurrency(storedCoin, userWallet.scanResponse)
|
||||
|
||||
return coin as? CryptoCurrency.Coin ?: error("Unable to create currency")
|
||||
}
|
||||
|
||||
override fun isTokensGrouped(userWalletId: UserWalletId): Flow<Boolean> {
|
||||
|
|
@ -135,7 +262,7 @@ internal class DefaultCurrenciesRepository(
|
|||
.map { it.group == UserTokensResponse.GroupType.NETWORK }
|
||||
.collect(::send)
|
||||
}
|
||||
}
|
||||
}.cancellable()
|
||||
}
|
||||
|
||||
override fun isTokensSortedByBalance(userWalletId: UserWalletId): Flow<Boolean> {
|
||||
|
|
@ -147,6 +274,24 @@ internal class DefaultCurrenciesRepository(
|
|||
.map { it.sort == UserTokensResponse.SortType.BALANCE }
|
||||
.collect(::send)
|
||||
}
|
||||
}.cancellable()
|
||||
}
|
||||
|
||||
override fun getMissedAddressesCryptoCurrencies(userWalletId: UserWalletId): Flow<List<CryptoCurrency>> {
|
||||
return channelFlow {
|
||||
ensureIsCorrectUserWallet(userWalletId, isMultiCurrencyWalletExpected = true)
|
||||
|
||||
val userWallet = getUserWallet(userWalletId = userWalletId)
|
||||
getMultiCurrencyWalletCurrencies(userWallet = userWallet)
|
||||
.map {
|
||||
it.filter { currency ->
|
||||
val blockchain = Blockchain.fromId(id = currency.network.id.value)
|
||||
val derivationPath = currency.network.derivationPath.value
|
||||
|
||||
derivationPath != null && !userWallet.scanResponse.hasDerivation(blockchain, derivationPath)
|
||||
}
|
||||
}
|
||||
.collectLatest(::send)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -154,7 +299,7 @@ internal class DefaultCurrenciesRepository(
|
|||
return userTokensStore.get(userWallet.walletId).map { storedTokens ->
|
||||
responseCurrenciesFactory.createCurrencies(
|
||||
response = storedTokens,
|
||||
card = userWallet.scanResponse.card,
|
||||
scanResponse = userWallet.scanResponse,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -168,40 +313,80 @@ internal class DefaultCurrenciesRepository(
|
|||
}
|
||||
|
||||
private suspend fun fetchTokens(userWallet: UserWallet) {
|
||||
try {
|
||||
val response = tangemTechApi.getUserTokens(userWallet.walletId.stringValue)
|
||||
val userWalletId = userWallet.walletId
|
||||
|
||||
userTokensStore.store(userWallet.walletId, response)
|
||||
} catch (e: Throwable) {
|
||||
handleFetchTokensErrorOrThrow(userWallet, e)
|
||||
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 = safeApiCall(
|
||||
call = {
|
||||
tangemTechApi.getUserTokens(userWalletId.stringValue).bind().let {
|
||||
it.copy(tokens = it.tokens.distinct())
|
||||
}
|
||||
},
|
||||
onError = { handleFetchTokensError(userWallet, it) },
|
||||
)
|
||||
|
||||
val compatibleUserTokensResponse = userTokensBackwardCompatibility.applyCompatibilityAndGetUpdated(response)
|
||||
userTokensStore.store(userWallet.walletId, compatibleUserTokensResponse)
|
||||
fetchExchangeableUserMarketCoinsByIds(userWalletId, compatibleUserTokensResponse)
|
||||
}
|
||||
|
||||
private suspend fun storeAndPushTokens(userWalletId: UserWalletId, response: UserTokensResponse) {
|
||||
userTokensStore.store(userWalletId, response)
|
||||
tangemTechApi.saveUserTokens(userWalletId.stringValue, response)
|
||||
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}")
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleFetchTokensErrorOrThrow(userWallet: UserWallet, error: Throwable) {
|
||||
val errorMessage = error.message ?: throw error
|
||||
private suspend fun fetchExchangeableUserMarketCoinsByIds(
|
||||
userWalletId: UserWalletId,
|
||||
userTokens: UserTokensResponse,
|
||||
) {
|
||||
try {
|
||||
val networkIds = userTokens.tokens
|
||||
.distinctBy { it.networkId }
|
||||
.joinToString(separator = ",") { it.networkId }
|
||||
val response = tangemTechApi.getCoins(networkIds = networkIds, exchangeable = true)
|
||||
|
||||
if (NOT_FOUND_HTTP_CODE in errorMessage) {
|
||||
val response = userTokensStore.getSyncOrNull(userWallet.walletId)
|
||||
?: userTokensResponseFactory.createUserTokensResponse(
|
||||
currencies = cardCurrenciesFactory.createDefaultCoinsForMultiCurrencyCard(
|
||||
card = userWallet.scanResponse.card,
|
||||
derivationStyleProvider = userWallet.scanResponse.derivationStyleProvider,
|
||||
),
|
||||
isGroupedByNetwork = false,
|
||||
isSortedByBalance = false,
|
||||
)
|
||||
|
||||
tangemTechApi.saveUserTokens(userWallet.walletId.stringValue, response)
|
||||
} else {
|
||||
Timber.e(error, "Unable to fetch currencies for: ${userWallet.walletId}")
|
||||
userMarketCoinsStore.store(userWalletId, response)
|
||||
} catch (e: Throwable) {
|
||||
Timber.e(e, "Unable to fetch user market coins for: ${userWalletId.stringValue}")
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
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)
|
||||
} else {
|
||||
cacheRegistry.invalidate(getTokensCacheKey(userWalletId))
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
private suspend fun getUserWallet(userWalletId: UserWalletId): UserWallet {
|
||||
return requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) {
|
||||
"Unable to find a user wallet with provided ID: $userWalletId"
|
||||
|
|
@ -236,8 +421,4 @@ internal class DefaultCurrenciesRepository(
|
|||
}
|
||||
|
||||
private fun getTokensCacheKey(userWalletId: UserWalletId): String = "tokens_cache_key_${userWalletId.stringValue}"
|
||||
|
||||
private companion object {
|
||||
const val NOT_FOUND_HTTP_CODE = "404"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package com.tangem.data.tokens.repository
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.datasource.local.token.UserMarketCoinsStore
|
||||
import com.tangem.domain.common.extensions.toNetworkId
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.repository.MarketCryptoCurrencyRepository
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
|
||||
class DefaultMarketCryptoCurrencyRepository(
|
||||
private val userMarketCoinsStore: UserMarketCoinsStore,
|
||||
) : MarketCryptoCurrencyRepository {
|
||||
|
||||
override suspend fun isExchangeable(userWalletId: UserWalletId, cryptoCurrencyId: CryptoCurrency.ID): Boolean {
|
||||
val blockchain = Blockchain.fromId(cryptoCurrencyId.rawNetworkId)
|
||||
val apiNetworkId = blockchain.toNetworkId()
|
||||
return userMarketCoinsStore.getSyncOrNull(userWalletId)?.coins
|
||||
?.firstOrNull { it.id == cryptoCurrencyId.rawCurrencyId }
|
||||
?.networks
|
||||
?.firstOrNull { it.networkId == apiNetworkId }?.exchangeable ?: false
|
||||
}
|
||||
}
|
||||
|
|
@ -1,25 +1,31 @@
|
|||
package com.tangem.data.tokens.repository
|
||||
|
||||
import com.tangem.data.common.cache.CacheRegistry
|
||||
import com.tangem.data.tokens.utils.CardCurrenciesFactory
|
||||
import com.tangem.data.tokens.utils.NetworkConverter
|
||||
import com.tangem.data.tokens.utils.CardCryptoCurrenciesFactory
|
||||
import com.tangem.data.tokens.utils.NetworkStatusFactory
|
||||
import com.tangem.data.tokens.utils.ResponseCurrenciesFactory
|
||||
import com.tangem.data.tokens.utils.ResponseCryptoCurrenciesFactory
|
||||
import com.tangem.datasource.local.network.NetworksStatusesStore
|
||||
import com.tangem.datasource.local.token.UserTokensStore
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.common.util.cardTypesResolver
|
||||
import com.tangem.domain.demo.DemoConfig
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.tokens.model.NetworkStatus
|
||||
import com.tangem.domain.tokens.models.CryptoCurrency
|
||||
import com.tangem.domain.tokens.models.Network
|
||||
import com.tangem.domain.tokens.repository.NetworksRepository
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.extensions.addOrReplace
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.cancellable
|
||||
import kotlinx.coroutines.flow.channelFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import timber.log.Timber
|
||||
|
||||
internal class DefaultNetworksRepository(
|
||||
private val networksStatusesStore: NetworksStatusesStore,
|
||||
private val walletManagersFacade: WalletManagersFacade,
|
||||
private val userWalletsStore: UserWalletsStore,
|
||||
private val userTokensStore: UserTokensStore,
|
||||
|
|
@ -28,103 +34,174 @@ internal class DefaultNetworksRepository(
|
|||
) : NetworksRepository {
|
||||
|
||||
private val demoConfig by lazy { DemoConfig() }
|
||||
private val networkConverter by lazy { NetworkConverter() }
|
||||
private val cardCurrenciesFactory by lazy { CardCurrenciesFactory(demoConfig) }
|
||||
private val responseCurrenciesFactory by lazy { ResponseCurrenciesFactory(demoConfig) }
|
||||
private val cardCurrenciesFactory by lazy { CardCryptoCurrenciesFactory(demoConfig) }
|
||||
private val responseCurrenciesFactory by lazy { ResponseCryptoCurrenciesFactory() }
|
||||
private val networkStatusFactory by lazy { NetworkStatusFactory() }
|
||||
|
||||
private val networksStatuses: MutableStateFlow<List<NetworkStatus>> = MutableStateFlow(emptyList())
|
||||
|
||||
override fun getNetworks(networksIds: Set<Network.ID>): Set<Network> {
|
||||
return networkConverter.convertSet(networksIds)
|
||||
}
|
||||
|
||||
override fun getNetworkStatusesUpdates(
|
||||
userWalletId: UserWalletId,
|
||||
networks: Set<Network.ID>,
|
||||
networks: Set<Network>,
|
||||
): Flow<Set<NetworkStatus>> = channelFlow {
|
||||
launch(dispatchers.io) {
|
||||
networksStatuses.collect {
|
||||
send(it.toSet())
|
||||
}
|
||||
networksStatusesStore.get(userWalletId)
|
||||
.collectLatest(::send)
|
||||
}
|
||||
|
||||
launch(dispatchers.io) {
|
||||
fetchNetworksStatusesIfCacheExpired(userWalletId, networks, refresh = false)
|
||||
withContext(dispatchers.io) {
|
||||
fetchNetworksStatusesIfCacheExpired(userWalletId, networks, false)
|
||||
}
|
||||
}
|
||||
.cancellable()
|
||||
|
||||
override suspend fun fetchNetworkPendingTransactions(userWalletId: UserWalletId, networks: Set<Network>) {
|
||||
withContext(dispatchers.io) {
|
||||
fetchNetworksPendingTransactions(userWalletId, networks)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getNetworkStatusesSync(
|
||||
userWalletId: UserWalletId,
|
||||
networks: Set<Network.ID>,
|
||||
networks: Set<Network>,
|
||||
refresh: Boolean,
|
||||
): Set<NetworkStatus> = withContext(dispatchers.io) {
|
||||
fetchNetworksStatusesIfCacheExpired(userWalletId, networks, refresh)
|
||||
networksStatuses.first().toSet()
|
||||
networksStatusesStore.getSyncOrNull(userWalletId).orEmpty()
|
||||
}
|
||||
|
||||
private suspend fun fetchNetworksStatusesIfCacheExpired(
|
||||
userWalletId: UserWalletId,
|
||||
networks: Set<Network.ID>,
|
||||
networks: Set<Network>,
|
||||
refresh: Boolean,
|
||||
) {
|
||||
cacheRegistry.invokeOnExpire(
|
||||
key = getNetworksStatusesCacheKey(userWalletId),
|
||||
skipCache = refresh,
|
||||
block = { fetchNetworksStatuses(userWalletId, networks) },
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun fetchNetworksStatuses(userWalletId: UserWalletId, networks: Set<Network.ID>) {
|
||||
coroutineScope {
|
||||
networks
|
||||
.map { networkId ->
|
||||
.map { network ->
|
||||
async {
|
||||
fetchNetworkStatus(userWalletId, networkId)
|
||||
fetchNetworkStatusIfCacheExpired(userWalletId, network, refresh)
|
||||
}
|
||||
}
|
||||
.awaitAll()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchNetworkStatus(userWalletId: UserWalletId, networkId: Network.ID) {
|
||||
val currencies = getCurrencies(userWalletId)
|
||||
.asSequence()
|
||||
.filter { it.network.id == networkId }
|
||||
private suspend fun fetchNetworksPendingTransactions(userWalletId: UserWalletId, networks: Set<Network>) {
|
||||
coroutineScope {
|
||||
networks
|
||||
.map { network ->
|
||||
async {
|
||||
fetchNetworkPendingTransactions(userWalletId, network)
|
||||
}
|
||||
}
|
||||
.awaitAll()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchNetworkStatusIfCacheExpired(
|
||||
userWalletId: UserWalletId,
|
||||
network: Network,
|
||||
refresh: Boolean,
|
||||
) {
|
||||
cacheRegistry.invokeOnExpire(
|
||||
key = getNetworksStatusesCacheKey(userWalletId, network),
|
||||
skipCache = refresh,
|
||||
block = { fetchNetworkStatus(userWalletId, network) },
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun fetchNetworkStatus(userWalletId: UserWalletId, network: Network) {
|
||||
val currencies = getCurrencies(userWalletId, network)
|
||||
|
||||
val result = walletManagersFacade.update(
|
||||
userWalletId = userWalletId,
|
||||
networkId = networkId,
|
||||
network = network,
|
||||
extraTokens = currencies.filterIsInstance<CryptoCurrency.Token>().toSet(),
|
||||
)
|
||||
|
||||
withContext(NonCancellable) {
|
||||
invalidateCacheKeyIfNeeded(userWalletId, network, result)
|
||||
}
|
||||
|
||||
val networkStatus = networkStatusFactory.createNetworkStatus(
|
||||
networkId = networkId,
|
||||
network = network,
|
||||
result = result,
|
||||
currencies = currencies.toSet(),
|
||||
)
|
||||
|
||||
networksStatuses.update { statuses ->
|
||||
statuses.addOrReplace(networkStatus) { it.networkId == networkStatus.networkId }
|
||||
}
|
||||
networksStatusesStore.store(userWalletId, networkStatus)
|
||||
}
|
||||
|
||||
private suspend fun getCurrencies(userWalletId: UserWalletId): List<CryptoCurrency> {
|
||||
private suspend fun fetchNetworkPendingTransactions(userWalletId: UserWalletId, network: Network) {
|
||||
val currencies = getCurrencies(userWalletId, network)
|
||||
|
||||
val result = walletManagersFacade.updatePendingTransactions(
|
||||
userWalletId = userWalletId,
|
||||
network = network,
|
||||
)
|
||||
|
||||
withContext(NonCancellable) {
|
||||
invalidateCacheKeyIfNeeded(userWalletId, network, result)
|
||||
}
|
||||
|
||||
val networkStatus = networkStatusFactory.createNetworkStatus(
|
||||
network = network,
|
||||
result = result,
|
||||
currencies = currencies.toSet(),
|
||||
)
|
||||
|
||||
networksStatusesStore.store(userWalletId, networkStatus)
|
||||
}
|
||||
|
||||
private suspend fun getCurrencies(userWalletId: UserWalletId, network: Network): Sequence<CryptoCurrency> {
|
||||
val userWallet = requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) {
|
||||
"Unable to find user wallet with provided ID: $userWalletId"
|
||||
}
|
||||
|
||||
return if (userWallet.isMultiCurrency) {
|
||||
val currencies = if (userWallet.isMultiCurrency) {
|
||||
val response = requireNotNull(userTokensStore.getSyncOrNull(userWalletId)) {
|
||||
"Unable to find tokens response for user wallet with provided ID: $userWalletId"
|
||||
}
|
||||
|
||||
responseCurrenciesFactory.createCurrencies(response, userWallet.scanResponse.card)
|
||||
responseCurrenciesFactory.createCurrencies(response, userWallet.scanResponse).asSequence()
|
||||
} else {
|
||||
val currency = cardCurrenciesFactory.createPrimaryCurrencyForSingleCurrencyCard(userWallet.scanResponse)
|
||||
if (userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken()) {
|
||||
cardCurrenciesFactory.createCurrenciesForSingleCurrencyCardWithToken(userWallet.scanResponse)
|
||||
.asSequence()
|
||||
} else {
|
||||
val currency = cardCurrenciesFactory.createPrimaryCurrencyForSingleCurrencyCard(userWallet.scanResponse)
|
||||
|
||||
listOf(currency)
|
||||
sequenceOf(currency)
|
||||
}
|
||||
}
|
||||
|
||||
return currencies.filter { it.network == network }
|
||||
}
|
||||
|
||||
private suspend fun invalidateCacheKeyIfNeeded(
|
||||
userWalletId: UserWalletId,
|
||||
network: Network,
|
||||
result: UpdateWalletManagerResult,
|
||||
) {
|
||||
when (result) {
|
||||
is UpdateWalletManagerResult.Verified,
|
||||
is UpdateWalletManagerResult.NoAccount,
|
||||
-> Unit
|
||||
is UpdateWalletManagerResult.Unreachable,
|
||||
is UpdateWalletManagerResult.MissedDerivation,
|
||||
-> {
|
||||
Timber.w(
|
||||
"""
|
||||
Invalidate network cache key
|
||||
|- User wallet ID: $userWalletId
|
||||
|- Network: ${network.id}
|
||||
""".trimIndent(),
|
||||
)
|
||||
|
||||
cacheRegistry.invalidate(getNetworksStatusesCacheKey(userWalletId, network))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getNetworksStatusesCacheKey(userWalletId: UserWalletId): String = "network_status_$userWalletId"
|
||||
private fun getNetworksStatusesCacheKey(userWalletId: UserWalletId, network: Network): String {
|
||||
return "network_status_${userWalletId}_${network.id.value}_${network.derivationPath.value}"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,18 +1,19 @@
|
|||
package com.tangem.data.tokens.repository
|
||||
|
||||
import com.tangem.data.common.api.safeApiCall
|
||||
import com.tangem.data.common.cache.CacheRegistry
|
||||
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.local.appcurrency.SelectedAppCurrencyStore
|
||||
import com.tangem.datasource.local.quote.QuotesStore
|
||||
import com.tangem.domain.tokens.models.CryptoCurrency
|
||||
import com.tangem.domain.tokens.models.Quote
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.Quote
|
||||
import com.tangem.domain.tokens.repository.QuotesRepository
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
|
||||
internal class DefaultQuotesRepository(
|
||||
private val tangemTechApi: TangemTechApi,
|
||||
|
|
@ -23,23 +24,22 @@ internal class DefaultQuotesRepository(
|
|||
) : QuotesRepository {
|
||||
|
||||
private val quotesConverter = QuotesConverter()
|
||||
private val quotesUnsupportedCurrenciesAdapter = QuotesUnsupportedCurrenciesIdAdapter()
|
||||
|
||||
@Volatile
|
||||
private var quotesFetchedForAppCurrency: String? = null
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
override fun getQuotesUpdates(currenciesIds: Set<CryptoCurrency.ID>): Flow<Set<Quote>> {
|
||||
return channelFlow {
|
||||
launch(dispatchers.io) {
|
||||
quotesStore.get(currenciesIds)
|
||||
.map(quotesConverter::convertSet)
|
||||
.collect(::send)
|
||||
}
|
||||
return selectedAppCurrencyStore.get()
|
||||
.distinctUntilChanged()
|
||||
.flatMapLatest { appCurrency ->
|
||||
fetchExpiredQuotes(currenciesIds, appCurrency.id, refresh = false)
|
||||
|
||||
launch(dispatchers.io) {
|
||||
selectedAppCurrencyStore.get().collectLatest { appCurrency ->
|
||||
fetchExpiredQuotes(currenciesIds, appCurrency.id, refresh = false)
|
||||
}
|
||||
quotesStore.get(currenciesIds).map(quotesConverter::convertSet)
|
||||
}
|
||||
}
|
||||
.cancellable()
|
||||
.flowOn(dispatchers.io)
|
||||
}
|
||||
|
||||
override suspend fun getQuotesSync(currenciesIds: Set<CryptoCurrency.ID>, refresh: Boolean): Set<Quote> {
|
||||
|
|
@ -73,16 +73,24 @@ internal class DefaultQuotesRepository(
|
|||
}
|
||||
|
||||
private suspend fun fetchQuotes(rawCurrenciesIds: Set<String>, appCurrencyId: String) {
|
||||
try {
|
||||
val response = tangemTechApi.getQuotes(
|
||||
currencyId = appCurrencyId,
|
||||
coinIds = rawCurrenciesIds.joinToString(separator = ","),
|
||||
)
|
||||
val replacementIdsResult = quotesUnsupportedCurrenciesAdapter.replaceUnsupportedCurrencies(rawCurrenciesIds)
|
||||
val response = safeApiCall(
|
||||
call = {
|
||||
val coinIds = replacementIdsResult.idsForRequest.joinToString(separator = ",")
|
||||
tangemTechApi.getQuotes(appCurrencyId, coinIds).bind()
|
||||
},
|
||||
onError = {
|
||||
cacheRegistry.invalidate(rawCurrenciesIds.map(::getQuoteCacheKey))
|
||||
null
|
||||
},
|
||||
)
|
||||
|
||||
quotesStore.store(response)
|
||||
} catch (e: Throwable) {
|
||||
Timber.e(e, "Unable to fetch quotes for: $rawCurrenciesIds")
|
||||
throw e
|
||||
if (response != null) {
|
||||
val updatedResponse = quotesUnsupportedCurrenciesAdapter.getResponseWithUnsupportedCurrencies(
|
||||
response,
|
||||
replacementIdsResult.idsFiltered,
|
||||
)
|
||||
quotesStore.store(updatedResponse)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,85 @@
|
|||
package com.tangem.data.tokens.utils
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.domain.common.TapWorkarounds.isTestCard
|
||||
import com.tangem.domain.common.util.cardTypesResolver
|
||||
import com.tangem.domain.common.util.derivationStyleProvider
|
||||
import com.tangem.domain.demo.DemoConfig
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
|
||||
internal class CardCryptoCurrenciesFactory(private val demoConfig: DemoConfig) {
|
||||
|
||||
private val cryptoCurrencyFactory = CryptoCurrencyFactory()
|
||||
|
||||
fun createDefaultCoinsForMultiCurrencyCard(scanResponse: ScanResponse): List<CryptoCurrency.Coin> {
|
||||
val cardDerivationStyleProvider = scanResponse.derivationStyleProvider
|
||||
val card = scanResponse.card
|
||||
|
||||
var blockchains = if (demoConfig.isDemoCardId(card.cardId)) {
|
||||
demoConfig.demoBlockchains
|
||||
} else {
|
||||
listOf(Blockchain.Bitcoin, Blockchain.Ethereum)
|
||||
}
|
||||
|
||||
if (card.isTestCard) {
|
||||
blockchains = blockchains.mapNotNull { it.getTestnetVersion() }
|
||||
}
|
||||
|
||||
return blockchains.mapNotNull {
|
||||
cryptoCurrencyFactory.createCoin(
|
||||
blockchain = it,
|
||||
extraDerivationPath = null,
|
||||
derivationStyleProvider = cardDerivationStyleProvider,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun createPrimaryCurrencyForSingleCurrencyCard(scanResponse: ScanResponse): CryptoCurrency {
|
||||
val cardDerivationStyleProvider = scanResponse.derivationStyleProvider
|
||||
val resolver = scanResponse.cardTypesResolver
|
||||
val blockchain = resolver.getBlockchain()
|
||||
|
||||
val coin = cryptoCurrencyFactory.createCoin(
|
||||
blockchain = blockchain,
|
||||
extraDerivationPath = null,
|
||||
derivationStyleProvider = cardDerivationStyleProvider,
|
||||
)
|
||||
requireNotNull(coin) { "Coin for the single currency card cannot be null" }
|
||||
|
||||
val primaryToken = resolver.getPrimaryToken()?.let { token ->
|
||||
cryptoCurrencyFactory.createToken(
|
||||
sdkToken = token,
|
||||
blockchain = blockchain,
|
||||
extraDerivationPath = null,
|
||||
derivationStyleProvider = cardDerivationStyleProvider,
|
||||
)
|
||||
}
|
||||
|
||||
return primaryToken ?: coin
|
||||
}
|
||||
|
||||
fun createCurrenciesForSingleCurrencyCardWithToken(scanResponse: ScanResponse): List<CryptoCurrency> {
|
||||
val cardDerivationStyleProvider = scanResponse.derivationStyleProvider
|
||||
val resolver = scanResponse.cardTypesResolver
|
||||
val blockchain = resolver.getBlockchain()
|
||||
|
||||
val coin = cryptoCurrencyFactory.createCoin(
|
||||
blockchain = blockchain,
|
||||
extraDerivationPath = null,
|
||||
derivationStyleProvider = cardDerivationStyleProvider,
|
||||
)
|
||||
requireNotNull(coin) { "Coin for the single currency card cannot be null" }
|
||||
|
||||
val primaryToken = resolver.getPrimaryToken()?.let { token ->
|
||||
cryptoCurrencyFactory.createToken(
|
||||
sdkToken = token,
|
||||
blockchain = blockchain,
|
||||
extraDerivationPath = null,
|
||||
derivationStyleProvider = cardDerivationStyleProvider,
|
||||
)
|
||||
}
|
||||
|
||||
return listOfNotNull(coin, primaryToken)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
package com.tangem.data.tokens.utils
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.domain.common.DerivationStyleProvider
|
||||
import com.tangem.domain.common.TapWorkarounds.isTestCard
|
||||
import com.tangem.domain.common.util.cardTypesResolver
|
||||
import com.tangem.domain.common.util.derivationStyleProvider
|
||||
import com.tangem.domain.demo.DemoConfig
|
||||
import com.tangem.domain.models.scan.CardDTO
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.tokens.models.CryptoCurrency
|
||||
|
||||
internal class CardCurrenciesFactory(private val demoConfig: DemoConfig) {
|
||||
|
||||
private val cryptoCurrencyFactory by lazy { CryptoCurrencyFactory() }
|
||||
|
||||
fun createDefaultCoinsForMultiCurrencyCard(
|
||||
card: CardDTO,
|
||||
derivationStyleProvider: DerivationStyleProvider,
|
||||
): List<CryptoCurrency.Coin> {
|
||||
var blockchains = if (demoConfig.isDemoCardId(card.cardId)) {
|
||||
demoConfig.demoBlockchains
|
||||
} else {
|
||||
listOf(Blockchain.Bitcoin, Blockchain.Ethereum)
|
||||
}
|
||||
|
||||
if (card.isTestCard) {
|
||||
blockchains = blockchains.mapNotNull { it.getTestnetVersion() }
|
||||
}
|
||||
|
||||
return blockchains.mapNotNull { cryptoCurrencyFactory.createCoin(it, derivationStyleProvider) }
|
||||
}
|
||||
|
||||
fun createPrimaryCurrencyForSingleCurrencyCard(scanResponse: ScanResponse): CryptoCurrency {
|
||||
val derivationStyleProvider = scanResponse.derivationStyleProvider
|
||||
val resolver = scanResponse.cardTypesResolver
|
||||
val blockchain = resolver.getBlockchain()
|
||||
|
||||
val coin = requireNotNull(cryptoCurrencyFactory.createCoin(blockchain, derivationStyleProvider)) {
|
||||
"Coin for the single currency card cannot be null"
|
||||
}
|
||||
val primaryToken = resolver.getPrimaryToken()?.let { token ->
|
||||
cryptoCurrencyFactory.createToken(token, blockchain, derivationStyleProvider)
|
||||
}
|
||||
|
||||
return primaryToken ?: coin
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,8 @@ package com.tangem.data.tokens.utils
|
|||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.domain.common.DerivationStyleProvider
|
||||
import com.tangem.domain.tokens.models.CryptoCurrency
|
||||
import com.tangem.domain.common.extensions.toCoinId
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import timber.log.Timber
|
||||
import com.tangem.blockchain.common.Token as SdkToken
|
||||
|
||||
|
|
@ -12,6 +13,7 @@ class CryptoCurrencyFactory {
|
|||
fun createToken(
|
||||
sdkToken: SdkToken,
|
||||
blockchain: Blockchain,
|
||||
extraDerivationPath: String?,
|
||||
derivationStyleProvider: DerivationStyleProvider,
|
||||
): CryptoCurrency.Token? {
|
||||
if (blockchain == Blockchain.Unknown) {
|
||||
|
|
@ -19,35 +21,40 @@ class CryptoCurrencyFactory {
|
|||
return null
|
||||
}
|
||||
|
||||
val id = getTokenId(blockchain, sdkToken)
|
||||
val network = getNetwork(blockchain, extraDerivationPath, derivationStyleProvider) ?: return null
|
||||
val id = getTokenId(network, sdkToken)
|
||||
|
||||
return CryptoCurrency.Token(
|
||||
id = id,
|
||||
network = getNetwork(blockchain) ?: return null,
|
||||
network = network,
|
||||
name = sdkToken.name,
|
||||
symbol = sdkToken.symbol,
|
||||
iconUrl = getTokenIconUrl(blockchain, sdkToken),
|
||||
decimals = sdkToken.decimals,
|
||||
isCustom = isCustomToken(id),
|
||||
isCustom = isCustomToken(id, network),
|
||||
contractAddress = sdkToken.contractAddress,
|
||||
derivationPath = getDerivationPath(blockchain, derivationStyleProvider),
|
||||
)
|
||||
}
|
||||
|
||||
fun createCoin(blockchain: Blockchain, derivationStyleProvider: DerivationStyleProvider): CryptoCurrency.Coin? {
|
||||
fun createCoin(
|
||||
blockchain: Blockchain,
|
||||
extraDerivationPath: String?,
|
||||
derivationStyleProvider: DerivationStyleProvider,
|
||||
): CryptoCurrency.Coin? {
|
||||
if (blockchain == Blockchain.Unknown) {
|
||||
Timber.e("Unable to map the SDK token to the domain token with Unknown blockchain")
|
||||
return null
|
||||
}
|
||||
val network = getNetwork(blockchain, extraDerivationPath, derivationStyleProvider) ?: return null
|
||||
|
||||
return CryptoCurrency.Coin(
|
||||
id = getCoinId(blockchain),
|
||||
network = getNetwork(blockchain) ?: return null,
|
||||
id = getCoinId(network, blockchain.toCoinId()),
|
||||
network = network,
|
||||
name = blockchain.fullName,
|
||||
symbol = blockchain.currency,
|
||||
iconUrl = getCoinIconUrl(blockchain),
|
||||
decimals = blockchain.decimals(),
|
||||
derivationPath = getDerivationPath(blockchain, derivationStyleProvider),
|
||||
isCustom = isCustomCoin(network),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
package com.tangem.data.tokens.utils
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.domain.tokens.models.Network
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
internal class NetworkConverter : Converter<Network.ID, Network?> {
|
||||
|
||||
override fun convert(value: Network.ID): Network? {
|
||||
val blockchain = Blockchain.fromId(value.value)
|
||||
|
||||
return getNetwork(blockchain)
|
||||
}
|
||||
|
||||
override fun convertList(input: Collection<Network.ID>): List<Network> {
|
||||
return input.mapNotNull(::convert)
|
||||
}
|
||||
|
||||
override fun convertSet(input: Collection<Network.ID>): Set<Network> {
|
||||
return input.mapNotNullTo(hashSetOf(), ::convert)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,19 @@
|
|||
package com.tangem.data.tokens.utils
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.domain.tokens.models.Network
|
||||
import com.tangem.domain.common.DerivationStyleProvider
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import timber.log.Timber
|
||||
|
||||
internal fun getNetwork(blockchain: Blockchain): Network? {
|
||||
internal fun getBlockchain(networkId: Network.ID): Blockchain {
|
||||
return Blockchain.fromId(networkId.value)
|
||||
}
|
||||
|
||||
internal fun getNetwork(
|
||||
blockchain: Blockchain,
|
||||
extraDerivationPath: String?,
|
||||
derivationStyleProvider: DerivationStyleProvider,
|
||||
): Network? {
|
||||
if (blockchain == Blockchain.Unknown) {
|
||||
Timber.e("Unable to convert Unknown blockchain to the domain network model")
|
||||
return null
|
||||
|
|
@ -14,10 +23,33 @@ internal fun getNetwork(blockchain: Blockchain): Network? {
|
|||
id = Network.ID(blockchain.id),
|
||||
name = blockchain.fullName,
|
||||
isTestnet = blockchain.isTestnet(),
|
||||
derivationPath = getNetworkDerivationPath(blockchain, extraDerivationPath, derivationStyleProvider),
|
||||
standardType = getNetworkStandardType(blockchain),
|
||||
)
|
||||
}
|
||||
|
||||
private fun getNetworkDerivationPath(
|
||||
blockchain: Blockchain,
|
||||
extraDerivationPath: String?,
|
||||
cardDerivationStyleProvider: DerivationStyleProvider,
|
||||
): Network.DerivationPath {
|
||||
val defaultDerivationPath = getDefaultDerivationPath(blockchain, cardDerivationStyleProvider)
|
||||
|
||||
return if (extraDerivationPath.isNullOrBlank()) {
|
||||
if (defaultDerivationPath.isNullOrBlank()) {
|
||||
Network.DerivationPath.None
|
||||
} else {
|
||||
Network.DerivationPath.Card(defaultDerivationPath)
|
||||
}
|
||||
} else {
|
||||
if (extraDerivationPath == defaultDerivationPath) {
|
||||
Network.DerivationPath.Card(defaultDerivationPath)
|
||||
} else {
|
||||
Network.DerivationPath.Custom(extraDerivationPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getNetworkStandardType(blockchain: Blockchain): Network.StandardType {
|
||||
return when (blockchain) {
|
||||
Blockchain.Ethereum, Blockchain.EthereumTestnet -> Network.StandardType.ERC20
|
||||
|
|
@ -26,4 +58,11 @@ private fun getNetworkStandardType(blockchain: Blockchain): Network.StandardType
|
|||
Blockchain.Tron, Blockchain.TronTestnet -> Network.StandardType.TRC20
|
||||
else -> Network.StandardType.Unspecified(blockchain.name)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getDefaultDerivationPath(
|
||||
blockchain: Blockchain,
|
||||
derivationStyleProvider: DerivationStyleProvider,
|
||||
): String? {
|
||||
return blockchain.derivationPath(derivationStyleProvider.getDerivationStyle())?.rawPath
|
||||
}
|
||||
|
|
@ -1,37 +1,36 @@
|
|||
package com.tangem.data.tokens.utils
|
||||
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.tokens.model.NetworkAddress
|
||||
import com.tangem.domain.tokens.model.NetworkStatus
|
||||
import com.tangem.domain.tokens.model.PendingTransaction
|
||||
import com.tangem.domain.tokens.models.CryptoCurrency
|
||||
import com.tangem.domain.tokens.models.Network
|
||||
import com.tangem.domain.txhistory.models.TxHistoryItem
|
||||
import com.tangem.domain.walletmanager.model.CryptoCurrencyAmount
|
||||
import com.tangem.domain.walletmanager.model.CryptoCurrencyTransaction
|
||||
import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult
|
||||
import timber.log.Timber
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal class NetworkStatusFactory {
|
||||
|
||||
fun createNetworkStatus(
|
||||
networkId: Network.ID,
|
||||
network: Network,
|
||||
result: UpdateWalletManagerResult,
|
||||
currencies: Set<CryptoCurrency>,
|
||||
): NetworkStatus {
|
||||
return NetworkStatus(
|
||||
networkId = networkId,
|
||||
network = network,
|
||||
value = when (result) {
|
||||
is UpdateWalletManagerResult.MissedDerivation -> NetworkStatus.MissedDerivation
|
||||
is UpdateWalletManagerResult.Unreachable -> NetworkStatus.Unreachable
|
||||
is UpdateWalletManagerResult.NoAccount -> NetworkStatus.NoAccount(
|
||||
address = getNetworkAddress(result.defaultAddress, result.addresses),
|
||||
amountToCreateAccount = result.amountToCreateAccount,
|
||||
errorMessage = result.errorMessage,
|
||||
)
|
||||
is UpdateWalletManagerResult.Verified -> NetworkStatus.Verified(
|
||||
address = getNetworkAddress(result.defaultAddress, result.addresses),
|
||||
amounts = formatAmounts(result.currenciesAmounts, currencies),
|
||||
pendingTransactions = formatTransactions(
|
||||
networksAddresses = result.addresses,
|
||||
transactions = result.currentTransactions,
|
||||
currencies = currencies,
|
||||
),
|
||||
|
|
@ -43,34 +42,33 @@ internal class NetworkStatusFactory {
|
|||
private fun formatAmounts(
|
||||
amounts: Set<CryptoCurrencyAmount>,
|
||||
currencies: Set<CryptoCurrency>,
|
||||
): Map<CryptoCurrency.ID, BigDecimal> {
|
||||
return amounts
|
||||
.asSequence()
|
||||
.mapNotNull { amount ->
|
||||
val currency = when (amount) {
|
||||
is CryptoCurrencyAmount.Coin -> currencies.singleOrNull { it is CryptoCurrency.Coin }
|
||||
is CryptoCurrencyAmount.Token -> currencies.firstOrNull {
|
||||
it is CryptoCurrency.Token &&
|
||||
it.id.rawCurrencyId == amount.tokenId &&
|
||||
it.contractAddress == amount.tokenContractAddress
|
||||
): Map<CryptoCurrency.ID, NetworkStatus.AmountStatus> {
|
||||
return currencies.associate { currency ->
|
||||
val amount = when (currency) {
|
||||
is CryptoCurrency.Coin -> {
|
||||
amounts.singleOrNull { it is CryptoCurrencyAmount.Coin }
|
||||
}
|
||||
is CryptoCurrency.Token -> {
|
||||
amounts.firstOrNull { amount ->
|
||||
amount is CryptoCurrencyAmount.Token &&
|
||||
currency.id.rawCurrencyId == amount.tokenId &&
|
||||
currency.contractAddress == amount.tokenContractAddress
|
||||
}
|
||||
}
|
||||
|
||||
if (currency == null) {
|
||||
Timber.e("Unable to find cryptocurrency for amount: $amount")
|
||||
null
|
||||
} else {
|
||||
currency.id to amount.value
|
||||
}
|
||||
}
|
||||
.toMap()
|
||||
if (amount == null) {
|
||||
Timber.e("Unable to find amount for cryptoCurrency: $currency")
|
||||
currency.id to NetworkStatus.UnreachableAmount
|
||||
} else {
|
||||
currency.id to NetworkStatus.LoadedAmount(amount.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatTransactions(
|
||||
networksAddresses: Set<String>,
|
||||
transactions: Set<CryptoCurrencyTransaction>,
|
||||
currencies: Set<CryptoCurrency>,
|
||||
): Map<CryptoCurrency.ID, Set<PendingTransaction>> {
|
||||
): Map<CryptoCurrency.ID, Set<TxHistoryItem>> {
|
||||
if (transactions.isEmpty()) return emptyMap()
|
||||
|
||||
return currencies
|
||||
|
|
@ -87,48 +85,13 @@ internal class NetworkStatusFactory {
|
|||
}
|
||||
}
|
||||
|
||||
currency.id to createCurrentTransactions(networksAddresses, currencyTransactions)
|
||||
currency.id to createCurrentTransactions(currencyTransactions)
|
||||
}
|
||||
.toMap()
|
||||
}
|
||||
|
||||
private fun createCurrentTransactions(
|
||||
networksAddresses: Set<String>,
|
||||
transactions: Set<CryptoCurrencyTransaction>,
|
||||
): Set<PendingTransaction> {
|
||||
return transactions.mapNotNullTo(hashSetOf()) { createCurrentTransaction(networksAddresses, it) }
|
||||
}
|
||||
|
||||
private fun createCurrentTransaction(
|
||||
networksAddresses: Set<String>,
|
||||
transaction: CryptoCurrencyTransaction,
|
||||
): PendingTransaction? {
|
||||
val direction = when {
|
||||
transaction.toAddress in networksAddresses -> PendingTransaction.Direction.Incoming(
|
||||
fromAddress = transaction.fromAddress,
|
||||
)
|
||||
transaction.fromAddress in networksAddresses -> PendingTransaction.Direction.Outgoing(
|
||||
toAddress = transaction.toAddress,
|
||||
)
|
||||
else -> {
|
||||
Timber.e(
|
||||
"""
|
||||
Unable to find transaction direction
|
||||
|- To address: ${transaction.toAddress}
|
||||
|- From address: ${transaction.fromAddress}
|
||||
|- Network addresses: $networksAddresses
|
||||
""".trimIndent(),
|
||||
)
|
||||
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
return PendingTransaction(
|
||||
amount = transaction.amount,
|
||||
direction = direction,
|
||||
sentAt = transaction.sentAt,
|
||||
)
|
||||
private fun createCurrentTransactions(transactions: Set<CryptoCurrencyTransaction>): Set<TxHistoryItem> {
|
||||
return transactions.mapTo(hashSetOf()) { it.txHistoryItem }
|
||||
}
|
||||
|
||||
private fun getNetworkAddress(defaultAddress: String, availableAddresses: Set<String>): NetworkAddress {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
package com.tangem.data.tokens.utils
|
||||
|
||||
import com.tangem.datasource.local.quote.model.StoredQuote
|
||||
import com.tangem.domain.tokens.models.Quote
|
||||
import com.tangem.domain.tokens.model.Quote
|
||||
import com.tangem.utils.converter.Converter
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal class QuotesConverter : Converter<StoredQuote, Quote> {
|
||||
|
||||
|
|
@ -11,8 +12,8 @@ internal class QuotesConverter : Converter<StoredQuote, Quote> {
|
|||
|
||||
return Quote(
|
||||
rawCurrencyId = rawCurrencyId,
|
||||
fiatRate = responseQuote.price,
|
||||
priceChange = responseQuote.priceChange.movePointLeft(2),
|
||||
fiatRate = responseQuote.price ?: BigDecimal.ZERO,
|
||||
priceChange = (responseQuote.priceChange ?: BigDecimal.ZERO).movePointLeft(2),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
package com.tangem.data.tokens.utils
|
||||
|
||||
import com.tangem.datasource.api.tangemTech.models.QuotesResponse
|
||||
|
||||
/**
|
||||
* Adapter to replace unsupported currencies for quotes request if it necessary
|
||||
*/
|
||||
internal class QuotesUnsupportedCurrenciesIdAdapter {
|
||||
|
||||
/**
|
||||
* Replaces unsupported currencies id to it replacements for request
|
||||
*/
|
||||
fun replaceUnsupportedCurrencies(currenciesIds: Set<String>): ReplacementResult {
|
||||
val idsForRequest = mutableSetOf<String>()
|
||||
val idsFiltered = mutableSetOf<String>()
|
||||
currenciesIds.forEach { currencyId ->
|
||||
val replacementId = UNSUPPORTED_IDS_WITH_REPLACEMENTS[currencyId]
|
||||
if (replacementId != null) {
|
||||
idsFiltered.add(currencyId)
|
||||
idsForRequest.add(replacementId)
|
||||
} else {
|
||||
idsForRequest.add(currencyId)
|
||||
}
|
||||
}
|
||||
return ReplacementResult(idsForRequest = idsForRequest, idsFiltered = idsFiltered)
|
||||
}
|
||||
|
||||
/**
|
||||
* Recover previously replaced currencies to response that uses in application
|
||||
*/
|
||||
fun getResponseWithUnsupportedCurrencies(response: QuotesResponse, filteredIds: Set<String>): QuotesResponse {
|
||||
val updatedQuotes = mutableMapOf<String, QuotesResponse.Quote>()
|
||||
filteredIds.forEach { filteredId ->
|
||||
val replacementId = UNSUPPORTED_IDS_WITH_REPLACEMENTS[filteredId]
|
||||
if (replacementId != null) {
|
||||
val quoteReplacement = response.quotes[replacementId]
|
||||
if (quoteReplacement != null) {
|
||||
updatedQuotes[filteredId] = quoteReplacement
|
||||
}
|
||||
}
|
||||
}
|
||||
return response.copy(quotes = updatedQuotes + response.quotes)
|
||||
}
|
||||
|
||||
data class ReplacementResult(val idsForRequest: Set<String>, val idsFiltered: Set<String>)
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Map that contains unsupported currencies and their replacement for request
|
||||
*/
|
||||
private val UNSUPPORTED_IDS_WITH_REPLACEMENTS = mapOf(
|
||||
"optimistic-ethereum" to "ethereum",
|
||||
"arbitrum-one" to "ethereum",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
package com.tangem.data.tokens.utils
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.Token
|
||||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
||||
import com.tangem.domain.common.DerivationStyleProvider
|
||||
import com.tangem.domain.common.TapWorkarounds.isTestCard
|
||||
import com.tangem.domain.common.extensions.fromNetworkId
|
||||
import com.tangem.domain.common.extensions.toCoinId
|
||||
import com.tangem.domain.common.extensions.toNetworkId
|
||||
import com.tangem.domain.common.util.derivationStyleProvider
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import timber.log.Timber
|
||||
import com.tangem.blockchain.common.Token as SdkToken
|
||||
|
||||
internal class ResponseCryptoCurrenciesFactory {
|
||||
|
||||
fun createCurrency(
|
||||
currencyId: CryptoCurrency.ID,
|
||||
response: UserTokensResponse,
|
||||
scanResponse: ScanResponse,
|
||||
derivationPath: String?,
|
||||
): CryptoCurrency {
|
||||
val responseTokenId = currencyId.rawCurrencyId
|
||||
val networkId = Blockchain.fromId(currencyId.rawNetworkId).toNetworkId()
|
||||
|
||||
val token = requireNotNull(
|
||||
value = response.tokens
|
||||
.find { it.id == responseTokenId && it.networkId == networkId && it.derivationPath == derivationPath },
|
||||
lazyMessage = { "Unable find a token with provided TokenID($responseTokenId) and NetworkID($networkId)" },
|
||||
)
|
||||
|
||||
return requireNotNull(createCurrency(token, scanResponse)) {
|
||||
"Unable to create a currency with provided ID: $currencyId"
|
||||
}
|
||||
}
|
||||
|
||||
fun createCurrencies(response: UserTokensResponse, scanResponse: ScanResponse): List<CryptoCurrency> {
|
||||
return response.tokens
|
||||
.asSequence()
|
||||
.mapNotNull { createCurrency(it, scanResponse) }
|
||||
.distinctBy { it.id }
|
||||
.toList()
|
||||
}
|
||||
|
||||
fun createCurrency(responseToken: UserTokensResponse.Token, scanResponse: ScanResponse): CryptoCurrency? {
|
||||
var blockchain = Blockchain.fromNetworkId(responseToken.networkId)
|
||||
if (blockchain == null || blockchain == Blockchain.Unknown) {
|
||||
Timber.e("Unable to find a blockchain with the network ID: ${responseToken.networkId}")
|
||||
return null
|
||||
}
|
||||
|
||||
val cardDerivationStyleProvider = scanResponse.derivationStyleProvider
|
||||
val card = scanResponse.card
|
||||
|
||||
if (card.isTestCard) {
|
||||
blockchain = blockchain.getTestnetVersion() ?: blockchain
|
||||
}
|
||||
|
||||
val sdkToken = createSdkToken(responseToken)
|
||||
return if (sdkToken == null) {
|
||||
createCoin(blockchain, responseToken, cardDerivationStyleProvider)
|
||||
} else {
|
||||
createToken(blockchain, sdkToken, responseToken.derivationPath, cardDerivationStyleProvider)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createSdkToken(token: UserTokensResponse.Token): SdkToken? {
|
||||
return token.contractAddress?.let { contractAddress ->
|
||||
SdkToken(
|
||||
name = token.name,
|
||||
symbol = token.symbol,
|
||||
contractAddress = contractAddress,
|
||||
decimals = token.decimals,
|
||||
id = token.id,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createCoin(
|
||||
blockchain: Blockchain,
|
||||
responseToken: UserTokensResponse.Token,
|
||||
derivationStyleProvider: DerivationStyleProvider,
|
||||
): CryptoCurrency.Coin? {
|
||||
val network = getNetwork(blockchain, responseToken.derivationPath, derivationStyleProvider) ?: return null
|
||||
|
||||
return CryptoCurrency.Coin(
|
||||
id = getCoinId(network, blockchain.toCoinId()),
|
||||
network = network,
|
||||
name = responseToken.name,
|
||||
symbol = responseToken.symbol,
|
||||
decimals = responseToken.decimals,
|
||||
iconUrl = getCoinIconUrl(blockchain),
|
||||
isCustom = isCustomCoin(network),
|
||||
)
|
||||
}
|
||||
|
||||
private fun createToken(
|
||||
blockchain: Blockchain,
|
||||
sdkToken: Token,
|
||||
responseDerivationPath: String?,
|
||||
derivationStyleProvider: DerivationStyleProvider,
|
||||
): CryptoCurrency.Token? {
|
||||
val network = getNetwork(blockchain, responseDerivationPath, derivationStyleProvider)
|
||||
?: return null
|
||||
val id = getTokenId(network, sdkToken)
|
||||
|
||||
return CryptoCurrency.Token(
|
||||
id = id,
|
||||
network = network,
|
||||
name = sdkToken.name,
|
||||
symbol = sdkToken.symbol,
|
||||
decimals = sdkToken.decimals,
|
||||
iconUrl = getTokenIconUrl(blockchain, sdkToken),
|
||||
contractAddress = sdkToken.contractAddress,
|
||||
isCustom = isCustomToken(id, network),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,89 +0,0 @@
|
|||
package com.tangem.data.tokens.utils
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.Token
|
||||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
||||
import com.tangem.domain.common.extensions.fromNetworkId
|
||||
import com.tangem.domain.demo.DemoConfig
|
||||
import com.tangem.domain.models.scan.CardDTO
|
||||
import com.tangem.domain.tokens.models.CryptoCurrency
|
||||
import timber.log.Timber
|
||||
import com.tangem.blockchain.common.Token as SdkToken
|
||||
|
||||
internal class ResponseCurrenciesFactory(private val demoConfig: DemoConfig) {
|
||||
|
||||
fun createCurrency(currencyId: CryptoCurrency.ID, response: UserTokensResponse, card: CardDTO): CryptoCurrency {
|
||||
val responseTokenId = currencyId.rawCurrencyId
|
||||
|
||||
val token = requireNotNull(response.tokens.firstOrNull { it.id == responseTokenId }) {
|
||||
"Unable find a token with provided ID: $responseTokenId"
|
||||
}
|
||||
|
||||
return requireNotNull(createCurrency(token, card)) {
|
||||
"Unable to create a currency with provided ID: $currencyId"
|
||||
}
|
||||
}
|
||||
|
||||
fun createCurrencies(response: UserTokensResponse, card: CardDTO): List<CryptoCurrency> {
|
||||
return response.tokens.mapNotNull { createCurrency(it, card) }
|
||||
}
|
||||
|
||||
private fun createCurrency(responseToken: UserTokensResponse.Token, card: CardDTO): CryptoCurrency? {
|
||||
var blockchain = Blockchain.fromNetworkId(responseToken.networkId)
|
||||
if (blockchain == null || blockchain == Blockchain.Unknown) {
|
||||
Timber.e("Unable to find a blockchain with the network ID: ${responseToken.networkId}")
|
||||
return null
|
||||
}
|
||||
|
||||
if (demoConfig.isDemoCardId(card.cardId)) {
|
||||
blockchain = blockchain.getTestnetVersion() ?: blockchain
|
||||
}
|
||||
|
||||
val sdkToken = createSdkToken(responseToken)
|
||||
return if (sdkToken == null) {
|
||||
createCoin(blockchain, responseToken)
|
||||
} else {
|
||||
createToken(blockchain, sdkToken, responseToken.derivationPath)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createSdkToken(token: UserTokensResponse.Token): SdkToken? {
|
||||
return token.contractAddress?.let { contractAddress ->
|
||||
SdkToken(
|
||||
name = token.name,
|
||||
symbol = token.symbol,
|
||||
contractAddress = contractAddress,
|
||||
decimals = token.decimals,
|
||||
id = token.id,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createCoin(blockchain: Blockchain, responseToken: UserTokensResponse.Token): CryptoCurrency.Coin? {
|
||||
return CryptoCurrency.Coin(
|
||||
id = getCoinId(blockchain),
|
||||
network = getNetwork(blockchain) ?: return null,
|
||||
name = responseToken.name,
|
||||
symbol = responseToken.symbol,
|
||||
decimals = responseToken.decimals,
|
||||
derivationPath = responseToken.derivationPath,
|
||||
iconUrl = getCoinIconUrl(blockchain),
|
||||
)
|
||||
}
|
||||
|
||||
private fun createToken(blockchain: Blockchain, sdkToken: Token, derivationPath: String?): CryptoCurrency.Token? {
|
||||
val id = getTokenId(blockchain, sdkToken)
|
||||
|
||||
return CryptoCurrency.Token(
|
||||
id = id,
|
||||
network = getNetwork(blockchain) ?: return null,
|
||||
name = sdkToken.name,
|
||||
symbol = sdkToken.symbol,
|
||||
decimals = sdkToken.decimals,
|
||||
derivationPath = derivationPath,
|
||||
iconUrl = getTokenIconUrl(blockchain, sdkToken),
|
||||
contractAddress = sdkToken.contractAddress,
|
||||
isCustom = isCustomToken(id),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -2,40 +2,44 @@ package com.tangem.data.tokens.utils
|
|||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.IconsUtil
|
||||
import com.tangem.domain.common.DerivationStyleProvider
|
||||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
||||
import com.tangem.domain.common.extensions.toCoinId
|
||||
import com.tangem.domain.common.extensions.toNetworkId
|
||||
import com.tangem.domain.tokens.models.CryptoCurrency.ID
|
||||
import com.tangem.domain.tokens.models.Network
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency.ID
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.blockchain.common.Token as SdkToken
|
||||
import com.tangem.domain.tokens.models.CryptoCurrency.ID.Prefix.COIN_PREFIX as COIN_ID_PREFIX
|
||||
import com.tangem.domain.tokens.models.CryptoCurrency.ID.Prefix.CUSTOM_TOKEN_PREFIX as CUSTOM_TOKEN_ID_PREFIX
|
||||
import com.tangem.domain.tokens.models.CryptoCurrency.ID.Prefix.TOKEN_PREFIX as TOKEN_ID_PREFIX
|
||||
import com.tangem.domain.tokens.models.CryptoCurrency.ID.Suffix.ContractAddress as CustomCurrencyIdSuffix
|
||||
import com.tangem.domain.tokens.models.CryptoCurrency.ID.Suffix.RawID as CurrencyIdSuffix
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency.ID.Body as CurrencyIdBody
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency.ID.Prefix.COIN_PREFIX as COIN_ID_PREFIX
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency.ID.Prefix.TOKEN_PREFIX as TOKEN_ID_PREFIX
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency.ID.Suffix.ContractAddress as CustomCurrencyIdSuffix
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency.ID.Suffix.RawID as CurrencyIdSuffix
|
||||
|
||||
private const val DEFAULT_TOKENS_ICONS_HOST = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins"
|
||||
private const val TOKEN_ICON_SIZE = "large"
|
||||
private const val TOKEN_ICON_EXT = "png"
|
||||
|
||||
internal fun isCustomToken(tokenId: ID): Boolean {
|
||||
return tokenId.rawCurrencyId == null
|
||||
internal fun isCustomToken(tokenId: ID, network: Network): Boolean {
|
||||
return network.derivationPath is Network.DerivationPath.Custom || tokenId.rawCurrencyId == null
|
||||
}
|
||||
|
||||
internal fun getDerivationPath(blockchain: Blockchain, derivationStyleProvider: DerivationStyleProvider): String? {
|
||||
return blockchain.derivationPath(derivationStyleProvider.getDerivationStyle())?.rawPath
|
||||
internal fun isCustomCoin(network: Network): Boolean {
|
||||
return network.derivationPath is Network.DerivationPath.Custom
|
||||
}
|
||||
|
||||
internal fun getBlockchain(networkId: Network.ID): Blockchain {
|
||||
return Blockchain.fromId(networkId.value)
|
||||
internal fun getCoinId(network: Network, coinId: String): ID {
|
||||
return ID(COIN_ID_PREFIX, getCurrencyIdBody(network), CurrencyIdSuffix(rawId = coinId))
|
||||
}
|
||||
|
||||
internal fun getCoinId(blockchain: Blockchain): ID {
|
||||
return getTokenOrCoinId(blockchain, token = null)
|
||||
}
|
||||
internal fun getTokenId(network: Network, sdkToken: SdkToken): ID {
|
||||
val sdkTokenId = sdkToken.id
|
||||
val suffix = if (sdkTokenId == null) {
|
||||
CustomCurrencyIdSuffix(contractAddress = sdkToken.contractAddress)
|
||||
} else {
|
||||
CurrencyIdSuffix(rawId = sdkTokenId, contractAddress = sdkToken.contractAddress)
|
||||
}
|
||||
|
||||
internal fun getTokenId(blockchain: Blockchain, token: SdkToken): ID {
|
||||
return getTokenOrCoinId(blockchain, token)
|
||||
return ID(TOKEN_ID_PREFIX, getCurrencyIdBody(network), suffix)
|
||||
}
|
||||
|
||||
internal fun getTokenIconUrl(blockchain: Blockchain, token: SdkToken): String? {
|
||||
|
|
@ -51,22 +55,31 @@ internal fun getTokenIconUrl(blockchain: Blockchain, token: SdkToken): String? {
|
|||
internal fun getCoinIconUrl(blockchain: Blockchain): String? {
|
||||
val coinId = when (blockchain) {
|
||||
Blockchain.Unknown -> null
|
||||
Blockchain.TerraV1, Blockchain.TerraV2 -> blockchain.toCoinId()
|
||||
Blockchain.TerraV1, Blockchain.TerraV2, Blockchain.Near -> blockchain.toCoinId()
|
||||
else -> blockchain.toNetworkId()
|
||||
}
|
||||
|
||||
return coinId?.let(::getTokenIconUrlFromDefaultHost)
|
||||
}
|
||||
|
||||
private fun getTokenOrCoinId(blockchain: Blockchain, token: SdkToken?): ID {
|
||||
val sdkTokenId = token?.id
|
||||
val (prefix, suffix) = when {
|
||||
token == null -> COIN_ID_PREFIX to CurrencyIdSuffix(rawId = blockchain.toCoinId())
|
||||
sdkTokenId == null -> CUSTOM_TOKEN_ID_PREFIX to CustomCurrencyIdSuffix(contractAddress = token.contractAddress)
|
||||
else -> TOKEN_ID_PREFIX to CurrencyIdSuffix(rawId = sdkTokenId)
|
||||
internal fun List<UserTokensResponse.Token>.hasCoinForToken(token: CryptoCurrency.Token): Boolean {
|
||||
return any {
|
||||
val blockchain = getBlockchain(networkId = token.network.id)
|
||||
val tokenDerivation = token.network.derivationPath.value
|
||||
it.id == blockchain.toCoinId() && it.derivationPath == tokenDerivation
|
||||
}
|
||||
}
|
||||
|
||||
return ID(prefix, Network.ID(blockchain.id), suffix)
|
||||
private fun getCurrencyIdBody(network: Network): CurrencyIdBody {
|
||||
return when (val path = network.derivationPath) {
|
||||
is Network.DerivationPath.Custom -> CurrencyIdBody.NetworkIdWithDerivationPath(
|
||||
rawId = network.id.value,
|
||||
derivationPath = path.value,
|
||||
)
|
||||
is Network.DerivationPath.Card,
|
||||
is Network.DerivationPath.None,
|
||||
-> CurrencyIdBody.NetworkId(network.id.value)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getTokenIconUrlFromDefaultHost(tokenId: String): String {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
package com.tangem.data.tokens.utils
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
||||
import com.tangem.domain.common.extensions.fromNetworkId
|
||||
import com.tangem.domain.common.extensions.toCoinId
|
||||
|
||||
/**
|
||||
* Helper to apply compatibility changes for [UserTokensResponse] to support old saved tokens
|
||||
* in new application with new IDs
|
||||
*/
|
||||
internal class UserTokensBackwardCompatibility {
|
||||
|
||||
fun applyCompatibilityAndGetUpdated(userTokensResponse: UserTokensResponse): UserTokensResponse {
|
||||
return userTokensResponse.copy(
|
||||
tokens = userTokensResponse.tokens.map { token ->
|
||||
val oldSavedId = NETWORKS_TO_OLD_SAVED_IDS[token.networkId]
|
||||
if (oldSavedId != null && token.id == oldSavedId) {
|
||||
Blockchain.fromNetworkId(token.networkId)?.let { blockchain ->
|
||||
token.copy(
|
||||
id = blockchain.toCoinId(),
|
||||
)
|
||||
} ?: token
|
||||
} else {
|
||||
token
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val NETWORKS_TO_OLD_SAVED_IDS = mapOf(
|
||||
"optimistic-ethereum" to "ethereum",
|
||||
"arbitrum-one" to "ethereum",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@ package com.tangem.data.tokens.utils
|
|||
|
||||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
||||
import com.tangem.domain.common.extensions.toNetworkId
|
||||
import com.tangem.domain.tokens.models.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
|
||||
internal class UserTokensResponseFactory {
|
||||
|
||||
|
|
@ -32,7 +32,7 @@ internal class UserTokensResponseFactory {
|
|||
return UserTokensResponse.Token(
|
||||
id = currency.id.rawCurrencyId,
|
||||
networkId = blockchain.toNetworkId(),
|
||||
derivationPath = currency.derivationPath,
|
||||
derivationPath = currency.network.derivationPath.value,
|
||||
name = currency.name,
|
||||
symbol = currency.symbol,
|
||||
decimals = currency.decimals,
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.data.common)
|
||||
|
||||
implementation(projects.core.utils)
|
||||
implementation(projects.core.datasource)
|
||||
implementation(projects.domain.legacy)
|
||||
|
|
@ -20,7 +22,9 @@ dependencies {
|
|||
|
||||
implementation(deps.kotlin.coroutines)
|
||||
implementation(deps.androidx.paging.runtime)
|
||||
implementation(deps.arrow.core)
|
||||
implementation(deps.timber)
|
||||
implementation(deps.jodatime)
|
||||
implementation(deps.tangem.blockchain)
|
||||
|
||||
implementation(deps.hilt.core)
|
||||
kapt(deps.hilt.kapt)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
package com.tangem.data.txhistory.di
|
||||
|
||||
import com.tangem.data.common.cache.CacheRegistry
|
||||
import com.tangem.data.txhistory.repository.DefaultTxHistoryRepository
|
||||
import com.tangem.datasource.local.txhistory.TxHistoryItemsStore
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.txhistory.repository.TxHistoryRepository
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
|
|
@ -17,10 +19,14 @@ internal object TxHistoryDataModule {
|
|||
@Provides
|
||||
@Singleton
|
||||
fun provideTxHistoryRepository(
|
||||
cacheRegistry: CacheRegistry,
|
||||
walletManagersFacade: WalletManagersFacade,
|
||||
userWalletsStore: UserWalletsStore,
|
||||
txHistoryItemsStore: TxHistoryItemsStore,
|
||||
): TxHistoryRepository = DefaultTxHistoryRepository(
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
userWalletsStore = userWalletsStore,
|
||||
cacheRegistry,
|
||||
walletManagersFacade,
|
||||
userWalletsStore,
|
||||
txHistoryItemsStore,
|
||||
)
|
||||
}
|
||||
|
|
@ -3,64 +3,75 @@ package com.tangem.data.txhistory.repository
|
|||
import androidx.paging.Pager
|
||||
import androidx.paging.PagingConfig
|
||||
import androidx.paging.PagingData
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.data.common.cache.CacheRegistry
|
||||
import com.tangem.data.txhistory.repository.paging.TxHistoryPagingSource
|
||||
import com.tangem.datasource.local.txhistory.TxHistoryItemsStore
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.tokens.models.Network
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.txhistory.models.TxHistoryItem
|
||||
import com.tangem.domain.txhistory.models.TxHistoryState
|
||||
import com.tangem.domain.txhistory.models.TxHistoryStateError
|
||||
import com.tangem.domain.txhistory.repository.TxHistoryRepository
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
class DefaultTxHistoryRepository(
|
||||
private val cacheRegistry: CacheRegistry,
|
||||
private val walletManagersFacade: WalletManagersFacade,
|
||||
private val userWalletsStore: UserWalletsStore,
|
||||
private val txHistoryItemsStore: TxHistoryItemsStore,
|
||||
) : TxHistoryRepository {
|
||||
|
||||
override suspend fun getTxHistoryItemsCount(networkId: Network.ID, derivationPath: String?): Int {
|
||||
val userWallet = getUserWallet()
|
||||
override suspend fun getTxHistoryItemsCount(userWalletId: UserWalletId, network: Network): Int {
|
||||
val userWallet = getUserWallet(userWalletId)
|
||||
val state = walletManagersFacade.getTxHistoryState(
|
||||
userWalletId = userWallet.walletId,
|
||||
networkId = networkId,
|
||||
rawDerivationPath = derivationPath,
|
||||
network = network,
|
||||
)
|
||||
return when (state) {
|
||||
is TxHistoryState.Failed.FetchError -> throw TxHistoryStateError.DataError(state.exception)
|
||||
TxHistoryState.NotImplemented -> throw TxHistoryStateError.TxHistoryNotImplemented
|
||||
TxHistoryState.Success.Empty -> throw TxHistoryStateError.EmptyTxHistories
|
||||
is TxHistoryState.NotImplemented -> throw TxHistoryStateError.TxHistoryNotImplemented
|
||||
is TxHistoryState.Success.Empty -> throw TxHistoryStateError.EmptyTxHistories
|
||||
is TxHistoryState.Success.HasTransactions -> state.txCount
|
||||
}
|
||||
}
|
||||
|
||||
override fun getTxHistoryItems(
|
||||
networkId: Network.ID,
|
||||
derivationPath: String?,
|
||||
userWalletId: UserWalletId,
|
||||
currency: CryptoCurrency,
|
||||
pageSize: Int,
|
||||
refresh: Boolean,
|
||||
): Flow<PagingData<TxHistoryItem>> {
|
||||
val userWallet = getUserWallet()
|
||||
return Pager(
|
||||
val pager = Pager(
|
||||
config = PagingConfig(
|
||||
pageSize = pageSize,
|
||||
initialLoadSize = pageSize,
|
||||
),
|
||||
pagingSourceFactory = {
|
||||
TxHistoryPagingSource(
|
||||
loadPage = { page: Int, pageSize: Int ->
|
||||
walletManagersFacade.getTxHistoryItems(
|
||||
userWalletId = userWallet.walletId,
|
||||
networkId = networkId,
|
||||
rawDerivationPath = derivationPath,
|
||||
page = page,
|
||||
pageSize = pageSize,
|
||||
)
|
||||
},
|
||||
sourceParams = TxHistoryPagingSource.Params(userWalletId, currency, pageSize, refresh),
|
||||
txHistoryItemsStore = txHistoryItemsStore,
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
cacheRegistry = cacheRegistry,
|
||||
)
|
||||
},
|
||||
).flow
|
||||
)
|
||||
|
||||
return pager.flow
|
||||
}
|
||||
|
||||
private fun getUserWallet(): UserWallet = requireNotNull(userWalletsStore.selectedUserWalletOrNull) {
|
||||
"Selected wallet must not be null"
|
||||
override fun getTxExploreUrl(txHash: String, networkId: Network.ID): String {
|
||||
val blockchain = Blockchain.fromId(networkId.value)
|
||||
return blockchain.getExploreTxUrl(txHash)
|
||||
}
|
||||
|
||||
private suspend fun getUserWallet(userWalletId: UserWalletId): UserWallet {
|
||||
return requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) {
|
||||
"Unable to find user wallet with provided ID: $userWalletId"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,34 +2,96 @@ package com.tangem.data.txhistory.repository.paging
|
|||
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.paging.PagingState
|
||||
import com.tangem.data.common.cache.CacheRegistry
|
||||
import com.tangem.datasource.local.txhistory.TxHistoryItemsStore
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.txhistory.models.PaginationWrapper
|
||||
import com.tangem.domain.txhistory.models.TxHistoryItem
|
||||
|
||||
private const val INITIAL_PAGE = 1
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import timber.log.Timber
|
||||
|
||||
internal class TxHistoryPagingSource(
|
||||
private val loadPage: suspend (page: Int, pageSize: Int) -> PaginationWrapper<TxHistoryItem>,
|
||||
private val sourceParams: Params,
|
||||
private val txHistoryItemsStore: TxHistoryItemsStore,
|
||||
private val walletManagersFacade: WalletManagersFacade,
|
||||
private val cacheRegistry: CacheRegistry,
|
||||
) : PagingSource<Int, TxHistoryItem>() {
|
||||
|
||||
private val storeKey = TxHistoryItemsStore.Key(sourceParams.userWalletId, sourceParams.currency)
|
||||
|
||||
override fun getRefreshKey(state: PagingState<Int, TxHistoryItem>): Int? {
|
||||
return state.anchorPosition?.let { anchorPosition ->
|
||||
state.closestPageToPosition(anchorPosition)?.prevKey?.plus(other = 1)
|
||||
?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(other = 1)
|
||||
val anchorPage = state.closestPageToPosition(anchorPosition)
|
||||
anchorPage?.prevKey?.inc() ?: anchorPage?.nextKey?.dec()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, TxHistoryItem> {
|
||||
val currentPage = params.key ?: INITIAL_PAGE
|
||||
return try {
|
||||
val result = loadPage(currentPage, params.loadSize)
|
||||
val pageToLoad = params.key ?: INITIAL_PAGE
|
||||
|
||||
LoadResult.Page(
|
||||
data = result.items,
|
||||
prevKey = if (currentPage > INITIAL_PAGE) currentPage.minus(1) else null,
|
||||
nextKey = if (result.page < result.totalPages) currentPage.plus(1) else null,
|
||||
return try {
|
||||
val wrappedItems = loadItems(
|
||||
pageToLoad = pageToLoad,
|
||||
pageSize = sourceParams.pageSize,
|
||||
refresh = sourceParams.refresh && params is LoadParams.Refresh,
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
|
||||
val items = wrappedItems.items
|
||||
val prevPage = when {
|
||||
items.isEmpty() -> null
|
||||
pageToLoad > INITIAL_PAGE -> pageToLoad.dec()
|
||||
else -> null
|
||||
}
|
||||
val nextPage = when {
|
||||
items.isEmpty() -> INITIAL_PAGE
|
||||
pageToLoad < wrappedItems.totalPages -> pageToLoad.inc()
|
||||
else -> null
|
||||
}
|
||||
|
||||
LoadResult.Page(items, prevKey = prevPage, nextKey = nextPage)
|
||||
} catch (e: Throwable) {
|
||||
Timber.e(e, "Unable to load the transaction history for the requested page: $pageToLoad")
|
||||
|
||||
LoadResult.Error(e)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loadItems(pageToLoad: Int, pageSize: Int, refresh: Boolean): PaginationWrapper<TxHistoryItem> {
|
||||
cacheRegistry.invokeOnExpire(
|
||||
key = getTxHistoryPageKey(pageToLoad),
|
||||
skipCache = refresh,
|
||||
block = { fetch(pageToLoad, pageSize) },
|
||||
)
|
||||
|
||||
return requireNotNull(txHistoryItemsStore.getSyncOrNull(storeKey, pageToLoad)) {
|
||||
"The transaction history page #$pageToLoad could not be retrieved"
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetch(pageToLoad: Int, pageSize: Int) {
|
||||
val wrappedItems = walletManagersFacade.getTxHistoryItems(
|
||||
userWalletId = sourceParams.userWalletId,
|
||||
currency = sourceParams.currency,
|
||||
page = pageToLoad,
|
||||
pageSize = pageSize,
|
||||
)
|
||||
|
||||
txHistoryItemsStore.store(storeKey, wrappedItems)
|
||||
}
|
||||
|
||||
private fun getTxHistoryPageKey(page: Int): String {
|
||||
return "tx_history_page_${sourceParams.currency}_${sourceParams.userWalletId}_$page"
|
||||
}
|
||||
|
||||
data class Params(
|
||||
val userWalletId: UserWalletId,
|
||||
val currency: CryptoCurrency,
|
||||
val pageSize: Int,
|
||||
val refresh: Boolean,
|
||||
)
|
||||
|
||||
private companion object {
|
||||
private const val INITIAL_PAGE = 1
|
||||
}
|
||||
}
|
||||
|
|
@ -11,6 +11,7 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.core.datasource)
|
||||
implementation(projects.core.utils)
|
||||
implementation(projects.data.source.preferences)
|
||||
implementation(projects.domain.wallets)
|
||||
|
|
|
|||
|
|
@ -1,16 +1,29 @@
|
|||
package com.tangem.data.wallets
|
||||
|
||||
import com.tangem.data.source.preferences.PreferencesDataSource
|
||||
import com.tangem.datasource.local.userwallet.ShouldSaveUserWalletStore
|
||||
import com.tangem.domain.wallets.repository.WalletsRepository
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
internal class DefaultWalletsRepository(
|
||||
private val preferencesDataSource: PreferencesDataSource,
|
||||
private val shouldSaveUserWalletStore: ShouldSaveUserWalletStore,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : WalletsRepository {
|
||||
|
||||
override suspend fun shouldSaveUserWallets(): Boolean {
|
||||
return withContext(dispatchers.io) { preferencesDataSource.shouldSaveUserWallets }
|
||||
override suspend fun initialize() {
|
||||
withContext(dispatchers.io) {
|
||||
shouldSaveUserWalletStore.getSyncOrNull() ?: shouldSaveUserWalletStore.store(item = false)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun shouldSaveUserWalletsSync(): Boolean {
|
||||
return withContext(dispatchers.io) { shouldSaveUserWalletStore.getSyncOrNull() ?: false }
|
||||
}
|
||||
|
||||
override fun shouldSaveUserWallets(): Flow<Boolean> = shouldSaveUserWalletStore.get()
|
||||
|
||||
override suspend fun saveShouldSaveUserWallets(item: Boolean) {
|
||||
withContext(dispatchers.io) { shouldSaveUserWalletStore.store(item = item) }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
package com.tangem.data.wallets.di
|
||||
|
||||
import com.tangem.data.source.preferences.PreferencesDataSource
|
||||
import com.tangem.data.wallets.DefaultWalletsRepository
|
||||
import com.tangem.datasource.local.userwallet.ShouldSaveUserWalletStore
|
||||
import com.tangem.domain.wallets.repository.WalletsRepository
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
|
|
@ -17,11 +17,11 @@ object WalletsDataModule {
|
|||
@Provides
|
||||
@Singleton
|
||||
fun providesWalletsRepository(
|
||||
preferencesDataSource: PreferencesDataSource,
|
||||
shouldSaveUserWalletStore: ShouldSaveUserWalletStore,
|
||||
coroutineDispatcherProvider: CoroutineDispatcherProvider,
|
||||
): WalletsRepository {
|
||||
return DefaultWalletsRepository(
|
||||
preferencesDataSource = preferencesDataSource,
|
||||
shouldSaveUserWalletStore = shouldSaveUserWalletStore,
|
||||
dispatchers = coroutineDispatcherProvider,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue