Updated on 2026-08-14
This commit is contained in:
commit
a33afd3583
684 changed files with 8006 additions and 4239 deletions
|
|
@ -6,11 +6,11 @@ import com.tangem.blockchain.common.FeePaidCurrency
|
|||
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
|
||||
import com.tangem.blockchainsdk.utils.toBlockchain
|
||||
import com.tangem.blockchainsdk.utils.toNetworkId
|
||||
import com.tangem.domain.card.DerivationStyleProvider
|
||||
import com.tangem.domain.card.common.extensions.canHandleToken
|
||||
import com.tangem.domain.card.common.util.derivationStyleProvider
|
||||
import com.tangem.domain.wallets.derivations.derivationStyleProvider
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.wallets.derivations.DerivationStyleProvider
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -111,6 +111,7 @@ class NetworkFactory @Inject constructor(
|
|||
hasFiatFeeRate = blockchain.feePaidCurrency() !is FeePaidCurrency.FeeResource,
|
||||
canHandleTokens = canHandleTokens,
|
||||
transactionExtrasType = blockchain.getSupportedTransactionExtras(),
|
||||
nameResolvingType = blockchain.getNameResolvingType(),
|
||||
)
|
||||
}
|
||||
.getOrNull()
|
||||
|
|
@ -328,6 +329,13 @@ class NetworkFactory @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun Blockchain.getNameResolvingType(): Network.NameResolvingType {
|
||||
return when (this) {
|
||||
Blockchain.Ethereum, Blockchain.EthereumTestnet -> Network.NameResolvingType.ENS
|
||||
else -> Network.NameResolvingType.NONE
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
|
||||
fun createNetworkStandardType(blockchain: Blockchain) = getNetworkStandardType(blockchain)
|
||||
|
||||
|
|
|
|||
|
|
@ -7,10 +7,10 @@ import com.tangem.common.test.domain.card.MockScanResponseFactory
|
|||
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
|
||||
import com.tangem.common.test.domain.wallet.MockUserWalletFactory
|
||||
import com.tangem.common.test.utils.ProvideTestModels
|
||||
import com.tangem.domain.card.DerivationStyleProvider
|
||||
import com.tangem.domain.wallets.derivations.DerivationStyleProvider
|
||||
import com.tangem.domain.card.configs.GenericCardConfig
|
||||
import com.tangem.domain.card.configs.MultiWalletCardConfig
|
||||
import com.tangem.domain.card.common.util.derivationStyleProvider
|
||||
import com.tangem.domain.wallets.derivations.derivationStyleProvider
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ dependencies {
|
|||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.manageTokens)
|
||||
implementation(projects.domain.card)
|
||||
implementation(projects.domain.wallets)
|
||||
implementation(projects.domain.tokens.models)
|
||||
implementation(projects.domain.wallets.models)
|
||||
implementation(projects.domain.legacy)
|
||||
|
|
|
|||
|
|
@ -22,8 +22,8 @@ import com.tangem.domain.managetokens.model.ManagedCryptoCurrency
|
|||
import com.tangem.domain.managetokens.repository.CustomTokensRepository
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.models.wallet.requireColdWallet
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.withContext
|
||||
|
|
@ -247,26 +247,43 @@ internal class DefaultCustomTokensRepository(
|
|||
val userWallet = requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) {
|
||||
"User wallet [$userWalletId] not found while getting supported networks"
|
||||
}
|
||||
val scanResponse = userWallet.requireColdWallet().scanResponse // TODO [REDACTED_TASK_KEY]
|
||||
|
||||
Blockchain.entries
|
||||
.mapNotNull { blockchain ->
|
||||
val canHandleBlockchain = scanResponse.card.canHandleBlockchain(
|
||||
blockchain,
|
||||
scanResponse.cardTypesResolver,
|
||||
excludedBlockchains,
|
||||
)
|
||||
when (userWallet) {
|
||||
is UserWallet.Hot -> {
|
||||
Blockchain.entries.mapNotNull {
|
||||
// TODO: refactor [REDACTED_JIRA]\
|
||||
if (it.isTestnet() || it in excludedBlockchains) return@mapNotNull null
|
||||
|
||||
if (canHandleBlockchain) {
|
||||
networkFactory.create(
|
||||
blockchain = blockchain,
|
||||
blockchain = it,
|
||||
extraDerivationPath = null,
|
||||
userWallet = userWallet,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
is UserWallet.Cold -> {
|
||||
val scanResponse = userWallet.scanResponse
|
||||
|
||||
Blockchain.entries
|
||||
.mapNotNull { blockchain ->
|
||||
val canHandleBlockchain = scanResponse.card.canHandleBlockchain(
|
||||
blockchain,
|
||||
scanResponse.cardTypesResolver,
|
||||
excludedBlockchains,
|
||||
)
|
||||
|
||||
if (canHandleBlockchain) {
|
||||
networkFactory.create(
|
||||
blockchain = blockchain,
|
||||
extraDerivationPath = null,
|
||||
userWallet = userWallet,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun createDerivationPath(rawPath: String): Network.DerivationPath {
|
||||
|
|
|
|||
|
|
@ -197,11 +197,10 @@ internal class DefaultManageTokensRepository(
|
|||
)
|
||||
|
||||
private fun getSupportedBlockchains(userWallet: UserWallet?): List<Blockchain> {
|
||||
return (userWallet as? UserWallet.Cold)?.scanResponse?.let {
|
||||
it.card.supportedBlockchains(it.cardTypesResolver, excludedBlockchains) // TODO [REDACTED_TASK_KEY]
|
||||
} ?: Blockchain.entries.filter {
|
||||
!it.isTestnet() && it !in excludedBlockchains
|
||||
}
|
||||
return userWallet?.supportedBlockchains(excludedBlockchains)
|
||||
?: Blockchain.entries.filter {
|
||||
!it.isTestnet() && it !in excludedBlockchains
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
||||
|
|
|
|||
|
|
@ -13,14 +13,14 @@ import com.tangem.data.common.network.NetworkFactory
|
|||
import com.tangem.datasource.api.tangemTech.models.CoinsResponse
|
||||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
||||
import com.tangem.datasource.local.config.testnet.models.TestnetTokensConfig
|
||||
import com.tangem.domain.card.DerivationStyleProvider
|
||||
import com.tangem.domain.card.common.extensions.canHandleToken
|
||||
import com.tangem.domain.card.common.util.derivationStyleProvider
|
||||
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency
|
||||
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency.SourceNetwork
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.wallets.derivations.DerivationStyleProvider
|
||||
import com.tangem.domain.wallets.derivations.derivationStyleProvider
|
||||
import timber.log.Timber
|
||||
|
||||
internal class ManagedCryptoCurrencyFactory(
|
||||
|
|
|
|||
|
|
@ -17,12 +17,10 @@ import com.tangem.datasource.local.nft.converter.NFTSdkCollectionConverter
|
|||
import com.tangem.datasource.local.nft.converter.NFTSdkCollectionIdentifierConverter
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.card.common.extensions.canHandleToken
|
||||
import com.tangem.domain.card.common.util.cardTypesResolver
|
||||
import com.tangem.domain.models.StatusSource
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.models.wallet.requireColdWallet
|
||||
import com.tangem.domain.nft.models.NFTAsset
|
||||
import com.tangem.domain.nft.models.NFTCollection
|
||||
import com.tangem.domain.nft.models.NFTCollections
|
||||
|
|
@ -542,11 +540,10 @@ internal class DefaultNFTRepository @Inject constructor(
|
|||
}
|
||||
|
||||
private fun Network.canHandleNFTs(userWalletId: UserWalletId): Boolean {
|
||||
// TODO [REDACTED_TASK_KEY]
|
||||
val scanResponse = userWalletsStore.getSyncStrict(userWalletId).requireColdWallet().scanResponse
|
||||
val userWallet = userWalletsStore.getSyncStrict(userWalletId)
|
||||
val blockchain = Blockchain.fromNetworkId(backendId) ?: return false
|
||||
|
||||
return blockchain.canHandleNFTs() &&
|
||||
scanResponse.card.canHandleToken(blockchain, scanResponse.cardTypesResolver, excludedBlockchains)
|
||||
userWallet.canHandleToken(blockchain, excludedBlockchains)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,48 +1,24 @@
|
|||
package com.tangem.data.notifications
|
||||
|
||||
import com.tangem.data.notifications.converters.NotificationsEligibleNetworkConverter
|
||||
import com.tangem.datasource.api.common.response.getOrThrow
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.api.tangemTech.models.NotificationApplicationCreateBody
|
||||
import com.tangem.utils.info.AppInfoProvider
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys
|
||||
import com.tangem.datasource.local.preferences.utils.*
|
||||
import com.tangem.domain.notifications.models.ApplicationId
|
||||
import com.tangem.datasource.local.preferences.utils.getObjectMapSync
|
||||
import com.tangem.datasource.local.preferences.utils.getSyncOrDefault
|
||||
import com.tangem.datasource.local.preferences.utils.getSyncOrNull
|
||||
import com.tangem.datasource.local.preferences.utils.store
|
||||
import com.tangem.domain.notifications.repository.NotificationsRepository
|
||||
import com.tangem.domain.notifications.models.NotificationsEligibleNetwork
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class DefaultNotificationsRepository @Inject constructor(
|
||||
private val tangemTechApi: TangemTechApi,
|
||||
private val appInfoProvider: AppInfoProvider,
|
||||
class DefaultNotificationsRepository @Inject constructor(
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : NotificationsRepository {
|
||||
|
||||
override suspend fun createApplicationId(pushToken: String?): ApplicationId = withContext(dispatchers.io) {
|
||||
tangemTechApi.createApplicationId(
|
||||
NotificationApplicationCreateBody(
|
||||
platform = appInfoProvider.platform.lowercase(),
|
||||
device = appInfoProvider.device,
|
||||
systemVersion = appInfoProvider.osVersion,
|
||||
language = appInfoProvider.language,
|
||||
timezone = appInfoProvider.timezone,
|
||||
version = appInfoProvider.appVersion,
|
||||
pushToken = pushToken,
|
||||
),
|
||||
).getOrThrow().appId.let(::ApplicationId)
|
||||
override suspend fun shouldShowNotification(key: String): Boolean {
|
||||
return appPreferencesStore.getSyncOrDefault(PreferencesKeys.getShouldShowNotificationKey(key), true)
|
||||
}
|
||||
|
||||
override suspend fun saveApplicationId(appId: ApplicationId) {
|
||||
appPreferencesStore.store(PreferencesKeys.NOTIFICATIONS_APPLICATION_ID_KEY, appId.value)
|
||||
}
|
||||
|
||||
override suspend fun getApplicationId(): ApplicationId? {
|
||||
return appPreferencesStore.getSyncOrNull(PreferencesKeys.NOTIFICATIONS_APPLICATION_ID_KEY)
|
||||
?.let(::ApplicationId)
|
||||
override suspend fun setShouldShowNotifications(key: String, value: Boolean) {
|
||||
appPreferencesStore.store(PreferencesKeys.getShouldShowNotificationKey(key), value)
|
||||
}
|
||||
|
||||
override suspend fun incrementTronTokenFeeNotificationShowCounter() {
|
||||
|
|
@ -62,27 +38,6 @@ internal class DefaultNotificationsRepository @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
override suspend fun sendPushToken(appId: ApplicationId, pushToken: String) {
|
||||
withContext(dispatchers.io) {
|
||||
tangemTechApi.updatePushTokenForApplicationId(
|
||||
appId.value,
|
||||
NotificationApplicationCreateBody(
|
||||
pushToken = pushToken,
|
||||
systemVersion = appInfoProvider.osVersion,
|
||||
language = appInfoProvider.language,
|
||||
timezone = appInfoProvider.timezone,
|
||||
version = appInfoProvider.appVersion,
|
||||
),
|
||||
).getOrThrow()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getEligibleNetworks(): List<NotificationsEligibleNetwork> = withContext(dispatchers.io) {
|
||||
tangemTechApi.getEligibleNetworksForPushNotifications().getOrThrow().mapNotNull {
|
||||
NotificationsEligibleNetworkConverter.convert(it)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun shouldShowSubscribeOnNotificationsAfterUpdate(): Boolean {
|
||||
return appPreferencesStore.getSyncOrNull(
|
||||
key = PreferencesKeys.NOTIFICATIONS_USER_ALLOW_SEND_ADDRESSES_KEY,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,68 @@
|
|||
package com.tangem.data.notifications
|
||||
|
||||
import com.tangem.data.notifications.converters.NotificationsEligibleNetworkConverter
|
||||
import com.tangem.datasource.api.common.response.getOrThrow
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.api.tangemTech.models.NotificationApplicationCreateBody
|
||||
import com.tangem.utils.info.AppInfoProvider
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys
|
||||
import com.tangem.datasource.local.preferences.utils.*
|
||||
import com.tangem.domain.notifications.models.ApplicationId
|
||||
import com.tangem.domain.notifications.repository.PushNotificationsRepository
|
||||
import com.tangem.domain.notifications.models.NotificationsEligibleNetwork
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class DefaultPushNotificationsRepository @Inject constructor(
|
||||
private val tangemTechApi: TangemTechApi,
|
||||
private val appInfoProvider: AppInfoProvider,
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : PushNotificationsRepository {
|
||||
|
||||
override suspend fun createApplicationId(pushToken: String?): ApplicationId = withContext(dispatchers.io) {
|
||||
tangemTechApi.createApplicationId(
|
||||
NotificationApplicationCreateBody(
|
||||
platform = appInfoProvider.platform.lowercase(),
|
||||
device = appInfoProvider.device,
|
||||
systemVersion = appInfoProvider.osVersion,
|
||||
language = appInfoProvider.language,
|
||||
timezone = appInfoProvider.timezone,
|
||||
version = appInfoProvider.appVersion,
|
||||
pushToken = pushToken,
|
||||
),
|
||||
).getOrThrow().appId.let(::ApplicationId)
|
||||
}
|
||||
|
||||
override suspend fun saveApplicationId(appId: ApplicationId) {
|
||||
appPreferencesStore.store(PreferencesKeys.NOTIFICATIONS_APPLICATION_ID_KEY, appId.value)
|
||||
}
|
||||
|
||||
override suspend fun getApplicationId(): ApplicationId? {
|
||||
return appPreferencesStore.getSyncOrNull(PreferencesKeys.NOTIFICATIONS_APPLICATION_ID_KEY)
|
||||
?.let(::ApplicationId)
|
||||
}
|
||||
|
||||
override suspend fun sendPushToken(appId: ApplicationId, pushToken: String) {
|
||||
withContext(dispatchers.io) {
|
||||
tangemTechApi.updatePushTokenForApplicationId(
|
||||
appId.value,
|
||||
NotificationApplicationCreateBody(
|
||||
pushToken = pushToken,
|
||||
systemVersion = appInfoProvider.osVersion,
|
||||
language = appInfoProvider.language,
|
||||
timezone = appInfoProvider.timezone,
|
||||
version = appInfoProvider.appVersion,
|
||||
),
|
||||
).getOrThrow()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getEligibleNetworks(): List<NotificationsEligibleNetwork> = withContext(dispatchers.io) {
|
||||
tangemTechApi.getEligibleNetworksForPushNotifications().getOrThrow().mapNotNull {
|
||||
NotificationsEligibleNetworkConverter.convert(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,9 @@
|
|||
package com.tangem.data.notifications.di
|
||||
|
||||
import com.tangem.data.notifications.DefaultNotificationsRepository
|
||||
import com.tangem.data.notifications.DefaultPushNotificationsRepository
|
||||
import com.tangem.domain.notifications.repository.NotificationsRepository
|
||||
import com.tangem.domain.notifications.repository.PushNotificationsRepository
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -12,6 +14,10 @@ import javax.inject.Singleton
|
|||
@InstallIn(SingletonComponent::class)
|
||||
internal interface NotificationsModule {
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindPushNotificationsRepository(repository: DefaultPushNotificationsRepository): PushNotificationsRepository
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindNotificationsRepository(repository: DefaultNotificationsRepository): NotificationsRepository
|
||||
|
|
|
|||
|
|
@ -1,30 +1,21 @@
|
|||
package com.tangem.data.notifications
|
||||
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.utils.info.AppInfoProvider
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.data.notifications.converters.NotificationsEligibleNetworkConverter
|
||||
import com.tangem.datasource.api.common.response.ApiResponse
|
||||
import com.tangem.datasource.api.tangemTech.models.*
|
||||
import com.tangem.domain.notifications.models.ApplicationId
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import com.squareup.moshi.Moshi
|
||||
import androidx.datastore.core.DataStore
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
|
||||
class DefaultNotificationsRepositoryTest {
|
||||
private val tangemTechApi: TangemTechApi = mockk()
|
||||
private val appInfoProvider: AppInfoProvider = mockk()
|
||||
private val preferencesDataStore: DataStore<Preferences> = mockk()
|
||||
private val appPreferencesStore = AppPreferencesStore(
|
||||
moshi = Moshi.Builder().build(),
|
||||
|
|
@ -32,147 +23,77 @@ class DefaultNotificationsRepositoryTest {
|
|||
preferencesDataStore = preferencesDataStore,
|
||||
)
|
||||
private val repository = DefaultNotificationsRepository(
|
||||
tangemTechApi = tangemTechApi,
|
||||
appInfoProvider = appInfoProvider,
|
||||
appPreferencesStore = appPreferencesStore,
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `GIVEN valid push token WHEN createApplicationId THEN returns application id`() = runTest {
|
||||
fun `GIVEN shouldShowNotification returns true WHEN called THEN returns true`() = runTest {
|
||||
// GIVEN
|
||||
val pushToken = "test-push-token"
|
||||
val expectedAppId = ApplicationId("test-app-id")
|
||||
val expectedAppIdResponse = NotificationApplicationIdResponse(
|
||||
appId = expectedAppId.value,
|
||||
)
|
||||
coEvery { appInfoProvider.platform } returns "android"
|
||||
coEvery { appInfoProvider.device } returns "test-device"
|
||||
coEvery { appInfoProvider.osVersion } returns "11"
|
||||
coEvery { appInfoProvider.language } returns "en"
|
||||
coEvery { appInfoProvider.appVersion } returns "5.21.1"
|
||||
coEvery { appInfoProvider.timezone } returns "UTC"
|
||||
coEvery { tangemTechApi.createApplicationId(any()) } returns ApiResponse.Success(
|
||||
expectedAppIdResponse,
|
||||
)
|
||||
val key = "test-key"
|
||||
val preferences = mockk<Preferences>(relaxed = true)
|
||||
every { preferences[PreferencesKeys.getShouldShowNotificationKey(key)] } returns true
|
||||
coEvery { preferencesDataStore.data } returns flowOf(preferences)
|
||||
|
||||
// WHEN
|
||||
val result = repository.createApplicationId(pushToken)
|
||||
val result = repository.shouldShowNotification(key)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isEqualTo(expectedAppId)
|
||||
coVerify {
|
||||
tangemTechApi.createApplicationId(
|
||||
NotificationApplicationCreateBody(
|
||||
platform = "android",
|
||||
device = "test-device",
|
||||
systemVersion = "11",
|
||||
language = "en",
|
||||
timezone = "UTC",
|
||||
version = "5.21.1",
|
||||
pushToken = pushToken,
|
||||
),
|
||||
)
|
||||
}
|
||||
assertThat(result).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN application id WHEN saveApplicationId THEN stores it in preferences`() = runTest {
|
||||
fun `GIVEN shouldShowNotification returns false WHEN called THEN returns false`() = runTest {
|
||||
// GIVEN
|
||||
val appId = ApplicationId("test-app-id")
|
||||
val key = "test-key"
|
||||
val preferences = mockk<Preferences>(relaxed = true)
|
||||
coEvery { preferencesDataStore.updateData(any()) } returns preferences
|
||||
every { preferences[PreferencesKeys.getShouldShowNotificationKey(key)] } returns false
|
||||
coEvery { preferencesDataStore.data } returns flowOf(preferences)
|
||||
|
||||
// WHEN
|
||||
repository.saveApplicationId(appId)
|
||||
val result = repository.shouldShowNotification(key)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN setShouldShowNotifications WHEN called THEN stores value in preferences`() = runTest {
|
||||
// GIVEN
|
||||
val key = "test-key"
|
||||
val value = false
|
||||
coEvery { preferencesDataStore.updateData(any()) } returns mockk(relaxed = true)
|
||||
|
||||
// WHEN
|
||||
repository.setShouldShowNotifications(key, value)
|
||||
|
||||
// THEN
|
||||
coVerify { preferencesDataStore.updateData(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN stored application id WHEN getApplicationId THEN returns it`() = runTest {
|
||||
fun `GIVEN incrementTronTokenFeeNotificationShowCounter WHEN called THEN increments counter`() = runTest {
|
||||
// GIVEN
|
||||
val expectedAppId = ApplicationId("test-app-id")
|
||||
coEvery { preferencesDataStore.updateData(any()) } returns mockk(relaxed = true)
|
||||
|
||||
// WHEN
|
||||
repository.incrementTronTokenFeeNotificationShowCounter()
|
||||
|
||||
// THEN
|
||||
coVerify { preferencesDataStore.updateData(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN getTronTokenFeeNotificationShowCounter WHEN called THEN returns counter value`() = runTest {
|
||||
// GIVEN
|
||||
val expectedCount = 5
|
||||
val preferences = mockk<Preferences>(relaxed = true)
|
||||
val key = stringPreferencesKey(PreferencesKeys.NOTIFICATIONS_APPLICATION_ID_KEY.name)
|
||||
every { preferences[key] } returns expectedAppId.value
|
||||
every { preferences[PreferencesKeys.TRON_NETWORK_FEE_NOTIFICATION_SHOW_COUNT_KEY] } returns expectedCount
|
||||
coEvery { preferencesDataStore.data } returns flowOf(preferences)
|
||||
|
||||
// WHEN
|
||||
val result = repository.getApplicationId()
|
||||
val result = repository.getTronTokenFeeNotificationShowCounter()
|
||||
|
||||
// THEN
|
||||
assertThat(result).isEqualTo(expectedAppId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN application id and push token WHEN sendPushToken THEN updates push token`() = runTest {
|
||||
// GIVEN
|
||||
val appId = ApplicationId("test-app-id")
|
||||
val pushToken = "test-push-token"
|
||||
coEvery { appInfoProvider.device } returns "test-device"
|
||||
coEvery { appInfoProvider.osVersion } returns "11"
|
||||
coEvery { appInfoProvider.language } returns "en"
|
||||
coEvery { appInfoProvider.appVersion } returns "5.21.1"
|
||||
coEvery { appInfoProvider.timezone } returns "UTC"
|
||||
coEvery {
|
||||
tangemTechApi.updatePushTokenForApplicationId(
|
||||
appId.value,
|
||||
NotificationApplicationCreateBody(
|
||||
pushToken = pushToken,
|
||||
systemVersion = "11",
|
||||
language = "en",
|
||||
timezone = "UTC",
|
||||
version = "5.21.1",
|
||||
),
|
||||
)
|
||||
} returns ApiResponse.Success(Unit)
|
||||
|
||||
// WHEN
|
||||
repository.sendPushToken(appId, pushToken)
|
||||
|
||||
// THEN
|
||||
coVerify {
|
||||
tangemTechApi.updatePushTokenForApplicationId(
|
||||
appId.value,
|
||||
NotificationApplicationCreateBody(
|
||||
pushToken = pushToken,
|
||||
systemVersion = "11",
|
||||
language = "en",
|
||||
timezone = "UTC",
|
||||
version = "5.21.1",
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN eligible networks WHEN getEligibleNetworks THEN returns converted networks`() = runTest {
|
||||
// GIVEN
|
||||
val expectedNetworks = listOf(
|
||||
CryptoNetworkResponse(
|
||||
id = 1,
|
||||
name = "Ethereum",
|
||||
networkId = "ethereum",
|
||||
),
|
||||
CryptoNetworkResponse(
|
||||
id = 2,
|
||||
name = "Bitcoin",
|
||||
networkId = "bitcoin",
|
||||
),
|
||||
)
|
||||
coEvery { tangemTechApi.getEligibleNetworksForPushNotifications() } returns ApiResponse.Success(
|
||||
expectedNetworks,
|
||||
)
|
||||
|
||||
// WHEN
|
||||
val result = repository.getEligibleNetworks()
|
||||
|
||||
// THEN
|
||||
assertThat(result).hasSize(2)
|
||||
assertThat(result[0]).isEqualTo(NotificationsEligibleNetworkConverter.convert(expectedNetworks[0]))
|
||||
assertThat(result[1]).isEqualTo(NotificationsEligibleNetworkConverter.convert(expectedNetworks[1]))
|
||||
coVerify { tangemTechApi.getEligibleNetworksForPushNotifications() }
|
||||
assertThat(result).isEqualTo(expectedCount)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,178 @@
|
|||
package com.tangem.data.notifications
|
||||
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.utils.info.AppInfoProvider
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.data.notifications.converters.NotificationsEligibleNetworkConverter
|
||||
import com.tangem.datasource.api.common.response.ApiResponse
|
||||
import com.tangem.datasource.api.tangemTech.models.*
|
||||
import com.tangem.domain.notifications.models.ApplicationId
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
class DefaultPushNotificationsRepositoryTest {
|
||||
private val tangemTechApi: TangemTechApi = mockk()
|
||||
private val appInfoProvider: AppInfoProvider = mockk()
|
||||
private val preferencesDataStore: DataStore<Preferences> = mockk()
|
||||
private val appPreferencesStore = AppPreferencesStore(
|
||||
moshi = Moshi.Builder().build(),
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
preferencesDataStore = preferencesDataStore,
|
||||
)
|
||||
private val repository = DefaultPushNotificationsRepository(
|
||||
tangemTechApi = tangemTechApi,
|
||||
appInfoProvider = appInfoProvider,
|
||||
appPreferencesStore = appPreferencesStore,
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `GIVEN valid push token WHEN createApplicationId THEN returns application id`() = runTest {
|
||||
// GIVEN
|
||||
val pushToken = "test-push-token"
|
||||
val expectedAppId = ApplicationId("test-app-id")
|
||||
val expectedAppIdResponse = NotificationApplicationIdResponse(
|
||||
appId = expectedAppId.value,
|
||||
)
|
||||
coEvery { appInfoProvider.platform } returns "android"
|
||||
coEvery { appInfoProvider.device } returns "test-device"
|
||||
coEvery { appInfoProvider.osVersion } returns "11"
|
||||
coEvery { appInfoProvider.language } returns "en"
|
||||
coEvery { appInfoProvider.appVersion } returns "5.21.1"
|
||||
coEvery { appInfoProvider.timezone } returns "UTC"
|
||||
coEvery { tangemTechApi.createApplicationId(any()) } returns ApiResponse.Success(
|
||||
expectedAppIdResponse,
|
||||
)
|
||||
|
||||
// WHEN
|
||||
val result = repository.createApplicationId(pushToken)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isEqualTo(expectedAppId)
|
||||
coVerify {
|
||||
tangemTechApi.createApplicationId(
|
||||
NotificationApplicationCreateBody(
|
||||
platform = "android",
|
||||
device = "test-device",
|
||||
systemVersion = "11",
|
||||
language = "en",
|
||||
timezone = "UTC",
|
||||
version = "5.21.1",
|
||||
pushToken = pushToken,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN application id WHEN saveApplicationId THEN stores it in preferences`() = runTest {
|
||||
// GIVEN
|
||||
val appId = ApplicationId("test-app-id")
|
||||
val preferences = mockk<Preferences>(relaxed = true)
|
||||
coEvery { preferencesDataStore.updateData(any()) } returns preferences
|
||||
|
||||
// WHEN
|
||||
repository.saveApplicationId(appId)
|
||||
|
||||
// THEN
|
||||
coVerify { preferencesDataStore.updateData(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN stored application id WHEN getApplicationId THEN returns it`() = runTest {
|
||||
// GIVEN
|
||||
val expectedAppId = ApplicationId("test-app-id")
|
||||
val preferences = mockk<Preferences>(relaxed = true)
|
||||
val key = stringPreferencesKey(PreferencesKeys.NOTIFICATIONS_APPLICATION_ID_KEY.name)
|
||||
every { preferences[key] } returns expectedAppId.value
|
||||
coEvery { preferencesDataStore.data } returns flowOf(preferences)
|
||||
|
||||
// WHEN
|
||||
val result = repository.getApplicationId()
|
||||
|
||||
// THEN
|
||||
assertThat(result).isEqualTo(expectedAppId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN application id and push token WHEN sendPushToken THEN updates push token`() = runTest {
|
||||
// GIVEN
|
||||
val appId = ApplicationId("test-app-id")
|
||||
val pushToken = "test-push-token"
|
||||
coEvery { appInfoProvider.device } returns "test-device"
|
||||
coEvery { appInfoProvider.osVersion } returns "11"
|
||||
coEvery { appInfoProvider.language } returns "en"
|
||||
coEvery { appInfoProvider.appVersion } returns "5.21.1"
|
||||
coEvery { appInfoProvider.timezone } returns "UTC"
|
||||
coEvery {
|
||||
tangemTechApi.updatePushTokenForApplicationId(
|
||||
appId.value,
|
||||
NotificationApplicationCreateBody(
|
||||
pushToken = pushToken,
|
||||
systemVersion = "11",
|
||||
language = "en",
|
||||
timezone = "UTC",
|
||||
version = "5.21.1",
|
||||
),
|
||||
)
|
||||
} returns ApiResponse.Success(Unit)
|
||||
|
||||
// WHEN
|
||||
repository.sendPushToken(appId, pushToken)
|
||||
|
||||
// THEN
|
||||
coVerify {
|
||||
tangemTechApi.updatePushTokenForApplicationId(
|
||||
appId.value,
|
||||
NotificationApplicationCreateBody(
|
||||
pushToken = pushToken,
|
||||
systemVersion = "11",
|
||||
language = "en",
|
||||
timezone = "UTC",
|
||||
version = "5.21.1",
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN eligible networks WHEN getEligibleNetworks THEN returns converted networks`() = runTest {
|
||||
// GIVEN
|
||||
val expectedNetworks = listOf(
|
||||
CryptoNetworkResponse(
|
||||
id = 1,
|
||||
name = "Ethereum",
|
||||
networkId = "ethereum",
|
||||
),
|
||||
CryptoNetworkResponse(
|
||||
id = 2,
|
||||
name = "Bitcoin",
|
||||
networkId = "bitcoin",
|
||||
),
|
||||
)
|
||||
coEvery { tangemTechApi.getEligibleNetworksForPushNotifications() } returns ApiResponse.Success(
|
||||
expectedNetworks,
|
||||
)
|
||||
|
||||
// WHEN
|
||||
val result = repository.getEligibleNetworks()
|
||||
|
||||
// THEN
|
||||
assertThat(result).hasSize(2)
|
||||
assertThat(result[0]).isEqualTo(NotificationsEligibleNetworkConverter.convert(expectedNetworks[0]))
|
||||
assertThat(result[1]).isEqualTo(NotificationsEligibleNetworkConverter.convert(expectedNetworks[1]))
|
||||
coVerify { tangemTechApi.getEligibleNetworksForPushNotifications() }
|
||||
}
|
||||
}
|
||||
|
|
@ -22,7 +22,6 @@ import com.tangem.datasource.local.token.UserTokensResponseStore
|
|||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.card.common.extensions.canHandleBlockchain
|
||||
import com.tangem.domain.card.common.extensions.canHandleToken
|
||||
import com.tangem.domain.card.common.util.cardTypesResolver
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.onramp.model.HotCryptoCurrency
|
||||
|
|
@ -170,23 +169,17 @@ internal class DefaultHotCryptoRepository(
|
|||
|
||||
// TODO: [REDACTED_JIRA]
|
||||
private fun UserWallet.canHandleHotCrypto(hotToken: HotCryptoResponse.Token): Boolean {
|
||||
if (this !is UserWallet.Cold) {
|
||||
return true // TODO [REDACTED_TASK_KEY]
|
||||
}
|
||||
|
||||
val isToken = hotToken.contractAddress != null && hotToken.decimalCount != null
|
||||
val blockchain = hotToken.networkId?.let { Blockchain.fromNetworkId(it) } ?: return false
|
||||
|
||||
return if (isToken) {
|
||||
scanResponse.card.canHandleToken(
|
||||
canHandleToken(
|
||||
blockchain = blockchain,
|
||||
cardTypesResolver = cardTypesResolver,
|
||||
excludedBlockchains = excludedBlockchains,
|
||||
)
|
||||
} else {
|
||||
scanResponse.card.canHandleBlockchain(
|
||||
canHandleBlockchain(
|
||||
blockchain = blockchain,
|
||||
cardTypesResolver = cardTypesResolver,
|
||||
excludedBlockchains = excludedBlockchains,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import com.squareup.moshi.Json
|
|||
import com.squareup.moshi.JsonClass
|
||||
import com.tangem.datasource.api.onramp.models.response.Status
|
||||
import com.tangem.datasource.api.onramp.models.response.model.OnrampCurrencyDTO
|
||||
import com.tangem.domain.core.serialization.SerializedBigDecimal
|
||||
import com.tangem.domain.models.serialization.SerializedBigDecimal
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -10,8 +10,6 @@ import com.tangem.blockchain.common.TransactionStatus
|
|||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.blockchainsdk.utils.fromNetworkId
|
||||
import com.tangem.blockchainsdk.utils.toBlockchain
|
||||
import com.tangem.blockchainsdk.utils.toCoinId
|
||||
import com.tangem.blockchainsdk.utils.toMigratedCoinId
|
||||
import com.tangem.common.extensions.hexToBytes
|
||||
import com.tangem.common.extensions.toCompressedPublicKey
|
||||
import com.tangem.data.staking.converters.YieldConverter
|
||||
|
|
@ -22,7 +20,6 @@ import com.tangem.data.staking.converters.transaction.StakingTransactionConverte
|
|||
import com.tangem.data.staking.converters.transaction.StakingTransactionStatusConverter
|
||||
import com.tangem.data.staking.converters.transaction.StakingTransactionTypeConverter
|
||||
import com.tangem.data.staking.store.YieldsBalancesStore
|
||||
import com.tangem.data.staking.utils.StakingIdFactory
|
||||
import com.tangem.datasource.api.common.response.ApiResponse
|
||||
import com.tangem.datasource.api.common.response.getOrThrow
|
||||
import com.tangem.datasource.api.stakekit.StakeKitApi
|
||||
|
|
@ -32,22 +29,22 @@ import com.tangem.datasource.api.stakekit.models.response.model.action.StakingAc
|
|||
import com.tangem.datasource.api.stakekit.models.response.model.transaction.tron.TronStakeKitTransaction
|
||||
import com.tangem.datasource.local.token.StakingYieldsStore
|
||||
import com.tangem.datasource.local.token.converter.StakingNetworkTypeConverter
|
||||
import com.tangem.datasource.local.token.converter.TokenConverter
|
||||
import com.tangem.datasource.local.token.converter.YieldTokenConverter
|
||||
import com.tangem.domain.card.common.TapWorkarounds.isWallet2
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.staking.NetworkType
|
||||
import com.tangem.domain.models.staking.YieldBalance
|
||||
import com.tangem.domain.models.staking.action.StakingActionType
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.staking.model.StakingApproval
|
||||
import com.tangem.domain.staking.model.StakingAvailability
|
||||
import com.tangem.domain.staking.model.StakingEntryInfo
|
||||
import com.tangem.domain.staking.model.stakekit.NetworkType
|
||||
import com.tangem.domain.staking.model.StakingIntegrationID
|
||||
import com.tangem.domain.staking.model.stakekit.Yield
|
||||
import com.tangem.domain.staking.model.stakekit.YieldBalance
|
||||
import com.tangem.domain.staking.model.stakekit.action.StakingAction
|
||||
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
|
||||
import com.tangem.domain.staking.model.stakekit.action.StakingActionStatus
|
||||
import com.tangem.domain.staking.model.stakekit.action.StakingActionType
|
||||
import com.tangem.domain.staking.model.stakekit.transaction.ActionParams
|
||||
import com.tangem.domain.staking.model.stakekit.transaction.StakingGasEstimate
|
||||
import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction
|
||||
|
|
@ -62,8 +59,6 @@ import com.tangem.utils.extensions.orZero
|
|||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import java.math.BigDecimal
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@Suppress("LargeClass", "LongParameterList", "TooManyFunctions")
|
||||
internal class DefaultStakingRepository(
|
||||
|
|
@ -74,7 +69,6 @@ internal class DefaultStakingRepository(
|
|||
private val walletManagersFacade: WalletManagersFacade,
|
||||
private val getUserWalletUseCase: GetUserWalletUseCase,
|
||||
private val stakingFeatureToggles: StakingFeatureToggles,
|
||||
private val stakingIdFactory: StakingIdFactory,
|
||||
moshi: Moshi,
|
||||
) : StakingRepository {
|
||||
|
||||
|
|
@ -95,10 +89,6 @@ internal class DefaultStakingRepository(
|
|||
private val networkTypeAdapter by lazy { moshi.adapter(NetworkTypeDTO::class.java) }
|
||||
private val stakingActionStatusAdapter by lazy { moshi.adapter(StakingActionStatusDTO::class.java) }
|
||||
|
||||
override fun getSupportedIntegrationId(cryptoCurrencyId: CryptoCurrency.ID): String? {
|
||||
return stakingIdFactory.createIntegrationId(currencyId = cryptoCurrencyId)
|
||||
}
|
||||
|
||||
override suspend fun fetchEnabledYields() {
|
||||
withContext(dispatchers.io) {
|
||||
when (val stakingTokensWithYields = stakeKitApi.getEnabledYields(preferredValidatorsOnly = false)) {
|
||||
|
|
@ -202,7 +192,7 @@ internal class DefaultStakingRepository(
|
|||
return@channelFlow
|
||||
}
|
||||
|
||||
val isSupportedInMobileApp = getSupportedIntegrationId(cryptoCurrency.id).isNullOrEmpty().not()
|
||||
val isSupportedInMobileApp = StakingIntegrationID.create(currencyId = cryptoCurrency.id) != null
|
||||
|
||||
getEnabledYields()
|
||||
.distinctUntilChanged()
|
||||
|
|
@ -248,7 +238,7 @@ internal class DefaultStakingRepository(
|
|||
return StakingAvailability.Unavailable
|
||||
}
|
||||
|
||||
val isSupportedInMobileApp = getSupportedIntegrationId(cryptoCurrency.id).isNullOrEmpty().not()
|
||||
val isSupportedInMobileApp = StakingIntegrationID.create(currencyId = cryptoCurrency.id) != null
|
||||
|
||||
val yields = getEnabledYieldsSync()
|
||||
if (yields.isEmpty()) {
|
||||
|
|
@ -382,32 +372,6 @@ internal class DefaultStakingRepository(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun getSingleYieldBalanceSync(
|
||||
userWalletId: UserWalletId,
|
||||
cryptoCurrency: CryptoCurrency,
|
||||
): YieldBalance {
|
||||
val stakingId = stakingIdFactory.create(
|
||||
userWalletId = userWalletId,
|
||||
currencyId = cryptoCurrency.id,
|
||||
network = cryptoCurrency.network,
|
||||
) ?: error("Could not create stakingId")
|
||||
|
||||
return stakingBalanceStoreV2.getSyncOrNull(userWalletId = userWalletId, stakingId = stakingId)
|
||||
?: YieldBalance.Error(integrationId = stakingId.integrationId, address = stakingId.address)
|
||||
}
|
||||
|
||||
override suspend fun getMultiYieldBalanceSync(
|
||||
userWalletId: UserWalletId,
|
||||
cryptoCurrencies: List<CryptoCurrency>,
|
||||
): List<YieldBalance>? {
|
||||
val stakingIds = cryptoCurrencies.mapNotNull {
|
||||
stakingIdFactory.create(userWalletId = userWalletId, currencyId = it.id, network = it.network)
|
||||
}
|
||||
|
||||
return stakingBalanceStoreV2.getAllSyncOrNull(userWalletId)
|
||||
?.filter { it.getStakingId() in stakingIds }
|
||||
}
|
||||
|
||||
override suspend fun isAnyTokenStaked(userWalletId: UserWalletId): Boolean {
|
||||
return withContext(dispatchers.default) {
|
||||
val balances = stakingBalanceStoreV2.getAllSyncOrNull(userWalletId) ?: return@withContext false
|
||||
|
|
@ -422,14 +386,6 @@ internal class DefaultStakingRepository(
|
|||
}
|
||||
}
|
||||
|
||||
override fun getActionRequirementAmount(integrationId: String, stakingActionType: StakingActionType): BigDecimal? {
|
||||
return when {
|
||||
stakingIdFactory.isPolygonIntegrationId(integrationId) &&
|
||||
stakingActionType == StakingActionType.CLAIM_REWARDS -> BigDecimal.ONE
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun createActionRequestBody(
|
||||
userWalletId: UserWalletId,
|
||||
network: Network,
|
||||
|
|
@ -443,7 +399,7 @@ internal class DefaultStakingRepository(
|
|||
),
|
||||
args = ActionRequestBodyArgs(
|
||||
amount = params.amount.toPlainString(),
|
||||
inputToken = TokenConverter.convertBack(params.token),
|
||||
inputToken = YieldTokenConverter.convertBack(params.token),
|
||||
validatorAddress = params.validatorAddress,
|
||||
validatorAddresses = listOf(params.validatorAddress), // check on other networks
|
||||
tronResource = getTronResource(network),
|
||||
|
|
@ -481,17 +437,6 @@ internal class DefaultStakingRepository(
|
|||
}
|
||||
}
|
||||
|
||||
override fun getStakingApproval(cryptoCurrency: CryptoCurrency): StakingApproval {
|
||||
val integrationId = stakingIdFactory.createIntegrationId(currencyId = cryptoCurrency.id)
|
||||
|
||||
return when (integrationId) {
|
||||
Blockchain.Ethereum.id + Blockchain.Polygon.toCoinId(),
|
||||
Blockchain.Ethereum.id + Blockchain.Polygon.toMigratedCoinId(),
|
||||
-> StakingApproval.Needed(ETHEREUM_POLYGON_APPROVE_SPENDER)
|
||||
else -> StakingApproval.Empty
|
||||
}
|
||||
}
|
||||
|
||||
private fun getTransactionDataType(networkId: String, unsignedTransaction: String): TransactionData.Compiled.Data {
|
||||
return when (Blockchain.fromId(networkId)) {
|
||||
Blockchain.Solana,
|
||||
|
|
@ -543,14 +488,7 @@ internal class DefaultStakingRepository(
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
companion object {
|
||||
private const val YIELDS_STORE_KEY = "yields"
|
||||
|
||||
private const val ETHEREUM_POLYGON_APPROVE_SPENDER = "0x5e3Ef299fDDf15eAa0432E6e66473ace8c13D908"
|
||||
|
||||
internal val YIELDS_WATITING_TIMEOUT = 15.seconds
|
||||
|
||||
private val INVALID_BATCHES_FOR_SOLANA = listOf("AC01", "CB79")
|
||||
}
|
||||
}
|
||||
|
|
@ -4,7 +4,7 @@ import com.tangem.datasource.api.stakekit.models.response.model.AddressArgumentD
|
|||
import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO
|
||||
import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO.MetadataDTO.RewardScheduleDTO
|
||||
import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO.ValidatorDTO.ValidatorStatusDTO
|
||||
import com.tangem.datasource.local.token.converter.TokenConverter
|
||||
import com.tangem.datasource.local.token.converter.YieldTokenConverter
|
||||
import com.tangem.domain.staking.model.stakekit.AddressArgument
|
||||
import com.tangem.domain.staking.model.stakekit.Yield
|
||||
import com.tangem.domain.staking.model.stakekit.Yield.Metadata.RewardSchedule
|
||||
|
|
@ -24,8 +24,8 @@ internal object YieldConverter : Converter<YieldDTO, Yield> {
|
|||
override fun convert(value: YieldDTO): Yield {
|
||||
return Yield(
|
||||
id = value.id.asMandatory("id"),
|
||||
token = TokenConverter.convert(value.token.asMandatory("token")),
|
||||
tokens = value.tokens.asMandatory("tokens").map(TokenConverter::convert),
|
||||
token = YieldTokenConverter.convert(value.token.asMandatory("token")),
|
||||
tokens = value.tokens.asMandatory("tokens").map(YieldTokenConverter::convert),
|
||||
args = convertArgs(value.args.asMandatory("args")),
|
||||
status = convertStatus(value.status.asMandatory("status")),
|
||||
apy = value.apy.asMandatory("apy"),
|
||||
|
|
@ -91,9 +91,9 @@ internal object YieldConverter : Converter<YieldDTO, Yield> {
|
|||
logoUri = metadataDTO.logoUri.asMandatory("logoUri"),
|
||||
description = metadataDTO.description.asMandatory("description"),
|
||||
documentation = metadataDTO.documentation,
|
||||
gasFeeToken = TokenConverter.convert(metadataDTO.gasFeeTokenDTO.asMandatory("gasFeeTokenDTO")),
|
||||
token = TokenConverter.convert(metadataDTO.tokenDTO.asMandatory("tokenDTO")),
|
||||
tokens = metadataDTO.tokensDTO.asMandatory("tokensDTO").map(TokenConverter::convert),
|
||||
gasFeeToken = YieldTokenConverter.convert(metadataDTO.gasFeeTokenDTO.asMandatory("gasFeeTokenDTO")),
|
||||
token = YieldTokenConverter.convert(metadataDTO.tokenDTO.asMandatory("tokenDTO")),
|
||||
tokens = metadataDTO.tokensDTO.asMandatory("tokensDTO").map(YieldTokenConverter::convert),
|
||||
type = metadataDTO.type.asMandatory("type"),
|
||||
rewardSchedule = convertRewardSchedule(metadataDTO.rewardSchedule.asMandatory("rewardSchedule")),
|
||||
cooldownPeriod = metadataDTO.cooldownPeriod?.let { convertPeriod(it) },
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package com.tangem.data.staking.converters.transaction
|
||||
|
||||
import com.tangem.datasource.api.stakekit.models.response.model.transaction.StakingGasEstimateDTO
|
||||
import com.tangem.datasource.local.token.converter.TokenConverter
|
||||
import com.tangem.datasource.local.token.converter.YieldTokenConverter
|
||||
import com.tangem.domain.staking.model.stakekit.transaction.StakingGasEstimate
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
|
|
@ -10,7 +10,7 @@ internal object GasEstimateConverter : Converter<StakingGasEstimateDTO, StakingG
|
|||
override fun convert(value: StakingGasEstimateDTO): StakingGasEstimate {
|
||||
return StakingGasEstimate(
|
||||
amount = value.amount,
|
||||
token = TokenConverter.convert(value.token),
|
||||
token = YieldTokenConverter.convert(value.token),
|
||||
gasLimit = value.gasLimit,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import com.tangem.data.staking.DefaultStakingTransactionHashRepository
|
|||
import com.tangem.data.staking.converters.error.StakeKitErrorConverter
|
||||
import com.tangem.data.staking.store.YieldsBalancesStore
|
||||
import com.tangem.data.staking.toggles.DefaultStakingFeatureToggles
|
||||
import com.tangem.data.staking.utils.StakingIdFactory
|
||||
import com.tangem.datasource.api.stakekit.StakeKitApi
|
||||
import com.tangem.datasource.api.stakekit.models.response.model.error.StakeKitErrorResponse
|
||||
import com.tangem.datasource.di.NetworkMoshi
|
||||
|
|
@ -45,7 +44,6 @@ internal object StakingDataModule {
|
|||
walletManagersFacade: WalletManagersFacade,
|
||||
getUserWalletUseCase: GetUserWalletUseCase,
|
||||
stakingFeatureToggles: StakingFeatureToggles,
|
||||
stakingIdFactory: StakingIdFactory,
|
||||
@NetworkMoshi moshi: Moshi,
|
||||
): StakingRepository {
|
||||
return DefaultStakingRepository(
|
||||
|
|
@ -57,7 +55,6 @@ internal object StakingDataModule {
|
|||
getUserWalletUseCase = getUserWalletUseCase,
|
||||
stakingFeatureToggles = stakingFeatureToggles,
|
||||
moshi = moshi,
|
||||
stakingIdFactory = stakingIdFactory,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -42,9 +42,8 @@ internal object YieldBalanceSupplierModule {
|
|||
listOf(
|
||||
"single_yield_balance",
|
||||
params.userWalletId.stringValue,
|
||||
params.currencyId.value,
|
||||
params.network.id.rawId,
|
||||
params.network.id.derivationPath,
|
||||
params.stakingId.integrationId,
|
||||
params.stakingId.address,
|
||||
)
|
||||
.joinToString(separator = "_")
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,15 +1,11 @@
|
|||
package com.tangem.data.staking.multi
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.getOrElse
|
||||
import arrow.core.left
|
||||
import arrow.core.raise.catch
|
||||
import arrow.core.raise.either
|
||||
import arrow.core.raise.ensure
|
||||
import arrow.core.right
|
||||
import arrow.core.toOption
|
||||
import com.tangem.data.common.api.safeApiCall
|
||||
import com.tangem.data.staking.store.YieldsBalancesStore
|
||||
import com.tangem.data.staking.utils.StakingIdFactory
|
||||
import com.tangem.data.staking.utils.YieldBalanceRequestBodyFactory
|
||||
import com.tangem.datasource.api.stakekit.StakeKitApi
|
||||
import com.tangem.datasource.api.stakekit.models.request.YieldBalanceRequestBody
|
||||
|
|
@ -18,10 +14,10 @@ import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO
|
|||
import com.tangem.datasource.local.token.StakingYieldsStore
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.core.utils.catchOn
|
||||
import com.tangem.domain.models.staking.StakingID
|
||||
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.staking.model.StakingID
|
||||
import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.async
|
||||
|
|
@ -36,7 +32,6 @@ import javax.inject.Inject
|
|||
* @property userWalletsStore user wallets store
|
||||
* @property stakingYieldsStore staking yields store
|
||||
* @property yieldsBalancesStore yields balances store
|
||||
* @property stakingIdFactory factory for creating StakingID
|
||||
* @property stakeKitApi stake kit API
|
||||
* @property dispatchers dispatchers
|
||||
*
|
||||
|
|
@ -46,7 +41,6 @@ internal class DefaultMultiYieldBalanceFetcher @Inject constructor(
|
|||
private val userWalletsStore: UserWalletsStore,
|
||||
private val stakingYieldsStore: StakingYieldsStore,
|
||||
private val yieldsBalancesStore: YieldsBalancesStore,
|
||||
private val stakingIdFactory: StakingIdFactory,
|
||||
private val stakeKitApi: StakeKitApi,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : MultiYieldBalanceFetcher {
|
||||
|
|
@ -54,11 +48,12 @@ internal class DefaultMultiYieldBalanceFetcher @Inject constructor(
|
|||
override suspend fun invoke(params: MultiYieldBalanceFetcher.Params): Either<Throwable, Unit> {
|
||||
Timber.i("Start fetching yield balances for params:\n$params")
|
||||
|
||||
checkIsSupportedByWalletOrElse(userWalletId = params.userWalletId) {
|
||||
return it.left()
|
||||
val stakingIds = params.stakingIds.ifEmpty {
|
||||
Timber.i("Nothing to fetch, empty stakingIds for ${params.userWalletId}")
|
||||
return Unit.right()
|
||||
}
|
||||
|
||||
val stakingIds = getStakingIds(params).getOrElse {
|
||||
checkIsSupportedByWalletOrElse(userWalletId = params.userWalletId) {
|
||||
return it.left()
|
||||
}
|
||||
|
||||
|
|
@ -94,30 +89,6 @@ internal class DefaultMultiYieldBalanceFetcher @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun getStakingIds(params: MultiYieldBalanceFetcher.Params) = either {
|
||||
val stakingIds = catch(
|
||||
block = {
|
||||
params.currencyIdWithNetworkMap.mapNotNullTo(hashSetOf()) { (currencyId, network) ->
|
||||
stakingIdFactory.create(
|
||||
userWalletId = params.userWalletId,
|
||||
currencyId = currencyId,
|
||||
network = network,
|
||||
)
|
||||
}
|
||||
},
|
||||
catch = ::raise,
|
||||
)
|
||||
|
||||
ensure(stakingIds.isNotEmpty()) {
|
||||
val exception = IllegalStateException("Unable to create staking ids for $params: list is empty")
|
||||
Timber.e(exception)
|
||||
|
||||
raise(exception)
|
||||
}
|
||||
|
||||
stakingIds
|
||||
}
|
||||
|
||||
private suspend fun getAvailableStakingIds(userWalletId: UserWalletId, stakingIds: Set<StakingID>): Set<StakingID> {
|
||||
val yieldIds = getYieldsIds(userWalletId = userWalletId)
|
||||
|
||||
|
|
@ -178,7 +149,9 @@ internal class DefaultMultiYieldBalanceFetcher @Inject constructor(
|
|||
// TODO: in the future, consider optimizing this part
|
||||
.chunked(size = 15) // StakeKitApi limitation: no more than 15 requests at the same time
|
||||
.map {
|
||||
async(dispatchers.io) { stakeKitApi.getMultipleYieldBalances(it).bind() }
|
||||
async(dispatchers.io) {
|
||||
stakeKitApi.getMultipleYieldBalances(it).bind()
|
||||
}
|
||||
}
|
||||
.awaitAll()
|
||||
.flatten()
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package com.tangem.data.staking.multi
|
||||
|
||||
import com.tangem.data.staking.store.YieldsBalancesStore
|
||||
import com.tangem.domain.staking.model.stakekit.YieldBalance
|
||||
import com.tangem.domain.models.staking.YieldBalance
|
||||
import com.tangem.domain.staking.multi.MultiYieldBalanceProducer
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.assisted.Assisted
|
||||
|
|
|
|||
|
|
@ -20,9 +20,7 @@ internal class DefaultSingleYieldBalanceFetcher @Inject constructor(
|
|||
return multiYieldBalanceFetcher(
|
||||
params = MultiYieldBalanceFetcher.Params(
|
||||
userWalletId = params.userWalletId,
|
||||
currencyIdWithNetworkMap = mapOf(
|
||||
params.currencyId to params.network,
|
||||
),
|
||||
stakingIds = setOf(params.stakingId),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,9 +2,7 @@ package com.tangem.data.staking.single
|
|||
|
||||
import com.tangem.core.analytics.api.AnalyticsExceptionHandler
|
||||
import com.tangem.core.analytics.models.ExceptionAnalyticsEvent
|
||||
import com.tangem.data.staking.utils.StakingIdFactory
|
||||
import com.tangem.domain.staking.model.StakingID
|
||||
import com.tangem.domain.staking.model.stakekit.YieldBalance
|
||||
import com.tangem.domain.models.staking.YieldBalance
|
||||
import com.tangem.domain.staking.multi.MultiYieldBalanceProducer
|
||||
import com.tangem.domain.staking.multi.MultiYieldBalanceSupplier
|
||||
import com.tangem.domain.staking.single.SingleYieldBalanceProducer
|
||||
|
|
@ -24,7 +22,7 @@ import timber.log.Timber
|
|||
*
|
||||
* @property params params
|
||||
* @property multiYieldBalanceSupplier multi yield balance supplier
|
||||
* @property stakingIdFactory factory for creating [StakingID]
|
||||
* @property analyticsExceptionHandler analytics exception handler
|
||||
* @property dispatchers dispatchers
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
|
|
@ -32,20 +30,14 @@ import timber.log.Timber
|
|||
internal class DefaultSingleYieldBalanceProducer @AssistedInject constructor(
|
||||
@Assisted private val params: SingleYieldBalanceProducer.Params,
|
||||
private val multiYieldBalanceSupplier: MultiYieldBalanceSupplier,
|
||||
private val stakingIdFactory: StakingIdFactory,
|
||||
private val analyticsExceptionHandler: AnalyticsExceptionHandler,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : SingleYieldBalanceProducer {
|
||||
|
||||
override val fallback: YieldBalance by lazy {
|
||||
YieldBalance.Error(
|
||||
integrationId = stakingIdFactory.createIntegrationId(currencyId = params.currencyId),
|
||||
address = null,
|
||||
)
|
||||
YieldBalance.Error(stakingId = params.stakingId)
|
||||
}
|
||||
|
||||
private var stakingId: StakingID? = null
|
||||
|
||||
override fun produce(): Flow<YieldBalance> {
|
||||
Timber.i("Producing yield balance for params:\n$params")
|
||||
|
||||
|
|
@ -53,14 +45,9 @@ internal class DefaultSingleYieldBalanceProducer @AssistedInject constructor(
|
|||
params = MultiYieldBalanceProducer.Params(userWalletId = params.userWalletId),
|
||||
)
|
||||
.mapNotNull { balances ->
|
||||
val currentStakingId = getStakingId()
|
||||
val currentStakingId = params.stakingId
|
||||
|
||||
if (currentStakingId == null) {
|
||||
Timber.i("Staking ID is null for params: $params")
|
||||
return@mapNotNull YieldBalance.Unsupported
|
||||
}
|
||||
|
||||
val currentBalances = balances.filter { it.getStakingId() == currentStakingId }
|
||||
val currentBalances = balances.filter { it.stakingId == currentStakingId }
|
||||
|
||||
if (currentBalances.size > 1) {
|
||||
analyticsExceptionHandler.sendException(
|
||||
|
|
@ -86,34 +73,16 @@ internal class DefaultSingleYieldBalanceProducer @AssistedInject constructor(
|
|||
currentBalances.first()
|
||||
}
|
||||
} else {
|
||||
val balance = currentBalances.firstOrNull()
|
||||
val balance = currentBalances.firstOrNull() ?: return@mapNotNull null
|
||||
|
||||
if (balance != null) {
|
||||
Timber.i("Yield balance found for $currentStakingId:\n$balance")
|
||||
balance
|
||||
} else {
|
||||
Timber.i("No yield balance found for $currentStakingId:\n${YieldBalance.Unsupported}")
|
||||
YieldBalance.Unsupported
|
||||
}
|
||||
Timber.i("Yield balance found for $currentStakingId:\n$balance")
|
||||
balance
|
||||
}
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.flowOn(dispatchers.default)
|
||||
}
|
||||
|
||||
private suspend fun getStakingId(): StakingID? {
|
||||
val saved = stakingId
|
||||
|
||||
if (saved != null) return saved
|
||||
|
||||
return stakingIdFactory.create(
|
||||
userWalletId = params.userWalletId,
|
||||
currencyId = params.currencyId,
|
||||
network = params.network,
|
||||
)
|
||||
.also { stakingId = it }
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : SingleYieldBalanceProducer.Factory {
|
||||
override fun create(params: SingleYieldBalanceProducer.Params): DefaultSingleYieldBalanceProducer
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@ import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrap
|
|||
import com.tangem.datasource.local.datastore.RuntimeSharedStore
|
||||
import com.tangem.datasource.local.token.converter.YieldBalanceConverter
|
||||
import com.tangem.domain.models.StatusSource
|
||||
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.staking.model.StakingID
|
||||
import com.tangem.domain.staking.model.stakekit.YieldBalance
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.extensions.addOrReplace
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
|
@ -46,6 +46,8 @@ internal class DefaultYieldsBalancesStore(
|
|||
value = cachedStatuses.map { (stringWalletId, wrappers) ->
|
||||
val key = UserWalletId(stringWalletId)
|
||||
val value = YieldBalanceConverter(isCached = true).convertSet(input = wrappers)
|
||||
.filterNotNull()
|
||||
.toSet()
|
||||
|
||||
key to value
|
||||
}
|
||||
|
|
@ -61,7 +63,7 @@ internal class DefaultYieldsBalancesStore(
|
|||
override suspend fun getSyncOrNull(userWalletId: UserWalletId, stakingId: StakingID): YieldBalance? {
|
||||
return runtimeStore.getSyncOrNull()
|
||||
?.get(userWalletId)
|
||||
?.firstOrNull { stakingId == it.getStakingId() }
|
||||
?.firstOrNull { it.stakingId == stakingId }
|
||||
}
|
||||
|
||||
override suspend fun getAllSyncOrNull(userWalletId: UserWalletId): Set<YieldBalance>? {
|
||||
|
|
@ -96,11 +98,13 @@ internal class DefaultYieldsBalancesStore(
|
|||
|
||||
private suspend fun storeInRuntime(userWalletId: UserWalletId, values: Set<YieldBalanceWrapperDTO>) {
|
||||
val newBalances = YieldBalanceConverter(isCached = false).convertSet(input = values)
|
||||
.filterNotNull()
|
||||
.toSet()
|
||||
|
||||
runtimeStore.update(default = emptyMap()) { saved ->
|
||||
saved.toMutableMap().apply {
|
||||
this[userWalletId] = saved[userWalletId]
|
||||
?.addOrReplace(newBalances) { old, new -> old.getStakingId() == new.getStakingId() }
|
||||
?.addOrReplace(newBalances) { old, new -> old.stakingId == new.stakingId }
|
||||
?: newBalances
|
||||
}
|
||||
}
|
||||
|
|
@ -135,7 +139,7 @@ internal class DefaultYieldsBalancesStore(
|
|||
|
||||
val balances = stakingIds.mapNotNullTo(hashSetOf()) { stakingId ->
|
||||
val balance = portfolioBalances
|
||||
.firstOrNull { stakingId == it.getStakingId() }
|
||||
.firstOrNull { it.stakingId == stakingId }
|
||||
?: ifNotFound(stakingId)
|
||||
?: return@mapNotNullTo null
|
||||
|
||||
|
|
@ -143,7 +147,7 @@ internal class DefaultYieldsBalancesStore(
|
|||
}
|
||||
|
||||
val updatedBalances = portfolioBalances.addOrReplace(items = balances) { old, new ->
|
||||
old.getStakingId() == new.getStakingId()
|
||||
old.stakingId == new.stakingId
|
||||
}
|
||||
|
||||
put(key = userWalletId, value = updatedBalances)
|
||||
|
|
@ -151,9 +155,7 @@ internal class DefaultYieldsBalancesStore(
|
|||
}
|
||||
}
|
||||
|
||||
private fun createErrorYieldBalance(id: StakingID): YieldBalance {
|
||||
return YieldBalance.Error(integrationId = id.integrationId, address = id.address)
|
||||
}
|
||||
private fun createErrorYieldBalance(id: StakingID): YieldBalance = YieldBalance.Error(stakingId = id)
|
||||
|
||||
private fun YieldBalanceWrapperDTO.getStakingId(): StakingID? {
|
||||
val integrationId = integrationId
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
package com.tangem.data.staking.store
|
||||
|
||||
import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO
|
||||
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.staking.model.StakingID
|
||||
import com.tangem.domain.staking.model.stakekit.YieldBalance
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,77 +0,0 @@
|
|||
package com.tangem.data.staking.utils
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchainsdk.utils.toCoinId
|
||||
import com.tangem.blockchainsdk.utils.toMigratedCoinId
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.staking.model.StakingID
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Factory of [StakingID]
|
||||
*
|
||||
* @property walletManagersFacade wallet manager facade
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class StakingIdFactory @Inject constructor(
|
||||
private val walletManagersFacade: WalletManagersFacade,
|
||||
) {
|
||||
|
||||
suspend fun create(userWalletId: UserWalletId, currencyId: CryptoCurrency.ID, network: Network): StakingID? {
|
||||
val address = walletManagersFacade.getDefaultAddress(userWalletId = userWalletId, network = network)
|
||||
val integrationId = createIntegrationId(currencyId)
|
||||
|
||||
if (address == null || integrationId == null) return null
|
||||
|
||||
return StakingID(integrationId = integrationId, address = address)
|
||||
}
|
||||
|
||||
fun createIntegrationId(currencyId: CryptoCurrency.ID): String? {
|
||||
val integrationKey = with(currencyId) { rawNetworkId.plus(rawCurrencyId) }
|
||||
return integrationIdMap[integrationKey]
|
||||
}
|
||||
|
||||
fun isPolygonIntegrationId(integrationId: String): Boolean = integrationId == ETHEREUM_POLYGON_INTEGRATION_ID
|
||||
|
||||
@Suppress("UnusedPrivateMember", "unused")
|
||||
companion object {
|
||||
|
||||
private const val TON_INTEGRATION_ID = "ton-ton-chorus-one-pools-staking"
|
||||
private const val SOLANA_INTEGRATION_ID = "solana-sol-native-multivalidator-staking"
|
||||
private const val COSMOS_INTEGRATION_ID = "cosmos-atom-native-staking"
|
||||
private const val ETHEREUM_POLYGON_INTEGRATION_ID = "ethereum-matic-native-staking"
|
||||
private const val BINANCE_INTEGRATION_ID = "bsc-bnb-native-staking"
|
||||
private const val POLKADOT_INTEGRATION_ID = "polkadot-dot-validator-staking"
|
||||
private const val AVALANCHE_INTEGRATION_ID = "avalanche-avax-native-staking"
|
||||
private const val TRON_INTEGRATION_ID = "tron-trx-native-staking"
|
||||
private const val CRONOS_INTEGRATION_ID = "cronos-cro-native-staking"
|
||||
private const val KAVA_INTEGRATION_ID = "kava-kava-native-staking"
|
||||
private const val NEAR_INTEGRATION_ID = "near-near-native-staking"
|
||||
private const val TEZOS_INTEGRATION_ID = "tezos-xtz-native-staking"
|
||||
private const val CARDANO_INTEGRATION_ID = "cardano-ada-native-staking"
|
||||
|
||||
// uncomment items as implementation is ready
|
||||
private val integrationIdMap = mapOf(
|
||||
Blockchain.TON.toDefaultKey() to TON_INTEGRATION_ID,
|
||||
Blockchain.Solana.toDefaultKey() to SOLANA_INTEGRATION_ID,
|
||||
Blockchain.Cosmos.toDefaultKey() to COSMOS_INTEGRATION_ID,
|
||||
Blockchain.Tron.toDefaultKey() to TRON_INTEGRATION_ID,
|
||||
Blockchain.Ethereum.id + Blockchain.Polygon.toMigratedCoinId() to ETHEREUM_POLYGON_INTEGRATION_ID,
|
||||
// Blockchain.Ethereum.id + Blockchain.Polygon.toCoinId() to ETHEREUM_POLYGON_INTEGRATION_ID,
|
||||
Blockchain.BSC.toDefaultKey() to BINANCE_INTEGRATION_ID,
|
||||
// Blockchain.Polkadot.toDefaultKey() to POLKADOT_INTEGRATION_ID,
|
||||
// Blockchain.Avalanche.toDefaultKey() to AVALANCHE_INTEGRATION_ID,
|
||||
// Blockchain.Cronos.toDefaultKey() to CRONOS_INTEGRATION_ID,
|
||||
// Blockchain.Kava.toDefaultKey() to KAVA_INTEGRATION_ID,
|
||||
// Blockchain.Near.toDefaultKey() to NEAR_INTEGRATION_ID,
|
||||
// Blockchain.Tezos.toDefaultKey() to TEZOS_INTEGRATION_ID,
|
||||
Blockchain.Cardano.toDefaultKey() to CARDANO_INTEGRATION_ID,
|
||||
)
|
||||
|
||||
private fun Blockchain.toDefaultKey(): String = id + toCoinId()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
package com.tangem.data.staking.utils
|
||||
|
||||
import com.tangem.datasource.api.stakekit.models.request.Address
|
||||
import com.tangem.domain.staking.model.StakingID
|
||||
import com.tangem.domain.models.staking.StakingID
|
||||
|
||||
/**
|
||||
* Factory for creating [Address]
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package com.tangem.data.staking.utils
|
||||
|
||||
import com.tangem.datasource.api.stakekit.models.request.YieldBalanceRequestBody
|
||||
import com.tangem.domain.staking.model.StakingID
|
||||
import com.tangem.domain.models.staking.StakingID
|
||||
|
||||
/**
|
||||
* Factory for creating [YieldBalanceRequestBody]
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ package com.tangem.data.staking
|
|||
import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO
|
||||
import com.tangem.datasource.local.token.converter.YieldBalanceConverter
|
||||
import com.tangem.domain.models.StatusSource
|
||||
import com.tangem.domain.staking.model.stakekit.YieldBalance
|
||||
import com.tangem.domain.models.staking.YieldBalance
|
||||
|
||||
internal fun YieldBalanceWrapperDTO.toDomain(source: StatusSource = StatusSource.CACHE): YieldBalance {
|
||||
return YieldBalanceConverter(source = source).convert(this)
|
||||
return YieldBalanceConverter(source = source).convert(this)!!
|
||||
}
|
||||
|
|
@ -1,14 +1,12 @@
|
|||
package com.tangem.data.staking.multi
|
||||
|
||||
import arrow.core.toOption
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.common.test.data.staking.MockYieldBalanceWrapperDTOFactory
|
||||
import com.tangem.common.test.data.staking.MockYieldDTOFactory
|
||||
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
|
||||
import com.tangem.common.test.domain.wallet.MockUserWalletFactory
|
||||
import com.tangem.common.test.utils.assertEitherLeft
|
||||
import com.tangem.common.test.utils.assertEitherRight
|
||||
import com.tangem.data.staking.store.YieldsBalancesStore
|
||||
import com.tangem.data.staking.utils.StakingIdFactory
|
||||
import com.tangem.data.staking.utils.YieldBalanceRequestBodyFactory
|
||||
import com.tangem.datasource.api.common.response.ApiResponse
|
||||
import com.tangem.datasource.api.common.response.ApiResponseError
|
||||
|
|
@ -16,8 +14,8 @@ import com.tangem.datasource.api.stakekit.StakeKitApi
|
|||
import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO
|
||||
import com.tangem.datasource.local.token.StakingYieldsStore
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.models.staking.StakingID
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.staking.model.StakingID
|
||||
import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import io.mockk.*
|
||||
|
|
@ -34,55 +32,46 @@ internal class DefaultMultiYieldBalanceFetcherTest {
|
|||
|
||||
private val userWalletsStore: UserWalletsStore = mockk()
|
||||
private val stakingYieldsStore: StakingYieldsStore = mockk()
|
||||
private val yieldsBalancesStore: YieldsBalancesStore = mockk()
|
||||
private val stakingIdFactory: StakingIdFactory = mockk()
|
||||
private val yieldsBalancesStore: YieldsBalancesStore = mockk(relaxUnitFun = true)
|
||||
private val stakeKitApi: StakeKitApi = mockk()
|
||||
|
||||
private val fetcher = DefaultMultiYieldBalanceFetcher(
|
||||
userWalletsStore = userWalletsStore,
|
||||
stakingYieldsStore = stakingYieldsStore,
|
||||
yieldsBalancesStore = yieldsBalancesStore,
|
||||
stakingIdFactory = stakingIdFactory,
|
||||
stakeKitApi = stakeKitApi,
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
)
|
||||
|
||||
@BeforeEach
|
||||
fun resetMocks() {
|
||||
clearMocks(userWalletsStore, stakingYieldsStore, yieldsBalancesStore, stakingIdFactory, stakeKitApi)
|
||||
clearMocks(userWalletsStore, stakingYieldsStore, yieldsBalancesStore, stakeKitApi)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fetch yields balances successfully`() = runTest {
|
||||
// Arrange
|
||||
val currencyIdWithNetworkMap = mapOf(ton.id to ton.network, solana.id to solana.network)
|
||||
|
||||
val params = MultiYieldBalanceFetcher.Params(userWalletId, currencyIdWithNetworkMap)
|
||||
val params = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds)
|
||||
|
||||
coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet
|
||||
coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns tonId
|
||||
coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns solanaId
|
||||
coEvery { yieldsBalancesStore.refresh(params.userWalletId, tonAndSolanaIds) } just Runs
|
||||
|
||||
val yields = listOf(MockYieldDTOFactory.create(tonId), MockYieldDTOFactory.create(solanaId))
|
||||
coEvery { stakingYieldsStore.getSyncWithTimeout() } returns yields
|
||||
|
||||
val requests = tonAndSolanaIds.map(YieldBalanceRequestBodyFactory::create).sortedBy { it.integrationId }
|
||||
val requests = tonAndSolanaIds.map(YieldBalanceRequestBodyFactory::create)
|
||||
val result = setOf(
|
||||
MockYieldBalanceWrapperDTOFactory.createWithBalance(solanaId),
|
||||
MockYieldBalanceWrapperDTOFactory.createWithBalance(tonId),
|
||||
)
|
||||
|
||||
coEvery { stakeKitApi.getMultipleYieldBalances(requests) } returns ApiResponse.Success(result)
|
||||
coEvery { yieldsBalancesStore.storeActual(userWalletId = userWalletId, values = result) } just Runs
|
||||
|
||||
// Actual
|
||||
val actual = fetcher.invoke(params)
|
||||
|
||||
// Assert
|
||||
coVerify {
|
||||
coVerifyOrder {
|
||||
userWalletsStore.getSyncOrNull(params.userWalletId)
|
||||
stakingIdFactory.create(params.userWalletId, ton.id, ton.network)
|
||||
stakingIdFactory.create(params.userWalletId, solana.id, solana.network)
|
||||
yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds)
|
||||
stakingYieldsStore.getSyncWithTimeout()
|
||||
stakeKitApi.getMultipleYieldBalances(requests)
|
||||
|
|
@ -91,40 +80,30 @@ internal class DefaultMultiYieldBalanceFetcherTest {
|
|||
|
||||
coVerify(inverse = true) { yieldsBalancesStore.storeError(any(), any()) }
|
||||
|
||||
Truth.assertThat(actual.isRight()).isTrue()
|
||||
assertEitherRight(actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fetch yields balances successfully if one of stakingIds is unavailable`() = runTest {
|
||||
// Arrange
|
||||
val currencyIdWithNetworkMap = mapOf(ton.id to ton.network, solana.id to solana.network)
|
||||
|
||||
val params = MultiYieldBalanceFetcher.Params(userWalletId, currencyIdWithNetworkMap)
|
||||
val params = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds)
|
||||
|
||||
coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet
|
||||
coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns tonId
|
||||
coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns solanaId
|
||||
coEvery { yieldsBalancesStore.refresh(params.userWalletId, tonAndSolanaIds) } just Runs
|
||||
|
||||
val yields = listOf(MockYieldDTOFactory.create(tonId))
|
||||
coEvery { stakingYieldsStore.getSyncWithTimeout() } returns yields
|
||||
|
||||
coEvery { yieldsBalancesStore.storeError(userWalletId = userWalletId, stakingIds = setOf(solanaId)) } just Runs
|
||||
|
||||
val requests = listOf(YieldBalanceRequestBodyFactory.create(tonId))
|
||||
val result = setOf(MockYieldBalanceWrapperDTOFactory.createWithBalance(tonId))
|
||||
|
||||
coEvery { stakeKitApi.getMultipleYieldBalances(requests) } returns ApiResponse.Success(result)
|
||||
coEvery { yieldsBalancesStore.storeActual(userWalletId = userWalletId, values = result) } just Runs
|
||||
|
||||
// Actual
|
||||
val actual = fetcher.invoke(params)
|
||||
|
||||
// Assert
|
||||
coVerify {
|
||||
coVerifyOrder {
|
||||
userWalletsStore.getSyncOrNull(params.userWalletId)
|
||||
stakingIdFactory.create(params.userWalletId, ton.id, ton.network)
|
||||
stakingIdFactory.create(params.userWalletId, solana.id, solana.network)
|
||||
yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds)
|
||||
stakingYieldsStore.getSyncWithTimeout()
|
||||
yieldsBalancesStore.storeError(userWalletId = userWalletId, stakingIds = setOf(solanaId))
|
||||
|
|
@ -132,15 +111,13 @@ internal class DefaultMultiYieldBalanceFetcherTest {
|
|||
yieldsBalancesStore.storeActual(userWalletId = userWalletId, values = result)
|
||||
}
|
||||
|
||||
Truth.assertThat(actual.isRight()).isTrue()
|
||||
assertEitherRight(actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fetch yields balances failure if user wallet is not supported`() = runTest {
|
||||
// Arrange
|
||||
val currencyIdWithNetworkMap = mapOf(ton.id to ton.network, solana.id to solana.network)
|
||||
|
||||
val params = MultiYieldBalanceFetcher.Params(userWalletId, currencyIdWithNetworkMap)
|
||||
val params = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds)
|
||||
|
||||
val userWallet = MockUserWalletFactory.create().copy(isMultiCurrency = false)
|
||||
coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet
|
||||
|
|
@ -149,10 +126,9 @@ internal class DefaultMultiYieldBalanceFetcherTest {
|
|||
val actual = fetcher.invoke(params)
|
||||
|
||||
// Assert
|
||||
coVerify { userWalletsStore.getSyncOrNull(params.userWalletId) }
|
||||
coVerifyOrder { userWalletsStore.getSyncOrNull(params.userWalletId) }
|
||||
|
||||
coVerify(inverse = true) {
|
||||
stakingIdFactory.create(params.userWalletId, ton.id, ton.network)
|
||||
yieldsBalancesStore.refresh(userWalletId = any(), stakingIds = any())
|
||||
stakingYieldsStore.getSyncWithTimeout()
|
||||
stakeKitApi.getSingleYieldBalance(integrationId = any(), body = any())
|
||||
|
|
@ -162,17 +138,13 @@ internal class DefaultMultiYieldBalanceFetcherTest {
|
|||
|
||||
val expected = IllegalStateException("Wallet ${params.userWalletId} is not supported: ${userWallet.toOption()}")
|
||||
|
||||
Truth.assertThat(actual.isLeft()).isTrue()
|
||||
Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected::class.java)
|
||||
Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.message)
|
||||
assertEitherLeft(actual, expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fetch yields balances failure if userWalletsStore returns null`() = runTest {
|
||||
// Arrange
|
||||
val currencyIdWithNetworkMap = mapOf(ton.id to ton.network, solana.id to solana.network)
|
||||
|
||||
val params = MultiYieldBalanceFetcher.Params(userWalletId, currencyIdWithNetworkMap)
|
||||
val params = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds)
|
||||
|
||||
coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns null
|
||||
|
||||
|
|
@ -180,10 +152,9 @@ internal class DefaultMultiYieldBalanceFetcherTest {
|
|||
val actual = fetcher.invoke(params)
|
||||
|
||||
// Assert
|
||||
coVerify { userWalletsStore.getSyncOrNull(params.userWalletId) }
|
||||
coVerifyOrder { userWalletsStore.getSyncOrNull(params.userWalletId) }
|
||||
|
||||
coVerify(inverse = true) {
|
||||
stakingIdFactory.create(params.userWalletId, ton.id, ton.network)
|
||||
yieldsBalancesStore.refresh(userWalletId = any(), stakingIds = any())
|
||||
stakingYieldsStore.getSyncWithTimeout()
|
||||
stakeKitApi.getSingleYieldBalance(integrationId = any(), body = any())
|
||||
|
|
@ -193,69 +164,23 @@ internal class DefaultMultiYieldBalanceFetcherTest {
|
|||
|
||||
val expected = IllegalStateException("Wallet ${params.userWalletId} is not supported: ${null.toOption()}")
|
||||
|
||||
Truth.assertThat(actual.isLeft()).isTrue()
|
||||
Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected::class.java)
|
||||
Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.message)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fetch yields balances failure if stakingIdFactory returns null`() = runTest {
|
||||
// Arrange
|
||||
val currencyIdWithNetworkMap = mapOf(ton.id to ton.network, solana.id to solana.network)
|
||||
|
||||
val params = MultiYieldBalanceFetcher.Params(userWalletId, currencyIdWithNetworkMap)
|
||||
|
||||
coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet
|
||||
coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns null
|
||||
coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns null
|
||||
|
||||
// Actual
|
||||
val actual = fetcher.invoke(params)
|
||||
|
||||
// Assert
|
||||
coVerify {
|
||||
userWalletsStore.getSyncOrNull(params.userWalletId)
|
||||
stakingIdFactory.create(params.userWalletId, ton.id, ton.network)
|
||||
stakingIdFactory.create(params.userWalletId, solana.id, solana.network)
|
||||
}
|
||||
|
||||
coVerify(inverse = true) {
|
||||
yieldsBalancesStore.refresh(any(), any<Set<StakingID>>())
|
||||
stakingYieldsStore.getSyncWithTimeout()
|
||||
stakeKitApi.getMultipleYieldBalances(any())
|
||||
yieldsBalancesStore.storeActual(any(), any())
|
||||
yieldsBalancesStore.storeError(any(), any())
|
||||
}
|
||||
|
||||
val expected = IllegalStateException("Unable to create staking ids for $params: list is empty")
|
||||
|
||||
Truth.assertThat(actual.isLeft()).isTrue()
|
||||
Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected::class.java)
|
||||
Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.message)
|
||||
assertEitherLeft(actual, expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fetch yields balances failure if stakingYieldsStore getSyncWithTimeout returns null`() = runTest {
|
||||
// Arrange
|
||||
val currencyIdWithNetworkMap = mapOf(ton.id to ton.network, solana.id to solana.network)
|
||||
|
||||
val params = MultiYieldBalanceFetcher.Params(userWalletId, currencyIdWithNetworkMap)
|
||||
val params = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds)
|
||||
|
||||
coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet
|
||||
coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns tonId
|
||||
coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns solanaId
|
||||
coEvery { yieldsBalancesStore.refresh(params.userWalletId, tonAndSolanaIds) } just Runs
|
||||
coEvery { stakingYieldsStore.getSyncWithTimeout() } returns null
|
||||
coEvery { yieldsBalancesStore.storeError(userWalletId, tonAndSolanaIds) } just Runs
|
||||
|
||||
// Actual
|
||||
val actual = fetcher.invoke(params)
|
||||
|
||||
// Assert
|
||||
coVerify {
|
||||
coVerifyOrder {
|
||||
userWalletsStore.getSyncOrNull(params.userWalletId)
|
||||
stakingIdFactory.create(params.userWalletId, ton.id, ton.network)
|
||||
stakingIdFactory.create(params.userWalletId, solana.id, solana.network)
|
||||
yieldsBalancesStore.refresh(params.userWalletId, tonAndSolanaIds)
|
||||
stakingYieldsStore.getSyncWithTimeout()
|
||||
yieldsBalancesStore.storeError(userWalletId, tonAndSolanaIds)
|
||||
|
|
@ -268,33 +193,23 @@ internal class DefaultMultiYieldBalanceFetcherTest {
|
|||
|
||||
val expected = IllegalStateException("No enabled yields for ${params.userWalletId}")
|
||||
|
||||
Truth.assertThat(actual.isLeft()).isTrue()
|
||||
Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected::class.java)
|
||||
Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.message)
|
||||
assertEitherLeft(actual, expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fetch yields balances failure if stakingYieldsStore getSyncWithTimeout returns empty list`() = runTest {
|
||||
// Arrange
|
||||
val currencyIdWithNetworkMap = mapOf(ton.id to ton.network, solana.id to solana.network)
|
||||
|
||||
val params = MultiYieldBalanceFetcher.Params(userWalletId, currencyIdWithNetworkMap)
|
||||
val params = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds)
|
||||
|
||||
coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet
|
||||
coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns tonId
|
||||
coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns solanaId
|
||||
coEvery { yieldsBalancesStore.refresh(params.userWalletId, tonAndSolanaIds) } just Runs
|
||||
coEvery { stakingYieldsStore.getSyncWithTimeout() } returns emptyList()
|
||||
coEvery { yieldsBalancesStore.storeError(userWalletId, tonAndSolanaIds) } just Runs
|
||||
|
||||
// Actual
|
||||
val actual = fetcher.invoke(params)
|
||||
|
||||
// Assert
|
||||
coVerify {
|
||||
coVerifyOrder {
|
||||
userWalletsStore.getSyncOrNull(params.userWalletId)
|
||||
stakingIdFactory.create(params.userWalletId, ton.id, ton.network)
|
||||
stakingIdFactory.create(params.userWalletId, solana.id, solana.network)
|
||||
yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds)
|
||||
stakingYieldsStore.getSyncWithTimeout()
|
||||
yieldsBalancesStore.storeError(userWalletId, tonAndSolanaIds)
|
||||
|
|
@ -307,38 +222,28 @@ internal class DefaultMultiYieldBalanceFetcherTest {
|
|||
|
||||
val expected = IllegalStateException("No enabled yields for ${params.userWalletId}")
|
||||
|
||||
Truth.assertThat(actual.isLeft()).isTrue()
|
||||
Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected::class.java)
|
||||
Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.message)
|
||||
assertEitherLeft(actual, expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fetch yields balances failure if yields converting is failed`() = runTest {
|
||||
// Arrange
|
||||
val currencyIdWithNetworkMap = mapOf(ton.id to ton.network, solana.id to solana.network)
|
||||
|
||||
val params = MultiYieldBalanceFetcher.Params(userWalletId, currencyIdWithNetworkMap)
|
||||
val params = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds)
|
||||
|
||||
coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet
|
||||
coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns tonId
|
||||
coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns solanaId
|
||||
coEvery { yieldsBalancesStore.refresh(params.userWalletId, tonAndSolanaIds) } just Runs
|
||||
|
||||
val yields = listOf(
|
||||
MockYieldDTOFactory.create(tonId).copy(id = null),
|
||||
MockYieldDTOFactory.create(solanaId).copy(id = null),
|
||||
)
|
||||
coEvery { stakingYieldsStore.getSyncWithTimeout() } returns yields
|
||||
coEvery { yieldsBalancesStore.storeError(userWalletId, tonAndSolanaIds) } just Runs
|
||||
|
||||
// Actual
|
||||
val actual = fetcher.invoke(params)
|
||||
|
||||
// Assert
|
||||
coVerify {
|
||||
coVerifyOrder {
|
||||
userWalletsStore.getSyncOrNull(params.userWalletId)
|
||||
stakingIdFactory.create(params.userWalletId, ton.id, ton.network)
|
||||
stakingIdFactory.create(params.userWalletId, solana.id, solana.network)
|
||||
yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds)
|
||||
stakingYieldsStore.getSyncWithTimeout()
|
||||
yieldsBalancesStore.storeError(userWalletId, tonAndSolanaIds)
|
||||
|
|
@ -351,35 +256,25 @@ internal class DefaultMultiYieldBalanceFetcherTest {
|
|||
|
||||
val expected = IllegalStateException("No enabled yields for ${params.userWalletId}")
|
||||
|
||||
Truth.assertThat(actual.isLeft()).isTrue()
|
||||
Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected::class.java)
|
||||
Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.message)
|
||||
assertEitherLeft(actual, expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fetch yields balances failure if available yields does not contain ids from params`() = runTest {
|
||||
// Arrange
|
||||
val currencyIdWithNetworkMap = mapOf(ton.id to ton.network, solana.id to solana.network)
|
||||
|
||||
val params = MultiYieldBalanceFetcher.Params(userWalletId, currencyIdWithNetworkMap)
|
||||
val params = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds)
|
||||
|
||||
coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet
|
||||
coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns tonId
|
||||
coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns solanaId
|
||||
coEvery { yieldsBalancesStore.refresh(params.userWalletId, tonAndSolanaIds) } just Runs
|
||||
|
||||
val yields = listOf(MockYieldDTOFactory.create(StakingID(integrationId = "polygon", address = "0x1")))
|
||||
coEvery { stakingYieldsStore.getSyncWithTimeout() } returns yields
|
||||
coEvery { yieldsBalancesStore.storeError(userWalletId, tonAndSolanaIds) } just Runs
|
||||
|
||||
// Actual
|
||||
val actual = fetcher.invoke(params)
|
||||
|
||||
// Assert
|
||||
coVerify {
|
||||
coVerifyOrder {
|
||||
userWalletsStore.getSyncOrNull(params.userWalletId)
|
||||
stakingIdFactory.create(params.userWalletId, ton.id, ton.network)
|
||||
stakingIdFactory.create(params.userWalletId, solana.id, solana.network)
|
||||
yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds)
|
||||
stakingYieldsStore.getSyncWithTimeout()
|
||||
yieldsBalancesStore.storeError(userWalletId, tonAndSolanaIds)
|
||||
|
|
@ -394,47 +289,37 @@ internal class DefaultMultiYieldBalanceFetcherTest {
|
|||
"""
|
||||
No available yields to fetch yield balances:
|
||||
– userWalletId: $userWalletId
|
||||
– stakingIds: ${setOf(solanaId, tonId).joinToString()}
|
||||
– stakingIds: ${tonAndSolanaIds.joinToString()}
|
||||
""".trimIndent(),
|
||||
)
|
||||
|
||||
Truth.assertThat(actual.isLeft()).isTrue()
|
||||
Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected::class.java)
|
||||
Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.message)
|
||||
assertEitherLeft(actual, expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fetch yields balances failure if stakeKitApi getMultipleYieldBalances is failed`() = runTest {
|
||||
// Arrange
|
||||
val currencyIdWithNetworkMap = mapOf(ton.id to ton.network, solana.id to solana.network)
|
||||
|
||||
val params = MultiYieldBalanceFetcher.Params(userWalletId, currencyIdWithNetworkMap)
|
||||
val params = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds)
|
||||
|
||||
coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet
|
||||
coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns tonId
|
||||
coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns solanaId
|
||||
coEvery { yieldsBalancesStore.refresh(params.userWalletId, tonAndSolanaIds) } just Runs
|
||||
|
||||
val yields = listOf(MockYieldDTOFactory.create(tonId), MockYieldDTOFactory.create(solanaId))
|
||||
coEvery { stakingYieldsStore.getSyncWithTimeout() } returns yields
|
||||
|
||||
val requests = setOf(solanaId, tonId).map(YieldBalanceRequestBodyFactory::create)
|
||||
val requests = setOf(tonId, solanaId).map(YieldBalanceRequestBodyFactory::create)
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val errorResponse = ApiResponse.Error(ApiResponseError.NetworkException)
|
||||
as ApiResponse<Set<YieldBalanceWrapperDTO>>
|
||||
|
||||
coEvery { stakeKitApi.getMultipleYieldBalances(requests) } returns errorResponse
|
||||
coEvery { yieldsBalancesStore.storeError(userWalletId, tonAndSolanaIds) } just Runs
|
||||
|
||||
// Actual
|
||||
val actual = fetcher.invoke(params)
|
||||
|
||||
// Assert
|
||||
coVerify {
|
||||
coVerifyOrder {
|
||||
userWalletsStore.getSyncOrNull(params.userWalletId)
|
||||
stakingIdFactory.create(params.userWalletId, ton.id, ton.network)
|
||||
stakingIdFactory.create(params.userWalletId, solana.id, solana.network)
|
||||
yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds)
|
||||
stakingYieldsStore.getSyncWithTimeout()
|
||||
stakeKitApi.getMultipleYieldBalances(requests)
|
||||
|
|
@ -445,20 +330,13 @@ internal class DefaultMultiYieldBalanceFetcherTest {
|
|||
|
||||
val expected = ApiResponseError.NetworkException
|
||||
|
||||
Truth.assertThat(actual.isLeft()).isTrue()
|
||||
Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected::class.java)
|
||||
Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.message)
|
||||
assertEitherLeft(actual, expected)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
val userWalletId = UserWalletId("011")
|
||||
val userWallet = MockUserWalletFactory.create()
|
||||
|
||||
val mocks = MockCryptoCurrencyFactory()
|
||||
|
||||
val ton = mocks.createCoin(Blockchain.TON)
|
||||
val solana = mocks.createCoin(Blockchain.Solana)
|
||||
|
||||
val tonId = MockYieldBalanceWrapperDTOFactory.defaultStakingId
|
||||
val solanaId = StakingID(
|
||||
integrationId = "solana-sol-native-multivalidator-staking",
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@ import com.tangem.common.test.data.staking.MockYieldBalanceWrapperDTOFactory
|
|||
import com.tangem.common.test.utils.getEmittedValues
|
||||
import com.tangem.data.staking.store.YieldsBalancesStore
|
||||
import com.tangem.data.staking.toDomain
|
||||
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.staking.model.StakingID
|
||||
import com.tangem.domain.staking.model.stakekit.YieldBalance
|
||||
import com.tangem.domain.staking.multi.MultiYieldBalanceProducer
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import io.mockk.every
|
||||
|
|
|
|||
|
|
@ -3,8 +3,7 @@ package com.tangem.data.staking.single
|
|||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
|
||||
import com.tangem.common.test.data.staking.MockYieldBalanceWrapperDTOFactory
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher
|
||||
import com.tangem.domain.staking.single.SingleYieldBalanceFetcher
|
||||
|
|
@ -37,15 +36,11 @@ internal class DefaultSingleYieldBalanceFetcherTest {
|
|||
@Test
|
||||
fun `fetch yield balance successfully`() = runTest {
|
||||
// Arrange
|
||||
val params = SingleYieldBalanceFetcher.Params(
|
||||
userWalletId = userWalletId,
|
||||
currencyId = ton.id,
|
||||
network = ton.network,
|
||||
)
|
||||
val params = SingleYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingId = tonId)
|
||||
|
||||
val multiParams = MultiYieldBalanceFetcher.Params(
|
||||
userWalletId = userWalletId,
|
||||
currencyIdWithNetworkMap = mapOf(ton.id to ton.network),
|
||||
stakingIds = setOf(tonId),
|
||||
)
|
||||
|
||||
val multiResult = Unit.right()
|
||||
|
|
@ -64,16 +59,9 @@ internal class DefaultSingleYieldBalanceFetcherTest {
|
|||
@Test
|
||||
fun `fetch yield balance failure`() = runTest {
|
||||
// Arrange
|
||||
val params = SingleYieldBalanceFetcher.Params(
|
||||
userWalletId = userWalletId,
|
||||
currencyId = ton.id,
|
||||
network = ton.network,
|
||||
)
|
||||
val params = SingleYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingId = tonId)
|
||||
|
||||
val multiParams = MultiYieldBalanceFetcher.Params(
|
||||
userWalletId = userWalletId,
|
||||
currencyIdWithNetworkMap = mapOf(ton.id to ton.network),
|
||||
)
|
||||
val multiParams = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = setOf(tonId))
|
||||
|
||||
val multiResult = IllegalStateException().left()
|
||||
|
||||
|
|
@ -89,6 +77,6 @@ internal class DefaultSingleYieldBalanceFetcherTest {
|
|||
|
||||
private companion object {
|
||||
val userWalletId = UserWalletId("011")
|
||||
val ton = MockCryptoCurrencyFactory().createCoin(Blockchain.TON)
|
||||
val tonId = MockYieldBalanceWrapperDTOFactory.defaultStakingId
|
||||
}
|
||||
}
|
||||
|
|
@ -1,54 +1,60 @@
|
|||
package com.tangem.data.staking.single
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.common.test.data.staking.MockYieldBalanceWrapperDTOFactory
|
||||
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
|
||||
import com.tangem.common.test.utils.getEmittedValues
|
||||
import com.tangem.core.analytics.api.AnalyticsExceptionHandler
|
||||
import com.tangem.data.staking.toDomain
|
||||
import com.tangem.data.staking.utils.StakingIdFactory
|
||||
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.staking.model.StakingID
|
||||
import com.tangem.domain.staking.model.stakekit.YieldBalance
|
||||
import com.tangem.domain.staking.multi.MultiYieldBalanceProducer
|
||||
import com.tangem.domain.staking.multi.MultiYieldBalanceSupplier
|
||||
import com.tangem.domain.staking.single.SingleYieldBalanceProducer
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import io.mockk.*
|
||||
import io.mockk.clearMocks
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class DefaultSingleYieldBalanceProducerTest {
|
||||
|
||||
private val params = SingleYieldBalanceProducer.Params(
|
||||
userWalletId = UserWalletId(stringValue = "011"),
|
||||
currencyId = ton.id,
|
||||
network = ton.network,
|
||||
stakingId = tonId,
|
||||
)
|
||||
|
||||
private val multiNetworkStatusSupplier = mockk<MultiYieldBalanceSupplier>()
|
||||
private val stakingIdFactory = mockk<StakingIdFactory>()
|
||||
private val analyticsExceptionHandler = mockk<AnalyticsExceptionHandler>(relaxUnitFun = true)
|
||||
private val dispatchers = TestingCoroutineDispatcherProvider()
|
||||
|
||||
private val producer = DefaultSingleYieldBalanceProducer(
|
||||
params = params,
|
||||
stakingIdFactory = stakingIdFactory,
|
||||
multiYieldBalanceSupplier = multiNetworkStatusSupplier,
|
||||
analyticsExceptionHandler = analyticsExceptionHandler,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
|
||||
@BeforeEach
|
||||
fun resetMocks() {
|
||||
clearMocks(multiNetworkStatusSupplier, analyticsExceptionHandler)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test that flow is mapped for data from params`() = runTest {
|
||||
fun `flow is mapped for data from params`() = runTest {
|
||||
// Arrange
|
||||
val balance = MockYieldBalanceWrapperDTOFactory.createWithBalance(tonId).toDomain()
|
||||
|
||||
val expected = flowOf(
|
||||
val multiFlow = flowOf(
|
||||
setOf(
|
||||
balance,
|
||||
MockYieldBalanceWrapperDTOFactory.createWithBalance(solanaId).toDomain(),
|
||||
|
|
@ -56,97 +62,89 @@ internal class DefaultSingleYieldBalanceProducerTest {
|
|||
)
|
||||
|
||||
val multiParams = MultiYieldBalanceProducer.Params(userWalletId = params.userWalletId)
|
||||
every { multiNetworkStatusSupplier(multiParams) } returns expected
|
||||
coEvery { stakingIdFactory.create(params.userWalletId, params.currencyId, params.network) } returns tonId
|
||||
every { multiNetworkStatusSupplier(multiParams) } returns multiFlow
|
||||
|
||||
val actual = producer.produce()
|
||||
// Act
|
||||
val actual = getEmittedValues(flow = producer.produce())
|
||||
|
||||
verify { multiNetworkStatusSupplier(multiParams) }
|
||||
Truth.assertThat(actual).hasSize(1)
|
||||
Truth.assertThat(actual).containsExactly(balance)
|
||||
|
||||
val values = getEmittedValues(flow = actual)
|
||||
|
||||
coVerify { stakingIdFactory.create(params.userWalletId, params.currencyId, params.network) }
|
||||
|
||||
Truth.assertThat(values.size).isEqualTo(1)
|
||||
Truth.assertThat(values).isEqualTo(listOf(balance))
|
||||
verify(exactly = 1) { multiNetworkStatusSupplier(multiParams) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test that flow is updated if yield balance is updated`() = runTest {
|
||||
val expected = MutableSharedFlow<Set<YieldBalance>>(replay = 2, extraBufferCapacity = 1)
|
||||
fun `flow is updated if yield balance is updated`() = runTest {
|
||||
// Arrange
|
||||
val multiFlow = MutableSharedFlow<Set<YieldBalance>>(replay = 2, extraBufferCapacity = 1)
|
||||
|
||||
val multiParams = MultiYieldBalanceProducer.Params(userWalletId = params.userWalletId)
|
||||
every { multiNetworkStatusSupplier(multiParams) } returns expected
|
||||
coEvery { stakingIdFactory.create(params.userWalletId, params.currencyId, params.network) } returns tonId
|
||||
every { multiNetworkStatusSupplier(multiParams) } returns multiFlow
|
||||
|
||||
val actual = producer.produceWithFallback()
|
||||
val producerFlow = producer.produceWithFallback()
|
||||
|
||||
verify { multiNetworkStatusSupplier(multiParams) }
|
||||
|
||||
// first emit
|
||||
val balance = MockYieldBalanceWrapperDTOFactory.createWithBalance(tonId).toDomain()
|
||||
expected.emit(value = setOf(balance))
|
||||
val updatedBalance = YieldBalance.Error(stakingId = tonId)
|
||||
|
||||
val values1 = getEmittedValues(flow = actual)
|
||||
// Act (first emit)
|
||||
multiFlow.emit(value = setOf(balance))
|
||||
val actual1 = getEmittedValues(flow = producerFlow)
|
||||
|
||||
coVerify { stakingIdFactory.create(params.userWalletId, params.currencyId, params.network) }
|
||||
// Assert (first emit)
|
||||
Truth.assertThat(actual1).hasSize(1)
|
||||
Truth.assertThat(actual1).containsExactly(balance)
|
||||
|
||||
Truth.assertThat(values1.size).isEqualTo(1)
|
||||
Truth.assertThat(values1).isEqualTo(listOf(balance))
|
||||
// Act (second emit)
|
||||
multiFlow.emit(value = setOf(updatedBalance))
|
||||
val actual2 = getEmittedValues(flow = producerFlow)
|
||||
|
||||
// second emit
|
||||
val updatedStatus = YieldBalance.Error(integrationId = tonId.integrationId, address = tonId.address)
|
||||
expected.emit(value = setOf(updatedStatus))
|
||||
// Assert (second emit)
|
||||
Truth.assertThat(actual2).hasSize(2)
|
||||
Truth.assertThat(actual2).containsExactly(balance, updatedBalance)
|
||||
|
||||
val values2 = getEmittedValues(flow = actual)
|
||||
|
||||
coVerify { stakingIdFactory.create(params.userWalletId, params.currencyId, params.network) }
|
||||
|
||||
Truth.assertThat(values2.size).isEqualTo(2)
|
||||
Truth.assertThat(values2).isEqualTo(listOf(balance, updatedStatus))
|
||||
verify(exactly = 1) { multiNetworkStatusSupplier(multiParams) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test that flow is filtered the same status`() = runTest {
|
||||
val expected = MutableSharedFlow<Set<YieldBalance>>(replay = 2, extraBufferCapacity = 1)
|
||||
fun `flow is filtered the same status`() = runTest {
|
||||
// Arrange
|
||||
val multiFlow = MutableSharedFlow<Set<YieldBalance>>(replay = 2, extraBufferCapacity = 1)
|
||||
|
||||
val multiParams = MultiYieldBalanceProducer.Params(userWalletId = params.userWalletId)
|
||||
every { multiNetworkStatusSupplier(multiParams) } returns expected
|
||||
coEvery { stakingIdFactory.create(params.userWalletId, params.currencyId, params.network) } returns tonId
|
||||
every { multiNetworkStatusSupplier(multiParams) } returns multiFlow
|
||||
|
||||
val actual = producer.produceWithFallback()
|
||||
val producerFlow = producer.produceWithFallback()
|
||||
|
||||
verify { multiNetworkStatusSupplier(multiParams) }
|
||||
|
||||
// first emit
|
||||
val balance = MockYieldBalanceWrapperDTOFactory.createWithBalance(tonId).toDomain()
|
||||
expected.emit(value = setOf(balance))
|
||||
|
||||
val values1 = getEmittedValues(flow = actual)
|
||||
// Act (first emit)
|
||||
multiFlow.emit(value = setOf(balance))
|
||||
val actual1 = getEmittedValues(flow = producerFlow)
|
||||
|
||||
coVerify { stakingIdFactory.create(params.userWalletId, params.currencyId, params.network) }
|
||||
// Assert (first emit)
|
||||
Truth.assertThat(actual1).hasSize(1)
|
||||
Truth.assertThat(actual1).containsExactly(balance)
|
||||
|
||||
Truth.assertThat(values1.size).isEqualTo(1)
|
||||
Truth.assertThat(values1).isEqualTo(listOf(balance))
|
||||
// Act (second emit)
|
||||
multiFlow.emit(value = setOf(balance))
|
||||
val actual2 = getEmittedValues(flow = producerFlow)
|
||||
|
||||
// second emit
|
||||
expected.emit(value = setOf(balance))
|
||||
// Assert (second emit)
|
||||
Truth.assertThat(actual2).hasSize(1)
|
||||
Truth.assertThat(actual2).containsExactly(balance)
|
||||
|
||||
val values2 = getEmittedValues(flow = actual)
|
||||
|
||||
coVerify { stakingIdFactory.create(params.userWalletId, params.currencyId, params.network) }
|
||||
|
||||
Truth.assertThat(values2.size).isEqualTo(1)
|
||||
Truth.assertThat(values2).isEqualTo(listOf(balance))
|
||||
verify(exactly = 1) { multiNetworkStatusSupplier(multiParams) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test if flow throws exception`() = runTest {
|
||||
fun `flow throws exception`() = runTest {
|
||||
// Arrange
|
||||
val exception = IllegalStateException()
|
||||
|
||||
val balance = MockYieldBalanceWrapperDTOFactory.createWithBalance(tonId).toDomain()
|
||||
|
||||
val innerFlow = MutableStateFlow(value = false)
|
||||
val expected = flow {
|
||||
val multiFlow = flow {
|
||||
if (innerFlow.value) {
|
||||
emit(setOf(balance))
|
||||
} else {
|
||||
|
|
@ -156,83 +154,52 @@ internal class DefaultSingleYieldBalanceProducerTest {
|
|||
.buffer(capacity = 5)
|
||||
|
||||
val multiParams = MultiYieldBalanceProducer.Params(userWalletId = params.userWalletId)
|
||||
every { multiNetworkStatusSupplier(multiParams) } returns expected
|
||||
every { stakingIdFactory.createIntegrationId(currencyId = params.currencyId) } returns tonId.integrationId
|
||||
every { multiNetworkStatusSupplier(multiParams) } returns multiFlow
|
||||
|
||||
val actual = producer.produceWithFallback()
|
||||
val producerFlow = producer.produceWithFallback()
|
||||
|
||||
verify { multiNetworkStatusSupplier(multiParams) }
|
||||
// Act (first emit)
|
||||
val actual1 = getEmittedValues(flow = producerFlow)
|
||||
|
||||
val values1 = getEmittedValues(flow = actual)
|
||||
// Assert (first emit)
|
||||
val fallbackStatus = YieldBalance.Error(stakingId = tonId.copy(address = "0x1"))
|
||||
|
||||
coVerify(inverse = true) { stakingIdFactory.create(any(), any(), any()) }
|
||||
|
||||
Truth.assertThat(values1.size).isEqualTo(1)
|
||||
val fallbackStatus = YieldBalance.Error(integrationId = tonId.integrationId, address = null)
|
||||
Truth.assertThat(values1).isEqualTo(listOf(fallbackStatus))
|
||||
|
||||
coEvery { stakingIdFactory.create(params.userWalletId, params.currencyId, params.network) } returns tonId
|
||||
Truth.assertThat(actual1).hasSize(1)
|
||||
Truth.assertThat(actual1).containsExactly(fallbackStatus)
|
||||
|
||||
// Act (second emit)
|
||||
innerFlow.emit(value = true)
|
||||
val actual2 = getEmittedValues(flow = producerFlow)
|
||||
|
||||
val values2 = getEmittedValues(flow = actual)
|
||||
Truth.assertThat(actual2).hasSize(1)
|
||||
Truth.assertThat(actual2).containsExactly(balance)
|
||||
|
||||
coVerify { stakingIdFactory.create(params.userWalletId, params.currencyId, params.network) }
|
||||
|
||||
Truth.assertThat(values2.size).isEqualTo(1)
|
||||
Truth.assertThat(values2).isEqualTo(listOf(balance))
|
||||
verify(exactly = 1) { multiNetworkStatusSupplier(multiParams) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test if flow doesn't contain network from params`() = runTest {
|
||||
fun `flow doesn't contain network from params`() = runTest {
|
||||
// Arrange
|
||||
val balance = MockYieldBalanceWrapperDTOFactory.createWithBalance(solanaId).toDomain()
|
||||
|
||||
val yieldBalancesFlow = flowOf(setOf(balance))
|
||||
val multiFlow = flowOf(setOf(balance))
|
||||
|
||||
val multiParams = MultiYieldBalanceProducer.Params(userWalletId = params.userWalletId)
|
||||
every { multiNetworkStatusSupplier(multiParams) } returns yieldBalancesFlow
|
||||
coEvery { stakingIdFactory.create(params.userWalletId, params.currencyId, params.network) } returns tonId
|
||||
every { multiNetworkStatusSupplier(multiParams) } returns multiFlow
|
||||
|
||||
val actual = producer.produce()
|
||||
val producerFlow = producer.produce()
|
||||
|
||||
verify { multiNetworkStatusSupplier(multiParams) }
|
||||
// Act
|
||||
val actual = getEmittedValues(flow = producerFlow)
|
||||
|
||||
val values = getEmittedValues(flow = actual)
|
||||
// Assert
|
||||
Truth.assertThat(actual).isEmpty()
|
||||
|
||||
coVerify { stakingIdFactory.create(params.userWalletId, params.currencyId, params.network) }
|
||||
|
||||
val expected = YieldBalance.Unsupported
|
||||
Truth.assertThat(values.first()).isEqualTo(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test if wallet manager facade returns null`() = runTest {
|
||||
val balance = MockYieldBalanceWrapperDTOFactory.createWithBalance(tonId).toDomain()
|
||||
|
||||
val yieldBalancesFlow = flowOf(setOf(balance))
|
||||
|
||||
val multiParams = MultiYieldBalanceProducer.Params(userWalletId = params.userWalletId)
|
||||
every { multiNetworkStatusSupplier(multiParams) } returns yieldBalancesFlow
|
||||
coEvery { stakingIdFactory.create(params.userWalletId, params.currencyId, params.network) } returns null
|
||||
|
||||
val actual = producer.produce()
|
||||
|
||||
verify { multiNetworkStatusSupplier(multiParams) }
|
||||
|
||||
val values = getEmittedValues(flow = actual)
|
||||
|
||||
coVerify { stakingIdFactory.create(params.userWalletId, params.currencyId, params.network) }
|
||||
|
||||
val expected = YieldBalance.Unsupported
|
||||
Truth.assertThat(values.first()).isEqualTo(expected)
|
||||
verify(exactly = 1) { multiNetworkStatusSupplier(multiParams) }
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
||||
val mocks = MockCryptoCurrencyFactory()
|
||||
|
||||
val ton = mocks.createCoin(Blockchain.TON)
|
||||
|
||||
val tonId = MockYieldBalanceWrapperDTOFactory.defaultStakingId
|
||||
val solanaId = StakingID(
|
||||
integrationId = "solana-sol-native-multivalidator-staking",
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ import com.tangem.common.test.datastore.MockStateDataStore
|
|||
import com.tangem.common.test.utils.getEmittedValues
|
||||
import com.tangem.data.staking.toDomain
|
||||
import com.tangem.datasource.local.datastore.RuntimeSharedStore
|
||||
import com.tangem.domain.models.staking.YieldBalance
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.staking.model.stakekit.YieldBalance
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ import com.tangem.common.test.data.staking.MockYieldBalanceWrapperDTOFactory
|
|||
import com.tangem.common.test.datastore.MockStateDataStore
|
||||
import com.tangem.data.staking.toDomain
|
||||
import com.tangem.datasource.local.datastore.RuntimeSharedStore
|
||||
import com.tangem.domain.models.staking.YieldBalance
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.staking.model.stakekit.YieldBalance
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
|
|
|
|||
|
|
@ -7,9 +7,9 @@ import com.tangem.data.staking.toDomain
|
|||
import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO
|
||||
import com.tangem.datasource.local.datastore.RuntimeSharedStore
|
||||
import com.tangem.domain.models.StatusSource
|
||||
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.staking.model.StakingID
|
||||
import com.tangem.domain.staking.model.stakekit.YieldBalance
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.test.runTest
|
||||
|
|
@ -129,12 +129,7 @@ internal class YieldsBalancesStoreUpdateMethodsTest {
|
|||
store.storeError(userWalletId = userWalletId, stakingIds = setOf(stakingId))
|
||||
|
||||
val runtimeExpected = mapOf(
|
||||
userWalletId to setOf(
|
||||
YieldBalance.Error(
|
||||
integrationId = stakingId.integrationId,
|
||||
address = stakingId.address,
|
||||
),
|
||||
),
|
||||
userWalletId to setOf(YieldBalance.Error(stakingId)),
|
||||
)
|
||||
|
||||
Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected)
|
||||
|
|
|
|||
|
|
@ -1,188 +0,0 @@
|
|||
package com.tangem.data.staking.utils
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchainsdk.utils.toCoinId
|
||||
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
|
||||
import com.tangem.common.test.utils.ProvideTestModels
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.staking.model.StakingID
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import io.mockk.clearMocks
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Nested
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class StakingIdFactoryTest {
|
||||
|
||||
private val walletManagersFacade: WalletManagersFacade = mockk()
|
||||
private val factory = StakingIdFactory(walletManagersFacade = walletManagersFacade)
|
||||
|
||||
@BeforeEach
|
||||
fun resetMocks() {
|
||||
clearMocks(walletManagersFacade)
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class CreateIntegrationId {
|
||||
|
||||
@ParameterizedTest
|
||||
@ProvideTestModels
|
||||
fun createIntegrationId(model: CreateIntegrationIdModel) {
|
||||
// Act
|
||||
val actual = factory.createIntegrationId(currencyId = model.currencyId)
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).isEqualTo(model.expected)
|
||||
}
|
||||
|
||||
private fun provideTestModels() = listOf(
|
||||
CreateIntegrationIdModel(
|
||||
currencyId = createCurrencyId(blockchain = Blockchain.TON),
|
||||
expected = "ton-ton-chorus-one-pools-staking",
|
||||
),
|
||||
CreateIntegrationIdModel(
|
||||
currencyId = createCurrencyId(blockchain = Blockchain.Solana),
|
||||
expected = "solana-sol-native-multivalidator-staking",
|
||||
),
|
||||
CreateIntegrationIdModel(
|
||||
currencyId = createCurrencyId(blockchain = Blockchain.Cosmos),
|
||||
expected = "cosmos-atom-native-staking",
|
||||
),
|
||||
CreateIntegrationIdModel(
|
||||
currencyId = createCurrencyId(blockchain = Blockchain.Tron),
|
||||
expected = "tron-trx-native-staking",
|
||||
),
|
||||
CreateIntegrationIdModel(
|
||||
currencyId = CryptoCurrency.ID.fromValue(value = "coin⟨ETH⟩polygon-ecosystem-token⚓"),
|
||||
expected = "ethereum-matic-native-staking",
|
||||
),
|
||||
CreateIntegrationIdModel(
|
||||
currencyId = createCurrencyId(blockchain = Blockchain.BSC),
|
||||
expected = "bsc-bnb-native-staking",
|
||||
),
|
||||
CreateIntegrationIdModel(
|
||||
currencyId = createCurrencyId(blockchain = Blockchain.Cardano),
|
||||
expected = "cardano-ada-native-staking",
|
||||
),
|
||||
CreateIntegrationIdModel(
|
||||
currencyId = createCurrencyId(blockchain = Blockchain.Bitcoin),
|
||||
expected = null,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
data class CreateIntegrationIdModel(val currencyId: CryptoCurrency.ID, val expected: String?)
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class Create {
|
||||
|
||||
private val defaultAddress = "address"
|
||||
|
||||
@Test
|
||||
fun `create returns null if address is null`() = runTest {
|
||||
// Arrange
|
||||
val userWalletId = UserWalletId(stringValue = "011")
|
||||
val currency = MockCryptoCurrencyFactory().createCoin(Blockchain.TON)
|
||||
|
||||
coEvery {
|
||||
walletManagersFacade.getDefaultAddress(userWalletId = userWalletId, network = currency.network)
|
||||
} returns null
|
||||
|
||||
// Act
|
||||
val actual = factory.create(
|
||||
userWalletId = userWalletId,
|
||||
currencyId = currency.id,
|
||||
network = currency.network,
|
||||
)
|
||||
|
||||
// Assert
|
||||
val expected = null
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
|
||||
coVerify(exactly = 1) {
|
||||
walletManagersFacade.getDefaultAddress(userWalletId = userWalletId, network = currency.network)
|
||||
}
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ProvideTestModels
|
||||
fun create(model: CreateModel) = runTest {
|
||||
// Arrange
|
||||
val userWalletId = UserWalletId(stringValue = "011")
|
||||
val network = MockCryptoCurrencyFactory().createCoin(Blockchain.TON).network
|
||||
|
||||
coEvery {
|
||||
walletManagersFacade.getDefaultAddress(userWalletId = userWalletId, network = network)
|
||||
} returns defaultAddress
|
||||
|
||||
// Act
|
||||
val actual = factory.create(userWalletId = userWalletId, currencyId = model.currencyId, network = network)
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).isEqualTo(model.expected)
|
||||
|
||||
coVerify(exactly = 1) {
|
||||
walletManagersFacade.getDefaultAddress(userWalletId = userWalletId, network = network)
|
||||
}
|
||||
}
|
||||
|
||||
private fun provideTestModels() = listOf(
|
||||
CreateModel(
|
||||
currencyId = createCurrencyId(blockchain = Blockchain.TON),
|
||||
expected = createStakingId(integrationId = "ton-ton-chorus-one-pools-staking"),
|
||||
),
|
||||
CreateModel(
|
||||
currencyId = createCurrencyId(blockchain = Blockchain.Solana),
|
||||
expected = createStakingId(integrationId = "solana-sol-native-multivalidator-staking"),
|
||||
),
|
||||
CreateModel(
|
||||
currencyId = createCurrencyId(blockchain = Blockchain.Cosmos),
|
||||
expected = createStakingId(integrationId = "cosmos-atom-native-staking"),
|
||||
),
|
||||
CreateModel(
|
||||
currencyId = createCurrencyId(blockchain = Blockchain.Tron),
|
||||
expected = createStakingId(integrationId = "tron-trx-native-staking"),
|
||||
),
|
||||
CreateModel(
|
||||
currencyId = CryptoCurrency.ID.fromValue(value = "coin⟨ETH⟩polygon-ecosystem-token⚓"),
|
||||
expected = createStakingId(integrationId = "ethereum-matic-native-staking"),
|
||||
),
|
||||
CreateModel(
|
||||
currencyId = createCurrencyId(blockchain = Blockchain.BSC),
|
||||
expected = createStakingId(integrationId = "bsc-bnb-native-staking"),
|
||||
),
|
||||
CreateModel(
|
||||
currencyId = createCurrencyId(blockchain = Blockchain.Cardano),
|
||||
expected = createStakingId(integrationId = "cardano-ada-native-staking"),
|
||||
),
|
||||
CreateModel(
|
||||
currencyId = createCurrencyId(blockchain = Blockchain.Bitcoin),
|
||||
expected = null,
|
||||
),
|
||||
)
|
||||
|
||||
private fun createStakingId(integrationId: String): StakingID {
|
||||
return StakingID(integrationId = integrationId, address = defaultAddress)
|
||||
}
|
||||
}
|
||||
|
||||
data class CreateModel(val currencyId: CryptoCurrency.ID, val expected: StakingID?)
|
||||
|
||||
private fun createCurrencyId(blockchain: Blockchain): CryptoCurrency.ID {
|
||||
return CryptoCurrency.ID.fromValue(value = "coin⟨${blockchain.id}⟩${blockchain.toCoinId()}⚓")
|
||||
}
|
||||
}
|
||||
|
|
@ -22,6 +22,7 @@ import com.tangem.domain.express.models.ExpressProvider
|
|||
import com.tangem.domain.express.models.ExpressProviderType
|
||||
import com.tangem.domain.express.models.ExpressRateType
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.network.NetworkStatus
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.quotes.single.SingleQuoteStatusFetcher
|
||||
|
|
@ -29,7 +30,6 @@ import com.tangem.domain.quotes.single.SingleQuoteStatusProducer
|
|||
import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier
|
||||
import com.tangem.domain.swap.SwapRepositoryV2
|
||||
import com.tangem.domain.swap.models.*
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.tokens.utils.CurrencyStatusProxyCreator
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import com.tangem.datasource.local.preferences.AppPreferencesStore
|
|||
import com.tangem.datasource.local.preferences.PreferencesKeys
|
||||
import com.tangem.datasource.local.preferences.utils.getObjectList
|
||||
import com.tangem.datasource.local.preferences.utils.getObjectListSync
|
||||
import com.tangem.datasource.local.preferences.utils.getObjectMapSync
|
||||
import com.tangem.datasource.local.preferences.utils.getObjectMap
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
|
@ -24,9 +24,8 @@ import com.tangem.domain.swap.models.SwapTransactionModel
|
|||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.extensions.addOrReplace
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
internal class DefaultSwapTransactionRepository(
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
|
|
@ -95,36 +94,34 @@ internal class DefaultSwapTransactionRepository(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun getTransactions(
|
||||
override fun getTransactions(
|
||||
userWallet: UserWallet,
|
||||
cryptoCurrencyId: CryptoCurrency.ID,
|
||||
): Flow<List<SwapTransactionListModel>?> {
|
||||
return withContext(dispatchers.io) {
|
||||
val txStatuses = appPreferencesStore.getObjectMapSync<SwapStatusDTO>(
|
||||
key = PreferencesKeys.SWAP_TRANSACTIONS_STATUSES_KEY,
|
||||
)
|
||||
appPreferencesStore.getObjectList<SwapTransactionListDTO>(
|
||||
key = PreferencesKeys.SWAP_TRANSACTIONS_KEY,
|
||||
).map { savedTransactions ->
|
||||
val currencyTxs = savedTransactions
|
||||
?.filter {
|
||||
it.userWalletId == userWallet.walletId.stringValue &&
|
||||
(
|
||||
it.toCryptoCurrencyId == cryptoCurrencyId.value ||
|
||||
it.fromCryptoCurrencyId == cryptoCurrencyId.value
|
||||
)
|
||||
}
|
||||
): Flow<List<SwapTransactionListModel>?> = combine(
|
||||
flow = appPreferencesStore.getObjectList<SwapTransactionListDTO>(
|
||||
key = PreferencesKeys.SWAP_TRANSACTIONS_KEY,
|
||||
),
|
||||
flow2 = appPreferencesStore.getObjectMap<SwapStatusDTO>(
|
||||
key = PreferencesKeys.SWAP_TRANSACTIONS_STATUSES_KEY,
|
||||
),
|
||||
) { savedTransactions, txStatuses ->
|
||||
val currencyTxs = savedTransactions
|
||||
?.filter {
|
||||
it.userWalletId == userWallet.walletId.stringValue &&
|
||||
(
|
||||
it.toCryptoCurrencyId == cryptoCurrencyId.value ||
|
||||
it.fromCryptoCurrencyId == cryptoCurrencyId.value
|
||||
)
|
||||
}
|
||||
|
||||
currencyTxs?.mapNotNull {
|
||||
listConverter.convertBack(
|
||||
value = it,
|
||||
userWallet = userWallet,
|
||||
txStatuses = txStatuses,
|
||||
)
|
||||
}
|
||||
}.flowOn(dispatchers.io)
|
||||
currencyTxs?.mapNotNull {
|
||||
listConverter.convertBack(
|
||||
value = it,
|
||||
userWallet = userWallet,
|
||||
txStatuses = txStatuses,
|
||||
)
|
||||
}
|
||||
}
|
||||
}.flowOn(dispatchers.default)
|
||||
|
||||
override suspend fun removeTransaction(
|
||||
userWalletId: UserWalletId,
|
||||
|
|
@ -188,7 +185,7 @@ internal class DefaultSwapTransactionRepository(
|
|||
)
|
||||
|
||||
val updatesMap = savedMap.toMutableMap()
|
||||
updatesMap[txId] = savedStatusConverter.convertBack(
|
||||
updatesMap[txId] = savedStatusConverter.convert(
|
||||
status.copy(
|
||||
refundTokensResponse = refundTokenCurrency?.let {
|
||||
userTokensResponseFactory.createResponseToken(refundTokenCurrency)
|
||||
|
|
|
|||
|
|
@ -6,9 +6,9 @@ import com.tangem.domain.swap.models.SwapStatus
|
|||
import com.tangem.domain.swap.models.SwapStatusModel
|
||||
import com.tangem.utils.converter.TwoWayConverter
|
||||
|
||||
internal class SavedSwapStatusConverter : TwoWayConverter<SwapStatusDTO, SwapStatusModel> {
|
||||
internal class SavedSwapStatusConverter : TwoWayConverter<SwapStatusModel, SwapStatusDTO> {
|
||||
|
||||
override fun convert(value: SwapStatusDTO) = SwapStatusModel(
|
||||
override fun convertBack(value: SwapStatusDTO) = SwapStatusModel(
|
||||
providerId = value.providerId,
|
||||
status = SwapStatus.entries.firstOrNull {
|
||||
it.name.lowercase() == value.status?.name?.lowercase()
|
||||
|
|
@ -22,7 +22,7 @@ internal class SavedSwapStatusConverter : TwoWayConverter<SwapStatusDTO, SwapSta
|
|||
averageDuration = value.averageDuration,
|
||||
)
|
||||
|
||||
override fun convertBack(value: SwapStatusModel) = SwapStatusDTO(
|
||||
override fun convert(value: SwapStatusModel) = SwapStatusDTO(
|
||||
providerId = value.providerId,
|
||||
status = SavedSwapStatus.entries.firstOrNull {
|
||||
it.name.lowercase() == value.status?.name?.lowercase()
|
||||
|
|
|
|||
|
|
@ -3,8 +3,10 @@ package com.tangem.data.swap.converter.transaction
|
|||
import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory
|
||||
import com.tangem.data.swap.models.SwapStatusDTO
|
||||
import com.tangem.data.swap.models.SwapTransactionDTO
|
||||
import com.tangem.data.swap.models.SwapTxTypeDTO
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.swap.models.SwapTransactionModel
|
||||
import com.tangem.domain.swap.models.SwapTxType
|
||||
import com.tangem.utils.converter.TwoWayConverter
|
||||
|
||||
internal class SavedSwapTransactionConverter(
|
||||
|
|
@ -21,7 +23,10 @@ internal class SavedSwapTransactionConverter(
|
|||
fromCryptoAmount = value.fromCryptoAmount,
|
||||
toCryptoAmount = value.toCryptoAmount,
|
||||
provider = value.provider,
|
||||
status = value.status,
|
||||
status = value.status?.let(statusConverter::convert),
|
||||
swapTxType = SwapTxTypeDTO.entries.firstOrNull {
|
||||
it.name.lowercase() == value.swapTxType?.name?.lowercase()
|
||||
},
|
||||
)
|
||||
|
||||
override fun convertBack(value: SwapTransactionDTO) = SwapTransactionModel(
|
||||
|
|
@ -30,7 +35,10 @@ internal class SavedSwapTransactionConverter(
|
|||
fromCryptoAmount = value.fromCryptoAmount,
|
||||
toCryptoAmount = value.toCryptoAmount,
|
||||
provider = value.provider,
|
||||
status = value.status,
|
||||
status = value.status?.let(statusConverter::convertBack),
|
||||
swapTxType = SwapTxType.entries.firstOrNull {
|
||||
it.name.lowercase() == value.swapTxType?.name?.lowercase()
|
||||
},
|
||||
)
|
||||
|
||||
fun convertBack(
|
||||
|
|
@ -53,7 +61,10 @@ internal class SavedSwapTransactionConverter(
|
|||
fromCryptoAmount = value.fromCryptoAmount,
|
||||
toCryptoAmount = value.toCryptoAmount,
|
||||
provider = value.provider,
|
||||
status = statusWithRefundCurrency?.let(statusConverter::convert),
|
||||
status = statusWithRefundCurrency?.let(statusConverter::convertBack),
|
||||
swapTxType = SwapTxType.entries.firstOrNull {
|
||||
it.name.lowercase() == value.swapTxType?.name?.lowercase()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -14,7 +14,6 @@ import com.tangem.datasource.local.preferences.AppPreferencesStore
|
|||
import com.tangem.domain.express.ExpressRepository
|
||||
import com.tangem.domain.quotes.single.SingleQuoteStatusFetcher
|
||||
import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier
|
||||
import com.tangem.domain.staking.repositories.StakingRepository
|
||||
import com.tangem.domain.swap.SwapErrorResolver
|
||||
import com.tangem.domain.swap.SwapRepositoryV2
|
||||
import com.tangem.domain.swap.SwapTransactionRepository
|
||||
|
|
@ -49,7 +48,6 @@ internal object SwapDataModule {
|
|||
dataSignatureVerifier: DataSignatureVerifier,
|
||||
singleQuoteStatusSupplier: SingleQuoteStatusSupplier,
|
||||
singleQuoteStatusFetcher: SingleQuoteStatusFetcher,
|
||||
stakingRepository: StakingRepository,
|
||||
@NetworkMoshi moshi: Moshi,
|
||||
): SwapRepositoryV2 {
|
||||
return DefaultSwapRepositoryV2(
|
||||
|
|
@ -61,7 +59,7 @@ internal object SwapDataModule {
|
|||
moshi = moshi,
|
||||
singleQuoteStatusSupplier = singleQuoteStatusSupplier,
|
||||
singleQuoteStatusFetcher = singleQuoteStatusFetcher,
|
||||
currencyStatusProxyCreator = CurrencyStatusProxyCreator(stakingRepository),
|
||||
currencyStatusProxyCreator = CurrencyStatusProxyCreator(),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import com.squareup.moshi.Json
|
|||
import com.squareup.moshi.JsonClass
|
||||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
||||
import com.tangem.domain.express.models.ExpressProvider
|
||||
import com.tangem.domain.swap.models.SwapStatusModel
|
||||
import java.math.BigDecimal
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
|
|
@ -36,5 +35,7 @@ internal data class SwapTransactionDTO(
|
|||
@Json(name = "provider")
|
||||
val provider: ExpressProvider,
|
||||
@Json(name = "status")
|
||||
val status: SwapStatusModel? = null,
|
||||
val status: SwapStatusDTO? = null,
|
||||
@Json(name = "swapTxType")
|
||||
val swapTxType: SwapTxTypeDTO? = SwapTxTypeDTO.Swap,
|
||||
)
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package com.tangem.data.swap.models
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = false)
|
||||
internal enum class SwapTxTypeDTO {
|
||||
@Json(name = "Swap")
|
||||
Swap,
|
||||
|
||||
@Json(name = "SendWithSwap")
|
||||
SendWithSwap,
|
||||
}
|
||||
|
|
@ -18,7 +18,6 @@ import com.tangem.domain.core.utils.catchOn
|
|||
import com.tangem.domain.demo.models.DemoConfig
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.isMultiCurrency
|
||||
import com.tangem.domain.models.wallet.requireColdWallet
|
||||
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesFetcher
|
||||
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesFetcher.Params
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
|
|
@ -68,7 +67,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcher(
|
|||
}
|
||||
},
|
||||
onError = {
|
||||
handleFetchTokensError(error = it, userWallet = userWallet.requireColdWallet()) // TODO 11142
|
||||
handleFetchTokensError(error = it, userWallet = userWallet)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory
|
|||
import com.tangem.datasource.local.token.UserTokensResponseStore
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.requireColdWallet
|
||||
import com.tangem.domain.models.wallet.isMultiCurrency
|
||||
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.assisted.Assisted
|
||||
|
|
@ -35,7 +35,7 @@ internal class DefaultMultiWalletCryptoCurrenciesProducer @AssistedInject constr
|
|||
get() = emptySet()
|
||||
|
||||
override fun produce(): Flow<Set<CryptoCurrency>> {
|
||||
val userWallet = userWalletsStore.getSyncStrict(key = params.userWalletId).requireColdWallet() // TODO [REDACTED_TASK_KEY]
|
||||
val userWallet = userWalletsStore.getSyncStrict(key = params.userWalletId)
|
||||
|
||||
if (!userWallet.isMultiCurrency) {
|
||||
error("${this::class.simpleName} supports only multi-currency wallet")
|
||||
|
|
|
|||
|
|
@ -22,9 +22,9 @@ import com.tangem.domain.card.common.util.cardTypesResolver
|
|||
import com.tangem.domain.core.error.DataError
|
||||
import com.tangem.domain.demo.models.DemoConfig
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.*
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.tokens.model.FeePaidCurrency
|
||||
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
|
|
|
|||
|
|
@ -7,11 +7,11 @@ import com.tangem.blockchain.common.ReserveAmountProvider
|
|||
import com.tangem.blockchain.common.UtxoAmountLimitProvider
|
||||
import com.tangem.data.tokens.converters.UtxoConverter
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.staking.YieldBalance
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.staking.model.stakekit.YieldBalance
|
||||
import com.tangem.domain.staking.utils.getTotalStakingBalance
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.tokens.model.CurrencyAmount
|
||||
import com.tangem.domain.tokens.model.blockchains.UtxoAmountLimit
|
||||
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ dependencies {
|
|||
/** Project - Domain */
|
||||
implementation(projects.domain.visa)
|
||||
implementation(projects.domain.card)
|
||||
implementation(projects.domain.wallets)
|
||||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.wallets.models)
|
||||
implementation(projects.domain.appCurrency.models)
|
||||
|
|
|
|||
|
|
@ -3,12 +3,11 @@ package com.tangem.data.visa.utils
|
|||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.data.common.currency.CryptoCurrencyFactory
|
||||
import com.tangem.data.common.network.NetworkFactory
|
||||
import com.tangem.domain.card.common.util.derivationStyleProvider
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.network.NetworkAddress
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.requireColdWallet
|
||||
import com.tangem.domain.visa.model.VisaCurrency
|
||||
import com.tangem.domain.wallets.derivations.derivationStyleProvider
|
||||
import com.tangem.lib.visa.model.VisaContractInfo
|
||||
import org.joda.time.DateTime
|
||||
import org.joda.time.DateTimeZone
|
||||
|
|
@ -34,7 +33,7 @@ internal class VisaCurrencyFactory @Inject constructor(
|
|||
val currencyNetwork = networkFactory.create(
|
||||
blockchain = Blockchain.Polygon,
|
||||
extraDerivationPath = null,
|
||||
derivationStyleProvider = userWallet.requireColdWallet().scanResponse.derivationStyleProvider,
|
||||
derivationStyleProvider = userWallet.derivationStyleProvider,
|
||||
canHandleTokens = true,
|
||||
) ?: error("Unable to create network for Visa currency")
|
||||
|
||||
|
|
|
|||
|
|
@ -7,10 +7,10 @@ import com.tangem.blockchainsdk.BlockchainSDKFactory
|
|||
import com.tangem.crypto.hdWallet.DerivationPath
|
||||
import com.tangem.data.walletmanager.extensions.makePublicKey
|
||||
import com.tangem.data.walletmanager.extensions.makeWalletManagerForApp
|
||||
import com.tangem.domain.card.DerivationStyleProvider
|
||||
import com.tangem.domain.card.common.util.derivationStyleProvider
|
||||
import com.tangem.domain.wallets.derivations.DerivationStyleProvider
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.wallets.derivations.derivationStyleProvider
|
||||
import timber.log.Timber
|
||||
|
||||
internal class WalletManagerFactory(
|
||||
|
|
|
|||
|
|
@ -11,9 +11,14 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.data.common)
|
||||
|
||||
/** Tangem libraries */
|
||||
implementation(tangemDeps.blockchain) // android-library
|
||||
implementation(tangemDeps.blockchain)
|
||||
implementation(tangemDeps.card.core)
|
||||
implementation(tangemDeps.hot.core)
|
||||
implementation(projects.libs.tangemSdkApi)
|
||||
implementation(projects.libs.blockchainSdk)
|
||||
|
||||
/** Core */
|
||||
implementation(projects.core.datasource)
|
||||
|
|
@ -21,6 +26,7 @@ dependencies {
|
|||
|
||||
/** Domain */
|
||||
implementation(projects.domain.wallets)
|
||||
implementation(projects.domain.card)
|
||||
api(projects.domain.models)
|
||||
|
||||
/** Domain models */
|
||||
|
|
@ -29,15 +35,17 @@ dependencies {
|
|||
|
||||
/** DI */
|
||||
implementation(deps.hilt.android)
|
||||
implementation(project(":domain:legacy"))
|
||||
kapt(deps.hilt.kapt)
|
||||
|
||||
/** Other deps */
|
||||
implementation(deps.androidx.datastore)
|
||||
implementation(deps.arrow.core)
|
||||
implementation(deps.kotlin.coroutines)
|
||||
implementation(deps.timber)
|
||||
|
||||
/** tests */
|
||||
testImplementation(projects.domain.models)
|
||||
testImplementation(projects.common.test)
|
||||
testImplementation(deps.test.junit)
|
||||
testImplementation(deps.test.coroutine)
|
||||
testImplementation(deps.test.truth)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,153 @@
|
|||
package com.tangem.data.wallets.cold
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchainsdk.utils.fromNetworkId
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.card.EllipticCurve
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.common.extensions.ByteArrayKey
|
||||
import com.tangem.common.extensions.toMapKey
|
||||
import com.tangem.crypto.hdWallet.DerivationPath
|
||||
import com.tangem.data.common.network.NetworkFactory
|
||||
import com.tangem.data.wallets.derivations.Derivations
|
||||
import com.tangem.data.wallets.derivations.MissedDerivationsFinder
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.wallets.derivations.ColdMapDerivationsRepository
|
||||
import com.tangem.domain.wallets.usecase.BackendId
|
||||
import com.tangem.operations.derivation.ExtendedPublicKeysMap
|
||||
import com.tangem.sdk.api.TangemSdkManager
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
private typealias DerivedKeys = Map<ByteArrayKey, ExtendedPublicKeysMap>
|
||||
|
||||
internal class DefaultColdMapDerivationsRepository @Inject constructor(
|
||||
private val tangemSdkManager: TangemSdkManager,
|
||||
private val networkFactory: NetworkFactory,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : ColdMapDerivationsRepository {
|
||||
|
||||
override suspend fun derivePublicKeys(
|
||||
userWallet: UserWallet.Cold,
|
||||
currencies: List<CryptoCurrency>,
|
||||
): UserWallet.Cold = withContext(dispatchers.io) {
|
||||
derivePublicKeysByNetworks(userWallet = userWallet, networks = currencies.map(CryptoCurrency::network))
|
||||
}
|
||||
|
||||
override suspend fun derivePublicKeysByNetworkIds(
|
||||
userWallet: UserWallet.Cold,
|
||||
networkIds: List<Network.RawID>,
|
||||
): UserWallet.Cold = withContext(dispatchers.io) {
|
||||
derivePublicKeysByNetworks(
|
||||
userWallet = userWallet,
|
||||
networks = networkIds.mapNotNull {
|
||||
networkFactory.create(
|
||||
blockchain = Blockchain.fromNetworkId(it.value) ?: return@mapNotNull null,
|
||||
extraDerivationPath = null,
|
||||
userWallet = userWallet,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun derivePublicKeysByNetworks(
|
||||
userWallet: UserWallet.Cold,
|
||||
networks: List<Network>,
|
||||
): UserWallet.Cold = withContext(dispatchers.io) {
|
||||
if (!userWallet.scanResponse.card.settings.isHDWalletAllowed) {
|
||||
Timber.d("Nothing to derive")
|
||||
return@withContext userWallet
|
||||
}
|
||||
|
||||
val derivations = MissedDerivationsFinder(userWallet)
|
||||
.findByNetworks(networks)
|
||||
.ifEmpty {
|
||||
Timber.d("Nothing to derive")
|
||||
return@withContext userWallet
|
||||
}
|
||||
|
||||
return@withContext derivePublicKeys(userWallet = userWallet, derivations = derivations).first
|
||||
}
|
||||
|
||||
override suspend fun derivePublicKeys(
|
||||
userWallet: UserWallet.Cold,
|
||||
derivations: Map<ByteArrayKey, List<DerivationPath>>,
|
||||
): Pair<UserWallet.Cold, Map<ByteArrayKey, ExtendedPublicKeysMap>> = withContext(dispatchers.io) {
|
||||
// todo replace it in task [REDACTED_JIRA]
|
||||
val preflightReadFilter = UserWalletIdPreflightReadFilter(userWallet.walletId)
|
||||
val result = tangemSdkManager.derivePublicKeys(
|
||||
cardId = null,
|
||||
derivations = derivations,
|
||||
preflightReadFilter = preflightReadFilter,
|
||||
)
|
||||
|
||||
when (result) {
|
||||
is CompletionResult.Success -> {
|
||||
userWallet.updateDerivedKeys(result.data.entries).also {
|
||||
validateDerivations(scanResponse = it.scanResponse, derivations = derivations)
|
||||
} to result.data.entries
|
||||
}
|
||||
is CompletionResult.Failure -> {
|
||||
throw result.error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun hasMissedDerivations(
|
||||
userWallet: UserWallet.Cold,
|
||||
networksWithDerivationPath: Map<BackendId, String?>,
|
||||
): Boolean = withContext(dispatchers.io) {
|
||||
val derivations =
|
||||
MissedDerivationsFinder(userWallet)
|
||||
.findByNetworks(
|
||||
networksWithDerivationPath.mapNotNull { (backendId, extraDerivationPath) ->
|
||||
networkFactory.create(
|
||||
blockchain = Blockchain.fromNetworkId(backendId) ?: return@mapNotNull null,
|
||||
extraDerivationPath = extraDerivationPath,
|
||||
userWallet = userWallet,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
derivations.isNotEmpty()
|
||||
}
|
||||
|
||||
/**
|
||||
* It throws an exception if any of the provided derivations are invalid
|
||||
* Validation for NonHardened moved to application layer, to avoid fails when derive multiple paths
|
||||
* It needs to be called after success [derivePublicKeys] or in same flows
|
||||
*/
|
||||
private fun validateDerivations(scanResponse: ScanResponse, derivations: Derivations) {
|
||||
derivations.entries.forEach { derivationForKey ->
|
||||
val wallet = scanResponse.card.wallets.firstOrNull { it.publicKey.toMapKey() == derivationForKey.key }
|
||||
if (wallet == null) return@forEach
|
||||
val hasHardenedNodes = derivationForKey.value.any { path -> path.nodes.any { node -> !node.isHardened } }
|
||||
if (wallet.curve == EllipticCurve.Ed25519Slip0010 && hasHardenedNodes) {
|
||||
throw TangemSdkError.NonHardenedDerivationNotSupported()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun UserWallet.Cold.updateDerivedKeys(keys: DerivedKeys): UserWallet.Cold {
|
||||
return copy(
|
||||
scanResponse = scanResponse.copy(
|
||||
derivedKeys = getUpdatedDerivedKeys(oldKeys = scanResponse.derivedKeys, newKeys = keys),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun getUpdatedDerivedKeys(oldKeys: DerivedKeys, newKeys: DerivedKeys): DerivedKeys {
|
||||
return (oldKeys.keys + newKeys.keys).toSet()
|
||||
.associateWith { walletKey ->
|
||||
val oldDerivations = ExtendedPublicKeysMap(oldKeys[walletKey] ?: emptyMap())
|
||||
val newDerivations = newKeys[walletKey] ?: ExtendedPublicKeysMap(emptyMap())
|
||||
|
||||
ExtendedPublicKeysMap(oldDerivations + newDerivations)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package com.tangem.data.wallets.cold
|
||||
|
||||
import com.tangem.common.card.Card
|
||||
import com.tangem.common.core.SessionEnvironment
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.domain.models.scan.CardDTO
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
|
||||
import com.tangem.operations.preflightread.PreflightReadFilter
|
||||
|
||||
/**
|
||||
* [PreflightReadFilter] for checking if card has expected user wallet id
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class UserWalletIdPreflightReadFilter(private val expectedUserWalletId: UserWalletId) : PreflightReadFilter {
|
||||
|
||||
override fun onCardRead(card: Card, environment: SessionEnvironment) = Unit
|
||||
|
||||
override fun onFullCardRead(card: Card, environment: SessionEnvironment) {
|
||||
val actualUserWalletId = UserWalletIdBuilder.card(card = CardDTO(card)).build() ?: return
|
||||
|
||||
if (expectedUserWalletId != actualUserWalletId) throw TangemSdkError.WalletNotFound()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
package com.tangem.data.wallets.derivations
|
||||
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.extensions.ByteArrayKey
|
||||
import com.tangem.common.map
|
||||
import com.tangem.crypto.hdWallet.DerivationPath
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.wallets.derivations.ColdMapDerivationsRepository
|
||||
import com.tangem.domain.wallets.derivations.DerivationsRepository
|
||||
import com.tangem.domain.wallets.derivations.HotMapDerivationsRepository
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.wallets.usecase.BackendId
|
||||
import com.tangem.operations.derivation.ExtendedPublicKeysMap
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class DefaultDerivationsRepository @Inject constructor(
|
||||
private val userWalletsStore: UserWalletsStore,
|
||||
private val hotDerivationsRepository: HotMapDerivationsRepository,
|
||||
private val coldDerivationsRepository: ColdMapDerivationsRepository,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : DerivationsRepository {
|
||||
|
||||
override suspend fun derivePublicKeys(userWalletId: UserWalletId, currencies: List<CryptoCurrency>) {
|
||||
derivePublicKeysByNetworks(userWalletId = userWalletId, networks = currencies.map(CryptoCurrency::network))
|
||||
}
|
||||
|
||||
override suspend fun derivePublicKeysByNetworkIds(userWalletId: UserWalletId, networkIds: List<Network.RawID>) {
|
||||
val userWallet = userWalletsStore.getSyncStrict(userWalletId)
|
||||
when (userWallet) {
|
||||
is UserWallet.Cold -> coldDerivationsRepository.derivePublicKeysByNetworkIds(userWallet, networkIds)
|
||||
is UserWallet.Hot -> hotDerivationsRepository.derivePublicKeysByNetworkIds(userWallet, networkIds)
|
||||
}.also {
|
||||
userWallet.update(it)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun derivePublicKeysByNetworks(userWalletId: UserWalletId, networks: List<Network>) {
|
||||
val userWallet = userWalletsStore.getSyncStrict(userWalletId)
|
||||
when (userWallet) {
|
||||
is UserWallet.Cold -> coldDerivationsRepository.derivePublicKeysByNetworks(userWallet, networks)
|
||||
is UserWallet.Hot -> hotDerivationsRepository.derivePublicKeysByNetworks(userWallet, networks)
|
||||
}.also {
|
||||
userWallet.update(it)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun derivePublicKeys(
|
||||
userWalletId: UserWalletId,
|
||||
derivations: Map<ByteArrayKey, List<DerivationPath>>,
|
||||
): Map<ByteArrayKey, ExtendedPublicKeysMap> {
|
||||
val userWallet = userWalletsStore.getSyncStrict(userWalletId)
|
||||
return when (userWallet) {
|
||||
is UserWallet.Cold -> coldDerivationsRepository.derivePublicKeys(userWallet, derivations)
|
||||
is UserWallet.Hot -> hotDerivationsRepository.derivePublicKeys(userWallet, derivations)
|
||||
}.let {
|
||||
userWallet.update(it.first)
|
||||
it.second
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun hasMissedDerivations(
|
||||
userWalletId: UserWalletId,
|
||||
networksWithDerivationPath: Map<BackendId, String?>,
|
||||
): Boolean {
|
||||
return when (val userWallet = userWalletsStore.getSyncStrict(userWalletId)) {
|
||||
is UserWallet.Cold -> coldDerivationsRepository.hasMissedDerivations(userWallet, networksWithDerivationPath)
|
||||
is UserWallet.Hot -> hotDerivationsRepository.hasMissedDerivations(userWallet, networksWithDerivationPath)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun UserWallet.update(newUserWallet: UserWallet) = withContext(dispatchers.io) {
|
||||
check(this@update.walletId == newUserWallet.walletId) {
|
||||
"Cannot update UserWallet with different walletId: ${newUserWallet.walletId}"
|
||||
}
|
||||
|
||||
if (this@update == newUserWallet) {
|
||||
return@withContext // No update needed
|
||||
}
|
||||
|
||||
val updateResult = userWalletsStore.update(
|
||||
userWalletId = newUserWallet.walletId,
|
||||
update = { userWalletToUpdate -> newUserWallet },
|
||||
)
|
||||
|
||||
when (updateResult) {
|
||||
is CompletionResult.Failure -> throw updateResult.error
|
||||
is CompletionResult.Success -> updateResult.data
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,144 @@
|
|||
package com.tangem.data.wallets.derivations
|
||||
|
||||
import com.tangem.blockchain.blockchains.cardano.CardanoUtils
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchainsdk.utils.toBlockchain
|
||||
import com.tangem.common.card.EllipticCurve
|
||||
import com.tangem.common.extensions.ByteArrayKey
|
||||
import com.tangem.common.extensions.toMapKey
|
||||
import com.tangem.crypto.hdWallet.DerivationPath
|
||||
import com.tangem.domain.card.configs.CardConfig
|
||||
import com.tangem.domain.card.configs.Wallet2CardConfig
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.scan.KeyWalletPublicKey
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.wallets.derivations.derivationStyleProvider
|
||||
import com.tangem.operations.derivation.ExtendedPublicKeysMap
|
||||
import kotlin.collections.forEach
|
||||
|
||||
private typealias DerivationData = Pair<ByteArrayKey, List<DerivationPath>>
|
||||
internal typealias Derivations = Map<ByteArrayKey, List<DerivationPath>>
|
||||
|
||||
/**
|
||||
* Finder of missed derivations
|
||||
*
|
||||
* @property userWallet User wallet to find derivations for
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class MissedDerivationsFinder(private val userWallet: UserWallet) {
|
||||
|
||||
/** Find missed derivations for given currencies [currencies] */
|
||||
fun find(currencies: List<CryptoCurrency>): Derivations {
|
||||
return currencies.map { it.network }.let(::findByNetworks)
|
||||
}
|
||||
|
||||
fun findByNetworks(networks: List<Network>): Derivations {
|
||||
return buildMap<ByteArrayKey, MutableList<DerivationPath>> {
|
||||
networks
|
||||
.mapToNewDerivations()
|
||||
.forEach { data ->
|
||||
val current = this[data.first]
|
||||
if (current != null) {
|
||||
current.addAll(data.second)
|
||||
current.distinct()
|
||||
} else {
|
||||
this[data.first] = data.second.toMutableList()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<Network>.mapToNewDerivations(): List<DerivationData> {
|
||||
val config = when (userWallet) {
|
||||
is UserWallet.Cold -> CardConfig.createConfig(userWallet.scanResponse.card)
|
||||
is UserWallet.Hot -> Wallet2CardConfig // TODO [REDACTED_TASK_KEY] [Hot Wallet] Derivation config for hot wallet
|
||||
}
|
||||
return mapNotNull { network ->
|
||||
val blockchain = network.toBlockchain()
|
||||
val curve = config.primaryCurve(blockchain) ?: return@mapNotNull null
|
||||
|
||||
val walletPublicKey = when (userWallet) {
|
||||
is UserWallet.Cold -> {
|
||||
val wallet = userWallet.scanResponse.card.wallets.firstOrNull { it.curve == curve }
|
||||
wallet?.publicKey
|
||||
}
|
||||
is UserWallet.Hot -> {
|
||||
val wallet = userWallet.wallets?.firstOrNull { it.curve == curve }
|
||||
wallet?.publicKey
|
||||
}
|
||||
}
|
||||
|
||||
walletPublicKey?.let {
|
||||
findNewDerivations(curve = curve, publicKey = it, network = network)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun findNewDerivations(curve: EllipticCurve, publicKey: ByteArray, network: Network): DerivationData? {
|
||||
val derivationCandidates = network
|
||||
.getDerivationCandidates(curve)
|
||||
.ifEmpty { return null }
|
||||
.filterAlreadyDerivedKeys(publicKey.toMapKey())
|
||||
.ifEmpty { return null }
|
||||
|
||||
return publicKey.toMapKey() to derivationCandidates
|
||||
}
|
||||
|
||||
private fun Network.getDerivationCandidates(curve: EllipticCurve): List<DerivationPath> {
|
||||
val blockchain = this.toBlockchain()
|
||||
|
||||
return buildList {
|
||||
add(blockchain.getDerivationPath(curve = curve))
|
||||
add(blockchain.getCustomDerivationPath(curve = curve, network = this@getDerivationCandidates))
|
||||
add(blockchain.getCardanoDerivationPathIfNeeded(network = this@getDerivationCandidates))
|
||||
}
|
||||
.filterNotNull()
|
||||
.distinct()
|
||||
}
|
||||
|
||||
private fun Blockchain.getDerivationPath(curve: EllipticCurve): DerivationPath? {
|
||||
return if (getSupportedCurves().contains(curve)) {
|
||||
derivationPath(style = userWallet.derivationStyleProvider.getDerivationStyle())
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun Blockchain.getCustomDerivationPath(curve: EllipticCurve, network: Network): DerivationPath? {
|
||||
return if (getSupportedCurves().contains(curve)) {
|
||||
network.derivationPath.value?.let(::DerivationPath)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun Blockchain.getCardanoDerivationPathIfNeeded(network: Network): DerivationPath? {
|
||||
return if (this == Blockchain.Cardano) {
|
||||
network.derivationPath.value?.let {
|
||||
CardanoUtils.extendedDerivationPath(derivationPath = DerivationPath(it))
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<DerivationPath>.filterAlreadyDerivedKeys(publicKey: KeyWalletPublicKey): List<DerivationPath> {
|
||||
val alreadyDerivedPaths = getAlreadyDerivedKeys(publicKey)
|
||||
return filterNot(alreadyDerivedPaths::contains)
|
||||
}
|
||||
|
||||
private fun getAlreadyDerivedKeys(publicKey: KeyWalletPublicKey): List<DerivationPath> {
|
||||
val extendedPublicKeysMap = when (userWallet) {
|
||||
is UserWallet.Cold -> userWallet.scanResponse.derivedKeys[publicKey] ?: ExtendedPublicKeysMap(emptyMap())
|
||||
is UserWallet.Hot -> {
|
||||
val wallets = userWallet.wallets ?: return emptyList()
|
||||
wallets.firstOrNull { it.publicKey.contentEquals(publicKey.bytes) }?.derivedKeys
|
||||
?: ExtendedPublicKeysMap(emptyMap())
|
||||
}
|
||||
}
|
||||
|
||||
return extendedPublicKeysMap.keys.toList()
|
||||
}
|
||||
}
|
||||
|
|
@ -2,14 +2,21 @@ package com.tangem.data.wallets.di
|
|||
|
||||
import com.tangem.data.wallets.DefaultWalletNamesMigrationRepository
|
||||
import com.tangem.data.wallets.DefaultWalletsRepository
|
||||
import com.tangem.data.wallets.cold.DefaultColdMapDerivationsRepository
|
||||
import com.tangem.data.wallets.derivations.DefaultDerivationsRepository
|
||||
import com.tangem.data.wallets.hot.DefaultHotMapDerivationsRepository
|
||||
import com.tangem.datasource.api.common.AuthProvider
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.local.datastore.RuntimeStateStore
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.wallets.derivations.ColdMapDerivationsRepository
|
||||
import com.tangem.domain.wallets.derivations.DerivationsRepository
|
||||
import com.tangem.domain.wallets.derivations.HotMapDerivationsRepository
|
||||
import com.tangem.domain.wallets.repository.WalletNamesMigrationRepository
|
||||
import com.tangem.domain.wallets.repository.WalletsRepository
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -44,4 +51,21 @@ internal object WalletsDataModule {
|
|||
fun provideMigrateNamesRepository(appPreferencesStore: AppPreferencesStore): WalletNamesMigrationRepository {
|
||||
return DefaultWalletNamesMigrationRepository(appPreferencesStore)
|
||||
}
|
||||
}
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal interface WalletsDataBindsModule {
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindDerivationsRepository(impl: DefaultDerivationsRepository): DerivationsRepository
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindHotMapDerivationsRepository(impl: DefaultHotMapDerivationsRepository): HotMapDerivationsRepository
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindColdMapDerivationsRepository(impl: DefaultColdMapDerivationsRepository): ColdMapDerivationsRepository
|
||||
}
|
||||
|
|
@ -0,0 +1,139 @@
|
|||
package com.tangem.data.wallets.hot
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchainsdk.utils.fromNetworkId
|
||||
import com.tangem.common.extensions.ByteArrayKey
|
||||
import com.tangem.common.extensions.toMapKey
|
||||
import com.tangem.crypto.hdWallet.DerivationPath
|
||||
import com.tangem.data.common.network.NetworkFactory
|
||||
import com.tangem.data.wallets.derivations.MissedDerivationsFinder
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.wallets.derivations.HotMapDerivationsRepository
|
||||
import com.tangem.domain.wallets.usecase.BackendId
|
||||
import com.tangem.hot.sdk.model.DeriveWalletRequest
|
||||
import com.tangem.operations.derivation.ExtendedPublicKeysMap
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class DefaultHotMapDerivationsRepository @Inject constructor(
|
||||
private val networkFactory: NetworkFactory,
|
||||
private val hotWalletAccessor: HotWalletAccessor,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : HotMapDerivationsRepository {
|
||||
|
||||
override suspend fun derivePublicKeys(
|
||||
userWallet: UserWallet.Hot,
|
||||
currencies: List<CryptoCurrency>,
|
||||
): UserWallet.Hot {
|
||||
return derivePublicKeysByNetworks(userWallet = userWallet, networks = currencies.map(CryptoCurrency::network))
|
||||
}
|
||||
|
||||
override suspend fun derivePublicKeysByNetworkIds(
|
||||
userWallet: UserWallet.Hot,
|
||||
networkIds: List<Network.RawID>,
|
||||
): UserWallet.Hot {
|
||||
return derivePublicKeysByNetworks(
|
||||
userWallet = userWallet,
|
||||
networks = networkIds.mapNotNull {
|
||||
networkFactory.create(
|
||||
blockchain = Blockchain.fromNetworkId(it.value) ?: return@mapNotNull null,
|
||||
extraDerivationPath = null,
|
||||
userWallet = userWallet,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun derivePublicKeysByNetworks(
|
||||
userWallet: UserWallet.Hot,
|
||||
networks: List<Network>,
|
||||
): UserWallet.Hot = withContext(dispatchers.default) {
|
||||
val derivations = MissedDerivationsFinder(userWallet)
|
||||
.findByNetworks(networks)
|
||||
.ifEmpty {
|
||||
Timber.d("Nothing to derive")
|
||||
return@withContext userWallet
|
||||
}
|
||||
|
||||
derivePublicKeys(userWallet, derivations).first
|
||||
}
|
||||
|
||||
override suspend fun derivePublicKeys(
|
||||
userWallet: UserWallet.Hot,
|
||||
derivations: Map<ByteArrayKey, List<DerivationPath>>,
|
||||
): Pair<UserWallet.Hot, Map<ByteArrayKey, ExtendedPublicKeysMap>> {
|
||||
val wallets = userWallet.wallets ?: return userWallet to emptyMap()
|
||||
|
||||
val request = DeriveWalletRequest(
|
||||
derivations.map { entry ->
|
||||
val wallet = wallets.first { it.publicKey.contentEquals(entry.key.bytes) }
|
||||
DeriveWalletRequest.Request(
|
||||
curve = wallet.curve,
|
||||
paths = entry.value,
|
||||
)
|
||||
},
|
||||
)
|
||||
val result = hotWalletAccessor.derivePublicKeys(
|
||||
hotWalletId = userWallet.hotWalletId,
|
||||
request = request,
|
||||
)
|
||||
val newKeys =
|
||||
result.responses.associate { ByteArrayKey(it.seedKey.publicKey) to ExtendedPublicKeysMap(it.publicKeys) }
|
||||
|
||||
return userWallet.updateWithNewKeys(newKeys) to newKeys
|
||||
}
|
||||
|
||||
override suspend fun hasMissedDerivations(
|
||||
userWallet: UserWallet.Hot,
|
||||
networksWithDerivationPath: Map<BackendId, String?>,
|
||||
): Boolean = withContext(dispatchers.default) {
|
||||
val derivations = MissedDerivationsFinder(userWallet)
|
||||
.findByNetworks(
|
||||
networksWithDerivationPath.mapNotNull { (backendId, extraDerivationPath) ->
|
||||
networkFactory.create(
|
||||
blockchain = Blockchain.fromNetworkId(backendId) ?: return@mapNotNull null,
|
||||
extraDerivationPath = extraDerivationPath,
|
||||
userWallet = userWallet,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
derivations.isNotEmpty()
|
||||
}
|
||||
|
||||
private fun UserWallet.Hot.updateWithNewKeys(newKeys: Map<ByteArrayKey, ExtendedPublicKeysMap>): UserWallet.Hot {
|
||||
val wallets = this.wallets ?: return this
|
||||
val derivedKeys = wallets.associate {
|
||||
it.publicKey.toMapKey() to ExtendedPublicKeysMap(it.derivedKeys)
|
||||
}
|
||||
val updatedKeys = getUpdatedDerivedKeys(
|
||||
oldKeys = derivedKeys,
|
||||
newKeys = newKeys,
|
||||
)
|
||||
|
||||
return copy(
|
||||
wallets = wallets.map { wallet ->
|
||||
wallet.copy(
|
||||
derivedKeys = updatedKeys[wallet.publicKey.toMapKey()] ?: ExtendedPublicKeysMap(emptyMap()),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun getUpdatedDerivedKeys(
|
||||
oldKeys: Map<ByteArrayKey, ExtendedPublicKeysMap>,
|
||||
newKeys: Map<ByteArrayKey, ExtendedPublicKeysMap>,
|
||||
): Map<ByteArrayKey, ExtendedPublicKeysMap> {
|
||||
return (oldKeys.keys + newKeys.keys).toSet()
|
||||
.associateWith { walletKey ->
|
||||
val oldDerivations = ExtendedPublicKeysMap(oldKeys[walletKey] ?: emptyMap())
|
||||
val newDerivations = newKeys[walletKey] ?: ExtendedPublicKeysMap(emptyMap())
|
||||
|
||||
ExtendedPublicKeysMap(oldDerivations + newDerivations)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
package com.tangem.data.wallets.hot
|
||||
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.domain.wallets.hot.HotWalletPasswordRequester
|
||||
import com.tangem.hot.sdk.TangemHotSdk
|
||||
import com.tangem.hot.sdk.exception.WrongPasswordException
|
||||
import com.tangem.hot.sdk.model.*
|
||||
import javax.inject.Inject
|
||||
|
||||
class HotWalletAccessor @Inject constructor(
|
||||
private val tangemHotSdk: TangemHotSdk,
|
||||
private val hotWalletPasswordRequester: HotWalletPasswordRequester,
|
||||
) {
|
||||
|
||||
suspend fun signHashes(hotWalletId: HotWalletId, dataToSign: List<DataToSign>): List<SignedData> =
|
||||
hotSdkRequest(hotWalletId) { unlock ->
|
||||
tangemHotSdk.signHashes(unlockHotWallet = unlock, dataToSign = dataToSign)
|
||||
}
|
||||
|
||||
suspend fun derivePublicKeys(hotWalletId: HotWalletId, request: DeriveWalletRequest): DerivedPublicKeyResponse =
|
||||
hotSdkRequest(hotWalletId) { unlock ->
|
||||
tangemHotSdk.derivePublicKey(unlockHotWallet = unlock, request = request)
|
||||
}
|
||||
|
||||
private suspend fun <T> hotSdkRequest(hotWalletId: HotWalletId, block: suspend (unlock: UnlockHotWallet) -> T): T {
|
||||
val auth = when (hotWalletId.authType) {
|
||||
HotWalletId.AuthType.NoPassword -> HotAuth.NoAuth
|
||||
HotWalletId.AuthType.Password -> requestPassword(false)
|
||||
HotWalletId.AuthType.Biometry -> HotAuth.Biometry
|
||||
}
|
||||
|
||||
return runCatchingSdkErrors(hotWalletId, auth) {
|
||||
block(UnlockHotWallet(hotWalletId, it)).also {
|
||||
hotWalletPasswordRequester.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun <T> runCatchingSdkErrors(
|
||||
hotWalletId: HotWalletId,
|
||||
auth: HotAuth,
|
||||
block: suspend (auth: HotAuth) -> T,
|
||||
): T {
|
||||
return runCatchingWrongPassInternal(
|
||||
originalAuth = auth,
|
||||
auth = auth,
|
||||
block = { blockAuth ->
|
||||
block(blockAuth).also {
|
||||
// TODO [REDACTED_TASK_KEY] [Hot Wallet] Authorization by access code
|
||||
// if user has biometry enabled, we set it as the new auth method
|
||||
if (blockAuth is HotAuth.Password /*&& has biometry enabled */) {
|
||||
tangemHotSdk.changeAuth(
|
||||
unlockHotWallet = UnlockHotWallet(
|
||||
walletId = hotWalletId,
|
||||
auth = blockAuth,
|
||||
),
|
||||
auth = HotAuth.Biometry,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun <T> runCatchingWrongPassInternal(
|
||||
originalAuth: HotAuth,
|
||||
auth: HotAuth,
|
||||
block: suspend (auth: HotAuth) -> T,
|
||||
): T = runCatching {
|
||||
block(auth)
|
||||
}.getOrElse { exception ->
|
||||
if (auth is HotAuth.Biometry && exception.isBiometryError()) {
|
||||
// fallback to password if biometry fails
|
||||
val passAuth = requestPassword(true)
|
||||
|
||||
return@getOrElse runCatchingWrongPassInternal(
|
||||
originalAuth = originalAuth,
|
||||
auth = passAuth,
|
||||
block = block,
|
||||
)
|
||||
}
|
||||
|
||||
if (exception !is WrongPasswordException) {
|
||||
throw exception
|
||||
}
|
||||
|
||||
// If the exception is a wrong password, we need to request the password again
|
||||
|
||||
hotWalletPasswordRequester.wrongPassword()
|
||||
val passResult = requestPassword(originalAuth is HotAuth.Biometry)
|
||||
|
||||
runCatchingWrongPassInternal(
|
||||
originalAuth = originalAuth,
|
||||
auth = passResult,
|
||||
block = block,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun requestPassword(hasBiometry: Boolean): HotAuth {
|
||||
return hotWalletPasswordRequester.requestPassword(hasBiometry).toAuth() ?: throw TangemSdkError.UserCancelled()
|
||||
}
|
||||
|
||||
private fun Throwable.isBiometryError(): Boolean {
|
||||
return this is TangemSdkError.AuthenticationFailed ||
|
||||
this is TangemSdkError.AuthenticationCanceled ||
|
||||
this is TangemSdkError.AuthenticationLockout ||
|
||||
this is TangemSdkError.AuthenticationUnavailable ||
|
||||
this is TangemSdkError.AuthenticationAlreadyInProgress ||
|
||||
this is TangemSdkError.AuthenticationNotInitialized ||
|
||||
this is TangemSdkError.AuthenticationPermanentLockout
|
||||
}
|
||||
|
||||
private fun HotWalletPasswordRequester.Result.toAuth() = when (this) {
|
||||
HotWalletPasswordRequester.Result.UseBiometry -> HotAuth.Biometry
|
||||
HotWalletPasswordRequester.Result.Dismiss -> null
|
||||
is HotWalletPasswordRequester.Result.EnteredPassword -> this.password
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
package com.tangem.data.wallets.hot
|
||||
|
||||
import com.tangem.blockchain.common.TransactionSigner
|
||||
import com.tangem.blockchain.common.Wallet
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.common.map
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.hot.sdk.model.DataToSign
|
||||
import com.tangem.operations.sign.SignData
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import timber.log.Timber
|
||||
|
||||
class TangemHotWalletSigner @AssistedInject constructor(
|
||||
@Assisted private val userWallet: UserWallet.Hot,
|
||||
private val hotWalletAccessor: HotWalletAccessor,
|
||||
) : TransactionSigner {
|
||||
|
||||
override suspend fun sign(hash: ByteArray, publicKey: Wallet.PublicKey): CompletionResult<ByteArray> {
|
||||
return sign(listOf(hash), publicKey).map { it.first() }
|
||||
}
|
||||
|
||||
override suspend fun sign(
|
||||
hashes: List<ByteArray>,
|
||||
publicKey: Wallet.PublicKey,
|
||||
): CompletionResult<List<ByteArray>> {
|
||||
val wallet = userWallet.wallets.orEmpty().firstOrNull { it.publicKey.contentEquals(publicKey.seedKey) }
|
||||
?: return CompletionResult.Failure(
|
||||
TangemSdkError.ExceptionError(IllegalStateException("wallet is locked")),
|
||||
)
|
||||
|
||||
val result = runCatching {
|
||||
hotWalletAccessor.signHashes(
|
||||
hotWalletId = userWallet.hotWalletId,
|
||||
dataToSign = listOf(
|
||||
DataToSign(
|
||||
curve = wallet.curve,
|
||||
hashes = hashes,
|
||||
derivationPath = publicKey.derivationPath,
|
||||
),
|
||||
),
|
||||
)
|
||||
}.getOrElse {
|
||||
Timber.e(it)
|
||||
return if (it is TangemSdkError) {
|
||||
CompletionResult.Failure(it)
|
||||
} else {
|
||||
CompletionResult.Failure(TangemSdkError.ExceptionError(it))
|
||||
}
|
||||
}
|
||||
|
||||
return CompletionResult.Success(result.map { it.signatures }.flatten())
|
||||
}
|
||||
|
||||
override suspend fun multiSign(
|
||||
dataToSign: List<SignData>,
|
||||
publicKey: Wallet.PublicKey,
|
||||
): CompletionResult<Map<ByteArray, ByteArray>> {
|
||||
val result = runCatching {
|
||||
hotWalletAccessor.signHashes(
|
||||
hotWalletId = userWallet.hotWalletId,
|
||||
dataToSign = dataToSign.map { signData ->
|
||||
val wallet =
|
||||
userWallet.wallets.orEmpty().firstOrNull { it.publicKey.contentEquals(signData.publicKey) }
|
||||
?: return CompletionResult.Failure(
|
||||
TangemSdkError.ExceptionError(IllegalStateException("wallet is locked")),
|
||||
)
|
||||
|
||||
DataToSign(
|
||||
curve = wallet.curve,
|
||||
hashes = listOf(signData.hash),
|
||||
derivationPath = signData.derivationPath,
|
||||
)
|
||||
},
|
||||
)
|
||||
}.getOrElse {
|
||||
Timber.e(it)
|
||||
return if (it is TangemSdkError) {
|
||||
CompletionResult.Failure(it)
|
||||
} else {
|
||||
CompletionResult.Failure(TangemSdkError.ExceptionError(it))
|
||||
}
|
||||
}
|
||||
|
||||
return CompletionResult.Success(
|
||||
result.mapIndexed { index, data ->
|
||||
dataToSign[index].publicKey to data.signatures.first()
|
||||
}.toMap(),
|
||||
)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(@Assisted userWallet: UserWallet.Hot): TangemHotWalletSigner
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,177 @@
|
|||
package com.tangem.data.wallets.derivations
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.test.domain.card.MockScanResponseFactory
|
||||
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
|
||||
import com.tangem.data.common.network.NetworkFactory
|
||||
import com.tangem.data.wallets.cold.DefaultColdMapDerivationsRepository
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.card.ScanCardException
|
||||
import com.tangem.domain.card.configs.GenericCardConfig
|
||||
import com.tangem.domain.card.configs.MultiWalletCardConfig
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.operations.derivation.DerivationTaskResponse
|
||||
import com.tangem.sdk.api.TangemSdkManager
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class DefaultDerivationsRepositoryTest {
|
||||
|
||||
private val tangemSdkManager = mockk<TangemSdkManager>()
|
||||
private val userWalletsStore = mockk<UserWalletsStore>()
|
||||
private val repository = DefaultDerivationsRepository(
|
||||
userWalletsStore = userWalletsStore,
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
hotDerivationsRepository = mockk(),
|
||||
coldDerivationsRepository = DefaultColdMapDerivationsRepository(
|
||||
tangemSdkManager = tangemSdkManager,
|
||||
networkFactory = NetworkFactory(excludedBlockchains = ExcludedBlockchains()),
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
),
|
||||
)
|
||||
|
||||
private val defaultUserWalletId = UserWalletId("011")
|
||||
private val defaultUserWallet = UserWallet.Cold(
|
||||
name = "",
|
||||
walletId = defaultUserWalletId,
|
||||
cardsInWallet = setOf(),
|
||||
isMultiCurrency = false,
|
||||
scanResponse = MockScanResponseFactory.create(cardConfig = GenericCardConfig(2), derivedKeys = emptyMap()),
|
||||
hasBackupError = false,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `error if userWalletId not found`() = runTest {
|
||||
coEvery { userWalletsStore.getSyncStrict(defaultUserWalletId) } throws IllegalStateException()
|
||||
|
||||
runCatching {
|
||||
repository.derivePublicKeys(userWalletId = defaultUserWalletId, currencies = emptyList())
|
||||
}
|
||||
.onSuccess { error("Should throws exception") }
|
||||
.onFailure { Truth.assertThat(it).isInstanceOf(IllegalStateException::class.java) }
|
||||
|
||||
coVerify(exactly = 1) { userWalletsStore.getSyncStrict(defaultUserWalletId) }
|
||||
coVerify(inverse = true) { tangemSdkManager.derivePublicKeys(null, any(), any()) }
|
||||
coVerify(inverse = true) { userWalletsStore.update(defaultUserWalletId, any()) }
|
||||
}
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
@Test
|
||||
fun `success if card is not supported derivations`() = runTest {
|
||||
coEvery { userWalletsStore.getSyncStrict(defaultUserWalletId) } returns defaultUserWallet
|
||||
|
||||
repository.derivePublicKeys(userWalletId = defaultUserWalletId, currencies = emptyList())
|
||||
|
||||
runCatching { }
|
||||
.onSuccess { Truth.assertThat(it) }
|
||||
.onFailure {
|
||||
error("Should returns success")
|
||||
}
|
||||
|
||||
coVerify(exactly = 1) { userWalletsStore.getSyncStrict(defaultUserWalletId) }
|
||||
coVerify(inverse = true) { tangemSdkManager.derivePublicKeys(null, any(), any()) }
|
||||
coVerify(inverse = true) { userWalletsStore.update(defaultUserWalletId, any()) }
|
||||
}
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
@Test
|
||||
fun `success if currencies is empty`() = runTest {
|
||||
val userWallet = defaultUserWallet.copy(
|
||||
scanResponse = MockScanResponseFactory.create(cardConfig = MultiWalletCardConfig, derivedKeys = emptyMap()),
|
||||
)
|
||||
coEvery { userWalletsStore.getSyncStrict(defaultUserWalletId) } returns userWallet
|
||||
|
||||
runCatching { repository.derivePublicKeys(userWalletId = defaultUserWalletId, currencies = emptyList()) }
|
||||
.onSuccess { Truth.assertThat(it) }
|
||||
.onFailure { error("Should returns success") }
|
||||
|
||||
coVerify(exactly = 1) { userWalletsStore.getSyncStrict(defaultUserWalletId) }
|
||||
coVerify(inverse = true) { tangemSdkManager.derivePublicKeys(null, any(), any()) }
|
||||
coVerify(inverse = true) { userWalletsStore.update(defaultUserWalletId, any()) }
|
||||
}
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
@Test
|
||||
fun `success if card already has derivations`() = runTest {
|
||||
val userWallet = defaultUserWallet.copy(
|
||||
scanResponse = MockScanResponseFactory.create(
|
||||
cardConfig = MultiWalletCardConfig,
|
||||
derivedKeys = DerivedKeysMocks.ethereumDerivedKeys,
|
||||
),
|
||||
)
|
||||
|
||||
coEvery { userWalletsStore.getSyncStrict(defaultUserWalletId) } returns userWallet
|
||||
|
||||
runCatching {
|
||||
repository.derivePublicKeys(
|
||||
userWalletId = defaultUserWalletId,
|
||||
currencies = MockCryptoCurrencyFactory(userWallet).ethereum.let(::listOf),
|
||||
)
|
||||
}
|
||||
.onSuccess { Truth.assertThat(it) }
|
||||
.onFailure { error("Should returns success") }
|
||||
|
||||
coVerify(exactly = 1) { userWalletsStore.getSyncStrict(defaultUserWalletId) }
|
||||
coVerify(inverse = true) { tangemSdkManager.derivePublicKeys(null, any(), any()) }
|
||||
coVerify(inverse = true) { userWalletsStore.update(defaultUserWalletId, any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `error if tangemSdkManager throws exception`() = runTest {
|
||||
val userWallet = defaultUserWallet.copy(
|
||||
scanResponse = MockScanResponseFactory.create(cardConfig = MultiWalletCardConfig, derivedKeys = emptyMap()),
|
||||
)
|
||||
coEvery { userWalletsStore.getSyncStrict(defaultUserWalletId) } returns userWallet
|
||||
coEvery { tangemSdkManager.derivePublicKeys(null, any(), any()) } throws ScanCardException.UserCancelled
|
||||
|
||||
runCatching {
|
||||
repository.derivePublicKeys(
|
||||
userWalletId = defaultUserWalletId,
|
||||
currencies = MockCryptoCurrencyFactory(userWallet).ethereum.let(::listOf),
|
||||
)
|
||||
}
|
||||
.onSuccess { error("Should throws exception") }
|
||||
.onFailure { Truth.assertThat(it).isInstanceOf(ScanCardException.UserCancelled::class.java) }
|
||||
|
||||
coVerify(exactly = 1) { userWalletsStore.getSyncStrict(defaultUserWalletId) }
|
||||
coVerify(exactly = 1) { tangemSdkManager.derivePublicKeys(null, any(), any()) }
|
||||
coVerify(inverse = true) { userWalletsStore.update(defaultUserWalletId, any()) }
|
||||
}
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
@Test
|
||||
fun `success case`() = runTest {
|
||||
val userWallet = defaultUserWallet.copy(
|
||||
scanResponse = MockScanResponseFactory.create(cardConfig = MultiWalletCardConfig, derivedKeys = emptyMap()),
|
||||
)
|
||||
coEvery { userWalletsStore.getSyncStrict(defaultUserWalletId) } returns userWallet
|
||||
coEvery { tangemSdkManager.derivePublicKeys(null, any(), any()) } returns CompletionResult.Success(
|
||||
DerivationTaskResponse(DerivedKeysMocks.ethereumDerivedKeys),
|
||||
)
|
||||
coEvery { userWalletsStore.update(defaultUserWalletId, any()) } returns CompletionResult.Success(userWallet)
|
||||
|
||||
runCatching {
|
||||
repository.derivePublicKeys(
|
||||
userWalletId = defaultUserWalletId,
|
||||
currencies = MockCryptoCurrencyFactory(userWallet).ethereum.let(::listOf),
|
||||
)
|
||||
}
|
||||
.onSuccess { Truth.assertThat(it) }
|
||||
.onFailure { error("Should returns success but $it") }
|
||||
|
||||
coVerify(exactly = 1) { userWalletsStore.getSyncStrict(defaultUserWalletId) }
|
||||
coVerify(exactly = 1) { tangemSdkManager.derivePublicKeys(null, any(), any()) }
|
||||
coVerify(exactly = 1) { userWalletsStore.update(defaultUserWalletId, any()) }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package com.tangem.data.wallets.derivations
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.derivation.DerivationConfigV2
|
||||
import com.tangem.common.card.EllipticCurve
|
||||
import com.tangem.common.extensions.toMapKey
|
||||
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
|
||||
import com.tangem.operations.derivation.ExtendedPublicKeysMap
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal object DerivedKeysMocks {
|
||||
|
||||
val ethereumDerivedKeys = mapOf(
|
||||
EllipticCurve.Secp256k1.name.toByteArray().toMapKey() to ExtendedPublicKeysMap(
|
||||
mapOf(
|
||||
DerivationConfigV2.derivations(Blockchain.Ethereum).values.first() to ExtendedPublicKey(
|
||||
publicKey = ByteArray(0),
|
||||
chainCode = ByteArray(0),
|
||||
depth = 2646,
|
||||
parentFingerprint = ByteArray(0),
|
||||
childNumber = 1142,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,146 @@
|
|||
package com.tangem.data.wallets.derivations
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.blockchain.blockchains.cardano.CardanoUtils
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.derivation.DerivationConfigV2
|
||||
import com.tangem.common.card.EllipticCurve
|
||||
import com.tangem.common.extensions.ByteArrayKey
|
||||
import com.tangem.common.test.domain.card.MockScanResponseFactory
|
||||
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
|
||||
import com.tangem.common.test.domain.wallet.MockUserWalletFactory
|
||||
import com.tangem.crypto.hdWallet.DerivationPath
|
||||
import com.tangem.domain.card.configs.GenericCardConfig
|
||||
import com.tangem.domain.card.configs.MultiWalletCardConfig
|
||||
import com.tangem.domain.card.configs.Wallet2CardConfig
|
||||
import com.tangem.domain.wallets.derivations.derivationStyleProvider
|
||||
import org.junit.Test
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class MissedDerivationsFinderTest {
|
||||
|
||||
@Test
|
||||
fun `empty derivations for empty currencies`() {
|
||||
val scanResponse = MockScanResponseFactory.create(cardConfig = GenericCardConfig(2), derivedKeys = emptyMap())
|
||||
val userWallet = MockUserWalletFactory.create(scanResponse)
|
||||
val finder = MissedDerivationsFinder(userWallet)
|
||||
|
||||
val actual = finder.find(emptyList())
|
||||
|
||||
Truth.assertThat(actual).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `empty derivations for non supported blockchains`() {
|
||||
// Bls is not supported
|
||||
val scanResponse = MockScanResponseFactory.create(cardConfig = GenericCardConfig(2), derivedKeys = emptyMap())
|
||||
val userWallet = MockUserWalletFactory.create(scanResponse)
|
||||
val finder = MissedDerivationsFinder(userWallet)
|
||||
|
||||
val currencies = MockCryptoCurrencyFactory(userWallet).chia.let(::listOf)
|
||||
val actual = finder.find(currencies)
|
||||
|
||||
Truth.assertThat(actual).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `derivations ONLY for supported blockchains`() {
|
||||
// Bls is not supported
|
||||
val scanResponse = MockScanResponseFactory.create(
|
||||
cardConfig = GenericCardConfig(2),
|
||||
derivedKeys = emptyMap(),
|
||||
).let {
|
||||
it.copy(
|
||||
card = it.card.copy(
|
||||
settings = it.card.settings.copy(isHDWalletAllowed = true, isBackupAllowed = true),
|
||||
),
|
||||
)
|
||||
}
|
||||
val userWallet = MockUserWalletFactory.create(scanResponse)
|
||||
val finder = MissedDerivationsFinder(userWallet)
|
||||
|
||||
val currencies = MockCryptoCurrencyFactory(userWallet).chiaAndEthereum
|
||||
val actual = finder.find(currencies)
|
||||
|
||||
Truth.assertThat(actual).containsExactly(
|
||||
ByteArrayKey(EllipticCurve.Secp256k1.name.toByteArray()),
|
||||
listOf(DerivationConfigV2.derivations(Blockchain.Ethereum).values.first()),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `derivations for custom token`() {
|
||||
val scanResponse = MockScanResponseFactory.create(cardConfig = MultiWalletCardConfig, derivedKeys = emptyMap())
|
||||
val userWallet = MockUserWalletFactory.create(scanResponse)
|
||||
val finder = MissedDerivationsFinder(userWallet)
|
||||
|
||||
val currencies = MockCryptoCurrencyFactory(userWallet).ethereumTokenWithBinanceDerivation
|
||||
val actual = finder.find(currencies)
|
||||
|
||||
Truth.assertThat(actual).containsExactly(
|
||||
ByteArrayKey(EllipticCurve.Secp256k1.name.toByteArray()),
|
||||
listOf(
|
||||
DerivationConfigV2.derivations(Blockchain.Ethereum).values.first(),
|
||||
DerivationConfigV2.derivations(Blockchain.Binance).values.first(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `derivations for cardano`() {
|
||||
val scanResponse = MockScanResponseFactory.create(cardConfig = MultiWalletCardConfig, derivedKeys = emptyMap())
|
||||
val userWallet = MockUserWalletFactory.create(scanResponse)
|
||||
val finder = MissedDerivationsFinder(userWallet)
|
||||
|
||||
val currencies = MockCryptoCurrencyFactory(userWallet).cardano.let(::listOf)
|
||||
val actual = finder.find(currencies)
|
||||
|
||||
Truth.assertThat(actual).containsExactly(
|
||||
ByteArrayKey(EllipticCurve.Ed25519.name.toByteArray()),
|
||||
listOf(
|
||||
DerivationConfigV2.derivations(Blockchain.Cardano).values.first(),
|
||||
CardanoUtils.extendedDerivationPath(
|
||||
derivationPath = DerivationPath(
|
||||
Blockchain.Cardano.derivationPath(scanResponse.derivationStyleProvider.getDerivationStyle())!!
|
||||
.rawPath,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `empty derivations for already derived currencies`() {
|
||||
val scanResponse = MockScanResponseFactory.create(
|
||||
cardConfig = Wallet2CardConfig,
|
||||
derivedKeys = DerivedKeysMocks.ethereumDerivedKeys,
|
||||
)
|
||||
val userWallet = MockUserWalletFactory.create(scanResponse)
|
||||
val finder = MissedDerivationsFinder(userWallet)
|
||||
|
||||
val currencies = MockCryptoCurrencyFactory(userWallet).ethereum.let(::listOf)
|
||||
val actual = finder.find(currencies)
|
||||
|
||||
Truth.assertThat(actual).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `derivations ONLY for never derived currencies`() {
|
||||
val scanResponse = MockScanResponseFactory.create(
|
||||
cardConfig = MultiWalletCardConfig,
|
||||
derivedKeys = DerivedKeysMocks.ethereumDerivedKeys,
|
||||
)
|
||||
val userWallet = MockUserWalletFactory.create(scanResponse)
|
||||
val finder = MissedDerivationsFinder(userWallet)
|
||||
|
||||
val currencies = MockCryptoCurrencyFactory(userWallet).ethereumAndStellar
|
||||
val actual = finder.find(currencies)
|
||||
|
||||
Truth.assertThat(actual).containsExactly(
|
||||
ByteArrayKey(EllipticCurve.Ed25519.name.toByteArray()),
|
||||
listOf(DerivationConfigV2.derivations(Blockchain.Stellar).values.first()),
|
||||
)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue