Updated on 2026-08-14
This commit is contained in:
commit
d036b23ba4
1274 changed files with 51752 additions and 15452 deletions
|
|
@ -12,4 +12,13 @@ interface AccountsExpandedRepository {
|
|||
suspend fun syncStore(walletId: UserWalletId, existAccounts: Set<AccountId>)
|
||||
suspend fun clearStore()
|
||||
suspend fun update(accountState: AccountExpandedState)
|
||||
|
||||
interface Factory {
|
||||
fun create(storeFileName: String): AccountsExpandedRepository
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val MAIN_STORE_FILE_NAME = "account_expanded_store"
|
||||
const val CHOOSE_TOKEN_FILE_NAME = "choose_token_account_expanded_store"
|
||||
}
|
||||
}
|
||||
|
|
@ -80,7 +80,7 @@ class ArchiveCryptoPortfolioUseCase(
|
|||
val hasNotReferralToken = statuses.none { status ->
|
||||
val currency = status.currency
|
||||
|
||||
currency.network.backendId == referralToken.networkId &&
|
||||
currency.network.rawId == referralToken.networkId &&
|
||||
(currency as? CryptoCurrency.Token)?.contractAddress == referralToken.contractAddress &&
|
||||
status.value.networkAddress?.availableAddresses?.any { it.value == address } == true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -81,7 +81,6 @@ class IsAccountsModeEnabledUseCase(
|
|||
is PaymentAccountStatusValue.IssuingCard,
|
||||
is PaymentAccountStatusValue.Loaded,
|
||||
is PaymentAccountStatusValue.Loading,
|
||||
is PaymentAccountStatusValue.Locked,
|
||||
is PaymentAccountStatusValue.UnderReview,
|
||||
is PaymentAccountStatusValue.Deactivated,
|
||||
-> true
|
||||
|
|
|
|||
|
|
@ -168,7 +168,7 @@ class ManageCryptoCurrenciesUseCase(
|
|||
val foundToken = accountStatus.tokenList.flattenCurrencies()
|
||||
.mapNotNull { it.currency as? CryptoCurrency.Token }
|
||||
.firstOrNull { token ->
|
||||
token.network.backendId == networkId &&
|
||||
token.network.rawId == networkId &&
|
||||
!token.isCustom &&
|
||||
token.contractAddress.equals(contractAddress, true)
|
||||
}
|
||||
|
|
@ -361,7 +361,7 @@ class ManageCryptoCurrenciesUseCase(
|
|||
launch {
|
||||
val assetIds = currencies.mapTo(hashSetOf()) { currency ->
|
||||
ExpressAsset.ID(
|
||||
networkId = currency.network.backendId,
|
||||
networkId = currency.network.rawId,
|
||||
contractAddress = (currency as? CryptoCurrency.Token)?.contractAddress,
|
||||
)
|
||||
}
|
||||
|
|
@ -388,19 +388,19 @@ class ManageCryptoCurrenciesUseCase(
|
|||
) {
|
||||
|
||||
constructor(network: Network) : this(
|
||||
networkId = network.backendId,
|
||||
networkId = network.rawId,
|
||||
derivationPath = network.derivationPath,
|
||||
contractAddress = null,
|
||||
)
|
||||
|
||||
constructor(currency: CryptoCurrency) : this(
|
||||
networkId = currency.network.backendId,
|
||||
networkId = currency.network.rawId,
|
||||
derivationPath = currency.network.derivationPath,
|
||||
contractAddress = (currency as? CryptoCurrency.Token)?.contractAddress,
|
||||
)
|
||||
|
||||
constructor(status: CryptoCurrencyStatus) : this(
|
||||
networkId = status.currency.network.backendId,
|
||||
networkId = status.currency.network.rawId,
|
||||
derivationPath = status.currency.network.derivationPath,
|
||||
contractAddress = (status.currency as? CryptoCurrency.Token)?.contractAddress,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
package com.tangem.domain.account.status.utils
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchainsdk.utils.toBlockchain
|
||||
import com.tangem.domain.account.models.AccountList
|
||||
import com.tangem.domain.account.models.AccountStatusList
|
||||
import com.tangem.domain.account.status.model.AccountCryptoCurrency
|
||||
|
|
@ -180,7 +180,7 @@ internal object AccountCryptoCurrencyStatusFinder {
|
|||
contractAddress: String?,
|
||||
): AccountCryptoCurrency? {
|
||||
return accountList.getExpectedAccounts(
|
||||
rawNetworkId = networkId.rawId.value,
|
||||
rawNetworkId = networkId.rawId,
|
||||
derivationPath = derivationPath,
|
||||
)
|
||||
.asSequence()
|
||||
|
|
@ -220,7 +220,7 @@ internal object AccountCryptoCurrencyStatusFinder {
|
|||
|
||||
internal fun AccountStatusList.getExpectedAccountStatuses(networkId: Network.ID): List<AccountStatus> {
|
||||
val possibleAccountIndex = getAccountIndexOrNull(
|
||||
rawNetworkId = networkId.rawId.value,
|
||||
rawNetworkId = networkId.rawId,
|
||||
derivationPath = networkId.derivationPath,
|
||||
)
|
||||
|
||||
|
|
@ -239,7 +239,7 @@ internal object AccountCryptoCurrencyStatusFinder {
|
|||
}
|
||||
|
||||
internal fun AccountStatusList.getExpectedAccountStatuses(networks: List<Network>): List<AccountStatus> {
|
||||
val possibleAccountIndexes = networks.mapNotNull { getAccountIndexOrNull(it.rawId, it.derivationPath) }
|
||||
val possibleAccountIndexes = networks.mapNotNull { getAccountIndexOrNull(it.id.rawId, it.derivationPath) }
|
||||
|
||||
if (possibleAccountIndexes.isEmpty()) return accountStatuses
|
||||
|
||||
|
|
@ -256,16 +256,14 @@ internal object AccountCryptoCurrencyStatusFinder {
|
|||
// region AccountList helpers
|
||||
|
||||
internal fun AccountList.getExpectedAccounts(network: Network?): List<Account> {
|
||||
return getExpectedAccounts(rawNetworkId = network?.rawId, derivationPath = network?.derivationPath)
|
||||
return getExpectedAccounts(rawNetworkId = network?.id?.rawId, derivationPath = network?.derivationPath)
|
||||
}
|
||||
|
||||
private fun AccountList.getExpectedAccounts(
|
||||
rawNetworkId: String?,
|
||||
rawNetworkId: Network.RawID?,
|
||||
derivationPath: Network.DerivationPath?,
|
||||
): List<Account> {
|
||||
val possibleAccountIndex = getAccountIndexOrNull(rawNetworkId, derivationPath)
|
||||
|
||||
return when (possibleAccountIndex) {
|
||||
return when (val possibleAccountIndex = getAccountIndexOrNull(rawNetworkId, derivationPath)) {
|
||||
null -> accounts
|
||||
DerivationIndex.Main.value -> listOf(mainAccount)
|
||||
// currency only in the account with specific derivation index or in the main account
|
||||
|
|
@ -283,10 +281,10 @@ internal object AccountCryptoCurrencyStatusFinder {
|
|||
|
||||
// region Common helpers
|
||||
|
||||
private fun getAccountIndexOrNull(rawNetworkId: String?, derivationPath: Network.DerivationPath?): Int? {
|
||||
private fun getAccountIndexOrNull(rawNetworkId: Network.RawID?, derivationPath: Network.DerivationPath?): Int? {
|
||||
if (rawNetworkId == null || derivationPath == null) return null
|
||||
|
||||
val blockchain = Blockchain.fromId(id = rawNetworkId)
|
||||
val blockchain = rawNetworkId.toBlockchain()
|
||||
val recognizer = AccountNodeRecognizer(blockchain)
|
||||
|
||||
return recognizer.recognize(derivationPath)?.toInt()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
package com.tangem.domain.account.status.utils
|
||||
|
||||
import com.tangem.domain.account.repository.AccountsExpandedRepository
|
||||
import com.tangem.domain.models.account.AccountId
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.emitAll
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class ChooseTokenExpandedAccountsHolder @Inject constructor(
|
||||
private val mainHolder: MainExpandedAccountsHolder,
|
||||
holderFactory: DefaultExpandedAccountsHolder.Factory,
|
||||
repositoryFactory: AccountsExpandedRepository.Factory,
|
||||
) : ExpandedAccountsHolder {
|
||||
|
||||
private val repository: AccountsExpandedRepository =
|
||||
repositoryFactory.create(AccountsExpandedRepository.CHOOSE_TOKEN_FILE_NAME)
|
||||
private val defaultHolder: DefaultExpandedAccountsHolder = holderFactory.create(repository)
|
||||
|
||||
override fun expandedAccounts(walletId: UserWalletId): Flow<Set<AccountId>> = flow {
|
||||
val isStored = repository.expandedAccounts.first()[walletId] != null
|
||||
|
||||
if (isStored) {
|
||||
emitAll(defaultHolder.expandedAccounts(walletId))
|
||||
} else {
|
||||
val initExpanded = mainHolder.expandedAccounts(walletId).first()
|
||||
emitAll(defaultHolder.expandedAccounts(walletId, initExpanded))
|
||||
}
|
||||
}
|
||||
|
||||
override fun expandAccount(accountId: AccountId) {
|
||||
defaultHolder.expandAccount(accountId)
|
||||
}
|
||||
|
||||
override fun collapseAccount(accountId: AccountId) {
|
||||
defaultHolder.collapseAccount(accountId)
|
||||
}
|
||||
}
|
||||
|
|
@ -9,20 +9,26 @@ import com.tangem.domain.models.account.AccountId
|
|||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
// todo swap separate for main and swap
|
||||
@Singleton
|
||||
class ExpandedAccountsHolder @Inject constructor(
|
||||
interface ExpandedAccountsHolder {
|
||||
fun expandedAccounts(userWallet: UserWallet): Flow<Set<AccountId>> = expandedAccounts(userWallet.walletId)
|
||||
fun expandedAccounts(walletId: UserWalletId): Flow<Set<AccountId>>
|
||||
fun expandAccount(accountId: AccountId)
|
||||
fun collapseAccount(accountId: AccountId)
|
||||
}
|
||||
|
||||
class DefaultExpandedAccountsHolder @AssistedInject constructor(
|
||||
private val singleAccountListSupplier: SingleAccountListSupplier,
|
||||
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
|
||||
private val accountsExpandedRepository: AccountsExpandedRepository,
|
||||
@Assisted private val accountsExpandedRepository: AccountsExpandedRepository,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) {
|
||||
|
||||
|
|
@ -31,71 +37,70 @@ class ExpandedAccountsHolder @Inject constructor(
|
|||
onBufferOverflow = BufferOverflow.DROP_OLDEST,
|
||||
)
|
||||
|
||||
fun expandedAccounts(userWallet: UserWallet): Flow<Set<AccountId>> = expandedAccounts(userWallet.walletId)
|
||||
fun expandedAccounts(walletId: UserWalletId, initExpanded: Set<AccountId> = emptySet()): Flow<Set<AccountId>> =
|
||||
channelFlow {
|
||||
val storedState = accountsExpandedRepository.expandedAccounts
|
||||
.map { it[walletId] ?: initExpanded.map { id -> AccountExpandedState(id, true) } }
|
||||
.stateIn(this)
|
||||
|
||||
fun expandedAccounts(walletId: UserWalletId): Flow<Set<AccountId>> = channelFlow {
|
||||
val storedState = accountsExpandedRepository.expandedAccounts
|
||||
.map { it[walletId].orEmpty() }
|
||||
.stateIn(this)
|
||||
val isAccountsMode = isAccountsModeEnabledUseCase.invoke()
|
||||
.stateIn(this)
|
||||
|
||||
val isAccountsMode = isAccountsModeEnabledUseCase.invoke()
|
||||
.stateIn(this)
|
||||
val initExpandedState = storedState.value
|
||||
.mapNotNull { it.takeIf { state -> state.isExpanded }?.accountId }
|
||||
.toSet()
|
||||
// main state holder
|
||||
val expandedAccounts = MutableStateFlow(initExpandedState)
|
||||
var debounceJob: Job? = null
|
||||
|
||||
val initExpandedState = storedState.value
|
||||
.mapNotNull { it.takeIf { state -> state.isExpanded }?.accountId }
|
||||
.toSet()
|
||||
// main state holder
|
||||
val expandedAccounts = MutableStateFlow(initExpandedState)
|
||||
var debounceJob: Job? = null
|
||||
|
||||
actionChannel
|
||||
.filter { (accountId, _) -> accountId.userWalletId == walletId }
|
||||
.filter { debounceJob?.isActive != true }
|
||||
.onEach { (accountId, isExpand) ->
|
||||
debounceJob = launch { delay(DEBOUNCE_MILLIS) }
|
||||
val newState = AccountExpandedState(accountId, isExpand)
|
||||
launch { accountsExpandedRepository.update(newState) }
|
||||
if (isExpand) {
|
||||
expandedAccounts.update { it.plus(accountId) }
|
||||
} else {
|
||||
expandedAccounts.update { it.minus(accountId) }
|
||||
actionChannel
|
||||
.filter { (accountId, _) -> accountId.userWalletId == walletId }
|
||||
.filter { debounceJob?.isActive != true }
|
||||
.onEach { (accountId, isExpand) ->
|
||||
debounceJob = launch { delay(DEBOUNCE_MILLIS) }
|
||||
val newState = AccountExpandedState(accountId, isExpand)
|
||||
launch { accountsExpandedRepository.update(newState) }
|
||||
if (isExpand) {
|
||||
expandedAccounts.update { it.plus(accountId) }
|
||||
} else {
|
||||
expandedAccounts.update { it.minus(accountId) }
|
||||
}
|
||||
}
|
||||
}
|
||||
.launchIn(this)
|
||||
.launchIn(this)
|
||||
|
||||
walletAccounts(walletId).onEach { accountList ->
|
||||
if (!isAccountsModeEnabledUseCase.invokeSync()) {
|
||||
accountsExpandedRepository.clearStore()
|
||||
expandedAccounts.update { emptySet() }
|
||||
return@onEach
|
||||
}
|
||||
val idsSet = accountList.accounts.mapTo(mutableSetOf()) { it.accountId }
|
||||
accountsExpandedRepository.syncStore(walletId, idsSet)
|
||||
|
||||
val isSingleAccount = accountList.accounts.size == 1
|
||||
val storedMainAccountState = storedState.value
|
||||
.find { it.accountId == accountList.mainAccount.accountId }
|
||||
|
||||
if (isSingleAccount && storedMainAccountState == null) {
|
||||
// force expand for single and not stored account
|
||||
expandedAccounts.update { setOf(accountList.mainAccount.accountId) }
|
||||
}
|
||||
}.launchIn(this)
|
||||
|
||||
combine(
|
||||
flow = expandedAccounts,
|
||||
flow2 = isAccountsMode,
|
||||
transform = { expanded, isAccountMode ->
|
||||
if (isAccountMode) {
|
||||
channel.send(expanded)
|
||||
} else {
|
||||
channel.send(emptySet())
|
||||
walletAccounts(walletId).onEach { accountList ->
|
||||
if (!isAccountsModeEnabledUseCase.invokeSync()) {
|
||||
accountsExpandedRepository.clearStore()
|
||||
expandedAccounts.update { emptySet() }
|
||||
return@onEach
|
||||
}
|
||||
},
|
||||
).collect()
|
||||
}
|
||||
.flowOn(dispatchers.default)
|
||||
.distinctUntilChanged()
|
||||
val idsSet = accountList.accounts.mapTo(mutableSetOf()) { it.accountId }
|
||||
accountsExpandedRepository.syncStore(walletId, idsSet)
|
||||
|
||||
val isSingleAccount = accountList.accounts.size == 1
|
||||
val storedMainAccountState = storedState.value
|
||||
.find { it.accountId == accountList.mainAccount.accountId }
|
||||
|
||||
if (isSingleAccount && storedMainAccountState == null) {
|
||||
// force expand for single and not stored account
|
||||
expandedAccounts.update { setOf(accountList.mainAccount.accountId) }
|
||||
}
|
||||
}.launchIn(this)
|
||||
|
||||
combine(
|
||||
flow = expandedAccounts,
|
||||
flow2 = isAccountsMode,
|
||||
transform = { expanded, isAccountMode ->
|
||||
if (isAccountMode) {
|
||||
channel.send(expanded)
|
||||
} else {
|
||||
channel.send(emptySet())
|
||||
}
|
||||
},
|
||||
).collect()
|
||||
}
|
||||
.flowOn(dispatchers.default)
|
||||
.distinctUntilChanged()
|
||||
|
||||
fun expandAccount(accountId: AccountId) {
|
||||
actionChannel.tryEmit(accountId to true)
|
||||
|
|
@ -107,6 +112,11 @@ class ExpandedAccountsHolder @Inject constructor(
|
|||
|
||||
private fun walletAccounts(walletId: UserWalletId): Flow<AccountList> = singleAccountListSupplier(walletId)
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(accountsExpandedRepository: AccountsExpandedRepository): DefaultExpandedAccountsHolder
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val DEBOUNCE_MILLIS = 200L
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
package com.tangem.domain.account.status.utils
|
||||
|
||||
import com.tangem.domain.account.repository.AccountsExpandedRepository
|
||||
import com.tangem.domain.models.account.AccountId
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class MainExpandedAccountsHolder @Inject constructor(
|
||||
holderFactory: DefaultExpandedAccountsHolder.Factory,
|
||||
repositoryFactory: AccountsExpandedRepository.Factory,
|
||||
) : ExpandedAccountsHolder {
|
||||
|
||||
private val repository: AccountsExpandedRepository = repositoryFactory
|
||||
.create(AccountsExpandedRepository.MAIN_STORE_FILE_NAME)
|
||||
private val default: DefaultExpandedAccountsHolder = holderFactory.create(repository)
|
||||
|
||||
override fun expandedAccounts(walletId: UserWalletId): Flow<Set<AccountId>> {
|
||||
return default.expandedAccounts(walletId)
|
||||
}
|
||||
|
||||
override fun expandAccount(accountId: AccountId) {
|
||||
default.expandAccount(accountId)
|
||||
}
|
||||
|
||||
override fun collapseAccount(accountId: AccountId) {
|
||||
default.collapseAccount(accountId)
|
||||
}
|
||||
}
|
||||
|
|
@ -143,7 +143,7 @@ class ArchiveCryptoPortfolioUseCaseTest {
|
|||
val defaultAddress = "0xABC"
|
||||
|
||||
val cryptoCurrency = mockk<CryptoCurrency.Token> {
|
||||
every { this@mockk.network.backendId } returns token.networkId
|
||||
every { this@mockk.network.rawId } returns token.networkId
|
||||
every { this@mockk.contractAddress } returns token.contractAddress!!
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@ import io.mockk.clearMocks
|
|||
import io.mockk.coEvery
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.flow.last
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.last
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Nested
|
||||
|
|
@ -81,17 +81,6 @@ class IsAccountsModeEnabledUseCaseTest {
|
|||
Truth.assertThat(actual).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns true when payment account is Locked`() = runTest {
|
||||
val list = createAccountList(WALLET_ID_1, listOf(mockCryptoPortfolio(), mockPaymentAccount()))
|
||||
every { multiAccountListSupplier.invoke() } returns flowOf(listOf(list))
|
||||
mockPaymentStatus(WALLET_ID_1, mockk<PaymentAccountStatusValue.Locked>())
|
||||
|
||||
val actual = useCase.invoke().last()
|
||||
|
||||
Truth.assertThat(actual).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns false when payment account is NotCreated`() = runTest {
|
||||
val list = createAccountList(WALLET_ID_1, listOf(mockCryptoPortfolio(), mockPaymentAccount()))
|
||||
|
|
@ -235,17 +224,6 @@ class IsAccountsModeEnabledUseCaseTest {
|
|||
Truth.assertThat(actual).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns true when payment account is Locked`() = runTest {
|
||||
val list = createAccountList(WALLET_ID_1, listOf(mockCryptoPortfolio(), mockPaymentAccount()))
|
||||
coEvery { multiAccountListSupplier.getSyncOrNull(Unit, any()) } returns listOf(list)
|
||||
mockPaymentStatusSync(WALLET_ID_1, mockk<PaymentAccountStatusValue.Locked>())
|
||||
|
||||
val actual = useCase.invokeSync()
|
||||
|
||||
Truth.assertThat(actual).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns false when payment account is NotCreated`() = runTest {
|
||||
val list = createAccountList(WALLET_ID_1, listOf(mockCryptoPortfolio(), mockPaymentAccount()))
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ plugins {
|
|||
}
|
||||
|
||||
android {
|
||||
namespace = "com.tangem.domain.tokensync"
|
||||
namespace = "com.tangem.domain.assetsdiscovery"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
|
@ -14,6 +14,9 @@ dependencies {
|
|||
implementation(projects.domain.account.status)
|
||||
implementation(projects.core.utils)
|
||||
|
||||
implementation(projects.libs.blockchainSdk)
|
||||
implementation(tangemDeps.blockchain)
|
||||
|
||||
implementation(deps.kotlin.coroutines)
|
||||
implementation(deps.arrow.core)
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.domain.assetsdiscovery
|
||||
|
||||
import com.tangem.blockchain.assetsdiscovery.AssetsDiscoveryService
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
||||
interface AssetsDiscoveryFacade {
|
||||
|
||||
suspend fun getAssetsDiscoveryService(userWalletId: UserWalletId, network: Network): AssetsDiscoveryServiceInfo?
|
||||
|
||||
data class AssetsDiscoveryServiceInfo(
|
||||
val address: String,
|
||||
val service: AssetsDiscoveryService,
|
||||
)
|
||||
}
|
||||
|
|
@ -1,13 +1,13 @@
|
|||
package com.tangem.domain.tokensync.model
|
||||
package com.tangem.domain.assetsdiscovery.model
|
||||
|
||||
sealed class TokenSyncProgress {
|
||||
sealed class AssetsDiscoveryProgress {
|
||||
|
||||
data object Idle : TokenSyncProgress()
|
||||
data object Idle : AssetsDiscoveryProgress()
|
||||
|
||||
data class InProgress(
|
||||
val completedNetworks: Int,
|
||||
val totalNetworks: Int,
|
||||
) : TokenSyncProgress() {
|
||||
) : AssetsDiscoveryProgress() {
|
||||
val progressPercent: Int
|
||||
get() = if (totalNetworks > 0) {
|
||||
completedNetworks * 100 / totalNetworks
|
||||
|
|
@ -16,5 +16,5 @@ sealed class TokenSyncProgress {
|
|||
}
|
||||
}
|
||||
|
||||
data object Completed : TokenSyncProgress()
|
||||
data object Completed : AssetsDiscoveryProgress()
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package com.tangem.domain.assetsdiscovery.repository
|
||||
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.assetsdiscovery.model.AssetsDiscoveryProgress
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface AssetsDiscoveryRepository {
|
||||
|
||||
suspend fun runDiscovery(userWalletId: UserWalletId)
|
||||
|
||||
suspend fun completeDiscovery(userWalletId: UserWalletId)
|
||||
|
||||
suspend fun getPendingDiscoveryWalletIds(): List<UserWalletId>
|
||||
|
||||
fun observeDiscoveryProgress(userWalletId: UserWalletId): Flow<AssetsDiscoveryProgress>
|
||||
|
||||
fun acknowledgeCompletion(userWalletId: UserWalletId)
|
||||
|
||||
suspend fun clearPendingFlag(userWalletId: UserWalletId)
|
||||
|
||||
suspend fun getDiscoveredCurrencies(userWalletId: UserWalletId): List<CryptoCurrency>
|
||||
|
||||
suspend fun clearDiscoveredTokens(userWalletId: UserWalletId)
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package com.tangem.domain.assetsdiscovery.usecase
|
||||
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.assetsdiscovery.repository.AssetsDiscoveryRepository
|
||||
|
||||
class AcknowledgeAssetsDiscoveryCompletionUseCase(
|
||||
private val assetsDiscoveryRepository: AssetsDiscoveryRepository,
|
||||
) {
|
||||
|
||||
operator fun invoke(userWalletId: UserWalletId) {
|
||||
assetsDiscoveryRepository.acknowledgeCompletion(userWalletId)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.domain.assetsdiscovery.usecase
|
||||
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.assetsdiscovery.model.AssetsDiscoveryProgress
|
||||
import com.tangem.domain.assetsdiscovery.repository.AssetsDiscoveryRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
class ObserveAssetsDiscoveryUseCase(
|
||||
private val assetsDiscoveryRepository: AssetsDiscoveryRepository,
|
||||
) {
|
||||
|
||||
operator fun invoke(userWalletId: UserWalletId): Flow<AssetsDiscoveryProgress> {
|
||||
return assetsDiscoveryRepository.observeDiscoveryProgress(userWalletId)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,20 +1,23 @@
|
|||
package com.tangem.domain.tokensync.usecase
|
||||
package com.tangem.domain.assetsdiscovery.usecase
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.event.AssetsDiscoveryAnalyticsEvent
|
||||
import com.tangem.domain.account.status.usecase.ManageCryptoCurrenciesUseCase
|
||||
import com.tangem.domain.models.account.AccountId
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.tokensync.repository.TokenSyncRepository
|
||||
import com.tangem.domain.assetsdiscovery.repository.AssetsDiscoveryRepository
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
class StartTokenSyncUseCase(
|
||||
private val tokenSyncRepository: TokenSyncRepository,
|
||||
class StartAssetsDiscoveryUseCase(
|
||||
private val assetsDiscoveryRepository: AssetsDiscoveryRepository,
|
||||
private val manageCryptoCurrenciesUseCase: ManageCryptoCurrenciesUseCase,
|
||||
private val appCoroutineScope: AppCoroutineScope,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
) {
|
||||
|
||||
private val activeSyncJobs = ConcurrentHashMap<UserWalletId, Job>()
|
||||
|
|
@ -23,9 +26,11 @@ class StartTokenSyncUseCase(
|
|||
activeSyncJobs[userWalletId]?.cancel()
|
||||
activeSyncJobs[userWalletId] = appCoroutineScope.launch {
|
||||
try {
|
||||
tokenSyncRepository.runSync(userWalletId)
|
||||
analyticsEventHandler.send(AssetsDiscoveryAnalyticsEvent.SyncStarted())
|
||||
assetsDiscoveryRepository.runDiscovery(userWalletId)
|
||||
applyDiscoveredTokens(userWalletId)
|
||||
tokenSyncRepository.completeSync(userWalletId)
|
||||
assetsDiscoveryRepository.completeDiscovery(userWalletId)
|
||||
analyticsEventHandler.send(AssetsDiscoveryAnalyticsEvent.SyncCompleted())
|
||||
} catch (e: Exception) {
|
||||
TangemLogger.e("Token sync failed for wallet: $userWalletId", e)
|
||||
} finally {
|
||||
|
|
@ -36,18 +41,18 @@ class StartTokenSyncUseCase(
|
|||
|
||||
suspend fun cancel(userWalletId: UserWalletId): Either<Throwable, Unit> = Either.catch {
|
||||
activeSyncJobs.remove(userWalletId)?.cancel()
|
||||
tokenSyncRepository.clearPendingFlag(userWalletId)
|
||||
tokenSyncRepository.clearDiscoveredTokens(userWalletId)
|
||||
assetsDiscoveryRepository.clearPendingFlag(userWalletId)
|
||||
assetsDiscoveryRepository.clearDiscoveredTokens(userWalletId)
|
||||
}
|
||||
|
||||
fun applyPendingSyncs() {
|
||||
fun applyPendingAssetsDiscovery() {
|
||||
appCoroutineScope.launch {
|
||||
try {
|
||||
val pendingIds = tokenSyncRepository.getPendingSyncWalletIds()
|
||||
val pendingIds = assetsDiscoveryRepository.getPendingDiscoveryWalletIds()
|
||||
for (walletId in pendingIds) {
|
||||
val isApplied = applyDiscoveredTokens(walletId)
|
||||
if (isApplied) {
|
||||
tokenSyncRepository.clearPendingFlag(walletId)
|
||||
assetsDiscoveryRepository.clearPendingFlag(walletId)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
|
@ -57,7 +62,7 @@ class StartTokenSyncUseCase(
|
|||
}
|
||||
|
||||
private suspend fun applyDiscoveredTokens(userWalletId: UserWalletId): Boolean {
|
||||
val currencies = tokenSyncRepository.getDiscoveredCurrencies(userWalletId)
|
||||
val currencies = assetsDiscoveryRepository.getDiscoveredCurrencies(userWalletId)
|
||||
|
||||
if (currencies.isEmpty()) return true
|
||||
|
||||
|
|
@ -67,7 +72,7 @@ class StartTokenSyncUseCase(
|
|||
add = currencies,
|
||||
).fold(
|
||||
ifRight = {
|
||||
tokenSyncRepository.clearDiscoveredTokens(userWalletId)
|
||||
assetsDiscoveryRepository.clearDiscoveredTokens(userWalletId)
|
||||
true
|
||||
},
|
||||
ifLeft = { error ->
|
||||
|
|
@ -33,4 +33,13 @@ sealed class TransactionParams {
|
|||
data class Solana(
|
||||
val transactions: List<String>,
|
||||
) : TransactionParams()
|
||||
|
||||
/**
|
||||
* Parameters for Bitcoin transactions
|
||||
*
|
||||
* @property params JSON-encoded transaction parameters
|
||||
*/
|
||||
data class Bitcoin(
|
||||
val params: String,
|
||||
) : TransactionParams()
|
||||
}
|
||||
|
|
@ -8,6 +8,10 @@ android {
|
|||
namespace = "com.tangem.domain.card"
|
||||
}
|
||||
|
||||
tasks.withType<Test>().configureEach {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.core.analytics.models)
|
||||
implementation(projects.core.error)
|
||||
|
|
@ -25,6 +29,7 @@ dependencies {
|
|||
implementation(projects.domain.visa.models)
|
||||
implementation(projects.core.utils)
|
||||
|
||||
implementation(projects.libs.tangemSdkApi)
|
||||
|
||||
implementation(tangemDeps.card.core)
|
||||
implementation(tangemDeps.blockchain) {
|
||||
|
|
@ -32,9 +37,8 @@ dependencies {
|
|||
}
|
||||
|
||||
/** Testing libraries */
|
||||
testImplementation(deps.test.junit5)
|
||||
testRuntimeOnly(deps.test.junit5.engine)
|
||||
testImplementation(deps.test.mockk)
|
||||
testImplementation(deps.test.truth)
|
||||
testRuntimeOnly(deps.test.junit5.vintage.engine)
|
||||
testImplementation(projects.common.test)
|
||||
testImplementation(projects.test.core)
|
||||
}
|
||||
|
|
@ -1,8 +1,33 @@
|
|||
package com.tangem.domain.card
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.raise.either
|
||||
import com.tangem.common.doOnFailure
|
||||
import com.tangem.sdk.api.TangemSdkManager
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
|
||||
interface DeleteSavedAccessCodesUseCase {
|
||||
/**
|
||||
* Removes saved user codes (access code and/or passcode) for a physical Tangem card
|
||||
* from the device's secure storage.
|
||||
*
|
||||
* Typically invoked after a successful factory reset of the card, so that stale codes
|
||||
* for an already-wiped card are not left on the device.
|
||||
*
|
||||
* @property tangemSdkManager Card SDK wrapper that performs the code removal operation
|
||||
*/
|
||||
class DeleteSavedAccessCodesUseCase(
|
||||
private val tangemSdkManager: TangemSdkManager,
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(cardId: String): Either<Throwable, Unit>
|
||||
/**
|
||||
* @param cardId identifier of the card whose saved codes must be removed
|
||||
* @return [Unit] on success; a Card SDK error (as [Throwable]) if removal failed
|
||||
*/
|
||||
suspend operator fun invoke(cardId: String): Either<Throwable, Unit> = either {
|
||||
tangemSdkManager.deleteSavedUserCodes(cardsIds = setOf(cardId))
|
||||
.doOnFailure { error ->
|
||||
TangemLogger.e("Failed to delete saved access codes for card with id: $cardId", error)
|
||||
raise(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
package com.tangem.domain.card
|
||||
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.sdk.api.TangemSdkManager
|
||||
import com.tangem.test.core.assertEitherLeft
|
||||
import com.tangem.test.core.assertEitherRight
|
||||
import io.mockk.clearMocks
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class DeleteSavedAccessCodesUseCaseTest {
|
||||
|
||||
private val tangemSdkManager = mockk<TangemSdkManager>()
|
||||
|
||||
private lateinit var useCase: DeleteSavedAccessCodesUseCase
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
clearMocks(tangemSdkManager)
|
||||
useCase = DeleteSavedAccessCodesUseCase(tangemSdkManager = tangemSdkManager)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns Right Unit when sdk deletes codes successfully`() = runTest {
|
||||
// Arrange
|
||||
val cardId = "AA00000000000001"
|
||||
coEvery { tangemSdkManager.deleteSavedUserCodes(setOf(cardId)) } returns CompletionResult.Success(Unit)
|
||||
|
||||
// Act
|
||||
val actual = useCase(cardId = cardId)
|
||||
|
||||
// Assert
|
||||
assertEitherRight(actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns Left with sdk error when sdk fails`() = runTest {
|
||||
// Arrange
|
||||
val cardId = "AA00000000000002"
|
||||
val sdkError = TangemSdkError.ExceptionError(RuntimeException("boom"))
|
||||
coEvery { tangemSdkManager.deleteSavedUserCodes(setOf(cardId)) } returns CompletionResult.Failure(sdkError)
|
||||
|
||||
// Act
|
||||
val actual = useCase(cardId = cardId)
|
||||
|
||||
// Assert
|
||||
assertEitherLeft(actual, sdkError)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `passes exactly the given cardId as a singleton set to sdk`() = runTest {
|
||||
// Arrange
|
||||
val cardId = "AA00000000000003"
|
||||
coEvery { tangemSdkManager.deleteSavedUserCodes(any()) } returns CompletionResult.Success(Unit)
|
||||
|
||||
// Act
|
||||
useCase(cardId = cardId)
|
||||
|
||||
// Assert
|
||||
coVerify(exactly = 1) { tangemSdkManager.deleteSavedUserCodes(setOf(cardId)) }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.domain.common.wallets
|
||||
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
|
||||
/**
|
||||
* Handler invoked when a user wallet becomes the active one.
|
||||
*
|
||||
* Side effects (analytics tracking context, Tangem SDK display config, access code request policy, etc.)
|
||||
* follow switch-latest semantics: if a new selection arrives while a previous one is still being processed,
|
||||
* the in-flight job is cancelled and only the latest selection is applied.
|
||||
*/
|
||||
interface UserWalletSelectedHandler {
|
||||
|
||||
suspend operator fun invoke(userWallet: UserWallet)
|
||||
}
|
||||
|
|
@ -8,13 +8,25 @@ android {
|
|||
namespace = "com.tangem.domain.dynamicaddresses"
|
||||
}
|
||||
|
||||
tasks.withType<Test>().configureEach {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api(projects.domain.core)
|
||||
api(projects.domain.dynamicAddresses.models)
|
||||
|
||||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.walletManager)
|
||||
implementation(projects.domain.wallets)
|
||||
implementation(projects.libs.blockchainSdk)
|
||||
|
||||
implementation(tangemDeps.blockchain) {
|
||||
exclude(module = "joda-time")
|
||||
}
|
||||
implementation(tangemDeps.card.core)
|
||||
|
||||
testRuntimeOnly(deps.test.junit5.engine)
|
||||
testImplementation(projects.common.test)
|
||||
testImplementation(projects.test.core)
|
||||
}
|
||||
|
|
@ -11,7 +11,7 @@ class DisableDynamicAddressesUseCase(
|
|||
|
||||
/**
|
||||
* Returns true when consolidation is required before disabling (non-base balances exist),
|
||||
* or false when DA was disabled immediately.
|
||||
* or false when dynamic addresses were disabled immediately.
|
||||
*/
|
||||
suspend operator fun invoke(userWalletId: UserWalletId, network: Network): Either<Throwable, Boolean> =
|
||||
Either.catch {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,52 @@
|
|||
package com.tangem.domain.dynamicaddresses
|
||||
|
||||
import com.tangem.crypto.hdWallet.DerivationPath
|
||||
|
||||
/**
|
||||
* Shared utility for checking if a derivation path conflicts with Dynamic Addresses.
|
||||
*
|
||||
* A path conflicts when it belongs to the same BIP44 account as the base path
|
||||
* (first 3 nodes: purpose / coin_type / account match) but has non-zero
|
||||
* change (node 3) or address_index (node 4).
|
||||
*/
|
||||
object DynamicAddressesDerivationChecker {
|
||||
|
||||
private const val BIP44_NODE_COUNT = 5
|
||||
private const val ACCOUNT_NODE_COUNT = 3
|
||||
private const val CHANGE_NODE_INDEX = 3
|
||||
private const val ADDRESS_INDEX_NODE_INDEX = 4
|
||||
|
||||
/**
|
||||
* @return `true` if [path] has zero change (node 3) and zero address_index (node 4).
|
||||
*/
|
||||
fun isBaseDerivation(path: String): Boolean {
|
||||
val nodes = runCatching { DerivationPath(path).nodes }.getOrNull() ?: return false
|
||||
if (nodes.size < BIP44_NODE_COUNT) return false
|
||||
|
||||
val change = nodes[CHANGE_NODE_INDEX].getIndex(includeHardened = false)
|
||||
val index = nodes[ADDRESS_INDEX_NODE_INDEX].getIndex(includeHardened = false)
|
||||
|
||||
return change == 0L && index == 0L
|
||||
}
|
||||
|
||||
/**
|
||||
* @return `true` if [customPath] shares the same account as [basePath] but has
|
||||
* non-zero change or address_index nodes.
|
||||
*/
|
||||
fun hasSameAccountWithNonZeroChangeOrIndex(customPath: String, basePath: String): Boolean {
|
||||
val customNodes = runCatching { DerivationPath(customPath).nodes }.getOrNull() ?: return false
|
||||
val baseNodes = runCatching { DerivationPath(basePath).nodes }.getOrNull() ?: return false
|
||||
|
||||
if (customNodes.size < BIP44_NODE_COUNT || baseNodes.size < BIP44_NODE_COUNT) return false
|
||||
|
||||
val isSameAccount = (0 until ACCOUNT_NODE_COUNT).all { i ->
|
||||
customNodes[i].getIndex(includeHardened = false) == baseNodes[i].getIndex(includeHardened = false)
|
||||
}
|
||||
if (!isSameAccount) return false
|
||||
|
||||
val change = customNodes[CHANGE_NODE_INDEX].getIndex(includeHardened = false)
|
||||
val index = customNodes[ADDRESS_INDEX_NODE_INDEX].getIndex(includeHardened = false)
|
||||
|
||||
return change != 0L || index != 0L
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
package com.tangem.domain.dynamicaddresses
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchainsdk.utils.toNetworkId
|
||||
|
||||
/**
|
||||
* List of blockchains that support Dynamic Addresses (XPUB-based multi-address mode).
|
||||
* Must match [Blockchain.isBip44DerivationStyleXPUB] from blockchain-sdk minus Kaspa (deferred).
|
||||
*
|
||||
* Dynamic addresses are NOT used for Legacy (m/44' for BTC/LTC) or Taproot (m/86') addresses.
|
||||
* Only the default derivation style per blockchain is supported.
|
||||
*/
|
||||
object DynamicAddressesSupportedBlockchains {
|
||||
|
||||
private const val BIP44_PURPOSE = 44L
|
||||
private const val BIP84_PURPOSE = 84L
|
||||
|
||||
private val supported = setOf(
|
||||
Blockchain.Bitcoin,
|
||||
Blockchain.BitcoinTestnet,
|
||||
Blockchain.BitcoinCash,
|
||||
Blockchain.BitcoinCashTestnet,
|
||||
Blockchain.Litecoin,
|
||||
Blockchain.Dogecoin,
|
||||
Blockchain.Dash,
|
||||
Blockchain.Ravencoin,
|
||||
Blockchain.RavencoinTestnet,
|
||||
)
|
||||
|
||||
private val supportedNetworkIds = supported.map { it.toNetworkId() }.toSet()
|
||||
|
||||
/**
|
||||
* Allowed BIP purpose nodes per network ID.
|
||||
* BTC/LTC use BIP-84 (SegWit), others use BIP-44 (Legacy P2PKH).
|
||||
*/
|
||||
private val allowedPurposeByNetworkId: Map<String, Long> = buildMap {
|
||||
put(Blockchain.Bitcoin.toNetworkId(), BIP84_PURPOSE)
|
||||
put(Blockchain.BitcoinTestnet.toNetworkId(), BIP84_PURPOSE)
|
||||
put(Blockchain.Litecoin.toNetworkId(), BIP84_PURPOSE)
|
||||
put(Blockchain.BitcoinCash.toNetworkId(), BIP44_PURPOSE)
|
||||
put(Blockchain.BitcoinCashTestnet.toNetworkId(), BIP44_PURPOSE)
|
||||
put(Blockchain.Dogecoin.toNetworkId(), BIP44_PURPOSE)
|
||||
put(Blockchain.Dash.toNetworkId(), BIP44_PURPOSE)
|
||||
put(Blockchain.Ravencoin.toNetworkId(), BIP44_PURPOSE)
|
||||
put(Blockchain.RavencoinTestnet.toNetworkId(), BIP44_PURPOSE)
|
||||
}
|
||||
|
||||
fun isSupported(blockchain: Blockchain): Boolean = blockchain in supported
|
||||
|
||||
fun isSupportedByNetworkId(networkId: String): Boolean = networkId in supportedNetworkIds
|
||||
|
||||
/** Returns the allowed BIP purpose node for the given network, or null if not supported */
|
||||
fun getAllowedPurpose(networkId: String): Long? = allowedPurposeByNetworkId[networkId]
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package com.tangem.domain.dynamicaddresses
|
||||
|
||||
sealed class EnableDynamicAddressesError {
|
||||
|
||||
data object ConflictingCustomTokens : EnableDynamicAddressesError()
|
||||
|
||||
data class ServiceError(val cause: Throwable) : EnableDynamicAddressesError()
|
||||
}
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
package com.tangem.domain.dynamicaddresses
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
import com.tangem.domain.dynamicaddresses.repository.DynamicAddressesRepository
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
|
@ -9,8 +11,19 @@ class EnableDynamicAddressesUseCase(
|
|||
private val dynamicAddressesRepository: DynamicAddressesRepository,
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(userWalletId: UserWalletId, network: Network, xpub: String): Either<Throwable, Unit> =
|
||||
Either.catch {
|
||||
suspend operator fun invoke(
|
||||
userWalletId: UserWalletId,
|
||||
network: Network,
|
||||
xpub: String,
|
||||
): Either<EnableDynamicAddressesError, Unit> {
|
||||
return try {
|
||||
if (dynamicAddressesRepository.hasConflictingCustomTokens(userWalletId, network)) {
|
||||
return EnableDynamicAddressesError.ConflictingCustomTokens.left()
|
||||
}
|
||||
dynamicAddressesRepository.enable(userWalletId, network, xpub)
|
||||
Unit.right()
|
||||
} catch (e: Throwable) {
|
||||
EnableDynamicAddressesError.ServiceError(e).left()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
package com.tangem.domain.dynamicaddresses
|
||||
|
||||
import com.tangem.blockchainsdk.utils.toBlockchain
|
||||
import com.tangem.common.extensions.ByteArrayKey
|
||||
import com.tangem.common.extensions.calculateRipemd160
|
||||
import com.tangem.common.extensions.calculateSha256
|
||||
import com.tangem.crypto.NetworkType
|
||||
import com.tangem.crypto.hdWallet.DerivationPath
|
||||
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.domain.wallets.derivations.DerivationsRepository
|
||||
|
||||
/**
|
||||
* Returns the XPUB string if account-level keys are already derived (no card scan needed),
|
||||
* or null if keys are not available.
|
||||
*/
|
||||
class GetDerivedXpubUseCase(
|
||||
private val walletManagersFacade: WalletManagersFacade,
|
||||
private val derivationsRepository: DerivationsRepository,
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(userWalletId: UserWalletId, network: Network): String? {
|
||||
val blockchain = network.toBlockchain()
|
||||
if (!DynamicAddressesSupportedBlockchains.isSupported(blockchain)) return null
|
||||
|
||||
val walletManager = walletManagersFacade.getOrCreateWalletManager(userWalletId, network) ?: return null
|
||||
val hdKey = walletManager.wallet.publicKey.derivationType?.hdKey ?: return null
|
||||
if (hdKey.path.nodes.size <= ACCOUNT_PATH_DROP_COUNT) return null
|
||||
|
||||
val seedKey = ByteArrayKey(walletManager.wallet.publicKey.seedKey)
|
||||
val existingKeys = derivationsRepository.getExistingDerivedKeys(userWalletId, seedKey)
|
||||
|
||||
val accountPath = DerivationPath(hdKey.path.nodes.dropLast(ACCOUNT_PATH_DROP_COUNT))
|
||||
val parentPath = DerivationPath(accountPath.nodes.dropLast(1))
|
||||
|
||||
val childExtKey = existingKeys[accountPath] ?: return null
|
||||
val parentExtKey = existingKeys[parentPath] ?: return null
|
||||
|
||||
val parentFingerprint = parentExtKey.publicKey
|
||||
.calculateSha256().calculateRipemd160()
|
||||
.take(PARENT_FINGERPRINT_SIZE).toByteArray()
|
||||
|
||||
val net = if (blockchain.isTestnet()) NetworkType.Testnet else NetworkType.Mainnet
|
||||
return ExtendedPublicKey(
|
||||
publicKey = childExtKey.publicKey,
|
||||
chainCode = childExtKey.chainCode,
|
||||
depth = accountPath.nodes.size,
|
||||
parentFingerprint = parentFingerprint,
|
||||
childNumber = accountPath.nodes.last().index,
|
||||
).serialize(net)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val ACCOUNT_PATH_DROP_COUNT = 2
|
||||
const val PARENT_FINGERPRINT_SIZE = 4
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package com.tangem.domain.dynamicaddresses
|
||||
|
||||
import com.tangem.blockchainsdk.utils.toBlockchain
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
|
||||
/**
|
||||
* Checks if XPUB generation is supported for the given wallet and network (hardware capability check).
|
||||
*/
|
||||
class IsXpubSupportedUseCase(
|
||||
private val walletManagersFacade: WalletManagersFacade,
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(userWalletId: UserWalletId, network: Network): Boolean {
|
||||
val blockchain = network.toBlockchain()
|
||||
if (!DynamicAddressesSupportedBlockchains.isSupported(blockchain)) return false
|
||||
|
||||
val walletManager = walletManagersFacade.getOrCreateWalletManager(userWalletId, network) ?: return false
|
||||
return walletManager.wallet.publicKey.derivationType?.hdKey != null
|
||||
}
|
||||
}
|
||||
|
|
@ -19,4 +19,18 @@ interface DynamicAddressesRepository {
|
|||
suspend fun getLastUsedReceiveAddress(userWalletId: UserWalletId, network: Network): String?
|
||||
|
||||
suspend fun hasNonBaseBalances(userWalletId: UserWalletId, network: Network): Boolean
|
||||
|
||||
/** Returns true if there are custom tokens with change/index ≠ 0 that conflict with dynamic addresses */
|
||||
suspend fun hasConflictingCustomTokens(userWalletId: UserWalletId, network: Network): Boolean
|
||||
|
||||
/** Lightweight check: is the DA flag enabled for the native coin of the given network (no xpub availability check) */
|
||||
fun isDynamicAddressesEnabledForNetwork(userWalletId: UserWalletId, networkId: Network.ID): Flow<Boolean>
|
||||
|
||||
/**
|
||||
* Emits true when dynamic addresses are DISABLED for this token but a silent xpub probe
|
||||
* detected non-zero balances on derived addresses beyond the base one. Only positive
|
||||
* results are cached per session; false results and probe failures are not cached and may
|
||||
* be re-probed on subsequent collections or when status changes.
|
||||
*/
|
||||
fun hasFundsOnAdditionalAddresses(userWalletId: UserWalletId, network: Network): Flow<Boolean>
|
||||
}
|
||||
|
|
@ -0,0 +1,203 @@
|
|||
package com.tangem.domain.dynamicaddresses
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
class DynamicAddressesDerivationCheckerTest {
|
||||
|
||||
// region Conflicting: same account, non-zero change or index
|
||||
|
||||
@Test
|
||||
fun `same account, non-zero address index`() {
|
||||
val result = check(custom = "m/44'/5'/0'/0/1", base = "m/44'/5'/0'/0/0")
|
||||
assertThat(result).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `same account, non-zero change`() {
|
||||
val result = check(custom = "m/44'/5'/0'/1/0", base = "m/44'/5'/0'/0/0")
|
||||
assertThat(result).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `same account, both change and index non-zero`() {
|
||||
val result = check(custom = "m/44'/5'/0'/1/5", base = "m/44'/5'/0'/0/0")
|
||||
assertThat(result).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `same account, large address index`() {
|
||||
val result = check(custom = "m/44'/5'/0'/0/8", base = "m/44'/5'/0'/0/0")
|
||||
assertThat(result).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `bitcoin BIP84, non-zero index`() {
|
||||
val result = check(custom = "m/84'/0'/0'/0/1", base = "m/84'/0'/0'/0/0")
|
||||
assertThat(result).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `litecoin BIP84, non-zero change`() {
|
||||
val result = check(custom = "m/84'/2'/0'/1/0", base = "m/84'/2'/0'/0/0")
|
||||
assertThat(result).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `dogecoin, non-zero index`() {
|
||||
val result = check(custom = "m/44'/3'/0'/0/8", base = "m/44'/3'/0'/0/0")
|
||||
assertThat(result).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `bitcoin cash, non-zero index`() {
|
||||
val result = check(custom = "m/44'/145'/0'/0/3", base = "m/44'/145'/0'/0/0")
|
||||
assertThat(result).isTrue()
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region Not conflicting: different account
|
||||
|
||||
@Test
|
||||
fun `different account index, non-zero address index`() {
|
||||
val result = check(custom = "m/44'/5'/1'/0/1", base = "m/44'/5'/0'/0/0")
|
||||
assertThat(result).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `different account index, zero change and index`() {
|
||||
val result = check(custom = "m/44'/5'/1'/0/0", base = "m/44'/5'/0'/0/0")
|
||||
assertThat(result).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `different coin type`() {
|
||||
val result = check(custom = "m/44'/0'/0'/0/1", base = "m/44'/5'/0'/0/0")
|
||||
assertThat(result).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `different purpose`() {
|
||||
val result = check(custom = "m/84'/5'/0'/0/1", base = "m/44'/5'/0'/0/0")
|
||||
assertThat(result).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `BIP44 custom against BIP84 base`() {
|
||||
val result = check(custom = "m/44'/0'/0'/0/1", base = "m/84'/0'/0'/0/0")
|
||||
assertThat(result).isFalse()
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region Not conflicting: same account, zero change and index
|
||||
|
||||
@Test
|
||||
fun `identical paths`() {
|
||||
val result = check(custom = "m/44'/5'/0'/0/0", base = "m/44'/5'/0'/0/0")
|
||||
assertThat(result).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `identical bitcoin BIP84 paths`() {
|
||||
val result = check(custom = "m/84'/0'/0'/0/0", base = "m/84'/0'/0'/0/0")
|
||||
assertThat(result).isFalse()
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region Edge cases: invalid or incomplete paths
|
||||
|
||||
@Test
|
||||
fun `invalid custom path`() {
|
||||
val result = check(custom = "invalid", base = "m/44'/5'/0'/0/0")
|
||||
assertThat(result).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invalid base path`() {
|
||||
val result = check(custom = "m/44'/5'/0'/0/1", base = "not_a_path")
|
||||
assertThat(result).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `empty custom path`() {
|
||||
val result = check(custom = "", base = "m/44'/5'/0'/0/0")
|
||||
assertThat(result).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `empty base path`() {
|
||||
val result = check(custom = "m/44'/5'/0'/0/1", base = "")
|
||||
assertThat(result).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `both paths invalid`() {
|
||||
val result = check(custom = "abc", base = "xyz")
|
||||
assertThat(result).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `custom path with fewer than 5 nodes`() {
|
||||
val result = check(custom = "m/44'/5'/0'", base = "m/44'/5'/0'/0/0")
|
||||
assertThat(result).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `base path with fewer than 5 nodes`() {
|
||||
val result = check(custom = "m/44'/5'/0'/0/1", base = "m/44'/5'")
|
||||
assertThat(result).isFalse()
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region isBaseDerivation
|
||||
|
||||
@Test
|
||||
fun `isBaseDerivation - standard BIP44 base path`() {
|
||||
assertThat(DynamicAddressesDerivationChecker.isBaseDerivation("m/44'/5'/0'/0/0")).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isBaseDerivation - BIP84 base path`() {
|
||||
assertThat(DynamicAddressesDerivationChecker.isBaseDerivation("m/84'/0'/0'/0/0")).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isBaseDerivation - non-zero index`() {
|
||||
assertThat(DynamicAddressesDerivationChecker.isBaseDerivation("m/44'/5'/0'/0/1")).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isBaseDerivation - non-zero change`() {
|
||||
assertThat(DynamicAddressesDerivationChecker.isBaseDerivation("m/44'/5'/0'/1/0")).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isBaseDerivation - non-zero account with zero change and index`() {
|
||||
assertThat(DynamicAddressesDerivationChecker.isBaseDerivation("m/44'/5'/2'/0/0")).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isBaseDerivation - invalid path`() {
|
||||
assertThat(DynamicAddressesDerivationChecker.isBaseDerivation("invalid")).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isBaseDerivation - too few nodes`() {
|
||||
assertThat(DynamicAddressesDerivationChecker.isBaseDerivation("m/44'/5'")).isFalse()
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
private fun check(custom: String, base: String): Boolean {
|
||||
return DynamicAddressesDerivationChecker.hasSameAccountWithNonZeroChangeOrIndex(
|
||||
customPath = custom,
|
||||
basePath = base,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -56,7 +56,7 @@ class GetEarnNetworksUseCase(
|
|||
accountLists
|
||||
.filter { it.userWalletId in unlockedWalletsId }
|
||||
.flatMap(AccountList::flattenCurrencies)
|
||||
.mapTo(HashSet()) { it.network.backendId }
|
||||
.mapTo(HashSet()) { it.network.rawId }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package com.tangem.domain.express.models
|
|||
|
||||
import java.math.BigDecimal
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
sealed class ExpressError : Throwable() {
|
||||
|
||||
abstract val code: Int
|
||||
|
|
@ -61,4 +62,12 @@ sealed class ExpressError : Throwable() {
|
|||
data object UnknownError : ExpressError() {
|
||||
override val code: Int = -1
|
||||
}
|
||||
|
||||
data class TooLargeSolanaTransactionError(override val code: Int = -2) : ExpressError() {
|
||||
override val message: String = "tooLargeSolanaTransaction"
|
||||
}
|
||||
|
||||
data class DexActiveSupplyError(override val code: Int = -3) : ExpressError() {
|
||||
override val message: String = "dexActiveSupplyError"
|
||||
}
|
||||
}
|
||||
|
|
@ -23,14 +23,18 @@ enum class ExpressProviderType(val typeName: String) {
|
|||
ONRAMP(typeName = "ONRAMP"),
|
||||
;
|
||||
|
||||
fun shouldStoreSwapTransaction() = when (this) {
|
||||
CEX,
|
||||
DEX_BRIDGE,
|
||||
DEX,
|
||||
-> true
|
||||
ONRAMP,
|
||||
-> false
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun ExpressProviderType.shouldStoreSwapTransaction() = when (this) {
|
||||
CEX,
|
||||
DEX_BRIDGE,
|
||||
-> true
|
||||
DEX,
|
||||
ONRAMP,
|
||||
-> false
|
||||
fun getSwapProviderTypes(): List<ExpressProviderType> {
|
||||
return listOf(CEX, DEX, DEX_BRIDGE)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,12 @@
|
|||
package com.tangem.domain.feedback.models
|
||||
|
||||
import com.tangem.domain.models.network.Network
|
||||
|
||||
/**
|
||||
* Information about blockchain's operation error
|
||||
*
|
||||
* @property errorMessage message about error
|
||||
* @property blockchainId blockchain id
|
||||
* @property derivationPath derivation path
|
||||
* @property networkId network ID
|
||||
* @property destinationAddress destination address
|
||||
* @property tokenSymbol token symbol or null, if it isn't operation with token
|
||||
* @property amount amount
|
||||
|
|
@ -13,8 +14,7 @@ package com.tangem.domain.feedback.models
|
|||
*/
|
||||
data class BlockchainErrorInfo(
|
||||
val errorMessage: String,
|
||||
val blockchainId: String,
|
||||
val derivationPath: String?,
|
||||
val networkId: Network.ID?,
|
||||
val destinationAddress: String,
|
||||
val tokenSymbol: String?,
|
||||
val amount: String,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.domain.feedback.repository
|
||||
|
||||
import com.tangem.domain.feedback.models.*
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import java.io.File
|
||||
|
|
@ -17,11 +18,7 @@ interface FeedbackRepository {
|
|||
|
||||
fun getPhoneInfo(): PhoneInfo
|
||||
|
||||
suspend fun getBlockchainInfo(
|
||||
userWalletId: UserWalletId,
|
||||
blockchainId: String,
|
||||
derivationPath: String?,
|
||||
): BlockchainInfo?
|
||||
suspend fun getBlockchainInfo(userWalletId: UserWalletId, networkId: Network.ID): BlockchainInfo?
|
||||
|
||||
fun saveBlockchainErrorInfo(error: BlockchainErrorInfo)
|
||||
|
||||
|
|
|
|||
|
|
@ -94,11 +94,10 @@ class EmailMessageBodyResolver(
|
|||
|
||||
val userWalletId = requireNotNull(type.walletMetaInfo.userWalletId) { "UserWalletId must be not null" }
|
||||
val blockchainError = feedbackRepository.getBlockchainErrorInfo(userWalletId = userWalletId)
|
||||
val blockchainInfo = blockchainError?.let {
|
||||
val blockchainInfo = blockchainError?.networkId?.let { networkId ->
|
||||
feedbackRepository.getBlockchainInfo(
|
||||
userWalletId = userWalletId,
|
||||
blockchainId = blockchainError.blockchainId,
|
||||
derivationPath = blockchainError.derivationPath,
|
||||
networkId = networkId,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -159,11 +158,10 @@ class EmailMessageBodyResolver(
|
|||
|
||||
val userWalletId = requireNotNull(walletMetaInfo.userWalletId) { "UserWalletId must be not null" }
|
||||
val blockchainError = feedbackRepository.getBlockchainErrorInfo(userWalletId = userWalletId)
|
||||
val blockchainInfo = blockchainError?.let {
|
||||
val blockchainInfo = blockchainError?.networkId?.let { networkId ->
|
||||
feedbackRepository.getBlockchainInfo(
|
||||
userWalletId = userWalletId,
|
||||
blockchainId = blockchainError.blockchainId,
|
||||
derivationPath = blockchainError.derivationPath,
|
||||
networkId = networkId,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -181,11 +179,10 @@ class EmailMessageBodyResolver(
|
|||
|
||||
val userWalletId = requireNotNull(type.walletMetaInfo.userWalletId) { "UserWalletId must be not null" }
|
||||
val blockchainError = feedbackRepository.getBlockchainErrorInfo(userWalletId = userWalletId)
|
||||
val blockchainInfo = blockchainError?.let {
|
||||
val blockchainInfo = blockchainError?.networkId?.let { networkId ->
|
||||
feedbackRepository.getBlockchainInfo(
|
||||
userWalletId = userWalletId,
|
||||
blockchainId = blockchainError.blockchainId,
|
||||
derivationPath = blockchainError.derivationPath,
|
||||
networkId = networkId,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -210,11 +207,10 @@ class EmailMessageBodyResolver(
|
|||
|
||||
val userWalletId = requireNotNull(type.walletMetaInfo.userWalletId) { "UserWalletId must be not null" }
|
||||
val blockchainError = feedbackRepository.getBlockchainErrorInfo(userWalletId = userWalletId)
|
||||
val blockchainInfo = blockchainError?.let {
|
||||
val blockchainInfo = blockchainError?.networkId?.let { networkId ->
|
||||
feedbackRepository.getBlockchainInfo(
|
||||
userWalletId = userWalletId,
|
||||
blockchainId = blockchainError.blockchainId,
|
||||
derivationPath = blockchainError.derivationPath,
|
||||
networkId = networkId,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -42,7 +42,6 @@ dependencies {
|
|||
implementation(deps.kotlin.coroutines)
|
||||
implementation(deps.moshi)
|
||||
implementation(deps.moshi.kotlin)
|
||||
implementation(deps.reKotlin)
|
||||
ksp(deps.moshi.kotlin.codegen)
|
||||
|
||||
/** Testing libraries */
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ object NetworkLogConfig {
|
|||
|
||||
object AnalyticsHandlersLogConfig {
|
||||
val isFirebaseLogEnabled: Boolean = BuildConfig.LOG_ENABLED
|
||||
val isAmplitudeLogEnabled: Boolean = BuildConfig.LOG_ENABLED
|
||||
val isAppsflyerLogEnabled: Boolean = BuildConfig.LOG_ENABLED
|
||||
val isCustomerIoLogEnabled: Boolean = BuildConfig.LOG_ENABLED
|
||||
}
|
||||
|
|
@ -31,11 +31,22 @@ interface RampStateManager {
|
|||
sendUnavailabilityReason: ScenarioUnavailabilityReason?,
|
||||
): Either<ScenarioUnavailabilityReason, Unit>
|
||||
|
||||
/**
|
||||
* Check if [CryptoCurrency] is available for swap (express/assets request)
|
||||
*
|
||||
* @param userWalletId the ID of the user's wallet
|
||||
* @param cryptoCurrency cryptocurrency
|
||||
*/
|
||||
suspend fun availableForSwap(
|
||||
userWalletId: UserWalletId,
|
||||
cryptoCurrency: CryptoCurrency,
|
||||
): ScenarioUnavailabilityReason
|
||||
|
||||
suspend fun availableForSwap(
|
||||
userWalletId: UserWalletId,
|
||||
cryptoCurrencies: List<CryptoCurrency>,
|
||||
): Map<CryptoCurrency, ScenarioUnavailabilityReason>
|
||||
|
||||
suspend fun fetchSellServiceData()
|
||||
|
||||
fun getSellInitializationStatus(): Flow<Lce<Throwable, Any>>
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
package com.tangem.domain.redux
|
||||
|
||||
import org.rekotlin.Action
|
||||
|
||||
sealed interface LegacyAction : Action {
|
||||
|
||||
data object PrepareDetailsScreen : LegacyAction
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
package com.tangem.domain.redux
|
||||
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import org.rekotlin.Action
|
||||
|
||||
interface ReduxStateHolder {
|
||||
|
||||
fun dispatch(action: Action)
|
||||
|
||||
suspend fun dispatchWithMain(action: Action)
|
||||
|
||||
suspend fun onUserWalletSelected(userWallet: UserWallet)
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.domain.markets
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
enum class PreselectedMarketsInterval(val value: String) {
|
||||
H24("24h"),
|
||||
W1("1w"),
|
||||
D30("30d"),
|
||||
;
|
||||
|
||||
companion object {
|
||||
fun parse(value: String?): PreselectedMarketsInterval? = entries.firstOrNull { it.value == value }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package com.tangem.domain.markets
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
enum class PreselectedMarketsOrder(val value: String) {
|
||||
Rating("rating"),
|
||||
Trending("trending"),
|
||||
Buyers("buyers"),
|
||||
Gainers("gainers"),
|
||||
Losers("losers"),
|
||||
;
|
||||
|
||||
companion object {
|
||||
fun parse(value: String?): PreselectedMarketsOrder? = entries.firstOrNull { it.value == value }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package com.tangem.domain.markets
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
enum class PreselectedTokenDetailsSection(val value: String) {
|
||||
News("news"),
|
||||
;
|
||||
|
||||
companion object {
|
||||
fun parse(value: String?): PreselectedTokenDetailsSection? = entries.firstOrNull { it.value == value }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.tangem.domain.markets
|
||||
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
|
||||
/**
|
||||
* minimal token info for add-to-portfolio flow
|
||||
*/
|
||||
data class RawMarketToken(
|
||||
val id: CryptoCurrency.RawID,
|
||||
val name: String,
|
||||
val symbol: String,
|
||||
)
|
||||
|
|
@ -10,7 +10,7 @@ class GetTokenMarketCryptoCurrency(
|
|||
) {
|
||||
suspend operator fun invoke(
|
||||
userWalletId: UserWalletId,
|
||||
tokenMarketParams: TokenMarketParams,
|
||||
tokenMarketParams: RawMarketToken,
|
||||
network: TokenMarketInfo.Network,
|
||||
accountIndex: DerivationIndex,
|
||||
): CryptoCurrency? {
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ interface MarketsTokenRepository {
|
|||
|
||||
suspend fun createCryptoCurrency(
|
||||
userWalletId: UserWalletId,
|
||||
token: TokenMarketParams,
|
||||
token: RawMarketToken,
|
||||
network: TokenMarketInfo.Network,
|
||||
accountIndex: DerivationIndex? = null,
|
||||
): CryptoCurrency?
|
||||
|
|
|
|||
|
|
@ -18,11 +18,11 @@ data class TokenReceiveConfig(
|
|||
|
||||
@Serializable
|
||||
data class ReceiveAddressModel(
|
||||
val nameService: NameService,
|
||||
val displayType: DisplayType,
|
||||
val value: String,
|
||||
) {
|
||||
enum class NameService {
|
||||
Default, Legacy, Ens
|
||||
enum class DisplayType {
|
||||
Default, Legacy, Ens, Dynamic,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -37,6 +37,8 @@ data class AccountId private constructor(
|
|||
|
||||
companion object {
|
||||
|
||||
const val PaymentAccountIdPrefix = "payment_"
|
||||
|
||||
private val sha256Digest: MessageDigest by lazy { MessageDigest.getInstance("SHA-256") }
|
||||
private val hexRegex = Regex("^[a-fA-F0-9]{64}$")
|
||||
|
||||
|
|
@ -73,7 +75,7 @@ data class AccountId private constructor(
|
|||
}
|
||||
|
||||
fun forPaymentAccount(userWalletId: UserWalletId): AccountId {
|
||||
return AccountId(value = "payment_$userWalletId", userWalletId = userWalletId)
|
||||
return AccountId(value = "$PaymentAccountIdPrefix$userWalletId", userWalletId = userWalletId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
package com.tangem.domain.models.account
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.raise.either
|
||||
import arrow.core.raise.ensure
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
@ConsistentCopyVisibility
|
||||
data class CardDisplayName private constructor(val value: String) {
|
||||
|
||||
@Serializable
|
||||
sealed interface Error {
|
||||
@Serializable
|
||||
data object Empty : Error
|
||||
|
||||
@Serializable
|
||||
data object ExceedsMaxLength : Error
|
||||
|
||||
@Serializable
|
||||
data object InvalidCharacters : Error
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val MAX_LENGTH = 20
|
||||
private val allowedPattern = Regex("^[\\p{L}\\p{N} ]+$")
|
||||
|
||||
operator fun invoke(name: String): Either<Error, CardDisplayName> = either {
|
||||
val trimmed = name.trim()
|
||||
ensure(trimmed.isNotEmpty()) { Error.Empty }
|
||||
ensure(trimmed.length <= MAX_LENGTH) { Error.ExceedsMaxLength }
|
||||
ensure(allowedPattern.matches(trimmed)) { Error.InvalidCharacters }
|
||||
CardDisplayName(trimmed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,9 +2,15 @@ package com.tangem.domain.models.account
|
|||
|
||||
import com.tangem.domain.models.StatusSource
|
||||
import com.tangem.domain.models.TotalFiatBalance
|
||||
import com.tangem.domain.models.account.PaymentAccountStatusValue.Loaded
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.kyc.KycStatus
|
||||
import com.tangem.domain.models.network.NetworkAddress
|
||||
import com.tangem.domain.models.pay.TangemPayCard
|
||||
import com.tangem.domain.models.serialization.SerializedBigDecimal
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* Represents the various states a payment account can have, encapsulating different information based on the state.
|
||||
|
|
@ -25,7 +31,6 @@ sealed class PaymentAccountStatusValue {
|
|||
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)
|
||||
is Deactivated -> TotalFiatBalance.Loaded(amount = fiatBalance.availableBalance, source = source)
|
||||
}
|
||||
|
|
@ -39,7 +44,6 @@ sealed class 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 Deactivated -> copy(source = source)
|
||||
is Loading,
|
||||
|
|
@ -102,57 +106,49 @@ sealed class PaymentAccountStatusValue {
|
|||
val fiatBalance: FiatBalance,
|
||||
) : 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.
|
||||
* @property cards The list of user's cards.
|
||||
*/
|
||||
@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()
|
||||
val cryptoCurrency: CryptoCurrency.Token,
|
||||
val cards: List<TangemPayCard>,
|
||||
) : PaymentAccountStatusValue() {
|
||||
val cryptoCurrencyStatus: CryptoCurrencyStatus = CryptoCurrencyStatus(
|
||||
currency = cryptoCurrency,
|
||||
value = CryptoCurrencyStatus.Loaded(
|
||||
amount = cryptoBalance.balance,
|
||||
fiatAmount = fiatBalance.availableBalance,
|
||||
fiatRate = BigDecimal.ONE,
|
||||
priceChange = BigDecimal.ZERO,
|
||||
networkAddress = NetworkAddress.Single(
|
||||
defaultAddress = NetworkAddress.Address(
|
||||
type = NetworkAddress.Address.Type.Primary,
|
||||
value = cryptoBalance.depositAddress,
|
||||
),
|
||||
),
|
||||
sources = CryptoCurrencyStatus.Sources(),
|
||||
pendingTransactions = emptySet(),
|
||||
stakingBalance = null,
|
||||
yieldSupplyStatus = null,
|
||||
hasCurrentNetworkTransactions = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
/** Represents an error state for the payment account status. */
|
||||
@Serializable
|
||||
|
|
@ -212,4 +208,10 @@ sealed class PaymentAccountStatusValue {
|
|||
val tokenContractAddress: String,
|
||||
val balance: SerializedBigDecimal,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun Loaded.hasCardWithId(cardId: String): Boolean = cards.any { it.id == cardId }
|
||||
|
||||
fun Loaded.findCardWithId(cardId: String): TangemPayCard? = cards.firstOrNull { it.id == cardId }
|
||||
|
||||
fun Loaded.requireCardWithId(cardId: String): TangemPayCard = requireNotNull(findCardWithId(cardId))
|
||||
|
|
@ -3,7 +3,7 @@ package com.tangem.domain.models.currency
|
|||
import java.math.BigDecimal
|
||||
|
||||
fun CryptoCurrency.Token.yieldSupplyKey(): String {
|
||||
return "${network.backendId}_$contractAddress"
|
||||
return "${network.rawId}_$contractAddress"
|
||||
}
|
||||
|
||||
fun CryptoCurrencyStatus.hasNotSuppliedAmount(): Boolean {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import kotlinx.serialization.Serializable
|
|||
* (e.g., ERC20, BEP20).
|
||||
*
|
||||
* @property id the unique identifier of the network
|
||||
* @property backendId the name of this network in the Tangem backend
|
||||
* @property name the human-readable name of the network, such as "Ethereum" or "Bitcoin"
|
||||
* @property currencySymbol the symbol of the currency associated with the network
|
||||
* @property derivationPath the path used to derive keys for this network
|
||||
|
|
@ -25,7 +24,6 @@ import kotlinx.serialization.Serializable
|
|||
@Serializable
|
||||
data class Network(
|
||||
val id: ID,
|
||||
val backendId: String,
|
||||
val name: String,
|
||||
val currencySymbol: String,
|
||||
val derivationPath: DerivationPath,
|
||||
|
|
@ -49,7 +47,7 @@ data class Network(
|
|||
/**
|
||||
* Represents a unique identifier for a blockchain network
|
||||
*
|
||||
* @property rawId raw network ID
|
||||
* @property rawId raw network ID (backend id)
|
||||
* @property derivationPath derivation path
|
||||
*/
|
||||
@Serializable
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
package com.tangem.domain.models.pay
|
||||
|
||||
import com.tangem.domain.models.account.CardDisplayName
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Represents a Tangem Pay card linked to a payment account.
|
||||
*
|
||||
* @property id unique card identifier assigned by the backend.
|
||||
* @property hasPinCode whether the card has a PIN code set.
|
||||
* @property displayName optional human-readable name assigned to the card; `null` if not set.
|
||||
* @property limit spending limit configuration for the card; `null` if not configured or not yet loaded.
|
||||
* @property isFrozen whether the card is currently frozen (blocked for payments).
|
||||
* @property lastDigits The last four digits of the card number.
|
||||
*/
|
||||
@Serializable
|
||||
data class TangemPayCard(
|
||||
@SerialName("id") val id: String,
|
||||
@SerialName("has_pin_code") val hasPinCode: Boolean,
|
||||
@SerialName("display_name") val displayName: CardDisplayName?,
|
||||
@SerialName("limit") val limit: TangemPayCardLimitData?,
|
||||
@SerialName("is_frozen") val isFrozen: Boolean,
|
||||
@SerialName("last_digits") val lastDigits: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
package com.tangem.domain.models.pay
|
||||
|
||||
import com.tangem.domain.models.serialization.SerializedBigDecimal
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.util.Locale
|
||||
|
||||
@Serializable
|
||||
data class TangemPayCardLimit(
|
||||
@SerialName("amount") val amount: SerializedBigDecimal,
|
||||
@SerialName("period") val period: TangemPayCardLimitPeriod,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
enum class TangemPayCardLimitPeriod {
|
||||
@SerialName("DAY")
|
||||
DAY,
|
||||
|
||||
@SerialName("WEEK")
|
||||
WEEK,
|
||||
|
||||
@SerialName("MONTH")
|
||||
MONTH,
|
||||
|
||||
@SerialName("YEAR")
|
||||
YEAR,
|
||||
|
||||
@SerialName("ALL_TIME")
|
||||
ALL_TIME,
|
||||
|
||||
@SerialName("AUTHORIZATION")
|
||||
AUTHORIZATION,
|
||||
|
||||
@SerialName("UNKNOWN")
|
||||
UNKNOWN,
|
||||
;
|
||||
|
||||
companion object {
|
||||
fun fromString(value: String) = when (value.uppercase(Locale.US)) {
|
||||
"DAY" -> DAY
|
||||
"WEEK" -> WEEK
|
||||
"MONTH" -> MONTH
|
||||
"YEAR" -> YEAR
|
||||
"ALL_TIME" -> ALL_TIME
|
||||
"AUTHORIZATION" -> AUTHORIZATION
|
||||
else -> UNKNOWN
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.domain.models.pay
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class TangemPayCardLimitData(
|
||||
@SerialName("actual_card_limit") val actualCardLimit: TangemPayCardLimit?,
|
||||
@SerialName("admin_card_limit") val adminCardLimit: TangemPayCardLimit?,
|
||||
)
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.domain.models
|
||||
package com.tangem.domain.models.pay
|
||||
|
||||
enum class TangemPayEligibilityType {
|
||||
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package com.tangem.domain.models.pay
|
||||
|
||||
import java.math.BigDecimal
|
||||
|
||||
data class TangemPayReissueCardFee(
|
||||
val amount: BigDecimal,
|
||||
val currencyCode: String,
|
||||
)
|
||||
|
|
@ -1,14 +1,16 @@
|
|||
package com.tangem.domain.search.model
|
||||
package com.tangem.domain.models.portfolio
|
||||
|
||||
import com.tangem.domain.models.account.AccountId
|
||||
import com.tangem.domain.models.account.AccountName
|
||||
import com.tangem.domain.models.account.CryptoPortfolioIcon
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
||||
data class UserAssetSearchEntry(
|
||||
data class UserAssetEntry(
|
||||
val userWalletId: UserWalletId,
|
||||
val userWalletName: String,
|
||||
val accountId: AccountId,
|
||||
val accountName: AccountName,
|
||||
val accountIcon: CryptoPortfolioIcon,
|
||||
val currencyStatus: CryptoCurrencyStatus,
|
||||
)
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
<?xml version="1.0" ?>
|
||||
<SmellBaseline>
|
||||
<ManuallySuppressedIssues/>
|
||||
<CurrentIssues>
|
||||
<ID>UnnecessaryAbstractClass:MultiNetworkStatusSupplier.kt$MultiNetworkStatusSupplier$MultiNetworkStatusSupplier</ID>
|
||||
<ID>UnnecessaryAbstractClass:SingleNetworkStatusSupplier.kt$SingleNetworkStatusSupplier$SingleNetworkStatusSupplier</ID>
|
||||
</CurrentIssues>
|
||||
</SmellBaseline>
|
||||
|
|
@ -11,7 +11,7 @@ import com.tangem.domain.models.network.NetworkStatus
|
|||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
abstract class MultiNetworkStatusSupplier(
|
||||
open class MultiNetworkStatusSupplier(
|
||||
override val factory: MultiNetworkStatusProducer.Factory,
|
||||
override val keyCreator: (MultiNetworkStatusProducer.Params) -> String,
|
||||
) : FlowCachingSupplier<MultiNetworkStatusProducer, MultiNetworkStatusProducer.Params, Set<NetworkStatus>>()
|
||||
|
|
@ -11,7 +11,7 @@ import com.tangem.domain.models.network.NetworkStatus
|
|||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
abstract class SingleNetworkStatusSupplier(
|
||||
open class SingleNetworkStatusSupplier(
|
||||
override val factory: SingleNetworkStatusProducer.Factory,
|
||||
override val keyCreator: (SingleNetworkStatusProducer.Params) -> String,
|
||||
) : FlowCachingSupplier<SingleNetworkStatusProducer, SingleNetworkStatusProducer.Params, NetworkStatus>()
|
||||
|
|
@ -7,14 +7,12 @@ import kotlinx.serialization.Serializable
|
|||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*
|
||||
* @param language device locale (ex: en, ru).
|
||||
* @param snapshot id snapshot (`meta.asOf`) to stabilize responses.
|
||||
* @param tokenIds filter by tokens.
|
||||
* @param categoryIds filter by category.
|
||||
*/
|
||||
@Serializable
|
||||
data class NewsListConfig(
|
||||
val language: String,
|
||||
val snapshot: String?,
|
||||
val tokenIds: List<String> = emptyList(),
|
||||
val categoryIds: List<Int> = emptyList(),
|
||||
|
|
|
|||
|
|
@ -38,15 +38,14 @@ interface NewsRepository {
|
|||
/**
|
||||
* Fetches and caches detailed articles for provided ids in parallel.
|
||||
*/
|
||||
suspend fun fetchDetailedArticles(newsIds: Collection<Int>, language: String?): Either<Map<Int, Throwable>, Unit>
|
||||
suspend fun fetchDetailedArticles(newsIds: Collection<Int>): Either<Map<Int, Throwable>, Unit>
|
||||
|
||||
/**
|
||||
* Fetch list of trending news by limit and with correct locale and store it in runtime data store.
|
||||
*
|
||||
* @param limit
|
||||
* @param language current device locale
|
||||
*/
|
||||
suspend fun fetchTrendingNews(limit: Int, language: String?)
|
||||
suspend fun fetchTrendingNews(limit: Int)
|
||||
|
||||
/**
|
||||
* Observes trending news with runtime viewed flag support.
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ package com.tangem.domain.news.usecase
|
|||
|
||||
import arrow.core.Either
|
||||
import com.tangem.domain.news.repository.NewsRepository
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* Fetches trending news to store it in runtime data store.
|
||||
|
|
@ -11,10 +10,7 @@ import java.util.Locale
|
|||
class FetchTrendingNewsUseCase(private val newsRepository: NewsRepository) {
|
||||
|
||||
suspend operator fun invoke(): Either<Throwable, Unit> = Either.catch {
|
||||
newsRepository.fetchTrendingNews(
|
||||
limit = LIMIT_FOR_TRENDING_NEWS,
|
||||
language = Locale.getDefault().language,
|
||||
)
|
||||
newsRepository.fetchTrendingNews(limit = LIMIT_FOR_TRENDING_NEWS)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
|||
|
|
@ -24,6 +24,6 @@ class ObserveNewsDetailsUseCase(
|
|||
/**
|
||||
* Prefetches the given article ids (can be called with current + next ids for pager preloading).
|
||||
*/
|
||||
suspend fun prefetch(newsIds: Collection<Int>, language: String?): Either<Map<Int, Throwable>, Unit> =
|
||||
repository.fetchDetailedArticles(newsIds, language)
|
||||
suspend fun prefetch(newsIds: Collection<Int>): Either<Map<Int, Throwable>, Unit> =
|
||||
repository.fetchDetailedArticles(newsIds)
|
||||
}
|
||||
|
|
@ -3,5 +3,5 @@ package com.tangem.domain.search.model
|
|||
data class SearchResult(
|
||||
val textHints: List<SearchTextHint>,
|
||||
val recentTokens: List<RecentSearchToken>,
|
||||
val userAssets: List<UserAssetSearchEntry>,
|
||||
val userAssets: List<UserAssetSearchItem>,
|
||||
)
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.domain.search.model
|
||||
|
||||
import com.tangem.domain.models.portfolio.UserAssetEntry
|
||||
|
||||
sealed interface UserAssetSearchItem {
|
||||
|
||||
data class Single(val entry: UserAssetEntry) : UserAssetSearchItem
|
||||
|
||||
data class Grouped(
|
||||
val tokenName: String,
|
||||
val tokenSymbol: String,
|
||||
val tokenIconUrl: String?,
|
||||
val entries: List<UserAssetEntry>,
|
||||
) : UserAssetSearchItem
|
||||
}
|
||||
|
|
@ -8,11 +8,13 @@ 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.models.portfolio.UserAssetEntry
|
||||
import com.tangem.domain.search.model.UserAssetSearchItem
|
||||
import com.tangem.domain.search.repository.SearchRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.map
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* Primary search use case that produces [SearchResult] based on the current query.
|
||||
|
|
@ -70,9 +72,12 @@ class GetSearchResultsUseCase(
|
|||
|
||||
if (unlockedWallets.isEmpty()) return@combine emptyList()
|
||||
|
||||
statusLists
|
||||
val entries = statusLists
|
||||
.filter { it.userWalletId in unlockedWallets }
|
||||
.flatMap { statusList -> extractMatchingAssets(statusList, unlockedWallets, lowerQuery) }
|
||||
|
||||
val shouldGroup = needsGrouping(unlockedWallets.values, statusLists)
|
||||
groupAndSort(entries, shouldGroup)
|
||||
}.map { userAssets ->
|
||||
SearchResult(
|
||||
textHints = emptyList(),
|
||||
|
|
@ -82,11 +87,49 @@ class GetSearchResultsUseCase(
|
|||
}
|
||||
}
|
||||
|
||||
private fun needsGrouping(unlockedWallets: Collection<UserWallet>, statusLists: List<AccountStatusList>): Boolean {
|
||||
if (unlockedWallets.size > 1) return true
|
||||
|
||||
val totalAccounts = statusLists
|
||||
.filter { sl -> unlockedWallets.any { it.walletId == sl.userWalletId } }
|
||||
.sumOf { it.accountStatuses.filterCryptoPortfolio().size }
|
||||
|
||||
return totalAccounts > 1
|
||||
}
|
||||
|
||||
private fun groupAndSort(entries: List<UserAssetEntry>, shouldGroup: Boolean): List<UserAssetSearchItem> {
|
||||
if (!shouldGroup) {
|
||||
return entries
|
||||
.map { UserAssetSearchItem.Single(it) }
|
||||
.sortedByDescending { it.entry.currencyStatus.value.fiatAmount ?: BigDecimal.ZERO }
|
||||
}
|
||||
|
||||
val grouped = entries.groupBy { entry ->
|
||||
val rawId = entry.currencyStatus.currency.id.rawCurrencyId
|
||||
rawId?.value ?: "${entry.currencyStatus.currency.name}|${entry.currencyStatus.currency.symbol}"
|
||||
}
|
||||
|
||||
return grouped.map { (_, groupEntries) ->
|
||||
val assetInfo = groupEntries.first()
|
||||
UserAssetSearchItem.Grouped(
|
||||
tokenName = assetInfo.currencyStatus.currency.name,
|
||||
tokenSymbol = assetInfo.currencyStatus.currency.symbol,
|
||||
tokenIconUrl = assetInfo.currencyStatus.currency.iconUrl,
|
||||
entries = groupEntries,
|
||||
)
|
||||
}.sortedByDescending { item ->
|
||||
when (item) {
|
||||
is UserAssetSearchItem.Grouped ->
|
||||
item.entries.sumOf { it.currencyStatus.value.fiatAmount ?: BigDecimal.ZERO }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun extractMatchingAssets(
|
||||
statusList: AccountStatusList,
|
||||
wallets: Map<UserWalletId, UserWallet>,
|
||||
lowerQuery: String,
|
||||
): List<UserAssetSearchEntry> {
|
||||
): List<UserAssetEntry> {
|
||||
val wallet = wallets[statusList.userWalletId] ?: return emptyList()
|
||||
return statusList.accountStatuses
|
||||
.filterCryptoPortfolio()
|
||||
|
|
@ -98,11 +141,12 @@ class GetSearchResultsUseCase(
|
|||
name.contains(lowerQuery) || symbol.contains(lowerQuery)
|
||||
}
|
||||
.map { currencyStatus ->
|
||||
UserAssetSearchEntry(
|
||||
UserAssetEntry(
|
||||
userWalletId = statusList.userWalletId,
|
||||
userWalletName = wallet.name,
|
||||
accountId = accountStatus.accountId,
|
||||
accountName = accountStatus.account.accountName,
|
||||
accountIcon = accountStatus.account.icon,
|
||||
currencyStatus = currencyStatus,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
package com.tangem.domain.settings
|
||||
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
/**
|
||||
* Manages the hot wallet creation restriction setting.
|
||||
*
|
||||
* When the restriction is enabled, users are forced to scan a physical Tangem card
|
||||
* instead of being able to create a new software (hot) wallet.
|
||||
*/
|
||||
interface HotWalletRestrictionManager {
|
||||
|
||||
/** Observes the current restriction state as a [StateFlow]. */
|
||||
fun isCreationEnabled(): StateFlow<Boolean>
|
||||
|
||||
/** Returns the latest cached restriction state synchronously. */
|
||||
fun isCreationEnabledSync(): Boolean
|
||||
|
||||
/** Toggles the restriction state. No-op in production. */
|
||||
suspend fun toggleCreationEnabled()
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
<?xml version="1.0" ?>
|
||||
<SmellBaseline>
|
||||
<ManuallySuppressedIssues/>
|
||||
<CurrentIssues>
|
||||
<ID>MultilineLambdaItParameter:FetchStakingYieldBalanceUseCase.kt$FetchStakingYieldBalanceUseCase${ when (it) { is StakingIdFactory.Error.UnableToGetAddress -> raise(StakingError.DomainError("$it")) StakingIdFactory.Error.UnsupportedCurrency -> Unit.right() } return@either }</ID>
|
||||
<ID>MultilineLambdaItParameter:InvalidatePendingTransactionsUseCase.kt$InvalidatePendingTransactionsUseCase${ !it.isPending && action.amount < it.amount && it.type == BalanceType.STAKED && it.validatorAddress == action.validatorAddress }</ID>
|
||||
<ID>NamedArguments:GetConstructedStakingTransactionUseCase.kt$GetConstructedStakingTransactionUseCase$constructTransaction(networkId, fee, amount, transactionId)</ID>
|
||||
<ID>UnnecessaryAbstractClass:MultiStakingBalanceSupplier.kt$MultiStakingBalanceSupplier$MultiStakingBalanceSupplier</ID>
|
||||
<ID>UnnecessaryAbstractClass:SingleStakingBalanceSupplier.kt$SingleStakingBalanceSupplier$SingleStakingBalanceSupplier</ID>
|
||||
<ID>UseEmptyCounterpart:StakingAnalyticsEvent.kt$StakingAnalyticsEvent$mapOf()</ID>
|
||||
<ID>UseOrEmpty:InvalidatePendingTransactionsUseCase.kt$InvalidatePendingTransactionsUseCase$action.validatorAddress ?: action.validatorAddresses?.getOrNull(0) ?: ""</ID>
|
||||
</CurrentIssues>
|
||||
</SmellBaseline>
|
||||
|
|
@ -23,9 +23,9 @@ class FetchStakingYieldBalanceUseCase(
|
|||
currencyId = cryptoCurrency.id,
|
||||
network = cryptoCurrency.network,
|
||||
)
|
||||
.getOrElse {
|
||||
when (it) {
|
||||
is StakingIdFactory.Error.UnableToGetAddress -> raise(StakingError.DomainError("$it"))
|
||||
.getOrElse { error ->
|
||||
when (error) {
|
||||
is StakingIdFactory.Error.UnableToGetAddress -> raise(StakingError.DomainError("$error"))
|
||||
StakingIdFactory.Error.UnsupportedCurrency -> Unit.right()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,10 +4,11 @@ import arrow.core.Either
|
|||
import com.tangem.blockchain.common.Amount
|
||||
import com.tangem.blockchain.common.TransactionData
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.staking.model.stakekit.StakingError
|
||||
import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction
|
||||
import com.tangem.domain.staking.repositories.StakingErrorResolver
|
||||
import com.tangem.domain.staking.repositories.StakeKitRepository
|
||||
import com.tangem.domain.staking.repositories.StakingErrorResolver
|
||||
|
||||
class GetConstructedStakingTransactionUseCase(
|
||||
private val stakeKitRepository: StakeKitRepository,
|
||||
|
|
@ -15,12 +16,17 @@ class GetConstructedStakingTransactionUseCase(
|
|||
) {
|
||||
|
||||
suspend operator fun invoke(
|
||||
networkId: String,
|
||||
networkId: Network.RawID,
|
||||
fee: Fee,
|
||||
amount: Amount,
|
||||
transactionId: String,
|
||||
): Either<StakingError, Pair<StakingTransaction, TransactionData.Compiled>> = Either.catch {
|
||||
stakeKitRepository.constructTransaction(networkId, fee, amount, transactionId)
|
||||
stakeKitRepository.constructTransaction(
|
||||
networkId = networkId,
|
||||
fee = fee,
|
||||
amount = amount,
|
||||
transactionId = transactionId,
|
||||
)
|
||||
}.mapLeft {
|
||||
stakingErrorResolver.resolve(it)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ class InvalidatePendingTransactionsUseCase(
|
|||
type = BalanceType.STAKED,
|
||||
amount = action.amount,
|
||||
rawCurrencyId = null,
|
||||
validatorAddress = action.validatorAddress ?: action.validatorAddresses?.getOrNull(0) ?: "",
|
||||
validatorAddress = action.validatorAddress ?: action.validatorAddresses?.getOrNull(0).orEmpty(),
|
||||
date = null,
|
||||
pendingActions = emptyList(),
|
||||
pendingActionsConstraints = emptyList(),
|
||||
|
|
@ -149,10 +149,10 @@ class InvalidatePendingTransactionsUseCase(
|
|||
}
|
||||
|
||||
private fun findPartialUnstake(balances: MutableList<BalanceItem>, action: StakingAction): Pair<Int, BigDecimal> {
|
||||
val index = balances.indexOfFirst {
|
||||
!it.isPending && action.amount < it.amount &&
|
||||
it.type == BalanceType.STAKED &&
|
||||
it.validatorAddress == action.validatorAddress
|
||||
val index = balances.indexOfFirst { balance ->
|
||||
!balance.isPending && action.amount < balance.amount &&
|
||||
balance.type == BalanceType.STAKED &&
|
||||
balance.validatorAddress == action.validatorAddress
|
||||
}
|
||||
return index to action.amount
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,23 +2,27 @@ package com.tangem.domain.staking
|
|||
|
||||
import arrow.core.Either
|
||||
import arrow.core.raise.either
|
||||
import arrow.core.raise.ensure
|
||||
import arrow.core.raise.ensureNotNull
|
||||
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.models.staking.StakingID
|
||||
import com.tangem.domain.staking.model.StakingIntegrationID
|
||||
import com.tangem.domain.staking.toggles.StakingFeatureToggles
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
|
||||
/**
|
||||
* Factory class for creating instances of [StakingID]
|
||||
*
|
||||
* @property walletManagersFacade wallet manager facade
|
||||
* @property walletManagersFacade wallet manager facade
|
||||
* @property stakingFeatureToggles staking feature toggles
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class StakingIdFactory(
|
||||
private val walletManagersFacade: WalletManagersFacade,
|
||||
private val stakingFeatureToggles: StakingFeatureToggles,
|
||||
) {
|
||||
|
||||
/**
|
||||
|
|
@ -72,6 +76,8 @@ class StakingIdFactory(
|
|||
|
||||
ensureNotNull(integrationId) { Error.UnsupportedCurrency }
|
||||
|
||||
ensure(stakingFeatureToggles.isIntegrationEnabled(integrationId)) { Error.UnsupportedCurrency }
|
||||
|
||||
val address = defaultAddressProvider().takeUnless { it.isNullOrEmpty() }
|
||||
|
||||
ensureNotNull(address) { Error.UnableToGetAddress(integrationId = integrationId) }
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import com.tangem.domain.models.staking.action.StakingActionType
|
|||
|
||||
sealed class StakingAnalyticsEvent(
|
||||
event: String,
|
||||
params: Map<String, String> = mapOf(),
|
||||
params: Map<String, String> = emptyMap(),
|
||||
) : AnalyticsEvent(
|
||||
category = "Staking",
|
||||
event = event,
|
||||
|
|
|
|||
|
|
@ -147,7 +147,7 @@ sealed interface StakingIntegrationID {
|
|||
* @return a [StakingIntegrationID] if supported, or `null` if not supported.
|
||||
*/
|
||||
fun create(currencyId: CryptoCurrency.ID): StakingIntegrationID? {
|
||||
val blockchain = Blockchain.fromId(id = currencyId.rawNetworkId)
|
||||
val blockchain = currencyId.toBlockchain()
|
||||
|
||||
return if (currencyId.contractAddress.isNullOrBlank()) {
|
||||
// Order is not important — either P2PEthPool or Stakekit.Coin can be in any order
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import com.tangem.domain.models.staking.StakingBalance
|
|||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
abstract class MultiStakingBalanceSupplier(
|
||||
open class MultiStakingBalanceSupplier(
|
||||
override val factory: FlowProducer.Factory<MultiStakingBalanceProducer.Params, MultiStakingBalanceProducer>,
|
||||
override val keyCreator: (MultiStakingBalanceProducer.Params) -> String,
|
||||
) : FlowCachingSupplier<MultiStakingBalanceProducer, MultiStakingBalanceProducer.Params, Set<StakingBalance>>()
|
||||
|
|
@ -5,11 +5,11 @@ import com.tangem.blockchain.common.TransactionData
|
|||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.staking.NetworkType
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.staking.model.StakingAvailability
|
||||
import com.tangem.domain.staking.model.StakingEntryInfo
|
||||
import com.tangem.domain.staking.model.StakingIntegrationID
|
||||
import com.tangem.domain.models.staking.NetworkType
|
||||
import com.tangem.domain.staking.model.stakekit.Yield
|
||||
import com.tangem.domain.staking.model.stakekit.action.StakingAction
|
||||
import com.tangem.domain.staking.model.stakekit.action.StakingActionStatus
|
||||
|
|
@ -55,7 +55,7 @@ interface StakeKitRepository {
|
|||
suspend fun estimateGas(userWalletId: UserWalletId, network: Network, params: ActionParams): StakingGasEstimate
|
||||
|
||||
suspend fun constructTransaction(
|
||||
networkId: String,
|
||||
networkId: Network.RawID,
|
||||
fee: Fee,
|
||||
amount: Amount,
|
||||
transactionId: String,
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import com.tangem.domain.models.staking.StakingBalance
|
|||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
abstract class SingleStakingBalanceSupplier(
|
||||
open class SingleStakingBalanceSupplier(
|
||||
override val factory: FlowProducer.Factory<SingleStakingBalanceProducer.Params, SingleStakingBalanceProducer>,
|
||||
override val keyCreator: (SingleStakingBalanceProducer.Params) -> String,
|
||||
) : FlowCachingSupplier<SingleStakingBalanceProducer, SingleStakingBalanceProducer.Params, StakingBalance>()
|
||||
|
|
@ -1,5 +1,8 @@
|
|||
package com.tangem.domain.staking.toggles
|
||||
|
||||
import com.tangem.domain.staking.model.StakingIntegrationID
|
||||
|
||||
interface StakingFeatureToggles {
|
||||
val isEthStakingEnabled: Boolean
|
||||
|
||||
fun isIntegrationEnabled(integrationId: StakingIntegrationID): Boolean
|
||||
}
|
||||
|
|
@ -5,17 +5,16 @@ import arrow.core.left
|
|||
import arrow.core.right
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchainsdk.utils.toNetworkId
|
||||
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.staking.StakingID
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.staking.model.StakingIntegrationID
|
||||
import com.tangem.domain.staking.toggles.StakingFeatureToggles
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.test.core.ProvideTestModels
|
||||
import io.mockk.clearMocks
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.mockk
|
||||
import io.mockk.*
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Nested
|
||||
|
|
@ -30,11 +29,16 @@ import org.junit.jupiter.params.ParameterizedTest
|
|||
internal class StakingIdFactoryTest {
|
||||
|
||||
private val walletManagersFacade: WalletManagersFacade = mockk()
|
||||
private val factory = StakingIdFactory(walletManagersFacade = walletManagersFacade)
|
||||
private val stakingFeatureToggles: StakingFeatureToggles = mockk()
|
||||
private val factory = StakingIdFactory(
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
stakingFeatureToggles = stakingFeatureToggles,
|
||||
)
|
||||
|
||||
@BeforeEach
|
||||
fun resetMocks() {
|
||||
clearMocks(walletManagersFacade)
|
||||
clearMocks(walletManagersFacade, stakingFeatureToggles)
|
||||
every { stakingFeatureToggles.isIntegrationEnabled(any()) } returns true
|
||||
}
|
||||
|
||||
@Nested
|
||||
|
|
@ -66,6 +70,33 @@ internal class StakingIdFactoryTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `create returns UnsupportedCurrency if integration is disabled by toggle`() = runTest {
|
||||
// Arrange
|
||||
val userWalletId = UserWalletId(stringValue = "011")
|
||||
val currency = MockCryptoCurrencyFactory().createCoin(Blockchain.TON)
|
||||
|
||||
every {
|
||||
stakingFeatureToggles.isIntegrationEnabled(StakingIntegrationID.StakeKit.Coin.Ton)
|
||||
} returns false
|
||||
|
||||
// Act
|
||||
val actual = factory.create(
|
||||
userWalletId = userWalletId,
|
||||
currencyId = currency.id,
|
||||
network = currency.network,
|
||||
)
|
||||
|
||||
// Assert
|
||||
val expected = StakingIdFactory.Error.UnsupportedCurrency
|
||||
|
||||
Truth.assertThat(actual.leftOrNull()).isEqualTo(expected)
|
||||
|
||||
coVerify(inverse = true) {
|
||||
walletManagersFacade.getDefaultAddress(userWalletId = any(), network = any())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `create returns UnableToGetAddress if address is null`() = runTest {
|
||||
// Arrange
|
||||
|
|
@ -154,7 +185,7 @@ internal class StakingIdFactoryTest {
|
|||
),
|
||||
CreateModel(
|
||||
currencyId = CryptoCurrency.ID.fromValue(
|
||||
value = "token⟨ETH⟩polygon-ecosystem-token⚓0x7d1afa7b718fb893db30a3abc0cfc608aacfebb0",
|
||||
value = "token⟨ethereum⟩polygon-ecosystem-token⚓0x7d1afa7b718fb893db30a3abc0cfc608aacfebb0",
|
||||
),
|
||||
expected = createStakingId(integrationId = StakingIntegrationID.StakeKit.EthereumToken.Polygon),
|
||||
),
|
||||
|
|
@ -168,6 +199,6 @@ internal class StakingIdFactoryTest {
|
|||
data class CreateModel(val currencyId: CryptoCurrency.ID, val expected: Either<StakingIdFactory.Error, StakingID>)
|
||||
|
||||
private fun createCurrencyId(blockchain: Blockchain): CryptoCurrency.ID {
|
||||
return CryptoCurrency.ID.fromValue(value = "coin⟨${blockchain.id}⟩")
|
||||
return CryptoCurrency.ID.fromValue(value = "coin⟨${blockchain.toNetworkId()}⟩")
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package com.tangem.domain.staking
|
|||
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchainsdk.utils.toNetworkId
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.staking.model.StakingApproval
|
||||
import com.tangem.domain.staking.model.StakingIntegrationID
|
||||
|
|
@ -144,11 +145,11 @@ class StakingIntegrationIDTest {
|
|||
expected = StakingIntegrationID.P2PEthPool,
|
||||
),
|
||||
CreateModel(
|
||||
currencyId = CryptoCurrency.ID.fromValue(value = "token⟨ETH⟩polygon-ecosystem-token⚓1234567890"),
|
||||
currencyId = CryptoCurrency.ID.fromValue(value = "token⟨ethereum⟩polygon-ecosystem-token⚓1234567890"),
|
||||
expected = StakingIntegrationID.StakeKit.EthereumToken.Polygon,
|
||||
),
|
||||
CreateModel(
|
||||
currencyId = CryptoCurrency.ID.fromValue(value = "token⟨SOLANA⟩solana⚓1234567890"),
|
||||
currencyId = CryptoCurrency.ID.fromValue(value = "token⟨solana⟩solana⚓1234567890"),
|
||||
expected = null,
|
||||
),
|
||||
)
|
||||
|
|
@ -157,6 +158,6 @@ class StakingIntegrationIDTest {
|
|||
data class CreateModel(val currencyId: CryptoCurrency.ID, val expected: StakingIntegrationID?)
|
||||
|
||||
private fun createCurrencyId(blockchain: Blockchain): CryptoCurrency.ID {
|
||||
return CryptoCurrency.ID.fromValue(value = "coin⟨${blockchain.id}⟩")
|
||||
return CryptoCurrency.ID.fromValue(value = "coin⟨${blockchain.toNetworkId()}⟩")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package com.tangem.domain.swap.models
|
||||
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
||||
/**
|
||||
* Represents the status of a cryptocurrency in the context of a swap operation.
|
||||
*
|
||||
* Combines the [UserWallet], [CryptoCurrencyStatus], and [Account] to provide
|
||||
* all necessary information about a currency participating in a swap.
|
||||
*
|
||||
* @property userWallet the user wallet that owns the currency
|
||||
* @property status the current status of the cryptocurrency, including balance and value state
|
||||
* @property account the account within the wallet that holds the currency
|
||||
* @property currency shortcut to the [CryptoCurrency] from [status]
|
||||
* @property userWalletId shortcut to the wallet ID from [userWallet]
|
||||
* @property isAvailableForSwap whether this currency can participate in a swap operation,
|
||||
* determined by [RampStateManager][com.tangem.domain.exchange.RampStateManager]
|
||||
*/
|
||||
data class SwapCurrencyStatus(
|
||||
val userWallet: UserWallet,
|
||||
val status: CryptoCurrencyStatus,
|
||||
val account: Account,
|
||||
val isAvailableForSwap: Boolean = true,
|
||||
) {
|
||||
val currency: CryptoCurrency
|
||||
get() = status.currency
|
||||
val userWalletId: UserWalletId
|
||||
get() = userWallet.walletId
|
||||
}
|
||||
|
|
@ -9,7 +9,8 @@ import java.math.BigDecimal
|
|||
* List of saved swap transactions
|
||||
*/
|
||||
data class SwapTransactionListModel(
|
||||
val userWalletId: String,
|
||||
val fromUserWalletId: String,
|
||||
val toUserWalletId: String,
|
||||
val fromCryptoCurrencyId: String,
|
||||
val toCryptoCurrencyId: String,
|
||||
val fromCryptoCurrency: CryptoCurrency,
|
||||
|
|
|
|||
|
|
@ -16,6 +16,21 @@ import java.math.BigDecimal
|
|||
@Suppress("LongParameterList")
|
||||
interface SwapRepositoryV2 {
|
||||
|
||||
/**
|
||||
* Returns express swap pairs for a specific primary and secondary currency.
|
||||
*
|
||||
* @param primarySwapCurrencyStatus primary currency status participating in the swap
|
||||
* @param secondarySwapCurrencyStatus secondary currency status participating in the swap
|
||||
* @param filterProviderTypes filters only specified provider types, if empty returns providers as is
|
||||
* @param swapTxType swap tx type
|
||||
*/
|
||||
suspend fun getPairs(
|
||||
primarySwapCurrencyStatus: SwapCurrencyStatus,
|
||||
secondarySwapCurrencyStatus: SwapCurrencyStatus,
|
||||
filterProviderTypes: List<ExpressProviderType>,
|
||||
swapTxType: SwapTxType,
|
||||
): List<SwapPairModel>
|
||||
|
||||
/**
|
||||
* Express swap pairs, both direct and reversed
|
||||
*
|
||||
|
|
@ -84,7 +99,7 @@ interface SwapRepositoryV2 {
|
|||
userWallet: UserWallet,
|
||||
fromCryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
toCryptoCurrency: CryptoCurrency,
|
||||
amount: String,
|
||||
amount: BigDecimal,
|
||||
amountType: SwapAmountType,
|
||||
toAddress: String,
|
||||
toExtraId: String?,
|
||||
|
|
|
|||
|
|
@ -18,7 +18,8 @@ interface SwapTransactionRepository {
|
|||
/**
|
||||
* Store new swap transaction
|
||||
*
|
||||
* @param userWalletId selected user wallet id
|
||||
* @param fromUserWalletId wallet id swap from
|
||||
* @param toUserWalletId wallet id swap to
|
||||
* @param fromCryptoCurrency currency swap from
|
||||
* @param toCryptoCurrency currency swap to
|
||||
* @param fromAccount account swap from
|
||||
|
|
@ -27,7 +28,8 @@ interface SwapTransactionRepository {
|
|||
*/
|
||||
@Suppress("LongParameterList")
|
||||
suspend fun storeTransaction(
|
||||
userWalletId: UserWalletId,
|
||||
fromUserWalletId: UserWalletId,
|
||||
toUserWalletId: UserWalletId,
|
||||
fromCryptoCurrency: CryptoCurrency,
|
||||
toCryptoCurrency: CryptoCurrency,
|
||||
fromAccount: Account?,
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import com.tangem.domain.swap.SwapErrorResolver
|
|||
import com.tangem.domain.swap.SwapRepositoryV2
|
||||
import com.tangem.domain.swap.models.SwapAmountType
|
||||
import com.tangem.domain.swap.models.SwapDataModel
|
||||
import java.math.BigDecimal
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
class GetSwapDataUseCase(
|
||||
|
|
@ -22,7 +23,7 @@ class GetSwapDataUseCase(
|
|||
suspend operator fun invoke(
|
||||
userWallet: UserWallet,
|
||||
fromCryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
amount: String,
|
||||
amount: BigDecimal,
|
||||
amountType: SwapAmountType,
|
||||
toCryptoCurrency: CryptoCurrency,
|
||||
toAddress: String,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
package com.tangem.domain.swap.usecase
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.domain.express.models.ExpressProviderType
|
||||
import com.tangem.domain.swap.SwapErrorResolver
|
||||
import com.tangem.domain.swap.SwapRepositoryV2
|
||||
import com.tangem.domain.swap.models.SwapCurrencyStatus
|
||||
import com.tangem.domain.swap.models.SwapTxType
|
||||
|
||||
/**
|
||||
* Use case for retrieving swap pairs between a specific primary and secondary currency.
|
||||
*
|
||||
* Returns either a list of available swap pairs or a resolved swap error.
|
||||
*
|
||||
* @property swapRepositoryV2 repository providing swap pair data
|
||||
* @property swapErrorResolver resolver that maps exceptions to domain swap errors
|
||||
*/
|
||||
class GetSwapPairUseCase(
|
||||
private val swapRepositoryV2: SwapRepositoryV2,
|
||||
private val swapErrorResolver: SwapErrorResolver,
|
||||
) {
|
||||
suspend operator fun invoke(
|
||||
primarySwapCurrencyStatus: SwapCurrencyStatus,
|
||||
secondarySwapCurrencyStatus: SwapCurrencyStatus,
|
||||
filterProviderTypes: List<ExpressProviderType>,
|
||||
swapTxType: SwapTxType,
|
||||
) = Either.catch {
|
||||
swapRepositoryV2.getPairs(
|
||||
primarySwapCurrencyStatus = primarySwapCurrencyStatus,
|
||||
secondarySwapCurrencyStatus = secondarySwapCurrencyStatus,
|
||||
filterProviderTypes = filterProviderTypes,
|
||||
swapTxType = swapTxType,
|
||||
)
|
||||
}.mapLeft(swapErrorResolver::resolve)
|
||||
}
|
||||
|
|
@ -2,7 +2,6 @@ package com.tangem.domain.swap.usecase
|
|||
|
||||
import arrow.core.Either
|
||||
import com.tangem.domain.express.models.ExpressProvider
|
||||
import com.tangem.domain.express.models.ExpressProviderType.Companion.shouldStoreSwapTransaction
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
|
|
@ -19,7 +18,8 @@ class SwapTransactionSentUseCase(
|
|||
) {
|
||||
|
||||
suspend operator fun invoke(
|
||||
userWallet: UserWallet,
|
||||
fromUserWallet: UserWallet,
|
||||
toUserWallet: UserWallet,
|
||||
fromCryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
toCryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
fromAccount: Account?,
|
||||
|
|
@ -33,7 +33,8 @@ class SwapTransactionSentUseCase(
|
|||
) = Either.catch {
|
||||
if (provider.type.shouldStoreSwapTransaction()) {
|
||||
swapTransactionRepository.storeTransaction(
|
||||
userWalletId = userWallet.walletId,
|
||||
fromUserWalletId = fromUserWallet.walletId,
|
||||
toUserWalletId = toUserWallet.walletId,
|
||||
fromCryptoCurrency = fromCryptoCurrencyStatus.currency,
|
||||
toCryptoCurrency = toCryptoCurrencyStatus.currency,
|
||||
fromAccount = fromAccount,
|
||||
|
|
@ -58,11 +59,11 @@ class SwapTransactionSentUseCase(
|
|||
}
|
||||
|
||||
swapTransactionRepository.storeLastSwappedCryptoCurrencyId(
|
||||
userWalletId = userWallet.walletId,
|
||||
userWalletId = fromUserWallet.walletId,
|
||||
cryptoCurrencyId = toCryptoCurrencyStatus.currency.id,
|
||||
)
|
||||
swapRepositoryV2.swapTransactionSent(
|
||||
userWallet = userWallet,
|
||||
userWallet = fromUserWallet,
|
||||
fromCryptoCurrencyStatus = fromCryptoCurrencyStatus,
|
||||
payInAddress = payInAddress,
|
||||
txId = swapDataTransactionModel.txId,
|
||||
|
|
|
|||
|
|
@ -56,7 +56,6 @@ dependencies {
|
|||
|
||||
/** Utils */
|
||||
implementation(deps.jodatime)
|
||||
implementation(deps.reKotlin)
|
||||
|
||||
implementation(tangemDeps.blockchain) {
|
||||
exclude(module = "joda-time")
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import com.tangem.core.analytics.models.AnalyticsParam.Key.BLOCKCHAIN
|
|||
import com.tangem.core.analytics.models.AnalyticsParam.Key.ENS
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.SOURCE
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM
|
||||
import com.tangem.core.analytics.models.AppsFlyerIncludedEvent
|
||||
|
||||
sealed class TokenReceiveNewAnalyticsEvent(
|
||||
event: String,
|
||||
|
|
@ -36,7 +37,7 @@ sealed class TokenReceiveNewAnalyticsEvent(
|
|||
BLOCKCHAIN to blockchainName,
|
||||
SOURCE to tokenReceiveSource.name,
|
||||
),
|
||||
)
|
||||
), AppsFlyerIncludedEvent
|
||||
|
||||
class ButtonCopyEns(
|
||||
token: String,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem.domain.tokens.model.warnings
|
||||
|
||||
sealed class DynamicAddressesWarnings : CryptoCurrencyWarning() {
|
||||
|
||||
data object FundsFound : DynamicAddressesWarnings()
|
||||
}
|
||||
|
|
@ -3,14 +3,11 @@ package com.tangem.domain.tokens.actions
|
|||
import com.tangem.domain.exchange.RampStateManager
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.staking.model.StakingAvailability
|
||||
import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason
|
||||
import com.tangem.domain.tokens.model.TokenActionsState.ActionState
|
||||
import com.tangem.domain.transaction.models.AssetRequirementsCondition
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.domain.yield.supply.models.YieldSupplyAvailability
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
|
||||
|
|
@ -65,20 +62,6 @@ internal class CommonActionsFactory(
|
|||
getSendUnavailabilityReason(userWalletId = userWallet.walletId, cryptoCurrencyStatus = cryptoCurrencyStatus)
|
||||
}
|
||||
|
||||
val swapUnavailabilityReason = if (!cryptoCurrencyStatus.currency.isCustom &&
|
||||
cryptoCurrencyStatus.value !is CryptoCurrencyStatus.NoQuote
|
||||
) {
|
||||
async {
|
||||
getSwapUnavailabilityReason(
|
||||
userWalletId = userWallet.walletId,
|
||||
currencyStatus = cryptoCurrencyStatus,
|
||||
requirementsDeferred = requirementsDeferred,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val hideTokenUnavailabilityReason = getTokenHideUnavailabilityReason(userWallet)
|
||||
|
||||
actionAvailabilityBuilder {
|
||||
|
|
@ -111,7 +94,6 @@ internal class CommonActionsFactory(
|
|||
createSwapAction(
|
||||
userWallet = userWallet,
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
swapUnavailableReasonDeferred = swapUnavailabilityReason,
|
||||
shouldShowSwapStories = shouldShowSwapStories,
|
||||
).addByReason()
|
||||
// endregion
|
||||
|
|
@ -140,11 +122,9 @@ internal class CommonActionsFactory(
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("CanBeNonNullable")
|
||||
private suspend fun createSwapAction(
|
||||
private fun createSwapAction(
|
||||
userWallet: UserWallet,
|
||||
cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
swapUnavailableReasonDeferred: Deferred<ScenarioUnavailabilityReason>?,
|
||||
shouldShowSwapStories: Boolean,
|
||||
): ActionState {
|
||||
val cryptoCurrency = cryptoCurrencyStatus.currency
|
||||
|
|
@ -172,35 +152,11 @@ internal class CommonActionsFactory(
|
|||
)
|
||||
}
|
||||
else -> {
|
||||
val reason = requireNotNull(swapUnavailableReasonDeferred) {
|
||||
"swapUnavailableReasonDeferred must not be null for available swap action"
|
||||
}.await()
|
||||
|
||||
return ActionState.Swap(
|
||||
unavailabilityReason = reason,
|
||||
shouldShowBadge = reason == ScenarioUnavailabilityReason.None && shouldShowSwapStories,
|
||||
ActionState.Swap(
|
||||
unavailabilityReason = ScenarioUnavailabilityReason.None,
|
||||
shouldShowBadge = shouldShowSwapStories,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getSwapUnavailabilityReason(
|
||||
userWalletId: UserWalletId,
|
||||
currencyStatus: CryptoCurrencyStatus,
|
||||
requirementsDeferred: Deferred<AssetRequirementsCondition?>?,
|
||||
): ScenarioUnavailabilityReason {
|
||||
val swapUnavailabilityReason = rampStateManager
|
||||
.availableForSwap(userWalletId = userWalletId, cryptoCurrency = currencyStatus.currency)
|
||||
val shouldCheckAssetRequirements =
|
||||
swapUnavailabilityReason == ScenarioUnavailabilityReason.None && requirementsDeferred != null
|
||||
|
||||
val yieldSupplyStatus = currencyStatus.value.yieldSupplyStatus
|
||||
val isUnavailableByYieldSupply = yieldSupplyStatus?.isAllowedToSpend == false && yieldSupplyStatus.isActive
|
||||
|
||||
return when {
|
||||
isUnavailableByYieldSupply -> ScenarioUnavailabilityReason.YieldSupplyApprovalRequired
|
||||
shouldCheckAssetRequirements -> getReceiveScenario(requirementsDeferred.await())
|
||||
else -> swapUnavailabilityReason
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
package com.tangem.domain.tokens.wallet
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.right
|
||||
import com.tangem.domain.card.common.util.cardTypesResolver
|
||||
import com.tangem.domain.common.tokens.CardCryptoCurrencyFactory
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
|
|
@ -147,18 +146,10 @@ class WalletBalanceFetcher internal constructor(
|
|||
|
||||
fetchExpressAssets(userWallet = userWallet, currencies = currencies)
|
||||
|
||||
fetcher.fetch(
|
||||
userWalletId = userWalletId,
|
||||
currencies = currencies,
|
||||
paymentAccountRefactorEnabled = params.isPaymentAccountRefactorEnabled,
|
||||
)
|
||||
fetcher.fetch(userWalletId = userWalletId, currencies = currencies)
|
||||
}
|
||||
|
||||
private suspend fun BaseWalletBalanceFetcher.fetch(
|
||||
userWalletId: UserWalletId,
|
||||
currencies: Set<CryptoCurrency>,
|
||||
paymentAccountRefactorEnabled: Boolean,
|
||||
) {
|
||||
private suspend fun BaseWalletBalanceFetcher.fetch(userWalletId: UserWalletId, currencies: Set<CryptoCurrency>) {
|
||||
coroutineScope {
|
||||
// Fetch balance sources in parallel
|
||||
val balanceErrors = fetchingSources.filterIsInstance<WalletFetchingSource.Balance>()
|
||||
|
|
@ -182,7 +173,7 @@ class WalletBalanceFetcher internal constructor(
|
|||
|
||||
// Fetch TangemPay separately — may run long-polling, so it must not block balance error checking
|
||||
if (fetchingSources.any { it is WalletFetchingSource.TangemPay }) {
|
||||
fetchPaymentAccount(userWalletId, paymentAccountRefactorEnabled)
|
||||
paymentAccountStatusFetcher.invoke(PaymentAccountStatusFetcher.Params(userWalletId))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -190,26 +181,17 @@ class WalletBalanceFetcher internal constructor(
|
|||
private suspend fun fetchExpressAssets(userWallet: UserWallet, currencies: Set<CryptoCurrency>) {
|
||||
val assetIds = currencies.mapTo(hashSetOf()) { currency ->
|
||||
ExpressAsset.ID(
|
||||
networkId = currency.network.backendId,
|
||||
networkId = currency.network.rawId,
|
||||
contractAddress = (currency as? CryptoCurrency.Token)?.contractAddress,
|
||||
)
|
||||
}
|
||||
expressServiceFetcher.fetch(userWallet = userWallet, assetIds = assetIds)
|
||||
}
|
||||
|
||||
private suspend fun fetchPaymentAccount(
|
||||
userWalletId: UserWalletId,
|
||||
paymentAccountRefactorEnabled: Boolean,
|
||||
): Either<Throwable, Unit> {
|
||||
if (!paymentAccountRefactorEnabled) return Unit.right()
|
||||
|
||||
return paymentAccountStatusFetcher.invoke(PaymentAccountStatusFetcher.Params(userWalletId))
|
||||
}
|
||||
|
||||
/**
|
||||
* Params of [WalletBalanceFetcher]
|
||||
*
|
||||
* @property userWalletId user wallet id
|
||||
*/
|
||||
data class Params(val userWalletId: UserWalletId, val isPaymentAccountRefactorEnabled: Boolean)
|
||||
data class Params(val userWalletId: UserWalletId)
|
||||
}
|
||||
|
|
@ -18,7 +18,6 @@ internal object MockNetworks {
|
|||
name = "Network One",
|
||||
isTestnet = false,
|
||||
standardType = Network.StandardType.ERC20,
|
||||
backendId = "network1",
|
||||
currencySymbol = "ETH",
|
||||
derivationPath = Network.DerivationPath.None,
|
||||
hasFiatFeeRate = true,
|
||||
|
|
@ -32,7 +31,6 @@ internal object MockNetworks {
|
|||
name = "Network Two",
|
||||
isTestnet = false,
|
||||
standardType = Network.StandardType.ERC20,
|
||||
backendId = "network1",
|
||||
currencySymbol = "ETH",
|
||||
derivationPath = Network.DerivationPath.None,
|
||||
hasFiatFeeRate = true,
|
||||
|
|
@ -46,7 +44,6 @@ internal object MockNetworks {
|
|||
name = "Network Three",
|
||||
isTestnet = false,
|
||||
standardType = Network.StandardType.ERC20,
|
||||
backendId = "network1",
|
||||
currencySymbol = "ETH",
|
||||
derivationPath = Network.DerivationPath.None,
|
||||
hasFiatFeeRate = true,
|
||||
|
|
|
|||
|
|
@ -96,7 +96,6 @@ internal class WalletBalanceFetcherTest {
|
|||
val actual = fetcher(
|
||||
params = WalletBalanceFetcher.Params(
|
||||
userWalletId = userWalletId,
|
||||
isPaymentAccountRefactorEnabled = false,
|
||||
),
|
||||
)
|
||||
|
||||
|
|
@ -133,7 +132,6 @@ internal class WalletBalanceFetcherTest {
|
|||
val actual = fetcher(
|
||||
params = WalletBalanceFetcher.Params(
|
||||
userWalletId = userWalletId,
|
||||
isPaymentAccountRefactorEnabled = false,
|
||||
),
|
||||
)
|
||||
|
||||
|
|
@ -171,7 +169,6 @@ internal class WalletBalanceFetcherTest {
|
|||
val actual = fetcher(
|
||||
params = WalletBalanceFetcher.Params(
|
||||
userWalletId = userWalletId,
|
||||
isPaymentAccountRefactorEnabled = false,
|
||||
),
|
||||
)
|
||||
|
||||
|
|
@ -209,7 +206,6 @@ internal class WalletBalanceFetcherTest {
|
|||
val actual = fetcher(
|
||||
params = WalletBalanceFetcher.Params(
|
||||
userWalletId = userWalletId,
|
||||
isPaymentAccountRefactorEnabled = false,
|
||||
),
|
||||
)
|
||||
|
||||
|
|
@ -260,7 +256,6 @@ internal class WalletBalanceFetcherTest {
|
|||
val actual = fetcher(
|
||||
params = WalletBalanceFetcher.Params(
|
||||
userWalletId = userWalletId,
|
||||
isPaymentAccountRefactorEnabled = false,
|
||||
),
|
||||
)
|
||||
|
||||
|
|
@ -315,7 +310,6 @@ internal class WalletBalanceFetcherTest {
|
|||
val actual = fetcher(
|
||||
params = WalletBalanceFetcher.Params(
|
||||
userWalletId = userWalletId,
|
||||
isPaymentAccountRefactorEnabled = false,
|
||||
),
|
||||
)
|
||||
|
||||
|
|
@ -376,7 +370,6 @@ internal class WalletBalanceFetcherTest {
|
|||
val actual = fetcher(
|
||||
params = WalletBalanceFetcher.Params(
|
||||
userWalletId = userWalletId,
|
||||
isPaymentAccountRefactorEnabled = false,
|
||||
),
|
||||
)
|
||||
|
||||
|
|
@ -428,7 +421,6 @@ internal class WalletBalanceFetcherTest {
|
|||
val actual = fetcher(
|
||||
params = WalletBalanceFetcher.Params(
|
||||
userWalletId = userWalletId,
|
||||
isPaymentAccountRefactorEnabled = false,
|
||||
),
|
||||
)
|
||||
|
||||
|
|
@ -479,7 +471,6 @@ internal class WalletBalanceFetcherTest {
|
|||
val actual = fetcher(
|
||||
params = WalletBalanceFetcher.Params(
|
||||
userWalletId = userWalletId,
|
||||
isPaymentAccountRefactorEnabled = false,
|
||||
),
|
||||
)
|
||||
|
||||
|
|
@ -536,7 +527,6 @@ internal class WalletBalanceFetcherTest {
|
|||
val actual = fetcher(
|
||||
params = WalletBalanceFetcher.Params(
|
||||
userWalletId = userWalletId,
|
||||
isPaymentAccountRefactorEnabled = false,
|
||||
),
|
||||
)
|
||||
|
||||
|
|
@ -607,7 +597,6 @@ internal class WalletBalanceFetcherTest {
|
|||
val actual = fetcher(
|
||||
params = WalletBalanceFetcher.Params(
|
||||
userWalletId = userWalletId,
|
||||
isPaymentAccountRefactorEnabled = false,
|
||||
),
|
||||
)
|
||||
|
||||
|
|
@ -682,7 +671,6 @@ internal class WalletBalanceFetcherTest {
|
|||
val actual = fetcher(
|
||||
params = WalletBalanceFetcher.Params(
|
||||
userWalletId = userWalletId,
|
||||
isPaymentAccountRefactorEnabled = false,
|
||||
),
|
||||
)
|
||||
|
||||
|
|
@ -743,7 +731,6 @@ internal class WalletBalanceFetcherTest {
|
|||
val actual = fetcher(
|
||||
params = WalletBalanceFetcher.Params(
|
||||
userWalletId = userWalletId,
|
||||
isPaymentAccountRefactorEnabled = false,
|
||||
),
|
||||
)
|
||||
|
||||
|
|
@ -802,7 +789,6 @@ internal class WalletBalanceFetcherTest {
|
|||
val actual = fetcher(
|
||||
params = WalletBalanceFetcher.Params(
|
||||
userWalletId = userWalletId,
|
||||
isPaymentAccountRefactorEnabled = false,
|
||||
),
|
||||
)
|
||||
|
||||
|
|
@ -869,7 +855,6 @@ internal class WalletBalanceFetcherTest {
|
|||
val actual = fetcher(
|
||||
params = WalletBalanceFetcher.Params(
|
||||
userWalletId = userWalletId,
|
||||
isPaymentAccountRefactorEnabled = false,
|
||||
),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,25 +0,0 @@
|
|||
package com.tangem.domain.tokensync.repository
|
||||
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.tokensync.model.TokenSyncProgress
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface TokenSyncRepository {
|
||||
|
||||
suspend fun runSync(userWalletId: UserWalletId)
|
||||
|
||||
suspend fun completeSync(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)
|
||||
}
|
||||
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