Updated on 2026-08-14

This commit is contained in:
Tangem 2025-09-10 16:15:30 +03:00
commit 2f62d482ca
635 changed files with 18802 additions and 9118 deletions

View file

@ -0,0 +1,11 @@
package com.tangem.domain.account.featuretoggle
/**
* Accounts feature toggle
*
[REDACTED_AUTHOR]
*/
interface AccountsFeatureToggles {
val isFeatureEnabled: Boolean
}

View file

@ -22,7 +22,7 @@ interface AccountsCRUDRepository {
* @param userWalletId the unique identifier of the user wallet
* @return an [Option] containing the [AccountList] if found, or `Option.None` if not
*/
suspend fun getAccounts(userWalletId: UserWalletId): Option<AccountList>
suspend fun getAccountListSync(userWalletId: UserWalletId): Option<AccountList>
/**
* Retrieves a specific account by its unique identifier
@ -30,14 +30,14 @@ interface AccountsCRUDRepository {
* @param accountId the unique identifier of the account
* @return an [Option] containing the [Account.CryptoPortfolio] if found, or `Option.None` if not
*/
suspend fun getAccount(accountId: AccountId): Option<Account.CryptoPortfolio>
suspend fun getAccountSync(accountId: AccountId): Option<Account.CryptoPortfolio>
/**
* Retrieves a archived account by its unique identifier
*
* @param accountId the unique identifier of the account
*/
suspend fun getArchivedAccount(accountId: AccountId): Option<ArchivedAccount>
suspend fun getArchivedAccountSync(accountId: AccountId): Option<ArchivedAccount>
/**
* Retrieves a list of archived accounts associated with a specific user wallet
@ -45,7 +45,7 @@ interface AccountsCRUDRepository {
* @param userWalletId the unique identifier of the user wallet
* @return an [Option] containing a list of [ArchivedAccount] if found, or `Option.None` if not
*/
suspend fun getArchivedAccountsSync(userWalletId: UserWalletId): Option<List<ArchivedAccount>>
suspend fun getArchivedAccountListSync(userWalletId: UserWalletId): Option<List<ArchivedAccount>>
/**
* Provides a flow of archived accounts associated with a specific user wallet
@ -73,7 +73,7 @@ interface AccountsCRUDRepository {
*
* @param userWalletId the unique identifier of the user wallet
*/
suspend fun getTotalAccountsCount(userWalletId: UserWalletId): Int
suspend fun getTotalAccountsCount(userWalletId: UserWalletId): Option<Int>
/**
* Retrieves a user wallet by its unique identifier

View file

@ -55,7 +55,7 @@ class AddCryptoPortfolioUseCase(
newAccount
}
private fun Raise<Error>.createAccount(
private fun createAccount(
userWalletId: UserWalletId,
accountName: AccountName,
icon: CryptoPortfolioIcon,
@ -72,7 +72,7 @@ class AddCryptoPortfolioUseCase(
private suspend fun Raise<Error>.getAccountList(userWalletId: UserWalletId): Option<AccountList> {
return catch(
block = { crudRepository.getAccounts(userWalletId = userWalletId) },
block = { crudRepository.getAccountListSync(userWalletId = userWalletId) },
catch = { raise(Error.DataOperationFailed(cause = it)) },
)
}

View file

@ -40,7 +40,7 @@ class ArchiveCryptoPortfolioUseCase(
private suspend fun Raise<Error>.getAccountList(userWalletId: UserWalletId): AccountList {
return catch(
block = { crudRepository.getAccounts(userWalletId = userWalletId) },
block = { crudRepository.getAccountListSync(userWalletId = userWalletId) },
catch = { raise(Error.DataOperationFailed(cause = it)) },
)
.getOrElse { raise(Error.CriticalTechError.AccountsNotCreated(userWalletId = userWalletId)) }

View file

@ -57,7 +57,7 @@ class GetArchivedAccountsUseCase(
private suspend fun getArchivedAccounts(userWalletId: UserWalletId): Either<Throwable, ArchivedAccountList> {
return Either.catch {
crudRepository.getArchivedAccountsSync(userWalletId = userWalletId).getOrElse {
crudRepository.getArchivedAccountListSync(userWalletId = userWalletId).getOrElse {
error("Archived accounts not found for user wallet: $userWalletId")
}
}
@ -70,7 +70,11 @@ class GetArchivedAccountsUseCase(
private suspend fun ProducerScope<Lce<Throwable, ArchivedAccountList>>.subscribeOnArchivedAccounts(
userWalletId: UserWalletId,
) {
crudRepository.getArchivedAccounts(userWalletId)
runCatching { crudRepository.getArchivedAccounts(userWalletId) }
.getOrElse {
send(it.lceError())
return
}
.distinctUntilChanged()
.retryWhen { cause, _ ->
send(cause.lceError())

View file

@ -38,6 +38,7 @@ class GetUnoccupiedAccountIndexUseCase(
block = { crudRepository.getTotalAccountsCount(userWalletId = userWalletId) },
catch = { raise(Error.DataOperationFailed(cause = it)) },
)
.getOrElse { raise(Error.DataNotFound) }
}
/**
@ -48,6 +49,10 @@ class GetUnoccupiedAccountIndexUseCase(
val tag: String
get() = this::class.simpleName ?: "GetUnoccupiedAccountIndexUseCase.Error"
data object DataNotFound : Error {
override fun toString(): String = "$tag: Data not found"
}
/** Error indicating that the derivation index is invalid */
data class InvalidDerivationIndex(val cause: DerivationIndex.Error) : Error {
override fun toString(): String = "$tag: Invalid derivation index: $cause"

View file

@ -44,7 +44,7 @@ class RecoverCryptoPortfolioUseCase(
private suspend fun Raise<Error>.getAccountList(userWalletId: UserWalletId): AccountList {
return catch(
block = { crudRepository.getAccounts(userWalletId = userWalletId) },
block = { crudRepository.getAccountListSync(userWalletId = userWalletId) },
catch = { raise(Error.DataOperationFailed(cause = it)) },
)
.getOrElse { raise(Error.CriticalTechError.AccountsNotCreated(userWalletId = userWalletId)) }
@ -52,7 +52,7 @@ class RecoverCryptoPortfolioUseCase(
private suspend fun Raise<Error>.getArchivedAccount(accountId: AccountId): ArchivedAccount {
return catch(
block = { crudRepository.getArchivedAccount(accountId = accountId) },
block = { crudRepository.getArchivedAccountSync(accountId = accountId) },
catch = { raise(Error.DataOperationFailed(cause = it)) },
)
.getOrElse {

View file

@ -61,7 +61,7 @@ class UpdateCryptoPortfolioUseCase(
private suspend fun Raise<Error>.getAccountList(userWalletId: UserWalletId): AccountList {
return catch(
block = { crudRepository.getAccounts(userWalletId = userWalletId) },
block = { crudRepository.getAccountListSync(userWalletId = userWalletId) },
catch = { raise(Error.DataOperationFailed(cause = it)) },
)
.getOrElse { raise(Error.CriticalTechError.AccountsNotCreated(userWalletId = userWalletId)) }

View file

@ -41,7 +41,7 @@ class AddCryptoPortfolioUseCaseTest {
val accountList = AccountList.empty(userWallet)
val updatedAccountList = (accountList + newAccount).getOrNull()!!
coEvery { crudRepository.getAccounts(userWalletId) } returns accountList.toOption()
coEvery { crudRepository.getAccountListSync(userWalletId) } returns accountList.toOption()
// Act
val actual = useCase(
@ -56,7 +56,7 @@ class AddCryptoPortfolioUseCaseTest {
Truth.assertThat(actual).isEqualTo(expected)
coVerifyOrder {
crudRepository.getAccounts(userWalletId)
crudRepository.getAccountListSync(userWalletId)
crudRepository.saveAccounts(updatedAccountList)
}
@ -69,7 +69,7 @@ class AddCryptoPortfolioUseCaseTest {
val newAccount = createNewAccount()
val newAccountList = (AccountList.empty(userWallet) + newAccount).getOrNull()!!
coEvery { crudRepository.getAccounts(userWalletId) } returns None
coEvery { crudRepository.getAccountListSync(userWalletId) } returns None
coEvery { crudRepository.getUserWallet(userWalletId) } returns userWallet
// Act
@ -85,7 +85,7 @@ class AddCryptoPortfolioUseCaseTest {
Truth.assertThat(actual).isEqualTo(expected)
coVerifyOrder {
crudRepository.getAccounts(userWalletId)
crudRepository.getAccountListSync(userWalletId)
crudRepository.getUserWallet(userWalletId)
crudRepository.saveAccounts(newAccountList)
}
@ -102,7 +102,7 @@ class AddCryptoPortfolioUseCaseTest {
val newAccount = createNewAccount(derivationIndex = 21)
coEvery { crudRepository.getAccounts(userWalletId) } returns accountList.toOption()
coEvery { crudRepository.getAccountListSync(userWalletId) } returns accountList.toOption()
// Act
val actual = useCase(
@ -119,7 +119,7 @@ class AddCryptoPortfolioUseCaseTest {
Truth.assertThat(actual).isEqualTo(expected)
coVerifyOrder { crudRepository.getAccounts(userWalletId) }
coVerifyOrder { crudRepository.getAccountListSync(userWalletId) }
coVerify(inverse = true) {
crudRepository.getUserWallet(any())
@ -133,7 +133,7 @@ class AddCryptoPortfolioUseCaseTest {
val newAccount = createNewAccount()
val exception = IllegalStateException("Test error")
coEvery { crudRepository.getAccounts(userWalletId) } throws exception
coEvery { crudRepository.getAccountListSync(userWalletId) } throws exception
// Act
val actual = useCase(
@ -147,7 +147,7 @@ class AddCryptoPortfolioUseCaseTest {
val expected = AddCryptoPortfolioUseCase.Error.DataOperationFailed(cause = exception).left()
Truth.assertThat(actual).isEqualTo(expected)
coVerifyOrder { crudRepository.getAccounts(userWalletId) }
coVerifyOrder { crudRepository.getAccountListSync(userWalletId) }
coVerify(inverse = true) {
crudRepository.getUserWallet(any())
@ -164,7 +164,7 @@ class AddCryptoPortfolioUseCaseTest {
val exception = IllegalStateException("Test error")
coEvery { crudRepository.getAccounts(userWalletId) } returns accountList.toOption()
coEvery { crudRepository.getAccountListSync(userWalletId) } returns accountList.toOption()
coEvery { crudRepository.saveAccounts(updatedAccountList) } throws exception
// Act
@ -180,7 +180,7 @@ class AddCryptoPortfolioUseCaseTest {
Truth.assertThat(actual).isEqualTo(expected)
coVerifyOrder {
crudRepository.getAccounts(userWalletId)
crudRepository.getAccountListSync(userWalletId)
crudRepository.saveAccounts(updatedAccountList)
}

View file

@ -41,7 +41,7 @@ class ArchiveCryptoPortfolioUseCaseTest {
val updatedAccountList = (accountList - account).getOrNull()!!
coEvery { crudRepository.getAccounts(userWalletId) } returns accountList.toOption()
coEvery { crudRepository.getAccountListSync(userWalletId) } returns accountList.toOption()
// Act
val actual = useCase(accountId)
@ -51,7 +51,7 @@ class ArchiveCryptoPortfolioUseCaseTest {
Truth.assertThat(actual).isEqualTo(expected)
coVerifyOrder {
crudRepository.getAccounts(userWalletId)
crudRepository.getAccountListSync(userWalletId)
crudRepository.saveAccounts(updatedAccountList)
}
}
@ -64,7 +64,7 @@ class ArchiveCryptoPortfolioUseCaseTest {
derivationIndex = DerivationIndex.Main,
)
coEvery { crudRepository.getAccounts(userWalletId) } returns None
coEvery { crudRepository.getAccountListSync(userWalletId) } returns None
// Act
val actual = useCase(accountId)
@ -73,7 +73,7 @@ class ArchiveCryptoPortfolioUseCaseTest {
val expected = Error.CriticalTechError.AccountsNotCreated(userWalletId).left()
Truth.assertThat(actual).isEqualTo(expected)
coVerifyOrder { crudRepository.getAccounts(userWalletId) }
coVerifyOrder { crudRepository.getAccountListSync(userWalletId) }
coVerify(inverse = true) { crudRepository.saveAccounts(any()) }
}
@ -87,7 +87,7 @@ class ArchiveCryptoPortfolioUseCaseTest {
val exception = IllegalStateException("Test error")
coEvery { crudRepository.getAccounts(userWalletId) } throws exception
coEvery { crudRepository.getAccountListSync(userWalletId) } throws exception
// Act
val actual = useCase(accountId)
@ -96,7 +96,7 @@ class ArchiveCryptoPortfolioUseCaseTest {
val expected = Error.DataOperationFailed(exception).left()
Truth.assertThat(actual).isEqualTo(expected)
coVerifyOrder { crudRepository.getAccounts(userWalletId) }
coVerifyOrder { crudRepository.getAccountListSync(userWalletId) }
coVerify(inverse = true) { crudRepository.saveAccounts(any()) }
}
@ -109,7 +109,7 @@ class ArchiveCryptoPortfolioUseCaseTest {
derivationIndex = DerivationIndex(1).getOrNull()!!,
)
coEvery { crudRepository.getAccounts(userWalletId) } returns accountList.toOption()
coEvery { crudRepository.getAccountListSync(userWalletId) } returns accountList.toOption()
// Act
val actual = useCase(accountId)
@ -118,7 +118,7 @@ class ArchiveCryptoPortfolioUseCaseTest {
val expected = Error.CriticalTechError.AccountNotFound(accountId).left()
Truth.assertThat(actual).isEqualTo(expected)
coVerifyOrder { crudRepository.getAccounts(userWalletId) }
coVerifyOrder { crudRepository.getAccountListSync(userWalletId) }
coVerify(inverse = true) { crudRepository.saveAccounts(any()) }
}
@ -133,7 +133,7 @@ class ArchiveCryptoPortfolioUseCaseTest {
val exception = IllegalStateException("Save failed")
coEvery { crudRepository.getAccounts(userWalletId) } returns accountList.toOption()
coEvery { crudRepository.getAccountListSync(userWalletId) } returns accountList.toOption()
coEvery { crudRepository.saveAccounts(updatedAccountList) } throws exception
// Act
@ -144,7 +144,7 @@ class ArchiveCryptoPortfolioUseCaseTest {
Truth.assertThat(actual).isEqualTo(expected)
coVerifyOrder {
crudRepository.getAccounts(userWalletId)
crudRepository.getAccountListSync(userWalletId)
crudRepository.saveAccounts(updatedAccountList)
}
}

View file

@ -43,7 +43,7 @@ class GetArchivedAccountsUseCaseTest {
mockk<ArchivedAccount>(),
mockk<ArchivedAccount>(),
)
coEvery { crudRepository.getArchivedAccountsSync(userWalletId) } returns archivedAccounts.toOption()
coEvery { crudRepository.getArchivedAccountListSync(userWalletId) } returns archivedAccounts.toOption()
every { crudRepository.getArchivedAccounts(userWalletId) } returns flowOf(archivedAccounts)
// Act
@ -54,7 +54,7 @@ class GetArchivedAccountsUseCaseTest {
Truth.assertThat(actual).isEqualTo(expected)
coVerifyOrder {
crudRepository.getArchivedAccountsSync(userWalletId)
crudRepository.getArchivedAccountListSync(userWalletId)
crudRepository.getArchivedAccounts(userWalletId)
}
@ -69,7 +69,7 @@ class GetArchivedAccountsUseCaseTest {
mockk<ArchivedAccount>(),
)
coEvery { crudRepository.getArchivedAccountsSync(userWalletId) } returns None
coEvery { crudRepository.getArchivedAccountListSync(userWalletId) } returns None
every { crudRepository.getArchivedAccounts(userWalletId) } returns flowOf(archivedAccounts)
// Act
@ -83,7 +83,7 @@ class GetArchivedAccountsUseCaseTest {
Truth.assertThat(actual).isEqualTo(expected)
coVerify(exactly = 1) {
crudRepository.getArchivedAccountsSync(userWalletId)
crudRepository.getArchivedAccountListSync(userWalletId)
crudRepository.fetchArchivedAccounts(userWalletId)
crudRepository.getArchivedAccounts(userWalletId)
}
@ -98,7 +98,7 @@ class GetArchivedAccountsUseCaseTest {
mockk<ArchivedAccount>(),
)
coEvery { crudRepository.getArchivedAccountsSync(userWalletId) } throws exception
coEvery { crudRepository.getArchivedAccountListSync(userWalletId) } throws exception
every { crudRepository.getArchivedAccounts(userWalletId) } returns flowOf(archivedAccounts)
// Act
@ -112,7 +112,7 @@ class GetArchivedAccountsUseCaseTest {
Truth.assertThat(actual).isEqualTo(expected)
coVerify(exactly = 1) {
crudRepository.getArchivedAccountsSync(userWalletId)
crudRepository.getArchivedAccountListSync(userWalletId)
crudRepository.fetchArchivedAccounts(userWalletId)
crudRepository.getArchivedAccounts(userWalletId)
}
@ -123,7 +123,7 @@ class GetArchivedAccountsUseCaseTest {
// Arrange
val exception = IllegalStateException("Fetch error")
coEvery { crudRepository.getArchivedAccountsSync(userWalletId) } returns None
coEvery { crudRepository.getArchivedAccountListSync(userWalletId) } returns None
every { crudRepository.getArchivedAccounts(userWalletId) } returns emptyFlow()
coEvery { crudRepository.fetchArchivedAccounts(userWalletId) } throws exception
@ -139,7 +139,7 @@ class GetArchivedAccountsUseCaseTest {
Truth.assertThat(actual).isEqualTo(expected)
coVerify(exactly = 1) {
crudRepository.getArchivedAccountsSync(userWalletId)
crudRepository.getArchivedAccountListSync(userWalletId)
crudRepository.fetchArchivedAccounts(userWalletId)
crudRepository.getArchivedAccounts(userWalletId)
}

View file

@ -1,6 +1,7 @@
package com.tangem.domain.account.usecase
import arrow.core.left
import arrow.core.toOption
import com.google.common.truth.Truth
import com.tangem.domain.account.repository.AccountsCRUDRepository
import com.tangem.domain.models.account.DerivationIndex
@ -29,7 +30,7 @@ class GetUnoccupiedAccountIndexUseCaseTest {
@Test
fun `invoke should return next unoccupied index when repository returns count`() = runTest {
// Arrange
coEvery { crudRepository.getTotalAccountsCount(userWalletId) } returns 3
coEvery { crudRepository.getTotalAccountsCount(userWalletId) } returns 3.toOption()
// Act
val actual = useCase(userWalletId = userWalletId)

View file

@ -52,8 +52,8 @@ class RecoverCryptoPortfolioUseCaseTest {
val updatedAccountList = (accountList + account).getOrNull()!!
coEvery { crudRepository.getAccounts(userWalletId) } returns accountList.toOption()
coEvery { crudRepository.getArchivedAccount(account.accountId) } returns archivedAccount.toOption()
coEvery { crudRepository.getAccountListSync(userWalletId) } returns accountList.toOption()
coEvery { crudRepository.getArchivedAccountSync(account.accountId) } returns archivedAccount.toOption()
// Act
val actual = useCase(account.accountId)
@ -63,8 +63,8 @@ class RecoverCryptoPortfolioUseCaseTest {
Truth.assertThat(actual).isEqualTo(expected)
coVerifyOrder {
crudRepository.getAccounts(userWalletId)
crudRepository.getArchivedAccount(account.accountId)
crudRepository.getAccountListSync(userWalletId)
crudRepository.getArchivedAccountSync(account.accountId)
crudRepository.saveAccounts(updatedAccountList)
}
}
@ -77,7 +77,7 @@ class RecoverCryptoPortfolioUseCaseTest {
derivationIndex = DerivationIndex.Main,
)
coEvery { crudRepository.getAccounts(userWalletId) } returns None
coEvery { crudRepository.getAccountListSync(userWalletId) } returns None
// Act
val actual = useCase(accountId)
@ -86,9 +86,9 @@ class RecoverCryptoPortfolioUseCaseTest {
val expected = Error.CriticalTechError.AccountsNotCreated(userWalletId).left()
Truth.assertThat(actual).isEqualTo(expected)
coVerifyOrder { crudRepository.getAccounts(userWalletId) }
coVerifyOrder { crudRepository.getAccountListSync(userWalletId) }
coVerify(inverse = true) {
crudRepository.getArchivedAccount(any())
crudRepository.getArchivedAccountSync(any())
crudRepository.saveAccounts(any())
}
}
@ -102,7 +102,7 @@ class RecoverCryptoPortfolioUseCaseTest {
)
val exception = IllegalStateException("Test error")
coEvery { crudRepository.getAccounts(userWalletId) } throws exception
coEvery { crudRepository.getAccountListSync(userWalletId) } throws exception
// Act
val actual = useCase(accountId)
@ -111,9 +111,9 @@ class RecoverCryptoPortfolioUseCaseTest {
val expected = Error.DataOperationFailed(exception).left()
Truth.assertThat(actual).isEqualTo(expected)
coVerifyOrder { crudRepository.getAccounts(userWalletId) }
coVerifyOrder { crudRepository.getAccountListSync(userWalletId) }
coVerify(inverse = true) {
crudRepository.getArchivedAccount(any())
crudRepository.getArchivedAccountSync(any())
crudRepository.saveAccounts(any())
}
}
@ -125,8 +125,8 @@ class RecoverCryptoPortfolioUseCaseTest {
val accountList = AccountList.empty(userWallet)
val exception = IllegalStateException("Test error")
coEvery { crudRepository.getAccounts(userWalletId) } returns accountList.toOption()
coEvery { crudRepository.getArchivedAccount(account.accountId) } throws exception
coEvery { crudRepository.getAccountListSync(userWalletId) } returns accountList.toOption()
coEvery { crudRepository.getArchivedAccountSync(account.accountId) } throws exception
// Act
val actual = useCase(account.accountId)
@ -136,8 +136,8 @@ class RecoverCryptoPortfolioUseCaseTest {
Truth.assertThat(actual).isEqualTo(expected)
coVerifyOrder {
crudRepository.getAccounts(userWalletId)
crudRepository.getArchivedAccount(account.accountId)
crudRepository.getAccountListSync(userWalletId)
crudRepository.getArchivedAccountSync(account.accountId)
}
coVerify(inverse = true) { crudRepository.saveAccounts(any()) }
}
@ -148,8 +148,8 @@ class RecoverCryptoPortfolioUseCaseTest {
val account = createAccount(userWalletId)
val accountList = AccountList.empty(userWallet)
coEvery { crudRepository.getAccounts(userWalletId) } returns accountList.toOption()
coEvery { crudRepository.getArchivedAccount(account.accountId) } returns None
coEvery { crudRepository.getAccountListSync(userWalletId) } returns accountList.toOption()
coEvery { crudRepository.getArchivedAccountSync(account.accountId) } returns None
// Act
val actual = useCase(account.accountId)
@ -159,8 +159,8 @@ class RecoverCryptoPortfolioUseCaseTest {
Truth.assertThat(actual).isEqualTo(expected)
coVerifyOrder {
crudRepository.getAccounts(userWalletId)
crudRepository.getArchivedAccount(account.accountId)
crudRepository.getAccountListSync(userWalletId)
crudRepository.getArchivedAccountSync(account.accountId)
}
coVerify(inverse = true) { crudRepository.saveAccounts(any()) }
}
@ -182,8 +182,8 @@ class RecoverCryptoPortfolioUseCaseTest {
val updatedAccountList = (accountList + account).getOrNull()!!
val exception = IllegalStateException("Save failed")
coEvery { crudRepository.getAccounts(userWalletId) } returns accountList.toOption()
coEvery { crudRepository.getArchivedAccount(account.accountId) } returns archivedAccount.toOption()
coEvery { crudRepository.getAccountListSync(userWalletId) } returns accountList.toOption()
coEvery { crudRepository.getArchivedAccountSync(account.accountId) } returns archivedAccount.toOption()
coEvery { crudRepository.saveAccounts(updatedAccountList) } throws exception
// Act
@ -194,8 +194,8 @@ class RecoverCryptoPortfolioUseCaseTest {
Truth.assertThat(actual).isEqualTo(expected)
coVerifyOrder {
crudRepository.getAccounts(userWalletId)
crudRepository.getArchivedAccount(account.accountId)
crudRepository.getAccountListSync(userWalletId)
crudRepository.getArchivedAccountSync(account.accountId)
crudRepository.saveAccounts(updatedAccountList)
}
}

View file

@ -48,7 +48,7 @@ class UpdateCryptoPortfolioUseCaseTest {
val updatedAccount = accountList.mainAccount.copy(accountName = newAccountName)
val updatedAccountList = (accountList + updatedAccount).getOrNull()!!
coEvery { crudRepository.getAccounts(userWalletId = userWalletId) } returns accountList.toOption()
coEvery { crudRepository.getAccountListSync(userWalletId = userWalletId) } returns accountList.toOption()
// Act
val actual = useCase(accountId = accountId, accountName = newAccountName)
@ -58,7 +58,7 @@ class UpdateCryptoPortfolioUseCaseTest {
Truth.assertThat(actual).isEqualTo(expected)
coVerifyOrder {
crudRepository.getAccounts(userWalletId = userWalletId)
crudRepository.getAccountListSync(userWalletId = userWalletId)
crudRepository.saveAccounts(accountList = updatedAccountList)
}
}
@ -76,7 +76,7 @@ class UpdateCryptoPortfolioUseCaseTest {
val updatedAccount = accountList.mainAccount.copy(icon = newAccountIcon)
val updatedAccountList = (accountList + updatedAccount).getOrNull()!!
coEvery { crudRepository.getAccounts(userWalletId = userWalletId) } returns accountList.toOption()
coEvery { crudRepository.getAccountListSync(userWalletId = userWalletId) } returns accountList.toOption()
// Act
val actual = useCase(accountId = accountId, icon = newAccountIcon)
@ -86,7 +86,7 @@ class UpdateCryptoPortfolioUseCaseTest {
Truth.assertThat(actual).isEqualTo(expected)
coVerifyOrder {
crudRepository.getAccounts(userWalletId = userWalletId)
crudRepository.getAccountListSync(userWalletId = userWalletId)
crudRepository.saveAccounts(accountList = updatedAccountList)
}
}
@ -105,7 +105,7 @@ class UpdateCryptoPortfolioUseCaseTest {
val updatedAccount = accountList.mainAccount.copy(accountName = newAccountName, icon = newAccountIcon)
val updatedAccountList = (accountList + updatedAccount).getOrNull()!!
coEvery { crudRepository.getAccounts(userWalletId = userWalletId) } returns accountList.toOption()
coEvery { crudRepository.getAccountListSync(userWalletId = userWalletId) } returns accountList.toOption()
// Act
val actual = useCase(accountId = accountId, accountName = newAccountName, icon = newAccountIcon)
@ -115,7 +115,7 @@ class UpdateCryptoPortfolioUseCaseTest {
Truth.assertThat(actual).isEqualTo(expected)
coVerifyOrder {
crudRepository.getAccounts(userWalletId = userWalletId)
crudRepository.getAccountListSync(userWalletId = userWalletId)
crudRepository.saveAccounts(accountList = updatedAccountList)
}
}
@ -126,7 +126,7 @@ class UpdateCryptoPortfolioUseCaseTest {
val accountList = AccountList.empty(userWallet = userWallet)
val accountId = accountList.mainAccount.accountId
coEvery { crudRepository.getAccounts(userWalletId = userWalletId) } returns accountList.toOption()
coEvery { crudRepository.getAccountListSync(userWalletId = userWalletId) } returns accountList.toOption()
// Act
val actual = useCase(accountId = accountId)
@ -136,7 +136,7 @@ class UpdateCryptoPortfolioUseCaseTest {
Truth.assertThat(actual).isEqualTo(expected)
coVerify(inverse = true) {
crudRepository.getAccounts(userWalletId = any())
crudRepository.getAccountListSync(userWalletId = any())
crudRepository.saveAccounts(accountList = any())
}
}
@ -151,7 +151,7 @@ class UpdateCryptoPortfolioUseCaseTest {
val exception = IllegalStateException("Test exception")
coEvery { crudRepository.getAccounts(userWalletId = userWalletId) } throws exception
coEvery { crudRepository.getAccountListSync(userWalletId = userWalletId) } throws exception
// Act
val actual = useCase(accountId = accountId, accountName = newAccountName)
@ -160,7 +160,7 @@ class UpdateCryptoPortfolioUseCaseTest {
val expected = Error.DataOperationFailed(cause = exception).left()
Truth.assertThat(actual).isEqualTo(expected)
coVerifyOrder { crudRepository.getAccounts(userWalletId = userWalletId) }
coVerifyOrder { crudRepository.getAccountListSync(userWalletId = userWalletId) }
coVerify(inverse = true) { crudRepository.saveAccounts(accountList = any()) }
}
@ -175,7 +175,7 @@ class UpdateCryptoPortfolioUseCaseTest {
val newAccountName = AccountName("New name").getOrNull()!!
coEvery { crudRepository.getAccounts(userWalletId = userWalletId) } returns accountList
coEvery { crudRepository.getAccountListSync(userWalletId = userWalletId) } returns accountList
// Act
val actual = useCase(accountId = accountId, accountName = newAccountName)
@ -184,7 +184,7 @@ class UpdateCryptoPortfolioUseCaseTest {
val expected = Error.CriticalTechError.AccountsNotCreated(userWalletId = userWalletId).left()
Truth.assertThat(actual).isEqualTo(expected)
coVerifyOrder { crudRepository.getAccounts(userWalletId = userWalletId) }
coVerifyOrder { crudRepository.getAccountListSync(userWalletId = userWalletId) }
coVerify(inverse = true) { crudRepository.saveAccounts(accountList = any()) }
}
@ -199,7 +199,7 @@ class UpdateCryptoPortfolioUseCaseTest {
val newAccountName = AccountName("New name").getOrNull()!!
coEvery { crudRepository.getAccounts(userWalletId = userWalletId) } returns accountList.toOption()
coEvery { crudRepository.getAccountListSync(userWalletId = userWalletId) } returns accountList.toOption()
// Act
val actual = useCase(accountId = accountId, accountName = newAccountName)
@ -208,7 +208,7 @@ class UpdateCryptoPortfolioUseCaseTest {
val expected = Error.CriticalTechError.AccountNotFound(accountId = accountId).left()
Truth.assertThat(actual).isEqualTo(expected)
coVerifyOrder { crudRepository.getAccounts(userWalletId = userWalletId) }
coVerifyOrder { crudRepository.getAccountListSync(userWalletId = userWalletId) }
coVerify(inverse = true) { crudRepository.saveAccounts(accountList = any()) }
}
@ -224,7 +224,7 @@ class UpdateCryptoPortfolioUseCaseTest {
val exception = IllegalStateException("Save failed")
coEvery { crudRepository.getAccounts(userWalletId = userWalletId) } returns accountList.toOption()
coEvery { crudRepository.getAccountListSync(userWalletId = userWalletId) } returns accountList.toOption()
coEvery { crudRepository.saveAccounts(accountList = updatedAccountList) } throws exception
// Act
@ -235,7 +235,7 @@ class UpdateCryptoPortfolioUseCaseTest {
Truth.assertThat(actual).isEqualTo(expected)
coVerifyOrder {
crudRepository.getAccounts(userWalletId = userWalletId)
crudRepository.getAccountListSync(userWalletId = userWalletId)
crudRepository.saveAccounts(accountList = updatedAccountList)
}
}

View file

@ -1,21 +0,0 @@
package com.tangem.domain.feedback.models
import com.tangem.domain.models.wallet.UserWalletId
import kotlinx.serialization.Serializable
@Serializable
data class CardInfo(
val userWalletId: UserWalletId?,
val cardId: String,
val firmwareVersion: String,
val cardsCount: String,
val cardBlockchain: String?,
val signedHashesList: List<SignedHashes>,
val isImported: Boolean,
val isStart2Coin: Boolean,
val isVisa: Boolean,
) {
@Serializable
data class SignedHashes(val curve: String, val total: String?)
}

View file

@ -9,32 +9,32 @@ import com.tangem.domain.visa.model.VisaTxDetails
*/
sealed interface FeedbackEmailType {
val cardInfo: CardInfo?
val walletMetaInfo: WalletMetaInfo?
/** User initiate request yourself. Example, button on DetailsScreen or OnboardingScreen */
data class DirectUserRequest(override val cardInfo: CardInfo) : FeedbackEmailType
data class DirectUserRequest(override val walletMetaInfo: WalletMetaInfo) : FeedbackEmailType
/** User rate the app as "can be better" */
data class RateCanBeBetter(override val cardInfo: CardInfo) : FeedbackEmailType
data class RateCanBeBetter(override val walletMetaInfo: WalletMetaInfo) : FeedbackEmailType
/** User has problem with scanning */
data object ScanningProblem : FeedbackEmailType {
override val cardInfo: CardInfo? = null
override val walletMetaInfo: WalletMetaInfo? = null
}
/** User has problem with sending transaction */
data class TransactionSendingProblem(override val cardInfo: CardInfo) : FeedbackEmailType
data class TransactionSendingProblem(override val walletMetaInfo: WalletMetaInfo) : FeedbackEmailType
/** User has problem with staking */
data class StakingProblem(
override val cardInfo: CardInfo,
override val walletMetaInfo: WalletMetaInfo,
val validatorName: String?,
val transactionTypes: List<String>,
val unsignedTransactions: List<String?>,
) : FeedbackEmailType
data class SwapProblem(
override val cardInfo: CardInfo,
override val walletMetaInfo: WalletMetaInfo,
val providerName: String,
val txId: String,
) : FeedbackEmailType
@ -46,23 +46,23 @@ sealed interface FeedbackEmailType {
* @property currencyName currency name
*/
data class CurrencyDescriptionError(val currencyId: String, val currencyName: String) : FeedbackEmailType {
override val cardInfo: CardInfo? = null
override val walletMetaInfo: WalletMetaInfo? = null
}
data class PreActivatedWallet(override val cardInfo: CardInfo) : FeedbackEmailType
data class PreActivatedWallet(override val walletMetaInfo: WalletMetaInfo) : FeedbackEmailType
data object CardAttestationFailed : FeedbackEmailType {
override val cardInfo: CardInfo? = null
override val walletMetaInfo: WalletMetaInfo? = null
}
sealed class Visa : FeedbackEmailType {
data class DirectUserRequest(override val cardInfo: CardInfo) : Visa()
data class DirectUserRequest(override val walletMetaInfo: WalletMetaInfo) : Visa()
data class Activation(override val cardInfo: CardInfo) : Visa()
data class Activation(override val walletMetaInfo: WalletMetaInfo) : Visa()
data class Dispute(
val visaTxDetails: VisaTxDetails,
override val cardInfo: CardInfo,
override val walletMetaInfo: WalletMetaInfo,
) : Visa()
}
}

View file

@ -0,0 +1,22 @@
package com.tangem.domain.feedback.models
import com.tangem.domain.models.wallet.UserWalletId
import kotlinx.serialization.Serializable
@Serializable
data class WalletMetaInfo(
val userWalletId: UserWalletId?,
val hotWalletIsBackedUp: Boolean? = null,
val cardId: String? = null,
val firmwareVersion: String? = null,
val cardsCount: String? = null,
val cardBlockchain: String? = null,
val signedHashesList: List<SignedHashes>? = null,
val isImported: Boolean? = null,
val isStart2Coin: Boolean? = null,
val isVisa: Boolean? = null,
) {
@Serializable
data class SignedHashes(val curve: String, val total: String?)
}

View file

@ -44,13 +44,14 @@ internal class FeedbackDataBuilder {
builder.appendKeyValue("Total saved wallets", userWalletsInfo.totalUserWallets.toString())
}
fun addCardInfo(cardInfo: CardInfo) {
builder.appendKeyValue("Card ID", cardInfo.cardId)
builder.appendKeyValue("Firmware version", cardInfo.firmwareVersion)
builder.appendKeyValue("Linked cards count", cardInfo.cardsCount)
builder.appendKeyValue("Has seed phrase", cardInfo.isImported.toString())
builder.appendKeyValue("Card Blockchain", cardInfo.cardBlockchain)
builder.appendSignedHashes(cardInfo.signedHashesList)
fun addUserWalletMetaInfo(walletMetaInfo: WalletMetaInfo) {
builder.appendKeyValue("Mobile Wallet is backed up", walletMetaInfo.hotWalletIsBackedUp?.toString())
builder.appendKeyValue("Card ID", walletMetaInfo.cardId)
builder.appendKeyValue("Firmware version", walletMetaInfo.firmwareVersion)
builder.appendKeyValue("Linked cards count", walletMetaInfo.cardsCount)
builder.appendKeyValue("Has seed phrase", walletMetaInfo.isImported?.toString())
builder.appendKeyValue("Card Blockchain", walletMetaInfo.cardBlockchain)
walletMetaInfo.signedHashesList?.let { builder.appendSignedHashes(it) }
}
fun addBlockchainInfoList(blockchainInfoList: List<BlockchainInfo>) {
@ -146,7 +147,7 @@ internal class FeedbackDataBuilder {
append("$keyValuePrefix$value\n")
}
private fun StringBuilder.appendSignedHashes(signedHashesList: List<CardInfo.SignedHashes>) {
private fun StringBuilder.appendSignedHashes(signedHashesList: List<WalletMetaInfo.SignedHashes>) {
signedHashesList.forEach {
appendKeyValue("Signed hashes [${it.curve}]", it.total)
}

View file

@ -1,23 +0,0 @@
package com.tangem.domain.feedback
import arrow.core.Either
import arrow.core.Either.Companion.catch
import com.tangem.domain.feedback.models.CardInfo
import com.tangem.domain.feedback.repository.FeedbackRepository
import com.tangem.domain.models.scan.ScanResponse
/**
* UseCase for creating 'CardInfo'
*
* @property feedbackRepository feedback repository
*
[REDACTED_AUTHOR]
*/
class GetCardInfoUseCase(
private val feedbackRepository: FeedbackRepository,
) {
operator fun invoke(scanResponse: ScanResponse): Either<Throwable, CardInfo> = catch {
feedbackRepository.getCardInfo(scanResponse)
}
}

View file

@ -0,0 +1,28 @@
package com.tangem.domain.feedback
import arrow.core.Either
import arrow.core.Either.Companion.catch
import com.tangem.domain.feedback.models.WalletMetaInfo
import com.tangem.domain.feedback.repository.FeedbackRepository
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.wallet.UserWalletId
/**
* UseCase for creating 'UserWalletMetaInfo' from [UserWalletId] or [ScanResponse]
*
* @property feedbackRepository feedback repository
*
[REDACTED_AUTHOR]
*/
class GetWalletMetaInfoUseCase(
private val feedbackRepository: FeedbackRepository,
) {
suspend operator fun invoke(userWalletId: UserWalletId): Either<Throwable, WalletMetaInfo> = catch {
feedbackRepository.getUserWalletMetaInfo(userWalletId)
}
operator fun invoke(scanResponse: ScanResponse): Either<Throwable, WalletMetaInfo> = catch {
feedbackRepository.getUserWalletMetaInfo(scanResponse)
}
}

View file

@ -37,8 +37,8 @@ class SendFeedbackEmailUseCase(
private fun getAddress(type: FeedbackEmailType): String {
return when {
type is FeedbackEmailType.Visa || type.cardInfo?.isVisa == true -> TANGEM_VISA_SUPPORT_EMAIL
type.cardInfo?.isStart2Coin == true -> START2COIN_SUPPORT_EMAIL
type is FeedbackEmailType.Visa || type.walletMetaInfo?.isVisa == true -> TANGEM_VISA_SUPPORT_EMAIL
type.walletMetaInfo?.isStart2Coin == true -> START2COIN_SUPPORT_EMAIL
else -> TANGEM_SUPPORT_EMAIL
}
}

View file

@ -7,7 +7,9 @@ import java.io.File
interface FeedbackRepository {
fun getCardInfo(scanResponse: ScanResponse): CardInfo
suspend fun getUserWalletMetaInfo(userWalletId: UserWalletId): WalletMetaInfo
fun getUserWalletMetaInfo(scanResponse: ScanResponse): WalletMetaInfo
fun getUserWalletsInfo(userWalletId: UserWalletId?): UserWalletsInfo

View file

@ -1,7 +1,7 @@
package com.tangem.domain.feedback.utils
import com.tangem.domain.feedback.FeedbackDataBuilder
import com.tangem.domain.feedback.models.CardInfo
import com.tangem.domain.feedback.models.WalletMetaInfo
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.feedback.repository.FeedbackRepository
import com.tangem.domain.visa.model.VisaTxDetails
@ -20,37 +20,40 @@ internal class EmailMessageBodyResolver(
/** Resolve email message body by [type] */
suspend fun resolve(type: FeedbackEmailType): String = with(FeedbackDataBuilder()) {
when (type) {
is FeedbackEmailType.DirectUserRequest -> addUserRequestBody(type.cardInfo)
is FeedbackEmailType.RateCanBeBetter -> addCardAndPhoneInfo(type.cardInfo)
is FeedbackEmailType.TransactionSendingProblem -> addTransactionSendingProblemBody(type.cardInfo)
is FeedbackEmailType.DirectUserRequest -> addUserRequestBody(type.walletMetaInfo)
is FeedbackEmailType.RateCanBeBetter -> addCardAndPhoneInfo(type.walletMetaInfo)
is FeedbackEmailType.TransactionSendingProblem -> addTransactionSendingProblemBody(type.walletMetaInfo)
is FeedbackEmailType.StakingProblem -> addStakingProblemBody(type)
is FeedbackEmailType.SwapProblem -> addSwapProblemBody(type)
is FeedbackEmailType.CurrencyDescriptionError -> addTokenInfo(type)
is FeedbackEmailType.PreActivatedWallet -> addUserRequestBody(type.cardInfo)
is FeedbackEmailType.PreActivatedWallet -> addUserRequestBody(type.walletMetaInfo)
is FeedbackEmailType.ScanningProblem,
is FeedbackEmailType.CardAttestationFailed,
-> addPhoneInfoBody()
is FeedbackEmailType.Visa.Activation -> addUserRequestBody(type.cardInfo)
is FeedbackEmailType.Visa.DirectUserRequest -> addUserRequestBody(type.cardInfo)
is FeedbackEmailType.Visa.Dispute -> addVisaRequestBody(type.cardInfo, type.visaTxDetails)
is FeedbackEmailType.Visa.Activation -> addUserRequestBody(type.walletMetaInfo)
is FeedbackEmailType.Visa.DirectUserRequest -> addUserRequestBody(type.walletMetaInfo)
is FeedbackEmailType.Visa.Dispute -> addVisaRequestBody(type.walletMetaInfo, type.visaTxDetails)
}
return build()
}
private suspend fun FeedbackDataBuilder.addVisaRequestBody(cardInfo: CardInfo, visaTxDetails: VisaTxDetails) {
addUserRequestBody(cardInfo)
private suspend fun FeedbackDataBuilder.addVisaRequestBody(
walletMetaInfo: WalletMetaInfo,
visaTxDetails: VisaTxDetails,
) {
addUserRequestBody(walletMetaInfo)
addDelimiter()
addVisaTxInfo(visaTxDetails)
}
private suspend fun FeedbackDataBuilder.addUserRequestBody(cardInfo: CardInfo) {
addUserWalletsInfo(userWalletsInfo = feedbackRepository.getUserWalletsInfo(cardInfo.userWalletId))
private suspend fun FeedbackDataBuilder.addUserRequestBody(walletMetaInfo: WalletMetaInfo) {
addUserWalletsInfo(userWalletsInfo = feedbackRepository.getUserWalletsInfo(walletMetaInfo.userWalletId))
addDelimiter()
addCardInfo(cardInfo)
addUserWalletMetaInfo(walletMetaInfo)
addDelimiter()
val userWalletId = cardInfo.userWalletId
val userWalletId = walletMetaInfo.userWalletId
if (userWalletId != null) {
val blockchainInfoList = feedbackRepository.getBlockchainInfoList(userWalletId)
@ -68,11 +71,11 @@ internal class EmailMessageBodyResolver(
addPhoneInfo(phoneInfo = feedbackRepository.getPhoneInfo())
}
private suspend fun FeedbackDataBuilder.addTransactionSendingProblemBody(cardInfo: CardInfo) {
addCardInfo(cardInfo)
private suspend fun FeedbackDataBuilder.addTransactionSendingProblemBody(walletMetaInfo: WalletMetaInfo) {
addUserWalletMetaInfo(walletMetaInfo)
addDelimiter()
val userWalletId = requireNotNull(cardInfo.userWalletId) { "UserWalletId must be not null" }
val userWalletId = requireNotNull(walletMetaInfo.userWalletId) { "UserWalletId must be not null" }
val blockchainError = feedbackRepository.getBlockchainErrorInfo(userWalletId = userWalletId)
val blockchainInfo = blockchainError?.let {
feedbackRepository.getBlockchainInfo(
@ -91,10 +94,10 @@ internal class EmailMessageBodyResolver(
}
private suspend fun FeedbackDataBuilder.addStakingProblemBody(type: FeedbackEmailType.StakingProblem) {
addCardInfo(type.cardInfo)
addUserWalletMetaInfo(type.walletMetaInfo)
addDelimiter()
val userWalletId = requireNotNull(type.cardInfo.userWalletId) { "UserWalletId must be not null" }
val userWalletId = requireNotNull(type.walletMetaInfo.userWalletId) { "UserWalletId must be not null" }
val blockchainError = feedbackRepository.getBlockchainErrorInfo(userWalletId = userWalletId)
val blockchainInfo = blockchainError?.let {
feedbackRepository.getBlockchainInfo(
@ -120,10 +123,10 @@ internal class EmailMessageBodyResolver(
}
private suspend fun FeedbackDataBuilder.addSwapProblemBody(type: FeedbackEmailType.SwapProblem) {
addCardInfo(type.cardInfo)
addUserWalletMetaInfo(type.walletMetaInfo)
addDelimiter()
val userWalletId = requireNotNull(type.cardInfo.userWalletId) { "UserWalletId must be not null" }
val userWalletId = requireNotNull(type.walletMetaInfo.userWalletId) { "UserWalletId must be not null" }
val blockchainError = feedbackRepository.getBlockchainErrorInfo(userWalletId = userWalletId)
val blockchainInfo = blockchainError?.let {
feedbackRepository.getBlockchainInfo(
@ -144,8 +147,8 @@ internal class EmailMessageBodyResolver(
addPhoneInfo(phoneInfo = feedbackRepository.getPhoneInfo())
}
private fun FeedbackDataBuilder.addCardAndPhoneInfo(cardInfo: CardInfo) {
addCardInfo(cardInfo)
private fun FeedbackDataBuilder.addCardAndPhoneInfo(walletMetaInfo: WalletMetaInfo) {
addUserWalletMetaInfo(walletMetaInfo)
addDelimiter()
addPhoneInfo(phoneInfo = feedbackRepository.getPhoneInfo())
}

View file

@ -18,7 +18,7 @@ internal class EmailSubjectResolver(private val resources: Resources) {
fun resolve(type: FeedbackEmailType): String {
return when (type) {
is FeedbackEmailType.DirectUserRequest -> {
if (type.cardInfo.isStart2Coin) {
if (type.walletMetaInfo.isStart2Coin == true) {
resources.getStringSafe(R.string.feedback_subject_support)
} else {
resources.getStringSafe(R.string.feedback_subject_support_tangem)

View file

@ -1,5 +1,8 @@
package com.tangem.domain.models.account
import arrow.core.Either
import arrow.core.raise.either
import arrow.core.raise.ensure
import com.tangem.common.extensions.toByteArray
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.utils.extensions.toHexString
@ -18,9 +21,39 @@ data class AccountId private constructor(
val userWalletId: UserWalletId,
) {
sealed interface Error {
val tag: String
get() = this::class.simpleName ?: "AccountId.Error"
data object Empty : Error {
override fun toString(): String = "$tag: Account ID cannot be blank"
}
data object InvalidFormat : Error {
override fun toString(): String = "$tag: Account ID must be a 64-character hexadecimal string"
}
}
companion object {
private val sha256Digest: MessageDigest by lazy { MessageDigest.getInstance("SHA-256") }
private val hexRegex = Regex("^[a-fA-F0-9]{64}$")
/**
* Creates a unique account identifier for a crypto portfolio
*
* @param userWalletId the identifier of the user wallet
* @param value the unique string value representing the account
*
* @return an [Either] containing the [AccountId] on success, or an [Error] on failure
*/
fun forCryptoPortfolio(userWalletId: UserWalletId, value: String): Either<Error, AccountId> = either {
ensure(value.isNotBlank()) { Error.Empty }
ensure(value.matches(hexRegex)) { Error.InvalidFormat }
AccountId(value = value, userWalletId = userWalletId)
}
/**
* Creates a unique account identifier for a crypto portfolio

View file

@ -1,32 +1,24 @@
package com.tangem.domain.nft
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.nft.repository.NFTRepository
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier
import com.tangem.domain.tokens.TokensFeatureToggles
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.wallets.repository.WalletsRepository
class DisableWalletNFTUseCase(
private val walletsRepository: WalletsRepository,
private val nftRepository: NFTRepository,
private val currenciesRepository: CurrenciesRepository,
private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
private val tokensFeatureToggles: TokensFeatureToggles,
) {
suspend operator fun invoke(userWalletId: UserWalletId) {
walletsRepository.disableNFT(userWalletId)
val currencies = if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) {
multiWalletCryptoCurrenciesSupplier.getSyncOrNull(
params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId),
)
.orEmpty()
} else {
currenciesRepository.getMultiCurrencyWalletCachedCurrenciesSync(userWalletId)
}
val currencies = multiWalletCryptoCurrenciesSupplier.getSyncOrNull(
params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId),
)
.orEmpty()
val networks = currencies.map { it.network }
nftRepository.clearCache(userWalletId, networks)

View file

@ -1,28 +1,20 @@
package com.tangem.domain.nft
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.nft.repository.NFTRepository
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier
import com.tangem.domain.tokens.TokensFeatureToggles
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.models.wallet.UserWalletId
class FetchNFTCollectionsUseCase(
private val currenciesRepository: CurrenciesRepository,
private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
private val tokensFeatureToggles: TokensFeatureToggles,
private val nftRepository: NFTRepository,
) {
suspend operator fun invoke(userWalletId: UserWalletId) {
val currencies = if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) {
multiWalletCryptoCurrenciesSupplier.getSyncOrNull(
params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId),
)
.orEmpty()
} else {
currenciesRepository.getMultiCurrencyWalletCurrenciesSync(userWalletId)
}
val currencies = multiWalletCryptoCurrenciesSupplier.getSyncOrNull(
params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId),
)
.orEmpty()
nftRepository.refreshCollections(userWalletId, currencies.map { it.network }.distinct())
}

View file

@ -1,29 +1,21 @@
package com.tangem.domain.nft
import arrow.core.Either
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.nft.repository.NFTRepository
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier
import com.tangem.domain.tokens.TokensFeatureToggles
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.models.wallet.UserWalletId
class RefreshAllNFTUseCase(
private val currenciesRepository: CurrenciesRepository,
private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
private val tokensFeatureToggles: TokensFeatureToggles,
private val nftRepository: NFTRepository,
) {
suspend operator fun invoke(userWalletId: UserWalletId): Either<Throwable, Unit> = Either.catch {
val currencies = if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) {
multiWalletCryptoCurrenciesSupplier.getSyncOrNull(
params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId),
)
.orEmpty()
} else {
currenciesRepository.getMultiCurrencyWalletCurrenciesSync(userWalletId)
}
val currencies = multiWalletCryptoCurrenciesSupplier.getSyncOrNull(
params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId),
)
.orEmpty()
nftRepository.refreshAll(userWalletId, currencies.map { it.network }.distinct())
}

View file

@ -1 +0,0 @@
/build

View file

@ -1,4 +0,0 @@
plugins {
alias(deps.plugins.kotlin.jvm)
id("configuration")
}

View file

@ -1,5 +0,0 @@
package com.tangem.domain.notifications.toggles
interface NotificationsFeatureToggles {
val isNotificationsEnabled: Boolean
}

View file

@ -4,6 +4,10 @@ plugins {
id("configuration")
}
tasks.withType<Test>().configureEach {
useJUnitPlatform()
}
dependencies {
/** Core modules */
implementation(projects.core.analytics.models)
@ -15,4 +19,11 @@ dependencies {
api(projects.domain.core)
api(projects.domain.settings)
implementation(deps.kotlin.serialization)
/** Tests */
testImplementation(deps.test.coroutine)
testImplementation(deps.test.junit5)
testRuntimeOnly(deps.test.junit5.engine)
testImplementation(deps.test.mockk)
testImplementation(deps.test.truth)
}

View file

@ -0,0 +1,23 @@
package com.tangem.domain.onramp.model
import java.math.BigDecimal
data class OnrampOffersBlock(
val category: OnrampOfferCategory,
val offers: List<OnrampOffer>,
val hasMoreOffers: Boolean,
)
data class OnrampOffer(
val quote: OnrampQuote,
val rateDif: BigDecimal?,
val advantages: OnrampOfferAdvantages = OnrampOfferAdvantages.Default,
)
enum class OnrampOfferAdvantages {
Default, BestRate, Fastest,
}
enum class OnrampOfferCategory {
Recent, Recommended,
}

View file

@ -13,27 +13,59 @@ data class OnrampPaymentMethod(
enum class PaymentMethodType(val id: String?) {
GOOGLE_PAY(id = "google-pay"),
CARD(id = "card"),
REVOLUT_PAY(id = "invoice-revolut-pay"),
SEPA(id = "sepa"),
OTHER(id = null),
;
@Suppress("MagicNumber")
fun getPriority(isGooglePayEnabled: Boolean): Int = if (isGooglePayEnabled) {
when (this) {
GOOGLE_PAY -> 0
CARD -> 1
OTHER -> 2
SEPA -> 2
REVOLUT_PAY -> 3
OTHER -> 4
}
} else {
when (this) {
CARD -> 0
GOOGLE_PAY -> 1
OTHER -> 2
SEPA -> 2
REVOLUT_PAY -> 3
OTHER -> 4
}
}
/**
* BE AWARE. HARDCODED. Returns the speed of transaction for payment method type.
*/
fun getProcessingSpeed(): PaymentSpeed = when (this) {
REVOLUT_PAY,
GOOGLE_PAY,
-> PaymentSpeed.Instant
CARD -> PaymentSpeed.FewMin
SEPA -> PaymentSpeed.FewDays
OTHER -> PaymentSpeed.PlentyDays
}
/**
* @param speed - the lower the value, the faster the speed.
*/
@Suppress("MagicNumber")
enum class PaymentSpeed(val speed: Int) {
Instant(0), FewMin(1), FewDays(2), PlentyDays(3), Unknown(4)
}
fun isInstant(): Boolean = getProcessingSpeed() == PaymentSpeed.Instant
companion object {
fun getType(id: String): PaymentMethodType = when (id) {
GOOGLE_PAY.id -> GOOGLE_PAY
CARD.id -> CARD
REVOLUT_PAY.id -> REVOLUT_PAY
SEPA.id -> SEPA
else -> OTHER
}
}

View file

@ -0,0 +1,19 @@
package com.tangem.domain.onramp.model
import java.math.BigDecimal
data class OnrampPaymentMethodGroup(
val paymentMethod: OnrampPaymentMethod,
val offers: List<OnrampOffer>,
val bestRateOffer: OnrampOffer?,
val providerCount: Int,
val isBestPaymentMethod: Boolean,
) {
val bestRateAmount: BigDecimal? = bestRateOffer?.let { offer ->
when (val quote = offer.quote) {
is OnrampQuote.Data -> quote.toAmount.value
else -> BigDecimal.ZERO
}
}
}

View file

@ -6,4 +6,5 @@ enum class OnrampSource(val analyticsName: String) {
TOKEN_LONG_TAP("Long Tap"),
TOKEN_DETAILS("Token"),
MARKETS("Markets"),
SEPA_BANNER("SEPA Banner"),
}

View file

@ -0,0 +1,81 @@
package com.tangem.domain.onramp
import arrow.core.left
import arrow.core.right
import com.tangem.domain.core.utils.EitherFlow
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.onramp.model.OnrampOffer
import com.tangem.domain.onramp.model.OnrampOfferAdvantages
import com.tangem.domain.onramp.model.OnrampPaymentMethodGroup
import com.tangem.domain.onramp.model.OnrampQuote
import com.tangem.domain.onramp.model.error.OnrampError
import com.tangem.domain.onramp.repositories.OnrampErrorResolver
import com.tangem.domain.onramp.repositories.OnrampRepository
import com.tangem.domain.onramp.utils.calculateRateDif
import com.tangem.domain.onramp.utils.compareOffersByRateSpeedAndPriority
import com.tangem.domain.settings.repositories.SettingsRepository
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map
import java.math.BigDecimal
class GetOnrampAllOffersUseCase(
private val onrampRepository: OnrampRepository,
private val errorResolver: OnrampErrorResolver,
private val settingsRepository: SettingsRepository,
) {
operator fun invoke(
userWalletId: UserWalletId,
cryptoCurrencyId: CryptoCurrency.ID,
): EitherFlow<OnrampError, List<OnrampPaymentMethodGroup>> {
return onrampRepository.getQuotes()
.map { quotes -> processAllOffers(quotes).right() }
.catch { throwable -> errorResolver.resolve(throwable).left() }
}
private suspend fun processAllOffers(quotes: List<OnrampQuote>): List<OnrampPaymentMethodGroup> {
val validQuotes = quotes.filterIsInstance<OnrampQuote.Data>()
if (validQuotes.isEmpty()) return emptyList()
val isGooglePayAvailable = settingsRepository.isGooglePayAvailability()
val overallBestRateQuote = validQuotes.maxWithOrNull(compareOffersByRateSpeedAndPriority(isGooglePayAvailable))
val bestRate = overallBestRateQuote?.toAmount?.value
val offersByPaymentMethod = validQuotes.groupBy { it.paymentMethod }
return offersByPaymentMethod.map { (paymentMethod, methodQuotes) ->
val methodOffers = methodQuotes.map { quote ->
val advantages = if (quote == overallBestRateQuote) {
OnrampOfferAdvantages.BestRate
} else {
OnrampOfferAdvantages.Default
}
val rateDif = calculateRateDif(quote.toAmount.value, bestRate)
OnrampOffer(quote = quote, rateDif = rateDif, advantages = advantages)
}
val groupBestRateOfferData =
methodQuotes.maxWithOrNull(compareOffersByRateSpeedAndPriority(isGooglePayAvailable))
val groupBestRateOffer = methodOffers.find {
when (val quote = it.quote) {
is OnrampQuote.Data -> quote == groupBestRateOfferData
else -> false
}
}
OnrampPaymentMethodGroup(
paymentMethod = paymentMethod,
offers = methodOffers.sortedByDescending { offer ->
when (val quote = offer.quote) {
is OnrampQuote.Data -> quote.toAmount.value
else -> BigDecimal.ZERO
}
},
providerCount = methodOffers.map { it.quote.provider.id }.distinct().size,
bestRateOffer = groupBestRateOffer,
isBestPaymentMethod = overallBestRateQuote?.paymentMethod == paymentMethod,
)
}.sortedBy { it.paymentMethod.type.getPriority(isGooglePayAvailable) }
}
}

View file

@ -0,0 +1,225 @@
package com.tangem.domain.onramp
import arrow.core.left
import arrow.core.right
import com.tangem.domain.core.utils.EitherFlow
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.onramp.model.*
import com.tangem.domain.onramp.model.cache.OnrampTransaction
import com.tangem.domain.onramp.model.error.OnrampError
import com.tangem.domain.onramp.repositories.OnrampErrorResolver
import com.tangem.domain.onramp.repositories.OnrampRepository
import com.tangem.domain.onramp.repositories.OnrampTransactionRepository
import com.tangem.domain.onramp.utils.calculateRateDif
import com.tangem.domain.onramp.utils.compareOffersByRateSpeedAndPriority
import com.tangem.domain.settings.repositories.SettingsRepository
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
class GetOnrampOffersUseCase(
private val onrampRepository: OnrampRepository,
private val onrampTransactionRepository: OnrampTransactionRepository,
private val errorResolver: OnrampErrorResolver,
private val settingsRepository: SettingsRepository,
) {
operator fun invoke(
userWalletId: UserWalletId,
cryptoCurrencyId: CryptoCurrency.ID,
): EitherFlow<OnrampError, List<OnrampOffersBlock>> {
return combine(
onrampRepository.getQuotes(),
onrampTransactionRepository.getTransactions(userWalletId, cryptoCurrencyId),
) { quotes, transactions ->
processOffers(quotes, transactions)
}
.map { offers -> offers.right() }
.catch { throwable -> errorResolver.resolve(throwable).left() }
}
private suspend fun processOffers(
quotes: List<OnrampQuote>,
transactions: List<OnrampTransaction>,
): List<OnrampOffersBlock> {
val validQuotes = quotes.filterIsInstance<OnrampQuote.Data>()
if (validQuotes.isEmpty()) return emptyList()
val isGooglePayAvailable = settingsRepository.isGooglePayAvailability()
val bestRateQuote = validQuotes.maxWithOrNull(compareOffersByRateSpeedAndPriority(isGooglePayAvailable))
val bestRate = bestRateQuote?.toAmount?.value
val offers = validQuotes.map { quote ->
val rateDif = calculateRateDif(quote.toAmount.value, bestRate)
OnrampOffer(quote = quote, rateDif = rateDif)
}
val recentOffer = findRecentOffer(offers, transactions)
val bestRateOffer = findBestRateOffer(offers, isGooglePayAvailable)
val fastestOffer = findFastestOffer(offers, isGooglePayAvailable)
return buildOffersBlocks(
recentOffer = recentOffer,
bestRateOffer = bestRateOffer,
fastestOffer = fastestOffer,
allOffers = offers,
)
}
private fun findRecentOffer(offers: List<OnrampOffer>, transactions: List<OnrampTransaction>): OnrampOffer? {
val lastTransaction = transactions.maxByOrNull { it.timestamp } ?: return null
return offers.find { offer ->
offer.quote.provider.id == lastTransaction.providerType &&
offer.quote.paymentMethod.id == lastTransaction.paymentMethod
}
}
private fun findBestRateOffer(offers: List<OnrampOffer>, isGooglePayAvailable: Boolean): OnrampOffer? {
return offers.maxWithOrNull(offerComparator(isGooglePayAvailable))
}
private fun findFastestOffer(offers: List<OnrampOffer>, isGooglePayAvailable: Boolean): OnrampOffer? {
val instantOffers = offers.filter { it.quote.paymentMethod.type.isInstant() }
return if (instantOffers.isNotEmpty()) {
instantOffers.maxWithOrNull(offerComparator(isGooglePayAvailable))
} else {
val offersBySpeed = offers.groupBy { offer ->
offer.quote.paymentMethod.type.getProcessingSpeed().speed
}
val fastestSpeed = offersBySpeed.keys.minOrNull() ?: return null
val fastestOffers = offersBySpeed[fastestSpeed] ?: return null
fastestOffers.maxWithOrNull(offerComparator(isGooglePayAvailable))
}
}
private fun offerComparator(isGooglePayAvailable: Boolean): Comparator<OnrampOffer> = Comparator { offer1, offer2 ->
when (val quote1 = offer1.quote) {
is OnrampQuote.Data -> {
when (val quote2 = offer2.quote) {
is OnrampQuote.Data -> {
compareOffersByRateSpeedAndPriority(isGooglePayAvailable).compare(quote1, quote2)
}
else -> 1
}
}
else -> -1
}
}
private fun buildOffersBlocks(
recentOffer: OnrampOffer?,
bestRateOffer: OnrampOffer?,
fastestOffer: OnrampOffer?,
allOffers: List<OnrampOffer>,
): List<OnrampOffersBlock> {
val recommendedOffers = buildRecommendedOffers(
recentOffer = recentOffer,
bestRateOffer = bestRateOffer,
fastestOffer = fastestOffer,
)
val shownOffersCount = (if (recentOffer != null) 1 else 0) + recommendedOffers.size
val hasMoreOffers = allOffers.size > shownOffersCount
return buildList {
if (recentOffer != null) {
add(
OnrampOffersBlock(
category = OnrampOfferCategory.Recent,
offers = listOf(
recentOffer.copy(
advantages = determineAdvantages(
recentOffer,
bestRateOffer,
fastestOffer,
),
rateDif = if (bestRateOffer != null) recentOffer.rateDif else null,
),
),
hasMoreOffers = false,
),
)
}
if (recommendedOffers.isNotEmpty() && hasOnlyOneMethodAndProvider(allOffers).not()) {
add(
OnrampOffersBlock(
category = OnrampOfferCategory.Recommended,
offers = recommendedOffers,
hasMoreOffers = hasMoreOffers,
),
)
}
}
}
private fun determineAdvantages(
recentOffer: OnrampOffer,
bestRateOffer: OnrampOffer?,
fastestOffer: OnrampOffer?,
): OnrampOfferAdvantages {
if (isSameOffer(recentOffer, bestRateOffer) && isSameOffer(recentOffer, fastestOffer)) {
return OnrampOfferAdvantages.BestRate
}
if (isSameOffer(recentOffer, bestRateOffer)) {
return OnrampOfferAdvantages.BestRate
}
if (isSameOffer(recentOffer, fastestOffer)) {
return OnrampOfferAdvantages.Fastest
}
return OnrampOfferAdvantages.Default
}
private fun buildRecommendedOffers(
recentOffer: OnrampOffer?,
bestRateOffer: OnrampOffer?,
fastestOffer: OnrampOffer?,
): List<OnrampOffer> {
return buildList {
if (isSameOffer(bestRateOffer, fastestOffer)) {
bestRateOffer?.let { offer ->
add(
offer.copy(
advantages = OnrampOfferAdvantages.BestRate,
rateDif = null,
),
)
}
} else {
if (bestRateOffer != null && !isSameOffer(bestRateOffer, recentOffer)) {
add(
bestRateOffer.copy(
advantages = OnrampOfferAdvantages.BestRate,
rateDif = null,
),
)
}
if (fastestOffer != null && !isSameOffer(fastestOffer, recentOffer) &&
!isSameOffer(fastestOffer, bestRateOffer)
) {
add(
fastestOffer.copy(
advantages = OnrampOfferAdvantages.Fastest,
rateDif = if (bestRateOffer != null) fastestOffer.rateDif else null,
),
)
}
}
}
}
private fun hasOnlyOneMethodAndProvider(offers: List<OnrampOffer>): Boolean {
val uniquePaymentMethods = offers.map { it.quote.paymentMethod.id }.distinct()
val uniqueProviders = offers.map { it.quote.provider.id }.distinct()
return uniquePaymentMethods.size == 1 && uniqueProviders.size == 1
}
private fun isSameOffer(offer1: OnrampOffer?, offer2: OnrampOffer?): Boolean {
if (offer1 == null || offer2 == null) return false
return offer1.quote.provider.id == offer2.quote.provider.id &&
offer1.quote.paymentMethod.id == offer2.quote.paymentMethod.id
}
}

View file

@ -0,0 +1,80 @@
package com.tangem.domain.onramp
import arrow.core.Either
import arrow.core.getOrElse
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.onramp.repositories.OnrampRepository
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.onramp.model.OnrampCountry
import com.tangem.domain.onramp.model.OnrampCurrency
class OnrampSepaAvailableUseCase(
private val repository: OnrampRepository,
) {
suspend operator fun invoke(
userWallet: UserWallet,
currency: OnrampCurrency,
country: OnrampCountry,
cryptoCurrency: CryptoCurrency,
): Boolean {
if (country.code !in SEPA_AVAILABLE_COUNTRY_CODES) {
return false
}
return Either.catch {
repository.hasMercuryoSepaMethod(
userWallet = userWallet,
currency = currency,
country = country,
cryptoCurrency = cryptoCurrency,
)
}.getOrElse { false }
}
companion object {
val SEPA_AVAILABLE_COUNTRY_CODES = listOf(
"AL", // Albania
"AD", // Andorra
"AT", // Austria
"BE", // Belgium
"BG", // Bulgaria
"HR", // Croatia
"CY", // Cyprus
"CZ", // Czech Republic
"DK", // Denmark
"EE", // Estonia
"FI", // Finland
"FR", // France
"DE", // Germany
"GR", // Greece
"HU", // Hungary
"IS", // Iceland
"IE", // Ireland
"IT", // Italy
"LV", // Latvia
"LI", // Liechtenstein
"LT", // Lithuania
"LU", // Luxembourg
"MT", // Malta
"MD", // Moldova
"MC", // Monaco
"ME", // Montenegro
"NL", // Netherlands
"MK", // North Macedonia
"NO", // Norway
"PL", // Poland
"PT", // Portugal
"RO", // Romania
"SM", // San Marino
"RS", // Serbia
"SK", // Slovakia
"SI", // Slovenia
"ES", // Spain
"SE", // Sweden
"CH", // Switzerland
"GB", // United Kingdom
"VA", // Vatican City
)
}
}

View file

@ -163,4 +163,35 @@ sealed class OnrampAnalyticsEvent(
ERROR_DESCRIPTION to errorDescription,
),
)
data class FastestBuyMethodClicked(
private val tokenSymbol: String,
private val providerName: String,
private val paymentMethod: String,
) : OnrampAnalyticsEvent(
event = "Fastest Method Clicked",
params = mapOf(
TOKEN_PARAM to tokenSymbol,
PROVIDER to providerName,
PAYMENT_METHOD to paymentMethod,
),
)
data class BestRateClicked(
private val tokenSymbol: String,
private val providerName: String,
private val paymentMethod: String,
) : OnrampAnalyticsEvent(
event = "Best Rate Clicked",
params = mapOf(
TOKEN_PARAM to tokenSymbol,
PROVIDER to providerName,
PAYMENT_METHOD to paymentMethod,
),
)
data object AllOffersClicked : OnrampAnalyticsEvent(
event = "Button - All Offers",
params = emptyMap(),
)
}

View file

@ -15,6 +15,12 @@ interface OnrampRepository {
suspend fun getCountriesSync(): List<OnrampCountry>?
suspend fun getCountryByIp(userWallet: UserWallet): OnrampCountry
suspend fun getStatus(userWallet: UserWallet, txId: String): OnrampStatus
suspend fun hasMercuryoSepaMethod(
userWallet: UserWallet,
currency: OnrampCurrency,
country: OnrampCountry,
cryptoCurrency: CryptoCurrency,
): Boolean
suspend fun fetchCurrencies(userWallet: UserWallet)
suspend fun fetchCountries(userWallet: UserWallet): List<OnrampCountry>
suspend fun fetchPaymentMethodsIfAbsent(userWallet: UserWallet)

View file

@ -0,0 +1,34 @@
package com.tangem.domain.onramp.utils
import com.tangem.domain.onramp.model.OnrampQuote
import java.math.BigDecimal
internal fun calculateRateDif(currentTokenRate: BigDecimal, bestRate: BigDecimal?): BigDecimal? {
if (bestRate == null) return null
return BigDecimal.ONE - currentTokenRate / bestRate
}
internal fun compareOffersByRateSpeedAndPriority(isGooglePayAvailable: Boolean): Comparator<OnrampQuote.Data> {
return Comparator { quote1, quote2 ->
val rateComparison = quote1
.toAmount
.value
.compareTo(quote2.toAmount.value)
if (rateComparison != 0) return@Comparator rateComparison
val speedComparison =
quote2
.paymentMethod
.type
.getProcessingSpeed()
.speed
.compareTo(quote1.paymentMethod.type.getProcessingSpeed().speed)
if (speedComparison != 0) return@Comparator speedComparison
quote1
.paymentMethod
.type
.getPriority(isGooglePayAvailable)
.compareTo(quote2.paymentMethod.type.getPriority(isGooglePayAvailable))
}
}

View file

@ -0,0 +1,177 @@
package com.tangem.domain.onramp
import com.google.common.truth.Truth
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.onramp.model.OnrampOfferAdvantages
import com.tangem.domain.onramp.model.OnrampPaymentMethod
import com.tangem.domain.onramp.model.OnrampProvider
import com.tangem.domain.onramp.model.OnrampQuote
import com.tangem.domain.onramp.repositories.OnrampErrorResolver
import com.tangem.domain.onramp.repositories.OnrampRepository
import com.tangem.domain.settings.repositories.SettingsRepository
import io.mockk.*
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import java.math.BigDecimal
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class GetOnrampAllOffersUseCaseTest {
private val onrampRepository: OnrampRepository = mockk(relaxUnitFun = true)
private val errorResolver: OnrampErrorResolver = mockk(relaxUnitFun = true)
private val settingsRepository: SettingsRepository = mockk(relaxUnitFun = true)
private val cryptoCurrencyId: CryptoCurrency.ID = mockk(relaxUnitFun = true)
private val userWalletId: UserWalletId = mockk(relaxUnitFun = true)
private lateinit var useCase: GetOnrampAllOffersUseCase
@BeforeEach
fun setup() {
clearMocks(onrampRepository, errorResolver, settingsRepository, cryptoCurrencyId)
useCase = GetOnrampAllOffersUseCase(
onrampRepository = onrampRepository,
errorResolver = errorResolver,
settingsRepository = settingsRepository,
)
}
@Test
fun `invoke should return empty list when no valid quotes`() = runTest {
val emptyQuotes = listOf<OnrampQuote>()
coEvery { onrampRepository.getQuotes() } returns flowOf(emptyQuotes)
val result = useCase(userWalletId, cryptoCurrencyId)
result.collect { either ->
Truth.assertThat(either.isRight()).isTrue()
either.fold(
ifLeft = { error -> Truth.assertThat(error).isNull() },
ifRight = { offers -> Truth.assertThat(offers).isEmpty() },
)
}
coVerify { onrampRepository.getQuotes() }
}
@Test
fun `invoke should return grouped offers with best rate marked`() = runTest {
val paymentMethod1 = createMockPaymentMethod("card", "Card")
val paymentMethod2 = createMockPaymentMethod("bank", "Bank Transfer")
val provider1 = createMockProvider("provider1", "Provider 1")
val provider2 = createMockProvider("provider2", "Provider 2")
val quotes = listOf(
createMockQuote(paymentMethod1, provider1, BigDecimal("100.0")),
createMockQuote(paymentMethod1, provider2, BigDecimal("95.0")),
createMockQuote(paymentMethod2, provider1, BigDecimal("98.0")),
)
coEvery { onrampRepository.getQuotes() } returns flowOf(quotes)
coEvery { settingsRepository.isGooglePayAvailability() } returns false
val result = useCase(userWalletId, cryptoCurrencyId)
result.collect { either ->
Truth.assertThat(either.isRight()).isTrue()
either.fold(
ifLeft = { error -> Truth.assertThat(error).isNull() },
ifRight = { offers ->
Truth.assertThat(offers).hasSize(2)
val cardGroup = offers.find { it.paymentMethod.id == "card" }
Truth.assertThat(cardGroup).isNotNull()
Truth.assertThat(cardGroup?.offers).hasSize(2)
Truth.assertThat(cardGroup?.providerCount).isEqualTo(2)
Truth.assertThat(cardGroup?.isBestPaymentMethod).isTrue()
val bestRateOffer = cardGroup?.offers?.find { it.advantages == OnrampOfferAdvantages.BestRate }
Truth.assertThat(bestRateOffer).isNotNull()
val bankGroup = offers.find { it.paymentMethod.id == "bank" }
Truth.assertThat(bankGroup).isNotNull()
Truth.assertThat(bankGroup?.offers).hasSize(1)
Truth.assertThat(bankGroup?.providerCount).isEqualTo(1)
Truth.assertThat(bankGroup?.isBestPaymentMethod).isFalse()
},
)
}
coVerify { onrampRepository.getQuotes() }
coVerify { settingsRepository.isGooglePayAvailability() }
}
@Test
fun `invoke should sort offers by toAmount descending`() = runTest {
val paymentMethod = createMockPaymentMethod("card", "Card")
val provider = createMockProvider("provider1", "Provider 1")
val quotes = listOf(
createMockQuote(paymentMethod, provider, BigDecimal("90.0")),
createMockQuote(paymentMethod, provider, BigDecimal("100.0")),
createMockQuote(paymentMethod, provider, BigDecimal("95.0")),
)
coEvery { onrampRepository.getQuotes() } returns flowOf(quotes)
coEvery { settingsRepository.isGooglePayAvailability() } returns false
val result = useCase(userWalletId, cryptoCurrencyId)
result.collect { either ->
Truth.assertThat(either.isRight()).isTrue()
either.fold(
ifLeft = { error -> Truth.assertThat(error).isNull() },
ifRight = { offers ->
Truth.assertThat(offers).hasSize(1)
val group = offers.first()
Truth.assertThat(group.offers).hasSize(3)
val amounts = group.offers.map { offer ->
when (val quote = offer.quote) {
is OnrampQuote.Data -> quote.toAmount.value
else -> BigDecimal.ZERO
}
}
Truth.assertThat(amounts).containsExactly(
BigDecimal("100.0"),
BigDecimal("95.0"),
BigDecimal("90.0"),
).inOrder()
},
)
}
}
private fun createMockPaymentMethod(id: String, name: String): OnrampPaymentMethod {
return mockk<OnrampPaymentMethod> {
every { this@mockk.id } returns id
every { this@mockk.name } returns name
every { this@mockk.type } returns mockk {
every { getPriority(any()) } returns 1
}
}
}
private fun createMockProvider(id: String, name: String): OnrampProvider {
return mockk<OnrampProvider> {
every { this@mockk.id } returns id
every { this@mockk.info.name } returns name
}
}
private fun createMockQuote(
paymentMethod: OnrampPaymentMethod,
provider: OnrampProvider,
toAmount: BigDecimal,
): OnrampQuote.Data {
return mockk<OnrampQuote.Data> {
every { this@mockk.paymentMethod } returns paymentMethod
every { this@mockk.provider } returns provider
every { this@mockk.toAmount } returns mockk {
every { value } returns toAmount
}
}
}
}

View file

@ -0,0 +1,288 @@
package com.tangem.domain.onramp
import com.google.common.truth.Truth
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.onramp.model.*
import com.tangem.domain.onramp.model.cache.OnrampTransaction
import com.tangem.domain.onramp.repositories.OnrampErrorResolver
import com.tangem.domain.onramp.repositories.OnrampRepository
import com.tangem.domain.onramp.repositories.OnrampTransactionRepository
import com.tangem.domain.settings.repositories.SettingsRepository
import io.mockk.*
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import java.math.BigDecimal
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class GetOnrampOffersUseCaseTest {
private val onrampRepository: OnrampRepository = mockk(relaxUnitFun = true)
private val onrampTransactionRepository: OnrampTransactionRepository = mockk(relaxUnitFun = true)
private val errorResolver: OnrampErrorResolver = mockk(relaxUnitFun = true)
private val settingsRepository: SettingsRepository = mockk(relaxUnitFun = true)
private val cryptoCurrencyId: CryptoCurrency.ID = mockk(relaxUnitFun = true)
private val userWalletId: UserWalletId = mockk(relaxUnitFun = true)
private lateinit var useCase: GetOnrampOffersUseCase
@BeforeEach
fun setup() {
clearMocks(onrampRepository, onrampTransactionRepository, errorResolver, cryptoCurrencyId)
useCase = GetOnrampOffersUseCase(
onrampRepository = onrampRepository,
onrampTransactionRepository = onrampTransactionRepository,
errorResolver = errorResolver,
settingsRepository = settingsRepository,
)
}
@Test
fun `invoke should return empty list when no valid quotes`() = runTest {
val emptyQuotes = listOf<OnrampQuote>()
val emptyTransactions = listOf<OnrampTransaction>()
coEvery { settingsRepository.isGooglePayAvailability() } returns false
coEvery { onrampRepository.getQuotes() } returns flowOf(emptyQuotes)
coEvery { onrampTransactionRepository.getTransactions(userWalletId, cryptoCurrencyId) } returns flowOf(
emptyTransactions,
)
val result = useCase(userWalletId, cryptoCurrencyId)
result.collect { either ->
Truth.assertThat(either.isRight()).isTrue()
either.fold(
ifLeft = { error -> Truth.assertThat(error).isNull() },
ifRight = { offers -> Truth.assertThat(offers).isEmpty() },
)
}
coVerify { onrampRepository.getQuotes() }
coVerify { onrampTransactionRepository.getTransactions(userWalletId, cryptoCurrencyId) }
}
@Test
fun `invoke should return offers blocks with recent and recommended categories`() = runTest {
val paymentMethod1 = createMockPaymentMethod("card", "Card", isInstant = true)
val paymentMethod2 = createMockPaymentMethod("bank", "Bank Transfer", isInstant = false)
val provider1 = createMockProvider("provider1", "Provider 1")
val provider2 = createMockProvider("provider2", "Provider 2")
val quotes = listOf(
createMockQuote(paymentMethod1, provider1, BigDecimal("90.0")),
createMockQuote(paymentMethod2, provider2, BigDecimal("100.0")),
)
val transactions = listOf(
createMockTransaction("provider1", "card", 1000L),
)
coEvery { settingsRepository.isGooglePayAvailability() } returns false
coEvery { onrampRepository.getQuotes() } returns flowOf(quotes)
coEvery { onrampTransactionRepository.getTransactions(userWalletId, cryptoCurrencyId) } returns flowOf(
transactions,
)
val result = useCase(userWalletId, cryptoCurrencyId)
result.collect { either ->
Truth.assertThat(either.isRight()).isTrue()
either.fold(
ifLeft = { error -> Truth.assertThat(error).isNull() },
ifRight = { offers ->
Truth.assertThat(offers).hasSize(2)
val recentBlock = offers.find { it.category == OnrampOfferCategory.Recent }
Truth.assertThat(recentBlock).isNotNull()
Truth.assertThat(recentBlock?.offers).hasSize(1)
Truth.assertThat(recentBlock?.offers?.first()?.advantages).isEqualTo(OnrampOfferAdvantages.Fastest)
val recommendedBlock = offers.find { it.category == OnrampOfferCategory.Recommended }
Truth.assertThat(recommendedBlock).isNotNull()
Truth.assertThat(recommendedBlock?.offers).hasSize(1)
Truth.assertThat(recommendedBlock?.offers?.first()?.advantages)
.isEqualTo(OnrampOfferAdvantages.BestRate)
},
)
}
}
@Test
fun `invoke should find best rate offer correctly`() = runTest {
val paymentMethod1 = createMockPaymentMethod("card", "Card", isInstant = false)
val paymentMethod2 = createMockPaymentMethod("bank", "Bank Transfer", isInstant = false)
val provider1 = createMockProvider("provider1", "Provider 1")
val provider2 = createMockProvider("provider2", "Provider 2")
val quotes = listOf(
createMockQuote(paymentMethod1, provider1, BigDecimal("90.0")),
createMockQuote(paymentMethod2, provider2, BigDecimal("100.0")),
createMockQuote(paymentMethod1, provider1, BigDecimal("95.0")),
)
val transactions = emptyList<OnrampTransaction>()
coEvery { settingsRepository.isGooglePayAvailability() } returns false
coEvery { onrampRepository.getQuotes() } returns flowOf(quotes)
coEvery { onrampTransactionRepository.getTransactions(userWalletId, cryptoCurrencyId) } returns flowOf(
transactions,
)
val result = useCase(userWalletId, cryptoCurrencyId)
result.collect { either ->
Truth.assertThat(either.isRight()).isTrue()
either.fold(
ifLeft = { error -> Truth.assertThat(error).isNull() },
ifRight = { offers ->
Truth.assertThat(offers).hasSize(1)
val recommendedBlock = offers.find { it.category == OnrampOfferCategory.Recommended }
Truth.assertThat(recommendedBlock).isNotNull()
Truth.assertThat(recommendedBlock?.offers).hasSize(1)
val bestRateOffer = recommendedBlock?.offers?.first()
Truth.assertThat(bestRateOffer?.advantages).isEqualTo(OnrampOfferAdvantages.BestRate)
when (val quote = bestRateOffer?.quote) {
is OnrampQuote.Data -> Truth.assertThat(quote.toAmount.value).isEqualTo(BigDecimal("100.0"))
else -> Truth.assertThat(false).isTrue()
}
},
)
}
}
@Test
fun `invoke should find fastest offer correctly`() = runTest {
val instantPaymentMethod = createMockPaymentMethod("card", "Card", isInstant = true)
val slowPaymentMethod = createMockPaymentMethod("bank", "Bank Transfer", isInstant = false)
val provider1 = createMockProvider("provider1", "Provider 1")
val provider2 = createMockProvider("provider2", "Provider 2")
val quotes = listOf(
createMockQuote(instantPaymentMethod, provider1, BigDecimal("90.0")),
createMockQuote(slowPaymentMethod, provider2, BigDecimal("100.0")),
)
val transactions = emptyList<OnrampTransaction>()
coEvery { settingsRepository.isGooglePayAvailability() } returns false
coEvery { onrampRepository.getQuotes() } returns flowOf(quotes)
coEvery { onrampTransactionRepository.getTransactions(userWalletId, cryptoCurrencyId) } returns flowOf(
transactions,
)
val result = useCase(userWalletId, cryptoCurrencyId)
result.collect { either ->
Truth.assertThat(either.isRight()).isTrue()
either.fold(
ifLeft = { error -> Truth.assertThat(error).isNull() },
ifRight = { offers ->
Truth.assertThat(offers).hasSize(1)
val recommendedBlock = offers.find { it.category == OnrampOfferCategory.Recommended }
Truth.assertThat(recommendedBlock).isNotNull()
Truth.assertThat(recommendedBlock?.offers).hasSize(2)
val bestRateOffer = recommendedBlock
?.offers
?.find { it.advantages == OnrampOfferAdvantages.BestRate }
val fastestOffer = recommendedBlock
?.offers
?.find { it.advantages == OnrampOfferAdvantages.Fastest }
Truth.assertThat(bestRateOffer).isNotNull()
Truth.assertThat(fastestOffer).isNotNull()
when (val quote = bestRateOffer?.quote) {
is OnrampQuote.Data -> Truth.assertThat(quote.toAmount.value).isEqualTo(BigDecimal("100.0"))
else -> Truth.assertThat(false).isTrue()
}
when (val quote = fastestOffer?.quote) {
is OnrampQuote.Data -> Truth.assertThat(quote.toAmount.value).isEqualTo(BigDecimal("90.0"))
else -> Truth.assertThat(false).isTrue()
}
},
)
}
}
@Test
fun `invoke should not show recommended block when only one method and provider`() = runTest {
val paymentMethod = createMockPaymentMethod("card", "Card", isInstant = false)
val provider = createMockProvider("provider1", "Provider 1")
val quotes = listOf(
createMockQuote(paymentMethod, provider, BigDecimal("100.0")),
)
val transactions = emptyList<OnrampTransaction>()
coEvery { settingsRepository.isGooglePayAvailability() } returns false
coEvery { onrampRepository.getQuotes() } returns flowOf(quotes)
coEvery { onrampTransactionRepository.getTransactions(userWalletId, cryptoCurrencyId) } returns flowOf(
transactions,
)
val result = useCase(userWalletId, cryptoCurrencyId)
result.collect { either ->
Truth.assertThat(either.isRight()).isTrue()
either.fold(
ifLeft = { error -> Truth.assertThat(error).isNull() },
ifRight = { offers ->
Truth.assertThat(offers).isEmpty()
},
)
}
}
private fun createMockPaymentMethod(id: String, name: String, isInstant: Boolean): OnrampPaymentMethod {
return mockk<OnrampPaymentMethod> {
every { this@mockk.id } returns id
every { this@mockk.name } returns name
every { this@mockk.type } returns mockk {
every { isInstant() } returns isInstant
every { getProcessingSpeed() } returns mockk {
every { speed } returns if (isInstant) 1 else 3
}
}
}
}
private fun createMockProvider(id: String, name: String): OnrampProvider {
return mockk<OnrampProvider> {
every { this@mockk.id } returns id
every { this@mockk.info.name } returns name
}
}
private fun createMockQuote(
paymentMethod: OnrampPaymentMethod,
provider: OnrampProvider,
toAmount: BigDecimal,
): OnrampQuote.Data {
return mockk<OnrampQuote.Data> {
every { this@mockk.paymentMethod } returns paymentMethod
every { this@mockk.provider } returns provider
every { this@mockk.toAmount } returns mockk {
every { value } returns toAmount
}
}
}
private fun createMockTransaction(providerType: String, paymentMethod: String, timestamp: Long): OnrampTransaction {
return mockk<OnrampTransaction> {
every { this@mockk.providerType } returns providerType
every { this@mockk.paymentMethod } returns paymentMethod
every { this@mockk.timestamp } returns timestamp
}
}
}

View file

@ -27,4 +27,5 @@ data class PromoBanner(
enum class PromoId {
Referral,
Sepa,
}

View file

@ -3,13 +3,13 @@ package com.tangem.domain.settings
import arrow.core.Either
import com.tangem.domain.settings.repositories.SettingsRepository
class SetSaveWalletScreenShownUseCase(
class SetAskBiometryShownUseCase(
private val settingsRepository: SettingsRepository,
) {
suspend operator fun invoke(): Either<Throwable, Unit> {
return Either.catch {
settingsRepository.setShouldShowSaveUserWalletScreen(value = false)
settingsRepository.setShouldShowAskBiometry(value = false)
}
}
}

View file

@ -2,7 +2,7 @@ package com.tangem.domain.settings
import com.tangem.domain.settings.repositories.SettingsRepository
class ShouldShowSaveWalletScreenUseCase(private val settingsRepository: SettingsRepository) {
class ShouldShowAskBiometryUseCase(private val settingsRepository: SettingsRepository) {
suspend operator fun invoke(): Boolean = settingsRepository.shouldShowSaveUserWalletScreen()
suspend operator fun invoke(): Boolean = settingsRepository.shouldShowAskBiometry()
}

View file

@ -7,9 +7,9 @@ import kotlinx.coroutines.flow.StateFlow
@Suppress("TooManyFunctions")
interface SettingsRepository {
suspend fun shouldShowSaveUserWalletScreen(): Boolean
suspend fun shouldShowAskBiometry(): Boolean
suspend fun setShouldShowSaveUserWalletScreen(value: Boolean)
suspend fun setShouldShowAskBiometry(value: Boolean)
suspend fun isWalletScrollPreviewEnabled(): Boolean

View file

@ -9,24 +9,24 @@ sealed class TokenSwapPromoAnalyticsEvent(
) : AnalyticsEvent(category = "Promotion", event = event, params = params) {
class NoticePromotionBanner(
source: AnalyticsParam.ScreensSources,
programName: ProgramName,
program: Program,
) : TokenSwapPromoAnalyticsEvent(
event = "Notice - Promotion Banner",
params = mapOf(
AnalyticsParam.SOURCE to source.value,
"Program Name" to programName.name,
"Program Name" to program.programName,
),
)
class PromotionBannerClicked(
source: AnalyticsParam.ScreensSources,
programName: ProgramName,
program: Program,
action: BannerAction,
) : TokenSwapPromoAnalyticsEvent(
event = "Promo Banner Clicked",
params = mapOf(
AnalyticsParam.SOURCE to source.value,
"Program Name" to programName.name,
"Program Name" to program.programName,
"Action" to action.action,
),
) {
@ -37,7 +37,8 @@ sealed class TokenSwapPromoAnalyticsEvent(
}
// Use it on new promo action
enum class ProgramName {
Empty,
enum class Program(val programName: String) {
Empty("Empty"),
Sepa("Sepa"),
}
}

View file

@ -33,7 +33,6 @@ class AddCryptoCurrenciesUseCase(
private val singleYieldBalanceFetcher: SingleYieldBalanceFetcher,
private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
private val stakingIdFactory: StakingIdFactory,
private val tokensFeatureToggles: TokensFeatureToggles,
) {
/**
@ -92,15 +91,11 @@ class AddCryptoCurrenciesUseCase(
): Either<Throwable, CryptoCurrency> = either {
val existingCurrencies = catch(
block = {
if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) {
multiWalletCryptoCurrenciesSupplier.getSyncOrNull(
params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId),
)
.orEmpty()
.toList()
} else {
currenciesRepository.getMultiCurrencyWalletCurrenciesSync(userWalletId)
}
multiWalletCryptoCurrenciesSupplier.getSyncOrNull(
params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId),
)
.orEmpty()
.toList()
},
catch = ::raise,
)

View file

@ -18,7 +18,6 @@ import kotlinx.coroutines.withContext
class ApplyTokenListSortingUseCase(
private val currenciesRepository: CurrenciesRepository,
private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
private val tokensFeatureToggles: TokensFeatureToggles,
private val dispatchers: CoroutineDispatcherProvider,
) {
@ -88,14 +87,10 @@ class ApplyTokenListSortingUseCase(
private suspend fun Raise<TokenListSortingError>.getCurrencies(userWalletId: UserWalletId): List<CryptoCurrency> {
val tokens = catch(
block = {
if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) {
multiWalletCryptoCurrenciesSupplier.getSyncOrNull(
params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId),
)
.orEmpty()
} else {
currenciesRepository.getMultiCurrencyWalletCurrenciesSync(userWalletId, refresh = false)
}
multiWalletCryptoCurrenciesSupplier.getSyncOrNull(
params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId),
)
.orEmpty()
},
catch = { raise(TokenListSortingError.DataError(it)) },
)

View file

@ -35,7 +35,6 @@ class FetchCurrencyStatusUseCase(
private val singleYieldBalanceFetcher: SingleYieldBalanceFetcher,
private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
private val stakingIdFactory: StakingIdFactory,
private val tokensFeatureToggles: TokensFeatureToggles,
) {
/**
@ -97,15 +96,11 @@ class FetchCurrencyStatusUseCase(
): CryptoCurrency {
return catch(
block = {
if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) {
multiWalletCryptoCurrenciesSupplier.getSyncOrNull(
params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId),
)
?.firstOrNull { it.id == id }
?: error("Unable to find currency with ID: $id")
} else {
currenciesRepository.getMultiCurrencyWalletCurrency(userWalletId = userWalletId, id = id)
}
multiWalletCryptoCurrenciesSupplier.getSyncOrNull(
params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId),
)
?.firstOrNull { it.id == id }
?: error("Unable to find currency with ID: $id")
},
) {
raise(CurrencyStatusError.DataError(it))

View file

@ -26,7 +26,6 @@ import java.math.BigDecimal
class GetBalanceNotEnoughForFeeWarningUseCase(
private val currenciesRepository: CurrenciesRepository,
private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
private val tokensFeatureToggles: TokensFeatureToggles,
private val dispatchers: CoroutineDispatcherProvider,
) {
suspend operator fun invoke(
@ -71,14 +70,10 @@ class GetBalanceNotEnoughForFeeWarningUseCase(
tokenStatus: CryptoCurrencyStatus,
feePaidToken: FeePaidCurrency.Token,
): CryptoCurrencyWarning {
val tokens = if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) {
multiWalletCryptoCurrenciesSupplier.getSyncOrNull(
params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId),
)
.orEmpty()
} else {
currenciesRepository.getMultiCurrencyWalletCurrenciesSync(userWalletId)
}
val tokens = multiWalletCryptoCurrenciesSupplier.getSyncOrNull(
params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId),
)
.orEmpty()
val token = tokens.find {
it is CryptoCurrency.Token &&

View file

@ -5,16 +5,15 @@ import arrow.core.raise.Raise
import arrow.core.raise.catch
import arrow.core.raise.either
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.tokens.error.CurrencyStatusError
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.isMultiCurrency
import com.tangem.domain.tokens.error.CurrencyStatusError
import com.tangem.domain.tokens.repository.CurrenciesRepository
class GetCryptoCurrencyUseCase(
private val currenciesRepository: CurrenciesRepository,
private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
private val tokensFeatureToggles: TokensFeatureToggles,
) {
/**
@ -55,15 +54,11 @@ class GetCryptoCurrencyUseCase(
): CryptoCurrency {
return catch(
block = {
if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) {
multiWalletCryptoCurrenciesSupplier.getSyncOrNull(
params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId),
)
?.firstOrNull { it.id.value == id }
?: error("Unable to find currency with ID: $id")
} else {
currenciesRepository.getMultiCurrencyWalletCurrency(userWalletId, id)
}
multiWalletCryptoCurrenciesSupplier.getSyncOrNull(
params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId),
)
?.firstOrNull { it.id.value == id }
?: error("Unable to find currency with ID: $id")
},
catch = { raise(CurrencyStatusError.DataError(it)) },
)

View file

@ -29,7 +29,6 @@ class GetCurrencyWarningsUseCase(
private val currencyChecksRepository: CurrencyChecksRepository,
private val currencyStatusOperations: BaseCurrencyStatusOperations,
private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
private val tokensFeatureToggles: TokensFeatureToggles,
) {
suspend operator fun invoke(
@ -152,14 +151,10 @@ class GetCurrencyWarningsUseCase(
tokenStatus: CryptoCurrencyStatus,
feePaidToken: FeePaidCurrency.Token,
): CryptoCurrencyWarning {
val tokens = if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) {
multiWalletCryptoCurrenciesSupplier.getSyncOrNull(
params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId),
)
.orEmpty()
} else {
currenciesRepository.getMultiCurrencyWalletCurrenciesSync(userWalletId)
}
val tokens = multiWalletCryptoCurrenciesSupplier.getSyncOrNull(
params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId),
)
.orEmpty()
val token = tokens.find {
it is CryptoCurrency.Token &&

View file

@ -1,27 +1,17 @@
package com.tangem.domain.tokens
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.models.wallet.UserWalletId
class IsCryptoCurrencyCoinCouldHideUseCase(
private val currenciesRepository: CurrenciesRepository,
private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
private val tokensFeatureToggles: TokensFeatureToggles,
) {
suspend operator fun invoke(userWalletId: UserWalletId, cryptoCurrencyCoin: CryptoCurrency.Coin): Boolean {
return if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) {
multiWalletCryptoCurrenciesSupplier.getSyncOrNull(
params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId),
)
.orEmpty()
} else {
currenciesRepository.getMultiCurrencyWalletCurrenciesSync(
userWalletId = userWalletId,
refresh = false,
)
}
return multiWalletCryptoCurrenciesSupplier.getSyncOrNull(
params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId),
)
.orEmpty()
.none { it is CryptoCurrency.Token && it.network == cryptoCurrencyCoin.network }
}
}

View file

@ -5,19 +5,16 @@ import arrow.core.getOrElse
import arrow.core.raise.catch
import arrow.core.raise.either
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher
import com.tangem.domain.tokens.error.QuotesError
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.models.wallet.UserWalletId
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
class RefreshMultiCurrencyWalletQuotesUseCase(
private val currenciesRepository: CurrenciesRepository,
private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
private val tokensFeatureToggles: TokensFeatureToggles,
) {
suspend operator fun invoke(userWalletId: UserWalletId): Either<QuotesError, Unit> {
@ -41,15 +38,11 @@ class RefreshMultiCurrencyWalletQuotesUseCase(
return either {
catch(
block = {
if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) {
multiWalletCryptoCurrenciesSupplier.getSyncOrNull(
params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId),
)
.orEmpty()
.toList()
} else {
currenciesRepository.getMultiCurrencyWalletCachedCurrenciesSync(userWalletId)
}
multiWalletCryptoCurrenciesSupplier.getSyncOrNull(
params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId),
)
.orEmpty()
.toList()
},
catch = ::raise,
)

View file

@ -4,16 +4,15 @@ import arrow.core.Either
import arrow.core.raise.catch
import arrow.core.raise.either
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.tokens.model.remove.RemoveCurrencyError
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.models.wallet.UserWalletId
class RemoveCurrencyUseCase(
private val currenciesRepository: CurrenciesRepository,
private val walletManagersFacade: WalletManagersFacade,
private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
private val tokensFeatureToggles: TokensFeatureToggles,
) {
suspend operator fun invoke(
@ -46,17 +45,10 @@ class RemoveCurrencyUseCase(
suspend fun hasLinkedTokens(userWalletId: UserWalletId, currency: CryptoCurrency): Boolean {
return when (currency) {
is CryptoCurrency.Coin -> {
val walletCurrencies = if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) {
multiWalletCryptoCurrenciesSupplier.getSyncOrNull(
params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId),
)
.orEmpty()
} else {
currenciesRepository.getMultiCurrencyWalletCurrenciesSync(
userWalletId = userWalletId,
refresh = false,
)
}
val walletCurrencies = multiWalletCryptoCurrenciesSupplier.getSyncOrNull(
params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId),
)
.orEmpty()
walletCurrencies.any { it is CryptoCurrency.Token && it.network == currency.network }
}

View file

@ -5,7 +5,4 @@ package com.tangem.domain.tokens
*
[REDACTED_AUTHOR]
*/
interface TokensFeatureToggles {
val isWalletBalanceFetcherEnabled: Boolean
}
interface TokensFeatureToggles

View file

@ -28,7 +28,6 @@ import com.tangem.domain.staking.single.SingleYieldBalanceProducer
import com.tangem.domain.staking.single.SingleYieldBalanceSupplier
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier
import com.tangem.domain.tokens.TokensFeatureToggles
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations.Error
import com.tangem.domain.tokens.repository.CurrenciesRepository
@ -53,7 +52,6 @@ abstract class BaseCurrencyStatusOperations(
private val multiYieldBalanceSupplier: MultiYieldBalanceSupplier,
private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
private val stakingIdFactory: StakingIdFactory,
private val tokensFeatureToggles: TokensFeatureToggles,
) {
protected val currencyStatusProxyCreator = CurrencyStatusProxyCreator()
@ -62,13 +60,6 @@ abstract class BaseCurrencyStatusOperations(
protected abstract fun getQuotes(id: CryptoCurrency.RawID): Flow<Either<Error, Set<QuoteStatus>>>
protected abstract suspend fun fetchComponents(
userWalletId: UserWalletId,
networks: Set<Network>,
currenciesIds: Set<CryptoCurrency.ID>,
currencies: List<CryptoCurrency>,
): Either<Throwable, Unit>
suspend fun getCurrencyStatusFlow(
userWalletId: UserWalletId,
currencyId: CryptoCurrency.ID,
@ -262,14 +253,10 @@ abstract class BaseCurrencyStatusOperations(
return either {
catch(
block = {
val nonEmptyCurrencies = if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) {
multiWalletCryptoCurrenciesSupplier.getSyncOrNull(
params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId),
)
?.toNonEmptyListOrNull()
} else {
currenciesRepository.getMultiCurrencyWalletCurrenciesSync(userWalletId).toNonEmptyListOrNull()
}
val nonEmptyCurrencies = multiWalletCryptoCurrenciesSupplier.getSyncOrNull(
params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId),
)
?.toNonEmptyListOrNull()
?: return emptyList<CryptoCurrencyStatus>().right()
val (_, currenciesIds) = getIds(nonEmptyCurrencies)
@ -332,15 +319,11 @@ abstract class BaseCurrencyStatusOperations(
currencyId: CryptoCurrency.ID,
): CryptoCurrency {
return Either.catch {
if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) {
multiWalletCryptoCurrenciesSupplier.getSyncOrNull(
params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId),
)
?.firstOrNull { it.id == currencyId }
?: error("Unable to find currency with ID: $currencyId")
} else {
currenciesRepository.getMultiCurrencyWalletCurrency(userWalletId = userWalletId, id = currencyId)
}
multiWalletCryptoCurrenciesSupplier.getSyncOrNull(
params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId),
)
?.firstOrNull { it.id == currencyId }
?: error("Unable to find currency with ID: $currencyId")
}
.mapLeft(Error::DataError)
.bind()
@ -382,16 +365,12 @@ abstract class BaseCurrencyStatusOperations(
derivationPath: Network.DerivationPath,
): CryptoCurrency {
return Either.catch {
if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) {
multiWalletCryptoCurrenciesSupplier.getSyncOrNull(
params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId),
)
?.filterIsInstance<CryptoCurrency.Coin>()
?.firstOrNull { it.network.id == networkId }
?: error("Unable to create network coin with ID: $networkId")
} else {
currenciesRepository.getNetworkCoin(userWalletId, networkId, derivationPath)
}
multiWalletCryptoCurrenciesSupplier.getSyncOrNull(
params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId),
)
?.filterIsInstance<CryptoCurrency.Coin>()
?.firstOrNull { it.network.id == networkId && it.network.derivationPath == derivationPath }
?: error("Unable to create network coin with ID: $networkId")
}
.mapLeft { Error.DataError(it) }
.bind()

View file

@ -1,7 +1,6 @@
package com.tangem.domain.tokens.operations
import arrow.core.*
import arrow.core.raise.either
import arrow.core.raise.recover
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.core.lce.LceFlow
@ -18,31 +17,27 @@ import com.tangem.domain.models.quote.QuoteStatus
import com.tangem.domain.models.staking.StakingID
import com.tangem.domain.models.staking.YieldBalance
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher
import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier
import com.tangem.domain.networks.single.SingleNetworkStatusFetcher
import com.tangem.domain.networks.single.SingleNetworkStatusProducer
import com.tangem.domain.networks.single.SingleNetworkStatusSupplier
import com.tangem.domain.quotes.QuotesRepository
import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher
import com.tangem.domain.quotes.single.SingleQuoteStatusProducer
import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier
import com.tangem.domain.staking.StakingIdFactory
import com.tangem.domain.staking.model.StakingIntegrationID
import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher
import com.tangem.domain.staking.multi.MultiYieldBalanceSupplier
import com.tangem.domain.staking.single.SingleYieldBalanceProducer
import com.tangem.domain.staking.single.SingleYieldBalanceSupplier
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier
import com.tangem.domain.tokens.TokensFeatureToggles
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations.Error
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.utils.extractAddress
import com.tangem.utils.extensions.addOrReplace
import com.tangem.utils.extensions.isSingleItem
import kotlinx.coroutines.*
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
@Suppress("LongParameterList", "LargeClass")
class CachedCurrenciesStatusesOperations(
@ -50,16 +45,11 @@ class CachedCurrenciesStatusesOperations(
quotesRepository: QuotesRepository,
private val singleNetworkStatusSupplier: SingleNetworkStatusSupplier,
multiNetworkStatusSupplier: MultiNetworkStatusSupplier,
private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
private val singleNetworkStatusFetcher: SingleNetworkStatusFetcher,
private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
private val singleQuoteStatusSupplier: SingleQuoteStatusSupplier,
private val singleYieldBalanceSupplier: SingleYieldBalanceSupplier,
multiYieldBalanceSupplier: MultiYieldBalanceSupplier,
private val multiYieldBalanceFetcher: MultiYieldBalanceFetcher,
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
private val stakingIdFactory: StakingIdFactory,
private val tokensFeatureToggles: TokensFeatureToggles,
) : BaseCurrencyStatusOperations(
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
@ -70,7 +60,6 @@ class CachedCurrenciesStatusesOperations(
multiYieldBalanceSupplier = multiYieldBalanceSupplier,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
stakingIdFactory = stakingIdFactory,
tokensFeatureToggles = tokensFeatureToggles,
) {
override fun getCurrenciesStatuses(
@ -90,20 +79,6 @@ class CachedCurrenciesStatusesOperations(
): LceFlow<TokenListError, List<CryptoCurrencyStatus>> = lceFlow {
val prevStatuses = MutableStateFlow(value = emptyList<CryptoCurrencyStatus>())
val nonEmptyCurrencies = currenciesFlow.mapNotNull { it.getOrNull() }.firstOrNull()?.toNonEmptyListOrNull()
if (!tokensFeatureToggles.isWalletBalanceFetcherEnabled && !isFetchingStarted(userWalletId) &&
nonEmptyCurrencies != null
) {
launch {
setFetchStarted(userWalletId)
val (networks, currenciesIds) = getIds(nonEmptyCurrencies)
fetchComponents(userWalletId, networks, currenciesIds, nonEmptyCurrencies)
}
.invokeOnCompletion { setFetchFinished(userWalletId) }
}
currenciesFlow.flatMapLatest { maybeCurrencies ->
val currencies = maybeCurrencies
.getOrElse { return@flatMapLatest flowOf(it.lceError()) }
@ -154,15 +129,6 @@ class CachedCurrenciesStatusesOperations(
)
}
if (!tokensFeatureToggles.isWalletBalanceFetcherEnabled && !isFetchingStarted(userWalletId)) {
launch {
setFetchStarted(userWalletId)
fetchComponents(userWalletId, networks, currenciesIds, currencies)
}
.invokeOnCompletion { setFetchFinished(userWalletId) }
}
val networksStatusesUpdates = getNetworkStatusesUpdates(userWalletId, networks)
combine(
@ -183,11 +149,7 @@ class CachedCurrenciesStatusesOperations(
getYieldsBalancesUpdates(userWalletId, currenciesAddresses)
},
flow4 = fetchingState.map {
val state = it[userWalletId] ?: return@map false
!state.isFinished()
},
flow4 = flowOf(value = false),
transform = ::createCurrenciesStatuses,
)
.distinctUntilChanged()
@ -200,55 +162,6 @@ class CachedCurrenciesStatusesOperations(
.launchIn(scope = this)
}
override suspend fun fetchComponents(
userWalletId: UserWalletId,
networks: Set<Network>,
currenciesIds: Set<CryptoCurrency.ID>,
currencies: List<CryptoCurrency>,
): Either<Throwable, Unit> = either {
coroutineScope {
awaitAll(
async {
if (networks.isSingleItem()) {
singleNetworkStatusFetcher(
params = SingleNetworkStatusFetcher.Params(
userWalletId = userWalletId,
network = networks.first(),
),
)
} else {
multiNetworkStatusFetcher(
params = MultiNetworkStatusFetcher.Params(
userWalletId = userWalletId,
networks = networks,
),
)
}
},
async {
val rawCurrenciesIds = currenciesIds.mapNotNullTo(mutableSetOf()) { it.rawCurrencyId }
multiQuoteStatusFetcher(
params = MultiQuoteStatusFetcher.Params(currenciesIds = rawCurrenciesIds, appCurrencyId = null),
)
},
async {
val stakingIds = currencies.mapNotNullTo(hashSetOf()) {
stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = it).getOrNull()
}
multiYieldBalanceFetcher(
params = MultiYieldBalanceFetcher.Params(
userWalletId = userWalletId,
stakingIds = stakingIds,
),
)
},
)
}
.map { }
}
private fun createCurrenciesStatuses(
currencies: NonEmptyList<CryptoCurrency>,
maybeQuotes: Either<TokenListError, Set<QuoteStatus>>?,
@ -428,36 +341,4 @@ class CachedCurrenciesStatusesOperations(
}
.distinctUntilChanged()
}
private fun isFetchingStarted(userWalletId: UserWalletId): Boolean {
return fetchingState.value[userWalletId]?.let { it.isStarted() || it.isFinished() } == true
}
private fun setFetchStarted(userWalletId: UserWalletId) {
fetchingState.update {
it.toMutableMap().apply {
put(key = userWalletId, value = FetchingState.STARTED)
}
}
}
private fun setFetchFinished(userWalletId: UserWalletId) {
fetchingState.update {
it.toMutableMap().apply {
put(key = userWalletId, value = FetchingState.FINISHED)
}
}
}
enum class FetchingState {
STARTED, FINISHED;
fun isStarted() = this == STARTED
fun isFinished() = this == FINISHED
}
companion object {
private val fetchingState = MutableStateFlow(value = emptyMap<UserWalletId, FetchingState>())
}
}

View file

@ -188,7 +188,6 @@ internal class ApplyTokenListSortingUseCaseTest {
currenciesRepository = tokensRepository,
dispatchers = TestingCoroutineDispatcherProvider(),
multiWalletCryptoCurrenciesSupplier = mockk(),
tokensFeatureToggles = mockk(),
)
private fun getTokensRepository(

View file

@ -12,8 +12,6 @@ import com.tangem.domain.networks.single.SingleNetworkStatusProducer
import com.tangem.domain.networks.single.SingleNetworkStatusSupplier
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier
import com.tangem.domain.tokens.TokensFeatureToggles
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.transaction.error.AssociateAssetError
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.utils.isNullOrZero
@ -22,10 +20,8 @@ import kotlinx.coroutines.flow.firstOrNull
class AssociateAssetUseCase(
private val cardSdkConfigRepository: CardSdkConfigRepository,
private val walletManagersFacade: WalletManagersFacade,
private val currenciesRepository: CurrenciesRepository,
private val singleNetworkStatusSupplier: SingleNetworkStatusSupplier,
private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
private val tokensFeatureToggles: TokensFeatureToggles,
) {
suspend operator fun invoke(
@ -33,25 +29,19 @@ class AssociateAssetUseCase(
currency: CryptoCurrency,
): Either<AssociateAssetError, Unit> {
return either {
val networkCoin = if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) {
multiWalletCryptoCurrenciesSupplier.getSyncOrNull(
params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId),
)
?.firstOrNull {
val network = currency.network
it.network.id == network.id && it.network.derivationPath == network.derivationPath
}
?: error("Unable to create network coin for currencyID: ${currency.id}")
} else {
currenciesRepository.getNetworkCoin(
userWalletId = userWalletId,
networkId = currency.network.id,
derivationPath = currency.network.derivationPath,
)
}
val networkCoin = multiWalletCryptoCurrenciesSupplier.getSyncOrNull(
params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId),
)
?.firstOrNull {
val network = currency.network
it.network.id == network.id && it.network.derivationPath == network.derivationPath
}
?: error("Unable to create network coin for currencyID: ${currency.id}")
if (isBalanceZero(userWalletId, networkCoin)) {
raise(AssociateAssetError.NotEnoughBalance(networkCoin))
}
val signer = cardSdkConfigRepository.getCommonSigner(
cardId = null,
twinKey = null, // use null here because no assets support for Twin cards

View file

@ -0,0 +1,8 @@
package com.tangem.domain.pay.datasource
import arrow.core.Either
interface TangemPayAuthDataSource {
suspend fun generateNewAuthHeader(address: String, cardId: String): Either<Throwable, String>
}

View file

@ -6,9 +6,13 @@ import com.tangem.domain.pay.KycStartInfo
interface KycRepository {
/**
* Returns fresh KYC data to start the survey. Used only for first time launch
*/
suspend fun getKycStartInfo(address: String, cardId: String): Either<UniversalError, KycStartInfo>
interface Factory {
fun create(): KycRepository
}
/**
* Returns KYC data to continue the survey. Used when KYC wasn't finished by the user
*/
suspend fun getKycStartInfo(authHeader: String): Either<UniversalError, KycStartInfo>
}

View file

@ -0,0 +1,10 @@
package com.tangem.domain.pay.usecase
import arrow.core.Either
import com.tangem.core.error.UniversalError
import com.tangem.domain.pay.KycStartInfo
interface KycStartInfoUseCase {
suspend operator fun invoke(): Either<UniversalError, KycStartInfo>
}

View file

@ -1,4 +1,4 @@
package com.tangem.domain.visa.repository
package com.tangem.domain.visa.datasource
import arrow.core.Either
import com.tangem.domain.visa.error.VisaApiError
@ -6,7 +6,7 @@ import com.tangem.domain.visa.model.VisaAuthChallenge
import com.tangem.domain.visa.model.VisaAuthSignedChallenge
import com.tangem.domain.visa.model.VisaAuthTokens
interface VisaAuthRepository {
interface VisaAuthRemoteDataSource {
suspend fun getCardAuthChallenge(
cardId: String,

View file

@ -1,16 +0,0 @@
package com.tangem.domain.walletconnect.model.legacy
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class Account(
@Json(name = "chainId")
val chainId: String,
@Json(name = "walletAddress")
val walletAddress: String,
@Json(name = "derivationPath")
val derivationPath: String?,
)

View file

@ -1,13 +0,0 @@
package com.tangem.domain.walletconnect.model.legacy
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class Session(
@Json(name = "topic")
val topic: String,
@Json(name = "accounts")
val accounts: List<Account>,
)

View file

@ -1,9 +0,0 @@
package com.tangem.domain.walletconnect.model.legacy
interface WalletConnectSessionsRepository {
suspend fun loadSessions(userWallet: String): List<Session>
suspend fun saveSession(userWallet: String, session: Session)
suspend fun removeSession(userWallet: String, topic: String)
}

View file

@ -0,0 +1,20 @@
package com.tangem.domain.wallets.hot
import com.tangem.hot.sdk.model.*
interface HotWalletAccessor {
suspend fun signHashes(hotWalletId: HotWalletId, dataToSign: List<DataToSign>): List<SignedData>
suspend fun derivePublicKeys(hotWalletId: HotWalletId, request: DeriveWalletRequest): DerivedPublicKeyResponse
suspend fun exportSeedPhrase(hotWalletId: HotWalletId): SeedPhrasePrivateInfo
suspend fun unlockContextual(hotWalletId: HotWalletId): UnlockHotWallet
fun getContextualUnlock(hotWalletId: HotWalletId): UnlockHotWallet?
fun clearContextualUnlock(hotWalletId: HotWalletId)
fun clearAllContextualUnlock()
}

View file

@ -59,6 +59,10 @@ interface WalletsRepository {
suspend fun setNotificationsEnabled(userWalletId: UserWalletId, isEnabled: Boolean)
fun isUpgradeWalletNotificationEnabled(userWalletId: UserWalletId): Flow<Boolean>
suspend fun dismissUpgradeWalletNotification(userWalletId: UserWalletId)
@Throws
suspend fun setWalletName(walletId: String, walletName: String)

View file

@ -0,0 +1,16 @@
package com.tangem.domain.wallets.usecase
import arrow.core.Either
import com.tangem.domain.wallets.hot.HotWalletAccessor
class ClearAllHotWalletContextualUnlockUseCase(
private val hotWalletAccessor: HotWalletAccessor,
) {
operator fun invoke(): Either<Throwable, Unit> {
return Either.catch {
hotWalletAccessor.clearAllContextualUnlock()
}
}
}

View file

@ -0,0 +1,17 @@
package com.tangem.domain.wallets.usecase
import arrow.core.Either
import com.tangem.domain.wallets.hot.HotWalletAccessor
import com.tangem.hot.sdk.model.HotWalletId
class ClearHotWalletContextualUnlockUseCase(
private val hotWalletAccessor: HotWalletAccessor,
) {
operator fun invoke(hotWalletId: HotWalletId): Either<Throwable, Unit> {
return Either.catch {
hotWalletAccessor.clearContextualUnlock(hotWalletId)
}
}
}

View file

@ -0,0 +1,12 @@
package com.tangem.domain.wallets.usecase
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.wallets.repository.WalletsRepository
class DismissUpgradeWalletNotificationUseCase(
private val walletsRepository: WalletsRepository,
) {
suspend operator fun invoke(userWalletId: UserWalletId) {
walletsRepository.dismissUpgradeWalletNotification(userWalletId)
}
}

View file

@ -0,0 +1,17 @@
package com.tangem.domain.wallets.usecase
import arrow.core.Either
import com.tangem.domain.wallets.hot.HotWalletAccessor
import com.tangem.hot.sdk.model.HotWalletId
import com.tangem.hot.sdk.model.SeedPhrasePrivateInfo
class ExportSeedPhraseUseCase(
private val hotWalletAccessor: HotWalletAccessor,
) {
suspend operator fun invoke(hotWalletId: HotWalletId): Either<Throwable, SeedPhrasePrivateInfo> {
return Either.catch {
hotWalletAccessor.exportSeedPhrase(hotWalletId)
}
}
}

View file

@ -0,0 +1,17 @@
package com.tangem.domain.wallets.usecase
import arrow.core.Either
import com.tangem.domain.wallets.hot.HotWalletAccessor
import com.tangem.hot.sdk.model.HotWalletId
import com.tangem.hot.sdk.model.UnlockHotWallet
class GetHotWalletContextualUnlockUseCase(
private val hotWalletAccessor: HotWalletAccessor,
) {
suspend operator fun invoke(hotWalletId: HotWalletId): Either<Throwable, UnlockHotWallet?> {
return Either.catch {
hotWalletAccessor.getContextualUnlock(hotWalletId)
}
}
}

View file

@ -0,0 +1,13 @@
package com.tangem.domain.wallets.usecase
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.wallets.repository.WalletsRepository
import kotlinx.coroutines.flow.Flow
class IsUpgradeWalletNotificationEnabledUseCase(
private val walletsRepository: WalletsRepository,
) {
operator fun invoke(userWalletId: UserWalletId): Flow<Boolean> {
return walletsRepository.isUpgradeWalletNotificationEnabled(userWalletId)
}
}

View file

@ -0,0 +1,18 @@
package com.tangem.domain.wallets.usecase
import arrow.core.Either
import com.tangem.domain.wallets.hot.HotWalletAccessor
import com.tangem.hot.sdk.model.HotWalletId
import com.tangem.hot.sdk.model.UnlockHotWallet
class UnlockHotWalletContextualUseCase(
private val hotWalletAccessor: HotWalletAccessor,
) {
suspend operator fun invoke(hotWalletId: HotWalletId): Either<Throwable, UnlockHotWallet> {
return Either.catch {
hotWalletAccessor.unlockContextual(hotWalletId)
}
}
}