Updated on 2026-08-14
This commit is contained in:
commit
af09ce9ca4
877 changed files with 16091 additions and 6613 deletions
|
|
@ -108,16 +108,14 @@ data class AccountList private constructor(
|
|||
}
|
||||
|
||||
fun flattenMapCurrencies(): Map<AccountCurrencyId, CryptoCurrency> = buildMap {
|
||||
accounts.forEach { acc ->
|
||||
val account = when (acc) {
|
||||
is Account.CryptoPortfolio -> acc
|
||||
is Account.Payment -> TODO("[REDACTED_JIRA]")
|
||||
accounts
|
||||
.filterIsInstance<Account.CryptoPortfolio>()
|
||||
.forEach { account ->
|
||||
account.cryptoCurrencies.forEach { currency ->
|
||||
val key = account.accountId to currency.id
|
||||
put(key, currency)
|
||||
}
|
||||
}
|
||||
account.cryptoCurrencies.forEach { currency ->
|
||||
val key = account.accountId to currency.id
|
||||
put(key, currency)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -28,18 +28,20 @@ dependencies {
|
|||
api(projects.domain.staking)
|
||||
api(projects.domain.tokens)
|
||||
api(projects.domain.tokens.models)
|
||||
api(projects.domain.visa)
|
||||
api(projects.domain.walletManager)
|
||||
api(projects.domain.wallets)
|
||||
|
||||
implementation(projects.libs.blockchainSdk)
|
||||
implementation(projects.libs.crypto)
|
||||
implementation(projects.core.utils)
|
||||
|
||||
implementation(deps.kotlin.datetime)
|
||||
implementation(deps.kotlin.serialization)
|
||||
implementation(deps.timber)
|
||||
implementation(deps.kermit)
|
||||
|
||||
implementation(tangemDeps.blockchain)
|
||||
implementation(tangemDeps.card.core)
|
||||
implementation(tangemDeps.hot.core)
|
||||
|
||||
// region DI
|
||||
implementation(deps.hilt.android)
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
package com.tangem.domain.account.status.producer
|
||||
|
||||
import co.touchlab.kermit.Logger
|
||||
import com.tangem.core.analytics.api.AnalyticsExceptionHandler
|
||||
import com.tangem.core.analytics.models.ExceptionAnalyticsEvent
|
||||
import com.tangem.domain.core.flow.FlowProducer
|
||||
import com.tangem.domain.core.flow.FlowProducerTools
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.*
|
||||
import javax.inject.Inject
|
||||
|
|
@ -55,7 +55,7 @@ class DefaultFlowProducerTools @Inject constructor(
|
|||
|
||||
private fun logError(cause: Throwable, flowProducerName: String, attempt: Long) {
|
||||
val tag = "FlowProducerRetryWhen"
|
||||
Logger.withTag(tag)
|
||||
TangemLogger.withTag(tag)
|
||||
.e("flowProducerName $flowProducerName attempt $attempt", cause)
|
||||
|
||||
val event = ExceptionAnalyticsEvent(
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.tangem.domain.account.status.producer
|
|||
import arrow.core.Option
|
||||
import arrow.core.none
|
||||
import arrow.core.toOption
|
||||
import com.tangem.common.card.FirmwareVersion
|
||||
import com.tangem.core.analytics.api.AnalyticsExceptionHandler
|
||||
import com.tangem.domain.account.models.AccountCurrencyId
|
||||
import com.tangem.domain.account.models.AccountList
|
||||
|
|
@ -33,6 +34,7 @@ import com.tangem.domain.models.wallet.isMultiCurrency
|
|||
import com.tangem.domain.networks.multi.MultiNetworkStatusProducer
|
||||
import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier
|
||||
import com.tangem.domain.networks.repository.NetworksRepository
|
||||
import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier
|
||||
import com.tangem.domain.quotes.multi.MultiQuoteStatusSupplier
|
||||
import com.tangem.domain.staking.StakingIdFactory
|
||||
import com.tangem.domain.staking.multi.MultiStakingBalanceProducer
|
||||
|
|
@ -42,6 +44,7 @@ import com.tangem.domain.tokens.operations.CryptoCurrencyStatusFactory
|
|||
import com.tangem.domain.tokens.operations.PriceChangeCalculator
|
||||
import com.tangem.domain.tokens.operations.TokenListFactory
|
||||
import com.tangem.domain.tokens.operations.TotalFiatBalanceCalculator
|
||||
import com.tangem.hot.sdk.model.HotWalletId
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
|
|
@ -69,6 +72,7 @@ import java.math.BigDecimal
|
|||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
// TODO: Move to :data:account:status [REDACTED_JIRA]
|
||||
@Suppress("LongParameterList")
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
internal class DefaultSingleAccountStatusListProducer @AssistedInject constructor(
|
||||
|
|
@ -76,6 +80,7 @@ internal class DefaultSingleAccountStatusListProducer @AssistedInject constructo
|
|||
override val flowProducerTools: FlowProducerTools,
|
||||
private val userWalletsListRepository: UserWalletsListRepository,
|
||||
private val singleAccountListSupplier: SingleAccountListSupplier,
|
||||
private val paymentAccountStatusSupplier: PaymentAccountStatusSupplier,
|
||||
private val networksRepository: NetworksRepository,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val networkStatusSupplier: MultiNetworkStatusSupplier,
|
||||
|
|
@ -116,31 +121,52 @@ internal class DefaultSingleAccountStatusListProducer @AssistedInject constructo
|
|||
flattenCurrency = flattenCurrency,
|
||||
)
|
||||
|
||||
combine(
|
||||
if (userWallet.isPaymentAccountSupported()) {
|
||||
combineWithPaymentAccount(
|
||||
accountListFlow = accountListFlow,
|
||||
cryptoCurrencyStatusFlow = cryptoCurrencyStatusFlow,
|
||||
paymentAccountStatusFlow = paymentAccountStatusSupplier.invoke(userWalletId = params.userWalletId),
|
||||
)
|
||||
} else {
|
||||
combineWithoutPaymentAccount(
|
||||
accountListFlow = accountListFlow,
|
||||
cryptoCurrencyStatusFlow = cryptoCurrencyStatusFlow,
|
||||
)
|
||||
}
|
||||
.collect { accountStatusList -> channel.send(accountStatusList) }
|
||||
}
|
||||
|
||||
private fun combineWithPaymentAccount(
|
||||
accountListFlow: StateFlow<AccountList>,
|
||||
cryptoCurrencyStatusFlow: Flow<Map<AccountCurrencyId, CryptoCurrencyStatus>>,
|
||||
paymentAccountStatusFlow: Flow<AccountStatus.Payment>,
|
||||
): Flow<AccountStatusList> {
|
||||
return combine(
|
||||
flow = accountListFlow,
|
||||
flow2 = cryptoCurrencyStatusFlow,
|
||||
transform = { accountList, currencyStatusMap ->
|
||||
val accountStatuses: List<AccountStatus.CryptoPortfolio> = accountList.accounts.map { acc ->
|
||||
val account: Account.CryptoPortfolio = when (acc) {
|
||||
is Account.CryptoPortfolio -> acc
|
||||
is Account.Payment -> TODO("[REDACTED_JIRA]")
|
||||
}
|
||||
if (account.cryptoCurrencies.isEmpty()) {
|
||||
account.toEmptyAccountStatus()
|
||||
} else {
|
||||
val statuses: List<CryptoCurrencyStatus> = account.cryptoCurrencies.map { currency ->
|
||||
val acId = account.accountId to currency.id
|
||||
currencyStatusMap[acId] ?: currency.toLoadingCurrencyStatus()
|
||||
flow3 = paymentAccountStatusFlow,
|
||||
transform = { accountList, currencyStatusMap, paymentAccountStatus ->
|
||||
val accountStatuses = accountList.accounts.map { account ->
|
||||
when (account) {
|
||||
is Account.Payment -> paymentAccountStatus
|
||||
is Account.CryptoPortfolio -> if (account.cryptoCurrencies.isEmpty()) {
|
||||
account.toEmptyAccountStatus()
|
||||
} else {
|
||||
val statuses: List<CryptoCurrencyStatus> =
|
||||
account.cryptoCurrencies.map { currency ->
|
||||
val acId = account.accountId to currency.id
|
||||
currencyStatusMap[acId] ?: currency.toLoadingCurrencyStatus()
|
||||
}
|
||||
AccountStatus.CryptoPortfolio(
|
||||
account = account,
|
||||
tokenList = TokenListFactory.create(
|
||||
statuses = statuses,
|
||||
groupType = accountList.groupType,
|
||||
sortType = accountList.sortType,
|
||||
),
|
||||
priceChangeLce = PriceChangeCalculator.calculate(statuses = statuses),
|
||||
)
|
||||
}
|
||||
AccountStatus.CryptoPortfolio(
|
||||
account = account,
|
||||
tokenList = TokenListFactory.create(
|
||||
statuses = statuses,
|
||||
groupType = accountList.groupType,
|
||||
sortType = accountList.sortType,
|
||||
),
|
||||
priceChangeLce = PriceChangeCalculator.calculate(statuses = statuses),
|
||||
)
|
||||
}
|
||||
}
|
||||
val balances = accountStatuses.flattenTotalFiatBalance()
|
||||
|
|
@ -156,7 +182,58 @@ internal class DefaultSingleAccountStatusListProducer @AssistedInject constructo
|
|||
)
|
||||
},
|
||||
)
|
||||
.collect { accountStatusList -> channel.send(accountStatusList) }
|
||||
}
|
||||
|
||||
private fun combineWithoutPaymentAccount(
|
||||
accountListFlow: StateFlow<AccountList>,
|
||||
cryptoCurrencyStatusFlow: Flow<Map<AccountCurrencyId, CryptoCurrencyStatus>>,
|
||||
): Flow<AccountStatusList> {
|
||||
return combine(
|
||||
flow = accountListFlow,
|
||||
flow2 = cryptoCurrencyStatusFlow,
|
||||
transform = { accountList, currencyStatusMap ->
|
||||
val accountStatuses = accountList.accounts
|
||||
.filterIsInstance<Account.CryptoPortfolio>()
|
||||
.map { account ->
|
||||
when (account) {
|
||||
is Account.CryptoPortfolio -> if (account.cryptoCurrencies.isEmpty()) {
|
||||
account.toEmptyAccountStatus()
|
||||
} else {
|
||||
val statuses: List<CryptoCurrencyStatus> =
|
||||
account.cryptoCurrencies.map { currency ->
|
||||
val acId = account.accountId to currency.id
|
||||
currencyStatusMap[acId] ?: currency.toLoadingCurrencyStatus()
|
||||
}
|
||||
AccountStatus.CryptoPortfolio(
|
||||
account = account,
|
||||
tokenList = TokenListFactory.create(
|
||||
statuses = statuses,
|
||||
groupType = accountList.groupType,
|
||||
sortType = accountList.sortType,
|
||||
),
|
||||
priceChangeLce = PriceChangeCalculator.calculate(statuses = statuses),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
val balances = accountStatuses.flattenTotalFiatBalance()
|
||||
|
||||
AccountStatusList(
|
||||
userWalletId = accountList.userWalletId,
|
||||
accountStatuses = accountStatuses,
|
||||
totalAccounts = accountList.totalAccounts,
|
||||
totalFiatBalance = TotalFiatBalanceCalculator.calculate(balances),
|
||||
totalArchivedAccounts = accountList.totalArchivedAccounts,
|
||||
sortType = accountList.sortType,
|
||||
groupType = accountList.groupType,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun UserWallet.isPaymentAccountSupported(): Boolean = when (this) {
|
||||
is UserWallet.Cold -> scanResponse.card.firmwareVersion >= FirmwareVersion.HDWalletAvailable
|
||||
is UserWallet.Hot -> hotWalletId.authType != HotWalletId.AuthType.NoPassword
|
||||
}
|
||||
|
||||
private fun ProducerScope<AccountStatusList>.flattenCurrencyStatusFlow(
|
||||
|
|
@ -278,7 +355,7 @@ internal class DefaultSingleAccountStatusListProducer @AssistedInject constructo
|
|||
return map { accountStatus ->
|
||||
when (accountStatus) {
|
||||
is AccountStatus.CryptoPortfolio -> accountStatus.tokenList.totalFiatBalance
|
||||
is AccountStatus.Payment -> accountStatus.totalFiatBalance
|
||||
is AccountStatus.Payment -> accountStatus.value.totalFiatBalance
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,8 +14,8 @@ import com.tangem.domain.models.currency.CryptoCurrency
|
|||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.tokens.error.TokenListSortingError
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import timber.log.Timber
|
||||
|
||||
private typealias SortingErrorByAccountId = MutableMap<AccountId, TokenListSortingError>
|
||||
|
||||
|
|
@ -68,7 +68,7 @@ class ApplyTokenListSortingUseCase(
|
|||
|
||||
maybeSortedAccountList.toEitherNeg()
|
||||
.onLeft { errorByAccountId ->
|
||||
Timber.e(
|
||||
TangemLogger.e(
|
||||
"""
|
||||
Unable to sort tokens for accounts: ${
|
||||
errorByAccountId.entries.joinToString { "${it.key.value}: ${it.value}" }
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import com.tangem.domain.models.wallet.UserWalletId
|
|||
import com.tangem.domain.models.wallet.isMultiCurrency
|
||||
import com.tangem.domain.networks.multi.MultiNetworkStatusProducer
|
||||
import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier
|
||||
import timber.log.Timber
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlin.contracts.ExperimentalContracts
|
||||
import kotlin.contracts.contract
|
||||
|
||||
|
|
@ -130,7 +130,7 @@ class GetAccountCurrencyByAddressUseCase(
|
|||
}
|
||||
|
||||
return value ?: run {
|
||||
Timber.d(message())
|
||||
TangemLogger.d(message())
|
||||
raise(None)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer
|
|||
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
|
||||
import com.tangem.domain.account.status.utils.CryptoCurrencyBalanceFetcher
|
||||
import com.tangem.domain.account.status.utils.CryptoCurrencyMetadataCleaner
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import com.tangem.domain.core.utils.eitherOn
|
||||
import com.tangem.domain.express.ExpressServiceFetcher
|
||||
import com.tangem.domain.express.models.ExpressAsset
|
||||
|
|
@ -22,10 +21,14 @@ import com.tangem.domain.models.wallet.UserWalletId
|
|||
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.domain.wallets.derivations.DerivationsRepository
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.runSuspendCatching
|
||||
import kotlinx.coroutines.*
|
||||
import timber.log.Timber
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
* Use case for saving crypto currencies to a specific account.
|
||||
|
|
@ -75,9 +78,36 @@ class ManageCryptoCurrenciesUseCase(
|
|||
add: List<CryptoCurrency> = emptyList(),
|
||||
remove: List<CryptoCurrency> = emptyList(),
|
||||
skipDerivationErrors: Boolean = true,
|
||||
): Either<Throwable, Unit> = invokeInternal(
|
||||
accountId = accountId,
|
||||
add = add,
|
||||
remove = remove,
|
||||
skipDerivationErrors = skipDerivationErrors,
|
||||
awaitTokensSyncFinished = false,
|
||||
)
|
||||
|
||||
suspend fun invokeAndAwait(
|
||||
accountId: AccountId,
|
||||
add: List<CryptoCurrency> = emptyList(),
|
||||
remove: List<CryptoCurrency> = emptyList(),
|
||||
skipDerivationErrors: Boolean = true,
|
||||
): Either<Throwable, Unit> = invokeInternal(
|
||||
accountId = accountId,
|
||||
add = add,
|
||||
remove = remove,
|
||||
skipDerivationErrors = skipDerivationErrors,
|
||||
awaitTokensSyncFinished = true,
|
||||
)
|
||||
|
||||
private suspend fun invokeInternal(
|
||||
accountId: AccountId,
|
||||
add: List<CryptoCurrency>,
|
||||
remove: List<CryptoCurrency>,
|
||||
skipDerivationErrors: Boolean,
|
||||
awaitTokensSyncFinished: Boolean,
|
||||
): Either<Throwable, Unit> = eitherOn(dispatchers.default) {
|
||||
if (add.isEmpty() && remove.isEmpty()) {
|
||||
Timber.d("No currencies to add or remove, skipping")
|
||||
TangemLogger.d("No currencies to add or remove, skipping")
|
||||
return@eitherOn
|
||||
}
|
||||
|
||||
|
|
@ -89,7 +119,7 @@ class ManageCryptoCurrenciesUseCase(
|
|||
.modify(add = add, remove = remove)
|
||||
|
||||
if (!modifiedCurrencyList.hasChanges) {
|
||||
Timber.d("No changes in currencies, skipping")
|
||||
TangemLogger.d("No changes in currencies, skipping")
|
||||
return@withContext
|
||||
}
|
||||
|
||||
|
|
@ -108,9 +138,11 @@ class ManageCryptoCurrenciesUseCase(
|
|||
account = accountStatus.account.copy(cryptoCurrencies = modifiedCurrencyList.total),
|
||||
)
|
||||
|
||||
parallelUpdatingScope.launch {
|
||||
syncTokens(userWalletId, modifiedCurrencyList)
|
||||
|
||||
syncTokensAndLaunchUpdates(
|
||||
userWalletId = userWalletId,
|
||||
modifiedCurrencyList = modifiedCurrencyList,
|
||||
awaitSync = awaitTokensSyncFinished,
|
||||
) {
|
||||
cryptoCurrencyBalanceFetcher(userWalletId = userWalletId, currencies = modifiedCurrencyList.added)
|
||||
refreshExpress(userWalletId = userWalletId, currencies = modifiedCurrencyList.total)
|
||||
clearMetadata(userWalletId = userWalletId, currencies = modifiedCurrencyList.removed)
|
||||
|
|
@ -126,6 +158,7 @@ class ManageCryptoCurrenciesUseCase(
|
|||
accountId: AccountId,
|
||||
networkId: String,
|
||||
contractAddress: String,
|
||||
awaitTokensSyncFinished: Boolean = false,
|
||||
): Either<Throwable, CryptoCurrency> = eitherOn(dispatchers.default) {
|
||||
val userWalletId = accountId.userWalletId
|
||||
|
||||
|
|
@ -149,9 +182,11 @@ class ManageCryptoCurrenciesUseCase(
|
|||
|
||||
saveAccount(account = accountStatus.account.copy(cryptoCurrencies = modifiedCurrencyList.total))
|
||||
|
||||
parallelUpdatingScope.launch {
|
||||
syncTokens(userWalletId, modifiedCurrencyList)
|
||||
|
||||
syncTokensAndLaunchUpdates(
|
||||
userWalletId = userWalletId,
|
||||
modifiedCurrencyList = modifiedCurrencyList,
|
||||
awaitSync = awaitTokensSyncFinished,
|
||||
) {
|
||||
cryptoCurrencyBalanceFetcher(userWalletId = userWalletId, currencies = listOf(tokenToAdd))
|
||||
refreshExpress(userWalletId = userWalletId, currencies = modifiedCurrencyList.total)
|
||||
}
|
||||
|
|
@ -279,7 +314,26 @@ class ManageCryptoCurrenciesUseCase(
|
|||
createWalletManagers(userWalletId = userWalletId, currencies = modifiedCurrencyList.added)
|
||||
|
||||
runSuspendCatching { accountsCRUDRepository.syncTokens(userWalletId) }
|
||||
.onFailure { Timber.e(it, "Failed to sync tokens for wallet $userWalletId") }
|
||||
.onFailure { TangemLogger.e("Failed to sync tokens for wallet $userWalletId", it) }
|
||||
}
|
||||
|
||||
private suspend fun syncTokensAndLaunchUpdates(
|
||||
userWalletId: UserWalletId,
|
||||
modifiedCurrencyList: ModifiedCurrencyList,
|
||||
awaitSync: Boolean,
|
||||
updates: suspend () -> Unit,
|
||||
) {
|
||||
if (awaitSync) {
|
||||
syncTokens(userWalletId, modifiedCurrencyList)
|
||||
parallelUpdatingScope.launch {
|
||||
updates()
|
||||
}
|
||||
} else {
|
||||
parallelUpdatingScope.launch {
|
||||
syncTokens(userWalletId, modifiedCurrencyList)
|
||||
updates()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -296,7 +350,7 @@ class ManageCryptoCurrenciesUseCase(
|
|||
runSuspendCatching {
|
||||
walletManagersFacade.getOrCreateWalletManager(userWalletId = userWalletId, network = network)
|
||||
}
|
||||
.onFailure { Timber.e(it, "Failed to create wallet manager for network ${network.id}") }
|
||||
.onFailure { TangemLogger.e("Failed to create wallet manager for network ${network.id}", it) }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,11 +5,11 @@ import com.tangem.domain.models.wallet.UserWalletId
|
|||
import com.tangem.domain.tokens.BalanceFetchingOperations
|
||||
import com.tangem.domain.tokens.FetchErrorFormatter
|
||||
import com.tangem.domain.tokens.FetchingSource
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
/**
|
||||
|
|
@ -87,7 +87,7 @@ class CryptoCurrencyBalanceFetcher(
|
|||
)
|
||||
|
||||
if (errors.isNotEmpty()) {
|
||||
Timber.e(FetchErrorFormatter.format(userWalletId, errors))
|
||||
TangemLogger.e(FetchErrorFormatter.format(userWalletId, errors))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import com.tangem.domain.account.status.utils.AccountCryptoCurrencyStatusFinder.
|
|||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.network.Network
|
||||
import timber.log.Timber
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
|
||||
/**
|
||||
* Extension functions for retrieving [CryptoCurrency] from an [AccountList] or [Account.CryptoPortfolio].
|
||||
|
|
@ -57,7 +57,7 @@ object CryptoCurrencyOperations {
|
|||
val currencyId = catch(
|
||||
block = { CryptoCurrency.ID.fromValue(currencyIdValue) },
|
||||
catch = { throwable ->
|
||||
Timber.e("Error on converting currencyId: $throwable")
|
||||
TangemLogger.e("Error on converting currencyId: $throwable")
|
||||
raise(None)
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -23,8 +23,8 @@ dependencies {
|
|||
implementation(projects.domain.tokens.models)
|
||||
implementation(projects.domain.wallets.models)
|
||||
implementation(projects.domain.visa.models)
|
||||
implementation(projects.core.utils)
|
||||
|
||||
implementation(deps.timber)
|
||||
|
||||
implementation(tangemDeps.card.core)
|
||||
implementation(tangemDeps.blockchain) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.domain.card
|
||||
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
|
||||
interface ScanFailsCounter {
|
||||
|
||||
fun reset()
|
||||
|
||||
fun onScanFailure(isUserCancelled: Boolean, source: AnalyticsParam.ScreensSources)
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.tangem.domain.card
|
||||
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
|
||||
interface ScanFailsRequester {
|
||||
|
||||
suspend fun show(source: AnalyticsParam.ScreensSources): Result
|
||||
|
||||
sealed class Result {
|
||||
data object Dismissed : Result()
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@ package com.tangem.domain.card.configs
|
|||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.common.card.EllipticCurve
|
||||
import timber.log.Timber
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
|
||||
object EdSingleCurrencyCardConfig : CardConfig {
|
||||
|
||||
|
|
@ -14,7 +14,7 @@ object EdSingleCurrencyCardConfig : CardConfig {
|
|||
EllipticCurve.Ed25519
|
||||
}
|
||||
else -> {
|
||||
Timber.e("Unsupported blockchain, curve not found")
|
||||
TangemLogger.e("Unsupported blockchain, curve not found")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ package com.tangem.domain.card.configs
|
|||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.common.card.EllipticCurve
|
||||
import timber.log.Timber
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
|
||||
class GenericCardConfig(maxWalletCount: Int) : CardConfig {
|
||||
|
||||
|
|
@ -25,7 +25,7 @@ class GenericCardConfig(maxWalletCount: Int) : CardConfig {
|
|||
EllipticCurve.Ed25519
|
||||
}
|
||||
else -> {
|
||||
Timber.e("Unsupported blockchain, curve not found")
|
||||
TangemLogger.e("Unsupported blockchain, curve not found")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ package com.tangem.domain.card.configs
|
|||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.common.card.EllipticCurve
|
||||
import timber.log.Timber
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
|
||||
object MultiWalletCardConfig : CardConfig {
|
||||
override val mandatoryCurves: List<EllipticCurve>
|
||||
|
|
@ -27,7 +27,7 @@ object MultiWalletCardConfig : CardConfig {
|
|||
EllipticCurve.Bls12381G2Aug
|
||||
}
|
||||
else -> {
|
||||
Timber.e("Unsupported blockchain, curve not found")
|
||||
TangemLogger.e("Unsupported blockchain, curve not found")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ package com.tangem.domain.card.configs
|
|||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.common.card.EllipticCurve
|
||||
import timber.log.Timber
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
|
||||
data object Wallet2CardConfig : CardConfig {
|
||||
override val mandatoryCurves: List<EllipticCurve>
|
||||
|
|
@ -37,14 +37,14 @@ data object Wallet2CardConfig : CardConfig {
|
|||
// EllipticCurve.Ed25519
|
||||
// }
|
||||
// else -> {
|
||||
// Timber.e("Unsupported blockchain, curve not found")
|
||||
// TangemLogger.e("Unsupported blockchain, curve not found")
|
||||
// null
|
||||
// }
|
||||
// }
|
||||
val curve = getPrimaryCurveForBlockchain(blockchain)
|
||||
// check curve supports
|
||||
if (!blockchain.getSupportedCurves().contains(curve)) {
|
||||
Timber.e("Unsupported curve $curve for blockchain $blockchain")
|
||||
TangemLogger.e("Unsupported curve $curve for blockchain $blockchain")
|
||||
return null
|
||||
}
|
||||
return curve
|
||||
|
|
|
|||
77
domain/core/CLAUDE.md
Normal file
77
domain/core/CLAUDE.md
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
# domain/core
|
||||
|
||||
Cross-cutting domain utilities for async data loading, error handling, and reactive streams. Not business logic — foundational abstractions used across all domain modules.
|
||||
|
||||
## LCE (Loading-Content-Error) Pattern
|
||||
|
||||
`Lce<E, C>` — sealed class representing async operation state:
|
||||
- `Loading(partialContent?)` — in progress, may carry partial data
|
||||
- `Content(content)` — success
|
||||
- `Error(error)` — failure with typed error
|
||||
|
||||
Key APIs:
|
||||
- `lce { }` builder — executes block in `LceRaise` context with Arrow's Raise DSL for typed error handling
|
||||
- `lceFlow { }` builder — creates `LceFlow<E, C>` (alias for `Flow<Lce<E, C>>`) via channel-based producer DSL
|
||||
- `LceRaise.bind()` — extracts content from Lce/Either or short-circuits on error
|
||||
- Extensions: `fold()`, `map()`, `mapError()`, `toLce()`, `toEither()`
|
||||
|
||||
## Flow Packaging
|
||||
|
||||
A pattern for complex data streams where work on a single flow is split into three logically separate components: **Producer** (creation), **Supplier** (delivery/caching), and **Fetcher** (refresh). Use it only when you need flexibility in creating, reusing, fetching, and updating a data stream (e.g., network status). Do NOT use for simple cases like reading preferences.
|
||||
|
||||
### FlowProducer
|
||||
|
||||
`FlowProducer<Data>` — creates the data flow. Implement:
|
||||
- `fallback: Data` — emitted when the flow throws an exception
|
||||
- `produce(): Flow<Data>` — the actual flow creation logic
|
||||
|
||||
Built-in `produceWithFallback()` catches errors, emits `fallback`, waits 2s, then retries — keeping the flow alive for subscribers.
|
||||
|
||||
`FlowProducer.Factory<Params, Producer>` — creates a Producer from params. Typically implemented via Hilt `@AssistedFactory`.
|
||||
|
||||
**Implementation pattern:**
|
||||
1. Define interface extending `FlowProducer<Data>` with inner `Params` data class and `Factory` interface
|
||||
2. Create `Default*Producer` with `@AssistedInject` constructor taking `@Assisted params` + dependencies
|
||||
3. Override `fallback` and `produce()`
|
||||
4. Declare inner `@AssistedFactory` interface extending the Producer's Factory
|
||||
|
||||
### FlowSupplier / FlowCachingSupplier
|
||||
|
||||
`FlowSupplier<Params, Data>` — delivers a flow by params via `operator fun invoke(params): Flow<Data>`. Also provides `getSyncOrNull(params, timeout)` for one-shot access.
|
||||
|
||||
`FlowCachingSupplier<Producer, Params, Data>` — abstract implementation that caches flows by key. Implement:
|
||||
- `factory: FlowProducer.Factory` — to create producers
|
||||
- `keyCreator: (Params) -> String` — to generate cache keys
|
||||
|
||||
Behavior: returns cached flow if exists, otherwise creates via `factory.create(params).produceWithFallback()`, caches it, and auto-evicts on terminal exception.
|
||||
|
||||
**Implementation pattern:**
|
||||
1. Define abstract class extending `FlowCachingSupplier` with `factory` and `keyCreator` in constructor
|
||||
2. In DI module, create anonymous subclass providing the factory (injected) and keyCreator lambda
|
||||
|
||||
### FlowFetcher
|
||||
|
||||
`FlowFetcher<Params>` — triggers data refresh, returns `Either<Throwable, Unit>`. Typically updates a store/data source, causing the Producer's flow to re-emit.
|
||||
|
||||
**Implementation pattern:**
|
||||
1. Define interface extending `FlowFetcher<Params>` with inner `Params` data class
|
||||
2. Create `Default*Fetcher` with `@Inject` constructor, override `invoke` wrapping logic in `Either.catch { }`, handle errors with `.onLeft { }`
|
||||
|
||||
### Testing
|
||||
|
||||
- **FlowProducer**: test flow creation logic, params usage, emission behavior, exception handling
|
||||
- **FlowFetcher**: test successful update path and error path (exception thrown)
|
||||
|
||||
## Chain Processing
|
||||
|
||||
- `Chain<E, R>` / `ResultChain<E, R>` — single operation in a chain, works with `Either<E, R>`
|
||||
- `ChainProcessor<E, R>` — folds chains sequentially, stops on first error
|
||||
|
||||
## Error Types
|
||||
|
||||
- `DataError` — sealed domain error hierarchy: `NetworkError.NoInternetConnection`, `UserWalletError.WrongUserWallet`
|
||||
|
||||
## Either Extensions
|
||||
|
||||
- `Either.catchOn(dispatcher, block)` — executes on dispatcher, catches exceptions
|
||||
- `eitherOn(dispatcher, block)` — Raise DSL block on specified dispatcher
|
||||
20
domain/dynamic-addresses/build.gradle.kts
Normal file
20
domain/dynamic-addresses/build.gradle.kts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.tangem.domain.dynamicaddresses"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api(projects.domain.core)
|
||||
api(projects.domain.dynamicAddresses.models)
|
||||
|
||||
implementation(projects.domain.models)
|
||||
|
||||
implementation(tangemDeps.blockchain) {
|
||||
exclude(module = "joda-time")
|
||||
}
|
||||
}
|
||||
7
domain/dynamic-addresses/models/build.gradle.kts
Normal file
7
domain/dynamic-addresses/models/build.gradle.kts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
plugins {
|
||||
alias(deps.plugins.kotlin.jvm)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.tangem.domain.dynamicaddresses.model
|
||||
|
||||
import java.math.BigDecimal
|
||||
|
||||
data class ConsolidationInfo(
|
||||
val fee: BigDecimal,
|
||||
val inputCount: Int,
|
||||
val canCoverFee: Boolean,
|
||||
)
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.tangem.domain.dynamicaddresses.model
|
||||
|
||||
import java.math.BigDecimal
|
||||
|
||||
enum class DynamicAddressesStatus {
|
||||
ENABLED,
|
||||
|
||||
DISABLED,
|
||||
|
||||
/** Enabled on backend, but local XPUB setup required (cross-device sync) */
|
||||
ENABLED_REQUIRES_SETUP,
|
||||
}
|
||||
|
||||
data class UsedAddress(
|
||||
val address: String,
|
||||
val path: String,
|
||||
val balance: BigDecimal,
|
||||
)
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package com.tangem.domain.dynamicaddresses
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.getOrElse
|
||||
import arrow.core.raise.either
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.dynamicaddresses.model.ConsolidationInfo
|
||||
import com.tangem.domain.dynamicaddresses.repository.ConsolidationRepository
|
||||
import com.tangem.domain.dynamicaddresses.repository.DynamicAddressesRepository
|
||||
|
||||
class DisableDynamicAddressesUseCase(
|
||||
private val dynamicAddressesRepository: DynamicAddressesRepository,
|
||||
private val consolidationRepository: ConsolidationRepository,
|
||||
) {
|
||||
|
||||
/**
|
||||
* Returns [ConsolidationInfo] when consolidation is required before disabling,
|
||||
* or null when DA can be disabled immediately (no non-base balances).
|
||||
*/
|
||||
suspend operator fun invoke(userWalletId: UserWalletId, network: Network): Either<Throwable, ConsolidationInfo?> =
|
||||
either {
|
||||
val hasNonBaseBalances = dynamicAddressesRepository.hasNonBaseBalances(userWalletId, network)
|
||||
|
||||
if (!hasNonBaseBalances) {
|
||||
dynamicAddressesRepository.disable(userWalletId, network)
|
||||
return@either null
|
||||
}
|
||||
|
||||
consolidationRepository.getConsolidationInfo(userWalletId, network)
|
||||
.getOrElse { raise(it) }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem.domain.dynamicaddresses
|
||||
|
||||
interface DynamicAddressesFeatureToggles {
|
||||
|
||||
val isDynamicAddressesEnabled: Boolean
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package com.tangem.domain.dynamicaddresses
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.domain.dynamicaddresses.repository.DynamicAddressesRepository
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
||||
class EnableDynamicAddressesUseCase(
|
||||
private val dynamicAddressesRepository: DynamicAddressesRepository,
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(userWalletId: UserWalletId, network: Network, xpub: String): Either<Throwable, Unit> =
|
||||
Either.catch {
|
||||
dynamicAddressesRepository.enable(userWalletId, network, xpub)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package com.tangem.domain.dynamicaddresses
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.domain.dynamicaddresses.model.ConsolidationInfo
|
||||
import com.tangem.domain.dynamicaddresses.repository.ConsolidationRepository
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
||||
class GetConsolidationInfoUseCase(
|
||||
private val consolidationRepository: ConsolidationRepository,
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(userWalletId: UserWalletId, network: Network): Either<Throwable, ConsolidationInfo> {
|
||||
return consolidationRepository.getConsolidationInfo(userWalletId, network)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package com.tangem.domain.dynamicaddresses
|
||||
|
||||
import com.tangem.domain.dynamicaddresses.model.DynamicAddressesStatus
|
||||
import com.tangem.domain.dynamicaddresses.repository.DynamicAddressesRepository
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
class GetDynamicAddressesStatusUseCase(
|
||||
private val dynamicAddressesRepository: DynamicAddressesRepository,
|
||||
) {
|
||||
|
||||
operator fun invoke(userWalletId: UserWalletId, network: Network): Flow<DynamicAddressesStatus> {
|
||||
return dynamicAddressesRepository.getStatus(userWalletId, network)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package com.tangem.domain.dynamicaddresses
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.domain.dynamicaddresses.repository.DynamicAddressesRepository
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
||||
class GetDynamicReceiveAddressUseCase(
|
||||
private val dynamicAddressesRepository: DynamicAddressesRepository,
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(userWalletId: UserWalletId, network: Network): Either<Throwable, String> =
|
||||
Either.catch {
|
||||
dynamicAddressesRepository.getReceiveAddress(userWalletId, network)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package com.tangem.domain.dynamicaddresses.repository
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.blockchain.common.TransactionSigner
|
||||
import com.tangem.domain.dynamicaddresses.model.ConsolidationInfo
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
||||
interface ConsolidationRepository {
|
||||
|
||||
suspend fun getConsolidationInfo(
|
||||
userWalletId: UserWalletId,
|
||||
network: Network,
|
||||
): Either<Throwable, ConsolidationInfo>
|
||||
|
||||
suspend fun sendConsolidationTransaction(
|
||||
userWalletId: UserWalletId,
|
||||
network: Network,
|
||||
signer: TransactionSigner,
|
||||
): Either<Throwable, String>
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package com.tangem.domain.dynamicaddresses.repository
|
||||
|
||||
import com.tangem.domain.dynamicaddresses.model.DynamicAddressesStatus
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface DynamicAddressesRepository {
|
||||
|
||||
fun getStatus(userWalletId: UserWalletId, network: Network): Flow<DynamicAddressesStatus>
|
||||
|
||||
suspend fun enable(userWalletId: UserWalletId, network: Network, xpub: String)
|
||||
|
||||
suspend fun disable(userWalletId: UserWalletId, network: Network)
|
||||
|
||||
suspend fun getReceiveAddress(userWalletId: UserWalletId, network: Network): String
|
||||
|
||||
// for explorer url
|
||||
suspend fun getLastUsedReceiveAddress(userWalletId: UserWalletId, network: Network): String?
|
||||
|
||||
suspend fun hasNonBaseBalances(userWalletId: UserWalletId, network: Network): Boolean
|
||||
}
|
||||
|
|
@ -43,7 +43,6 @@ dependencies {
|
|||
implementation(deps.moshi)
|
||||
implementation(deps.moshi.kotlin)
|
||||
implementation(deps.reKotlin)
|
||||
implementation(deps.timber)
|
||||
ksp(deps.moshi.kotlin.codegen)
|
||||
|
||||
/** Testing libraries */
|
||||
|
|
|
|||
|
|
@ -1,27 +0,0 @@
|
|||
package com.tangem.domain.features.addCustomToken
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.Token
|
||||
import com.tangem.crypto.hdWallet.DerivationPath
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
sealed class CustomCurrency(
|
||||
val network: Blockchain,
|
||||
val derivationPath: DerivationPath?,
|
||||
) {
|
||||
|
||||
@Deprecated("It will be removed in next releases")
|
||||
class CustomBlockchain(
|
||||
network: Blockchain,
|
||||
derivationPath: DerivationPath?,
|
||||
) : CustomCurrency(network, derivationPath)
|
||||
|
||||
@Deprecated("It will be removed in next releases")
|
||||
class CustomToken(
|
||||
val token: Token,
|
||||
network: Blockchain,
|
||||
derivationPath: DerivationPath?,
|
||||
) : CustomCurrency(network, derivationPath)
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
package com.tangem.domain.redux
|
||||
|
||||
import org.rekotlin.Action
|
||||
|
||||
sealed class OnboardingManageTokensAction : Action {
|
||||
data object CurrenciesSaved : OnboardingManageTokensAction()
|
||||
}
|
||||
|
|
@ -10,6 +10,4 @@ interface ReduxStateHolder {
|
|||
suspend fun dispatchWithMain(action: Action)
|
||||
|
||||
suspend fun onUserWalletSelected(userWallet: UserWallet)
|
||||
|
||||
fun dispatchDialogShow(dialog: StateDialog)
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
package com.tangem.domain.redux
|
||||
|
||||
interface StateDialog {
|
||||
|
||||
data object NfcFeatureIsUnavailable : StateDialog
|
||||
|
||||
data class ScanFailsDialog(val source: ScanFailsSource, val onTryAgain: (() -> Unit)? = null) : StateDialog
|
||||
|
||||
enum class ScanFailsSource {
|
||||
MAIN, SIGN_IN, SETTINGS, INTRO
|
||||
}
|
||||
}
|
||||
|
|
@ -49,6 +49,7 @@ data class TokenMarketInfo(
|
|||
|
||||
data class Metrics(
|
||||
val marketRating: Int?,
|
||||
val marketRatingChange24h: Int?,
|
||||
val circulatingSupply: BigDecimal?,
|
||||
val marketCap: BigDecimal?,
|
||||
val volume24h: BigDecimal?,
|
||||
|
|
|
|||
|
|
@ -178,12 +178,15 @@ sealed interface Account {
|
|||
@Serializable
|
||||
data class Payment(
|
||||
override val accountId: AccountId,
|
||||
override val accountName: AccountName,
|
||||
val cryptoCurrencies: List<CryptoCurrency>,
|
||||
) : Account {
|
||||
override val accountName: AccountName.Custom = AccountName.Custom("Payment").getOrElse {
|
||||
error("Can not create account name for Payment account with userWalletId = ${accountId.userWalletId}")
|
||||
}
|
||||
|
||||
init {
|
||||
error("Not yet implemented")
|
||||
companion object {
|
||||
operator fun invoke(userWalletId: UserWalletId): Payment {
|
||||
return Payment(accountId = AccountId.forPaymentAccount(userWalletId = userWalletId))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package com.tangem.domain.models.account
|
||||
|
||||
import com.tangem.domain.core.lce.Lce
|
||||
import com.tangem.domain.models.TotalFiatBalance
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.quote.PriceChange
|
||||
import com.tangem.domain.models.tokenlist.TokenList
|
||||
|
|
@ -43,7 +42,7 @@ sealed interface AccountStatus {
|
|||
@Serializable
|
||||
data class Payment(
|
||||
override val account: Account.Payment,
|
||||
val totalFiatBalance: TotalFiatBalance,
|
||||
val value: PaymentAccountStatusValue,
|
||||
) : AccountStatus
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,193 @@
|
|||
package com.tangem.domain.models.account
|
||||
|
||||
import com.tangem.domain.models.StatusSource
|
||||
import com.tangem.domain.models.TotalFiatBalance
|
||||
import com.tangem.domain.models.kyc.KycStatus
|
||||
import com.tangem.domain.models.serialization.SerializedBigDecimal
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Represents the various states a payment account can have, encapsulating different information based on the state.
|
||||
*
|
||||
* @property source The source of the status information.
|
||||
*/
|
||||
@Serializable
|
||||
sealed class PaymentAccountStatusValue {
|
||||
abstract val source: StatusSource
|
||||
|
||||
/** The total fiat balance associated with this status. */
|
||||
val totalFiatBalance: TotalFiatBalance
|
||||
get() = when (this) {
|
||||
is Error,
|
||||
is IssuingCard,
|
||||
is NotCreated,
|
||||
is UnderReview,
|
||||
-> TotalFiatBalance.Loaded(amount = SerializedBigDecimal.ZERO, source = source)
|
||||
is Loading -> TotalFiatBalance.Loading
|
||||
is Locked -> TotalFiatBalance.Loaded(amount = fiatBalance.availableBalance, source = source)
|
||||
is Loaded -> TotalFiatBalance.Loaded(amount = fiatBalance.availableBalance, source = source)
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies the status with a new [source].
|
||||
*
|
||||
* @param source The new source of the status information.
|
||||
*/
|
||||
fun copySealed(source: StatusSource): PaymentAccountStatusValue {
|
||||
return when (this) {
|
||||
is IssuingCard -> copy(source = source)
|
||||
is Loaded -> copy(source = source)
|
||||
is Locked -> copy(source = source)
|
||||
is UnderReview -> copy(source = source)
|
||||
is Loading,
|
||||
is NotCreated,
|
||||
is Error,
|
||||
-> this
|
||||
}
|
||||
}
|
||||
|
||||
/** Represents the Loading state of a payment account, typically while fetching its details. */
|
||||
@Serializable
|
||||
data object Loading : PaymentAccountStatusValue() {
|
||||
override val source: StatusSource = StatusSource.ACTUAL
|
||||
}
|
||||
|
||||
/** Represents a state where the payment account has not been created yet. */
|
||||
@Serializable
|
||||
data object NotCreated : PaymentAccountStatusValue() {
|
||||
override val source: StatusSource = StatusSource.ACTUAL
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a state where the payment account is under review (KYC).
|
||||
*
|
||||
* @property source The source of the status information.
|
||||
* @property kycStatus The current KYC status.
|
||||
* @property customerId The unique identifier of the customer.
|
||||
*/
|
||||
@Serializable
|
||||
data class UnderReview(
|
||||
override val source: StatusSource,
|
||||
val kycStatus: KycStatus,
|
||||
val customerId: String,
|
||||
) : PaymentAccountStatusValue()
|
||||
|
||||
/**
|
||||
* Represents a state where the card for the payment account is being issued.
|
||||
*
|
||||
* @property source The source of the status information.
|
||||
*/
|
||||
@Serializable
|
||||
data class IssuingCard(override val source: StatusSource) : PaymentAccountStatusValue()
|
||||
|
||||
/**
|
||||
* Represents a state where the payment account is locked.
|
||||
*
|
||||
* @property source The source of the status information.
|
||||
* @property customerId The unique identifier of the customer.
|
||||
* @property cardId The unique identifier of the card.
|
||||
* @property lastFourDigits The last four digits of the card number.
|
||||
* @property currencyCode The code of the currency.
|
||||
* @property depositAddress The address for deposits, if available.
|
||||
* @property isPinSet Indicates if the PIN is set for the card.
|
||||
* @property fiatBalance The fiat balance details.
|
||||
* @property cryptoBalance The crypto balance details.
|
||||
*/
|
||||
@Serializable
|
||||
data class Locked(
|
||||
override val source: StatusSource,
|
||||
val customerId: String,
|
||||
val cardId: String,
|
||||
val lastFourDigits: String,
|
||||
val currencyCode: String,
|
||||
val depositAddress: String?,
|
||||
val isPinSet: Boolean,
|
||||
val fiatBalance: FiatBalance,
|
||||
val cryptoBalance: CryptoBalance,
|
||||
) : PaymentAccountStatusValue()
|
||||
|
||||
/**
|
||||
* Represents a state where the payment account is successfully loaded with complete information.
|
||||
*
|
||||
* @property source The source of the status information.
|
||||
* @property customerId The unique identifier of the customer.
|
||||
* @property cardId The unique identifier of the card.
|
||||
* @property lastFourDigits The last four digits of the card number.
|
||||
* @property currencyCode The code of the currency.
|
||||
* @property depositAddress The address for deposits, if available.
|
||||
* @property isPinSet Indicates if the PIN is set for the card.
|
||||
* @property fiatBalance The fiat balance details.
|
||||
* @property cryptoBalance The crypto balance details.
|
||||
*/
|
||||
@Serializable
|
||||
data class Loaded(
|
||||
override val source: StatusSource,
|
||||
val customerId: String,
|
||||
val cardId: String,
|
||||
val lastFourDigits: String,
|
||||
val currencyCode: String,
|
||||
val depositAddress: String?,
|
||||
val isPinSet: Boolean,
|
||||
val fiatBalance: FiatBalance,
|
||||
val cryptoBalance: CryptoBalance,
|
||||
) : PaymentAccountStatusValue()
|
||||
|
||||
/** Represents an error state for the payment account status. */
|
||||
@Serializable
|
||||
sealed class Error : PaymentAccountStatusValue() {
|
||||
/** Error state indicating the device is exposed. */
|
||||
@Serializable
|
||||
data object ExposedDevice : Error() {
|
||||
override val source: StatusSource = StatusSource.ACTUAL
|
||||
}
|
||||
|
||||
/** Error state indicating the account is unavailable. */
|
||||
@Serializable
|
||||
data object Unavailable : Error() {
|
||||
override val source: StatusSource = StatusSource.ACTUAL
|
||||
}
|
||||
|
||||
/** Error state indicating the account data is not synced. */
|
||||
@Serializable
|
||||
data object NotSynced : Error() {
|
||||
override val source: StatusSource = StatusSource.ACTUAL
|
||||
}
|
||||
|
||||
/**
|
||||
* Error state indicating that card issuance failed.
|
||||
*
|
||||
* @property customerId The unique identifier of the customer.
|
||||
*/
|
||||
@Serializable
|
||||
data class CardIssueFailed(val customerId: String) : Error() {
|
||||
override val source: StatusSource = StatusSource.ACTUAL
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the fiat balance of the payment account.
|
||||
*
|
||||
* @property availableBalance The amount of available balance in fiat.
|
||||
* @property currency The currency of the balance.
|
||||
*/
|
||||
@Serializable
|
||||
data class FiatBalance(val availableBalance: SerializedBigDecimal, val currency: String)
|
||||
|
||||
/**
|
||||
* Represents the crypto balance of the payment account.
|
||||
*
|
||||
* @property id The unique identifier of the crypto asset.
|
||||
* @property chainId The identifier of the blockchain network.
|
||||
* @property depositAddress The address for deposits.
|
||||
* @property tokenContractAddress The contract address of the token.
|
||||
* @property balance The amount of the crypto balance.
|
||||
*/
|
||||
@Serializable
|
||||
data class CryptoBalance(
|
||||
val id: String,
|
||||
val chainId: Long,
|
||||
val depositAddress: String,
|
||||
val tokenContractAddress: String,
|
||||
val balance: SerializedBigDecimal,
|
||||
)
|
||||
}
|
||||
|
|
@ -40,11 +40,13 @@ data class QuoteStatus(val rawCurrencyId: CryptoCurrency.RawID, val value: Value
|
|||
*
|
||||
* @property source status source
|
||||
* @property fiatRate the current fiat exchange rate for the cryptocurrency
|
||||
* @property fiatRateUSD the current fiat exchange rate in USD for the cryptocurrency
|
||||
* @property priceChange the price change for the cryptocurrency
|
||||
*/
|
||||
data class Data(
|
||||
override val source: StatusSource,
|
||||
val fiatRate: BigDecimal,
|
||||
val fiatRateUSD: BigDecimal,
|
||||
val priceChange: BigDecimal,
|
||||
) : Value
|
||||
}
|
||||
|
|
|
|||
1
domain/payment/.gitignore
vendored
Normal file
1
domain/payment/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
10
domain/payment/build.gradle.kts
Normal file
10
domain/payment/build.gradle.kts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
plugins {
|
||||
alias(deps.plugins.kotlin.jvm)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
/** Project - Domain */
|
||||
api(projects.domain.models)
|
||||
implementation(projects.domain.payment.models)
|
||||
}
|
||||
1
domain/payment/models/.gitignore
vendored
Normal file
1
domain/payment/models/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
24
domain/payment/models/build.gradle.kts
Normal file
24
domain/payment/models/build.gradle.kts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
plugins {
|
||||
alias(deps.plugins.kotlin.jvm)
|
||||
alias(deps.plugins.kotlin.serialization)
|
||||
alias(deps.plugins.ksp)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
/** Project - Core */
|
||||
implementation(projects.core.error)
|
||||
|
||||
/** Domain models */
|
||||
implementation(projects.domain.models)
|
||||
|
||||
/** Libs - Tangem */
|
||||
implementation(tangemDeps.card.core)
|
||||
|
||||
/** Libs - Other */
|
||||
implementation(deps.moshi.adapters)
|
||||
implementation(deps.kotlin.serialization)
|
||||
implementation(deps.jodatime)
|
||||
implementation(deps.moshi.kotlin)
|
||||
ksp(deps.moshi.kotlin.codegen)
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package com.tangem.domain.quotes
|
||||
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.quote.QuoteStatus
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* Get currency USD quote use case
|
||||
*/
|
||||
class GetCurrencyUSDQuoteUseCase(
|
||||
private val quotesRepository: QuotesRepository,
|
||||
) {
|
||||
|
||||
/** Get quote by [currencyId] synchronously or null */
|
||||
suspend operator fun invoke(currencyId: CryptoCurrency.RawID): BigDecimal? {
|
||||
val value = quotesRepository.getCurrencyUSDQuote(currencyId)?.value
|
||||
|
||||
return if (value is QuoteStatus.Data) {
|
||||
value.fiatRateUSD
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -12,4 +12,7 @@ interface QuotesRepository {
|
|||
|
||||
/** Get quotes by [currenciesIds] synchronously or null */
|
||||
suspend fun getMultiQuoteSyncOrNull(currenciesIds: Set<CryptoCurrency.RawID>): Set<QuoteStatus>?
|
||||
|
||||
/** Get quote by [currencyId] synchronously or null */
|
||||
suspend fun getCurrencyUSDQuote(currencyId: CryptoCurrency.RawID): QuoteStatus?
|
||||
}
|
||||
1
domain/search/.gitignore
vendored
Normal file
1
domain/search/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
20
domain/search/build.gradle.kts
Normal file
20
domain/search/build.gradle.kts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.tangem.domain.search"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api(projects.domain.core)
|
||||
api(projects.domain.models)
|
||||
implementation(projects.domain.common)
|
||||
implementation(projects.domain.markets.models)
|
||||
implementation(projects.domain.wallets)
|
||||
implementation(projects.domain.appCurrency)
|
||||
implementation(projects.domain.account)
|
||||
implementation(projects.domain.account.status)
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.tangem.domain.search.model
|
||||
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
|
||||
/**
|
||||
* @property timestamp epoch milliseconds
|
||||
*/
|
||||
data class RecentSearchToken(
|
||||
val id: CryptoCurrency.RawID,
|
||||
val name: String,
|
||||
val symbol: String,
|
||||
val imageUrl: String?,
|
||||
val timestamp: Long,
|
||||
)
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.domain.search.model
|
||||
|
||||
import com.tangem.domain.markets.TokenMarket
|
||||
|
||||
data class SearchResult(
|
||||
val textHints: List<SearchTextHint>,
|
||||
val recentTokens: List<RecentSearchToken>,
|
||||
val userAssets: List<UserAssetSearchEntry>,
|
||||
val marketTokens: List<TokenMarket>,
|
||||
)
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem.domain.search.model
|
||||
|
||||
/**
|
||||
* @property timestamp epoch milliseconds
|
||||
*/
|
||||
data class SearchTextHint(val text: String, val timestamp: Long)
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.tangem.domain.search.model
|
||||
|
||||
import com.tangem.domain.models.account.AccountId
|
||||
import com.tangem.domain.models.account.AccountName
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
||||
data class UserAssetSearchEntry(
|
||||
val userWalletId: UserWalletId,
|
||||
val userWalletName: String,
|
||||
val accountId: AccountId,
|
||||
val accountName: AccountName,
|
||||
val currencyStatus: CryptoCurrencyStatus,
|
||||
)
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package com.tangem.domain.search.repository
|
||||
|
||||
import com.tangem.domain.search.model.RecentSearchToken
|
||||
import com.tangem.domain.search.model.SearchTextHint
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
* Repository responsible for managing local search history storage.
|
||||
* Handles persistence of user's past search queries and recently viewed market tokens.
|
||||
* Each history type is limited to 3 entries, sorted by timestamp in descending order.
|
||||
*/
|
||||
interface SearchRepository {
|
||||
|
||||
/** Observes the list of saved text hints, sorted by timestamp descending. */
|
||||
fun getTextHints(): Flow<List<SearchTextHint>>
|
||||
|
||||
/** Observes the list of recently viewed market tokens, sorted by timestamp descending. */
|
||||
fun getRecentTokens(): Flow<List<RecentSearchToken>>
|
||||
|
||||
/**
|
||||
* Saves a text hint to the search history.
|
||||
* If the hint already exists, its timestamp is updated. Oldest entries are evicted when the limit is exceeded.
|
||||
*
|
||||
* @param text the search query text to save
|
||||
*/
|
||||
suspend fun saveTextHint(text: String)
|
||||
|
||||
/**
|
||||
* Saves a recently viewed market token to the search history.
|
||||
* If a token with the same ID already exists, it is moved to the top. Oldest entries are evicted when the limit
|
||||
* is exceeded.
|
||||
*
|
||||
* @param token the market token entry to save
|
||||
*/
|
||||
suspend fun saveRecentToken(token: RecentSearchToken)
|
||||
|
||||
/** Clears all search history, including both text hints and recent tokens. */
|
||||
suspend fun clearHistory()
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package com.tangem.domain.search.usecase
|
||||
|
||||
import com.tangem.domain.search.repository.SearchRepository
|
||||
|
||||
/**
|
||||
* Clears the entire search history, removing both text hints and recently viewed tokens.
|
||||
*
|
||||
* @property searchRepository local search history storage
|
||||
*/
|
||||
class ClearSearchHistoryUseCase(
|
||||
private val searchRepository: SearchRepository,
|
||||
) {
|
||||
|
||||
suspend operator fun invoke() {
|
||||
searchRepository.clearHistory()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
package com.tangem.domain.search.usecase
|
||||
|
||||
import com.tangem.domain.account.models.AccountStatusList
|
||||
import com.tangem.domain.account.status.supplier.MultiAccountStatusListSupplier
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.markets.TokenMarket
|
||||
import com.tangem.domain.models.account.filterCryptoPortfolio
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.models.wallet.isLocked
|
||||
import com.tangem.domain.search.model.SearchResult
|
||||
import com.tangem.domain.search.model.UserAssetSearchEntry
|
||||
import com.tangem.domain.search.repository.SearchRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
|
||||
/**
|
||||
* Primary search use case that produces [SearchResult] based on the current query.
|
||||
*
|
||||
* Behavior depends on the query:
|
||||
* - **Empty query** — returns search history: text hints and recently viewed tokens.
|
||||
* - **Non-empty query** — performs the search across all unlocked user wallets,
|
||||
* matching currencies by name or symbol, and combines the results with externally provided market tokens.
|
||||
*
|
||||
* @property searchRepository local search history storage
|
||||
* @property multiAccountStatusListSupplier supplier for loaded account status lists across all wallets
|
||||
* @property userWalletsListRepository repository providing the list of user wallets
|
||||
*/
|
||||
class GetSearchResultsUseCase(
|
||||
private val searchRepository: SearchRepository,
|
||||
private val multiAccountStatusListSupplier: MultiAccountStatusListSupplier,
|
||||
private val userWalletsListRepository: UserWalletsListRepository,
|
||||
) {
|
||||
|
||||
/**
|
||||
* Produces a [Flow] of [SearchResult] for the given [query].
|
||||
*
|
||||
* @param query the search query string; blank means "show history"
|
||||
* @param marketTokens external flow of market token search results (provided by presentation layer)
|
||||
*/
|
||||
operator fun invoke(
|
||||
query: String,
|
||||
marketTokens: Flow<List<TokenMarket>> = flowOf(emptyList()),
|
||||
): Flow<SearchResult> {
|
||||
return if (query.isBlank()) {
|
||||
observeHistory()
|
||||
} else {
|
||||
searchAssets(query, marketTokens)
|
||||
}
|
||||
}
|
||||
|
||||
private fun observeHistory(): Flow<SearchResult> {
|
||||
return combine(
|
||||
searchRepository.getTextHints(),
|
||||
searchRepository.getRecentTokens(),
|
||||
) { hints, tokens ->
|
||||
SearchResult(
|
||||
textHints = hints,
|
||||
recentTokens = tokens,
|
||||
userAssets = emptyList(),
|
||||
marketTokens = emptyList(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun searchAssets(query: String, marketTokens: Flow<List<TokenMarket>>): Flow<SearchResult> {
|
||||
return combine(
|
||||
observeUserAssets(query),
|
||||
marketTokens,
|
||||
) { userAssets, markets ->
|
||||
SearchResult(
|
||||
textHints = emptyList(),
|
||||
recentTokens = emptyList(),
|
||||
userAssets = userAssets,
|
||||
marketTokens = markets,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun observeUserAssets(query: String): Flow<List<UserAssetSearchEntry>> {
|
||||
val lowerQuery = query.lowercase()
|
||||
return combine(
|
||||
multiAccountStatusListSupplier(),
|
||||
userWalletsListRepository.userWallets,
|
||||
) { statusLists, wallets ->
|
||||
val unlockedWallets = wallets
|
||||
.orEmpty()
|
||||
.filterNot(UserWallet::isLocked)
|
||||
.associateBy { it.walletId }
|
||||
|
||||
if (unlockedWallets.isEmpty()) return@combine emptyList()
|
||||
|
||||
statusLists
|
||||
.filter { it.userWalletId in unlockedWallets }
|
||||
.flatMap { statusList -> extractMatchingAssets(statusList, unlockedWallets, lowerQuery) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun extractMatchingAssets(
|
||||
statusList: AccountStatusList,
|
||||
wallets: Map<UserWalletId, UserWallet>,
|
||||
lowerQuery: String,
|
||||
): List<UserAssetSearchEntry> {
|
||||
val wallet = wallets[statusList.userWalletId] ?: return emptyList()
|
||||
return statusList.accountStatuses
|
||||
.filterCryptoPortfolio()
|
||||
.flatMap { accountStatus ->
|
||||
accountStatus.flattenCurrencies()
|
||||
.filter { currencyStatus ->
|
||||
val name = currencyStatus.currency.name.lowercase()
|
||||
val symbol = currencyStatus.currency.symbol.lowercase()
|
||||
name.contains(lowerQuery) || symbol.contains(lowerQuery)
|
||||
}
|
||||
.map { currencyStatus ->
|
||||
UserAssetSearchEntry(
|
||||
userWalletId = statusList.userWalletId,
|
||||
userWalletName = wallet.name,
|
||||
accountId = accountStatus.accountId,
|
||||
accountName = accountStatus.account.accountName,
|
||||
currencyStatus = currencyStatus,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package com.tangem.domain.search.usecase
|
||||
|
||||
import com.tangem.domain.search.model.RecentSearchToken
|
||||
import com.tangem.domain.search.repository.SearchRepository
|
||||
|
||||
/**
|
||||
* Saves a market token to the "recently viewed" search history.
|
||||
* Only market assets should be saved (user's own assets are ignored).
|
||||
* The history is limited to 3 entries; oldest entries are evicted automatically.
|
||||
*
|
||||
* @property searchRepository local search history storage
|
||||
*/
|
||||
class SaveRecentSearchTokenUseCase(
|
||||
private val searchRepository: SearchRepository,
|
||||
) {
|
||||
|
||||
/**
|
||||
* @param token the market token to persist as a recent search entry
|
||||
*/
|
||||
suspend operator fun invoke(token: RecentSearchToken) {
|
||||
searchRepository.saveRecentToken(token)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package com.tangem.domain.search.usecase
|
||||
|
||||
import com.tangem.domain.search.repository.SearchRepository
|
||||
|
||||
/**
|
||||
* Saves the current search query text to the local search history.
|
||||
* Blank queries are ignored. The history is limited to 3 entries; oldest entries are evicted automatically.
|
||||
* Should be called when the user selects any asset from the search results.
|
||||
*
|
||||
* @property searchRepository local search history storage
|
||||
*/
|
||||
class SaveSearchQueryUseCase(
|
||||
private val searchRepository: SearchRepository,
|
||||
) {
|
||||
|
||||
/**
|
||||
* @param query the search text to persist; blank values are silently ignored
|
||||
*/
|
||||
suspend operator fun invoke(query: String) {
|
||||
if (query.isBlank()) return
|
||||
searchRepository.saveTextHint(query.trim())
|
||||
}
|
||||
}
|
||||
|
|
@ -22,7 +22,6 @@ dependencies {
|
|||
implementation(deps.kotlin.datetime)
|
||||
implementation(deps.kotlin.serialization)
|
||||
implementation(deps.jodatime)
|
||||
implementation(deps.timber)
|
||||
|
||||
implementation(projects.domain.legacy)
|
||||
implementation(projects.domain.walletManager) // TODO refactor to use from data module
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import com.tangem.domain.models.staking.StakingBalance
|
|||
import com.tangem.domain.models.staking.StakingID
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.utils.extensions.indexOfFirstOrNull
|
||||
import timber.log.Timber
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
|
||||
/**
|
||||
* Producer of staking balance for selected wallet [UserWalletId]
|
||||
|
|
@ -51,9 +51,8 @@ interface SingleStakingBalanceProducer : FlowProducer<StakingBalance> {
|
|||
),
|
||||
)
|
||||
|
||||
Timber.e(
|
||||
"Multiple balances found for staking ID $currentStakingId:\n%s",
|
||||
currentBalances.joinToString("\n"),
|
||||
TangemLogger.e(
|
||||
"Multiple balances found for staking ID $currentStakingId:\n${currentBalances.joinToString("\n")}",
|
||||
)
|
||||
|
||||
val dataIndex = currentBalances.indexOfFirstOrNull { it is StakingBalance.Data }
|
||||
|
|
@ -66,7 +65,7 @@ interface SingleStakingBalanceProducer : FlowProducer<StakingBalance> {
|
|||
} else {
|
||||
val balance = currentBalances.firstOrNull() ?: return null
|
||||
|
||||
Timber.i("Staking balance found for $currentStakingId:\n$balance")
|
||||
TangemLogger.i("Staking balance found for $currentStakingId:\n$balance")
|
||||
balance
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.domain.swap.models
|
||||
|
||||
import com.tangem.domain.express.models.ExpressProvider
|
||||
import com.tangem.domain.express.models.ExpressRateType
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
|
||||
/**
|
||||
|
|
@ -50,4 +51,16 @@ data class SwapCurrenciesGroup(
|
|||
data class SwapCryptoCurrency(
|
||||
val currencyStatus: CryptoCurrencyStatus,
|
||||
val providers: List<ExpressProvider>,
|
||||
)
|
||||
)
|
||||
|
||||
/**
|
||||
* Get initial rate type based on available providers
|
||||
*/
|
||||
fun List<ExpressProvider>.getInitialRateType(): ExpressRateType {
|
||||
val availableRateTypes = this.flatMap { it.rateTypes }.toSet()
|
||||
return if (availableRateTypes.contains(ExpressRateType.Fixed)) {
|
||||
ExpressRateType.Fixed
|
||||
} else {
|
||||
ExpressRateType.Float
|
||||
}
|
||||
}
|
||||
|
|
@ -8,7 +8,7 @@ import java.math.BigDecimal
|
|||
*
|
||||
* @property provider swap provider
|
||||
* @property toTokenAmount amount of token you want to receive
|
||||
* @property fromTokenAmount amount of from-token required (for fixed rate quotes)
|
||||
* @property fromTokenAmount amount of from-token required (only set for fixed rate quotes)
|
||||
* @property allowanceContract whether swap occurs via third token
|
||||
*/
|
||||
data class SwapQuoteModel(
|
||||
|
|
@ -16,4 +16,5 @@ data class SwapQuoteModel(
|
|||
val toTokenAmount: BigDecimal,
|
||||
val fromTokenAmount: BigDecimal?,
|
||||
val allowanceContract: String?,
|
||||
val quoteId: String? = null,
|
||||
)
|
||||
|
|
@ -91,6 +91,7 @@ interface SwapRepositoryV2 {
|
|||
expressProvider: ExpressProvider,
|
||||
rateType: ExpressRateType,
|
||||
expressOperationType: ExpressOperationType,
|
||||
quoteId: String?,
|
||||
): SwapDataModel
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ class GetSwapDataUseCase(
|
|||
expressProvider: ExpressProvider,
|
||||
rateType: ExpressRateType,
|
||||
expressOperationType: ExpressOperationType,
|
||||
quoteId: String? = null,
|
||||
): Either<ExpressError, SwapDataModel> = Either.catch {
|
||||
swapRepositoryV2.getSwapData(
|
||||
userWallet = userWallet,
|
||||
|
|
@ -42,6 +43,7 @@ class GetSwapDataUseCase(
|
|||
expressProvider = expressProvider,
|
||||
rateType = rateType,
|
||||
expressOperationType = expressOperationType,
|
||||
quoteId = quoteId,
|
||||
)
|
||||
}.mapLeft { throwable ->
|
||||
swapErrorResolver.resolve(throwable)
|
||||
|
|
|
|||
|
|
@ -53,7 +53,6 @@ dependencies {
|
|||
|
||||
/** Android - Other */
|
||||
implementation(deps.androidx.paging.runtime)
|
||||
implementation(deps.timber)
|
||||
|
||||
/** Utils */
|
||||
implementation(deps.jodatime)
|
||||
|
|
|
|||
|
|
@ -1,14 +0,0 @@
|
|||
package com.tangem.domain.tokens.model.tokensync
|
||||
|
||||
import java.math.BigDecimal
|
||||
|
||||
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,
|
||||
)
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
package com.tangem.domain.tokens.model.tokensync
|
||||
|
||||
sealed class TokenSyncProgress {
|
||||
|
||||
data object Idle : TokenSyncProgress()
|
||||
|
||||
data class InProgress(
|
||||
val completedNetworks: Int,
|
||||
val totalNetworks: Int,
|
||||
) : TokenSyncProgress() {
|
||||
val progressPercent: Int
|
||||
get() = if (totalNetworks > 0) {
|
||||
completedNetworks * 100 / totalNetworks
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
data object Completed : TokenSyncProgress()
|
||||
|
||||
data class Error(val cause: Throwable) : TokenSyncProgress()
|
||||
}
|
||||
|
|
@ -11,4 +11,5 @@ data class CryptoCurrencyCheck(
|
|||
val utxoAmountLimit: UtxoAmountLimit?,
|
||||
val isAccountFunded: Boolean,
|
||||
val rentWarning: CryptoCurrencyWarning.Rent?,
|
||||
val isMemoRequired: Boolean = false,
|
||||
)
|
||||
|
|
@ -8,10 +8,10 @@ import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher
|
|||
import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher
|
||||
import com.tangem.domain.staking.StakingIdFactory
|
||||
import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Shared utility for fetching cryptocurrency balance data from multiple sources.
|
||||
|
|
@ -116,14 +116,14 @@ class BalanceFetchingOperations(
|
|||
val stakingId = stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = currency)
|
||||
|
||||
if (stakingId.isLeft { it is StakingIdFactory.Error.UnableToGetAddress }) {
|
||||
Timber.e("Unable to get staking ID for user wallet $userWalletId and currency ${currency.id}")
|
||||
TangemLogger.e("Unable to get staking ID for user wallet $userWalletId and currency ${currency.id}")
|
||||
}
|
||||
|
||||
stakingId.getOrNull()
|
||||
}
|
||||
|
||||
if (stakingIds.isEmpty()) {
|
||||
Timber.i("No staking IDs found for user wallet $userWalletId")
|
||||
TangemLogger.i("No staking IDs found for user wallet $userWalletId")
|
||||
return Unit.right()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -45,6 +45,14 @@ class GetCurrencyCheckUseCase(
|
|||
} else {
|
||||
false
|
||||
}
|
||||
val isMemoRequired = if (recipientAddress != null) {
|
||||
currencyChecksRepository.checkIfMemoRequired(
|
||||
network = network,
|
||||
address = recipientAddress,
|
||||
)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
val utxoAmountLimit = if (currency is CryptoCurrency.Coin && amount != null && fee != null) {
|
||||
currencyChecksRepository.checkUtxoAmountLimit(
|
||||
userWalletId = userWalletId,
|
||||
|
|
@ -65,6 +73,7 @@ class GetCurrencyCheckUseCase(
|
|||
utxoAmountLimit = utxoAmountLimit,
|
||||
isAccountFunded = isAccountFunded,
|
||||
rentWarning = rentWarning,
|
||||
isMemoRequired = isMemoRequired,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,4 @@ package com.tangem.domain.tokens
|
|||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
interface TokensFeatureToggles {
|
||||
val isMultiAddressUtxoEnabled: Boolean
|
||||
}
|
||||
interface TokensFeatureToggles
|
||||
|
|
@ -35,6 +35,9 @@ interface CurrencyChecksRepository {
|
|||
/** Returns true if account with [address] was reserved with minimum amount */
|
||||
suspend fun checkIfAccountFunded(userWalletId: UserWalletId, network: Network, address: String): Boolean
|
||||
|
||||
/** Returns true if a memo/destination tag is required for the given [address] on [network] */
|
||||
suspend fun checkIfMemoRequired(network: Network, address: String): Boolean
|
||||
|
||||
/** Checks if transaction amount is within the UTXO limit */
|
||||
suspend fun checkUtxoAmountLimit(
|
||||
userWalletId: UserWalletId,
|
||||
|
|
|
|||
|
|
@ -1,23 +0,0 @@
|
|||
package com.tangem.domain.tokens.repository
|
||||
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.tokens.model.tokensync.TokenSyncProgress
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface TokenSyncRepository {
|
||||
|
||||
suspend fun runSync(userWalletId: UserWalletId)
|
||||
|
||||
suspend fun getPendingSyncWalletIds(): List<UserWalletId>
|
||||
|
||||
fun observeSyncProgress(userWalletId: UserWalletId): Flow<TokenSyncProgress>
|
||||
|
||||
fun acknowledgeCompletion(userWalletId: UserWalletId)
|
||||
|
||||
suspend fun clearPendingFlag(userWalletId: UserWalletId)
|
||||
|
||||
suspend fun getDiscoveredCurrencies(userWalletId: UserWalletId): List<CryptoCurrency>
|
||||
|
||||
suspend fun clearDiscoveredTokens(userWalletId: UserWalletId)
|
||||
}
|
||||
|
|
@ -26,10 +26,10 @@ import com.tangem.domain.tokens.wallet.implementor.MultiWalletBalanceFetcher
|
|||
import com.tangem.domain.tokens.wallet.implementor.SingleWalletBalanceFetcher
|
||||
import com.tangem.domain.tokens.wallet.implementor.SingleWalletWithTokenBalanceFetcher
|
||||
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 timber.log.Timber
|
||||
|
||||
/**
|
||||
* Fetcher of wallet balance by [UserWalletId]
|
||||
|
|
@ -184,7 +184,7 @@ class WalletBalanceFetcher internal constructor(
|
|||
|
||||
check(errors.isEmpty()) {
|
||||
val message = FetchErrorFormatter.formatWalletErrors(userWalletId, errors)
|
||||
Timber.e(message)
|
||||
TangemLogger.e(message)
|
||||
message
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,14 +2,14 @@ package com.tangem.domain.tokens.wallet.implementor
|
|||
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.tokens.FetchingSource
|
||||
import com.tangem.domain.tokens.MultiWalletAccountListFetcher
|
||||
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer
|
||||
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier
|
||||
import com.tangem.domain.tokens.FetchingSource
|
||||
import com.tangem.domain.tokens.wallet.BaseWalletBalanceFetcher
|
||||
import com.tangem.domain.tokens.wallet.WalletFetchingSource
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Implementation of [BaseWalletBalanceFetcher] for MULTI-CURRENCY wallet
|
||||
|
|
@ -37,7 +37,7 @@ internal class MultiWalletBalanceFetcher(
|
|||
multiWalletAccountListFetcher(
|
||||
params = MultiWalletAccountListFetcher.Params(userWalletId = userWalletId),
|
||||
)
|
||||
.onLeft(Timber::e)
|
||||
.onLeft { TangemLogger.e("Error", it) }
|
||||
|
||||
return multiWalletCryptoCurrenciesSupplier(
|
||||
params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId = userWalletId),
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ internal object MockQuotes {
|
|||
rawCurrencyId = MockTokens.token1.id.rawCurrencyId!!,
|
||||
value = QuoteStatus.Data(
|
||||
fiatRate = BigDecimal("1.23"),
|
||||
fiatRateUSD = BigDecimal("1.23"),
|
||||
priceChange = BigDecimal("0.01"),
|
||||
source = StatusSource.ACTUAL,
|
||||
),
|
||||
|
|
@ -22,6 +23,7 @@ internal object MockQuotes {
|
|||
rawCurrencyId = MockTokens.token2.id.rawCurrencyId!!,
|
||||
value = QuoteStatus.Data(
|
||||
fiatRate = BigDecimal("2.34"),
|
||||
fiatRateUSD = BigDecimal("2.34"),
|
||||
priceChange = BigDecimal("-0.02"),
|
||||
source = StatusSource.ACTUAL,
|
||||
),
|
||||
|
|
@ -31,6 +33,7 @@ internal object MockQuotes {
|
|||
rawCurrencyId = MockTokens.token3.id.rawCurrencyId!!,
|
||||
value = QuoteStatus.Data(
|
||||
fiatRate = BigDecimal("3.45"),
|
||||
fiatRateUSD = BigDecimal("3.45"),
|
||||
priceChange = BigDecimal("0.03"),
|
||||
source = StatusSource.ACTUAL,
|
||||
),
|
||||
|
|
@ -40,6 +43,7 @@ internal object MockQuotes {
|
|||
rawCurrencyId = MockTokens.token4.id.rawCurrencyId!!,
|
||||
value = QuoteStatus.Data(
|
||||
fiatRate = BigDecimal("4.56"),
|
||||
fiatRateUSD = BigDecimal("4.56"),
|
||||
priceChange = BigDecimal("-0.04"),
|
||||
source = StatusSource.ACTUAL,
|
||||
),
|
||||
|
|
@ -49,6 +53,7 @@ internal object MockQuotes {
|
|||
rawCurrencyId = MockTokens.token5.id.rawCurrencyId!!,
|
||||
value = QuoteStatus.Data(
|
||||
fiatRate = BigDecimal("5.67"),
|
||||
fiatRateUSD = BigDecimal("5.67"),
|
||||
priceChange = BigDecimal("0.05"),
|
||||
source = StatusSource.ACTUAL,
|
||||
),
|
||||
|
|
@ -58,6 +63,7 @@ internal object MockQuotes {
|
|||
rawCurrencyId = MockTokens.token6.id.rawCurrencyId!!,
|
||||
value = QuoteStatus.Data(
|
||||
fiatRate = BigDecimal("6.78"),
|
||||
fiatRateUSD = BigDecimal("6.78"),
|
||||
priceChange = BigDecimal("-0.06"),
|
||||
source = StatusSource.ACTUAL,
|
||||
),
|
||||
|
|
@ -67,6 +73,7 @@ internal object MockQuotes {
|
|||
rawCurrencyId = MockTokens.token7.id.rawCurrencyId!!,
|
||||
value = QuoteStatus.Data(
|
||||
fiatRate = BigDecimal("7.89"),
|
||||
fiatRateUSD = BigDecimal("7.89"),
|
||||
priceChange = BigDecimal("0.07"),
|
||||
source = StatusSource.ACTUAL,
|
||||
),
|
||||
|
|
@ -76,6 +83,7 @@ internal object MockQuotes {
|
|||
rawCurrencyId = MockTokens.token8.id.rawCurrencyId!!,
|
||||
value = QuoteStatus.Data(
|
||||
fiatRate = BigDecimal("8.90"),
|
||||
fiatRateUSD = BigDecimal("8.90"),
|
||||
priceChange = BigDecimal("-0.08"),
|
||||
source = StatusSource.ACTUAL,
|
||||
),
|
||||
|
|
@ -85,6 +93,7 @@ internal object MockQuotes {
|
|||
rawCurrencyId = MockTokens.token9.id.rawCurrencyId!!,
|
||||
value = QuoteStatus.Data(
|
||||
fiatRate = BigDecimal("9.01"),
|
||||
fiatRateUSD = BigDecimal("9.01"),
|
||||
priceChange = BigDecimal("0.09"),
|
||||
source = StatusSource.ACTUAL,
|
||||
),
|
||||
|
|
@ -94,6 +103,7 @@ internal object MockQuotes {
|
|||
rawCurrencyId = MockTokens.token10.id.rawCurrencyId!!,
|
||||
value = QuoteStatus.Data(
|
||||
fiatRate = BigDecimal("10.12"),
|
||||
fiatRateUSD = BigDecimal("10.12"),
|
||||
priceChange = BigDecimal("-0.10"),
|
||||
source = StatusSource.ACTUAL,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ class CryptoCurrencyStatusFactoryTest {
|
|||
|
||||
private val fullQuote = QuoteStatus.Data(
|
||||
fiatRate = 1800.0.toBigDecimal(),
|
||||
fiatRateUSD = 1800.0.toBigDecimal(),
|
||||
priceChange = (-2.5).toBigDecimal(),
|
||||
source = StatusSource.ACTUAL,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -16,5 +16,4 @@ dependencies {
|
|||
|
||||
implementation(deps.kotlin.coroutines)
|
||||
implementation(deps.arrow.core)
|
||||
implementation(deps.timber)
|
||||
}
|
||||
|
|
@ -17,6 +17,4 @@ sealed class TokenSyncProgress {
|
|||
}
|
||||
|
||||
data object Completed : TokenSyncProgress()
|
||||
|
||||
data class Error(val cause: Throwable) : TokenSyncProgress()
|
||||
}
|
||||
|
|
@ -9,6 +9,8 @@ interface TokenSyncRepository {
|
|||
|
||||
suspend fun runSync(userWalletId: UserWalletId)
|
||||
|
||||
suspend fun completeSync(userWalletId: UserWalletId)
|
||||
|
||||
suspend fun getPendingSyncWalletIds(): List<UserWalletId>
|
||||
|
||||
fun observeSyncProgress(userWalletId: UserWalletId): Flow<TokenSyncProgress>
|
||||
|
|
|
|||
|
|
@ -6,12 +6,12 @@ import com.tangem.domain.models.account.AccountId
|
|||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.tokensync.repository.TokenSyncRepository
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
class SyncTokensUseCase(
|
||||
class StartTokenSyncUseCase(
|
||||
private val tokenSyncRepository: TokenSyncRepository,
|
||||
private val manageCryptoCurrenciesUseCase: ManageCryptoCurrenciesUseCase,
|
||||
private val appCoroutineScope: AppCoroutineScope,
|
||||
|
|
@ -25,8 +25,9 @@ class SyncTokensUseCase(
|
|||
try {
|
||||
tokenSyncRepository.runSync(userWalletId)
|
||||
applyDiscoveredTokens(userWalletId)
|
||||
tokenSyncRepository.completeSync(userWalletId)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Token sync failed for wallet: $userWalletId")
|
||||
TangemLogger.e("Token sync failed for wallet: $userWalletId", e)
|
||||
} finally {
|
||||
activeSyncJobs.remove(userWalletId)
|
||||
}
|
||||
|
|
@ -50,7 +51,7 @@ class SyncTokensUseCase(
|
|||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Failed to apply pending syncs")
|
||||
TangemLogger.e("Failed to apply pending syncs", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -61,7 +62,7 @@ class SyncTokensUseCase(
|
|||
if (currencies.isEmpty()) return true
|
||||
|
||||
val accountId = AccountId.forMainCryptoPortfolio(userWalletId)
|
||||
return manageCryptoCurrenciesUseCase(
|
||||
return manageCryptoCurrenciesUseCase.invokeAndAwait(
|
||||
accountId = accountId,
|
||||
add = currencies,
|
||||
).fold(
|
||||
|
|
@ -70,7 +71,7 @@ class SyncTokensUseCase(
|
|||
true
|
||||
},
|
||||
ifLeft = { error ->
|
||||
Timber.e("Failed to apply discovered tokens for wallet: $userWalletId, error: $error")
|
||||
TangemLogger.e("Failed to apply discovered tokens for wallet: $userWalletId, error: $error")
|
||||
false
|
||||
},
|
||||
)
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
package com.tangem.domain.transaction
|
||||
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.transaction.models.AllowanceInfo
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* Repository interface for managing token allowances in the context of blockchain transactions.
|
||||
*/
|
||||
interface AllowanceRepository {
|
||||
|
||||
/**
|
||||
* Retrieves the allowance information for a specific spender and required amount.
|
||||
*
|
||||
* @param userWalletId The ID of the user's wallet.
|
||||
* @param cryptoCurrency The cryptocurrency for which the allowance is being checked (must be a token).
|
||||
* @param spenderAddress The address of the spender for whom the allowance is being checked.
|
||||
* @param requiredAmount The amount that is required for the transaction.
|
||||
*
|
||||
* @return An [AllowanceInfo] object that indicates whether the current allowance.
|
||||
* @throws IllegalStateException if the provided [cryptoCurrency] is not a token.
|
||||
*/
|
||||
suspend fun getAllowanceInfo(
|
||||
userWalletId: UserWalletId,
|
||||
cryptoCurrency: CryptoCurrency,
|
||||
spenderAddress: String,
|
||||
requiredAmount: BigDecimal,
|
||||
): AllowanceInfo
|
||||
|
||||
/**
|
||||
* Retrieves the current allowance for a specific spender.
|
||||
*
|
||||
* @param userWalletId The ID of the user's wallet.
|
||||
* @param cryptoCurrency The cryptocurrency for which the allowance is being checked (must be a token).
|
||||
* @param spenderAddress The address of the spender for whom the allowance is being checked.
|
||||
*
|
||||
* @return The current allowance as a [BigDecimal].
|
||||
* @throws IllegalStateException if the provided [cryptoCurrency] is not a token.
|
||||
*/
|
||||
suspend fun getAllowance(
|
||||
userWalletId: UserWalletId,
|
||||
cryptoCurrency: CryptoCurrency,
|
||||
spenderAddress: String,
|
||||
): BigDecimal
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package com.tangem.domain.transaction
|
||||
|
||||
import com.tangem.domain.models.network.Network
|
||||
|
||||
/**
|
||||
* Facade for memo validation operations.
|
||||
*/
|
||||
interface MemoValidatorFacade {
|
||||
|
||||
/**
|
||||
* Returns true if a memo/destination tag is required for the given [destinationAddress] on [network].
|
||||
* Returns false on network errors or for unsupported blockchains.
|
||||
*/
|
||||
suspend fun isMemoRequired(network: Network, destinationAddress: String): Boolean
|
||||
|
||||
/**
|
||||
* Returns true if [memo] is valid for the given [network], or if memo is not supported.
|
||||
* Returns true on errors (lenient fallback).
|
||||
*/
|
||||
suspend fun validateMemo(network: Network, memo: String): Boolean
|
||||
}
|
||||
|
|
@ -6,11 +6,9 @@ import com.tangem.blockchain.common.transaction.Fee
|
|||
import com.tangem.blockchain.common.transaction.TransactionSendResult
|
||||
import com.tangem.blockchain.common.transaction.TransactionsSendResult
|
||||
import com.tangem.blockchain.nft.models.NFTAsset
|
||||
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.EventTransactionTypeDto
|
||||
import java.math.BigDecimal
|
||||
import java.math.BigInteger
|
||||
|
||||
interface TransactionRepository {
|
||||
|
|
@ -91,12 +89,6 @@ interface TransactionRepository {
|
|||
gasLimit: BigInteger?,
|
||||
): TransactionExtras
|
||||
|
||||
suspend fun getAllowance(
|
||||
userWalletId: UserWalletId,
|
||||
cryptoCurrency: CryptoCurrency.Token,
|
||||
spenderAddress: String,
|
||||
): BigDecimal
|
||||
|
||||
suspend fun prepareForSend(
|
||||
transactionData: TransactionData,
|
||||
signer: TransactionSigner,
|
||||
|
|
|
|||
|
|
@ -23,7 +23,9 @@ interface WalletAddressServiceRepository {
|
|||
|
||||
suspend fun validateAddress(userWalletId: UserWalletId, network: Network, address: String): Boolean
|
||||
|
||||
suspend fun validateMemo(userWalletId: UserWalletId, network: Network, memo: String): Boolean
|
||||
suspend fun validateMemo(network: Network, memo: String): Boolean
|
||||
|
||||
suspend fun isMemoRequired(network: Network, destinationAddress: String): Boolean
|
||||
|
||||
suspend fun parseSharedAddress(input: String, network: Network): ParsedQrCode
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package com.tangem.domain.transaction.models
|
||||
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* Model that represents the allowance information for a specific spender and required amount.
|
||||
*/
|
||||
sealed class AllowanceInfo {
|
||||
|
||||
/**
|
||||
* Represents a state where the current allowance is sufficient to cover the required amount.
|
||||
*/
|
||||
data class Enough(val allowance: BigDecimal) : AllowanceInfo()
|
||||
|
||||
/**
|
||||
* Represents a state where the current allowance is insufficient to cover the required amount.
|
||||
*/
|
||||
data class NotEnough(val allowance: BigDecimal, val requiredAmount: BigDecimal) : AllowanceInfo()
|
||||
|
||||
/**
|
||||
* Represents a state where the current allowance is insufficient,
|
||||
* but it must be reset to cover the required amount (specific to certain tokens like Tether in Ethereum).
|
||||
*/
|
||||
data class ResetNeeded(val allowance: BigDecimal, val requiredAmount: BigDecimal) : AllowanceInfo()
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package com.tangem.domain.transaction.usecase
|
||||
|
||||
import arrow.core.Either
|
||||
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 java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* Use case for retrieving the allowance information for a specific spender and required amount.
|
||||
*/
|
||||
class GetAllowanceInfoUseCase(
|
||||
private val allowanceRepository: AllowanceRepository,
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(
|
||||
userWalletId: UserWalletId,
|
||||
cryptoCurrency: CryptoCurrency,
|
||||
spenderAddress: String,
|
||||
requiredAmount: BigDecimal,
|
||||
): Either<Throwable, AllowanceInfo> {
|
||||
return Either.catch {
|
||||
allowanceRepository.getAllowanceInfo(
|
||||
userWalletId = userWalletId,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
spenderAddress = spenderAddress,
|
||||
requiredAmount = requiredAmount,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,12 +2,15 @@ package com.tangem.domain.transaction.usecase
|
|||
|
||||
import arrow.core.Either
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.transaction.TransactionRepository
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.transaction.AllowanceRepository
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* Use case for retrieving the current allowance for a specific spender.
|
||||
*/
|
||||
class GetAllowanceUseCase(
|
||||
private val transactionRepository: TransactionRepository,
|
||||
private val allowanceRepository: AllowanceRepository,
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(
|
||||
|
|
@ -16,9 +19,9 @@ class GetAllowanceUseCase(
|
|||
spenderAddress: String,
|
||||
): Either<Throwable, BigDecimal> {
|
||||
return Either.catch {
|
||||
transactionRepository.getAllowance(
|
||||
allowanceRepository.getAllowance(
|
||||
userWalletId = userWalletId,
|
||||
cryptoCurrency = cryptoCurrency as CryptoCurrency.Token,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
spenderAddress = spenderAddress,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ class ValidateWalletMemoUseCase(
|
|||
): Either<ValidateMemoError, Unit> {
|
||||
return try {
|
||||
val isValidMemo = walletAddressServiceRepository.validateMemo(
|
||||
userWalletId = userWalletId,
|
||||
network = cryptoCurrency.network,
|
||||
memo = memo,
|
||||
)
|
||||
|
|
|
|||
1
domain/virtual-account/.gitignore
vendored
Normal file
1
domain/virtual-account/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
13
domain/virtual-account/build.gradle.kts
Normal file
13
domain/virtual-account/build.gradle.kts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
alias(deps.plugins.ksp)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.tangem.domain.virtualaccount"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
}
|
||||
1
domain/virtual-account/models/.gitignore
vendored
Normal file
1
domain/virtual-account/models/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
13
domain/virtual-account/models/build.gradle.kts
Normal file
13
domain/virtual-account/models/build.gradle.kts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
alias(deps.plugins.ksp)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.tangem.domain.virtualaccount.models"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
}
|
||||
|
|
@ -29,7 +29,6 @@ dependencies {
|
|||
implementation(deps.spongecastle.core)
|
||||
|
||||
/** Libs - Other */
|
||||
implementation(deps.timber)
|
||||
implementation(deps.jodatime)
|
||||
implementation(deps.androidx.paging.runtime)
|
||||
implementation(deps.moshi)
|
||||
|
|
|
|||
|
|
@ -1,66 +0,0 @@
|
|||
package com.tangem.domain.pay
|
||||
|
||||
import com.tangem.domain.models.StatusSource
|
||||
import com.tangem.domain.models.kyc.KycStatus
|
||||
import com.tangem.domain.models.serialization.SerializedBigDecimal
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
sealed class PaymentAccountStatus {
|
||||
|
||||
abstract val source: StatusSource
|
||||
|
||||
@Serializable
|
||||
data object Loading : PaymentAccountStatus() {
|
||||
override val source: StatusSource = StatusSource.ACTUAL
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data object NotCreated : PaymentAccountStatus() {
|
||||
override val source: StatusSource = StatusSource.ACTUAL
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class UnderReview(
|
||||
override val source: StatusSource,
|
||||
val kycStatus: KycStatus,
|
||||
) : PaymentAccountStatus()
|
||||
|
||||
@Serializable
|
||||
data class IssuingCard(override val source: StatusSource) : PaymentAccountStatus()
|
||||
|
||||
@Serializable
|
||||
data class Locked(override val source: StatusSource) : PaymentAccountStatus()
|
||||
|
||||
@Serializable
|
||||
data class Loaded(
|
||||
override val source: StatusSource,
|
||||
val cardId: String,
|
||||
val lastFourDigits: String,
|
||||
val balance: SerializedBigDecimal,
|
||||
val currencyCode: String,
|
||||
val depositAddress: String?,
|
||||
val isPinSet: Boolean,
|
||||
) : PaymentAccountStatus()
|
||||
|
||||
@Serializable
|
||||
sealed class Error : PaymentAccountStatus() {
|
||||
@Serializable
|
||||
data object ExposedDevice : Error() {
|
||||
override val source: StatusSource = StatusSource.ACTUAL
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Unavailable(override val source: StatusSource) : Error()
|
||||
|
||||
@Serializable
|
||||
data object NotSynced : Error() {
|
||||
override val source: StatusSource = StatusSource.ACTUAL
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data object CardIssueFailed : Error() {
|
||||
override val source: StatusSource = StatusSource.ACTUAL
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -8,4 +8,5 @@ import com.tangem.domain.models.wallet.UserWallet
|
|||
interface TangemPayCryptoCurrencyFactory {
|
||||
|
||||
fun create(userWallet: UserWallet, chainId: Int): Either<UniversalError, CryptoCurrency>
|
||||
fun create(userWallet: UserWallet): Either<UniversalError, CryptoCurrency.Token>
|
||||
}
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
package com.tangem.domain.pay.flow
|
||||
|
||||
import com.tangem.domain.core.flow.FlowProducer
|
||||
import com.tangem.domain.models.account.AccountStatus
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.pay.PaymentAccountStatus
|
||||
|
||||
interface PaymentAccountStatusProducer : FlowProducer<PaymentAccountStatus> {
|
||||
interface PaymentAccountStatusProducer : FlowProducer<AccountStatus.Payment> {
|
||||
data class Params(val userWalletId: UserWalletId)
|
||||
|
||||
interface Factory : FlowProducer.Factory<Params, PaymentAccountStatusProducer>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,18 @@
|
|||
package com.tangem.domain.pay.flow
|
||||
|
||||
import com.tangem.domain.core.flow.FlowCachingSupplier
|
||||
import com.tangem.domain.pay.PaymentAccountStatus
|
||||
import com.tangem.domain.models.account.AccountStatus
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Suppress("UnnecessaryAbstractClass")
|
||||
abstract class PaymentAccountStatusSupplier(
|
||||
override val factory: PaymentAccountStatusProducer.Factory,
|
||||
override val keyCreator: (PaymentAccountStatusProducer.Params) -> String,
|
||||
) : FlowCachingSupplier<PaymentAccountStatusProducer, PaymentAccountStatusProducer.Params, PaymentAccountStatus>()
|
||||
) : FlowCachingSupplier<PaymentAccountStatusProducer, PaymentAccountStatusProducer.Params, AccountStatus.Payment>() {
|
||||
|
||||
operator fun invoke(userWalletId: UserWalletId): Flow<AccountStatus.Payment> {
|
||||
val params = PaymentAccountStatusProducer.Params(userWalletId)
|
||||
return this.invoke(params)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
package com.tangem.domain.pay.model
|
||||
|
||||
import com.tangem.domain.models.account.PaymentAccountStatusValue
|
||||
import com.tangem.domain.models.kyc.KycStatus
|
||||
import com.tangem.domain.visa.model.TangemPayCardFrozenState
|
||||
import java.math.BigDecimal
|
||||
|
||||
sealed class MainCustomerInfoContentState {
|
||||
|
|
@ -25,6 +27,7 @@ data class CustomerInfo(
|
|||
data class ProductInstance(
|
||||
val id: String,
|
||||
val cardId: String,
|
||||
val frozenState: TangemPayCardFrozenState,
|
||||
)
|
||||
|
||||
data class CardInfo(
|
||||
|
|
@ -33,5 +36,7 @@ data class CustomerInfo(
|
|||
val currencyCode: String,
|
||||
val depositAddress: String?,
|
||||
val isPinSet: Boolean,
|
||||
val fiatBalance: PaymentAccountStatusValue.FiatBalance,
|
||||
val cryptoBalance: PaymentAccountStatusValue.CryptoBalance,
|
||||
)
|
||||
}
|
||||
|
|
@ -12,10 +12,8 @@ import com.tangem.domain.pay.repository.OnboardingRepository
|
|||
import com.tangem.domain.visa.error.VisaApiError
|
||||
import com.tangem.security.DeviceSecurityInfoProvider
|
||||
import com.tangem.security.isSecurityExposed
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.flow.*
|
||||
import timber.log.Timber
|
||||
|
||||
private const val TAG = "TangemPayMainScreenCustomerInfoUseCase"
|
||||
|
||||
class TangemPayMainScreenCustomerInfoUseCase(
|
||||
private val onboardingRepository: OnboardingRepository,
|
||||
|
|
@ -27,13 +25,15 @@ class TangemPayMainScreenCustomerInfoUseCase(
|
|||
val state: StateFlow<Map<UserWalletId, Either<TangemPayCustomerInfoError, MainCustomerInfoContentState>>>
|
||||
field = MutableStateFlow(value = mapOf())
|
||||
|
||||
private val logger = TangemLogger.withTag("TangemPayMainScreenCustomerInfoUseCase")
|
||||
|
||||
suspend fun fetch(userWalletId: UserWalletId) {
|
||||
Timber.tag(TAG).i("fetch: ${userWalletId.stringValue}")
|
||||
logger.i("fetch: ${userWalletId.stringValue}")
|
||||
|
||||
if (deviceSecurity.isSecurityExposed()) {
|
||||
Timber.tag(TAG).i("fetch security info: rooted: ${deviceSecurity.isRooted}")
|
||||
Timber.tag(TAG).i("fetch security info: xposed: ${deviceSecurity.isXposed}")
|
||||
Timber.tag(TAG).i("fetch security info: bootloader unlocked: ${deviceSecurity.isBootloaderUnlocked}")
|
||||
logger.i("fetch security info: rooted: ${deviceSecurity.isRooted}")
|
||||
logger.i("fetch security info: xposed: ${deviceSecurity.isXposed}")
|
||||
logger.i("fetch security info: bootloader unlocked: ${deviceSecurity.isBootloaderUnlocked}")
|
||||
|
||||
updateState(userWalletId = userWalletId, either = TangemPayCustomerInfoError.ExposedDeviceError.left())
|
||||
return // fast exit
|
||||
|
|
@ -42,7 +42,7 @@ class TangemPayMainScreenCustomerInfoUseCase(
|
|||
onboardingRepository.hasTangemPayInWallet(userWalletId)
|
||||
.fold(
|
||||
ifLeft = { error ->
|
||||
Timber.tag(TAG).e("Failed checkCustomerWallet for $userWalletId: ${error.javaClass.simpleName}")
|
||||
logger.e("Failed checkCustomerWallet for $userWalletId: ${error.javaClass.simpleName}")
|
||||
if (error is VisaApiError.NotPaeraCustomer) {
|
||||
showOnboardingBannerIfEligible(userWalletId)
|
||||
} else {
|
||||
|
|
@ -50,7 +50,7 @@ class TangemPayMainScreenCustomerInfoUseCase(
|
|||
}
|
||||
},
|
||||
ifRight = { hasTangemPay ->
|
||||
Timber.tag(TAG).i("checkCustomerWallet for $userWalletId: $hasTangemPay")
|
||||
logger.i("checkCustomerWallet for $userWalletId: $hasTangemPay")
|
||||
if (hasTangemPay) {
|
||||
val oldResult = state.value[userWalletId]
|
||||
if (oldResult == null) {
|
||||
|
|
@ -125,14 +125,13 @@ class TangemPayMainScreenCustomerInfoUseCase(
|
|||
): Either<TangemPayCustomerInfoError, MainScreenCustomerInfo> {
|
||||
return onboardingRepository.getCustomerInfo(userWalletId)
|
||||
.mapLeft { error ->
|
||||
Timber.tag(TAG).e("mapErrorForCustomer: $error")
|
||||
logger.e("mapErrorForCustomer: $error")
|
||||
error.mapErrorForCustomer()
|
||||
}
|
||||
.map { customerInfo ->
|
||||
Timber.tag(TAG).i("customerInfo")
|
||||
logger.i("customerInfo")
|
||||
if (customerInfo.productInstance == null) {
|
||||
onboardingRepository.createOrder(userWalletId)
|
||||
Timber.tag("ddk9499").d("TangemPayMainScreenCustomerInfoUseCase.proceedWithoutOrder: ")
|
||||
MainScreenCustomerInfo(info = customerInfo, orderStatus = OrderStatus.NEW)
|
||||
} else {
|
||||
MainScreenCustomerInfo(info = customerInfo, orderStatus = OrderStatus.COMPLETED)
|
||||
|
|
|
|||
|
|
@ -52,7 +52,6 @@ dependencies {
|
|||
/** Other libraries */
|
||||
implementation(platform(deps.firebase.bom))
|
||||
implementation(deps.firebase.analytics)
|
||||
implementation(deps.timber)
|
||||
|
||||
// region DI
|
||||
implementation(deps.hilt.android)
|
||||
|
|
|
|||
|
|
@ -12,13 +12,14 @@ dependencies {
|
|||
// endregion
|
||||
|
||||
// region Domain modules
|
||||
implementation(project(":domain:models"))
|
||||
implementation(projects.domain.models)
|
||||
// endregion
|
||||
|
||||
implementation(projects.core.utils)
|
||||
|
||||
// region Other libraries
|
||||
implementation(deps.kotlin.serialization)
|
||||
implementation(deps.moshi.kotlin)
|
||||
implementation(deps.timber)
|
||||
ksp(deps.moshi.kotlin.codegen)
|
||||
// endregion
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue