Updated on 2026-08-14
This commit is contained in:
commit
2f62d482ca
635 changed files with 18802 additions and 9118 deletions
|
|
@ -0,0 +1,11 @@
|
|||
package com.tangem.domain.account.featuretoggle
|
||||
|
||||
/**
|
||||
* Accounts feature toggle
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
interface AccountsFeatureToggles {
|
||||
|
||||
val isFeatureEnabled: Boolean
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)) },
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)) }
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)) }
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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?)
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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?)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
|
|
|
|||
1
domain/notifications/toggles/.gitignore
vendored
1
domain/notifications/toggles/.gitignore
vendored
|
|
@ -1 +0,0 @@
|
|||
/build
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
plugins {
|
||||
alias(deps.plugins.kotlin.jvm)
|
||||
id("configuration")
|
||||
}
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
package com.tangem.domain.notifications.toggles
|
||||
|
||||
interface NotificationsFeatureToggles {
|
||||
val isNotificationsEnabled: Boolean
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -6,4 +6,5 @@ enum class OnrampSource(val analyticsName: String) {
|
|||
TOKEN_LONG_TAP("Long Tap"),
|
||||
TOKEN_DETAILS("Token"),
|
||||
MARKETS("Markets"),
|
||||
SEPA_BANNER("SEPA Banner"),
|
||||
}
|
||||
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -27,4 +27,5 @@ data class PromoBanner(
|
|||
|
||||
enum class PromoId {
|
||||
Referral,
|
||||
Sepa,
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)) },
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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 &&
|
||||
|
|
|
|||
|
|
@ -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)) },
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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 &&
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,4 @@ package com.tangem.domain.tokens
|
|||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
interface TokensFeatureToggles {
|
||||
|
||||
val isWalletBalanceFetcherEnabled: Boolean
|
||||
}
|
||||
interface TokensFeatureToggles
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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>())
|
||||
}
|
||||
}
|
||||
|
|
@ -188,7 +188,6 @@ internal class ApplyTokenListSortingUseCaseTest {
|
|||
currenciesRepository = tokensRepository,
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
multiWalletCryptoCurrenciesSupplier = mockk(),
|
||||
tokensFeatureToggles = mockk(),
|
||||
)
|
||||
|
||||
private fun getTokensRepository(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
@ -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?,
|
||||
)
|
||||
|
|
@ -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>,
|
||||
)
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue