Updated on 2026-08-14

This commit is contained in:
Tangem 2025-11-24 12:21:14 +04:00
parent e381fafeb8
commit 7cb4b57fe9
21 changed files with 556 additions and 230 deletions

View file

@ -3,10 +3,13 @@ package com.tangem.tap.di.domain
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.account.fetcher.SingleAccountListFetcher import com.tangem.domain.account.fetcher.SingleAccountListFetcher
import com.tangem.domain.account.repository.AccountsCRUDRepository import com.tangem.domain.account.repository.AccountsCRUDRepository
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
import com.tangem.domain.account.status.usecase.ArchiveCryptoPortfolioUseCase
import com.tangem.domain.account.status.usecase.RecoverCryptoPortfolioUseCase import com.tangem.domain.account.status.usecase.RecoverCryptoPortfolioUseCase
import com.tangem.domain.account.status.utils.CryptoCurrencyBalanceFetcher import com.tangem.domain.account.status.utils.CryptoCurrencyBalanceFetcher
import com.tangem.domain.account.tokens.MainAccountTokensMigration import com.tangem.domain.account.tokens.MainAccountTokensMigration
import com.tangem.domain.account.usecase.* import com.tangem.domain.account.usecase.*
import com.tangem.feature.referral.data.ExternalReferralRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
@ -43,9 +46,15 @@ internal object AccountDomainModule {
@Provides @Provides
@Singleton @Singleton
fun provideArchiveCryptoPortfolioUseCase( fun provideArchiveCryptoPortfolioUseCase(
singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
accountsCRUDRepository: AccountsCRUDRepository, accountsCRUDRepository: AccountsCRUDRepository,
referralRepository: ExternalReferralRepository,
): ArchiveCryptoPortfolioUseCase { ): ArchiveCryptoPortfolioUseCase {
return ArchiveCryptoPortfolioUseCase(crudRepository = accountsCRUDRepository) return ArchiveCryptoPortfolioUseCase(
singleAccountStatusListSupplier = singleAccountStatusListSupplier,
crudRepository = accountsCRUDRepository,
referralRepository = referralRepository,
)
} }
@Provides @Provides

View file

@ -1,5 +1,6 @@
package com.tangem.domain.account.models package com.tangem.domain.account.models
import arrow.core.Either
import com.tangem.domain.models.TokensGroupType import com.tangem.domain.models.TokensGroupType
import com.tangem.domain.models.TokensSortType import com.tangem.domain.models.TokensSortType
import com.tangem.domain.models.TotalFiatBalance import com.tangem.domain.models.TotalFiatBalance
@ -40,4 +41,14 @@ data class AccountStatusList(
fun flattenCurrencies(): List<CryptoCurrencyStatus> = accountStatuses fun flattenCurrencies(): List<CryptoCurrencyStatus> = accountStatuses
.map { accountStatus -> accountStatus.flattenCurrencies() } .map { accountStatus -> accountStatus.flattenCurrencies() }
.flatten() .flatten()
fun toAccountList(): Either<AccountList.Error, AccountList> {
return AccountList(
userWalletId = userWalletId,
accounts = accountStatuses.map(AccountStatus::account),
totalAccounts = totalAccounts,
sortType = sortType,
groupType = groupType,
)
}
} }

View file

@ -1,152 +0,0 @@
package com.tangem.domain.account.usecase
import arrow.core.None
import arrow.core.left
import arrow.core.right
import arrow.core.toOption
import com.google.common.truth.Truth
import com.tangem.domain.account.models.AccountList
import com.tangem.domain.account.repository.AccountsCRUDRepository
import com.tangem.domain.account.usecase.ArchiveCryptoPortfolioUseCase.Error
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.account.DerivationIndex
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.test.mock.MockAccounts.createAccount
import io.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)
class ArchiveCryptoPortfolioUseCaseTest {
private val crudRepository: AccountsCRUDRepository = mockk(relaxUnitFun = true)
private val useCase = ArchiveCryptoPortfolioUseCase(crudRepository)
@BeforeEach
fun resetMocks() {
clearMocks(crudRepository)
}
@Test
fun `invoke should archive existing crypto portfolio account`() = runTest {
// Arrange
val account = createAccount(derivationIndex = 1)
val accountList = (AccountList.empty(userWalletId) + account).getOrNull()!!
val accountId = account.accountId
val updatedAccountList = (accountList - account).getOrNull()!!
coEvery { crudRepository.getAccountListSync(userWalletId) } returns accountList.toOption()
// Act
val actual = useCase(accountId)
// Assert
val expected = Unit.right()
Truth.assertThat(actual).isEqualTo(expected)
coVerifyOrder {
crudRepository.getAccountListSync(userWalletId)
crudRepository.saveAccounts(updatedAccountList)
}
}
@Test
fun `invoke should return error if getAccounts returns None`() = runTest {
// Arrange
val accountId = AccountId.forCryptoPortfolio(
userWalletId = userWalletId,
derivationIndex = DerivationIndex.Main,
)
coEvery { crudRepository.getAccountListSync(userWalletId) } returns None
// Act
val actual = useCase(accountId)
// Assert
val expected = Error.CriticalTechError.AccountsNotCreated(userWalletId).left()
Truth.assertThat(actual).isEqualTo(expected)
coVerifyOrder { crudRepository.getAccountListSync(userWalletId) }
coVerify(inverse = true) { crudRepository.saveAccounts(any()) }
}
@Test
fun `invoke should return error if getAccounts throws exception`() = runTest {
// Arrange
val accountId = AccountId.forCryptoPortfolio(
userWalletId = userWalletId,
derivationIndex = DerivationIndex.Main,
)
val exception = IllegalStateException("Test error")
coEvery { crudRepository.getAccountListSync(userWalletId) } throws exception
// Act
val actual = useCase(accountId)
// Assert
val expected = Error.DataOperationFailed(exception).left()
Truth.assertThat(actual).isEqualTo(expected)
coVerifyOrder { crudRepository.getAccountListSync(userWalletId) }
coVerify(inverse = true) { crudRepository.saveAccounts(any()) }
}
@Test
fun `invoke should return error if account not found`() = runTest {
// Arrange
val accountList = AccountList.empty(userWalletId)
val accountId = AccountId.forCryptoPortfolio(
userWalletId = userWalletId,
derivationIndex = DerivationIndex(1).getOrNull()!!,
)
coEvery { crudRepository.getAccountListSync(userWalletId) } returns accountList.toOption()
// Act
val actual = useCase(accountId)
// Assert
val expected = Error.CriticalTechError.AccountNotFound(accountId).left()
Truth.assertThat(actual).isEqualTo(expected)
coVerifyOrder { crudRepository.getAccountListSync(userWalletId) }
coVerify(inverse = true) { crudRepository.saveAccounts(any()) }
}
@Test
fun `invoke should return error if saveAccounts throws exception`() = runTest {
// Arrange
val account = createAccount(derivationIndex = 1)
val accountList = (AccountList.empty(userWalletId) + account).getOrNull()!!
val accountId = account.accountId
val updatedAccountList = (accountList - account).getOrNull()!!
val exception = IllegalStateException("Save failed")
coEvery { crudRepository.getAccountListSync(userWalletId) } returns accountList.toOption()
coEvery { crudRepository.saveAccounts(updatedAccountList) } throws exception
// Act
val actual = useCase(accountId)
// Assert
val expected = Error.DataOperationFailed(exception).left()
Truth.assertThat(actual).isEqualTo(expected)
coVerifyOrder {
crudRepository.getAccountListSync(userWalletId)
crudRepository.saveAccounts(updatedAccountList)
}
}
private companion object {
val userWalletId = UserWalletId("011")
}
}

View file

@ -23,6 +23,7 @@ dependencies {
api(projects.domain.quotes) api(projects.domain.quotes)
api(projects.domain.models) api(projects.domain.models)
api(projects.domain.networks) api(projects.domain.networks)
api(projects.domain.referral)
api(projects.domain.staking) api(projects.domain.staking)
api(projects.domain.tokens) api(projects.domain.tokens)
api(projects.domain.wallets) api(projects.domain.wallets)
@ -42,7 +43,7 @@ dependencies {
// end // end
testRuntimeOnly(deps.test.junit5.engine) testRuntimeOnly(deps.test.junit5.engine)
testImplementation(tangemDeps.blockchain)
testImplementation(projects.common.test) testImplementation(projects.common.test)
testImplementation(projects.test.core) testImplementation(projects.test.core)
testImplementation(projects.test.mock)
} }

View file

@ -91,7 +91,7 @@ internal object AccountStatusUseCaseModule {
@Provides @Provides
@Singleton @Singleton
fun provideManageCryptoCurrenciesUseCase( fun provideManageCryptoCurrenciesUseCase(
singleAccountListSupplier: SingleAccountListSupplier, singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
accountsCRUDRepository: AccountsCRUDRepository, accountsCRUDRepository: AccountsCRUDRepository,
currenciesRepository: CurrenciesRepository, currenciesRepository: CurrenciesRepository,
derivationsRepository: DerivationsRepository, derivationsRepository: DerivationsRepository,
@ -103,7 +103,7 @@ internal object AccountStatusUseCaseModule {
dispatchers: CoroutineDispatcherProvider, dispatchers: CoroutineDispatcherProvider,
): ManageCryptoCurrenciesUseCase { ): ManageCryptoCurrenciesUseCase {
return ManageCryptoCurrenciesUseCase( return ManageCryptoCurrenciesUseCase(
singleAccountListSupplier = singleAccountListSupplier, singleAccountStatusListSupplier = singleAccountStatusListSupplier,
accountsCRUDRepository = accountsCRUDRepository, accountsCRUDRepository = accountsCRUDRepository,
currenciesRepository = currenciesRepository, currenciesRepository = currenciesRepository,
derivationsRepository = derivationsRepository, derivationsRepository = derivationsRepository,

View file

@ -1,31 +1,48 @@
package com.tangem.domain.account.usecase package com.tangem.domain.account.status.usecase
import arrow.core.Either import arrow.core.Either
import arrow.core.getOrElse import arrow.core.getOrElse
import arrow.core.raise.Raise import arrow.core.raise.*
import arrow.core.raise.catch
import arrow.core.raise.either
import com.tangem.domain.account.models.AccountList import com.tangem.domain.account.models.AccountList
import com.tangem.domain.account.models.AccountStatusList
import com.tangem.domain.account.repository.AccountsCRUDRepository import com.tangem.domain.account.repository.AccountsCRUDRepository
import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.referral.ReferralRepository
/** /**
* Use case for archiving a crypto portfolio. * Use case for archiving a crypto portfolio.
* This class provides functionality to archive a specific account within a user's crypto portfolio. * This class provides functionality to archive a specific account within a user's crypto portfolio.
* It ensures that the account exists and meets the necessary requirements before performing the operation. * It ensures that the account exists and meets the necessary requirements before performing the operation.
* *
* @property singleAccountStatusListSupplier supplier to get the list of account statuses
* @property crudRepository repository for performing CRUD operations on accounts * @property crudRepository repository for performing CRUD operations on accounts
* @property referralRepository repository for handling referral status checks
* *
[REDACTED_AUTHOR] [REDACTED_AUTHOR]
*/ */
class ArchiveCryptoPortfolioUseCase( class ArchiveCryptoPortfolioUseCase(
private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
private val crudRepository: AccountsCRUDRepository, private val crudRepository: AccountsCRUDRepository,
private val referralRepository: ReferralRepository,
) { ) {
/** Archives the specified account by its [accountId] */ /** Archives the specified account by its [accountId] */
suspend operator fun invoke(accountId: AccountId): Either<Error, Unit> = either { suspend operator fun invoke(accountId: AccountId): Either<Error, Unit> = either {
val accountList = getAccountList(userWalletId = accountId.userWalletId) val accountStatusList = getAccountStatusList(userWalletId = accountId.userWalletId)
val accountList = accountStatusList.toAccountList().getOrElse {
raise(Error.CriticalTechError.AccountListRequirementsNotMet(cause = it))
}
ensure(accountList.accounts.any { it.accountId == accountId }) {
Error.CriticalTechError.AccountNotFound(accountId = accountId)
}
checkReferralStatus(accountStatusList = accountStatusList, accountId = accountId)
val archivingAccount = accountList.accounts val archivingAccount = accountList.accounts
.firstOrNull { it.accountId == accountId } .firstOrNull { it.accountId == accountId }
@ -38,16 +55,48 @@ class ArchiveCryptoPortfolioUseCase(
saveAccounts(updatedAccounts) saveAccounts(updatedAccounts)
} }
private suspend fun Raise<Error>.getAccountList(userWalletId: UserWalletId): AccountList { private suspend fun Raise<Error>.checkReferralStatus(accountStatusList: AccountStatusList, accountId: AccountId) {
return catch( val referralStatus = catch(
block = { crudRepository.getAccountListSync(userWalletId = userWalletId) }, block = { referralRepository.getReferralStatus(userWalletId = accountStatusList.userWalletId.stringValue) },
catch = { raise(Error.DataOperationFailed(cause = it)) }, catch = { raise(Error.DataOperationFailed(cause = it)) },
) )
.getOrElse { raise(Error.CriticalTechError.AccountsNotCreated(userWalletId = userWalletId)) }
val referralToken = referralStatus.token
val address = referralStatus.address
if (!referralStatus.isActive || referralToken == null || address == null) return
val account = accountStatusList.accountStatuses
.asSequence()
.filterIsInstance<AccountStatus.CryptoPortfolio>()
.firstOrNull { it.accountId == accountId }
ensureNotNull(account) {
Error.CriticalTechError.AccountNotFound(accountId = accountId)
}
val statuses = account.flattenCurrencies()
val hasNotReferralToken = statuses.none { status ->
val currency = status.currency
currency.network.backendId == referralToken.networkId &&
(currency as? CryptoCurrency.Token)?.contractAddress == referralToken.contractAddress &&
status.value.networkAddress?.availableAddresses?.any { it.value == address } == true
}
ensure(hasNotReferralToken) { Error.ActiveReferralStatus }
}
private suspend fun Raise<Error>.getAccountStatusList(userWalletId: UserWalletId): AccountStatusList {
return singleAccountStatusListSupplier.getSyncOrNull(
SingleAccountStatusListProducer.Params(userWalletId = userWalletId),
)
?: raise(Error.CriticalTechError.AccountsNotCreated(userWalletId = userWalletId))
} }
private suspend fun Raise<Error>.saveAccounts(accountList: AccountList) { private suspend fun Raise<Error>.saveAccounts(accountList: AccountList) {
catch( arrow.core.raise.catch(
block = { crudRepository.saveAccounts(accountList) }, block = { crudRepository.saveAccounts(accountList) },
catch = { raise(Error.DataOperationFailed(cause = it)) }, catch = { raise(Error.DataOperationFailed(cause = it)) },
) )
@ -58,9 +107,14 @@ class ArchiveCryptoPortfolioUseCase(
*/ */
sealed interface Error { sealed interface Error {
val tag: String
get() = this.javaClass.simpleName
data object ActiveReferralStatus : Error
/** Error indicating that a data operation failed */ /** Error indicating that a data operation failed */
data class DataOperationFailed(val cause: Throwable) : Error { data class DataOperationFailed(val cause: Throwable) : Error {
override fun toString(): String = "$this: Data operation failed: ${cause.message ?: "Unknown error"}" override fun toString(): String = "$tag: Data operation failed: ${cause.message ?: "Unknown error"}"
} }
/** /**

View file

@ -82,6 +82,7 @@ class GetAccountCurrencyByAddressUseCase(
for (id in userWalletIds) { for (id in userWalletIds) {
val networkStatus = multiNetworkStatusSupplier.getSyncOrNull( val networkStatus = multiNetworkStatusSupplier.getSyncOrNull(
params = MultiNetworkStatusProducer.Params(userWalletId = id), params = MultiNetworkStatusProducer.Params(userWalletId = id),
timeMillis = 1000L,
) )
?.firstOrNull { it.getAddress() == address } ?.firstOrNull { it.getAddress() == address }

View file

@ -3,16 +3,18 @@ package com.tangem.domain.account.status.usecase
import arrow.core.Either import arrow.core.Either
import arrow.core.raise.Raise import arrow.core.raise.Raise
import arrow.core.raise.catch import arrow.core.raise.catch
import com.tangem.domain.account.producer.SingleAccountListProducer
import com.tangem.domain.account.repository.AccountsCRUDRepository import com.tangem.domain.account.repository.AccountsCRUDRepository
import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
import com.tangem.domain.account.status.utils.CryptoCurrencyBalanceFetcher import com.tangem.domain.account.status.utils.CryptoCurrencyBalanceFetcher
import com.tangem.domain.account.supplier.SingleAccountListSupplier
import com.tangem.domain.core.utils.eitherOn import com.tangem.domain.core.utils.eitherOn
import com.tangem.domain.express.ExpressServiceFetcher import com.tangem.domain.express.ExpressServiceFetcher
import com.tangem.domain.express.models.ExpressAsset import com.tangem.domain.express.models.ExpressAsset
import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.network.Network import com.tangem.domain.models.network.Network
import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.networks.utils.NetworksCleaner import com.tangem.domain.networks.utils.NetworksCleaner
@ -27,7 +29,7 @@ import timber.log.Timber
/** /**
* Use case for saving crypto currencies to a specific account. * Use case for saving crypto currencies to a specific account.
* *
* @property singleAccountListSupplier Supplier to get account details. * @property singleAccountStatusListSupplier Supplier for fetching the status of a single account.
* @property accountsCRUDRepository Repository for performing CRUD operations on accounts. * @property accountsCRUDRepository Repository for performing CRUD operations on accounts.
* @property currenciesRepository Repository for managing currencies. * @property currenciesRepository Repository for managing currencies.
* @property derivationsRepository Repository for deriving public keys. * @property derivationsRepository Repository for deriving public keys.
@ -43,7 +45,7 @@ import timber.log.Timber
*/ */
@Suppress("LongParameterList") @Suppress("LongParameterList")
class ManageCryptoCurrenciesUseCase( class ManageCryptoCurrenciesUseCase(
private val singleAccountListSupplier: SingleAccountListSupplier, private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
private val accountsCRUDRepository: AccountsCRUDRepository, private val accountsCRUDRepository: AccountsCRUDRepository,
private val currenciesRepository: CurrenciesRepository, private val currenciesRepository: CurrenciesRepository,
private val derivationsRepository: DerivationsRepository, private val derivationsRepository: DerivationsRepository,
@ -76,12 +78,13 @@ class ManageCryptoCurrenciesUseCase(
val userWalletId = accountId.userWalletId val userWalletId = accountId.userWalletId
withContext(NonCancellable) { withContext(NonCancellable) {
val account = getAccount(accountId = accountId) val accountStatus = getAccountStatus(accountId = accountId)
val modifiedCurrencyList = account.cryptoCurrencies.modify(add = add, remove = remove) val modifiedCurrencyList = accountStatus.tokenList.flattenCurrencies()
.modify(add = add, remove = remove)
saveAccount( saveAccount(
account = account.copy(cryptoCurrencies = modifiedCurrencyList.total.toSet()), account = accountStatus.account.copy(cryptoCurrencies = modifiedCurrencyList.total.toSet()),
) )
val isDerivingFailed = derivePublicKeys( val isDerivingFailed = derivePublicKeys(
@ -117,10 +120,10 @@ class ManageCryptoCurrenciesUseCase(
val userWalletId = accountId.userWalletId val userWalletId = accountId.userWalletId
withContext(NonCancellable) { withContext(NonCancellable) {
val account = getAccount(accountId = accountId) val accountStatus = getAccountStatus(accountId = accountId)
val foundToken = account.cryptoCurrencies val foundToken = accountStatus.tokenList.flattenCurrencies()
.filterIsInstance<CryptoCurrency.Token>() .mapNotNull { it.currency as? CryptoCurrency.Token }
.firstOrNull { .firstOrNull {
it.network.backendId == networkId && it.network.backendId == networkId &&
!it.isCustom && !it.isCustom &&
@ -131,9 +134,10 @@ class ManageCryptoCurrenciesUseCase(
val tokenToAdd = findToken(userWalletId, contractAddress, networkId) val tokenToAdd = findToken(userWalletId, contractAddress, networkId)
val modifiedCurrencyList = account.cryptoCurrencies.modify(add = listOf(tokenToAdd)) val modifiedCurrencyList = accountStatus.tokenList.flattenCurrencies()
.modify(add = listOf(tokenToAdd))
saveAccount(account = account.copy(cryptoCurrencies = modifiedCurrencyList.total.toSet())) saveAccount(account = accountStatus.account.copy(cryptoCurrencies = modifiedCurrencyList.total.toSet()))
parallelUpdatingScope.launch { parallelUpdatingScope.launch {
cryptoCurrencyBalanceFetcher(userWalletId = userWalletId, currencies = listOf(tokenToAdd)) cryptoCurrencyBalanceFetcher(userWalletId = userWalletId, currencies = listOf(tokenToAdd))
@ -144,26 +148,31 @@ class ManageCryptoCurrenciesUseCase(
} }
} }
private suspend fun Raise<Throwable>.getAccount(accountId: AccountId): Account.CryptoPortfolio { private suspend fun Raise<Throwable>.getAccountStatus(accountId: AccountId): AccountStatus.CryptoPortfolio {
val accountList = singleAccountListSupplier.getSyncOrNull( val accountStatusList = singleAccountStatusListSupplier.getSyncOrNull(
params = SingleAccountListProducer.Params(userWalletId = accountId.userWalletId), params = SingleAccountStatusListProducer.Params(userWalletId = accountId.userWalletId),
) ?: raise(IllegalStateException("No accounts for wallet ${accountId.userWalletId}")) ) ?: raise(IllegalStateException("No accounts for wallet ${accountId.userWalletId}"))
return accountList.accounts.firstOrNull { it.accountId == accountId } as? Account.CryptoPortfolio return accountStatusList.accountStatuses
.firstOrNull { it.accountId == accountId } as? AccountStatus.CryptoPortfolio
?: raise(IllegalStateException("No account with id $accountId")) ?: raise(IllegalStateException("No account with id $accountId"))
} }
private fun Set<CryptoCurrency>.modify( private fun List<CryptoCurrencyStatus>.modify(
add: List<CryptoCurrency>, add: List<CryptoCurrency>,
remove: List<CryptoCurrency> = emptyList(), remove: List<CryptoCurrency> = emptyList(),
): ModifiedCurrencyList { ): ModifiedCurrencyList {
val mutableCurrencies = this.toMutableList() val mutableCurrencies = this.map(CryptoCurrencyStatus::currency).toMutableList()
val added = mutableListOf<CryptoCurrency>() val added = mutableListOf<CryptoCurrency>()
val removed = mutableListOf<CryptoCurrency>() val removed = mutableListOf<CryptoCurrency>()
val existingCurrenciesById = mutableCurrencies.associateBy(::TempID) val existingCurrenciesById = this.associateBy(::TempID)
add.groupByNetwork { !existingCurrenciesById.containsKey(it) } add.groupByNetwork { tempID ->
val found = existingCurrenciesById[tempID] ?: return@groupByNetwork true
found.value is CryptoCurrencyStatus.MissedDerivation
}
.forEach { (network, currenciesById) -> .forEach { (network, currenciesById) ->
val coinTempId = TempID(network) val coinTempId = TempID(network)
@ -204,20 +213,6 @@ class ManageCryptoCurrenciesUseCase(
return ModifiedCurrencyList(added = added, removed = removed, total = mutableCurrencies) return ModifiedCurrencyList(added = added, removed = removed, total = mutableCurrencies)
} }
private suspend fun Raise<Throwable>.saveAccount(account: Account.CryptoPortfolio) {
catch(
block = { accountsCRUDRepository.saveAccount(account) },
catch = ::raise,
)
}
private suspend fun derivePublicKeys(
userWalletId: UserWalletId,
currencies: List<CryptoCurrency>,
): Either<Throwable, Unit> = Either.catch {
derivationsRepository.derivePublicKeys(userWalletId = userWalletId, currencies = currencies)
}
private fun List<CryptoCurrency>.groupByNetwork( private fun List<CryptoCurrency>.groupByNetwork(
valuePredicate: (TempID) -> Boolean, valuePredicate: (TempID) -> Boolean,
): LinkedHashMap<Network, MutableMap<TempID, CryptoCurrency>> { ): LinkedHashMap<Network, MutableMap<TempID, CryptoCurrency>> {
@ -230,13 +225,27 @@ class ManageCryptoCurrenciesUseCase(
val id = TempID(currency) val id = TempID(currency)
if (valuePredicate(id)) { if (valuePredicate(id)) {
mutableMap.put(id, currency) mutableMap[id] = currency
} }
} }
return destination return destination
} }
private suspend fun Raise<Throwable>.saveAccount(account: Account.CryptoPortfolio) {
catch(
block = { accountsCRUDRepository.saveAccount(account) },
catch = ::raise,
)
}
private suspend fun derivePublicKeys(
userWalletId: UserWalletId,
currencies: List<CryptoCurrency>,
): Either<Throwable, Unit> = Either.catch {
derivationsRepository.derivePublicKeys(userWalletId = userWalletId, currencies = currencies)
}
private suspend fun Raise<Throwable>.findToken( private suspend fun Raise<Throwable>.findToken(
userWalletId: UserWalletId, userWalletId: UserWalletId,
networkId: String, networkId: String,
@ -307,6 +316,12 @@ class ManageCryptoCurrenciesUseCase(
derivationPath = currency.network.derivationPath, derivationPath = currency.network.derivationPath,
contractAddress = (currency as? CryptoCurrency.Token)?.contractAddress, contractAddress = (currency as? CryptoCurrency.Token)?.contractAddress,
) )
constructor(status: CryptoCurrencyStatus) : this(
networkId = status.currency.network.backendId,
derivationPath = status.currency.network.derivationPath,
contractAddress = (status.currency as? CryptoCurrency.Token)?.contractAddress,
)
} }
private data class ModifiedCurrencyList( private data class ModifiedCurrencyList(

View file

@ -0,0 +1,307 @@
package com.tangem.domain.account.status.usecase
import arrow.core.left
import arrow.core.right
import com.google.common.truth.Truth
import com.tangem.domain.account.models.AccountList
import com.tangem.domain.account.models.AccountStatusList
import com.tangem.domain.account.repository.AccountsCRUDRepository
import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
import com.tangem.domain.account.status.usecase.ArchiveCryptoPortfolioUseCase.Error
import com.tangem.domain.core.utils.lceError
import com.tangem.domain.models.TokensSortType
import com.tangem.domain.models.TotalFiatBalance
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.account.DerivationIndex
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.network.NetworkAddress
import com.tangem.domain.models.tokenlist.TokenList
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.referral.ReferralRepository
import com.tangem.domain.referral.ReferralStatus
import com.tangem.test.mock.MockAccounts.createAccountList
import io.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)
class ArchiveCryptoPortfolioUseCaseTest {
private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier = mockk()
private val crudRepository: AccountsCRUDRepository = mockk(relaxUnitFun = true)
private val referralRepository: ReferralRepository = mockk(relaxUnitFun = true)
private val useCase = ArchiveCryptoPortfolioUseCase(
singleAccountStatusListSupplier = singleAccountStatusListSupplier,
crudRepository = crudRepository,
referralRepository = referralRepository,
)
@BeforeEach
fun resetMocks() {
clearMocks(singleAccountStatusListSupplier, referralRepository, crudRepository)
}
@Test
fun `invoke should archive existing crypto portfolio account`() = runTest {
// Arrange
val accountList = createAccountList(activeAccounts = 2)
val archivingAccount = accountList.accounts.last()
val updatedAccountList = (accountList - archivingAccount).getOrNull()!!
val params = SingleAccountStatusListProducer.Params(userWalletId)
coEvery { singleAccountStatusListSupplier.getSyncOrNull(params) } returns accountList.toStatus()
val referralStatus = ReferralStatus(isActive = false, token = null, address = null)
coEvery { referralRepository.getReferralStatus(userWalletId.stringValue) } returns referralStatus
// Act
val actual = useCase(accountId = archivingAccount.accountId)
// Assert
val expected = Unit.right()
Truth.assertThat(actual).isEqualTo(expected)
coVerifyOrder {
singleAccountStatusListSupplier.getSyncOrNull(params)
referralRepository.getReferralStatus(userWalletId.stringValue)
crudRepository.saveAccounts(updatedAccountList)
}
}
@Test
fun `invoke should archive if referral status is active but token is null`() = runTest {
// Arrange
val accountList = createAccountList(activeAccounts = 2)
val archivingAccount = accountList.accounts.last()
val updatedAccountList = (accountList - archivingAccount).getOrNull()!!
val params = SingleAccountStatusListProducer.Params(userWalletId)
coEvery { singleAccountStatusListSupplier.getSyncOrNull(params) } returns accountList.toStatus()
val referralStatus = ReferralStatus(isActive = true, token = null, address = null)
coEvery { referralRepository.getReferralStatus(userWalletId.stringValue) } returns referralStatus
// Act
val actual = useCase(accountId = archivingAccount.accountId)
// Assert
val expected = Unit.right()
Truth.assertThat(actual).isEqualTo(expected)
coVerifyOrder {
singleAccountStatusListSupplier.getSyncOrNull(params)
referralRepository.getReferralStatus(userWalletId.stringValue)
crudRepository.saveAccounts(updatedAccountList)
}
}
@Test
fun `invoke should archive if referral status is active but token is absent`() = runTest {
// Arrange
val accountList = createAccountList(activeAccounts = 2)
val archivingAccount = accountList.accounts.last()
val updatedAccountList = (accountList - archivingAccount).getOrNull()!!
val params = SingleAccountStatusListProducer.Params(userWalletId)
coEvery { singleAccountStatusListSupplier.getSyncOrNull(params) } returns accountList.toStatus()
val token = ReferralStatus.Token(
networkId = "ethereum",
contractAddress = "0x1",
)
val referralStatus = ReferralStatus(isActive = true, token = token, address = "0xABC")
coEvery { referralRepository.getReferralStatus(userWalletId.stringValue) } returns referralStatus
// Act
val actual = useCase(accountId = archivingAccount.accountId)
// Assert
val expected = Unit.right()
Truth.assertThat(actual).isEqualTo(expected)
coVerifyOrder {
singleAccountStatusListSupplier.getSyncOrNull(params)
referralRepository.getReferralStatus(userWalletId.stringValue)
crudRepository.saveAccounts(updatedAccountList)
}
}
@Test
fun `invoke returns error if account contains referral token `() = runTest {
// Arrange
val token = ReferralStatus.Token(
networkId = "ethereum",
contractAddress = "0x1",
)
val defaultAddress = "0xABC"
val cryptoCurrency = mockk<CryptoCurrency.Token> {
every { this@mockk.network.backendId } returns token.networkId
every { this@mockk.contractAddress } returns token.contractAddress!!
}
val networkAddress = NetworkAddress.Single(
defaultAddress = NetworkAddress.Address(
value = defaultAddress,
type = NetworkAddress.Address.Type.Primary,
),
)
val cryptoCurrencyStatus = mockk<CryptoCurrencyStatus> {
every { this@mockk.currency } returns cryptoCurrency
every { this@mockk.value.networkAddress } returns networkAddress
}
val accountList = createAccountList(activeAccounts = 2)
val archivingAccount = accountList.accounts.last()
val archivingAccountStatus = AccountStatus.CryptoPortfolio(
account = archivingAccount as Account.CryptoPortfolio,
tokenList = TokenList.Ungrouped(
totalFiatBalance = TotalFiatBalance.Failed,
sortedBy = TokensSortType.NONE,
currencies = listOf(cryptoCurrencyStatus),
),
priceChangeLce = Unit.lceError(),
)
val accountListStatus = with(accountList.toStatus()) {
copy(
accountStatuses = accountStatuses.filter { it.accountId != archivingAccount.accountId } +
archivingAccountStatus,
)
}
(accountList - archivingAccount).getOrNull()!!
val params = SingleAccountStatusListProducer.Params(userWalletId)
coEvery { singleAccountStatusListSupplier.getSyncOrNull(params) } returns accountListStatus
val referralStatus = ReferralStatus(isActive = true, token = token, address = defaultAddress)
coEvery { referralRepository.getReferralStatus(userWalletId.stringValue) } returns referralStatus
// Act
val actual = useCase(accountId = archivingAccount.accountId)
// Assert
val expected = Error.ActiveReferralStatus.left()
Truth.assertThat(actual).isEqualTo(expected)
coVerifyOrder {
singleAccountStatusListSupplier.getSyncOrNull(params)
referralRepository.getReferralStatus(userWalletId.stringValue)
}
coVerify(inverse = true) { crudRepository.saveAccounts(any()) }
}
@Test
fun `invoke should return error if supplier returns null`() = runTest {
// Arrange
val accountId = AccountId.forCryptoPortfolio(
userWalletId = userWalletId,
derivationIndex = DerivationIndex(1).getOrNull()!!,
)
val params = SingleAccountStatusListProducer.Params(userWalletId)
coEvery { singleAccountStatusListSupplier.getSyncOrNull(params) } returns null
// Act
val actual = useCase(accountId)
// Assert
val expected = Error.CriticalTechError.AccountsNotCreated(userWalletId).left()
Truth.assertThat(actual).isEqualTo(expected)
coVerifyOrder { singleAccountStatusListSupplier.getSyncOrNull(params) }
coVerify(inverse = true) {
referralRepository.getReferralStatus(any())
crudRepository.saveAccounts(any())
}
}
@Test
fun `invoke should return error if account not found`() = runTest {
// Arrange
val accountList = AccountList.empty(userWalletId)
val accountId = AccountId.forCryptoPortfolio(
userWalletId = userWalletId,
derivationIndex = DerivationIndex.Companion(1).getOrNull()!!,
)
val params = SingleAccountStatusListProducer.Params(userWalletId)
coEvery { singleAccountStatusListSupplier.getSyncOrNull(params) } returns accountList.toStatus()
val referralStatus = ReferralStatus(isActive = false, token = null, address = null)
coEvery { referralRepository.getReferralStatus(userWalletId.stringValue) } returns referralStatus
// Act
val actual = useCase(accountId)
// Assert
val expected = Error.CriticalTechError.AccountNotFound(accountId).left()
Truth.assertThat(actual).isEqualTo(expected)
coVerifyOrder { singleAccountStatusListSupplier.getSyncOrNull(params) }
coVerify(inverse = true) {
referralRepository.getReferralStatus(any())
crudRepository.saveAccounts(any())
}
}
@Test
fun `invoke should return error if saveAccounts throws exception`() = runTest {
val accountList = createAccountList(activeAccounts = 2)
val archivingAccount = accountList.accounts.last()
val updatedAccountList = (accountList - archivingAccount).getOrNull()!!
val params = SingleAccountStatusListProducer.Params(userWalletId)
coEvery { singleAccountStatusListSupplier.getSyncOrNull(params) } returns accountList.toStatus()
val referralStatus = ReferralStatus(isActive = false, token = null, address = null)
coEvery { referralRepository.getReferralStatus(userWalletId.stringValue) } returns referralStatus
val exception = IllegalStateException("Save failed")
coEvery { crudRepository.saveAccounts(updatedAccountList) } throws exception
// Act
val actual = useCase(accountId = archivingAccount.accountId)
// Assert
val expected = Error.DataOperationFailed(exception).left()
Truth.assertThat(actual).isEqualTo(expected)
coVerifyOrder {
singleAccountStatusListSupplier.getSyncOrNull(params)
referralRepository.getReferralStatus(userWalletId.stringValue)
crudRepository.saveAccounts(updatedAccountList)
}
}
private fun AccountList.toStatus(): AccountStatusList {
return AccountStatusList(
userWalletId = Companion.userWalletId,
accountStatuses = accounts.map {
AccountStatus.CryptoPortfolio(
account = it as Account.CryptoPortfolio,
tokenList = TokenList.Empty,
priceChangeLce = Unit.lceError(),
)
},
totalAccounts = totalAccounts,
totalFiatBalance = TotalFiatBalance.Loading,
sortType = sortType,
groupType = groupType,
)
}
private companion object {
val userWalletId = UserWalletId("011")
}
}

View file

@ -113,7 +113,7 @@ class GetAccountCurrencyByAddressUseCaseTest {
// Arrange // Arrange
every { accountsCRUDRepository.getUserWalletsSync() } returns listOf(multiUserWallet) every { accountsCRUDRepository.getUserWalletsSync() } returns listOf(multiUserWallet)
coEvery { coEvery {
multiNetworkStatusSupplier.getSyncOrNull(params = MultiNetworkStatusProducer.Params(userWalletId)) multiNetworkStatusSupplier.getSyncOrNull(params = MultiNetworkStatusProducer.Params(userWalletId), 1000)
} returns null } returns null
// Act // Act
@ -124,7 +124,7 @@ class GetAccountCurrencyByAddressUseCaseTest {
coVerifySequence { coVerifySequence {
accountsCRUDRepository.getUserWalletsSync() accountsCRUDRepository.getUserWalletsSync()
multiNetworkStatusSupplier.getSyncOrNull(params = MultiNetworkStatusProducer.Params(userWalletId)) multiNetworkStatusSupplier.getSyncOrNull(params = MultiNetworkStatusProducer.Params(userWalletId), 1000)
} }
} }
@ -133,7 +133,7 @@ class GetAccountCurrencyByAddressUseCaseTest {
// Arrange // Arrange
every { accountsCRUDRepository.getUserWalletsSync() } returns listOf(multiUserWallet) every { accountsCRUDRepository.getUserWalletsSync() } returns listOf(multiUserWallet)
coEvery { coEvery {
multiNetworkStatusSupplier.getSyncOrNull(params = MultiNetworkStatusProducer.Params(userWalletId)) multiNetworkStatusSupplier.getSyncOrNull(params = MultiNetworkStatusProducer.Params(userWalletId), 1000)
} returns emptySet() } returns emptySet()
// Act // Act
@ -144,7 +144,7 @@ class GetAccountCurrencyByAddressUseCaseTest {
coVerifySequence { coVerifySequence {
accountsCRUDRepository.getUserWalletsSync() accountsCRUDRepository.getUserWalletsSync()
multiNetworkStatusSupplier.getSyncOrNull(params = MultiNetworkStatusProducer.Params(userWalletId)) multiNetworkStatusSupplier.getSyncOrNull(params = MultiNetworkStatusProducer.Params(userWalletId), 1000)
} }
} }
@ -158,7 +158,7 @@ class GetAccountCurrencyByAddressUseCaseTest {
every { accountsCRUDRepository.getUserWalletsSync() } returns listOf(multiUserWallet) every { accountsCRUDRepository.getUserWalletsSync() } returns listOf(multiUserWallet)
coEvery { coEvery {
multiNetworkStatusSupplier.getSyncOrNull(params = MultiNetworkStatusProducer.Params(userWalletId)) multiNetworkStatusSupplier.getSyncOrNull(params = MultiNetworkStatusProducer.Params(userWalletId), 1000)
} returns setOf(networkStatus) } returns setOf(networkStatus)
// Act // Act
@ -169,7 +169,7 @@ class GetAccountCurrencyByAddressUseCaseTest {
coVerifySequence { coVerifySequence {
accountsCRUDRepository.getUserWalletsSync() accountsCRUDRepository.getUserWalletsSync()
multiNetworkStatusSupplier.getSyncOrNull(params = MultiNetworkStatusProducer.Params(userWalletId)) multiNetworkStatusSupplier.getSyncOrNull(params = MultiNetworkStatusProducer.Params(userWalletId), 1000)
} }
} }
@ -185,7 +185,7 @@ class GetAccountCurrencyByAddressUseCaseTest {
every { accountsCRUDRepository.getUserWalletsSync() } returns listOf(multiUserWallet) every { accountsCRUDRepository.getUserWalletsSync() } returns listOf(multiUserWallet)
coEvery { coEvery {
multiNetworkStatusSupplier.getSyncOrNull(params = MultiNetworkStatusProducer.Params(userWalletId)) multiNetworkStatusSupplier.getSyncOrNull(params = MultiNetworkStatusProducer.Params(userWalletId), 1000)
} returns setOf(networkStatus) } returns setOf(networkStatus)
coEvery { coEvery {
singleAccountListSupplier.getSyncOrNull( singleAccountListSupplier.getSyncOrNull(
@ -201,7 +201,7 @@ class GetAccountCurrencyByAddressUseCaseTest {
coVerifySequence { coVerifySequence {
accountsCRUDRepository.getUserWalletsSync() accountsCRUDRepository.getUserWalletsSync()
multiNetworkStatusSupplier.getSyncOrNull(params = MultiNetworkStatusProducer.Params(userWalletId)) multiNetworkStatusSupplier.getSyncOrNull(params = MultiNetworkStatusProducer.Params(userWalletId), 1000)
singleAccountListSupplier.getSyncOrNull( singleAccountListSupplier.getSyncOrNull(
params = SingleAccountListProducer.Params(userWalletId = userWalletId), params = SingleAccountListProducer.Params(userWalletId = userWalletId),
) )
@ -222,7 +222,7 @@ class GetAccountCurrencyByAddressUseCaseTest {
every { accountsCRUDRepository.getUserWalletsSync() } returns listOf(multiUserWallet) every { accountsCRUDRepository.getUserWalletsSync() } returns listOf(multiUserWallet)
coEvery { coEvery {
multiNetworkStatusSupplier.getSyncOrNull(params = MultiNetworkStatusProducer.Params(userWalletId)) multiNetworkStatusSupplier.getSyncOrNull(params = MultiNetworkStatusProducer.Params(userWalletId), 1000)
} returns setOf(networkStatus) } returns setOf(networkStatus)
coEvery { coEvery {
singleAccountListSupplier.getSyncOrNull( singleAccountListSupplier.getSyncOrNull(
@ -238,7 +238,7 @@ class GetAccountCurrencyByAddressUseCaseTest {
coVerifySequence { coVerifySequence {
accountsCRUDRepository.getUserWalletsSync() accountsCRUDRepository.getUserWalletsSync()
multiNetworkStatusSupplier.getSyncOrNull(params = MultiNetworkStatusProducer.Params(userWalletId)) multiNetworkStatusSupplier.getSyncOrNull(params = MultiNetworkStatusProducer.Params(userWalletId), 1000)
singleAccountListSupplier.getSyncOrNull( singleAccountListSupplier.getSyncOrNull(
params = SingleAccountListProducer.Params(userWalletId = userWalletId), params = SingleAccountListProducer.Params(userWalletId = userWalletId),
) )
@ -257,7 +257,7 @@ class GetAccountCurrencyByAddressUseCaseTest {
every { accountsCRUDRepository.getUserWalletsSync() } returns listOf(multiUserWallet) every { accountsCRUDRepository.getUserWalletsSync() } returns listOf(multiUserWallet)
coEvery { coEvery {
multiNetworkStatusSupplier.getSyncOrNull(params = MultiNetworkStatusProducer.Params(userWalletId)) multiNetworkStatusSupplier.getSyncOrNull(params = MultiNetworkStatusProducer.Params(userWalletId), 1000)
} returns setOf(networkStatus) } returns setOf(networkStatus)
coEvery { coEvery {
singleAccountListSupplier.getSyncOrNull( singleAccountListSupplier.getSyncOrNull(
@ -274,7 +274,7 @@ class GetAccountCurrencyByAddressUseCaseTest {
coVerifySequence { coVerifySequence {
accountsCRUDRepository.getUserWalletsSync() accountsCRUDRepository.getUserWalletsSync()
multiNetworkStatusSupplier.getSyncOrNull(params = MultiNetworkStatusProducer.Params(userWalletId)) multiNetworkStatusSupplier.getSyncOrNull(params = MultiNetworkStatusProducer.Params(userWalletId), 1000)
singleAccountListSupplier.getSyncOrNull( singleAccountListSupplier.getSyncOrNull(
params = SingleAccountListProducer.Params(userWalletId = userWalletId), params = SingleAccountListProducer.Params(userWalletId = userWalletId),
) )

View file

@ -2,6 +2,7 @@ package com.tangem.domain.core.flow
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.withTimeoutOrNull
/** /**
* [Flow] supplier * [Flow] supplier
@ -16,8 +17,14 @@ interface FlowSupplier<Params : Any, Data : Any> {
/** Supply [Flow] by [params] */ /** Supply [Flow] by [params] */
operator fun invoke(params: Params): Flow<Data> operator fun invoke(params: Params): Flow<Data>
/** Get first [Data] by [params] or null if [Flow] is empty */ /** Synchronously get first value or null from [Flow] within [timeMillis] */
suspend fun getSyncOrNull(params: Params): Data? { suspend fun getSyncOrNull(params: Params, timeMillis: Long? = null): Data? {
return invoke(params).firstOrNull() val block = suspend { invoke(params).firstOrNull() }
return if (timeMillis == null) {
block()
} else {
withTimeoutOrNull(timeMillis) { block() }
}
} }
} }

1
domain/referral/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,9 @@
plugins {
alias(deps.plugins.kotlin.jvm)
alias(deps.plugins.kotlin.serialization)
id("configuration")
}
dependencies {
api(deps.kotlin.serialization)
}

View file

@ -0,0 +1,28 @@
package com.tangem.domain.referral
import kotlinx.serialization.Serializable
/**
* Repository for handling referral-related operations.
*
[REDACTED_AUTHOR]
*/
interface ReferralRepository {
/** Retrieves the referral status for a given [userWalletId] */
suspend fun getReferralStatus(userWalletId: String): ReferralStatus
}
@Serializable
data class ReferralStatus(
val isActive: Boolean,
val token: Token?,
val address: String?,
) {
@Serializable
data class Token(
val networkId: String,
val contractAddress: String?,
)
}

View file

@ -13,8 +13,8 @@ import com.tangem.core.ui.message.DialogMessage
import com.tangem.core.ui.message.EventMessageAction import com.tangem.core.ui.message.EventMessageAction
import com.tangem.core.ui.message.ToastMessage import com.tangem.core.ui.message.ToastMessage
import com.tangem.domain.account.producer.SingleAccountProducer import com.tangem.domain.account.producer.SingleAccountProducer
import com.tangem.domain.account.status.usecase.ArchiveCryptoPortfolioUseCase
import com.tangem.domain.account.supplier.SingleAccountSupplier import com.tangem.domain.account.supplier.SingleAccountSupplier
import com.tangem.domain.account.usecase.ArchiveCryptoPortfolioUseCase
import com.tangem.domain.models.PortfolioId import com.tangem.domain.models.PortfolioId
import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.Account
import com.tangem.domain.models.wallet.isMultiCurrency import com.tangem.domain.models.wallet.isMultiCurrency
@ -104,13 +104,14 @@ internal class AccountDetailsModel @Inject constructor(
} }
private fun failedArchiveDialog(error: ArchiveCryptoPortfolioUseCase.Error) { private fun failedArchiveDialog(error: ArchiveCryptoPortfolioUseCase.Error) {
// todo account referral case
val titleRes = when (error) { val titleRes = when (error) {
is ArchiveCryptoPortfolioUseCase.Error.CriticalTechError.AccountListRequirementsNotMet, is ArchiveCryptoPortfolioUseCase.Error.CriticalTechError.AccountListRequirementsNotMet,
is ArchiveCryptoPortfolioUseCase.Error.CriticalTechError.AccountNotFound, is ArchiveCryptoPortfolioUseCase.Error.CriticalTechError.AccountNotFound,
is ArchiveCryptoPortfolioUseCase.Error.CriticalTechError.AccountsNotCreated, is ArchiveCryptoPortfolioUseCase.Error.CriticalTechError.AccountsNotCreated,
is ArchiveCryptoPortfolioUseCase.Error.DataOperationFailed, is ArchiveCryptoPortfolioUseCase.Error.DataOperationFailed,
-> R.string.common_something_went_wrong -> R.string.common_something_went_wrong
is ArchiveCryptoPortfolioUseCase.Error.ActiveReferralStatus,
-> R.string.account_could_not_archive_referral_program_title
} }
val messageRes = when (error) { val messageRes = when (error) {
is ArchiveCryptoPortfolioUseCase.Error.CriticalTechError.AccountListRequirementsNotMet, is ArchiveCryptoPortfolioUseCase.Error.CriticalTechError.AccountListRequirementsNotMet,
@ -118,6 +119,8 @@ internal class AccountDetailsModel @Inject constructor(
is ArchiveCryptoPortfolioUseCase.Error.CriticalTechError.AccountsNotCreated, is ArchiveCryptoPortfolioUseCase.Error.CriticalTechError.AccountsNotCreated,
is ArchiveCryptoPortfolioUseCase.Error.DataOperationFailed, is ArchiveCryptoPortfolioUseCase.Error.DataOperationFailed,
-> R.string.account_could_not_archive -> R.string.account_could_not_archive
is ArchiveCryptoPortfolioUseCase.Error.ActiveReferralStatus,
-> R.string.account_could_not_archive_referral_program_message
} }
val dialogMessage = DialogMessage( val dialogMessage = DialogMessage(

View file

@ -25,6 +25,7 @@ dependencies {
implementation(projects.domain.models) implementation(projects.domain.models)
implementation(projects.domain.tokens.models) implementation(projects.domain.tokens.models)
implementation(projects.domain.wallets.models) implementation(projects.domain.wallets.models)
implementation(projects.domain.referral)
implementation(projects.features.referral.domain) implementation(projects.features.referral.domain)
/** Libs */ /** Libs */

View file

@ -1,7 +0,0 @@
<?xml version="1.0" ?>
<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>MultilineLambdaItParameter:ReferralConverter.kt$ExpectedAwardsConverter${ ExpectedAward( paymentDate = it.paymentDate.toDateTimeAtStartOfDay().millis.toDateFormat(), amount = "${it.amount} ${it.currency}", ) }</ID>
</CurrentIssues>
</SmellBaseline>

View file

@ -70,10 +70,10 @@ private class ExpectedAwardsConverter : Converter<ReferralResponse.ExpectedAward
override fun convert(value: ReferralResponse.ExpectedAwards): ExpectedAwards { override fun convert(value: ReferralResponse.ExpectedAwards): ExpectedAwards {
return ExpectedAwards( return ExpectedAwards(
numberOfWallets = value.numberOfWallets, numberOfWallets = value.numberOfWallets,
expectedAwards = value.list.map { expectedAwards = value.list.map { awardItem ->
ExpectedAward( ExpectedAward(
paymentDate = it.paymentDate.toDateTimeAtStartOfDay().millis.toDateFormat(), paymentDate = awardItem.paymentDate.toDateTimeAtStartOfDay().millis.toDateFormat(),
amount = "${it.amount} ${it.currency}", amount = "${awardItem.amount} ${awardItem.currency}",
) )
}, },
) )

View file

@ -12,6 +12,7 @@ import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.models.account.DerivationIndex import com.tangem.domain.models.account.DerivationIndex
import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.referral.ReferralStatus
import com.tangem.feature.referral.converters.ReferralConverter import com.tangem.feature.referral.converters.ReferralConverter
import com.tangem.feature.referral.domain.ReferralRepository import com.tangem.feature.referral.domain.ReferralRepository
import com.tangem.feature.referral.domain.models.ReferralData import com.tangem.feature.referral.domain.models.ReferralData
@ -21,13 +22,15 @@ import kotlinx.coroutines.withContext
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import javax.inject.Inject import javax.inject.Inject
typealias ExternalReferralRepository = com.tangem.domain.referral.ReferralRepository
internal class ReferralRepositoryImpl @Inject constructor( internal class ReferralRepositoryImpl @Inject constructor(
private val referralApi: TangemTechApi, private val referralApi: TangemTechApi,
private val referralConverter: ReferralConverter, private val referralConverter: ReferralConverter,
private val coroutineDispatcher: CoroutineDispatcherProvider, private val coroutineDispatcher: CoroutineDispatcherProvider,
private val userWalletsStore: UserWalletsStore, private val userWalletsStore: UserWalletsStore,
excludedBlockchains: ExcludedBlockchains, excludedBlockchains: ExcludedBlockchains,
) : ReferralRepository { ) : ReferralRepository, ExternalReferralRepository {
private val cryptoCurrencyFactory = CryptoCurrencyFactory(excludedBlockchains) private val cryptoCurrencyFactory = CryptoCurrencyFactory(excludedBlockchains)
@ -47,6 +50,21 @@ internal class ReferralRepositoryImpl @Inject constructor(
} }
} }
override suspend fun getReferralStatus(userWalletId: String): ReferralStatus {
val data = getReferralData(userWalletId)
return ReferralStatus(
isActive = data is ReferralData.ParticipantData,
token = data.tokens.firstOrNull()?.let { tokenData ->
ReferralStatus.Token(
networkId = tokenData.networkId,
contractAddress = tokenData.contractAddress,
)
},
address = (data as? ReferralData.ParticipantData)?.referral?.address,
)
}
override suspend fun isReferralParticipant(userWalletId: UserWalletId): Boolean { override suspend fun isReferralParticipant(userWalletId: UserWalletId): Boolean {
val storedReferralData = referralStatus[userWalletId.stringValue] ?: getReferralData(userWalletId.stringValue) val storedReferralData = referralStatus[userWalletId.stringValue] ?: getReferralData(userWalletId.stringValue)
return storedReferralData is ReferralData.ParticipantData return storedReferralData is ReferralData.ParticipantData

View file

@ -4,6 +4,7 @@ import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.feature.referral.converters.ReferralConverter import com.tangem.feature.referral.converters.ReferralConverter
import com.tangem.feature.referral.data.ExternalReferralRepository
import com.tangem.feature.referral.data.ReferralRepositoryImpl import com.tangem.feature.referral.data.ReferralRepositoryImpl
import com.tangem.feature.referral.domain.ReferralRepository import com.tangem.feature.referral.domain.ReferralRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -34,4 +35,22 @@ class ReferralRepositoryModule {
excludedBlockchains = excludedBlockchains, excludedBlockchains = excludedBlockchains,
) )
} }
@Provides
@Singleton
fun provideExternalReferralRepository(
tangemTechApi: TangemTechApi,
referralConverter: ReferralConverter,
coroutineDispatcherProvider: CoroutineDispatcherProvider,
userWalletsStore: UserWalletsStore,
excludedBlockchains: ExcludedBlockchains,
): ExternalReferralRepository {
return ReferralRepositoryImpl(
referralApi = tangemTechApi,
referralConverter = referralConverter,
coroutineDispatcher = coroutineDispatcherProvider,
userWalletsStore = userWalletsStore,
excludedBlockchains = excludedBlockchains,
)
}
} }

View file

@ -360,6 +360,7 @@ include(":domain:notifications")
include(":domain:notifications:models") include(":domain:notifications:models")
include(":domain:express") include(":domain:express")
include(":domain:express:models") include(":domain:express:models")
include(":domain:referral")
include(":domain:swap") include(":domain:swap")
include(":domain:swap:models") include(":domain:swap:models")
include(":domain:wallet-manager") include(":domain:wallet-manager")