Updated on 2026-08-14
This commit is contained in:
commit
af09ce9ca4
877 changed files with 16091 additions and 6613 deletions
|
|
@ -33,8 +33,11 @@ dependencies {
|
|||
api(projects.domain.models)
|
||||
api(projects.domain.tokens)
|
||||
api(projects.domain.wallets)
|
||||
api(projects.domain.visa)
|
||||
// endregion
|
||||
|
||||
implementation(projects.features.tangempay.details.api) // Remove after TANGEM_PAY_ACCOUNTS_REFACTOR_ENABLED
|
||||
|
||||
// region Project - Data
|
||||
implementation(projects.data.common)
|
||||
// endregion
|
||||
|
|
@ -47,6 +50,7 @@ dependencies {
|
|||
// region Tangem dependencies
|
||||
implementation(tangemDeps.card.core)
|
||||
implementation(tangemDeps.blockchain)
|
||||
implementation(tangemDeps.hot.core)
|
||||
// endregion
|
||||
|
||||
// region DI
|
||||
|
|
@ -63,7 +67,6 @@ dependencies {
|
|||
implementation(deps.kotlin.coroutines)
|
||||
implementation(deps.moshi)
|
||||
implementation(deps.moshi.kotlin)
|
||||
implementation(deps.timber)
|
||||
// endregion
|
||||
|
||||
// region Test
|
||||
|
|
|
|||
|
|
@ -7,9 +7,9 @@ import com.tangem.domain.account.fetcher.SingleAccountListFetcher
|
|||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -30,7 +30,7 @@ internal class DefaultMultiAccountListFetcher @Inject constructor(
|
|||
when (params) {
|
||||
is MultiAccountListFetcher.Params.Set -> {
|
||||
if (params.ids.isEmpty()) {
|
||||
Timber.d("No wallet ids provided to fetch accounts.")
|
||||
TangemLogger.d("No wallet ids provided to fetch accounts.")
|
||||
return@either
|
||||
}
|
||||
|
||||
|
|
@ -52,7 +52,7 @@ internal class DefaultMultiAccountListFetcher @Inject constructor(
|
|||
"Failed to fetch accounts for wallets:\n${errors.entries.joinToString(separator = "\n")}",
|
||||
)
|
||||
|
||||
Timber.e(exception)
|
||||
TangemLogger.e("Error", exception)
|
||||
|
||||
raise(exception)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResp
|
|||
import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO
|
||||
import com.tangem.datasource.api.tangemTech.models.account.toUserTokensResponse
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import timber.log.Timber
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
|
|
@ -60,7 +60,7 @@ internal class FetchWalletAccountsErrorHandler @Inject constructor(
|
|||
): FetchResult {
|
||||
val isResponseUpToDate = error.isNetworkError(code = Code.NOT_MODIFIED)
|
||||
if (isResponseUpToDate) {
|
||||
Timber.e("ETag is up to date, no need to update accounts for wallet: $userWalletId")
|
||||
TangemLogger.e("ETag is up to date, no need to update accounts for wallet: $userWalletId")
|
||||
val response = requireNotNull(savedAccountsResponse) {
|
||||
"Saved accounts response is null for wallet: $userWalletId"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,18 @@
|
|||
package com.tangem.data.account.producer
|
||||
|
||||
import arrow.core.Option
|
||||
import arrow.core.getOrElse
|
||||
import arrow.core.none
|
||||
import com.tangem.common.card.FirmwareVersion
|
||||
import com.tangem.domain.account.models.AccountList
|
||||
import com.tangem.domain.account.producer.SingleAccountListProducer
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.common.wallets.getSyncStrict
|
||||
import com.tangem.domain.core.flow.FlowProducerTools
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.features.tangempay.TangemPayFeatureToggles
|
||||
import com.tangem.hot.sdk.model.HotWalletId
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
|
|
@ -12,6 +20,7 @@ import dagger.assisted.AssistedInject
|
|||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
/**
|
||||
* Default implementation of [SingleAccountListProducer].
|
||||
|
|
@ -27,6 +36,8 @@ internal class DefaultSingleAccountListProducer @AssistedInject constructor(
|
|||
@Assisted val params: SingleAccountListProducer.Params,
|
||||
override val flowProducerTools: FlowProducerTools,
|
||||
private val walletAccountListFlowFactory: WalletAccountListFlowFactory,
|
||||
private val tangemPayFeatureToggles: TangemPayFeatureToggles,
|
||||
private val userWalletsListRepository: UserWalletsListRepository,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : SingleAccountListProducer {
|
||||
|
||||
|
|
@ -34,8 +45,32 @@ internal class DefaultSingleAccountListProducer @AssistedInject constructor(
|
|||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
override fun produce(): Flow<AccountList> {
|
||||
return walletAccountListFlowFactory.create(userWalletId = params.userWalletId)
|
||||
.flowOn(dispatchers.default)
|
||||
val accountListFlow: Flow<AccountList> = if (tangemPayFeatureToggles.isTangemPayAccountsRefactorEnabled) {
|
||||
combineWithPaymentAccount()
|
||||
} else {
|
||||
walletAccountListFlowFactory.create(userWalletId = params.userWalletId)
|
||||
}
|
||||
|
||||
return accountListFlow.flowOn(dispatchers.default)
|
||||
}
|
||||
|
||||
private fun combineWithPaymentAccount(): Flow<AccountList> {
|
||||
return walletAccountListFlowFactory.create(params.userWalletId)
|
||||
.map { accountList ->
|
||||
val userWallet = userWalletsListRepository.getSyncStrict(id = params.userWalletId)
|
||||
if (userWallet.isPaymentAccountSupported()) {
|
||||
accountList.plus(Account.Payment(params.userWalletId)).getOrElse { throwable ->
|
||||
error("Can not combine account list and payment account status: $throwable")
|
||||
}
|
||||
} else {
|
||||
accountList
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun UserWallet.isPaymentAccountSupported(): Boolean = when (this) {
|
||||
is UserWallet.Cold -> scanResponse.card.firmwareVersion >= FirmwareVersion.HDWalletAvailable
|
||||
is UserWallet.Hot -> hotWalletId.authType != HotWalletId.AuthType.NoPassword
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
|
|
|
|||
|
|
@ -32,10 +32,10 @@ import com.tangem.domain.models.account.AccountName
|
|||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.extensions.replaceBy
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
|
|
@ -174,7 +174,7 @@ internal class DefaultAccountsCRUDRepository(
|
|||
val response = getAccountsResponseSync(userWalletId = userWalletId)
|
||||
|
||||
if (response == null) {
|
||||
Timber.e("Can't sync tokens. No accounts response found for wallet: $userWalletId")
|
||||
TangemLogger.e("Can't sync tokens. No accounts response found for wallet: $userWalletId")
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,12 +20,12 @@ import com.tangem.datasource.api.tangemTech.models.account.toUserTokensResponse
|
|||
import com.tangem.datasource.local.accounts.AccountTokenMigrationStore
|
||||
import com.tangem.datasource.utils.getSyncOrNull
|
||||
import com.tangem.domain.account.tokens.MainAccountTokensMigration
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import com.tangem.domain.models.account.DerivationIndex
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.lib.crypto.derivation.AccountNodeRecognizer
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Implementation of [MainAccountTokensMigration] for migrating tokens associated with a main account.
|
||||
|
|
@ -49,7 +49,7 @@ internal class DefaultMainAccountTokensMigration(
|
|||
val response = store.getSyncOrNull()
|
||||
ensureNotNull(response) {
|
||||
val exception = IllegalStateException("No cached accounts response found")
|
||||
Timber.e(exception)
|
||||
TangemLogger.e("Error", exception)
|
||||
exception
|
||||
}
|
||||
val mainAccount = findAccount(response = response, derivationIndex = DerivationIndex.Main)
|
||||
|
|
@ -57,7 +57,7 @@ internal class DefaultMainAccountTokensMigration(
|
|||
.filterNot { accountDTO -> accountDTO.derivationIndex.toDerivationIndex().isMain }
|
||||
|
||||
if (customAccounts.isEmpty()) {
|
||||
Timber.i("There is only the Main account. Nothing to migrate")
|
||||
TangemLogger.i("There is only the Main account. Nothing to migrate")
|
||||
return@either response
|
||||
}
|
||||
|
||||
|
|
@ -66,7 +66,7 @@ internal class DefaultMainAccountTokensMigration(
|
|||
.filter { it.key.value in customAccountIndexes }
|
||||
|
||||
if (unassignedTokens.isEmpty()) {
|
||||
Timber.i("No unassigned tokens found for migration")
|
||||
TangemLogger.i("No unassigned tokens found for migration")
|
||||
return@either response
|
||||
}
|
||||
|
||||
|
|
@ -103,7 +103,7 @@ internal class DefaultMainAccountTokensMigration(
|
|||
derivationIndex: DerivationIndex,
|
||||
): Either<Throwable, Unit> = either {
|
||||
if (derivationIndex == DerivationIndex.Main) {
|
||||
Timber.i("Migration skipped: derivation index is Main")
|
||||
TangemLogger.i("Migration skipped: derivation index is Main")
|
||||
return@either
|
||||
}
|
||||
|
||||
|
|
@ -113,7 +113,7 @@ internal class DefaultMainAccountTokensMigration(
|
|||
|
||||
ensureNotNull(response) {
|
||||
val exception = IllegalStateException("No cached accounts response found")
|
||||
Timber.e(exception)
|
||||
TangemLogger.e("Error", exception)
|
||||
exception
|
||||
}
|
||||
|
||||
|
|
@ -123,7 +123,7 @@ internal class DefaultMainAccountTokensMigration(
|
|||
val unassignedTokens = mainAccount.findUnassignedTokens(derivationIndex)
|
||||
|
||||
if (unassignedTokens.isNullOrEmpty()) {
|
||||
Timber.i("No unassigned tokens found for migration")
|
||||
TangemLogger.i("No unassigned tokens found for migration")
|
||||
return@either
|
||||
}
|
||||
|
||||
|
|
@ -158,7 +158,7 @@ internal class DefaultMainAccountTokensMigration(
|
|||
|
||||
return ensureNotNull(account) {
|
||||
val exception = IllegalStateException("No account found with derivation index: $derivationIndex")
|
||||
Timber.e(exception)
|
||||
TangemLogger.e("Error", exception)
|
||||
exception
|
||||
}
|
||||
}
|
||||
|
|
@ -198,13 +198,13 @@ internal class DefaultMainAccountTokensMigration(
|
|||
return filter { savedToken ->
|
||||
val blockchain = Blockchain.fromNetworkId(networkId = savedToken.networkId)
|
||||
if (blockchain == null) {
|
||||
Timber.e("Token has unknown networkId: $savedToken")
|
||||
TangemLogger.e("Token has unknown networkId: $savedToken")
|
||||
return@filter false
|
||||
}
|
||||
|
||||
val derivationPathValue = savedToken.derivationPath
|
||||
if (derivationPathValue == null) {
|
||||
Timber.e("Token has no derivation path: $savedToken")
|
||||
TangemLogger.e("Token has no derivation path: $savedToken")
|
||||
return@filter false
|
||||
}
|
||||
|
||||
|
|
@ -212,7 +212,7 @@ internal class DefaultMainAccountTokensMigration(
|
|||
val accountNodeValue = accountNodeRecognizer.recognize(derivationPathValue)
|
||||
|
||||
if (accountNodeValue == null) {
|
||||
Timber.e("Token has unrecognized derivation path: $savedToken")
|
||||
TangemLogger.e("Token has unrecognized derivation path: $savedToken")
|
||||
return@filter false
|
||||
}
|
||||
|
||||
|
|
@ -230,7 +230,7 @@ internal class DefaultMainAccountTokensMigration(
|
|||
eTagsStore.clear(userWalletId = userWalletId, key = ETagsStore.Key.WalletAccounts)
|
||||
}
|
||||
val exception = IllegalStateException("Failed to push updated tokens after migration")
|
||||
Timber.e(exception)
|
||||
TangemLogger.e("Error", exception)
|
||||
raise(exception)
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -3,10 +3,12 @@ package com.tangem.data.account.producer
|
|||
import com.google.common.truth.Truth
|
||||
import com.tangem.domain.account.models.AccountList
|
||||
import com.tangem.domain.account.producer.SingleAccountListProducer
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.core.flow.FlowProducerTools
|
||||
import com.tangem.domain.models.TokensSortType
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.features.tangempay.TangemPayFeatureToggles
|
||||
import com.tangem.test.core.getEmittedValues
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import io.mockk.*
|
||||
|
|
@ -29,6 +31,10 @@ class DefaultSingleAccountListProducerTest {
|
|||
|
||||
private val userWalletId = UserWalletId("011")
|
||||
private val flowProducerTools: FlowProducerTools = mockk()
|
||||
private val tangemPayFeatureToggles = mockk<TangemPayFeatureToggles> {
|
||||
every { this@mockk.isTangemPayAccountsRefactorEnabled } returns false
|
||||
}
|
||||
private val userWalletsListRepository = mockk<UserWalletsListRepository>()
|
||||
private val userWallet = mockk<UserWallet> {
|
||||
every { this@mockk.walletId } returns userWalletId
|
||||
}
|
||||
|
|
@ -38,6 +44,8 @@ class DefaultSingleAccountListProducerTest {
|
|||
walletAccountListFlowFactory = walletAccountListFlowFactory,
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
flowProducerTools = flowProducerTools,
|
||||
tangemPayFeatureToggles = tangemPayFeatureToggles,
|
||||
userWalletsListRepository = userWalletsListRepository,
|
||||
)
|
||||
|
||||
@AfterEach
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ dependencies {
|
|||
|
||||
/** Project - Analytics */
|
||||
implementation(projects.core.analytics.models)
|
||||
implementation(projects.core.utils)
|
||||
|
||||
/** Project - Data */
|
||||
implementation(projects.core.datasource)
|
||||
|
|
@ -32,5 +33,4 @@ dependencies {
|
|||
|
||||
/** Other */
|
||||
implementation(deps.kotlin.coroutines)
|
||||
implementation(deps.timber)
|
||||
}
|
||||
|
|
@ -33,5 +33,4 @@ dependencies {
|
|||
/** Other */
|
||||
implementation(deps.jodatime)
|
||||
implementation(deps.kotlin.coroutines)
|
||||
implementation(deps.timber)
|
||||
}
|
||||
|
|
@ -25,10 +25,10 @@ internal class FlipListener(private val action: () -> Unit) : SensorEventListene
|
|||
isScreenDown = true
|
||||
lastTriggerTime = currentTime
|
||||
// TODO add module logging
|
||||
// Timber.tag("onSensorChanged").d("screen down")
|
||||
// Logger.withTag("onSensorChanged").d("screen down")
|
||||
} else if (zAxisValue >= zAxisThreshold) {
|
||||
if (isScreenDown && currentTime - lastTriggerTime <= throttleTimeMs) {
|
||||
// Timber.tag("onSensorChanged").d("screen up!")
|
||||
// Logger.withTag("onSensorChanged").d("screen up!")
|
||||
lastTriggerTime = currentTime
|
||||
action.invoke()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,7 +45,6 @@ dependencies {
|
|||
implementation(deps.arrow.core)
|
||||
implementation(deps.jodatime)
|
||||
implementation(deps.kotlin.coroutines)
|
||||
implementation(deps.timber)
|
||||
|
||||
/* Test */
|
||||
testImplementation(projects.common.test)
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ 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 com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import timber.log.Timber
|
||||
import kotlin.time.Duration
|
||||
|
||||
/**
|
||||
|
|
@ -67,7 +67,7 @@ suspend inline fun <T> safeApiCall(
|
|||
): T = recover(
|
||||
block = { call(ApiResponseRaise(raise = this)) },
|
||||
recover = { error ->
|
||||
Timber.e(error, "Unable to perform safe API call")
|
||||
TangemLogger.e("Unable to perform safe API call", error)
|
||||
onError(error)
|
||||
},
|
||||
)
|
||||
|
|
@ -2,13 +2,13 @@ package com.tangem.data.common.cache
|
|||
|
||||
import com.tangem.datasource.local.cache.CacheKeysStore
|
||||
import com.tangem.datasource.local.cache.model.CacheKey
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.joda.time.Duration
|
||||
import org.joda.time.LocalDateTime
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
internal class DefaultCacheRegistry(
|
||||
|
|
@ -26,17 +26,17 @@ internal class DefaultCacheRegistry(
|
|||
}
|
||||
|
||||
override suspend fun invalidate(key: String) {
|
||||
Timber.d("Invalidate the cache key: $key")
|
||||
TangemLogger.d("Invalidate the cache key: $key")
|
||||
withContext(NonCancellable) { cacheKeysStore.remove(key) }
|
||||
}
|
||||
|
||||
override suspend fun invalidate(keys: Collection<String>) {
|
||||
Timber.d("Invalidate cache keys: $keys")
|
||||
TangemLogger.d("Invalidate cache keys: $keys")
|
||||
withContext(NonCancellable) { cacheKeysStore.remove(keys) }
|
||||
}
|
||||
|
||||
override suspend fun invalidateAll() {
|
||||
Timber.d("Invalidate all cache keys")
|
||||
TangemLogger.d("Invalidate all cache keys")
|
||||
withContext(NonCancellable) { cacheKeysStore.clear() }
|
||||
}
|
||||
|
||||
|
|
@ -58,7 +58,7 @@ internal class DefaultCacheRegistry(
|
|||
}
|
||||
|
||||
try {
|
||||
Timber.d("Invoke the action associated with the cache key: $key")
|
||||
TangemLogger.d("Invoke the action associated with the cache key: $key")
|
||||
|
||||
cacheKeysStore.store(
|
||||
key = CacheKey(
|
||||
|
|
@ -70,7 +70,7 @@ internal class DefaultCacheRegistry(
|
|||
|
||||
block()
|
||||
} catch (e: Throwable) {
|
||||
Timber.e(e, "The action related to the cache key has failed: $key")
|
||||
TangemLogger.e("The action related to the cache key has failed: $key", e)
|
||||
|
||||
invalidate(key)
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import com.tangem.datasource.local.preferences.AppPreferencesStore
|
|||
import com.tangem.datasource.local.preferences.utils.getSyncOrNull
|
||||
import com.tangem.datasource.local.preferences.utils.store
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import timber.log.Timber
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
|
||||
/**
|
||||
* Default implementation of the [ETagsStore] interface for managing ETag values
|
||||
|
|
@ -25,7 +25,7 @@ internal class DefaultETagsStore(
|
|||
|
||||
override suspend fun store(userWalletId: UserWalletId, key: ETagsStore.Key, value: String) {
|
||||
if (value.isBlank()) {
|
||||
Timber.e("ETag value is blank, not storing it. userWalletId: $userWalletId, key: $key")
|
||||
TangemLogger.e("ETag value is blank, not storing it. userWalletId: $userWalletId, key: $key")
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import com.tangem.domain.models.account.DerivationIndex
|
|||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import timber.log.Timber
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import com.tangem.blockchain.common.Token as SdkToken
|
||||
|
||||
// FIXME: Make internal
|
||||
|
|
@ -52,7 +52,7 @@ class CryptoCurrencyFactory(
|
|||
accountIndex: DerivationIndex? = null,
|
||||
): CryptoCurrency.Token? {
|
||||
if (blockchain == Blockchain.Unknown) {
|
||||
Timber.e("Unable to map the SDK token to the domain token with Unknown blockchain")
|
||||
TangemLogger.e("Unable to map the SDK token to the domain token with Unknown blockchain")
|
||||
return null
|
||||
}
|
||||
|
||||
|
|
@ -93,7 +93,7 @@ class CryptoCurrencyFactory(
|
|||
accountIndex = accountIndex,
|
||||
)
|
||||
} else {
|
||||
Timber.e("Unable to get blockchain from chainId == $chainId")
|
||||
TangemLogger.e("Unable to get blockchain from chainId == $chainId")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
|
@ -105,7 +105,7 @@ class CryptoCurrencyFactory(
|
|||
accountIndex: DerivationIndex? = null,
|
||||
): CryptoCurrency.Coin? {
|
||||
if (blockchain == Blockchain.Unknown) {
|
||||
Timber.e("Unable to map the SDK token to the domain token with Unknown blockchain")
|
||||
TangemLogger.e("Unable to map the SDK token to the domain token with Unknown blockchain")
|
||||
return null
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import com.tangem.domain.models.account.DerivationIndex
|
|||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import timber.log.Timber
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import javax.inject.Inject
|
||||
import com.tangem.blockchain.common.Token as SdkToken
|
||||
|
||||
|
|
@ -46,7 +46,7 @@ class ResponseCryptoCurrenciesFactory @Inject constructor(
|
|||
): 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}")
|
||||
TangemLogger.e("Unable to find a blockchain with the network ID: ${responseToken.networkId}")
|
||||
return null
|
||||
}
|
||||
|
||||
|
|
@ -71,7 +71,7 @@ class ResponseCryptoCurrenciesFactory @Inject constructor(
|
|||
): 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}")
|
||||
TangemLogger.e("Unable to find a blockchain with the network ID: ${responseToken.networkId}")
|
||||
return null
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import com.tangem.domain.models.account.AccountId
|
|||
import com.tangem.domain.models.account.DerivationIndex
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.lib.crypto.derivation.AccountNodeRecognizer
|
||||
import timber.log.Timber
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
|
||||
/**
|
||||
* Enriches the [UserTokensResponse] with accountId values for tokens
|
||||
|
|
@ -61,20 +61,20 @@ object UserTokensResponseAccountIdEnricher {
|
|||
.groupBy { savedToken ->
|
||||
val derivationPathValue = savedToken.derivationPath
|
||||
if (derivationPathValue == null) {
|
||||
Timber.e("Token $savedToken has no derivation path")
|
||||
TangemLogger.e("Token $savedToken has no derivation path")
|
||||
return@groupBy null
|
||||
}
|
||||
|
||||
val blockchain = Blockchain.fromNetworkId(networkId = savedToken.networkId)
|
||||
if (blockchain == null) {
|
||||
Timber.e("Token $savedToken has unknown networkId")
|
||||
TangemLogger.e("Token $savedToken has unknown networkId")
|
||||
return@groupBy null
|
||||
}
|
||||
|
||||
val accountNodeRecognizer = AccountNodeRecognizer(blockchain)
|
||||
val accountIndex = accountNodeRecognizer.recognize(derivationPathValue)
|
||||
if (accountIndex == null) {
|
||||
Timber.e("Token $savedToken has unrecognized derivation path")
|
||||
TangemLogger.e("Token $savedToken has unrecognized derivation path")
|
||||
return@groupBy null
|
||||
}
|
||||
|
||||
|
|
@ -89,7 +89,7 @@ object UserTokensResponseAccountIdEnricher {
|
|||
if (accountIndex == null) return@mapKeys null
|
||||
|
||||
val derivationIndex = DerivationIndex.invoke(value = accountIndex.toInt()).getOrElse {
|
||||
Timber.e("Failed to parse derivation index from account index: $accountIndex")
|
||||
TangemLogger.e("Failed to parse derivation index from account index: $accountIndex")
|
||||
return@mapKeys null
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,10 +12,10 @@ import com.tangem.domain.common.wallets.getSyncOrNull
|
|||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import com.tangem.utils.retryer.Retryer
|
||||
import com.tangem.utils.retryer.RetryerPool
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
class UserTokensSaver(
|
||||
|
|
@ -36,7 +36,7 @@ class UserTokensSaver(
|
|||
val userWallet = userWalletsListRepository.getSyncOrNull(id = userWalletId)
|
||||
|
||||
if (userWallet == null) {
|
||||
Timber.e("UserWallet with id $userWalletId not found. Cannot push tokens.")
|
||||
TangemLogger.e("UserWallet with id $userWalletId not found. Cannot push tokens.")
|
||||
onFailSend()
|
||||
return@withContext
|
||||
}
|
||||
|
|
@ -116,7 +116,7 @@ class UserTokensSaver(
|
|||
userWalletId = userWalletId,
|
||||
response = response,
|
||||
onFailSend = {
|
||||
Timber.e(
|
||||
TangemLogger.e(
|
||||
"Retryer: Failed to push updated tokens on attempt ${iteration + 1} for $userWalletId",
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import com.tangem.domain.models.wallet.UserWallet
|
|||
import com.tangem.domain.wallets.derivations.DerivationStyleProvider
|
||||
import com.tangem.domain.wallets.derivations.derivationStyleProvider
|
||||
import com.tangem.lib.crypto.derivation.toMutable
|
||||
import timber.log.Timber
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
|
|
@ -154,11 +154,11 @@ class NetworkFactory @Inject constructor(
|
|||
|
||||
private fun Blockchain.isBlockchainSupported(): Boolean {
|
||||
if (this == Blockchain.Unknown) {
|
||||
Timber.w("Unable to convert Unknown blockchain to the domain network model")
|
||||
TangemLogger.w("Unable to convert Unknown blockchain to the domain network model")
|
||||
return false
|
||||
}
|
||||
if (this in excludedBlockchains) {
|
||||
Timber.w("Unable to convert excluded blockchain to the domain network model")
|
||||
TangemLogger.w("Unable to convert excluded blockchain to the domain network model")
|
||||
return false
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,11 +15,11 @@ import com.tangem.datasource.api.tangemTech.TangemTechApi
|
|||
import com.tangem.datasource.api.tangemTech.models.QuotesResponse
|
||||
import com.tangem.domain.core.utils.eitherOn
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.joda.time.DateTime
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
|
|
@ -194,7 +194,7 @@ internal class DefaultQuotesFetcher(
|
|||
return this
|
||||
}
|
||||
|
||||
Timber.d("Some quotes are missing from the server response: $skippedIds")
|
||||
TangemLogger.d("Some quotes are missing from the server response: $skippedIds")
|
||||
|
||||
val emptyQuotes = skippedIds.associateWith { QuotesResponse.Quote.EMPTY }
|
||||
|
||||
|
|
|
|||
|
|
@ -43,8 +43,9 @@ interface QuotesFetcher {
|
|||
PRICE_CHANGE_24H(value = "priceChange24h"),
|
||||
PRICE_CHANGE_1W(value = "priceChange1w"),
|
||||
PRICE_CHANGE_30D(value = "priceChange30d"),
|
||||
PRICE_USD(value = "priceUsd"),
|
||||
ALL_PRICES(
|
||||
value = setOf(PRICE, PRICE_CHANGE_24H, PRICE_CHANGE_1W, PRICE_CHANGE_30D).combine(),
|
||||
value = setOf(PRICE, PRICE_CHANGE_24H, PRICE_CHANGE_1W, PRICE_CHANGE_30D, PRICE_USD).combine(),
|
||||
),
|
||||
LAST_UPDATED_AT(value = "lastUpdatedAt"),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
package com.tangem.data.common.utils
|
||||
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.currentCoroutineContext
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.ensureActive
|
||||
import kotlinx.coroutines.yield
|
||||
import timber.log.Timber
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
|
||||
@Suppress("UnconditionalJumpStatementInLoop", "MagicNumber")
|
||||
|
|
@ -19,7 +19,7 @@ suspend fun <T> retryOnError(priority: Boolean = false, startRetryDelay: Int = 5
|
|||
currentCoroutineContext().ensureActive()
|
||||
}
|
||||
|
||||
Timber.e(e, "Error occurred during retryOnError block")
|
||||
TangemLogger.e("Error occurred during retryOnError block", e)
|
||||
|
||||
if (priority && priorityCounter > 0) {
|
||||
--priorityCounter
|
||||
|
|
|
|||
|
|
@ -37,7 +37,6 @@ dependencies {
|
|||
// region Other libraries
|
||||
implementation(deps.androidx.datastore)
|
||||
implementation(deps.moshi.kotlin)
|
||||
implementation(deps.timber)
|
||||
implementation(tangemDeps.blockchain)
|
||||
// endregion
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,6 @@ dependencies {
|
|||
/** Other */
|
||||
implementation(deps.moshi)
|
||||
implementation(deps.moshi.kotlin)
|
||||
implementation(deps.timber)
|
||||
|
||||
/** DI */
|
||||
implementation(deps.hilt.android)
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import com.tangem.domain.express.models.ExpressProviderType
|
|||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.extensions.filterIf
|
||||
import timber.log.Timber
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
|
||||
internal class DefaultExpressRepository(
|
||||
private val tangemExpressApi: TangemExpressApi,
|
||||
|
|
@ -36,7 +36,7 @@ internal class DefaultExpressRepository(
|
|||
.filterIf(filterProviderTypes.isNotEmpty()) { it.type in filterProviderTypes }
|
||||
},
|
||||
onError = { error ->
|
||||
Timber.w(error, "Unable to fetch express providers")
|
||||
TangemLogger.w("Unable to fetch express providers", error)
|
||||
throw error
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -22,11 +22,11 @@ import com.tangem.domain.express.models.ExpressAsset
|
|||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
typealias InitializationStatusFlow = MutableStateFlow<Lce<Throwable, List<ExpressAsset>>>
|
||||
|
|
@ -85,7 +85,7 @@ internal class DefaultExpressServiceFetcher @Inject constructor(
|
|||
if (expressAssetsStore.getSyncOrNull(userWallet.walletId) == null) {
|
||||
initializationStatus.update { e.lceError() }
|
||||
}
|
||||
Timber.e(e, "Unable to fetch assets for: ${userWallet.walletId.stringValue}")
|
||||
TangemLogger.e("Unable to fetch assets for: ${userWallet.walletId.stringValue}", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ dependencies {
|
|||
// endregion
|
||||
|
||||
// Other libraries
|
||||
implementation(deps.timber)
|
||||
// endregion
|
||||
|
||||
// region Core modules
|
||||
|
|
|
|||
|
|
@ -14,10 +14,10 @@ import com.tangem.domain.models.scan.ScanResponse
|
|||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import com.tangem.utils.version.AppVersionProvider
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
|
|
@ -102,7 +102,7 @@ internal class DefaultFeedbackRepository(
|
|||
|
||||
override fun getBlockchainErrorInfo(userWalletId: UserWalletId): BlockchainErrorInfo? {
|
||||
return blockchainsErrors.value[userWalletId].also {
|
||||
if (it == null) Timber.e("Blockchain error info is null for $userWalletId")
|
||||
if (it == null) TangemLogger.e("Blockchain error info is null for $userWalletId")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -51,7 +51,6 @@ dependencies {
|
|||
|
||||
/** Other */
|
||||
implementation(deps.moshi.kotlin)
|
||||
implementation(deps.timber)
|
||||
ksp(deps.moshi.kotlin.codegen)
|
||||
kaptForObfuscatingVariants(deps.retrofit.response.type.keeper)
|
||||
}
|
||||
|
|
@ -22,7 +22,7 @@ import com.tangem.domain.models.network.Network
|
|||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.wallets.derivations.derivationStyleProvider
|
||||
import com.tangem.lib.crypto.BlockchainUtils
|
||||
import timber.log.Timber
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
|
||||
internal class ManagedCryptoCurrencyFactory(
|
||||
private val networkFactory: NetworkFactory,
|
||||
|
|
@ -223,7 +223,7 @@ internal class ManagedCryptoCurrencyFactory(
|
|||
blockchain.canHandleTokens() -> {
|
||||
val formattedContractAddress = blockchain.reformatContractAddress(contractAddress)
|
||||
if (formattedContractAddress == null) {
|
||||
Timber.w("Couldn't reformat $contractAddress")
|
||||
TangemLogger.w("Couldn't reformat $contractAddress")
|
||||
return null
|
||||
}
|
||||
SourceNetwork.Default(
|
||||
|
|
|
|||
|
|
@ -45,7 +45,6 @@ dependencies {
|
|||
implementation(deps.jodatime)
|
||||
implementation(deps.moshi)
|
||||
implementation(deps.moshi.kotlin)
|
||||
implementation(deps.timber)
|
||||
implementation(tangemDeps.blockchain)
|
||||
ksp(deps.moshi.kotlin.codegen)
|
||||
kaptForObfuscatingVariants(deps.retrofit.response.type.keeper)
|
||||
|
|
|
|||
|
|
@ -105,6 +105,7 @@ internal class TokenMarketInfoConverter(
|
|||
private fun TokenMarketInfoResponse.Metrics.convert(): TokenMarketInfo.Metrics {
|
||||
return TokenMarketInfo.Metrics(
|
||||
marketRating = marketRating,
|
||||
marketRatingChange24h = marketRatingChange24h,
|
||||
circulatingSupply = circulatingSupply,
|
||||
marketCap = marketCap,
|
||||
volume24h = volume24h,
|
||||
|
|
|
|||
|
|
@ -45,7 +45,6 @@ dependencies {
|
|||
// region Other libraries
|
||||
implementation(deps.androidx.datastore)
|
||||
implementation(deps.moshi)
|
||||
implementation(deps.timber)
|
||||
// endregion
|
||||
|
||||
// region Tests
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ package com.tangem.data.networks.converters
|
|||
import com.tangem.datasource.local.network.entity.NetworkStatusDM
|
||||
import com.tangem.domain.models.network.NetworkAddress
|
||||
import com.tangem.utils.converter.TwoWayConverter
|
||||
import timber.log.Timber
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
|
||||
/**
|
||||
* Converter from [NetworkAddressConverter.Value] to [NetworkAddress] and vice versa
|
||||
|
|
@ -58,7 +58,7 @@ internal object NetworkAddressConverter : TwoWayConverter<NetworkAddressConverte
|
|||
}
|
||||
|
||||
if (address.value.isBlank()) {
|
||||
Timber.w("Address value is blank")
|
||||
TangemLogger.w("Address value is blank")
|
||||
}
|
||||
|
||||
return NetworkAddress.Address(value = address.value, type = type)
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@ import com.tangem.domain.models.network.Network
|
|||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
|
|
@ -63,8 +63,8 @@ internal class CommonNetworkStatusFetcher @Inject constructor(
|
|||
|
||||
networksStatusesStore.storeStatus(userWalletId = userWalletId, status = status)
|
||||
}
|
||||
.onLeft {
|
||||
Timber.e("Failed to fetch network status for $userWalletId [${network.rawId}]: $it")
|
||||
.onLeft { throwable ->
|
||||
TangemLogger.e("Failed to fetch network status for $userWalletId [${network.rawId}]", throwable)
|
||||
networksStatusesStore.setSourceAsOnlyCache(userWalletId = userWalletId, network = network)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,11 +10,11 @@ import com.tangem.domain.core.flow.FlowProducerTools
|
|||
import com.tangem.domain.models.network.NetworkStatus
|
||||
import com.tangem.domain.networks.multi.MultiNetworkStatusProducer
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.flow.*
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Default implementation of [MultiNetworkStatusProducer]
|
||||
|
|
@ -43,7 +43,7 @@ internal class DefaultMultiNetworkStatusProducer @AssistedInject constructor(
|
|||
val userWallet = userWalletsListRepository.getSyncOrNull(params.userWalletId)
|
||||
|
||||
if (userWallet == null) {
|
||||
Timber.e("Unable to get UserWallet with provided ID: ${params.userWalletId}")
|
||||
TangemLogger.e("Unable to get UserWallet with provided ID: ${params.userWalletId}")
|
||||
return@mapNotNull null
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
package com.tangem.data.networks.repository
|
||||
|
||||
import com.tangem.domain.common.tokens.CardCryptoCurrencyFactory
|
||||
import com.tangem.data.networks.store.NetworksStatusesStore
|
||||
import com.tangem.data.networks.store.storeStatus
|
||||
import com.tangem.data.networks.utils.NetworkStatusFactory
|
||||
import com.tangem.domain.common.tokens.CardCryptoCurrencyFactory
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.network.CryptoCurrencyAddress
|
||||
import com.tangem.domain.models.network.Network
|
||||
|
|
@ -11,8 +11,8 @@ import com.tangem.domain.models.wallet.UserWalletId
|
|||
import com.tangem.domain.networks.repository.NetworksRepository
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Default implementation of [NetworksRepository]
|
||||
|
|
@ -36,8 +36,8 @@ internal class DefaultNetworksRepository(
|
|||
val currencies = runCatching {
|
||||
cardCryptoCurrencyFactory.create(userWalletId = userWalletId, network = network)
|
||||
}
|
||||
.getOrElse {
|
||||
Timber.e(it, "Unable to create wallet currencies")
|
||||
.getOrElse { error ->
|
||||
TangemLogger.e("Unable to create wallet currencies", error)
|
||||
return@withContext
|
||||
}
|
||||
|
||||
|
|
@ -50,8 +50,8 @@ internal class DefaultNetworksRepository(
|
|||
network: Network,
|
||||
): List<CryptoCurrencyAddress> {
|
||||
return runCatching { cardCryptoCurrencyFactory.create(userWalletId = userWalletId, network = network) }
|
||||
.getOrElse {
|
||||
Timber.e(it, "Unable to create wallet currencies")
|
||||
.getOrElse { error ->
|
||||
TangemLogger.e("Unable to create wallet currencies", error)
|
||||
return emptyList()
|
||||
}
|
||||
.map { currency ->
|
||||
|
|
@ -67,8 +67,8 @@ internal class DefaultNetworksRepository(
|
|||
network: Network.RawID,
|
||||
): List<CryptoCurrencyAddress> {
|
||||
return runCatching { cardCryptoCurrencyFactory.createByRawId(userWalletId = userWalletId, network = network) }
|
||||
.getOrElse {
|
||||
Timber.e(it, "Unable to create wallet currencies")
|
||||
.getOrElse { error ->
|
||||
TangemLogger.e("Unable to create wallet currencies", error)
|
||||
return emptyList()
|
||||
}
|
||||
.map { currency ->
|
||||
|
|
|
|||
|
|
@ -7,15 +7,17 @@ import com.tangem.data.networks.converters.SimpleNetworkStatusConverter
|
|||
import com.tangem.data.networks.models.SimpleNetworkStatus
|
||||
import com.tangem.datasource.local.datastore.RuntimeSharedStore
|
||||
import com.tangem.datasource.local.network.entity.NetworkStatusDM
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import com.tangem.domain.models.StatusSource
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.network.NetworkStatus
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import com.tangem.utils.extensions.addOrReplace
|
||||
import kotlinx.coroutines.*
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.*
|
||||
import timber.log.Timber
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
|
||||
internal typealias WalletIdWithSimpleStatus = Map<String, Set<SimpleNetworkStatus>>
|
||||
|
|
@ -45,7 +47,7 @@ internal class DefaultNetworksStatusesStore(
|
|||
oldFile.delete()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Error while deleting old networks statuses datastore file")
|
||||
TangemLogger.e("Error while deleting old networks statuses datastore file", e)
|
||||
}
|
||||
|
||||
val cachedStatuses = persistenceDataStore.data.firstOrNull() ?: return@launch
|
||||
|
|
@ -89,7 +91,7 @@ internal class DefaultNetworksStatusesStore(
|
|||
ifNotFound: (Network.ID) -> SimpleNetworkStatus?,
|
||||
) {
|
||||
if (networks.isEmpty()) {
|
||||
Timber.d("Nothing to update: networks are empty")
|
||||
TangemLogger.d("Nothing to update: networks are empty")
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import com.tangem.domain.models.StatusSource
|
|||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.network.NetworkStatus
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import timber.log.Timber
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
|
||||
/**
|
||||
* Store actual network [status] by [userWalletId].
|
||||
|
|
@ -41,14 +41,14 @@ internal suspend fun NetworksStatusesStore.storeStatus(userWalletId: UserWalletI
|
|||
internal suspend fun NetworksStatusesStore.storeSuccess(userWalletId: UserWalletId, status: NetworkStatus) {
|
||||
if (status.value is NetworkStatus.Unreachable) {
|
||||
val message = "Use storeError method to save unreachable status"
|
||||
Timber.d(message)
|
||||
TangemLogger.d(message)
|
||||
|
||||
error(message)
|
||||
}
|
||||
|
||||
if (status.value.source != StatusSource.ACTUAL) {
|
||||
val message = "Method storeActual can be called only with StatusSource.ACTUAL"
|
||||
Timber.d(message)
|
||||
TangemLogger.d(message)
|
||||
|
||||
error(message)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,10 +8,10 @@ import com.tangem.domain.networks.utils.NetworksCleaner
|
|||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.runSuspendCatching
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Default implementation of [NetworksCleaner].
|
||||
|
|
@ -30,7 +30,7 @@ internal class DefaultNetworksCleaner(
|
|||
|
||||
override suspend fun invoke(userWalletId: UserWalletId, currencies: List<CryptoCurrency>) {
|
||||
if (currencies.isEmpty()) {
|
||||
Timber.d("No currencies to clear for wallet: $userWalletId")
|
||||
TangemLogger.d("No currencies to clear for wallet: $userWalletId")
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -49,7 +49,7 @@ internal class DefaultNetworksCleaner(
|
|||
runSuspendCatching {
|
||||
networksStatusesStore.clear(userWalletId = userWalletId, networks = networks)
|
||||
}
|
||||
.onFailure { Timber.e(it, "Failed to clear network statuses for wallet: $userWalletId") }
|
||||
.onFailure { TangemLogger.e("Failed to clear network statuses for wallet: $userWalletId", it) }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -63,7 +63,7 @@ internal class DefaultNetworksCleaner(
|
|||
walletManagersFacade.remove(userWalletId = userWalletId, networks = networks)
|
||||
}
|
||||
.onFailure {
|
||||
Timber.e(it, "Failed to remove networks from Blockchain SDK for wallet: $userWalletId")
|
||||
TangemLogger.e("Failed to remove networks from Blockchain SDK for wallet: $userWalletId", it)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -72,7 +72,7 @@ internal class DefaultNetworksCleaner(
|
|||
walletManagersFacade.removeTokens(userWalletId = userWalletId, tokens = tokens)
|
||||
}
|
||||
.onFailure {
|
||||
Timber.e(it, "Failed to remove tokens from Blockchain SDK for wallet: $userWalletId")
|
||||
TangemLogger.e("Failed to remove tokens from Blockchain SDK for wallet: $userWalletId", it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import com.tangem.domain.models.network.NetworkAddress
|
|||
import com.tangem.domain.models.network.NetworkStatus
|
||||
import com.tangem.domain.models.network.TxInfo
|
||||
import com.tangem.domain.models.yield.supply.YieldSupplyStatus
|
||||
import timber.log.Timber
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
|
||||
/** Factory for creating [NetworkStatus] */
|
||||
object NetworkStatusFactory {
|
||||
|
|
@ -104,7 +104,7 @@ object NetworkStatusFactory {
|
|||
}
|
||||
|
||||
if (amount == null) {
|
||||
Timber.w("Unable to find amount for cryptocurrency: $currency")
|
||||
TangemLogger.w("Unable to find amount for cryptocurrency: $currency")
|
||||
currency.id to NetworkStatus.Amount.NotFound
|
||||
} else {
|
||||
currency.id to NetworkStatus.Amount.Loaded(amount.value)
|
||||
|
|
@ -129,7 +129,7 @@ object NetworkStatusFactory {
|
|||
}
|
||||
|
||||
if (amount == null) {
|
||||
Timber.w("Unable to find amount for cryptocurrency: $currency")
|
||||
TangemLogger.w("Unable to find amount for cryptocurrency: $currency")
|
||||
currency.id to null
|
||||
} else {
|
||||
currency.id to amount.yieldSupplyStatus
|
||||
|
|
@ -191,7 +191,7 @@ object NetworkStatusFactory {
|
|||
}
|
||||
|
||||
if (address.value.isBlank()) {
|
||||
Timber.w("Address value is blank")
|
||||
TangemLogger.w("Address value is blank")
|
||||
}
|
||||
|
||||
return NetworkAddress.Address(address.value, type)
|
||||
|
|
|
|||
|
|
@ -39,7 +39,6 @@ dependencies {
|
|||
// region Other libraries
|
||||
implementation(deps.androidx.datastore)
|
||||
implementation(deps.moshi.kotlin)
|
||||
implementation(deps.timber)
|
||||
// endregion
|
||||
|
||||
// region Tests
|
||||
|
|
|
|||
|
|
@ -28,9 +28,9 @@ import com.tangem.pagination.exception.EndOfPaginationException
|
|||
import com.tangem.pagination.fetcher.BatchFetcher
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.runSuspendCatching
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Implementation of [NewsRepository].
|
||||
|
|
@ -72,7 +72,7 @@ internal class DefaultNewsRepository(
|
|||
response.items
|
||||
},
|
||||
onError = { error ->
|
||||
Timber.e(error, "Failed to get list of news")
|
||||
TangemLogger.e("Failed to get list of news", error)
|
||||
throw error
|
||||
},
|
||||
)
|
||||
|
|
@ -224,8 +224,7 @@ internal class DefaultNewsRepository(
|
|||
return withContext(dispatchers.io) {
|
||||
when (val apiResponse = newsApi.getTrendingNews(limit = limit, language = language)) {
|
||||
is ApiResponse.Error -> {
|
||||
Timber.e(
|
||||
apiResponse.cause,
|
||||
TangemLogger.e(
|
||||
"Trending news fetch failed cause: ${
|
||||
when (val error = apiResponse.cause) {
|
||||
is ApiResponseError.HttpException -> error.code
|
||||
|
|
@ -234,6 +233,7 @@ internal class DefaultNewsRepository(
|
|||
is ApiResponseError.UnknownException -> "UnknownException"
|
||||
}
|
||||
}",
|
||||
apiResponse.cause,
|
||||
)
|
||||
trendingNewsStore.clear()
|
||||
trendingNewsStore.store(
|
||||
|
|
|
|||
|
|
@ -45,7 +45,6 @@ dependencies {
|
|||
implementation(deps.arrow.core)
|
||||
implementation(deps.arrow.fx)
|
||||
implementation(deps.jodatime)
|
||||
implementation(deps.timber)
|
||||
implementation(deps.androidx.paging.runtime)
|
||||
implementation(deps.moshi.kotlin)
|
||||
ksp(deps.moshi.kotlin.codegen)
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
|||
import com.tangem.utils.coroutines.JobHolder
|
||||
import com.tangem.utils.coroutines.runSuspendCatching
|
||||
import com.tangem.utils.coroutines.saveIn
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
|
@ -42,7 +43,6 @@ import kotlinx.coroutines.launch
|
|||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import javax.inject.Inject
|
||||
import com.tangem.blockchain.nft.models.NFTAsset as SdkNFTAsset
|
||||
|
|
@ -241,7 +241,7 @@ internal class DefaultNFTRepository @Inject constructor(
|
|||
// NFTCleaner implementation
|
||||
override suspend fun invoke(userWalletId: UserWalletId, networks: Set<Network>) {
|
||||
if (networks.isEmpty()) {
|
||||
Timber.d("No networks to clear for wallet: $userWalletId")
|
||||
TangemLogger.d("No networks to clear for wallet: $userWalletId")
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -252,7 +252,7 @@ internal class DefaultNFTRepository @Inject constructor(
|
|||
// nftRuntimeStoreFactory.provide(network = network).clear()
|
||||
}
|
||||
.onFailure { throwable ->
|
||||
Timber.e(throwable, "Failed to clear NFT data for network $network for wallet: $userWalletId")
|
||||
TangemLogger.e("Failed to clear NFT data for network $network for wallet: $userWalletId", throwable)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,7 +47,6 @@ dependencies {
|
|||
implementation(deps.kotlin.immutable.collections)
|
||||
implementation(deps.moshi)
|
||||
implementation(deps.moshi.kotlin)
|
||||
implementation(deps.timber)
|
||||
ksp(deps.moshi.kotlin.codegen)
|
||||
kaptForObfuscatingVariants(deps.retrofit.response.type.keeper)
|
||||
implementation(deps.kotlin.serialization)
|
||||
|
|
|
|||
|
|
@ -22,19 +22,15 @@ import com.tangem.domain.card.common.extensions.canHandleToken
|
|||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.common.wallets.getSyncStrict
|
||||
import com.tangem.domain.common.wallets.loadAndGet
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.onramp.model.HotCryptoCurrency
|
||||
import com.tangem.domain.onramp.repositories.HotCryptoRepository
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.JobHolder
|
||||
import com.tangem.utils.coroutines.runCatching
|
||||
import com.tangem.utils.coroutines.saveIn
|
||||
import com.tangem.utils.coroutines.*
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.plus
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Default implementation of [HotCryptoRepository]
|
||||
|
|
@ -123,9 +119,9 @@ internal class DefaultHotCryptoRepository(
|
|||
return runCatching(dispatchers.io) {
|
||||
tangemTechApi.getHotCrypto(currencyId = appCurrencyId).getOrThrow()
|
||||
}
|
||||
.onSuccess { Timber.d("HotCrypto is successfully updated") }
|
||||
.onSuccess { TangemLogger.d("HotCrypto is successfully updated") }
|
||||
.onFailure { throwable ->
|
||||
Timber.e(throwable, "Unable to fetch hot crypto")
|
||||
TangemLogger.e("Unable to fetch hot crypto", throwable)
|
||||
|
||||
val httpException = throwable as? ApiResponseError.HttpException
|
||||
analyticsEventHandler.send(
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ import com.tangem.domain.onramp.repositories.OnrampRepository
|
|||
import com.tangem.domain.tokens.model.Amount
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
|
|
@ -57,7 +58,6 @@ import kotlinx.coroutines.flow.Flow
|
|||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.joda.time.DateTime
|
||||
import timber.log.Timber
|
||||
import java.util.UUID
|
||||
|
||||
@Suppress("LongParameterList", "LargeClass", "TooManyFunctions")
|
||||
|
|
@ -221,7 +221,7 @@ internal class DefaultOnrampRepository(
|
|||
).bind()
|
||||
},
|
||||
onError = { error ->
|
||||
Timber.w(error, "Unable to fetch onramp payment methods")
|
||||
TangemLogger.w("Unable to fetch onramp payment methods", error)
|
||||
throw error
|
||||
},
|
||||
)
|
||||
|
|
@ -256,7 +256,7 @@ internal class DefaultOnrampRepository(
|
|||
).bind()
|
||||
},
|
||||
onError = { error ->
|
||||
Timber.w(error, "Unable to fetch onramp pairs")
|
||||
TangemLogger.w("Unable to fetch onramp pairs", error)
|
||||
throw error
|
||||
},
|
||||
)
|
||||
|
|
@ -273,7 +273,7 @@ internal class DefaultOnrampRepository(
|
|||
).bind()
|
||||
},
|
||||
onError = { error ->
|
||||
Timber.w(error, "Unable to fetch express providers")
|
||||
TangemLogger.w("Unable to fetch express providers", error)
|
||||
throw error
|
||||
},
|
||||
)
|
||||
|
|
@ -321,7 +321,7 @@ internal class DefaultOnrampRepository(
|
|||
).bind()
|
||||
},
|
||||
onError = { error ->
|
||||
Timber.w(error, "Unable to fetch onramp pairs")
|
||||
TangemLogger.w("Unable to fetch onramp pairs", error)
|
||||
throw error
|
||||
},
|
||||
)
|
||||
|
|
@ -455,7 +455,7 @@ internal class DefaultOnrampRepository(
|
|||
).bind()
|
||||
},
|
||||
onError = { e ->
|
||||
Timber.e(e)
|
||||
TangemLogger.e("Error", e)
|
||||
throw e
|
||||
},
|
||||
)
|
||||
|
|
@ -478,7 +478,7 @@ internal class DefaultOnrampRepository(
|
|||
throw OnrampRedirectError.VerificationFailed
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
TangemLogger.e("Error", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
|
@ -602,7 +602,7 @@ internal class DefaultOnrampRepository(
|
|||
countryCode = countryCode,
|
||||
)
|
||||
} else {
|
||||
Timber.w(error, "Unable to fetch onramp quotes for ${provider.id}. $error")
|
||||
TangemLogger.w("Unable to fetch onramp quotes for ${provider.id}. $error", error)
|
||||
OnrampQuote.Error(
|
||||
paymentMethod = paymentMethod,
|
||||
provider = provider,
|
||||
|
|
@ -612,7 +612,7 @@ internal class DefaultOnrampRepository(
|
|||
)
|
||||
}
|
||||
} else {
|
||||
Timber.w(error, "Unable to fetch onramp quotes for ${provider.id}. $error")
|
||||
TangemLogger.w("Unable to fetch onramp quotes for ${provider.id}. $error", error)
|
||||
null
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -95,6 +95,7 @@ internal class HotCryptoCurrencyConverter(
|
|||
rawCurrencyId = rawCurrencyId,
|
||||
value = QuoteStatus.Data(
|
||||
fiatRate = fiatRate,
|
||||
fiatRateUSD = BigDecimal.ZERO,
|
||||
priceChange = priceChange.movePointLeft(2),
|
||||
source = StatusSource.ACTUAL, // It doesn't matter
|
||||
),
|
||||
|
|
|
|||
1
data/payment/.gitignore
vendored
Normal file
1
data/payment/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
48
data/payment/build.gradle.kts
Normal file
48
data/payment/build.gradle.kts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
alias(deps.plugins.kotlin.kapt)
|
||||
alias(deps.plugins.ksp)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.tangem.data.payment"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
/** Project - Data */
|
||||
implementation(projects.core.error)
|
||||
implementation(projects.core.error.ext)
|
||||
implementation(projects.data.common)
|
||||
implementation(projects.data.wallets)
|
||||
|
||||
/** Project - Domain */
|
||||
implementation(projects.domain.payment)
|
||||
implementation(projects.domain.payment.models)
|
||||
implementation(projects.domain.wallets)
|
||||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.common)
|
||||
|
||||
/** Project - Utils */
|
||||
implementation(projects.core.utils)
|
||||
implementation(projects.domain.legacy)
|
||||
implementation(projects.libs.blockchainSdk)
|
||||
|
||||
/** Libs - Tangem */
|
||||
implementation(tangemDeps.blockchain)
|
||||
implementation(tangemDeps.card.core)
|
||||
implementation(tangemDeps.card.android)
|
||||
implementation(tangemDeps.hot.core)
|
||||
implementation(projects.libs.tangemSdkApi)
|
||||
|
||||
/** Libs - Other */
|
||||
implementation(deps.kotlin.coroutines)
|
||||
implementation(deps.arrow.core)
|
||||
implementation(deps.moshi.kotlin)
|
||||
ksp(deps.moshi.kotlin.codegen)
|
||||
|
||||
/** DI */
|
||||
implementation(deps.hilt.android)
|
||||
kapt(deps.hilt.kapt)
|
||||
}
|
||||
|
|
@ -14,7 +14,6 @@ dependencies {
|
|||
implementation(deps.androidx.datastore)
|
||||
|
||||
implementation(deps.jodatime)
|
||||
implementation(deps.timber)
|
||||
|
||||
implementation(deps.hilt.android)
|
||||
kapt(deps.hilt.kapt)
|
||||
|
|
|
|||
|
|
@ -44,7 +44,6 @@ dependencies {
|
|||
// region Other libraries
|
||||
implementation(deps.androidx.datastore)
|
||||
implementation(deps.moshi.kotlin)
|
||||
implementation(deps.timber)
|
||||
// endregion
|
||||
|
||||
// region Tests
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ internal class QuoteStatusConverter(
|
|||
source = source,
|
||||
fiatRate = quote.price.orZero(),
|
||||
priceChange = quote.priceChange24h.orZero().movePointLeft(2),
|
||||
fiatRateUSD = quote.priceUsd.orZero(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,10 +14,11 @@ import com.tangem.datasource.di.NetworkMoshi
|
|||
import com.tangem.datasource.local.datastore.RuntimeSharedStore
|
||||
import com.tangem.datasource.utils.MoshiDataStoreSerializer
|
||||
import com.tangem.datasource.utils.mapWithStringKeyTypes
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import com.tangem.domain.quotes.GetCurrencyUSDQuoteUseCase
|
||||
import com.tangem.domain.quotes.QuotesRepository
|
||||
import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher
|
||||
import com.tangem.domain.quotes.multi.MultiQuoteUpdater
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -72,4 +73,10 @@ internal object QuotesDataModule {
|
|||
coroutineScope = coroutineScope,
|
||||
)
|
||||
}
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideGetCurrencyUSDQuoteUseCase(quotesRepository: QuotesRepository): GetCurrencyUSDQuoteUseCase {
|
||||
return GetCurrencyUSDQuoteUseCase(quotesRepository)
|
||||
}
|
||||
}
|
||||
|
|
@ -13,7 +13,7 @@ import com.tangem.domain.core.utils.catchOn
|
|||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import timber.log.Timber
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
|
|
@ -37,7 +37,7 @@ internal class DefaultMultiQuoteStatusFetcher @Inject constructor(
|
|||
|
||||
override suspend fun invoke(params: MultiQuoteStatusFetcher.Params) = Either.catchOn(dispatchers.default) {
|
||||
if (params.currenciesIds.isEmpty()) {
|
||||
Timber.d("No currencies to fetch quotes for")
|
||||
TangemLogger.d("No currencies to fetch quotes for")
|
||||
return@catchOn
|
||||
}
|
||||
|
||||
|
|
@ -55,7 +55,7 @@ internal class DefaultMultiQuoteStatusFetcher @Inject constructor(
|
|||
val response = quotesFetcher.fetch(
|
||||
fiatCurrencyId = appCurrencyId,
|
||||
currenciesIds = replacementIdsResult.idsForRequest,
|
||||
fields = setOf(Field.PRICE, Field.PRICE_CHANGE_24H),
|
||||
fields = setOf(Field.PRICE, Field.PRICE_CHANGE_24H, Field.PRICE_USD),
|
||||
)
|
||||
.getOrElse { error("Cause: $it") }
|
||||
|
||||
|
|
@ -67,7 +67,7 @@ internal class DefaultMultiQuoteStatusFetcher @Inject constructor(
|
|||
quotesStatusesStore.store(values = updatedResponse.quotes)
|
||||
}
|
||||
.onLeft { throwable ->
|
||||
Timber.e(throwable)
|
||||
TangemLogger.e("Error", throwable)
|
||||
quotesStatusesStore.setSourceAsOnlyCache(currenciesIds = params.currenciesIds)
|
||||
}
|
||||
|
||||
|
|
@ -77,7 +77,7 @@ internal class DefaultMultiQuoteStatusFetcher @Inject constructor(
|
|||
|
||||
if (appCurrencyId.isNullOrBlank()) {
|
||||
val exception = IllegalStateException("Unable to get AppCurrency for updating quotes")
|
||||
Timber.e(exception)
|
||||
TangemLogger.e("Error", exception)
|
||||
|
||||
throw exception
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,17 +4,17 @@ import androidx.annotation.VisibleForTesting
|
|||
import arrow.core.left
|
||||
import com.tangem.data.quotes.store.QuotesStatusesStore
|
||||
import com.tangem.datasource.appcurrency.AppCurrencyResponseStore
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import com.tangem.domain.core.utils.EitherFlow
|
||||
import com.tangem.domain.models.quote.QuoteStatus
|
||||
import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher
|
||||
import com.tangem.domain.quotes.multi.MultiQuoteUpdater
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import com.tangem.utils.coroutines.JobHolder
|
||||
import com.tangem.utils.coroutines.saveIn
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.*
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Default implementation of [MultiQuoteUpdater] which updates quotes when the app currency changes
|
||||
|
|
@ -36,14 +36,14 @@ internal class DefaultMultiQuoteUpdater(
|
|||
private val updaterHolder = JobHolder()
|
||||
|
||||
override fun subscribe() {
|
||||
Timber.d("Subscribe on quotes updates")
|
||||
TangemLogger.d("Subscribe on quotes updates")
|
||||
getMultiQuoteUpdates()
|
||||
.launchIn(coroutineScope)
|
||||
.saveIn(updaterHolder)
|
||||
}
|
||||
|
||||
override fun unsubscribe() {
|
||||
Timber.e("Unsubscribe from quotes updates")
|
||||
TangemLogger.i("Unsubscribe from quotes updates")
|
||||
updaterHolder.cancel()
|
||||
}
|
||||
|
||||
|
|
@ -63,10 +63,10 @@ internal class DefaultMultiQuoteUpdater(
|
|||
appCurrencyId = appCurrency.id,
|
||||
),
|
||||
)
|
||||
.onLeft(Timber::e)
|
||||
.onLeft { TangemLogger.e("Error", it) }
|
||||
}
|
||||
.retryWhen { cause, _ ->
|
||||
Timber.e("Retry updating quotes: $cause")
|
||||
TangemLogger.e("Retry updating quotes: $cause")
|
||||
|
||||
emit(cause.left())
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import com.tangem.data.quotes.store.QuotesStatusesStore
|
|||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.quote.QuoteStatus
|
||||
import com.tangem.domain.quotes.QuotesRepository
|
||||
import timber.log.Timber
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
|
||||
/**
|
||||
* Default implementation of [QuotesRepository]
|
||||
|
|
@ -19,7 +19,7 @@ internal class DefaultQuotesRepository(
|
|||
|
||||
override suspend fun getMultiQuoteSyncOrNull(currenciesIds: Set<CryptoCurrency.RawID>): Set<QuoteStatus> {
|
||||
if (currenciesIds.isEmpty()) {
|
||||
Timber.e("currenciesIds are empty")
|
||||
TangemLogger.e("currenciesIds are empty")
|
||||
return emptySet()
|
||||
}
|
||||
|
||||
|
|
@ -31,4 +31,8 @@ internal class DefaultQuotesRepository(
|
|||
?: QuoteStatus(rawCurrencyId = currencyId)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getCurrencyUSDQuote(currencyId: CryptoCurrency.RawID): QuoteStatus? {
|
||||
return getMultiQuoteSyncOrNull(currenciesIds = setOf(currencyId)).firstOrNull()
|
||||
}
|
||||
}
|
||||
|
|
@ -4,16 +4,16 @@ import androidx.datastore.core.DataStore
|
|||
import com.tangem.data.quotes.converter.QuoteStatusConverter
|
||||
import com.tangem.datasource.api.tangemTech.models.QuotesResponse
|
||||
import com.tangem.datasource.local.datastore.RuntimeSharedStore
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import com.tangem.domain.models.StatusSource
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.quote.QuoteStatus
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import com.tangem.utils.extensions.addOrReplace
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
internal typealias CurrencyIdWithQuote = Map<String, QuotesResponse.Quote>
|
||||
|
||||
|
|
@ -64,7 +64,7 @@ internal class DefaultQuotesStatusesStore(
|
|||
ifNotFound: (CryptoCurrency.RawID) -> QuoteStatus?,
|
||||
) {
|
||||
if (currenciesIds.isEmpty()) {
|
||||
Timber.d("Nothing to update: currencies ids are empty")
|
||||
TangemLogger.d("Nothing to update: currencies ids are empty")
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ internal class QuoteStatusConverterTest {
|
|||
priceChange24h = null,
|
||||
priceChange1w = null,
|
||||
priceChange30d = null,
|
||||
priceUsd = null,
|
||||
),
|
||||
),
|
||||
expected = QuoteStatus(
|
||||
|
|
@ -62,6 +63,7 @@ internal class QuoteStatusConverterTest {
|
|||
value = QuoteStatus.Data(
|
||||
source = StatusSource.ACTUAL,
|
||||
fiatRate = BigDecimal.ZERO,
|
||||
fiatRateUSD = BigDecimal.ZERO,
|
||||
priceChange = BigDecimal("0.00"),
|
||||
),
|
||||
),
|
||||
|
|
@ -74,6 +76,7 @@ internal class QuoteStatusConverterTest {
|
|||
priceChange24h = null,
|
||||
priceChange1w = BigDecimal.ZERO,
|
||||
priceChange30d = BigDecimal.ZERO,
|
||||
priceUsd = null,
|
||||
),
|
||||
),
|
||||
expected = QuoteStatus(
|
||||
|
|
@ -81,6 +84,7 @@ internal class QuoteStatusConverterTest {
|
|||
value = QuoteStatus.Data(
|
||||
source = StatusSource.ACTUAL,
|
||||
fiatRate = BigDecimal.ZERO,
|
||||
fiatRateUSD = BigDecimal.ZERO,
|
||||
priceChange = BigDecimal("0.00"),
|
||||
),
|
||||
),
|
||||
|
|
@ -93,6 +97,7 @@ internal class QuoteStatusConverterTest {
|
|||
priceChange24h = BigDecimal.ONE,
|
||||
priceChange1w = null,
|
||||
priceChange30d = null,
|
||||
priceUsd = BigDecimal.ONE
|
||||
),
|
||||
),
|
||||
expected = QuoteStatus(
|
||||
|
|
@ -100,6 +105,7 @@ internal class QuoteStatusConverterTest {
|
|||
value = QuoteStatus.Data(
|
||||
source = StatusSource.ACTUAL,
|
||||
fiatRate = BigDecimal.ONE,
|
||||
fiatRateUSD = BigDecimal.ONE,
|
||||
priceChange = BigDecimal("0.01"),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -240,6 +240,6 @@ internal class DefaultMultiQuoteStatusFetcherTest {
|
|||
),
|
||||
)
|
||||
|
||||
val fields = setOf(QuotesFetcher.Field.PRICE, QuotesFetcher.Field.PRICE_CHANGE_24H)
|
||||
val fields = setOf(QuotesFetcher.Field.PRICE, QuotesFetcher.Field.PRICE_CHANGE_24H, QuotesFetcher.Field.PRICE_USD)
|
||||
}
|
||||
}
|
||||
|
|
@ -43,6 +43,7 @@ internal class DefaultQuotesRepositoryTest {
|
|||
value = QuoteStatus.Data(
|
||||
source = StatusSource.ACTUAL,
|
||||
fiatRate = BigDecimal.ZERO,
|
||||
fiatRateUSD = BigDecimal.ZERO,
|
||||
priceChange = BigDecimal.ZERO,
|
||||
),
|
||||
)
|
||||
|
|
@ -100,9 +101,75 @@ internal class DefaultQuotesRepositoryTest {
|
|||
)
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class GetCurrencyUSDQuote {
|
||||
|
||||
private val btcRawId = CryptoCurrency.RawID(value = "BTC")
|
||||
private val ethRawId = CryptoCurrency.RawID(value = "ETH")
|
||||
|
||||
private val ethQuote = QuoteStatus(
|
||||
rawCurrencyId = ethRawId,
|
||||
value = QuoteStatus.Data(
|
||||
source = StatusSource.ACTUAL,
|
||||
fiatRate = BigDecimal.ZERO,
|
||||
fiatRateUSD = BigDecimal.ZERO,
|
||||
priceChange = BigDecimal.ZERO,
|
||||
),
|
||||
)
|
||||
|
||||
@ParameterizedTest
|
||||
@ProvideTestModels
|
||||
fun getCurrencyUSDQuote(model: GetCurrencyUSDQuoteModel) = runTest {
|
||||
// Arrange
|
||||
coEvery { quotesStatusesStore.getAllSyncOrNull() } returns model.initialStore
|
||||
|
||||
// Act
|
||||
val actual = repository.getCurrencyUSDQuote(currencyId = model.currencyId)
|
||||
|
||||
// Assert
|
||||
val expected = model.expected
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
}
|
||||
|
||||
private fun provideTestModels() = listOf(
|
||||
GetCurrencyUSDQuoteModel(
|
||||
initialStore = null,
|
||||
currencyId = ethRawId,
|
||||
expected = QuoteStatus(rawCurrencyId = ethRawId),
|
||||
),
|
||||
GetCurrencyUSDQuoteModel(
|
||||
initialStore = emptySet(),
|
||||
currencyId = ethRawId,
|
||||
expected = QuoteStatus(rawCurrencyId = ethRawId),
|
||||
),
|
||||
GetCurrencyUSDQuoteModel(
|
||||
initialStore = setOf(ethQuote),
|
||||
currencyId = ethRawId,
|
||||
expected = ethQuote,
|
||||
),
|
||||
GetCurrencyUSDQuoteModel(
|
||||
initialStore = setOf(QuoteStatus(rawCurrencyId = btcRawId)),
|
||||
currencyId = ethRawId,
|
||||
expected = QuoteStatus(rawCurrencyId = ethRawId),
|
||||
),
|
||||
GetCurrencyUSDQuoteModel(
|
||||
initialStore = setOf(ethQuote, QuoteStatus(rawCurrencyId = btcRawId)),
|
||||
currencyId = ethRawId,
|
||||
expected = ethQuote,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
data class GetMultiQuoteSyncOrNullModel(
|
||||
val initialStore: Set<QuoteStatus>?,
|
||||
val currencyIds: Set<CryptoCurrency.RawID>,
|
||||
val expected: Set<QuoteStatus>?,
|
||||
)
|
||||
|
||||
data class GetCurrencyUSDQuoteModel(
|
||||
val initialStore: Set<QuoteStatus>?,
|
||||
val currencyId: CryptoCurrency.RawID,
|
||||
val expected: QuoteStatus?,
|
||||
)
|
||||
}
|
||||
|
|
@ -83,6 +83,7 @@ internal class DefaultSingleQuoteStatusProducerTest {
|
|||
value = QuoteStatus.Data(
|
||||
fiatRate = BigDecimal.ONE,
|
||||
priceChange = BigDecimal.ZERO,
|
||||
fiatRateUSD = BigDecimal.ZERO,
|
||||
source = StatusSource.ACTUAL,
|
||||
),
|
||||
)
|
||||
|
|
@ -129,6 +130,7 @@ internal class DefaultSingleQuoteStatusProducerTest {
|
|||
rawCurrencyId = params.rawCurrencyId,
|
||||
value = QuoteStatus.Data(
|
||||
fiatRate = BigDecimal.ONE,
|
||||
fiatRateUSD = BigDecimal.ZERO,
|
||||
priceChange = BigDecimal.ZERO,
|
||||
source = StatusSource.ACTUAL,
|
||||
),
|
||||
|
|
|
|||
1
data/search/.gitignore
vendored
Normal file
1
data/search/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
40
data/search/build.gradle.kts
Normal file
40
data/search/build.gradle.kts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
alias(deps.plugins.kotlin.kapt)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.tangem.data.search"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// region Project - Core
|
||||
implementation(projects.core.datasource)
|
||||
api(projects.core.utils)
|
||||
// endregion
|
||||
|
||||
// region Project - Data
|
||||
implementation(projects.data.common)
|
||||
// endregion
|
||||
|
||||
// region Project - Domain
|
||||
implementation(projects.domain.search)
|
||||
implementation(projects.domain.common)
|
||||
implementation(projects.domain.account.status)
|
||||
implementation(projects.domain.markets.models)
|
||||
implementation(projects.domain.wallets)
|
||||
implementation(projects.domain.appCurrency)
|
||||
// endregion
|
||||
|
||||
// region DI
|
||||
implementation(deps.hilt.android)
|
||||
kapt(deps.hilt.kapt)
|
||||
// endregion
|
||||
|
||||
// region Other libraries
|
||||
implementation(deps.androidx.datastore)
|
||||
implementation(deps.moshi.kotlin)
|
||||
// endregion
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
package com.tangem.data.search.converter
|
||||
|
||||
import com.tangem.data.search.model.RecentTokenDTO
|
||||
import com.tangem.data.search.model.TextHintDTO
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.search.model.RecentSearchToken
|
||||
import com.tangem.domain.search.model.SearchTextHint
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
internal class TextHintDTOToSearchTextHintConverter : Converter<TextHintDTO, SearchTextHint> {
|
||||
override fun convert(value: TextHintDTO): SearchTextHint {
|
||||
return SearchTextHint(
|
||||
text = value.text,
|
||||
timestamp = value.timestamp,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal class RecentTokenDTOToRecentSearchTokenConverter : Converter<RecentTokenDTO, RecentSearchToken> {
|
||||
override fun convert(value: RecentTokenDTO): RecentSearchToken {
|
||||
return RecentSearchToken(
|
||||
id = CryptoCurrency.RawID(value.id),
|
||||
name = value.name,
|
||||
symbol = value.symbol,
|
||||
imageUrl = value.imageUrl,
|
||||
timestamp = value.timestamp,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal class RecentSearchTokenToRecentTokenDTOConverter : Converter<RecentSearchToken, RecentTokenDTO> {
|
||||
override fun convert(value: RecentSearchToken): RecentTokenDTO {
|
||||
return RecentTokenDTO(
|
||||
id = value.id.value,
|
||||
name = value.name,
|
||||
symbol = value.symbol,
|
||||
imageUrl = value.imageUrl,
|
||||
timestamp = value.timestamp,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
package com.tangem.data.search.di
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.core.DataStoreFactory
|
||||
import androidx.datastore.dataStoreFile
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.adapter
|
||||
import com.tangem.data.search.model.SearchHistoryDTO
|
||||
import com.tangem.data.search.repository.DefaultSearchRepository
|
||||
import com.tangem.data.search.store.DefaultSearchHistoryStore
|
||||
import com.tangem.data.search.store.SearchHistoryStore
|
||||
import com.tangem.datasource.di.NetworkMoshi
|
||||
import com.tangem.datasource.utils.MoshiDataStoreSerializer
|
||||
import com.tangem.domain.account.status.supplier.MultiAccountStatusListSupplier
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.search.repository.SearchRepository
|
||||
import com.tangem.domain.search.usecase.ClearSearchHistoryUseCase
|
||||
import com.tangem.domain.search.usecase.GetSearchResultsUseCase
|
||||
import com.tangem.domain.search.usecase.SaveRecentSearchTokenUseCase
|
||||
import com.tangem.domain.search.usecase.SaveSearchQueryUseCase
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object SearchDataModule {
|
||||
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSearchHistoryDataStore(
|
||||
@NetworkMoshi moshi: Moshi,
|
||||
@ApplicationContext context: Context,
|
||||
appScope: AppCoroutineScope,
|
||||
): DataStore<SearchHistoryDTO> {
|
||||
return DataStoreFactory.create(
|
||||
serializer = MoshiDataStoreSerializer(
|
||||
defaultValue = SearchHistoryDTO(),
|
||||
adapter = moshi.adapter<SearchHistoryDTO>(),
|
||||
),
|
||||
produceFile = { context.dataStoreFile(fileName = "search_history") },
|
||||
scope = appScope,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSearchHistoryStore(dataStore: DataStore<SearchHistoryDTO>): SearchHistoryStore {
|
||||
return DefaultSearchHistoryStore(dataStore = dataStore)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSearchRepository(
|
||||
store: SearchHistoryStore,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): SearchRepository {
|
||||
return DefaultSearchRepository(
|
||||
store = store,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideGetSearchResultsUseCase(
|
||||
searchRepository: SearchRepository,
|
||||
multiAccountStatusListSupplier: MultiAccountStatusListSupplier,
|
||||
userWalletsListRepository: UserWalletsListRepository,
|
||||
): GetSearchResultsUseCase {
|
||||
return GetSearchResultsUseCase(
|
||||
searchRepository = searchRepository,
|
||||
multiAccountStatusListSupplier = multiAccountStatusListSupplier,
|
||||
userWalletsListRepository = userWalletsListRepository,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideSaveSearchQueryUseCase(searchRepository: SearchRepository): SaveSearchQueryUseCase {
|
||||
return SaveSearchQueryUseCase(searchRepository = searchRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideSaveRecentSearchTokenUseCase(searchRepository: SearchRepository): SaveRecentSearchTokenUseCase {
|
||||
return SaveRecentSearchTokenUseCase(searchRepository = searchRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideClearSearchHistoryUseCase(searchRepository: SearchRepository): ClearSearchHistoryUseCase {
|
||||
return ClearSearchHistoryUseCase(searchRepository = searchRepository)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package com.tangem.data.search.model
|
||||
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class SearchHistoryDTO(
|
||||
val textHints: List<TextHintDTO> = emptyList(),
|
||||
val recentTokens: List<RecentTokenDTO> = emptyList(),
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class TextHintDTO(
|
||||
val text: String,
|
||||
val timestamp: Long,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class RecentTokenDTO(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val symbol: String,
|
||||
val imageUrl: String?,
|
||||
val timestamp: Long,
|
||||
)
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
package com.tangem.data.search.repository
|
||||
|
||||
import com.tangem.data.search.converter.RecentSearchTokenToRecentTokenDTOConverter
|
||||
import com.tangem.data.search.converter.RecentTokenDTOToRecentSearchTokenConverter
|
||||
import com.tangem.data.search.converter.TextHintDTOToSearchTextHintConverter
|
||||
import com.tangem.data.search.model.TextHintDTO
|
||||
import com.tangem.data.search.store.SearchHistoryStore
|
||||
import com.tangem.domain.search.model.RecentSearchToken
|
||||
import com.tangem.domain.search.model.SearchTextHint
|
||||
import com.tangem.domain.search.repository.SearchRepository
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
internal class DefaultSearchRepository(
|
||||
private val store: SearchHistoryStore,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : SearchRepository {
|
||||
|
||||
private val textHintConverter by lazy {
|
||||
TextHintDTOToSearchTextHintConverter()
|
||||
}
|
||||
private val recentTokenConverter by lazy {
|
||||
RecentTokenDTOToRecentSearchTokenConverter()
|
||||
}
|
||||
private val recentTokenDMConverter by lazy {
|
||||
RecentSearchTokenToRecentTokenDTOConverter()
|
||||
}
|
||||
|
||||
override fun getTextHints(): Flow<List<SearchTextHint>> {
|
||||
return store.getTextHints()
|
||||
.map { textHintConverter.convertList(it) }
|
||||
.flowOn(dispatchers.io)
|
||||
}
|
||||
|
||||
override fun getRecentTokens(): Flow<List<RecentSearchToken>> {
|
||||
return store.getRecentTokens()
|
||||
.map { recentTokenConverter.convertList(it) }
|
||||
.flowOn(dispatchers.io)
|
||||
}
|
||||
|
||||
override suspend fun saveTextHint(text: String) = withContext(dispatchers.io) {
|
||||
store.saveTextHint(
|
||||
TextHintDTO(
|
||||
text = text,
|
||||
timestamp = System.currentTimeMillis(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun saveRecentToken(token: RecentSearchToken) = withContext(dispatchers.io) {
|
||||
store.saveRecentToken(recentTokenDMConverter.convert(token))
|
||||
}
|
||||
|
||||
override suspend fun clearHistory() = withContext(dispatchers.io) {
|
||||
store.clearAll()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
package com.tangem.data.search.store
|
||||
|
||||
import androidx.datastore.core.DataStore
|
||||
import com.tangem.data.search.model.RecentTokenDTO
|
||||
import com.tangem.data.search.model.SearchHistoryDTO
|
||||
import com.tangem.data.search.model.TextHintDTO
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
internal class DefaultSearchHistoryStore(
|
||||
private val dataStore: DataStore<SearchHistoryDTO>,
|
||||
) : SearchHistoryStore {
|
||||
|
||||
override fun getTextHints(): Flow<List<TextHintDTO>> {
|
||||
return dataStore.data.map { it.textHints.sortedByDescending(TextHintDTO::timestamp) }
|
||||
}
|
||||
|
||||
override fun getRecentTokens(): Flow<List<RecentTokenDTO>> {
|
||||
return dataStore.data.map { it.recentTokens.sortedByDescending(RecentTokenDTO::timestamp) }
|
||||
}
|
||||
|
||||
override suspend fun saveTextHint(hint: TextHintDTO) {
|
||||
dataStore.updateData { current ->
|
||||
val updated = current.textHints
|
||||
.filter { it.text != hint.text }
|
||||
.toMutableList()
|
||||
.apply { add(0, hint) }
|
||||
.take(MAX_HISTORY_SIZE)
|
||||
current.copy(textHints = updated)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun saveRecentToken(token: RecentTokenDTO) {
|
||||
dataStore.updateData { current ->
|
||||
val updated = current.recentTokens
|
||||
.filter { it.id != token.id }
|
||||
.toMutableList()
|
||||
.apply { add(0, token) }
|
||||
.take(MAX_HISTORY_SIZE)
|
||||
current.copy(recentTokens = updated)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun clearAll() {
|
||||
dataStore.updateData {
|
||||
SearchHistoryDTO()
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val MAX_HISTORY_SIZE = 3
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package com.tangem.data.search.store
|
||||
|
||||
import com.tangem.data.search.model.RecentTokenDTO
|
||||
import com.tangem.data.search.model.TextHintDTO
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
internal interface SearchHistoryStore {
|
||||
fun getTextHints(): Flow<List<TextHintDTO>>
|
||||
fun getRecentTokens(): Flow<List<RecentTokenDTO>>
|
||||
suspend fun saveTextHint(hint: TextHintDTO)
|
||||
suspend fun saveRecentToken(token: RecentTokenDTO)
|
||||
suspend fun clearAll()
|
||||
}
|
||||
|
|
@ -33,7 +33,6 @@ dependencies {
|
|||
implementation(deps.kotlin.coroutines)
|
||||
implementation(deps.moshi)
|
||||
implementation(deps.moshi.kotlin)
|
||||
implementation(deps.timber)
|
||||
ksp(deps.moshi.kotlin.codegen)
|
||||
kaptForObfuscatingVariants(deps.retrofit.response.type.keeper)
|
||||
// endregion
|
||||
|
|
|
|||
|
|
@ -14,11 +14,11 @@ import com.tangem.domain.settings.usercountry.models.GB_COUNTRY
|
|||
import com.tangem.domain.settings.usercountry.models.UserCountry
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.runSuspendCatching
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import java.util.Locale
|
||||
|
||||
@Suppress("TooManyFunctions")
|
||||
|
|
@ -145,7 +145,7 @@ internal class DefaultSettingsRepository(
|
|||
override fun getUserCountryCode(): StateFlow<UserCountry?> = userCountryFlow
|
||||
|
||||
override suspend fun fetchUserCountryCode() {
|
||||
Timber.i("Start fetching user country code")
|
||||
TangemLogger.i("Start fetching user country code")
|
||||
|
||||
// for GB locale avoid request geo and use device default (FCA fixes)
|
||||
if (Locale.getDefault().country == GB_COUNTRY.code) {
|
||||
|
|
@ -166,7 +166,7 @@ internal class DefaultSettingsRepository(
|
|||
else -> UserCountry.Other(code = country)
|
||||
}
|
||||
|
||||
Timber.i("User code country is $code")
|
||||
TangemLogger.i("User code country is $code")
|
||||
|
||||
userCountryFlow.value = code
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,7 +55,6 @@ dependencies {
|
|||
implementation(deps.kotlin.immutable.collections)
|
||||
implementation(deps.moshi)
|
||||
implementation(deps.moshi.kotlin)
|
||||
implementation(deps.timber)
|
||||
implementation(deps.firebase.crashlytics)
|
||||
ksp(deps.moshi.kotlin.codegen)
|
||||
kaptForObfuscatingVariants(deps.retrofit.response.type.keeper)
|
||||
|
|
|
|||
|
|
@ -5,11 +5,7 @@ import arrow.core.getOrElse
|
|||
import arrow.core.raise.Raise
|
||||
import arrow.core.raise.either
|
||||
import arrow.core.raise.ensure
|
||||
import com.tangem.data.staking.converters.ethpool.P2PEthPoolBroadcastResultConverter
|
||||
import com.tangem.data.staking.converters.ethpool.P2PEthPoolErrorConverter
|
||||
import com.tangem.data.staking.converters.ethpool.P2PEthPoolStakingAccountConverter
|
||||
import com.tangem.data.staking.converters.ethpool.P2PEthPoolUnsignedTxConverter
|
||||
import com.tangem.data.staking.converters.ethpool.P2PEthPoolVaultConverter
|
||||
import com.tangem.data.staking.converters.ethpool.*
|
||||
import com.tangem.datasource.api.common.response.ApiResponse
|
||||
import com.tangem.datasource.api.ethpool.P2PEthPoolApi
|
||||
import com.tangem.datasource.api.ethpool.models.request.P2PEthPoolBroadcastRequest
|
||||
|
|
@ -28,11 +24,11 @@ import com.tangem.domain.staking.model.stakekit.StakingError
|
|||
import com.tangem.domain.staking.repositories.P2PEthPoolRepository
|
||||
import com.tangem.domain.staking.toggles.StakingFeatureToggles
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* P2PEthPool staking repository implementation
|
||||
|
|
@ -71,7 +67,7 @@ internal class DefaultP2PEthPoolRepository(
|
|||
override suspend fun fetchVaults(network: P2PEthPoolNetwork) {
|
||||
val vaults = if (stakingFeatureToggles.isEthStakingEnabled) {
|
||||
getVaults(network).getOrElse { error ->
|
||||
Timber.e("Error fetching P2PEthPool vaults: $error")
|
||||
TangemLogger.e("Error fetching P2PEthPool vaults: $error")
|
||||
emptyList()
|
||||
}
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -47,13 +47,13 @@ import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction
|
|||
import com.tangem.domain.staking.repositories.StakeKitRepository
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
|
||||
@Suppress("LargeClass", "LongParameterList", "TooManyFunctions")
|
||||
internal class DefaultStakeKitRepository(
|
||||
|
|
@ -91,7 +91,7 @@ internal class DefaultStakeKitRepository(
|
|||
when (response) {
|
||||
is ApiResponse.Success -> response.data.data.filter { yield -> yield.isAvailable == true }
|
||||
is ApiResponse.Error -> {
|
||||
Timber.e("Error fetching enabled yields: ${response.cause}")
|
||||
TangemLogger.e("Error fetching enabled yields: ${response.cause}")
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
|
@ -142,7 +142,7 @@ internal class DefaultStakeKitRepository(
|
|||
network = networkTypeString,
|
||||
status = actionStatusString,
|
||||
).getOrThrow().data,
|
||||
onError = { Timber.e("Error converting staking actions list: $it") },
|
||||
onError = { TangemLogger.e("Error converting staking actions list: $it") },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -348,7 +348,7 @@ internal class DefaultStakeKitRepository(
|
|||
private suspend fun getEnabledYieldsSync(): List<Yield> {
|
||||
return YieldConverter.convertListIgnoreErrors(
|
||||
input = stakingYieldsStore.getSync(),
|
||||
onError = { Timber.e("Error converting one of the items in enabled yields: $it") },
|
||||
onError = { TangemLogger.e("Error converting one of the items in enabled yields: $it") },
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -356,7 +356,7 @@ internal class DefaultStakeKitRepository(
|
|||
return stakingYieldsStore.get().map { yields ->
|
||||
YieldConverter.convertListIgnoreErrors(
|
||||
input = yields,
|
||||
onError = { Timber.e("Error converting one of the items in enabled yields: $it") },
|
||||
onError = { TangemLogger.e("Error converting one of the items in enabled yields: $it") },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
package com.tangem.data.staking
|
||||
|
||||
import com.tangem.datasource.api.stakekit.StakeKitApi
|
||||
import com.tangem.datasource.api.stakekit.models.request.*
|
||||
import com.tangem.datasource.api.stakekit.models.request.SubmitTransactionHashRequestBody
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys
|
||||
import com.tangem.datasource.local.preferences.utils.getObjectListSync
|
||||
import com.tangem.domain.staking.model.UnsubmittedTransactionMetadata
|
||||
import com.tangem.domain.staking.repositories.StakeKitTransactionHashRepository
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
|
||||
internal class DefaultStakeKitTransactionHashRepository(
|
||||
private val stakeKitApi: StakeKitApi,
|
||||
|
|
@ -75,7 +75,7 @@ internal class DefaultStakeKitTransactionHashRepository(
|
|||
append("StakeKit id = ${transaction.transactionId} and\n")
|
||||
append("transaction hash = ${transaction.transactionHash}")
|
||||
}
|
||||
Timber.e(logMessage)
|
||||
TangemLogger.e(logMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,11 +30,11 @@ import com.tangem.domain.staking.model.ethpool.P2PEthPoolVault
|
|||
import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.runSuspendCatching
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
|
|
@ -66,10 +66,10 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor(
|
|||
) : MultiStakingBalanceFetcher {
|
||||
|
||||
override suspend fun invoke(params: MultiStakingBalanceFetcher.Params): Either<Throwable, Unit> {
|
||||
Timber.i("Start fetching staking balances for params:\n$params")
|
||||
TangemLogger.i("Start fetching staking balances for params:\n$params")
|
||||
|
||||
val stakingIds = params.stakingIds.ifEmpty {
|
||||
Timber.i("Nothing to fetch, empty stakingIds for ${params.userWalletId}")
|
||||
TangemLogger.i("Nothing to fetch, empty stakingIds for ${params.userWalletId}")
|
||||
return Unit.right()
|
||||
}
|
||||
|
||||
|
|
@ -84,7 +84,7 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor(
|
|||
stakingIntegrationID is StakingIntegrationID.StakeKit
|
||||
}
|
||||
|
||||
Timber.i(
|
||||
TangemLogger.i(
|
||||
"""
|
||||
Staking IDs to fetch:
|
||||
- StakeKit: ${stakeKitIds.joinToString()}
|
||||
|
|
@ -104,7 +104,7 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor(
|
|||
}
|
||||
}
|
||||
.onLeft { throwable ->
|
||||
Timber.e(throwable, "Unable to fetch staking balances $params")
|
||||
TangemLogger.e("Unable to fetch staking balances $params", throwable)
|
||||
|
||||
if (stakeKitIds.isNotEmpty()) {
|
||||
stakeKitBalancesStore.storeError(
|
||||
|
|
@ -137,7 +137,7 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor(
|
|||
|
||||
val vaults = runSuspendCatching { p2pEthPoolVaultsStore.getSync() }.getOrNull().orEmpty()
|
||||
if (vaults.isEmpty()) {
|
||||
Timber.w("No P2PEthPool vaults available for $userWalletId, storing empty balances")
|
||||
TangemLogger.w("No P2PEthPool vaults available for $userWalletId, storing empty balances")
|
||||
p2PEthPoolBalancesStore.storeEmpty(userWalletId = userWalletId, stakingIds = stakingIds)
|
||||
return
|
||||
}
|
||||
|
|
@ -155,7 +155,7 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor(
|
|||
val addresses = stakingIds.map { it.address }.toSet()
|
||||
val responses = fetchP2PAccountResponses(vaults = vaults, addresses = addresses)
|
||||
|
||||
Timber.i("Successfully fetched ${responses.size} P2PEthPool balances for $userWalletId")
|
||||
TangemLogger.i("Successfully fetched ${responses.size} P2PEthPool balances for $userWalletId")
|
||||
|
||||
if (responses.isNotEmpty()) {
|
||||
p2PEthPoolBalancesStore.storeActual(userWalletId = userWalletId, values = responses)
|
||||
|
|
@ -167,19 +167,22 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor(
|
|||
}
|
||||
|
||||
if (missingStakingIds.isNotEmpty()) {
|
||||
Timber.i("Missing responses for ${missingStakingIds.size} staking IDs: $missingStakingIds")
|
||||
TangemLogger.i(
|
||||
"Missing responses for ${missingStakingIds.size} staking IDs:" +
|
||||
" $missingStakingIds",
|
||||
)
|
||||
p2PEthPoolBalancesStore.storeError(
|
||||
userWalletId = userWalletId,
|
||||
stakingIds = missingStakingIds.toSet(),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Timber.i("No P2PEthPool responses received for $userWalletId")
|
||||
TangemLogger.i("No P2PEthPool responses received for $userWalletId")
|
||||
p2PEthPoolBalancesStore.storeError(userWalletId = userWalletId, stakingIds = stakingIds)
|
||||
}
|
||||
},
|
||||
onError = { throwable ->
|
||||
Timber.e(throwable, "Unable to fetch P2PEthPool balances $userWalletId")
|
||||
TangemLogger.e("Unable to fetch P2PEthPool balances $userWalletId", throwable)
|
||||
|
||||
p2PEthPoolBalancesStore.storeError(userWalletId = userWalletId, stakingIds = stakingIds)
|
||||
|
||||
|
|
@ -207,7 +210,7 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor(
|
|||
is ApiResponse.Success -> {
|
||||
val data = response.data
|
||||
if (data.error != null) {
|
||||
Timber.w(
|
||||
TangemLogger.w(
|
||||
"P2PEthPool API returned error for vault ${vault.vaultAddress}, " +
|
||||
"address $address: ${data.error ?: "error"}",
|
||||
)
|
||||
|
|
@ -219,17 +222,17 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor(
|
|||
}
|
||||
}
|
||||
is ApiResponse.Error -> {
|
||||
Timber.w(
|
||||
response.cause,
|
||||
TangemLogger.w(
|
||||
"Failed to fetch P2PEthPool balance for vault ${vault.vaultAddress}, " +
|
||||
"address $address",
|
||||
response.cause,
|
||||
)
|
||||
}
|
||||
}
|
||||
}.onFailure { error ->
|
||||
Timber.w(
|
||||
error,
|
||||
TangemLogger.w(
|
||||
"Failed to fetch P2PEthPool balance for vault ${vault.vaultAddress}, address $address",
|
||||
error,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -245,7 +248,7 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor(
|
|||
|
||||
if (!isSupportedByWallet) {
|
||||
val exception = IllegalStateException("Wallet $userWalletId is not supported: $maybeUserWallet")
|
||||
Timber.e(exception)
|
||||
TangemLogger.e("Error", exception)
|
||||
|
||||
ifNotSupported(exception)
|
||||
}
|
||||
|
|
@ -263,7 +266,7 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor(
|
|||
val availableStakingIds = groupedStakingIds[true].orEmpty()
|
||||
val unavailableStakingIds = groupedStakingIds[false].orEmpty()
|
||||
|
||||
Timber.i(
|
||||
TangemLogger.i(
|
||||
"""
|
||||
Available staking IDs: ${availableStakingIds.joinToString()}
|
||||
Unavailable staking IDs: ${unavailableStakingIds.joinToString()}
|
||||
|
|
@ -282,7 +285,7 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor(
|
|||
– stakingIds: ${stakingIds.joinToString()}
|
||||
""".trimIndent(),
|
||||
)
|
||||
Timber.i(exception)
|
||||
TangemLogger.i(exception.toString())
|
||||
throw exception
|
||||
}
|
||||
}
|
||||
|
|
@ -293,7 +296,7 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor(
|
|||
|
||||
if (yieldsIds.isEmpty()) {
|
||||
val exception = IllegalStateException("No enabled yields for $userWalletId")
|
||||
Timber.e(exception)
|
||||
TangemLogger.e("Error", exception)
|
||||
|
||||
throw exception
|
||||
}
|
||||
|
|
@ -320,7 +323,7 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor(
|
|||
.toSet()
|
||||
}
|
||||
|
||||
Timber.i(
|
||||
TangemLogger.i(
|
||||
"Successfully fetched staking balances for $userWalletId:\n${yieldBalances.joinToString("\n")}",
|
||||
)
|
||||
stakeKitBalancesStore.storeActual(userWalletId = userWalletId, values = yieldBalances)
|
||||
|
|
@ -337,7 +340,7 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor(
|
|||
}
|
||||
},
|
||||
onError = { throwable ->
|
||||
Timber.e(throwable, "Unable to fetch staking balances $userWalletId")
|
||||
TangemLogger.e("Unable to fetch staking balances $userWalletId", throwable)
|
||||
|
||||
stakeKitBalancesStore.storeError(userWalletId = userWalletId, stakingIds = stakingIds)
|
||||
|
||||
|
|
|
|||
|
|
@ -10,13 +10,13 @@ import com.tangem.domain.staking.multi.MultiStakingBalanceSupplier
|
|||
import com.tangem.domain.staking.single.SingleStakingBalanceProducer
|
||||
import com.tangem.domain.staking.single.SingleStakingBalanceProducer.Companion.selectStakingBalance
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Default implementation of [SingleStakingBalanceProducer]
|
||||
|
|
@ -39,7 +39,7 @@ internal class DefaultSingleStakingBalanceProducer @AssistedInject constructor(
|
|||
override val fallback: Option<StakingBalance> = StakingBalance.Error(stakingId = params.stakingId).some()
|
||||
|
||||
override fun produce(): Flow<StakingBalance> {
|
||||
Timber.i("Producing staking balance for params:\n$params")
|
||||
TangemLogger.i("Producing staking balance for params:\n$params")
|
||||
|
||||
return multiStakingBalanceSupplier(
|
||||
params = MultiStakingBalanceProducer.Params(userWalletId = params.userWalletId),
|
||||
|
|
|
|||
|
|
@ -9,10 +9,10 @@ import com.tangem.domain.staking.StakingIdFactory
|
|||
import com.tangem.domain.staking.utils.StakingCleaner
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.runSuspendCatching
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Default implementation of [StakingCleaner].
|
||||
|
|
@ -32,7 +32,7 @@ internal class DefaultStakingCleaner(
|
|||
|
||||
override suspend fun invoke(userWalletId: UserWalletId, currencies: List<CryptoCurrency>) {
|
||||
if (currencies.isEmpty()) {
|
||||
Timber.d("No currencies to clear for wallet: $userWalletId")
|
||||
TangemLogger.d("No currencies to clear for wallet: $userWalletId")
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -41,7 +41,7 @@ internal class DefaultStakingCleaner(
|
|||
}
|
||||
|
||||
if (stakingIds.isEmpty()) {
|
||||
Timber.d("All currencies have no stakingIds to clear for wallet: $userWalletId")
|
||||
TangemLogger.d("All currencies have no stakingIds to clear for wallet: $userWalletId")
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -50,7 +50,7 @@ internal class DefaultStakingCleaner(
|
|||
|
||||
override suspend fun invoke(userWalletId: UserWalletId, stakingIds: Set<StakingID>) {
|
||||
if (stakingIds.isEmpty()) {
|
||||
Timber.d("No stakingIds to clear for wallet: $userWalletId")
|
||||
TangemLogger.d("No stakingIds to clear for wallet: $userWalletId")
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -66,13 +66,13 @@ internal class DefaultStakingCleaner(
|
|||
runSuspendCatching {
|
||||
stakeKitBalancesStore.clear(userWalletId, stakingIds)
|
||||
}
|
||||
.onFailure { Timber.e(it, "Failed to clear StakeKit balance statuses for wallet: $userWalletId") }
|
||||
.onFailure { TangemLogger.e("Failed to clear StakeKit balance statuses for wallet: $userWalletId", it) }
|
||||
}
|
||||
|
||||
private suspend fun clearP2PEthPoolBalancesStore(userWalletId: UserWalletId, stakingIds: Set<StakingID>) {
|
||||
runSuspendCatching {
|
||||
p2pEthPoolBalancesStore.clear(userWalletId, stakingIds)
|
||||
}
|
||||
.onFailure { Timber.e(it, "Failed to clear P2PEthPool balance statuses for wallet: $userWalletId") }
|
||||
.onFailure { TangemLogger.e("Failed to clear P2PEthPool balance statuses for wallet: $userWalletId", it) }
|
||||
}
|
||||
}
|
||||
|
|
@ -52,7 +52,6 @@ dependencies {
|
|||
implementation(deps.kotlin.coroutines)
|
||||
implementation(deps.moshi)
|
||||
implementation(deps.moshi.kotlin)
|
||||
implementation(deps.timber)
|
||||
|
||||
/** DI */
|
||||
implementation(deps.hilt.android)
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ import com.tangem.datasource.api.express.TangemExpressApi
|
|||
import com.tangem.datasource.api.express.models.request.ExchangeSentRequestBody
|
||||
import com.tangem.datasource.api.express.models.request.PairsRequestBody
|
||||
import com.tangem.datasource.api.express.models.response.ExchangeDataResponseWithTxDetails
|
||||
import com.tangem.datasource.api.express.models.response.RateType
|
||||
import com.tangem.datasource.api.express.models.response.SwapPairProvider
|
||||
import com.tangem.datasource.api.express.models.response.TxDetails
|
||||
import com.tangem.datasource.crypto.DataSignatureVerifier
|
||||
import com.tangem.datasource.di.NetworkMoshi
|
||||
|
|
@ -35,11 +37,11 @@ import com.tangem.domain.swap.models.*
|
|||
import com.tangem.domain.swap.models.SwapAmountType
|
||||
import com.tangem.domain.tokens.operations.CryptoCurrencyStatusFactory
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import java.io.IOException
|
||||
import java.math.BigDecimal
|
||||
import java.util.UUID
|
||||
|
|
@ -98,8 +100,7 @@ internal class DefaultSwapRepositoryV2 @Inject constructor(
|
|||
}
|
||||
|
||||
val mappedProviders = pair.providers
|
||||
.filterNot { it.hasOnlyFixedRateType() }
|
||||
.mapNotNull { expressProviders[it.providerId] }
|
||||
.mapNotNull { it.withExpressProvider(expressProviders) }
|
||||
.filterYieldSupplyProvider(statusFrom)
|
||||
|
||||
if (statusFrom != null && statusTo != null && mappedProviders.isNotEmpty()) {
|
||||
|
|
@ -157,8 +158,7 @@ internal class DefaultSwapRepositoryV2 @Inject constructor(
|
|||
val currencyStatusTo = createSendWithSwapCryptoCurrencyStatus(statusToDeferred.await())
|
||||
|
||||
val mappedProvider = pair.providers
|
||||
.filterNot { it.hasOnlyFixedRateType() }
|
||||
.mapNotNull { mappedProviders[it.providerId] }
|
||||
.mapNotNull { it.withExpressProvider(mappedProviders) }
|
||||
.filterYieldSupplyProvider(currencyStatusFrom)
|
||||
|
||||
if (currencyStatusFrom != null && currencyStatusTo != null && mappedProvider.isNotEmpty()) {
|
||||
|
|
@ -221,6 +221,7 @@ internal class DefaultSwapRepositoryV2 @Inject constructor(
|
|||
toTokenAmount = toTokenAmount,
|
||||
fromTokenAmount = fromTokenAmount,
|
||||
allowanceContract = response.allowanceContract,
|
||||
quoteId = response.quoteId,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -235,6 +236,7 @@ internal class DefaultSwapRepositoryV2 @Inject constructor(
|
|||
expressProvider: ExpressProvider,
|
||||
rateType: ExpressRateType,
|
||||
expressOperationType: ExpressOperationType,
|
||||
quoteId: String?,
|
||||
): SwapDataModel = withContext(coroutineDispatcher.io) {
|
||||
val requestId = UUID.randomUUID().toString()
|
||||
val (fromCurrency, fromStatus) = fromCryptoCurrencyStatus
|
||||
|
|
@ -273,6 +275,7 @@ internal class DefaultSwapRepositoryV2 @Inject constructor(
|
|||
appPreferencesStore = appPreferencesStore,
|
||||
),
|
||||
toExtraId = toExtraId?.ifEmpty { null },
|
||||
quoteId = quoteId,
|
||||
).getOrThrow()
|
||||
|
||||
if (dataSignatureVerifier.verifySignature(response.signature, response.txDetailsJson)) {
|
||||
|
|
@ -392,7 +395,7 @@ internal class DefaultSwapRepositoryV2 @Inject constructor(
|
|||
).getOrThrow()
|
||||
},
|
||||
onError = { error ->
|
||||
Timber.w(error, "Unable to get pairs")
|
||||
TangemLogger.w("Unable to get pairs", error)
|
||||
throw error
|
||||
},
|
||||
)
|
||||
|
|
@ -438,11 +441,23 @@ internal class DefaultSwapRepositoryV2 @Inject constructor(
|
|||
return try {
|
||||
txDetailsMoshiAdapter.fromJson(txDetailsJson)
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e, "error parsing txDetailsJson")
|
||||
TangemLogger.e("error parsing txDetailsJson", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun SwapPairProvider.withExpressProvider(
|
||||
expressProviders: Map<String, ExpressProvider>,
|
||||
): ExpressProvider? {
|
||||
val provider = expressProviders[providerId] ?: return null
|
||||
return provider.copy(rateTypes = rateTypes.map { it.toExpressRateType() })
|
||||
}
|
||||
|
||||
private fun RateType.toExpressRateType(): ExpressRateType = when (this) {
|
||||
RateType.FLOAT -> ExpressRateType.Float
|
||||
RateType.FIXED -> ExpressRateType.Fixed
|
||||
}
|
||||
|
||||
private fun CryptoCurrency.getContractAddress(): String {
|
||||
return when (this) {
|
||||
is CryptoCurrency.Token -> this.contractAddress
|
||||
|
|
@ -463,8 +478,6 @@ internal class DefaultSwapRepositoryV2 @Inject constructor(
|
|||
}
|
||||
|
||||
private val MEMO_RESTRICTED_NETWORKS = setOf(
|
||||
Blockchain.XRP.toNetworkId(),
|
||||
Blockchain.Stellar.toNetworkId(),
|
||||
Blockchain.InternetComputer.toNetworkId(),
|
||||
Blockchain.Casper.toNetworkId(),
|
||||
Blockchain.Algorand.toNetworkId(),
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ dependencies {
|
|||
implementation(projects.domain.tokens.models)
|
||||
implementation(projects.domain.txhistory.models)
|
||||
implementation(projects.domain.walletManager)
|
||||
implementation(projects.domain.transaction)
|
||||
implementation(projects.domain.wallets.models)
|
||||
// endregion
|
||||
|
||||
|
|
@ -72,7 +73,6 @@ dependencies {
|
|||
implementation(deps.kotlin.coroutines)
|
||||
implementation(deps.moshi.kotlin)
|
||||
implementation(deps.retrofit) // For HttpException
|
||||
implementation(deps.timber)
|
||||
ksp(deps.moshi.kotlin.codegen)
|
||||
kaptForObfuscatingVariants(deps.retrofit.response.type.keeper)
|
||||
// endregion
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import com.tangem.domain.tokens.repository.CurrenciesRepository
|
|||
import com.tangem.domain.tokens.repository.CurrencyChecksRepository
|
||||
import com.tangem.domain.tokens.repository.TokenReceiveWarningsViewedRepository
|
||||
import com.tangem.domain.tokens.repository.YieldSupplyWarningsViewedRepository
|
||||
import com.tangem.domain.transaction.MemoValidatorFacade
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
|
|
@ -47,10 +48,12 @@ internal object TokensDataModule {
|
|||
@Singleton
|
||||
fun provideCurrencyChecksRepository(
|
||||
walletManagersFacade: WalletManagersFacade,
|
||||
memoValidatorFacade: MemoValidatorFacade,
|
||||
coroutineDispatcherProvider: CoroutineDispatcherProvider,
|
||||
): CurrencyChecksRepository {
|
||||
return DefaultCurrencyChecksRepository(
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
memoValidatorFacade = memoValidatorFacade,
|
||||
coroutineDispatchers = coroutineDispatcherProvider,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import com.tangem.domain.tokens.model.CurrencyAmount
|
|||
import com.tangem.domain.tokens.model.blockchains.UtxoAmountLimit
|
||||
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning
|
||||
import com.tangem.domain.tokens.repository.CurrencyChecksRepository
|
||||
import com.tangem.domain.transaction.MemoValidatorFacade
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.extensions.isZero
|
||||
|
|
@ -23,6 +24,7 @@ import java.math.BigDecimal
|
|||
|
||||
internal class DefaultCurrencyChecksRepository(
|
||||
private val walletManagersFacade: WalletManagersFacade,
|
||||
private val memoValidatorFacade: MemoValidatorFacade,
|
||||
private val coroutineDispatchers: CoroutineDispatcherProvider,
|
||||
) : CurrencyChecksRepository {
|
||||
|
||||
|
|
@ -112,6 +114,10 @@ internal class DefaultCurrencyChecksRepository(
|
|||
return if (manager is ReserveAmountProvider) manager.isAccountFunded(address) else true
|
||||
}
|
||||
|
||||
override suspend fun checkIfMemoRequired(network: Network, address: String): Boolean {
|
||||
return memoValidatorFacade.isMemoRequired(network, address)
|
||||
}
|
||||
|
||||
override suspend fun checkUtxoAmountLimit(
|
||||
userWalletId: UserWalletId,
|
||||
network: Network,
|
||||
|
|
|
|||
|
|
@ -8,10 +8,10 @@ import com.tangem.datasource.api.tangemTech.models.CoinsResponse
|
|||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Responsible for merging custom tokens into a user's token response.
|
||||
|
|
@ -105,7 +105,7 @@ internal class CustomTokensMerger(
|
|||
).bind()
|
||||
},
|
||||
onError = { error ->
|
||||
Timber.e(error, "Unable to fetch token:\n$token")
|
||||
TangemLogger.e("Unable to fetch token:\n$token", error)
|
||||
null
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -11,14 +11,24 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
api(projects.domain.tokensync)
|
||||
implementation(projects.domain.tokens)
|
||||
implementation(projects.domain.tokens.models)
|
||||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.walletManager)
|
||||
implementation(projects.domain.wallets)
|
||||
implementation(projects.data.common)
|
||||
implementation(projects.libs.blockchainSdk)
|
||||
implementation(projects.core.datasource)
|
||||
implementation(projects.core.utils)
|
||||
implementation(projects.domain.models)
|
||||
|
||||
implementation(tangemDeps.blockchain)
|
||||
|
||||
implementation(deps.androidx.datastore)
|
||||
|
||||
implementation(deps.hilt.android)
|
||||
kapt(deps.hilt.kapt)
|
||||
|
||||
implementation(deps.androidx.datastore)
|
||||
implementation(deps.kotlin.coroutines)
|
||||
implementation(deps.moshi)
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
package com.tangem.data.tokensync.di
|
||||
|
||||
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
|
||||
import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory
|
||||
import com.tangem.data.common.network.NetworkFactory
|
||||
import com.tangem.data.tokensync.repository.DefaultTokenSyncRepository
|
||||
import com.tangem.data.tokensync.store.TokenSyncStoreFactory
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.tokensync.repository.TokenSyncRepository
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object TokenSyncDataModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideTokenSyncRepository(
|
||||
walletManagersFacade: WalletManagersFacade,
|
||||
tangemTechApi: TangemTechApi,
|
||||
userWalletsListRepository: UserWalletsListRepository,
|
||||
networkFactory: NetworkFactory,
|
||||
appPreferencesStore: AppPreferencesStore,
|
||||
tokenSyncStoreFactory: TokenSyncStoreFactory,
|
||||
responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
excludedBlockchains: ExcludedBlockchains,
|
||||
): TokenSyncRepository {
|
||||
return DefaultTokenSyncRepository(
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
tangemTechApi = tangemTechApi,
|
||||
userWalletsListRepository = userWalletsListRepository,
|
||||
networkFactory = networkFactory,
|
||||
appPreferencesStore = appPreferencesStore,
|
||||
tokenSyncStoreFactory = tokenSyncStoreFactory,
|
||||
responseCryptoCurrenciesFactory = responseCryptoCurrenciesFactory,
|
||||
dispatchers = dispatchers,
|
||||
excludedBlockchains = excludedBlockchains,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,346 @@
|
|||
package com.tangem.data.tokensync.repository
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.tokenbalance.models.TokenBalance
|
||||
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
|
||||
import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory
|
||||
import com.tangem.data.common.network.NetworkFactory
|
||||
import com.tangem.data.tokensync.store.TokenSyncStore
|
||||
import com.tangem.data.tokensync.store.TokenSyncStoreFactory
|
||||
import com.tangem.datasource.api.common.response.getOrThrow
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.api.tangemTech.models.CoinsResponse
|
||||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys
|
||||
import com.tangem.datasource.local.preferences.utils.getObjectMapSync
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.common.wallets.getSyncStrict
|
||||
import com.tangem.domain.models.account.DerivationIndex
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.tokensync.model.TokenSyncProgress
|
||||
import com.tangem.domain.tokensync.repository.TokenSyncRepository
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.sync.Semaphore
|
||||
import kotlinx.coroutines.sync.withPermit
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.math.BigDecimal
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
internal class DefaultTokenSyncRepository(
|
||||
private val walletManagersFacade: WalletManagersFacade,
|
||||
private val tangemTechApi: TangemTechApi,
|
||||
private val userWalletsListRepository: UserWalletsListRepository,
|
||||
private val networkFactory: NetworkFactory,
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
private val tokenSyncStoreFactory: TokenSyncStoreFactory,
|
||||
private val responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val excludedBlockchains: ExcludedBlockchains,
|
||||
) : TokenSyncRepository {
|
||||
|
||||
private val semaphore = Semaphore(MAX_CONCURRENT_REQUESTS)
|
||||
private val progressStates = ConcurrentHashMap<String, MutableStateFlow<TokenSyncProgress>>()
|
||||
|
||||
override fun observeSyncProgress(userWalletId: UserWalletId): Flow<TokenSyncProgress> {
|
||||
return getProgressFlow(userWalletId)
|
||||
}
|
||||
|
||||
override fun acknowledgeCompletion(userWalletId: UserWalletId) {
|
||||
val key = userWalletId.stringValue
|
||||
val stateFlow = progressStates[key] ?: return
|
||||
stateFlow.value = TokenSyncProgress.Idle
|
||||
progressStates.remove(key, stateFlow)
|
||||
}
|
||||
|
||||
override suspend fun getDiscoveredCurrencies(userWalletId: UserWalletId): List<CryptoCurrency> {
|
||||
val tokenSyncStore = tokenSyncStoreFactory.provide(userWalletId)
|
||||
val storedTokens = tokenSyncStore.get()
|
||||
if (storedTokens.isEmpty()) return emptyList()
|
||||
|
||||
val userWallet = userWalletsListRepository.getSyncStrict(userWalletId)
|
||||
return responseCryptoCurrenciesFactory.createCurrencies(
|
||||
tokens = storedTokens,
|
||||
userWallet = userWallet,
|
||||
accountIndex = DerivationIndex.Main,
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun clearDiscoveredTokens(userWalletId: UserWalletId) {
|
||||
val tokenSyncStore = tokenSyncStoreFactory.provide(userWalletId)
|
||||
tokenSyncStore.clear()
|
||||
}
|
||||
|
||||
override suspend fun clearPendingFlag(userWalletId: UserWalletId) {
|
||||
setPendingFlag(userWalletId, value = false)
|
||||
}
|
||||
|
||||
override suspend fun getPendingSyncWalletIds(): List<UserWalletId> {
|
||||
val pendingMap = appPreferencesStore
|
||||
.getObjectMapSync<Boolean>(PreferencesKeys.PENDING_DISCOVERY_SYNC_KEY)
|
||||
return pendingMap
|
||||
.filter { it.value }
|
||||
.map { UserWalletId(it.key) }
|
||||
}
|
||||
|
||||
override suspend fun runSync(userWalletId: UserWalletId) {
|
||||
val networks = getSupportedNetworks(userWalletId)
|
||||
|
||||
if (networks.isEmpty()) return
|
||||
|
||||
setPendingFlag(userWalletId, value = true)
|
||||
val tokenSyncStore = tokenSyncStoreFactory.provide(userWalletId)
|
||||
tokenSyncStore.clear()
|
||||
|
||||
val batches = networks.chunked(MAX_CONCURRENT_REQUESTS)
|
||||
var completedNetworks = 0
|
||||
getProgressFlow(userWalletId).value = TokenSyncProgress.InProgress(
|
||||
completedNetworks = 0,
|
||||
totalNetworks = networks.size,
|
||||
)
|
||||
|
||||
for (batch in batches) {
|
||||
val batchResults = processBatch(userWalletId, batch)
|
||||
completedNetworks = handleBatchResults(
|
||||
userWalletId = userWalletId,
|
||||
results = batchResults,
|
||||
tokenSyncStore = tokenSyncStore,
|
||||
completedNetworks = completedNetworks,
|
||||
totalNetworks = networks.size,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun completeSync(userWalletId: UserWalletId) {
|
||||
setPendingFlag(userWalletId, value = false)
|
||||
getProgressFlow(userWalletId).value = TokenSyncProgress.Completed
|
||||
}
|
||||
|
||||
private suspend fun processBatch(userWalletId: UserWalletId, batch: List<Network>): List<NetworkResult> {
|
||||
return coroutineScope {
|
||||
batch.map { network ->
|
||||
async(dispatchers.io) {
|
||||
semaphore.withPermit {
|
||||
processNetwork(userWalletId, network)
|
||||
}
|
||||
}
|
||||
}.awaitAll()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleBatchResults(
|
||||
userWalletId: UserWalletId,
|
||||
results: List<NetworkResult>,
|
||||
tokenSyncStore: TokenSyncStore,
|
||||
completedNetworks: Int,
|
||||
totalNetworks: Int,
|
||||
): Int {
|
||||
var completed = completedNetworks
|
||||
val progressFlow = getProgressFlow(userWalletId)
|
||||
|
||||
for (result in results) {
|
||||
completed++
|
||||
handleNetworkResult(result, tokenSyncStore)
|
||||
progressFlow.value = TokenSyncProgress.InProgress(
|
||||
completedNetworks = completed,
|
||||
totalNetworks = totalNetworks,
|
||||
)
|
||||
}
|
||||
|
||||
return completed
|
||||
}
|
||||
|
||||
private suspend fun handleNetworkResult(result: NetworkResult, tokenSyncStore: TokenSyncStore) {
|
||||
when (result) {
|
||||
is NetworkResult.Success -> {
|
||||
if (result.responseTokens.isNotEmpty()) {
|
||||
try {
|
||||
tokenSyncStore.append(result.responseTokens)
|
||||
} catch (e: Exception) {
|
||||
TangemLogger.e("Failed to store discovered tokens for network: ${result.networkId}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
is NetworkResult.Error -> {
|
||||
TangemLogger.e("Token sync failed for network: ${result.networkId}", result.cause)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun processNetwork(userWalletId: UserWalletId, network: Network): NetworkResult {
|
||||
return try {
|
||||
val tokenBalances = fetchAndFilterTokenBalances(userWalletId, network)
|
||||
|
||||
if (tokenBalances.isEmpty()) {
|
||||
return NetworkResult.Success(
|
||||
networkId = network.backendId,
|
||||
responseTokens = emptyList(),
|
||||
)
|
||||
}
|
||||
|
||||
val enrichedTokens = enrichTokensWithCatalog(tokenBalances, network)
|
||||
|
||||
val responseTokens = enrichedTokens
|
||||
.filter { it.contractAddress != null }
|
||||
.map { it.toResponseToken() }
|
||||
|
||||
NetworkResult.Success(
|
||||
networkId = network.backendId,
|
||||
responseTokens = responseTokens,
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
NetworkResult.Error(networkId = network.backendId, cause = e)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchAndFilterTokenBalances(userWalletId: UserWalletId, network: Network): List<TokenBalance> {
|
||||
return withContext(dispatchers.io) {
|
||||
walletManagersFacade.getTokenBalances(userWalletId, network)
|
||||
.filter { it.amount > BigDecimal.ZERO }
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun enrichTokensWithCatalog(
|
||||
tokenBalances: List<TokenBalance>,
|
||||
network: Network,
|
||||
): List<DiscoveredToken> = withContext(dispatchers.io) {
|
||||
val tokensToEnrich = tokenBalances.filter { !it.isNativeToken }
|
||||
|
||||
val catalogMap = fetchCatalogInfo(
|
||||
networkId = network.backendId,
|
||||
contractAddresses = tokensToEnrich.mapNotNull(TokenBalance::contractAddress),
|
||||
)
|
||||
|
||||
tokenBalances.mapNotNull { balance ->
|
||||
if (balance.isNativeToken) return@mapNotNull null
|
||||
|
||||
val contractAddressLower = balance.contractAddress?.lowercase()
|
||||
val coin = contractAddressLower?.let { catalogMap[it] } ?: return@mapNotNull null
|
||||
val decimals = coin.networks
|
||||
.find { it.contractAddress?.lowercase() == contractAddressLower }
|
||||
?.decimalCount
|
||||
?.toInt()
|
||||
?: 0
|
||||
|
||||
DiscoveredToken(
|
||||
contractAddress = balance.contractAddress,
|
||||
symbol = coin.symbol,
|
||||
name = coin.name,
|
||||
decimals = decimals,
|
||||
amount = balance.amount,
|
||||
isNativeToken = false,
|
||||
currencyId = coin.id,
|
||||
networkId = network.backendId,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchCatalogInfo(
|
||||
networkId: String,
|
||||
contractAddresses: List<String>,
|
||||
): Map<String, CoinsResponse.Coin> {
|
||||
if (contractAddresses.isEmpty()) return emptyMap()
|
||||
|
||||
return try {
|
||||
val response = tangemTechApi.getCoins(
|
||||
networkId = networkId,
|
||||
contractAddresses = contractAddresses.joinToString(","),
|
||||
active = true,
|
||||
).getOrThrow()
|
||||
|
||||
buildMap {
|
||||
for (coin in response.coins) {
|
||||
for (network in coin.networks) {
|
||||
val address = network.contractAddress?.lowercase() ?: continue
|
||||
put(address, coin)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
TangemLogger.w(
|
||||
"Failed to fetch catalog info for networkId=$networkId, addresses=${contractAddresses.size}",
|
||||
e,
|
||||
)
|
||||
emptyMap()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getSupportedNetworks(userWalletId: UserWalletId): List<Network> {
|
||||
val userWallet = userWalletsListRepository.getSyncStrict(userWalletId)
|
||||
|
||||
return Blockchain.entries
|
||||
.filter { !it.isTestnet() }
|
||||
.filter { it !in excludedBlockchains }
|
||||
.mapNotNull { blockchain ->
|
||||
networkFactory.create(
|
||||
blockchain = blockchain,
|
||||
extraDerivationPath = null,
|
||||
userWallet = userWallet,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getProgressFlow(userWalletId: UserWalletId): MutableStateFlow<TokenSyncProgress> {
|
||||
return progressStates.getOrPut(userWalletId.stringValue) {
|
||||
MutableStateFlow(TokenSyncProgress.Idle)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun setPendingFlag(userWalletId: UserWalletId, value: Boolean) {
|
||||
appPreferencesStore.editData { prefs ->
|
||||
prefs.setObjectMap(
|
||||
key = PreferencesKeys.PENDING_DISCOVERY_SYNC_KEY,
|
||||
value = prefs.getObjectMap<Boolean>(PreferencesKeys.PENDING_DISCOVERY_SYNC_KEY)
|
||||
.plus(userWalletId.stringValue to value),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun DiscoveredToken.toResponseToken(): UserTokensResponse.Token {
|
||||
return UserTokensResponse.Token(
|
||||
id = currencyId,
|
||||
networkId = networkId,
|
||||
name = name,
|
||||
symbol = symbol,
|
||||
decimals = decimals,
|
||||
contractAddress = contractAddress,
|
||||
)
|
||||
}
|
||||
|
||||
private data class DiscoveredToken(
|
||||
val contractAddress: String?,
|
||||
val symbol: String,
|
||||
val name: String,
|
||||
val decimals: Int,
|
||||
val amount: BigDecimal,
|
||||
val isNativeToken: Boolean,
|
||||
val currencyId: String?,
|
||||
val networkId: String,
|
||||
)
|
||||
|
||||
private sealed class NetworkResult {
|
||||
data class Success(
|
||||
val networkId: String,
|
||||
val responseTokens: List<UserTokensResponse.Token>,
|
||||
) : NetworkResult()
|
||||
|
||||
data class Error(
|
||||
val networkId: String,
|
||||
val cause: Throwable,
|
||||
) : NetworkResult()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val MAX_CONCURRENT_REQUESTS = 3
|
||||
}
|
||||
}
|
||||
|
|
@ -29,6 +29,7 @@ dependencies {
|
|||
|
||||
/** Domain */
|
||||
implementation(projects.libs.blockchainSdk)
|
||||
implementation(projects.libs.crypto)
|
||||
implementation(projects.domain.legacy)
|
||||
implementation(projects.domain.walletManager)
|
||||
implementation(projects.domain.wallets.models)
|
||||
|
|
@ -45,7 +46,6 @@ dependencies {
|
|||
kapt(deps.hilt.kapt)
|
||||
|
||||
/** Other */
|
||||
implementation(deps.timber)
|
||||
|
||||
/** tests */
|
||||
testImplementation(projects.common.test)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,73 @@
|
|||
package com.tangem.data.transaction
|
||||
|
||||
import com.tangem.blockchain.common.Approver
|
||||
import com.tangem.blockchain.common.Token
|
||||
import com.tangem.blockchainsdk.utils.toBlockchain
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.transaction.AllowanceRepository
|
||||
import com.tangem.domain.transaction.models.AllowanceInfo
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.lib.crypto.BlockchainUtils
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal class DefaultAllowanceRepository(
|
||||
private val walletManagersFacade: WalletManagersFacade,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : AllowanceRepository {
|
||||
|
||||
override suspend fun getAllowanceInfo(
|
||||
userWalletId: UserWalletId,
|
||||
cryptoCurrency: CryptoCurrency,
|
||||
spenderAddress: String,
|
||||
requiredAmount: BigDecimal,
|
||||
): AllowanceInfo {
|
||||
if (cryptoCurrency !is CryptoCurrency.Token) {
|
||||
error("CryptoCurrency must be of type Token")
|
||||
}
|
||||
|
||||
val allowance = getAllowance(
|
||||
userWalletId = userWalletId,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
spenderAddress = spenderAddress,
|
||||
)
|
||||
|
||||
return when {
|
||||
allowance >= requiredAmount -> AllowanceInfo.Enough(allowance)
|
||||
allowance > BigDecimal.ZERO && allowance < requiredAmount &&
|
||||
BlockchainUtils.isTetherInEthereum(
|
||||
blockchainId = cryptoCurrency.network.rawId,
|
||||
contractAddress = cryptoCurrency.contractAddress,
|
||||
) -> AllowanceInfo.ResetNeeded(allowance, requiredAmount)
|
||||
else -> AllowanceInfo.NotEnough(allowance, requiredAmount)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getAllowance(
|
||||
userWalletId: UserWalletId,
|
||||
cryptoCurrency: CryptoCurrency,
|
||||
spenderAddress: String,
|
||||
): BigDecimal = withContext(dispatchers.io) {
|
||||
if (cryptoCurrency !is CryptoCurrency.Token) {
|
||||
error("CryptoCurrency must be of type Token")
|
||||
}
|
||||
|
||||
val walletManager = walletManagersFacade.getOrCreateWalletManager(userWalletId, cryptoCurrency.network)
|
||||
val blockchain = cryptoCurrency.network.toBlockchain()
|
||||
val allowanceResult = (walletManager as? Approver)?.getAllowance(
|
||||
spenderAddress,
|
||||
Token(
|
||||
symbol = blockchain.currency,
|
||||
contractAddress = cryptoCurrency.contractAddress,
|
||||
decimals = cryptoCurrency.decimals,
|
||||
),
|
||||
) ?: error("Cannot cast to Approver")
|
||||
|
||||
allowanceResult.fold(
|
||||
onSuccess = { it },
|
||||
onFailure = { error(it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
package com.tangem.data.transaction
|
||||
|
||||
import com.tangem.blockchain.common.memo.MemoState
|
||||
import com.tangem.blockchain.extensions.Result
|
||||
import com.tangem.blockchainsdk.BlockchainSDKFactory
|
||||
import com.tangem.blockchainsdk.utils.toBlockchain
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.transaction.MemoValidatorFacade
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
internal class DefaultMemoValidatorFacade(
|
||||
private val blockchainSDKFactory: BlockchainSDKFactory,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : MemoValidatorFacade {
|
||||
|
||||
override suspend fun isMemoRequired(network: Network, destinationAddress: String): Boolean =
|
||||
withContext(dispatchers.io) {
|
||||
val blockchain = network.toBlockchain()
|
||||
val factory = blockchainSDKFactory.getMemoValidatorFactorySync() ?: return@withContext false
|
||||
val validator = factory.create(blockchain)
|
||||
when (val result = validator.isMemoRequired(destinationAddress)) {
|
||||
is Result.Success -> result.data
|
||||
is Result.Failure -> false
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun validateMemo(network: Network, memo: String): Boolean = withContext(dispatchers.io) {
|
||||
val blockchain = network.toBlockchain()
|
||||
val factory = blockchainSDKFactory.getMemoValidatorFactorySync() ?: return@withContext true
|
||||
val validator = factory.create(blockchain)
|
||||
when (val result = validator.validateMemo(memo)) {
|
||||
is Result.Success -> when (result.data) {
|
||||
MemoState.Valid,
|
||||
MemoState.NotSupported,
|
||||
-> true
|
||||
MemoState.Invalid -> false
|
||||
}
|
||||
is Result.Failure -> true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -25,7 +25,6 @@ import com.tangem.datasource.api.tangemTech.TangemTechApi
|
|||
import com.tangem.datasource.api.tangemTech.models.OperationType
|
||||
import com.tangem.datasource.api.tangemTech.models.TransactionEventBody
|
||||
import com.tangem.datasource.local.walletmanager.WalletManagersStore
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.transaction.TransactionRepository
|
||||
|
|
@ -33,8 +32,8 @@ import com.tangem.domain.transaction.models.EventTransactionTypeDto
|
|||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.runCatching
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import java.math.BigDecimal
|
||||
import java.math.BigInteger
|
||||
|
||||
|
|
@ -271,7 +270,7 @@ internal class DefaultTransactionRepository(
|
|||
Result.failure(ex)
|
||||
}
|
||||
} else {
|
||||
Timber.e("${walletManager?.wallet?.blockchain} does not support transaction validation")
|
||||
TangemLogger.e("${walletManager?.wallet?.blockchain} does not support transaction validation")
|
||||
Result.success(Unit)
|
||||
}
|
||||
}
|
||||
|
|
@ -332,28 +331,6 @@ internal class DefaultTransactionRepository(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun getAllowance(
|
||||
userWalletId: UserWalletId,
|
||||
cryptoCurrency: CryptoCurrency.Token,
|
||||
spenderAddress: String,
|
||||
): BigDecimal {
|
||||
val walletManager = walletManagersFacade.getOrCreateWalletManager(userWalletId, cryptoCurrency.network)
|
||||
val blockchain = cryptoCurrency.network.toBlockchain()
|
||||
val allowanceResult = (walletManager as? Approver)?.getAllowance(
|
||||
spenderAddress,
|
||||
Token(
|
||||
symbol = blockchain.currency,
|
||||
contractAddress = cryptoCurrency.contractAddress,
|
||||
decimals = cryptoCurrency.decimals,
|
||||
),
|
||||
) ?: error("Cannot cast to Approver")
|
||||
|
||||
return allowanceResult.fold(
|
||||
onSuccess = { it },
|
||||
onFailure = { error(it) },
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("CyclomaticComplexMethod")
|
||||
private fun getMemoExtras(networkId: String, memo: String?): TransactionExtras? {
|
||||
val blockchain = Blockchain.fromId(networkId)
|
||||
|
|
@ -441,14 +418,14 @@ internal class DefaultTransactionRepository(
|
|||
val response = tangemTechApi.transactionEvents(body)
|
||||
response.fold(
|
||||
onSuccess = {
|
||||
Timber.d("Successfully sent yield supply transaction hash: $hash")
|
||||
TangemLogger.d("Successfully sent yield supply transaction hash: $hash")
|
||||
},
|
||||
onError = { error ->
|
||||
Timber.e(error, "Failed to send yield supply transaction hash: $hash")
|
||||
TangemLogger.e("Failed to send yield supply transaction hash: $hash", error)
|
||||
},
|
||||
)
|
||||
}.onFailure { error ->
|
||||
Timber.e(error, "Failed to send yield supply transaction hash: $hash")
|
||||
TangemLogger.e("Failed to send yield supply transaction hash: $hash", error)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -460,7 +437,7 @@ internal class DefaultTransactionRepository(
|
|||
derivationPath = network.derivationPath.value,
|
||||
)
|
||||
val preparer = walletManager as? TransactionPreparer ?: run {
|
||||
Timber.e("${walletManager?.wallet?.blockchain} does not support TransactionBuilder")
|
||||
TangemLogger.e("${walletManager?.wallet?.blockchain} does not support TransactionBuilder")
|
||||
error("Wallet manager does not support TransactionPreparer")
|
||||
}
|
||||
return preparer
|
||||
|
|
|
|||
|
|
@ -6,12 +6,10 @@ import com.tangem.blockchain.common.Blockchain
|
|||
import com.tangem.blockchain.common.NameResolver
|
||||
import com.tangem.blockchain.common.ResolveAddressResult
|
||||
import com.tangem.blockchain.common.ReverseResolveAddressResult
|
||||
import com.tangem.blockchain.common.TransactionValidator
|
||||
import com.tangem.blockchain.common.memo.MemoState
|
||||
import com.tangem.blockchain.extensions.Result
|
||||
import com.tangem.blockchainsdk.utils.toBlockchain
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.transaction.MemoValidatorFacade
|
||||
import com.tangem.domain.transaction.WalletAddressServiceRepository
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.domain.wallets.models.ParsedQrCode
|
||||
|
|
@ -21,6 +19,7 @@ import kotlinx.coroutines.withContext
|
|||
|
||||
class DefaultWalletAddressServiceRepository(
|
||||
private val walletManagersFacade: WalletManagersFacade,
|
||||
private val memoValidatorFacade: MemoValidatorFacade,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : WalletAddressServiceRepository {
|
||||
|
||||
|
|
@ -96,28 +95,11 @@ class DefaultWalletAddressServiceRepository(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun validateMemo(userWalletId: UserWalletId, network: Network, memo: String): Boolean =
|
||||
withContext(dispatchers.io) {
|
||||
val walletManager = walletManagersFacade.getOrCreateWalletManager(
|
||||
userWalletId = userWalletId,
|
||||
network = network,
|
||||
) ?: return@withContext true
|
||||
override suspend fun validateMemo(network: Network, memo: String): Boolean =
|
||||
memoValidatorFacade.validateMemo(network, memo)
|
||||
|
||||
val memoStateResult = (walletManager as? TransactionValidator)?.validateMemo(memo)
|
||||
if (memoStateResult != null) {
|
||||
when (memoStateResult) {
|
||||
is Result.Success -> when (memoStateResult.data) {
|
||||
MemoState.NotSupported,
|
||||
MemoState.Valid,
|
||||
-> true
|
||||
MemoState.Invalid -> false
|
||||
}
|
||||
is Result.Failure -> true
|
||||
}
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
override suspend fun isMemoRequired(network: Network, destinationAddress: String): Boolean =
|
||||
memoValidatorFacade.isMemoRequired(network, destinationAddress)
|
||||
|
||||
override suspend fun parseSharedAddress(input: String, network: Network): ParsedQrCode {
|
||||
val blockchain = network.toBlockchain()
|
||||
|
|
|
|||
|
|
@ -1,19 +1,14 @@
|
|||
package com.tangem.data.transaction.di
|
||||
|
||||
import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory
|
||||
import com.tangem.data.transaction.DefaultFeeRepository
|
||||
import com.tangem.data.transaction.DefaultGaslessTransactionRepository
|
||||
import com.tangem.data.transaction.DefaultTransactionRepository
|
||||
import com.tangem.data.transaction.DefaultWalletAddressServiceRepository
|
||||
import com.tangem.data.transaction.*
|
||||
import com.tangem.data.transaction.error.DefaultFeeErrorResolver
|
||||
import com.tangem.blockchainsdk.BlockchainSDKFactory
|
||||
import com.tangem.datasource.api.gasless.GaslessTxServiceApi
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.local.walletmanager.WalletManagersStore
|
||||
import com.tangem.domain.demo.models.DemoConfig
|
||||
import com.tangem.domain.transaction.FeeRepository
|
||||
import com.tangem.domain.transaction.GaslessTransactionRepository
|
||||
import com.tangem.domain.transaction.TransactionRepository
|
||||
import com.tangem.domain.transaction.WalletAddressServiceRepository
|
||||
import com.tangem.domain.transaction.*
|
||||
import com.tangem.domain.transaction.error.FeeErrorResolver
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
|
|
@ -52,14 +47,28 @@ internal object TransactionDataModule {
|
|||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesMemoValidatorFacade(
|
||||
blockchainSDKFactory: BlockchainSDKFactory,
|
||||
coroutineDispatcherProvider: CoroutineDispatcherProvider,
|
||||
): MemoValidatorFacade {
|
||||
return DefaultMemoValidatorFacade(
|
||||
blockchainSDKFactory = blockchainSDKFactory,
|
||||
dispatchers = coroutineDispatcherProvider,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesWalletAddressServiceRepository(
|
||||
walletManagersFacade: WalletManagersFacade,
|
||||
memoValidatorFacade: MemoValidatorFacade,
|
||||
coroutineDispatcherProvider: CoroutineDispatcherProvider,
|
||||
): WalletAddressServiceRepository {
|
||||
return DefaultWalletAddressServiceRepository(
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
memoValidatorFacade = memoValidatorFacade,
|
||||
dispatchers = coroutineDispatcherProvider,
|
||||
)
|
||||
}
|
||||
|
|
@ -83,4 +92,16 @@ internal object TransactionDataModule {
|
|||
responseCryptoCurrenciesFactory = responseCryptoCurrenciesFactory,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideAllowanceRepository(
|
||||
walletManagersFacade: WalletManagersFacade,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): AllowanceRepository {
|
||||
return DefaultAllowanceRepository(
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,305 @@
|
|||
package com.tangem.data.transaction
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.blockchain.common.Approver
|
||||
import com.tangem.blockchain.common.Token
|
||||
import com.tangem.blockchain.common.WalletManager
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.transaction.models.AllowanceInfo
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Nested
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.assertThrows
|
||||
import java.math.BigDecimal
|
||||
|
||||
class DefaultAllowanceRepositoryTest {
|
||||
|
||||
private val userWalletId = UserWalletId(stringValue = "1234567890ABCDEF")
|
||||
private val spenderAddress = "0xSpender"
|
||||
|
||||
private val approverWalletManager: WalletManager =
|
||||
mockk<WalletManager>(moreInterfaces = arrayOf(Approver::class))
|
||||
|
||||
private val walletManagersFacade: WalletManagersFacade = mockk {
|
||||
coEvery { getOrCreateWalletManager(userWalletId, any<Network>()) } returns approverWalletManager
|
||||
}
|
||||
|
||||
private val dispatchers = TestingCoroutineDispatcherProvider()
|
||||
private lateinit var repository: DefaultAllowanceRepository
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
repository = DefaultAllowanceRepository(
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
}
|
||||
|
||||
// region getAllowance
|
||||
|
||||
@Nested
|
||||
inner class GetAllowanceTests {
|
||||
|
||||
@Test
|
||||
fun `throws when cryptoCurrency is Coin`() = runTest {
|
||||
val coin = buildCoin()
|
||||
|
||||
assertThrows<IllegalStateException> {
|
||||
repository.getAllowance(userWalletId, coin, spenderAddress)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `throws when walletManager is null`() = runTest {
|
||||
val token = buildToken()
|
||||
coEvery {
|
||||
walletManagersFacade.getOrCreateWalletManager(userWalletId, token.network)
|
||||
} returns null
|
||||
|
||||
assertThrows<IllegalStateException> {
|
||||
repository.getAllowance(userWalletId, token, spenderAddress)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `throws when walletManager is not Approver`() = runTest {
|
||||
val token = buildToken()
|
||||
val nonApproverWalletManager: WalletManager = mockk()
|
||||
coEvery {
|
||||
walletManagersFacade.getOrCreateWalletManager(userWalletId, token.network)
|
||||
} returns nonApproverWalletManager
|
||||
|
||||
assertThrows<IllegalStateException> {
|
||||
repository.getAllowance(userWalletId, token, spenderAddress)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns allowance on success`() = runTest {
|
||||
val token = buildToken()
|
||||
val expected = BigDecimal("100")
|
||||
|
||||
coEvery {
|
||||
(approverWalletManager as Approver).getAllowance(spenderAddress, any<Token>())
|
||||
} returns Result.success(expected)
|
||||
|
||||
val result = repository.getAllowance(userWalletId, token, spenderAddress)
|
||||
|
||||
assertThat(result).isEqualTo(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `throws when approver returns failure`() = runTest {
|
||||
val token = buildToken()
|
||||
coEvery {
|
||||
(approverWalletManager as Approver).getAllowance(spenderAddress, any<Token>())
|
||||
} returns Result.failure(RuntimeException("rpc error"))
|
||||
|
||||
assertThrows<IllegalStateException> {
|
||||
repository.getAllowance(userWalletId, token, spenderAddress)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region getAllowanceInfo
|
||||
|
||||
@Nested
|
||||
inner class GetAllowanceInfoTests {
|
||||
|
||||
@Test
|
||||
fun `throws when cryptoCurrency is Coin`() = runTest {
|
||||
val coin = buildCoin()
|
||||
|
||||
assertThrows<IllegalStateException> {
|
||||
repository.getAllowanceInfo(userWalletId, coin, spenderAddress, BigDecimal.ONE)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns Enough when allowance equals required amount`() = runTest {
|
||||
val token = buildToken(rawNetworkId = "polygon", rawCurrencyId = "usd-coin")
|
||||
val amount = BigDecimal("100")
|
||||
|
||||
coEvery {
|
||||
(approverWalletManager as Approver).getAllowance(spenderAddress, any())
|
||||
} returns Result.success(amount)
|
||||
|
||||
val result = repository.getAllowanceInfo(userWalletId, token, spenderAddress, amount)
|
||||
|
||||
assertThat(result).isInstanceOf(AllowanceInfo.Enough::class.java)
|
||||
assertThat((result as AllowanceInfo.Enough).allowance).isEqualTo(amount)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns Enough when allowance exceeds required amount`() = runTest {
|
||||
val token = buildToken(rawNetworkId = "polygon", rawCurrencyId = "usd-coin")
|
||||
|
||||
coEvery {
|
||||
(approverWalletManager as Approver).getAllowance(spenderAddress, any())
|
||||
} returns Result.success(BigDecimal("200"))
|
||||
|
||||
val result = repository.getAllowanceInfo(userWalletId, token, spenderAddress, BigDecimal("100"))
|
||||
|
||||
assertThat(result).isInstanceOf(AllowanceInfo.Enough::class.java)
|
||||
assertThat((result as AllowanceInfo.Enough).allowance).isEqualTo(BigDecimal("200"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns NotEnough when allowance is zero`() = runTest {
|
||||
val token = buildToken(rawNetworkId = "ethereum", rawCurrencyId = "tether")
|
||||
|
||||
coEvery {
|
||||
(approverWalletManager as Approver).getAllowance(spenderAddress, any())
|
||||
} returns Result.success(BigDecimal.ZERO)
|
||||
|
||||
val result = repository.getAllowanceInfo(userWalletId, token, spenderAddress, BigDecimal("50"))
|
||||
|
||||
assertThat(result).isInstanceOf(AllowanceInfo.NotEnough::class.java)
|
||||
result as AllowanceInfo.NotEnough
|
||||
assertThat(result.allowance).isEqualTo(BigDecimal.ZERO)
|
||||
assertThat(result.requiredAmount).isEqualTo(BigDecimal("50"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns NotEnough when partial allowance for non-tether token`() = runTest {
|
||||
val token = buildToken(rawNetworkId = "ethereum", rawCurrencyId = "usd-coin")
|
||||
|
||||
coEvery {
|
||||
(approverWalletManager as Approver).getAllowance(spenderAddress, any())
|
||||
} returns Result.success(BigDecimal("30"))
|
||||
|
||||
val result = repository.getAllowanceInfo(userWalletId, token, spenderAddress, BigDecimal("100"))
|
||||
|
||||
assertThat(result).isInstanceOf(AllowanceInfo.NotEnough::class.java)
|
||||
result as AllowanceInfo.NotEnough
|
||||
assertThat(result.allowance).isEqualTo(BigDecimal("30"))
|
||||
assertThat(result.requiredAmount).isEqualTo(BigDecimal("100"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns NotEnough when partial allowance for tether on non-ethereum network`() = runTest {
|
||||
val token = buildToken(rawNetworkId = "polygon", rawCurrencyId = "tether")
|
||||
|
||||
coEvery {
|
||||
(approverWalletManager as Approver).getAllowance(spenderAddress, any())
|
||||
} returns Result.success(BigDecimal("30"))
|
||||
|
||||
val result = repository.getAllowanceInfo(userWalletId, token, spenderAddress, BigDecimal("100"))
|
||||
|
||||
assertThat(result).isInstanceOf(AllowanceInfo.NotEnough::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns ResetNeeded when partial allowance for tether on ethereum`() = runTest {
|
||||
val token = buildToken(rawNetworkId = "ETH", rawCurrencyId = "tether")
|
||||
|
||||
coEvery {
|
||||
(approverWalletManager as Approver).getAllowance(spenderAddress, any())
|
||||
} returns Result.success(BigDecimal("30"))
|
||||
|
||||
val result = repository.getAllowanceInfo(userWalletId, token, spenderAddress, BigDecimal("100"))
|
||||
|
||||
assertThat(result).isInstanceOf(AllowanceInfo.ResetNeeded::class.java)
|
||||
result as AllowanceInfo.ResetNeeded
|
||||
assertThat(result.allowance).isEqualTo(BigDecimal("30"))
|
||||
assertThat(result.requiredAmount).isEqualTo(BigDecimal("100"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns ResetNeeded when partial allowance for tether on ethereum testnet`() = runTest {
|
||||
val token = buildToken(rawNetworkId = "ETH/test", rawCurrencyId = "tether")
|
||||
|
||||
coEvery {
|
||||
(approverWalletManager as Approver).getAllowance(spenderAddress, any())
|
||||
} returns Result.success(BigDecimal("10"))
|
||||
|
||||
val result = repository.getAllowanceInfo(userWalletId, token, spenderAddress, BigDecimal("50"))
|
||||
|
||||
assertThat(result).isInstanceOf(AllowanceInfo.ResetNeeded::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns Enough for tether on ethereum when allowance is sufficient`() = runTest {
|
||||
val token = buildToken(rawNetworkId = "ethereum", rawCurrencyId = "tether")
|
||||
|
||||
coEvery {
|
||||
(approverWalletManager as Approver).getAllowance(spenderAddress, any())
|
||||
} returns Result.success(BigDecimal("100"))
|
||||
|
||||
val result = repository.getAllowanceInfo(userWalletId, token, spenderAddress, BigDecimal("100"))
|
||||
|
||||
assertThat(result).isInstanceOf(AllowanceInfo.Enough::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region Helpers
|
||||
|
||||
private fun buildNetwork(rawNetworkId: String): Network {
|
||||
val derivationPath = Network.DerivationPath.None
|
||||
return Network(
|
||||
id = Network.ID(Network.RawID(rawNetworkId), derivationPath),
|
||||
backendId = rawNetworkId,
|
||||
name = rawNetworkId.replaceFirstChar { it.uppercase() },
|
||||
currencySymbol = "ETH",
|
||||
derivationPath = derivationPath,
|
||||
isTestnet = rawNetworkId.contains("test"),
|
||||
standardType = Network.StandardType.ERC20,
|
||||
hasFiatFeeRate = false,
|
||||
canHandleTokens = true,
|
||||
transactionExtrasType = Network.TransactionExtrasType.NONE,
|
||||
nameResolvingType = Network.NameResolvingType.NONE,
|
||||
)
|
||||
}
|
||||
|
||||
private fun buildToken(
|
||||
rawNetworkId: String = "ETH",
|
||||
rawCurrencyId: String = "tether",
|
||||
contractAddress: String = "0xdAC17F958D2ee523a2206206994597C13D831ec7",
|
||||
): CryptoCurrency.Token {
|
||||
val network = buildNetwork(rawNetworkId)
|
||||
return CryptoCurrency.Token(
|
||||
id = CryptoCurrency.ID(
|
||||
prefix = CryptoCurrency.ID.Prefix.TOKEN_PREFIX,
|
||||
body = CryptoCurrency.ID.Body.NetworkId(rawNetworkId),
|
||||
suffix = CryptoCurrency.ID.Suffix.RawID(rawCurrencyId),
|
||||
),
|
||||
network = network,
|
||||
name = "Token",
|
||||
symbol = "TKN",
|
||||
decimals = 6,
|
||||
iconUrl = null,
|
||||
isCustom = false,
|
||||
contractAddress = contractAddress,
|
||||
)
|
||||
}
|
||||
|
||||
private fun buildCoin(rawNetworkId: String = "ethereum"): CryptoCurrency.Coin {
|
||||
val network = buildNetwork(rawNetworkId)
|
||||
return CryptoCurrency.Coin(
|
||||
id = CryptoCurrency.ID(
|
||||
prefix = CryptoCurrency.ID.Prefix.COIN_PREFIX,
|
||||
body = CryptoCurrency.ID.Body.NetworkId(rawNetworkId),
|
||||
suffix = CryptoCurrency.ID.Suffix.RawID("ethereum"),
|
||||
),
|
||||
network = network,
|
||||
name = "Ethereum",
|
||||
symbol = "ETH",
|
||||
decimals = 18,
|
||||
iconUrl = null,
|
||||
isCustom = false,
|
||||
)
|
||||
}
|
||||
|
||||
// endregion
|
||||
}
|
||||
|
|
@ -29,7 +29,6 @@ dependencies {
|
|||
|
||||
implementation(deps.kotlin.coroutines)
|
||||
implementation(deps.androidx.paging.runtime)
|
||||
implementation(deps.timber)
|
||||
implementation(deps.jodatime)
|
||||
implementation(tangemDeps.blockchain)
|
||||
|
||||
|
|
|
|||
|
|
@ -20,9 +20,9 @@ import com.tangem.domain.txhistory.repository.TxHistoryRepository
|
|||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.domain.walletmanager.utils.SdkPageConverter
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
|
||||
class DefaultTxHistoryRepository(
|
||||
private val cacheRegistry: CacheRegistry,
|
||||
|
|
@ -110,7 +110,7 @@ class DefaultTxHistoryRepository(
|
|||
)
|
||||
?.items.orEmpty()
|
||||
} catch (e: Throwable) {
|
||||
Timber.e(e, "Unable to load the transaction history for the requested page: ${Page.Initial}")
|
||||
TangemLogger.e("Unable to load the transaction history for the requested page: ${Page.Initial}", e)
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import com.tangem.pagination.BatchFetchResult
|
|||
import com.tangem.pagination.BatchListSource
|
||||
import com.tangem.pagination.toBatchFlow
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import timber.log.Timber
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
|
||||
internal class RefactoredTxHistoryRepository(
|
||||
private val walletManagersFacade: WalletManagersFacade,
|
||||
|
|
@ -96,16 +96,17 @@ internal class RefactoredTxHistoryRepository(
|
|||
.filterIfTxAlreadyAdded(apiItems = items)
|
||||
|
||||
return if (recentItems.isEmpty()) {
|
||||
Timber.d("Nothing to add to TxHistory")
|
||||
TangemLogger.d("Nothing to add to TxHistory")
|
||||
this
|
||||
} else {
|
||||
Timber.d(
|
||||
"Recent transactions were added to TxHistory: %s",
|
||||
recentItems.joinToString(
|
||||
prefix = "[",
|
||||
postfix = "]",
|
||||
transform = TxInfo::txHash,
|
||||
),
|
||||
TangemLogger.d(
|
||||
"Recent transactions were added to TxHistory: ${
|
||||
recentItems.joinToString(
|
||||
prefix = "[",
|
||||
postfix = "]",
|
||||
transform = TxInfo::txHash,
|
||||
)
|
||||
}",
|
||||
)
|
||||
|
||||
return copy(items = recentItems + items)
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import com.tangem.domain.txhistory.models.Page
|
|||
import com.tangem.domain.txhistory.models.PaginationWrapper
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.domain.walletmanager.utils.SdkPageConverter
|
||||
import timber.log.Timber
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
|
||||
internal class TxHistoryPagingSource(
|
||||
private val sourceParams: Params,
|
||||
|
|
@ -44,7 +44,7 @@ internal class TxHistoryPagingSource(
|
|||
}
|
||||
LoadResult.Page(wrappedItems.items, prevKey = null, nextKey = nextKey)
|
||||
} catch (e: Throwable) {
|
||||
Timber.e(e, "Unable to load the transaction history for the requested page: $pageToLoad")
|
||||
TangemLogger.e("Unable to load the transaction history for the requested page: $pageToLoad", e)
|
||||
|
||||
LoadResult.Error(e)
|
||||
}
|
||||
|
|
@ -89,16 +89,17 @@ internal class TxHistoryPagingSource(
|
|||
.filterIfTxAlreadyAdded(apiItems = items)
|
||||
|
||||
return if (recentItems.isEmpty()) {
|
||||
Timber.d("Nothing to add to TxHistory")
|
||||
TangemLogger.d("Nothing to add to TxHistory")
|
||||
this
|
||||
} else {
|
||||
Timber.d(
|
||||
"Recent transactions were added to TxHistory: %s",
|
||||
recentItems.joinToString(
|
||||
prefix = "[",
|
||||
postfix = "]",
|
||||
transform = TxInfo::txHash,
|
||||
),
|
||||
TangemLogger.d(
|
||||
"Recent transactions were added to TxHistory: ${
|
||||
recentItems.joinToString(
|
||||
prefix = "[",
|
||||
postfix = "]",
|
||||
transform = TxInfo::txHash,
|
||||
)
|
||||
}",
|
||||
)
|
||||
|
||||
return copy(items = recentItems + items)
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue