Updated on 2026-08-14

This commit is contained in:
Tangem 2026-04-02 10:24:41 +03:00
commit af09ce9ca4
877 changed files with 16091 additions and 6613 deletions

View file

@ -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

View file

@ -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)
}

View file

@ -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"
}

View file

@ -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

View file

@ -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
}

View file

@ -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)
},
)

View file

@ -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

View file

@ -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)
}

View file

@ -33,5 +33,4 @@ dependencies {
/** Other */
implementation(deps.jodatime)
implementation(deps.kotlin.coroutines)
implementation(deps.timber)
}

View file

@ -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()
}

View file

@ -45,7 +45,6 @@ dependencies {
implementation(deps.arrow.core)
implementation(deps.jodatime)
implementation(deps.kotlin.coroutines)
implementation(deps.timber)
/* Test */
testImplementation(projects.common.test)

View file

@ -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)
},
)

View file

@ -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)

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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",
)

View file

@ -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
}

View file

@ -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 }

View file

@ -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"),
}

View file

@ -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

View file

@ -37,7 +37,6 @@ dependencies {
// region Other libraries
implementation(deps.androidx.datastore)
implementation(deps.moshi.kotlin)
implementation(deps.timber)
implementation(tangemDeps.blockchain)
// endregion

View file

@ -30,7 +30,6 @@ dependencies {
/** Other */
implementation(deps.moshi)
implementation(deps.moshi.kotlin)
implementation(deps.timber)
/** DI */
implementation(deps.hilt.android)

View file

@ -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
},
)

View file

@ -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
}
}

View file

@ -25,7 +25,6 @@ dependencies {
// endregion
// Other libraries
implementation(deps.timber)
// endregion
// region Core modules

View file

@ -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")
}
}

View file

@ -51,7 +51,6 @@ dependencies {
/** Other */
implementation(deps.moshi.kotlin)
implementation(deps.timber)
ksp(deps.moshi.kotlin.codegen)
kaptForObfuscatingVariants(deps.retrofit.response.type.keeper)
}

View file

@ -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(

View file

@ -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)

View file

@ -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,

View file

@ -45,7 +45,6 @@ dependencies {
// region Other libraries
implementation(deps.androidx.datastore)
implementation(deps.moshi)
implementation(deps.timber)
// endregion
// region Tests

View file

@ -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)

View file

@ -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)
}
}

View file

@ -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
}

View file

@ -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 ->

View file

@ -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
}

View file

@ -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)
}

View file

@ -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)
}
}
}

View file

@ -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)

View file

@ -39,7 +39,6 @@ dependencies {
// region Other libraries
implementation(deps.androidx.datastore)
implementation(deps.moshi.kotlin)
implementation(deps.timber)
// endregion
// region Tests

View file

@ -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(

View file

@ -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)

View file

@ -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)
}
}
}

View file

@ -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)

View file

@ -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(

View file

@ -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
}

View file

@ -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
View file

@ -0,0 +1 @@
/build

View 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)
}

View file

@ -14,7 +14,6 @@ dependencies {
implementation(deps.androidx.datastore)
implementation(deps.jodatime)
implementation(deps.timber)
implementation(deps.hilt.android)
kapt(deps.hilt.kapt)

View file

@ -44,7 +44,6 @@ dependencies {
// region Other libraries
implementation(deps.androidx.datastore)
implementation(deps.moshi.kotlin)
implementation(deps.timber)
// endregion
// region Tests

View file

@ -27,6 +27,7 @@ internal class QuoteStatusConverter(
source = source,
fiatRate = quote.price.orZero(),
priceChange = quote.priceChange24h.orZero().movePointLeft(2),
fiatRateUSD = quote.priceUsd.orZero(),
),
)
}

View file

@ -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)
}
}

View file

@ -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
}

View file

@ -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())

View file

@ -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()
}
}

View file

@ -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
}

View file

@ -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"),
),
),

View file

@ -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)
}
}

View file

@ -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?,
)
}

View file

@ -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
View file

@ -0,0 +1 @@
/build

View 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
}

View file

@ -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,
)
}
}

View file

@ -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)
}
}

View file

@ -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,
)

View file

@ -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()
}
}

View file

@ -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
}
}

View file

@ -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()
}

View file

@ -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

View file

@ -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
}

View file

@ -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)

View file

@ -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 {

View file

@ -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") },
)
}
}

View file

@ -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)
}
}
}

View file

@ -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)

View file

@ -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),

View file

@ -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) }
}
}

View file

@ -52,7 +52,6 @@ dependencies {
implementation(deps.kotlin.coroutines)
implementation(deps.moshi)
implementation(deps.moshi.kotlin)
implementation(deps.timber)
/** DI */
implementation(deps.hilt.android)

View file

@ -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(),

View file

@ -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

View file

@ -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,
)
}

View file

@ -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,

View file

@ -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
},
)

View file

@ -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)
}

View file

@ -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,
)
}
}

View file

@ -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
}
}

View file

@ -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)

View file

@ -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) },
)
}
}

View file

@ -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
}
}
}

View file

@ -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

View file

@ -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()

View file

@ -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,
)
}
}

View file

@ -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
}

View file

@ -29,7 +29,6 @@ dependencies {
implementation(deps.kotlin.coroutines)
implementation(deps.androidx.paging.runtime)
implementation(deps.timber)
implementation(deps.jodatime)
implementation(tangemDeps.blockchain)

View file

@ -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()
}
}

View file

@ -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)

View file

@ -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