Updated on 2026-08-14
This commit is contained in:
commit
3cbc2dadfb
822 changed files with 12838 additions and 5366 deletions
|
|
@ -36,6 +36,7 @@ internal class AccountListConverter @AssistedInject constructor(
|
|||
userWalletId = userWallet.walletId,
|
||||
accounts = value.accounts.map(cryptoPortfolioConverter::convert),
|
||||
totalAccounts = value.wallet.totalAccounts,
|
||||
totalArchivedAccounts = value.wallet.totalArchivedAccounts,
|
||||
sortType = sortType,
|
||||
groupType = groupType,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ internal class GetWalletAccountsResponseConverter @AssistedInject constructor(
|
|||
group = TokensGroupTypeConverter.convertBack(value.groupType),
|
||||
sort = TokensSortTypeConverter.convertBack(value.sortType),
|
||||
totalAccounts = value.totalAccounts,
|
||||
totalArchivedAccounts = value.totalArchivedAccounts,
|
||||
),
|
||||
accounts = value.accounts
|
||||
.filterIsInstance<Account.CryptoPortfolio>()
|
||||
|
|
|
|||
|
|
@ -94,21 +94,23 @@ internal class DefaultWalletAccountsFetcher @Inject constructor(
|
|||
override suspend fun push(
|
||||
userWalletId: UserWalletId,
|
||||
body: SaveWalletAccountsResponse,
|
||||
): GetWalletAccountsResponse? {
|
||||
return pushInternal(userWalletId = userWalletId, body = body)
|
||||
}
|
||||
|
||||
private suspend fun pushInternal(
|
||||
userWalletId: UserWalletId,
|
||||
body: SaveWalletAccountsResponse,
|
||||
eTag: String? = null,
|
||||
): GetWalletAccountsResponse? {
|
||||
return safeApiCall(
|
||||
call = {
|
||||
var eTag = getETag(userWalletId)
|
||||
|
||||
if (eTag == null) {
|
||||
fetch(userWalletId)
|
||||
|
||||
eTag = getETag(userWalletId) ?: error("ETag is null after fetch")
|
||||
}
|
||||
val resolvedETag = eTag ?: getETagForPush(userWalletId)
|
||||
|
||||
val apiResponse = withContext(dispatchers.io) {
|
||||
tangemTechApi.saveWalletAccounts(
|
||||
walletId = userWalletId.stringValue,
|
||||
eTag = eTag,
|
||||
eTag = resolvedETag,
|
||||
body = body,
|
||||
)
|
||||
}
|
||||
|
|
@ -127,6 +129,19 @@ internal class DefaultWalletAccountsFetcher @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
private suspend fun getETagForPush(userWalletId: UserWalletId): String {
|
||||
var savedETag = getETag(userWalletId)
|
||||
|
||||
if (savedETag == null) {
|
||||
fetch(userWalletId)
|
||||
|
||||
savedETag = getETag(userWalletId)
|
||||
?: error("Failed to retrieve ETag after fetching wallet accounts for wallet $userWalletId")
|
||||
}
|
||||
|
||||
return savedETag
|
||||
}
|
||||
|
||||
private suspend fun fetchWalletAccounts(
|
||||
userWalletId: UserWalletId,
|
||||
savedAccountsResponse: GetWalletAccountsResponse?,
|
||||
|
|
@ -154,7 +169,13 @@ internal class DefaultWalletAccountsFetcher @Inject constructor(
|
|||
error = throwable,
|
||||
userWalletId = userWalletId,
|
||||
savedAccountsResponse = savedAccountsResponse,
|
||||
pushWalletAccounts = ::push,
|
||||
pushWalletAccounts = { accounts, eTag ->
|
||||
pushInternal(
|
||||
userWalletId = userWalletId,
|
||||
body = SaveWalletAccountsResponse(accounts),
|
||||
eTag = eTag,
|
||||
)
|
||||
},
|
||||
storeWalletAccounts = ::store,
|
||||
)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ package com.tangem.data.account.fetcher
|
|||
|
||||
import com.tangem.data.account.fetcher.DefaultWalletAccountsFetcher.FetchResult
|
||||
import com.tangem.data.account.utils.DefaultWalletAccountsResponseFactory
|
||||
import com.tangem.data.common.cache.etag.ETagsStore
|
||||
import com.tangem.data.common.currency.UserTokensResponseAccountIdEnricher
|
||||
import com.tangem.data.common.currency.UserTokensSaver
|
||||
import com.tangem.datasource.api.common.response.ApiResponse
|
||||
|
|
@ -29,10 +28,10 @@ import javax.inject.Singleton
|
|||
* Handles errors that occur during the fetching of wallet accounts
|
||||
*
|
||||
* @property tangemTechApi API for network requests
|
||||
* @property userWalletsStore provides access to user wallets storage
|
||||
* @property userTokensSaver saves user tokens to the storage
|
||||
* @property userTokensResponseStore provides access to user token responses.
|
||||
* @property defaultWalletAccountsResponseFactory creates [GetWalletAccountsResponse] from [UserTokensResponse]
|
||||
* @property eTagsStore store for ETags to manage caching
|
||||
* @property dispatchers dispatchers
|
||||
*
|
||||
* @see DefaultWalletAccountsFetcher
|
||||
|
|
@ -47,7 +46,6 @@ internal class FetchWalletAccountsErrorHandler @Inject constructor(
|
|||
private val userTokensSaver: UserTokensSaver,
|
||||
private val userTokensResponseStore: UserTokensResponseStore,
|
||||
private val defaultWalletAccountsResponseFactory: DefaultWalletAccountsResponseFactory,
|
||||
private val eTagsStore: ETagsStore,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) {
|
||||
|
||||
|
|
@ -66,7 +64,7 @@ internal class FetchWalletAccountsErrorHandler @Inject constructor(
|
|||
error: ApiResponseError,
|
||||
userWalletId: UserWalletId,
|
||||
savedAccountsResponse: GetWalletAccountsResponse?,
|
||||
pushWalletAccounts: suspend (UserWalletId, List<WalletAccountDTO>) -> GetWalletAccountsResponse?,
|
||||
pushWalletAccounts: suspend (List<WalletAccountDTO>, String) -> GetWalletAccountsResponse?,
|
||||
storeWalletAccounts: suspend (UserWalletId, GetWalletAccountsResponse) -> Unit,
|
||||
): FetchResult {
|
||||
val isResponseUpToDate = error.isNetworkError(code = Code.NOT_MODIFIED)
|
||||
|
|
@ -87,9 +85,7 @@ internal class FetchWalletAccountsErrorHandler @Inject constructor(
|
|||
val eTag = createWallet(userWalletId)
|
||||
|
||||
if (eTag != null) {
|
||||
eTagsStore.store(userWalletId = userWalletId, key = ETagsStore.Key.WalletAccounts, value = eTag)
|
||||
|
||||
pushWalletAccounts(userWalletId, accountDTOs)
|
||||
pushWalletAccounts(accountDTOs, eTag)
|
||||
userTokensSaver.pushWithRetryer(userWalletId, userTokensResponse)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ internal class DefaultWalletAccountsResponseFactory @Inject constructor(
|
|||
group = response.group,
|
||||
sort = response.sort,
|
||||
totalAccounts = accountDTOs.size,
|
||||
totalArchivedAccounts = 0,
|
||||
),
|
||||
accounts = accountDTOs.assignTokens(userWalletId = userWalletId, tokens = response.tokens),
|
||||
unassignedTokens = emptyList(),
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ internal fun createGetWalletAccountsResponse(
|
|||
group = groupType,
|
||||
sort = sortType,
|
||||
totalAccounts = 1,
|
||||
totalArchivedAccounts = 0,
|
||||
),
|
||||
accounts = buildList {
|
||||
createWalletAccountDTO(
|
||||
|
|
@ -79,6 +80,7 @@ internal fun createAccountList(
|
|||
userWalletId = userWalletId,
|
||||
accounts = listOf(createCryptoPortfolio(userWalletId)),
|
||||
totalAccounts = 1,
|
||||
totalArchivedAccounts = 0,
|
||||
sortType = sortType,
|
||||
groupType = groupType,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -137,6 +137,7 @@ class AccountListConverterTest {
|
|||
group = UserTokensResponse.GroupType.NETWORK,
|
||||
sort = UserTokensResponse.SortType.BALANCE,
|
||||
totalAccounts = 1,
|
||||
totalArchivedAccounts = 0,
|
||||
),
|
||||
accounts = emptyList(),
|
||||
unassignedTokens = emptyList(),
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package com.tangem.data.account.fetcher
|
|||
import com.tangem.data.account.converter.createGetWalletAccountsResponse
|
||||
import com.tangem.data.account.converter.createWalletAccountDTO
|
||||
import com.tangem.data.account.utils.DefaultWalletAccountsResponseFactory
|
||||
import com.tangem.data.common.cache.etag.ETagsStore
|
||||
import com.tangem.data.common.currency.UserTokensSaver
|
||||
import com.tangem.datasource.api.common.response.ApiResponse
|
||||
import com.tangem.datasource.api.common.response.ApiResponseError
|
||||
|
|
@ -38,7 +37,6 @@ class FetchWalletAccountsErrorHandlerTest {
|
|||
private val userTokensSaver: UserTokensSaver = mockk(relaxUnitFun = true)
|
||||
private val userTokensResponseStore: UserTokensResponseStore = mockk(relaxUnitFun = true)
|
||||
private val defaultWalletAccountsResponseFactory: DefaultWalletAccountsResponseFactory = mockk()
|
||||
private val eTagsStore: ETagsStore = mockk(relaxUnitFun = true)
|
||||
|
||||
private val handler = FetchWalletAccountsErrorHandler(
|
||||
tangemTechApi = tangemTechApi,
|
||||
|
|
@ -46,17 +44,18 @@ class FetchWalletAccountsErrorHandlerTest {
|
|||
userTokensSaver = userTokensSaver,
|
||||
userTokensResponseStore = userTokensResponseStore,
|
||||
defaultWalletAccountsResponseFactory = defaultWalletAccountsResponseFactory,
|
||||
eTagsStore = eTagsStore,
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
)
|
||||
|
||||
private val pushWalletAccounts: suspend (UserWalletId, List<WalletAccountDTO>) -> GetWalletAccountsResponse =
|
||||
private val pushWalletAccounts: suspend (List<WalletAccountDTO>, String) -> GetWalletAccountsResponse =
|
||||
mockk(relaxed = true)
|
||||
private val storeWalletAccounts: suspend (UserWalletId, GetWalletAccountsResponse) -> Unit = mockk(relaxed = true)
|
||||
|
||||
@BeforeEach
|
||||
fun setupEach() {
|
||||
clearMocks(
|
||||
tangemTechApi,
|
||||
userWalletsStore,
|
||||
userTokensSaver,
|
||||
userTokensResponseStore,
|
||||
defaultWalletAccountsResponseFactory,
|
||||
|
|
@ -129,7 +128,7 @@ class FetchWalletAccountsErrorHandlerTest {
|
|||
),
|
||||
)
|
||||
} returns apiResponse
|
||||
coEvery { pushWalletAccounts(userWalletId, listOf(accountDTO)) } returns savedAccountsResponse
|
||||
coEvery { pushWalletAccounts(listOf(accountDTO), eTagValue) } returns savedAccountsResponse
|
||||
|
||||
// Act
|
||||
handler.handle(
|
||||
|
|
@ -150,8 +149,7 @@ class FetchWalletAccountsErrorHandlerTest {
|
|||
walletType = WalletType.COLD,
|
||||
),
|
||||
)
|
||||
eTagsStore.store(userWalletId, ETagsStore.Key.WalletAccounts, eTagValue)
|
||||
pushWalletAccounts(userWalletId, listOf(accountDTO))
|
||||
pushWalletAccounts(listOf(accountDTO), eTagValue)
|
||||
storeWalletAccounts(userWalletId, savedAccountsResponse)
|
||||
}
|
||||
|
||||
|
|
@ -182,6 +180,7 @@ class FetchWalletAccountsErrorHandlerTest {
|
|||
group = UserTokensResponse.GroupType.NONE,
|
||||
sort = UserTokensResponse.SortType.MANUAL,
|
||||
totalAccounts = 1,
|
||||
totalArchivedAccounts = 0,
|
||||
),
|
||||
accounts = listOf(accountDTO),
|
||||
unassignedTokens = emptyList(),
|
||||
|
|
|
|||
|
|
@ -179,6 +179,7 @@ class DefaultMainAccountTokensMigrationTest {
|
|||
group = UserTokensResponse.GroupType.NONE,
|
||||
sort = UserTokensResponse.SortType.MANUAL,
|
||||
totalAccounts = 2,
|
||||
totalArchivedAccounts = 0,
|
||||
),
|
||||
accounts = listOf(mainAccount, selectedAccount),
|
||||
unassignedTokens = emptyList(),
|
||||
|
|
|
|||
|
|
@ -81,6 +81,7 @@ class DefaultWalletAccountsResponseFactoryTest {
|
|||
group = UserTokensResponse.GroupType.NETWORK,
|
||||
sort = UserTokensResponse.SortType.BALANCE,
|
||||
totalAccounts = 0,
|
||||
totalArchivedAccounts = 0,
|
||||
),
|
||||
accounts = emptyList(),
|
||||
unassignedTokens = emptyList(),
|
||||
|
|
@ -135,6 +136,7 @@ class DefaultWalletAccountsResponseFactoryTest {
|
|||
group = defaultResponse.group,
|
||||
sort = defaultResponse.sort,
|
||||
totalAccounts = 1,
|
||||
totalArchivedAccounts = 0,
|
||||
),
|
||||
accounts = listOf(accountsDTO.copy(tokens = listOf(token))),
|
||||
unassignedTokens = emptyList(),
|
||||
|
|
@ -190,6 +192,7 @@ class DefaultWalletAccountsResponseFactoryTest {
|
|||
group = defaultResponse.group,
|
||||
sort = defaultResponse.sort,
|
||||
totalAccounts = 0,
|
||||
totalArchivedAccounts = 0,
|
||||
),
|
||||
accounts = emptyList(),
|
||||
unassignedTokens = emptyList(),
|
||||
|
|
@ -228,6 +231,7 @@ class DefaultWalletAccountsResponseFactoryTest {
|
|||
group = userTokensResponse.group,
|
||||
sort = userTokensResponse.sort,
|
||||
totalAccounts = 1,
|
||||
totalArchivedAccounts = 0,
|
||||
),
|
||||
accounts = listOf(accountsDTO.copy(tokens = userTokensResponse.tokens)),
|
||||
unassignedTokens = emptyList(),
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ class GetWalletAccountsResponseExtTest {
|
|||
group = UserTokensResponse.GroupType.NONE,
|
||||
sort = UserTokensResponse.SortType.MANUAL,
|
||||
totalAccounts = 0,
|
||||
totalArchivedAccounts = 0,
|
||||
),
|
||||
accounts = emptyList(),
|
||||
unassignedTokens = emptyList(),
|
||||
|
|
@ -55,6 +56,7 @@ class GetWalletAccountsResponseExtTest {
|
|||
group = UserTokensResponse.GroupType.NONE,
|
||||
sort = UserTokensResponse.SortType.MANUAL,
|
||||
totalAccounts = 1,
|
||||
totalArchivedAccounts = 0,
|
||||
),
|
||||
accounts = listOf(account),
|
||||
unassignedTokens = emptyList(),
|
||||
|
|
@ -84,6 +86,7 @@ class GetWalletAccountsResponseExtTest {
|
|||
group = UserTokensResponse.GroupType.NONE,
|
||||
sort = UserTokensResponse.SortType.MANUAL,
|
||||
totalAccounts = 2,
|
||||
totalArchivedAccounts = 0,
|
||||
),
|
||||
accounts = listOf(account1, account2, account3),
|
||||
unassignedTokens = emptyList(),
|
||||
|
|
@ -110,6 +113,7 @@ class GetWalletAccountsResponseExtTest {
|
|||
group = UserTokensResponse.GroupType.NONE,
|
||||
sort = UserTokensResponse.SortType.MANUAL,
|
||||
totalAccounts = 0,
|
||||
totalArchivedAccounts = 0,
|
||||
),
|
||||
accounts = emptyList(),
|
||||
unassignedTokens = emptyList(),
|
||||
|
|
@ -141,6 +145,7 @@ class GetWalletAccountsResponseExtTest {
|
|||
group = UserTokensResponse.GroupType.NETWORK,
|
||||
sort = UserTokensResponse.SortType.BALANCE,
|
||||
totalAccounts = 1,
|
||||
totalArchivedAccounts = 0,
|
||||
),
|
||||
accounts = listOf(account),
|
||||
unassignedTokens = listOf(token2),
|
||||
|
|
@ -178,6 +183,7 @@ class GetWalletAccountsResponseExtTest {
|
|||
group = UserTokensResponse.GroupType.NONE,
|
||||
sort = UserTokensResponse.SortType.MANUAL,
|
||||
totalAccounts = 2,
|
||||
totalArchivedAccounts = 0,
|
||||
),
|
||||
accounts = listOf(account1, account2),
|
||||
unassignedTokens = listOf(token1, token2),
|
||||
|
|
@ -217,6 +223,7 @@ class GetWalletAccountsResponseExtTest {
|
|||
group = UserTokensResponse.GroupType.NONE,
|
||||
sort = UserTokensResponse.SortType.MANUAL,
|
||||
totalAccounts = 1,
|
||||
totalArchivedAccounts = 0,
|
||||
),
|
||||
accounts = listOf(account),
|
||||
unassignedTokens = emptyList(),
|
||||
|
|
@ -248,6 +255,7 @@ class GetWalletAccountsResponseExtTest {
|
|||
group = UserTokensResponse.GroupType.NONE,
|
||||
sort = UserTokensResponse.SortType.MANUAL,
|
||||
totalAccounts = 2,
|
||||
totalArchivedAccounts = 0,
|
||||
),
|
||||
accounts = listOf(account),
|
||||
unassignedTokens = listOf(token1, token2),
|
||||
|
|
|
|||
|
|
@ -5,12 +5,8 @@ import com.tangem.data.common.api.safeApiCall
|
|||
import com.tangem.data.common.cache.CacheRegistry
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse
|
||||
import com.tangem.datasource.appcurrency.AppCurrencyResponseStore
|
||||
import com.tangem.datasource.local.appcurrency.AvailableAppCurrenciesStore
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys
|
||||
import com.tangem.datasource.local.preferences.utils.getObject
|
||||
import com.tangem.datasource.local.preferences.utils.getSyncOrNull
|
||||
import com.tangem.datasource.local.preferences.utils.storeObject
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
|
|
@ -24,7 +20,7 @@ import org.joda.time.Duration
|
|||
|
||||
internal class DefaultAppCurrencyRepository(
|
||||
private val tangemTechApi: TangemTechApi,
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
private val appCurrencyResponseStore: AppCurrencyResponseStore,
|
||||
private val availableAppCurrenciesStore: AvailableAppCurrenciesStore,
|
||||
private val cacheRegistry: CacheRegistry,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
|
|
@ -35,15 +31,15 @@ internal class DefaultAppCurrencyRepository(
|
|||
override fun getSelectedAppCurrency(): Flow<AppCurrency> {
|
||||
return channelFlow {
|
||||
launch {
|
||||
appPreferencesStore
|
||||
.getObject<CurrenciesResponse.Currency>(key = PreferencesKeys.SELECTED_APP_CURRENCY_KEY)
|
||||
appCurrencyResponseStore
|
||||
.get()
|
||||
.filterNotNull()
|
||||
.map(appCurrencyConverter::convert)
|
||||
.collect(::send)
|
||||
}
|
||||
|
||||
withContext(dispatchers.io) {
|
||||
if (appPreferencesStore.getSyncOrNull(PreferencesKeys.SELECTED_APP_CURRENCY_KEY) == null) {
|
||||
if (appCurrencyResponseStore.getSyncOrNull() == null) {
|
||||
fetchDefaultAppCurrency()
|
||||
}
|
||||
}
|
||||
|
|
@ -70,17 +66,14 @@ internal class DefaultAppCurrencyRepository(
|
|||
"Unable to find app currency with provided code: $currencyCode"
|
||||
}
|
||||
|
||||
appPreferencesStore.storeObject(
|
||||
key = PreferencesKeys.SELECTED_APP_CURRENCY_KEY,
|
||||
value = currency,
|
||||
)
|
||||
appCurrencyResponseStore.store(currency)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun fetchDefaultAppCurrency(isRefresh: Boolean) {
|
||||
withContext(dispatchers.io) {
|
||||
fetchAvailableCurrenciesIfExpired(isRefresh)
|
||||
val appCurrency = appPreferencesStore.getSyncOrNull(PreferencesKeys.SELECTED_APP_CURRENCY_KEY)
|
||||
val appCurrency = appCurrencyResponseStore.getSyncOrNull()?.code
|
||||
changeAppCurrency(appCurrency ?: DEFAULT_CURRENCY_CODE)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ package com.tangem.data.appcurrency.di
|
|||
import com.tangem.data.appcurrency.DefaultAppCurrencyRepository
|
||||
import com.tangem.data.common.cache.CacheRegistry
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.appcurrency.AppCurrencyResponseStore
|
||||
import com.tangem.datasource.local.appcurrency.AvailableAppCurrenciesStore
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
|
|
@ -21,17 +21,17 @@ internal object AppCurrencyDataModule {
|
|||
@Singleton
|
||||
fun provideAppCurrencyRepository(
|
||||
tangemTechApi: TangemTechApi,
|
||||
appPreferencesStore: AppPreferencesStore,
|
||||
appCurrencyResponseStore: AppCurrencyResponseStore,
|
||||
availableAppCurrenciesStore: AvailableAppCurrenciesStore,
|
||||
cacheRegistry: CacheRegistry,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): AppCurrencyRepository {
|
||||
return DefaultAppCurrencyRepository(
|
||||
tangemTechApi = tangemTechApi,
|
||||
appPreferencesStore = appPreferencesStore,
|
||||
availableAppCurrenciesStore = availableAppCurrenciesStore,
|
||||
cacheRegistry = cacheRegistry,
|
||||
dispatchers = dispatchers,
|
||||
appCurrencyResponseStore = appCurrencyResponseStore,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -27,6 +27,7 @@ dependencies {
|
|||
implementation(projects.domain.tokens.models)
|
||||
implementation(projects.domain.wallets.models)
|
||||
implementation(projects.domain.networks)
|
||||
implementation(projects.domain.walletManager)
|
||||
implementation(projects.domain.wallets)
|
||||
|
||||
/* Libs - SDK */
|
||||
|
|
|
|||
|
|
@ -1,55 +1,45 @@
|
|||
package com.tangem.data.common.currency
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.address.Address
|
||||
import com.tangem.blockchainsdk.utils.fromNetworkId
|
||||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
||||
import com.tangem.domain.models.network.NetworkStatus
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.networks.multi.MultiNetworkStatusProducer
|
||||
import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.domain.wallets.repository.WalletsRepository
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import javax.inject.Inject
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
class UserTokensResponseAddressesEnricher @Inject constructor(
|
||||
private val walletsRepository: WalletsRepository,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val multiNetworkStatusSupplier: MultiNetworkStatusSupplier,
|
||||
private val walletManagersFacade: WalletManagersFacade,
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(userWalletId: UserWalletId, response: UserTokensResponse): UserTokensResponse {
|
||||
val isNotificationsEnabled = walletsRepository.isNotificationsEnabled(userWalletId)
|
||||
|
||||
return withContext(dispatchers.default) {
|
||||
val networksStatuses = if (isNotificationsEnabled) {
|
||||
withTimeoutOrNull(
|
||||
FETCH_TIMEOUT_SECONDS.seconds,
|
||||
{ multiNetworkStatusSupplier.invoke(MultiNetworkStatusProducer.Params(userWalletId)).first() },
|
||||
).orEmpty()
|
||||
val addressByToken = if (isNotificationsEnabled) {
|
||||
response.tokens.associateWith { token ->
|
||||
val blockchain = Blockchain.fromNetworkId(token.networkId) ?: return@associateWith null
|
||||
|
||||
val walletManager = walletManagersFacade.getOrCreateWalletManager(
|
||||
userWalletId = userWalletId,
|
||||
blockchain = blockchain,
|
||||
derivationPath = token.derivationPath,
|
||||
)
|
||||
|
||||
walletManager?.wallet?.addresses?.map(Address::value)
|
||||
}
|
||||
} else {
|
||||
emptySet()
|
||||
emptyMap()
|
||||
}
|
||||
|
||||
val enrichedTokens = response.tokens.map { token ->
|
||||
if (isNotificationsEnabled) {
|
||||
val matchingNetwork = networksStatuses.find { status ->
|
||||
status.network.backendId == token.networkId &&
|
||||
status.network.derivationPath.value == token.derivationPath
|
||||
} ?: return@map token
|
||||
|
||||
val networkAddress = when (matchingNetwork.value) {
|
||||
is NetworkStatus.Verified -> (matchingNetwork.value as NetworkStatus.Verified).address
|
||||
is NetworkStatus.NoAccount -> (matchingNetwork.value as NetworkStatus.NoAccount).address
|
||||
else -> null
|
||||
}
|
||||
|
||||
val addresses = networkAddress
|
||||
?.availableAddresses
|
||||
?.map { it.value }
|
||||
?.toList()
|
||||
.orEmpty()
|
||||
val addresses = addressByToken[token] ?: return@map token
|
||||
|
||||
token.copy(addresses = addresses)
|
||||
} else {
|
||||
|
|
@ -60,8 +50,4 @@ class UserTokensResponseAddressesEnricher @Inject constructor(
|
|||
response.copy(tokens = enrichedTokens, notifyStatus = isNotificationsEnabled)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val FETCH_TIMEOUT_SECONDS = 3
|
||||
}
|
||||
}
|
||||
|
|
@ -2,12 +2,17 @@ package com.tangem.data.common.currency
|
|||
|
||||
import com.tangem.data.common.api.safeApiCall
|
||||
import com.tangem.data.common.tokens.UserTokensBackwardCompatibility
|
||||
import com.tangem.datasource.api.common.response.ApiResponse
|
||||
import com.tangem.datasource.api.common.response.ApiResponseError
|
||||
import com.tangem.datasource.api.common.response.isNetworkError
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.api.tangemTech.converters.WalletIdBodyConverter
|
||||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
||||
import com.tangem.datasource.api.tangemTech.models.WalletType
|
||||
import com.tangem.datasource.local.token.UserTokensResponseStore
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.retryer.Retryer
|
||||
|
|
@ -50,24 +55,26 @@ class UserTokensSaver(
|
|||
response: UserTokensResponse,
|
||||
useEnricher: Boolean = true,
|
||||
onFailSend: () -> Unit = {},
|
||||
) {
|
||||
withContext(dispatchers.default) {
|
||||
val userWallet = userWalletsStore.getSyncOrNull(key = userWalletId)
|
||||
) = withContext(dispatchers.io) {
|
||||
val userWallet = userWalletsStore.getSyncOrNull(key = userWalletId)
|
||||
|
||||
if (userWallet == null) {
|
||||
Timber.e("UserWallet with id $userWalletId not found. Cannot push tokens.")
|
||||
onFailSend()
|
||||
return@withContext
|
||||
}
|
||||
|
||||
if (accountsFeatureToggles.isFeatureEnabled) {
|
||||
val enrichedResponse = response.enrichIf(userWalletId = userWalletId, condition = useEnricher)
|
||||
|
||||
pushNew(userWallet = userWallet, response = enrichedResponse, onFailSend = onFailSend)
|
||||
} else {
|
||||
val enrichedResponse = response.enrichIf(userWalletId = userWalletId, condition = useEnricher).copy(
|
||||
walletName = userWallet?.name,
|
||||
walletName = userWallet.name,
|
||||
walletType = WalletType.from(userWallet),
|
||||
)
|
||||
|
||||
safeApiCall(
|
||||
call = {
|
||||
withContext(dispatchers.io) {
|
||||
tangemTechApi.saveUserTokens(userId = userWalletId.stringValue, userTokens = enrichedResponse)
|
||||
.bind()
|
||||
}
|
||||
},
|
||||
onError = { onFailSend() },
|
||||
)
|
||||
pushLegacy(userWalletId = userWalletId, response = enrichedResponse, onFailSend = onFailSend)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -88,6 +95,39 @@ class UserTokensSaver(
|
|||
)
|
||||
}
|
||||
|
||||
private suspend fun pushLegacy(userWalletId: UserWalletId, response: UserTokensResponse, onFailSend: () -> Unit) {
|
||||
safeApiCall(
|
||||
call = { tangemTechApi.saveUserTokens(userId = userWalletId.stringValue, userTokens = response).bind() },
|
||||
onError = { onFailSend() },
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun pushNew(userWallet: UserWallet, response: UserTokensResponse, onFailSend: () -> Unit) {
|
||||
safeApiCall(
|
||||
call = {
|
||||
val apiResponse = tangemTechApi.saveTokens(
|
||||
userId = userWallet.walletId.stringValue,
|
||||
userTokens = response,
|
||||
)
|
||||
|
||||
val isWalletNotFound = apiResponse is ApiResponse.Error &&
|
||||
apiResponse.cause.isNetworkError(ApiResponseError.HttpException.Code.NOT_FOUND)
|
||||
|
||||
if (isWalletNotFound) {
|
||||
tangemTechApi.createWallet(body = WalletIdBodyConverter.convert(userWallet)).bind()
|
||||
|
||||
tangemTechApi.saveTokens(
|
||||
userId = userWallet.walletId.stringValue,
|
||||
userTokens = response,
|
||||
).bind()
|
||||
} else {
|
||||
apiResponse.bind()
|
||||
}
|
||||
},
|
||||
onError = { onFailSend() },
|
||||
)
|
||||
}
|
||||
|
||||
private fun UserTokensResponse.applyCompatibility(): UserTokensResponse {
|
||||
return userTokensBackwardCompatibility.applyCompatibilityAndGetUpdated(userTokensResponse = this)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import com.tangem.datasource.local.token.UserTokensResponseStore
|
|||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
|
||||
import com.tangem.domain.demo.models.DemoConfig
|
||||
import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.domain.wallets.repository.WalletsRepository
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.retryer.RetryerPool
|
||||
|
|
@ -54,12 +54,12 @@ internal object DataCommonModule {
|
|||
@Singleton
|
||||
fun provideUserTokensEncricher(
|
||||
walletsRepository: WalletsRepository,
|
||||
multiNetworkStatusSupplier: MultiNetworkStatusSupplier,
|
||||
walletManagersFacade: WalletManagersFacade,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): UserTokensResponseAddressesEnricher {
|
||||
return UserTokensResponseAddressesEnricher(
|
||||
walletsRepository = walletsRepository,
|
||||
multiNetworkStatusSupplier = multiNetworkStatusSupplier,
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,92 +1,68 @@
|
|||
package com.tangem.data.common.currency
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.Wallet
|
||||
import com.tangem.blockchain.common.WalletManager
|
||||
import com.tangem.blockchain.common.address.Address
|
||||
import com.tangem.blockchain.common.address.AddressType
|
||||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
||||
import com.tangem.domain.models.StatusSource
|
||||
import com.tangem.domain.models.network.NetworkAddress
|
||||
import com.tangem.domain.models.network.NetworkStatus
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.domain.wallets.repository.WalletsRepository
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import io.mockk.clearAllMocks
|
||||
import io.mockk.clearMocks
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
class UserTokensResponseAddressesEnricherTest {
|
||||
|
||||
private lateinit var walletsRepository: WalletsRepository
|
||||
private val dispatchers: CoroutineDispatcherProvider = TestingCoroutineDispatcherProvider()
|
||||
private lateinit var multiNetworkStatusSupplier: MultiNetworkStatusSupplier
|
||||
private lateinit var enricher: UserTokensResponseAddressesEnricher
|
||||
private val walletsRepository: WalletsRepository = mockk()
|
||||
private val walletManagersFacade: WalletManagersFacade = mockk()
|
||||
private val enricher: UserTokensResponseAddressesEnricher = UserTokensResponseAddressesEnricher(
|
||||
walletsRepository = walletsRepository,
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
)
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
walletsRepository = mockk()
|
||||
multiNetworkStatusSupplier = mockk()
|
||||
private val userWalletId = UserWalletId("1234567890abcdef")
|
||||
|
||||
enricher = UserTokensResponseAddressesEnricher(
|
||||
walletsRepository = walletsRepository,
|
||||
dispatchers = dispatchers,
|
||||
multiNetworkStatusSupplier = multiNetworkStatusSupplier,
|
||||
)
|
||||
}
|
||||
|
||||
@After
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
clearAllMocks()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN notifications are disabled globally WHEN invoke THEN return original response`() = runTest {
|
||||
// GIVEN
|
||||
val userWalletId = UserWalletId("1234567890abcdef")
|
||||
val token = createToken()
|
||||
val response = createUserTokensResponse(tokens = listOf(token))
|
||||
|
||||
// WHEN
|
||||
val result = enricher(userWalletId, response)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isEqualTo(response)
|
||||
clearMocks(walletsRepository, walletManagersFacade)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN notifications are disabled for wallet WHEN invoke THEN return response with empty addresses`() =
|
||||
runTest {
|
||||
// GIVEN
|
||||
val userWalletId = UserWalletId("1234567890abcdef")
|
||||
val token = createToken()
|
||||
val response = createUserTokensResponse(tokens = listOf(token))
|
||||
val walletManager = mockk<WalletManager> {
|
||||
val wallet = mockk<Wallet> {
|
||||
every { addresses } returns setOf(
|
||||
Address(value = "0x12345", type = AddressType.Default),
|
||||
)
|
||||
}
|
||||
|
||||
every { this@mockk.wallet } returns wallet
|
||||
}
|
||||
|
||||
coEvery { walletsRepository.isNotificationsEnabled(userWalletId) } returns false
|
||||
|
||||
coEvery {
|
||||
multiNetworkStatusSupplier.invoke(any())
|
||||
} returns flowOf(
|
||||
setOf(
|
||||
NetworkStatus(
|
||||
network = mockk {
|
||||
every { backendId } returns "ethereum"
|
||||
every { derivationPath.value } returns "m/44'/60'/0'/0/0"
|
||||
},
|
||||
value = NetworkStatus.Verified(
|
||||
address = mockk {
|
||||
every { availableAddresses } returns emptySet()
|
||||
},
|
||||
amounts = emptyMap(),
|
||||
pendingTransactions = emptyMap(),
|
||||
yieldSupplyStatuses = emptyMap(),
|
||||
source = StatusSource.ACTUAL,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
walletManagersFacade.getOrCreateWalletManager(
|
||||
userWalletId = userWalletId,
|
||||
blockchain = Blockchain.Ethereum,
|
||||
derivationPath = token.derivationPath,
|
||||
)
|
||||
} returns walletManager
|
||||
|
||||
// WHEN
|
||||
val result = enricher(userWalletId, response)
|
||||
|
|
@ -100,75 +76,52 @@ class UserTokensResponseAddressesEnricherTest {
|
|||
fun `GIVEN notifications are enabled and addresses available WHEN invoke THEN return enriched response`() =
|
||||
runTest {
|
||||
// GIVEN
|
||||
val userWalletId = UserWalletId("1234567890abcdef")
|
||||
val token = createToken()
|
||||
val response = createUserTokensResponse(tokens = listOf(token))
|
||||
val addresses = listOf("0x123", "0x456")
|
||||
val addresses = setOf(
|
||||
Address(value = "0x123", type = AddressType.Default),
|
||||
Address(value = "0x456", type = AddressType.Legacy),
|
||||
)
|
||||
|
||||
val walletManager = mockk<WalletManager> {
|
||||
val wallet = mockk<Wallet> {
|
||||
every { this@mockk.addresses } returns addresses
|
||||
}
|
||||
|
||||
every { this@mockk.wallet } returns wallet
|
||||
}
|
||||
|
||||
coEvery { walletsRepository.isNotificationsEnabled(userWalletId) } returns true
|
||||
coEvery {
|
||||
multiNetworkStatusSupplier.invoke(any())
|
||||
} returns flowOf(
|
||||
setOf(
|
||||
NetworkStatus(
|
||||
network = mockk {
|
||||
every { backendId } returns "ethereum"
|
||||
every { derivationPath.value } returns "m/44'/60'/0'/0/0"
|
||||
},
|
||||
value = NetworkStatus.Verified(
|
||||
address = mockk {
|
||||
every { availableAddresses } returns addresses.map { address ->
|
||||
mockk<NetworkAddress.Address> {
|
||||
every { value } returns address
|
||||
}
|
||||
}.toSet()
|
||||
},
|
||||
amounts = emptyMap(),
|
||||
pendingTransactions = emptyMap(),
|
||||
yieldSupplyStatuses = emptyMap(),
|
||||
source = StatusSource.ACTUAL,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
walletManagersFacade.getOrCreateWalletManager(
|
||||
userWalletId = userWalletId,
|
||||
blockchain = Blockchain.Ethereum,
|
||||
derivationPath = token.derivationPath,
|
||||
)
|
||||
} returns walletManager
|
||||
|
||||
// WHEN
|
||||
val result = enricher(userWalletId, response)
|
||||
|
||||
// THEN
|
||||
assertThat(result.tokens).hasSize(1)
|
||||
assertThat(result.tokens[0].addresses).containsExactlyElementsIn(addresses)
|
||||
assertThat(result.tokens[0].addresses).containsExactlyElementsIn(addresses.map { it.value })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN notifications are enabled but no matching network WHEN invoke THEN return original token`() = runTest {
|
||||
// GIVEN
|
||||
val userWalletId = UserWalletId("1234567890abcdef")
|
||||
val token = createToken()
|
||||
val response = createUserTokensResponse(tokens = listOf(token))
|
||||
|
||||
coEvery { walletsRepository.isNotificationsEnabled(userWalletId) } returns true
|
||||
coEvery {
|
||||
multiNetworkStatusSupplier.invoke(any())
|
||||
} returns flowOf(
|
||||
setOf(
|
||||
NetworkStatus(
|
||||
network = mockk {
|
||||
every { backendId } returns "bitcoin"
|
||||
every { derivationPath.value } returns "m/44'/0'/0'/0/0"
|
||||
},
|
||||
value = NetworkStatus.Verified(
|
||||
address = mockk {
|
||||
every { availableAddresses } returns emptySet()
|
||||
},
|
||||
amounts = emptyMap(),
|
||||
pendingTransactions = emptyMap(),
|
||||
yieldSupplyStatuses = emptyMap(),
|
||||
source = StatusSource.ACTUAL,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
walletManagersFacade.getOrCreateWalletManager(
|
||||
userWalletId = userWalletId,
|
||||
blockchain = Blockchain.Ethereum,
|
||||
derivationPath = token.derivationPath,
|
||||
)
|
||||
} returns null
|
||||
|
||||
// WHEN
|
||||
val result = enricher(userWalletId, response)
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ class UserTokensSaverTest {
|
|||
}
|
||||
|
||||
coVerify(inverse = true) {
|
||||
tangemTechApi.saveUserTokens(any(), any())
|
||||
tangemTechApi.saveTokens(any(), any())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -108,7 +108,8 @@ class UserTokensSaverTest {
|
|||
|
||||
coEvery { userWalletsStore.getSyncOrNull(userWalletId) } returns userWallet
|
||||
coEvery { enricher(userWalletId, response) } returns enrichedResponse
|
||||
coEvery { tangemTechApi.saveUserTokens(any(), any()) } returns ApiResponse.Error(error) as ApiResponse<Unit>
|
||||
coEvery { tangemTechApi.saveTokens(any(), any()) } returns ApiResponse.Error(error) as ApiResponse<Unit>
|
||||
coEvery { tangemTechApi.createWallet(body = any()) } returns ApiResponse.Error(error) as ApiResponse<Unit>
|
||||
|
||||
// WHEN
|
||||
userTokensSaver.push(
|
||||
|
|
@ -120,7 +121,7 @@ class UserTokensSaverTest {
|
|||
// THEN
|
||||
coVerifyOrder {
|
||||
enricher(userWalletId, response)
|
||||
tangemTechApi.saveUserTokens(userWalletId.stringValue, enrichedResponse)
|
||||
tangemTechApi.saveTokens(userWalletId.stringValue, enrichedResponse)
|
||||
}
|
||||
|
||||
assert(onFailSendCalled) { "onFailSend callback should be called when API call fails" }
|
||||
|
|
@ -155,7 +156,7 @@ class UserTokensSaverTest {
|
|||
coEvery { userWalletsStore.getSyncOrNull(userWalletId) } returns userWallet
|
||||
coEvery { enricher(userWalletId, response) } returns enrichedResponse
|
||||
coEvery {
|
||||
tangemTechApi.saveUserTokens(userWalletId.stringValue, enrichedResponse)
|
||||
tangemTechApi.saveTokens(userWalletId.stringValue, enrichedResponse)
|
||||
} returns ApiResponse.Success(Unit)
|
||||
|
||||
// WHEN
|
||||
|
|
@ -164,7 +165,7 @@ class UserTokensSaverTest {
|
|||
// THEN
|
||||
coVerifyOrder {
|
||||
enricher(userWalletId, response)
|
||||
tangemTechApi.saveUserTokens(userWalletId.stringValue, enrichedResponse)
|
||||
tangemTechApi.saveTokens(userWalletId.stringValue, enrichedResponse)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
<?xml version="1.0" ?>
|
||||
<SmellBaseline>
|
||||
<ManuallySuppressedIssues/>
|
||||
<CurrentIssues>
|
||||
<ID>BooleanPropertyNaming:DefaultFeedbackRepository.kt$DefaultFeedbackRepository$private val useNewUserWalletsRepository: Boolean</ID>
|
||||
</CurrentIssues>
|
||||
</SmellBaseline>
|
||||
|
|
@ -26,7 +26,7 @@ import java.io.File
|
|||
*
|
||||
* @property appLogsStore app logs store
|
||||
* @property userWalletsListManager user wallets list manager
|
||||
* @property useNewUserWalletsRepository flag to use new user wallets repository
|
||||
* @property shouldUseNewUserWalletsRepository flag to use new user wallets repository
|
||||
* @property userWalletsListRepository user wallets repository
|
||||
* @property walletManagersStore wallet managers store
|
||||
* @property emailSender email sender
|
||||
|
|
@ -37,7 +37,7 @@ import java.io.File
|
|||
@Suppress("LongParameterList")
|
||||
internal class DefaultFeedbackRepository(
|
||||
private val appLogsStore: AppLogsStore,
|
||||
private val useNewUserWalletsRepository: Boolean,
|
||||
private val shouldUseNewUserWalletsRepository: Boolean,
|
||||
private val userWalletsListRepository: UserWalletsListRepository,
|
||||
private val userWalletsListManager: UserWalletsListManager,
|
||||
private val walletManagersStore: WalletManagersStore,
|
||||
|
|
@ -126,7 +126,7 @@ internal class DefaultFeedbackRepository(
|
|||
}
|
||||
|
||||
private suspend fun getUserWalletById(userWalletId: UserWalletId): UserWallet? {
|
||||
return if (useNewUserWalletsRepository) {
|
||||
return if (shouldUseNewUserWalletsRepository) {
|
||||
userWalletsListRepository.userWalletsSync().find { it.walletId == userWalletId }
|
||||
} else {
|
||||
userWalletsListManager.userWalletsSync.find { it.walletId == userWalletId }
|
||||
|
|
@ -134,7 +134,7 @@ internal class DefaultFeedbackRepository(
|
|||
}
|
||||
|
||||
private fun totalUserWallets(): Int {
|
||||
return if (useNewUserWalletsRepository) {
|
||||
return if (shouldUseNewUserWalletsRepository) {
|
||||
userWalletsListRepository.userWallets.value?.size ?: 0
|
||||
} else {
|
||||
userWalletsListManager.walletsCount
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ internal object FeedbackModule {
|
|||
emailSender = emailSender,
|
||||
appVersionProvider = appVersionProvider,
|
||||
userWalletsListRepository = userWalletsListRepository,
|
||||
useNewUserWalletsRepository = hotWalletFeatureToggles.isHotWalletEnabled,
|
||||
shouldUseNewUserWalletsRepository = hotWalletFeatureToggles.isHotWalletEnabled,
|
||||
getSelectedWalletUseCase = getSelectedWalletUseCase,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
package com.tangem.data.hotwallet
|
||||
|
||||
import android.os.Build
|
||||
import androidx.annotation.ChecksSdkIntAtLeast
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys
|
||||
import com.tangem.datasource.local.preferences.utils.getObjectMap
|
||||
|
|
@ -12,6 +14,13 @@ internal class DefaultHotWalletRepository(
|
|||
private val appPreferencesStore: AppPreferencesStore,
|
||||
) : HotWalletRepository {
|
||||
|
||||
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.Q)
|
||||
override fun isWalletCreationSupported(): Boolean {
|
||||
return BuildConfig.DEBUG || Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
|
||||
}
|
||||
|
||||
override fun getLeastSupportedAndroidVersionName(): String = "Android 10"
|
||||
|
||||
override fun accessCodeSkipped(userWalletId: UserWalletId): Flow<Boolean> = appPreferencesStore
|
||||
.getObjectMap<Boolean>(PreferencesKeys.ACCESS_CODE_SKIPPED_STATES_KEY)
|
||||
.map { it[userWalletId.stringValue] == true }
|
||||
|
|
|
|||
|
|
@ -1,15 +1,16 @@
|
|||
package com.tangem.data.news.repository
|
||||
|
||||
import com.tangem.datasource.api.common.response.ApiResponse
|
||||
import com.tangem.datasource.api.common.response.ApiResponseError
|
||||
import com.tangem.datasource.api.common.response.getOrThrow
|
||||
import com.tangem.datasource.api.news.NewsApi
|
||||
import com.tangem.datasource.api.news.models.response.NewsTrendingResponse
|
||||
import com.tangem.datasource.local.news.details.NewsDetailsStore
|
||||
import com.tangem.datasource.local.news.trending.TrendingNewsStore
|
||||
import com.tangem.domain.models.news.*
|
||||
import com.tangem.domain.news.model.NewsListBatchFlow
|
||||
import com.tangem.domain.news.model.NewsListBatchingContext
|
||||
import com.tangem.domain.news.model.NewsListConfig
|
||||
import com.tangem.domain.models.news.ArticleCategory
|
||||
import com.tangem.domain.models.news.DetailedArticle
|
||||
import com.tangem.domain.models.news.ShortArticle
|
||||
import com.tangem.domain.news.repository.NewsRepository
|
||||
import com.tangem.pagination.BatchFetchResult
|
||||
import com.tangem.pagination.BatchListSource
|
||||
|
|
@ -18,20 +19,19 @@ import com.tangem.pagination.fetcher.BatchFetcher
|
|||
import com.tangem.pagination.toBatchFlow
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.runSuspendCatching
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.collections.orEmpty
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Implementation of [NewsRepository].
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class DefaultNewsRepository @Inject constructor(
|
||||
internal class DefaultNewsRepository(
|
||||
private val newsApi: NewsApi,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val newsDetailsStore: NewsDetailsStore,
|
||||
|
|
@ -68,26 +68,26 @@ internal class DefaultNewsRepository @Inject constructor(
|
|||
fetchDetailedArticlesInternal(newsIds = newsIds, language = language)
|
||||
}
|
||||
|
||||
override suspend fun getTrendingNews(limit: Int, language: String?): List<ShortArticle> {
|
||||
return fetchAndStoreTrendingNews(limit = limit, language = language)
|
||||
}
|
||||
|
||||
override fun observeTrendingNews(): Flow<List<ShortArticle>> {
|
||||
return trendingNewsStore.get(TRENDING_NEWS_KEY)
|
||||
}
|
||||
|
||||
override suspend fun refreshTrendingNews(limit: Int, language: String?) {
|
||||
override suspend fun fetchTrendingNews(limit: Int, language: String?) {
|
||||
fetchAndStoreTrendingNews(limit = limit, language = language)
|
||||
}
|
||||
|
||||
override fun observeTrendingNews(): Flow<TrendingNews> {
|
||||
return trendingNewsStore.get(TRENDING_NEWS_KEY)
|
||||
}
|
||||
|
||||
override suspend fun updateTrendingNewsViewed(articleIds: Collection<Int>, viewed: Boolean) {
|
||||
if (articleIds.isEmpty()) return
|
||||
|
||||
val current = trendingNewsStore.getSyncOrNull(TRENDING_NEWS_KEY).orEmpty()
|
||||
if (current.isEmpty()) return
|
||||
val currentResult = trendingNewsStore.getSyncOrNull(TRENDING_NEWS_KEY) ?: return
|
||||
val currentArticles = when (currentResult) {
|
||||
is TrendingNews.Data -> currentResult.articles
|
||||
is TrendingNews.Error -> return
|
||||
}
|
||||
if (currentArticles.isEmpty()) return
|
||||
|
||||
val ids = articleIds.toSet()
|
||||
val updated = current.map { article ->
|
||||
val updated = currentArticles.map { article ->
|
||||
if (article.id in ids) {
|
||||
article.copy(viewed = viewed)
|
||||
} else {
|
||||
|
|
@ -95,7 +95,7 @@ internal class DefaultNewsRepository @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
trendingNewsStore.store(TRENDING_NEWS_KEY, updated)
|
||||
trendingNewsStore.store(TRENDING_NEWS_KEY, TrendingNews.Data(updated))
|
||||
}
|
||||
|
||||
override suspend fun getCategories(): List<ArticleCategory> {
|
||||
|
|
@ -138,16 +138,46 @@ internal class DefaultNewsRepository @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchAndStoreTrendingNews(limit: Int, language: String?): List<ShortArticle> {
|
||||
private suspend fun fetchAndStoreTrendingNews(limit: Int, language: String?) {
|
||||
return withContext(dispatchers.io) {
|
||||
val response = newsApi.getTrendingNews(limit = limit, language = language).getOrThrow()
|
||||
val freshArticles = response.items.map { it.toDomainShortArticle() }
|
||||
val currentArticles = trendingNewsStore.getSyncOrNull(TRENDING_NEWS_KEY).orEmpty()
|
||||
val merged = mergeTrendingArticles(current = currentArticles, fresh = freshArticles).take(limit)
|
||||
|
||||
trendingNewsStore.store(TRENDING_NEWS_KEY, merged)
|
||||
|
||||
merged
|
||||
val apiResponse = newsApi.getTrendingNews(limit = limit, language = language)
|
||||
when (val result = apiResponse) {
|
||||
is ApiResponse.Error -> {
|
||||
Timber.e(
|
||||
result.cause.cause,
|
||||
"Trending news fetch failed cause: ${
|
||||
when (val error = result.cause) {
|
||||
is ApiResponseError.HttpException -> error.code
|
||||
is ApiResponseError.NetworkException -> "NetworkException"
|
||||
is ApiResponseError.TimeoutException -> "TimeoutException"
|
||||
is ApiResponseError.UnknownException -> "UnknownException"
|
||||
}
|
||||
}",
|
||||
)
|
||||
trendingNewsStore.clear()
|
||||
trendingNewsStore.store(
|
||||
key = TRENDING_NEWS_KEY,
|
||||
value = TrendingNews.Error(
|
||||
NewsError.Unknown(
|
||||
message = result.cause.message,
|
||||
code = null,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
is ApiResponse.Success<NewsTrendingResponse> -> {
|
||||
val freshArticles = result.data.items.map { it.toDomainShortArticle() }
|
||||
val cachedArticles = trendingNewsStore.getSyncOrNull(TRENDING_NEWS_KEY)
|
||||
val currentArticles = when (cachedArticles) {
|
||||
is TrendingNews.Data -> cachedArticles.articles
|
||||
is TrendingNews.Error -> emptyList()
|
||||
null -> emptyList()
|
||||
}
|
||||
val merged = mergeTrendingArticles(current = currentArticles, fresh = freshArticles).take(limit)
|
||||
trendingNewsStore.store(TRENDING_NEWS_KEY, TrendingNews.Data(merged))
|
||||
TrendingNews.Data(merged)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,10 @@ android {
|
|||
namespace = "com.tangem.data.nft"
|
||||
}
|
||||
|
||||
tasks.withType<Test>().configureEach {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
/** Project - Data */
|
||||
|
|
@ -53,4 +57,8 @@ dependencies {
|
|||
/** DI */
|
||||
implementation(deps.hilt.android)
|
||||
kapt(deps.hilt.kapt)
|
||||
|
||||
testImplementation(projects.test.core)
|
||||
testImplementation(projects.common.test)
|
||||
testRuntimeOnly(deps.test.junit5.engine)
|
||||
}
|
||||
|
|
@ -3,7 +3,6 @@
|
|||
<ManuallySuppressedIssues/>
|
||||
<CurrentIssues>
|
||||
<ID>MultilineLambdaItParameter:DefaultNFTRepository.kt$DefaultNFTRepository${ NFTCollections( network = network, content = NFTCollections.Content.Collections( collections = it ?.map { collection -> nftSdkCollectionConverter.convert(network to collection) } ?.filter { it.id !is NFTCollection.Identifier.Unknown }, source = StatusSource.CACHE, ), ) }</ID>
|
||||
<ID>MultilineLambdaItParameter:DefaultNFTRepository.kt$DefaultNFTRepository${ getNFTPersistenceStore(userWalletId, it).clear() getNFTRuntimeStore(userWalletId, it).clear() }</ID>
|
||||
<ID>MultilineLambdaItParameter:DefaultNFTRepository.kt$DefaultNFTRepository${ if (it !is UnsupportedOperationException) { saveFailedStateInRuntime( userWalletId = userWalletId, network = network, error = it, ) } }</ID>
|
||||
<ID>MultilineLambdaItParameter:DefaultNFTRepository.kt$DefaultNFTRepository${ if (it.id == collectionId) { it.changeAssetsStatusSource(source) } else { it } }</ID>
|
||||
<ID>MultilineLambdaItParameter:DefaultNFTRepository.kt$DefaultNFTRepository${ if (it.identifier == sdkCollectionId) { it.copy(assets = assets) } else { it } }</ID>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.data.nft
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.Resources
|
||||
import arrow.core.Either
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
|
|
@ -26,15 +27,19 @@ import com.tangem.domain.nft.models.NFTCollection
|
|||
import com.tangem.domain.nft.models.NFTCollections
|
||||
import com.tangem.domain.nft.models.NFTSalePrice
|
||||
import com.tangem.domain.nft.repository.NFTRepository
|
||||
import com.tangem.domain.nft.utils.NFTCleaner
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.JobHolder
|
||||
import com.tangem.utils.coroutines.runSuspendCatching
|
||||
import com.tangem.utils.coroutines.saveIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.joinAll
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import javax.inject.Inject
|
||||
import com.tangem.blockchain.nft.models.NFTAsset as SdkNFTAsset
|
||||
|
|
@ -49,9 +54,10 @@ internal class DefaultNFTRepository @Inject constructor(
|
|||
private val userWalletsStore: UserWalletsStore,
|
||||
private val networkFactory: NetworkFactory,
|
||||
private val excludedBlockchains: ExcludedBlockchains,
|
||||
resources: Resources,
|
||||
) : NFTRepository {
|
||||
@ApplicationContext private val context: Context,
|
||||
) : NFTRepository, NFTCleaner {
|
||||
|
||||
private val resources: Resources by lazy { context.resources }
|
||||
private val networkJobs = ConcurrentHashMap<Network, JobHolder>()
|
||||
private val collectionJobs = ConcurrentHashMap<NFTCollection.Identifier, JobHolder>()
|
||||
private val cryptoCurrencyFactory = CryptoCurrencyFactory(excludedBlockchains)
|
||||
|
|
@ -218,10 +224,22 @@ internal class DefaultNFTRepository @Inject constructor(
|
|||
assetIdentifier = assetIdConverter.convertBack(assetIdentifier),
|
||||
)
|
||||
|
||||
override suspend fun clearCache(userWalletId: UserWalletId, networks: List<Network>) {
|
||||
networks.forEach {
|
||||
getNFTPersistenceStore(userWalletId, it).clear()
|
||||
getNFTRuntimeStore(userWalletId, it).clear()
|
||||
// NFTCleaner implementation
|
||||
override suspend fun invoke(userWalletId: UserWalletId, networks: Set<Network>) {
|
||||
if (networks.isEmpty()) {
|
||||
Timber.d("No networks to clear for wallet: $userWalletId")
|
||||
return
|
||||
}
|
||||
|
||||
networks.forEach { network ->
|
||||
runSuspendCatching {
|
||||
getNFTPersistenceStore(userWalletId = userWalletId, network = network).clear()
|
||||
// FIXME: nftRuntimeStore is created with only network, so clearing it may affect other wallets
|
||||
// nftRuntimeStoreFactory.provide(network = network).clear()
|
||||
}
|
||||
.onFailure { throwable ->
|
||||
Timber.e(throwable, "Failed to clear NFT data for network $network for wallet: $userWalletId")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,45 +1,23 @@
|
|||
package com.tangem.data.nft.di
|
||||
|
||||
import android.content.Context
|
||||
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
|
||||
import com.tangem.data.common.network.NetworkFactory
|
||||
import com.tangem.data.nft.DefaultNFTRepository
|
||||
import com.tangem.datasource.local.nft.NFTPersistenceStoreFactory
|
||||
import com.tangem.datasource.local.nft.NFTRuntimeStoreFactory
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.nft.repository.NFTRepository
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.domain.nft.utils.NFTCleaner
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object NFTDataModule {
|
||||
internal interface NFTDataModule {
|
||||
|
||||
@Provides
|
||||
@Binds
|
||||
@Singleton
|
||||
fun provideNFTRepository(
|
||||
@ApplicationContext context: Context,
|
||||
nftPersistenceStoreFactory: NFTPersistenceStoreFactory,
|
||||
nftRuntimeStoreFactory: NFTRuntimeStoreFactory,
|
||||
walletManagersFacade: WalletManagersFacade,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
excludedBlockchains: ExcludedBlockchains,
|
||||
userWalletsStore: UserWalletsStore,
|
||||
networkFactory: NetworkFactory,
|
||||
): NFTRepository = DefaultNFTRepository(
|
||||
nftPersistenceStoreFactory = nftPersistenceStoreFactory,
|
||||
nftRuntimeStoreFactory = nftRuntimeStoreFactory,
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
dispatchers = dispatchers,
|
||||
excludedBlockchains = excludedBlockchains,
|
||||
userWalletsStore = userWalletsStore,
|
||||
networkFactory = networkFactory,
|
||||
resources = context.resources,
|
||||
)
|
||||
fun bindNFTRepository(defaultNFTRepository: DefaultNFTRepository): NFTRepository
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindNFTCleaner(defaultNFTRepository: DefaultNFTRepository): NFTCleaner
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
package com.tangem.data.nft
|
||||
|
||||
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
|
||||
import com.tangem.datasource.local.nft.NFTPersistenceStore
|
||||
import com.tangem.datasource.local.nft.NFTPersistenceStoreFactory
|
||||
import com.tangem.datasource.local.nft.NFTRuntimeStore
|
||||
import com.tangem.datasource.local.nft.NFTRuntimeStoreFactory
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import io.mockk.*
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
class NFTCleanerTest {
|
||||
|
||||
private val nftPersistenceStoreFactory: NFTPersistenceStoreFactory = mockk()
|
||||
private val nftRuntimeStoreFactory: NFTRuntimeStoreFactory = mockk()
|
||||
|
||||
private val nftCleaner = DefaultNFTRepository(
|
||||
nftPersistenceStoreFactory = nftPersistenceStoreFactory,
|
||||
nftRuntimeStoreFactory = nftRuntimeStoreFactory,
|
||||
walletManagersFacade = mockk(),
|
||||
dispatchers = mockk(),
|
||||
userWalletsStore = mockk(),
|
||||
networkFactory = mockk(),
|
||||
excludedBlockchains = mockk(),
|
||||
context = mockk(),
|
||||
)
|
||||
|
||||
private val userWalletId = UserWalletId("011")
|
||||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
clearMocks(nftPersistenceStoreFactory, nftRuntimeStoreFactory)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should call invoke with multiple networks`() = runTest {
|
||||
// Arrange
|
||||
val mockCryptoCurrencyFactory = MockCryptoCurrencyFactory()
|
||||
val networks = mockCryptoCurrencyFactory.ethereumAndStellar.map(CryptoCurrency.Coin::network)
|
||||
val persistenceByNetwork = networks.associateWith { mockk<NFTPersistenceStore>(relaxUnitFun = true) }
|
||||
val runtimeByNetwork = networks.associateWith { mockk<NFTRuntimeStore>(relaxUnitFun = true) }
|
||||
|
||||
networks.forEach { network ->
|
||||
every { nftPersistenceStoreFactory.provide(userWalletId, network) } returns persistenceByNetwork[network]!!
|
||||
every { nftRuntimeStoreFactory.provide(network) } returns runtimeByNetwork[network]!!
|
||||
}
|
||||
|
||||
// Act
|
||||
nftCleaner.invoke(userWalletId = userWalletId, networks = networks.toSet())
|
||||
|
||||
// Assert
|
||||
coVerifyOrder {
|
||||
networks.forEach { network ->
|
||||
nftPersistenceStoreFactory.provide(userWalletId, network)
|
||||
persistenceByNetwork[network]!!.clear()
|
||||
// nftRuntimeStoreFactory.provide(network)
|
||||
// runtimeByNetwork[network]!!.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should handle empty networks set`() = runTest {
|
||||
// Act
|
||||
nftCleaner.invoke(userWalletId, emptySet())
|
||||
|
||||
// Assert
|
||||
coVerify(inverse = true) {
|
||||
nftPersistenceStoreFactory.provide(userWalletId = any(), network = any())
|
||||
nftRuntimeStoreFactory.provide(network = any())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -11,13 +11,10 @@ import com.tangem.data.onramp.converters.HotCryptoCurrencyConverter
|
|||
import com.tangem.datasource.api.common.response.ApiResponseError
|
||||
import com.tangem.datasource.api.common.response.getOrThrow
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse
|
||||
import com.tangem.datasource.api.tangemTech.models.HotCryptoResponse
|
||||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
||||
import com.tangem.datasource.appcurrency.AppCurrencyResponseStore
|
||||
import com.tangem.datasource.exchangeservice.hotcrypto.HotCryptoResponseStore
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys
|
||||
import com.tangem.datasource.local.preferences.utils.getObject
|
||||
import com.tangem.datasource.local.token.UserTokensResponseStore
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.card.common.extensions.canHandleBlockchain
|
||||
|
|
@ -39,13 +36,13 @@ import timber.log.Timber
|
|||
/**
|
||||
* Default implementation of [HotCryptoRepository]
|
||||
*
|
||||
* @property excludedBlockchains excluded blockchains
|
||||
* @property hotCryptoResponseStore store of `HotCryptoResponse`
|
||||
* @property userWalletsStore store of `UserWallet`
|
||||
* @property tangemTechApi tangem tech api
|
||||
* @property appPreferencesStore app preferences store
|
||||
* @property dispatchers dispatchers
|
||||
* @property analyticsEventHandler analytics event handler
|
||||
* @property excludedBlockchains excluded blockchains
|
||||
* @property hotCryptoResponseStore store of `HotCryptoResponse`
|
||||
* @property userWalletsStore store of `UserWallet`
|
||||
* @property tangemTechApi tangem tech api
|
||||
* @property appCurrencyResponseStore store of current app currency
|
||||
* @property dispatchers dispatchers
|
||||
* @property analyticsEventHandler analytics event handler
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
|
|
@ -56,7 +53,7 @@ internal class DefaultHotCryptoRepository(
|
|||
private val hotCryptoResponseStore: HotCryptoResponseStore,
|
||||
private val userWalletsStore: UserWalletsStore,
|
||||
private val tangemTechApi: TangemTechApi,
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
private val appCurrencyResponseStore: AppCurrencyResponseStore,
|
||||
private val userTokensResponseStore: UserTokensResponseStore,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
|
|
@ -112,8 +109,8 @@ internal class DefaultHotCryptoRepository(
|
|||
}
|
||||
|
||||
private fun getHotCryptoFlow(): Flow<HotCryptoResponse?> {
|
||||
return appPreferencesStore
|
||||
.getObject<CurrenciesResponse.Currency>(key = PreferencesKeys.SELECTED_APP_CURRENCY_KEY)
|
||||
return appCurrencyResponseStore
|
||||
.get()
|
||||
.map { it?.id ?: "usd" }
|
||||
.distinctUntilChanged()
|
||||
.map { getHotCrypto(appCurrencyId = it).getOrNull() }
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.data.onramp
|
||||
|
||||
import com.tangem.data.onramp.converters.TransactionConverter
|
||||
import com.tangem.data.onramp.models.OnrampTerminalTransactionDTO
|
||||
import com.tangem.data.onramp.models.OnrampTransactionDTO
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys
|
||||
|
|
@ -15,7 +16,9 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
|||
import com.tangem.utils.extensions.addOrReplace
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
internal class DefaultOnrampTransactionRepository(
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
|
|
@ -47,6 +50,7 @@ internal class DefaultOnrampTransactionRepository(
|
|||
override fun getAllTransactions(): Flow<List<OnrampTransaction>> {
|
||||
return appPreferencesStore
|
||||
.getObjectSet<OnrampTransactionDTO>(PreferencesKeys.ONRAMP_TRANSACTIONS_STATUSES_KEY)
|
||||
.onStart { cleanupExpiredTerminalTransactions() }
|
||||
.map { transactions -> transactions.map(transactionConverter::convert) }
|
||||
}
|
||||
|
||||
|
|
@ -63,6 +67,7 @@ internal class DefaultOnrampTransactionRepository(
|
|||
cryptoCurrencyId: CryptoCurrency.ID,
|
||||
): Flow<List<OnrampTransaction>> = appPreferencesStore
|
||||
.getObjectSet<OnrampTransactionDTO>(PreferencesKeys.ONRAMP_TRANSACTIONS_STATUSES_KEY)
|
||||
.onStart { cleanupExpiredTerminalTransactions() }
|
||||
.map { transactions ->
|
||||
transactions.filter {
|
||||
it.userWalletId == userWalletId && it.toCurrencyId == cryptoCurrencyId.value
|
||||
|
|
@ -101,4 +106,64 @@ internal class DefaultOnrampTransactionRepository(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun isHandledTransaction(txId: String): Boolean = withContext(dispatchers.io) {
|
||||
val terminated = appPreferencesStore.getObjectSetSync<OnrampTerminalTransactionDTO>(
|
||||
PreferencesKeys.ONRAMP_HANDLED_TRANSACTIONS_KEY,
|
||||
)
|
||||
|
||||
terminated.any { it.txId == txId }
|
||||
}
|
||||
|
||||
override suspend fun storeHandledTransaction(txId: String) {
|
||||
withContext(dispatchers.io) {
|
||||
appPreferencesStore.editData { mutablePreferences ->
|
||||
runCatching {
|
||||
val archived = mutablePreferences.getObjectSet<OnrampTerminalTransactionDTO>(
|
||||
PreferencesKeys.ONRAMP_HANDLED_TRANSACTIONS_KEY,
|
||||
).orEmpty().toMutableSet()
|
||||
|
||||
val record = OnrampTerminalTransactionDTO(
|
||||
txId = txId,
|
||||
terminatedAt = System.currentTimeMillis(),
|
||||
)
|
||||
|
||||
archived.add(record)
|
||||
|
||||
mutablePreferences.setObjectSet(
|
||||
key = PreferencesKeys.ONRAMP_HANDLED_TRANSACTIONS_KEY,
|
||||
value = archived,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun cleanupExpiredTerminalTransactions() {
|
||||
withContext(dispatchers.io) {
|
||||
appPreferencesStore.editData { mutablePreferences ->
|
||||
runCatching {
|
||||
val archived = mutablePreferences.getObjectSet<OnrampTerminalTransactionDTO>(
|
||||
PreferencesKeys.ONRAMP_HANDLED_TRANSACTIONS_KEY,
|
||||
)?.toMutableSet() ?: return@editData
|
||||
|
||||
val oneWeekAgo = System.currentTimeMillis() -
|
||||
TimeUnit.DAYS.toMillis(HANDLED_TRANSACTIONS_RETENTION_DAYS)
|
||||
val cleaned = archived
|
||||
.filterTo(mutableSetOf()) { it.terminatedAt > oneWeekAgo }
|
||||
|
||||
if (cleaned.size != archived.size) {
|
||||
mutablePreferences.setObjectSet(
|
||||
key = PreferencesKeys.ONRAMP_HANDLED_TRANSACTIONS_KEY,
|
||||
value = cleaned,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val HANDLED_TRANSACTIONS_RETENTION_DAYS = 7L
|
||||
}
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ import com.tangem.datasource.api.express.TangemExpressApi
|
|||
import com.tangem.datasource.api.express.models.response.ExpressErrorResponse
|
||||
import com.tangem.datasource.api.onramp.OnrampApi
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.appcurrency.AppCurrencyResponseStore
|
||||
import com.tangem.datasource.crypto.DataSignatureVerifier
|
||||
import com.tangem.datasource.di.NetworkMoshi
|
||||
import com.tangem.datasource.exchangeservice.hotcrypto.HotCryptoResponseStore
|
||||
|
|
@ -104,7 +105,7 @@ internal object OnrampDataModule {
|
|||
hotCryptoResponseStore: HotCryptoResponseStore,
|
||||
userWalletsStore: UserWalletsStore,
|
||||
tangemTechApi: TangemTechApi,
|
||||
appPreferencesStore: AppPreferencesStore,
|
||||
appCurrencyResponseStore: AppCurrencyResponseStore,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
analyticsEventHandler: AnalyticsEventHandler,
|
||||
userTokensResponseStore: UserTokensResponseStore,
|
||||
|
|
@ -114,7 +115,7 @@ internal object OnrampDataModule {
|
|||
hotCryptoResponseStore = hotCryptoResponseStore,
|
||||
userWalletsStore = userWalletsStore,
|
||||
tangemTechApi = tangemTechApi,
|
||||
appPreferencesStore = appPreferencesStore,
|
||||
appCurrencyResponseStore = appCurrencyResponseStore,
|
||||
dispatchers = dispatchers,
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
userTokensResponseStore = userTokensResponseStore,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
package com.tangem.data.onramp.models
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* Model for terminal onramp transaction
|
||||
*
|
||||
* @property txId Transaction ID
|
||||
* @property terminatedAt Termination timestamp in milliseconds
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class OnrampTerminalTransactionDTO(
|
||||
@Json(name = "txId")
|
||||
val txId: String,
|
||||
@Json(name = "terminatedAt")
|
||||
val terminatedAt: Long,
|
||||
)
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
<?xml version="1.0" ?>
|
||||
<SmellBaseline>
|
||||
<ManuallySuppressedIssues/>
|
||||
<CurrentIssues>
|
||||
<ID>SuspendFunSwallowedCancellation:DefaultPromoRepository.kt$DefaultPromoRepository$runCatching</ID>
|
||||
<ID>SuspendFunWithFlowReturnType:DefaultPromoRepository.kt$DefaultPromoRepository$suspend</ID>
|
||||
</CurrentIssues>
|
||||
</SmellBaseline>
|
||||
|
|
@ -20,6 +20,7 @@ import com.tangem.domain.promo.models.StoryContent
|
|||
import com.tangem.feature.referral.domain.ReferralRepository
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.runCatching
|
||||
import com.tangem.utils.coroutines.runSuspendCatching
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
|
|
@ -44,7 +45,7 @@ internal class DefaultPromoRepository(
|
|||
.distinctUntilChanged()
|
||||
.map { shouldShow ->
|
||||
when (promoId) {
|
||||
PromoId.Referral -> runCatching {
|
||||
PromoId.Referral -> runSuspendCatching {
|
||||
!referralRepository.isReferralParticipant(userWalletId) && shouldShow
|
||||
}.getOrDefault(false)
|
||||
PromoId.Sepa -> {
|
||||
|
|
@ -89,7 +90,7 @@ internal class DefaultPromoRepository(
|
|||
appPreferencesStore.store(PreferencesKeys.getShouldShowPromoKey(promoId = promoId.name), false)
|
||||
}
|
||||
|
||||
override suspend fun isMarketsStakingNotificationHideClicked(): Flow<Boolean> {
|
||||
override fun isMarketsStakingNotificationHideClicked(): Flow<Boolean> {
|
||||
return appPreferencesStore.get(
|
||||
key = PreferencesKeys.MARKETS_STAKING_NOTIFICATION_HIDE_CLICKED_KEY,
|
||||
default = false,
|
||||
|
|
@ -125,7 +126,7 @@ internal class DefaultPromoRepository(
|
|||
val storedPromo = promoStoriesStore.getSyncOrNull(storyId = id)
|
||||
// Get last stored promo by id if possible or get from network
|
||||
val story = if (storedPromo == null && refresh) {
|
||||
val storyContent = runCatching {
|
||||
val storyContent = runSuspendCatching {
|
||||
// Important to return
|
||||
withTimeoutOrNull(STORIES_LOAD_DELAY) {
|
||||
tangemApi.getStoryById(storyId = id).getOrThrow()
|
||||
|
|
@ -193,8 +194,8 @@ internal class DefaultPromoRepository(
|
|||
const val SEPA_NAME = "sepa"
|
||||
const val VISA_NAME = "visa-waitlist"
|
||||
const val BLACK_FRIDAY_NAME = "black-friday"
|
||||
const val ONE_PLUS_ONE_NAME = "one-plus-one"
|
||||
const val MOONPAY_NAME = "moonpay"
|
||||
const val ONE_PLUS_ONE_NAME = "one-plus-one"
|
||||
const val STORIES_LOAD_DELAY = 1000L
|
||||
}
|
||||
}
|
||||
|
|
@ -51,6 +51,7 @@ dependencies {
|
|||
implementation(deps.androidx.datastore)
|
||||
implementation(deps.jodatime)
|
||||
implementation(deps.kotlin.coroutines)
|
||||
implementation(deps.kotlin.datetime)
|
||||
implementation(deps.kotlin.immutable.collections)
|
||||
implementation(deps.moshi)
|
||||
implementation(deps.moshi.kotlin)
|
||||
|
|
|
|||
|
|
@ -223,30 +223,18 @@ internal class DefaultP2PEthPoolRepository(
|
|||
.map { vaults ->
|
||||
if (vaults.isEmpty()) {
|
||||
return@map StakingAvailability.TemporaryUnavailable
|
||||
}
|
||||
|
||||
val vault = findPublicVault(vaults = vaults)
|
||||
|
||||
if (vault != null) {
|
||||
StakingAvailability.Available(StakingOption.P2P(vault))
|
||||
} else {
|
||||
StakingAvailability.TemporaryUnavailable
|
||||
StakingAvailability.Available(StakingOption.P2P(vaults))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getStakingAvailabilitySync(): StakingAvailability {
|
||||
val vaults = getVaultsSync()
|
||||
if (vaults.isEmpty()) {
|
||||
return StakingAvailability.TemporaryUnavailable
|
||||
}
|
||||
|
||||
val vault = findPublicVault(vaults = vaults)
|
||||
|
||||
return if (vault != null) {
|
||||
StakingAvailability.Available(StakingOption.P2P(vault))
|
||||
} else {
|
||||
return if (vaults.isEmpty()) {
|
||||
StakingAvailability.TemporaryUnavailable
|
||||
} else {
|
||||
StakingAvailability.Available(StakingOption.P2P(vaults))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -257,8 +245,4 @@ internal class DefaultP2PEthPoolRepository(
|
|||
private fun getVaultsFlow(): Flow<List<P2PEthPoolVault>> {
|
||||
return p2pEthPoolVaultsStore.get()
|
||||
}
|
||||
|
||||
private fun findPublicVault(vaults: List<P2PEthPoolVault>): P2PEthPoolVault? {
|
||||
return vaults.firstOrNull { vault -> !vault.isPrivate }
|
||||
}
|
||||
}
|
||||
|
|
@ -3,11 +3,11 @@ package com.tangem.data.staking
|
|||
import arrow.core.getOrElse
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchainsdk.utils.toBlockchain
|
||||
import com.tangem.data.staking.store.YieldsBalancesStore
|
||||
import com.tangem.data.staking.store.StakingBalancesStore
|
||||
import com.tangem.domain.card.common.TapWorkarounds.isWallet2
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.staking.StakingBalance
|
||||
import com.tangem.domain.models.staking.StakingID
|
||||
import com.tangem.domain.models.staking.YieldBalance
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.staking.model.StakingAvailability
|
||||
|
|
@ -30,7 +30,7 @@ import kotlinx.coroutines.withContext
|
|||
internal class DefaultStakingRepository(
|
||||
private val stakeKitRepository: StakeKitRepository,
|
||||
private val p2pEthPoolRepository: P2PEthPoolRepository,
|
||||
private val stakingBalanceStoreV2: YieldsBalancesStore,
|
||||
private val stakingBalanceStoreV2: StakingBalancesStore,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val getUserWalletUseCase: GetUserWalletUseCase,
|
||||
private val stakingFeatureToggles: StakingFeatureToggles,
|
||||
|
|
@ -106,13 +106,13 @@ internal class DefaultStakingRepository(
|
|||
return withContext(dispatchers.default) {
|
||||
val balances = stakingBalanceStoreV2.getAllSyncOrNull(userWalletId) ?: return@withContext false
|
||||
|
||||
val hasDataYieldBalance by lazy {
|
||||
balances.any { yieldBalance ->
|
||||
(yieldBalance as? YieldBalance.Data)?.balance?.items?.isNotEmpty() == true
|
||||
val hasDataStakingBalance by lazy {
|
||||
balances.any { stakingBalance ->
|
||||
stakingBalance is StakingBalance.Data
|
||||
}
|
||||
}
|
||||
|
||||
balances.isNotEmpty() && hasDataYieldBalance
|
||||
balances.isNotEmpty() && hasDataStakingBalance
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -135,7 +135,7 @@ internal class DefaultStakingRepository(
|
|||
address = address,
|
||||
),
|
||||
)
|
||||
if (balance != null && balance is YieldBalance.Data && balance.balance.items.isNotEmpty()) {
|
||||
if ((balance as? StakingBalance.Data.StakeKit)?.balance?.items?.isNotEmpty() == true) {
|
||||
return true
|
||||
} else {
|
||||
stakingFeatureToggles.isCardanoStakingEnabled
|
||||
|
|
|
|||
|
|
@ -0,0 +1,60 @@
|
|||
package com.tangem.data.staking.converters.ethpool
|
||||
|
||||
import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolAccountResponse
|
||||
import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolExitQueueDTO
|
||||
import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolExitRequestDTO
|
||||
import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolStakeDTO
|
||||
import com.tangem.domain.models.StatusSource
|
||||
import com.tangem.domain.models.staking.*
|
||||
import com.tangem.domain.staking.model.StakingIntegrationID
|
||||
import kotlinx.datetime.Instant
|
||||
|
||||
/** Converts P2P ETH Pool API response to [StakingBalance.Data.P2P] */
|
||||
internal object P2PStakingBalanceConverter {
|
||||
|
||||
fun convert(response: P2PEthPoolAccountResponse, source: StatusSource): StakingBalance.Data.P2P {
|
||||
val stakingId = StakingID(
|
||||
integrationId = StakingIntegrationID.P2P.EthereumPooled.value,
|
||||
address = response.delegatorAddress,
|
||||
)
|
||||
|
||||
val account = P2PStakingAccount(
|
||||
delegatorAddress = response.delegatorAddress,
|
||||
vaultAddress = response.vaultAddress,
|
||||
stake = convertStake(response.stake),
|
||||
availableToUnstake = response.availableToUnstake,
|
||||
availableToWithdraw = response.availableToWithdraw,
|
||||
exitQueue = convertExitQueue(response.exitQueue),
|
||||
)
|
||||
|
||||
return StakingBalance.Data.P2P(
|
||||
stakingId = stakingId,
|
||||
source = source,
|
||||
account = account,
|
||||
)
|
||||
}
|
||||
|
||||
private fun convertStake(dto: P2PEthPoolStakeDTO): P2PStake {
|
||||
return P2PStake(
|
||||
assets = dto.assets,
|
||||
totalEarnedAssets = dto.totalEarnedAssets,
|
||||
)
|
||||
}
|
||||
|
||||
private fun convertExitQueue(dto: P2PEthPoolExitQueueDTO): P2PExitQueue {
|
||||
return P2PExitQueue(
|
||||
total = dto.total.toBigDecimal(),
|
||||
requests = dto.requests.map(::convertExitRequest),
|
||||
)
|
||||
}
|
||||
|
||||
private fun convertExitRequest(dto: P2PEthPoolExitRequestDTO): P2PExitRequest {
|
||||
return P2PExitRequest(
|
||||
ticket = dto.ticket,
|
||||
totalAssets = dto.totalAssets.toBigDecimal(),
|
||||
timestamp = Instant.fromEpochSeconds(dto.timestamp),
|
||||
withdrawalTimestamp = Instant.fromEpochSeconds(dto.withdrawalTimestamp),
|
||||
isClaimable = dto.isClaimable,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
package com.tangem.data.staking.converters.ethpool
|
||||
|
||||
import com.tangem.domain.models.StatusSource
|
||||
import com.tangem.domain.models.staking.*
|
||||
import com.tangem.domain.staking.model.ethpool.P2PEthPoolAccount
|
||||
import com.tangem.domain.staking.model.ethpool.P2PEthPoolVault
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* tmp solution before facade implementation
|
||||
*/
|
||||
internal object P2PYieldBalanceConverter {
|
||||
|
||||
private const val ETH_DECIMALS = 18
|
||||
private const val ETH_SYMBOL = "ETH"
|
||||
private const val ETH_NAME = "Ethereum"
|
||||
private const val ETH_COINGECKO_ID = "ethereum"
|
||||
|
||||
fun convert(
|
||||
account: P2PEthPoolAccount,
|
||||
vault: P2PEthPoolVault,
|
||||
address: String,
|
||||
source: StatusSource,
|
||||
): YieldBalance {
|
||||
val integrationId = "p2p-ethereum-pooled"
|
||||
val stakingId = StakingID(
|
||||
integrationId = integrationId,
|
||||
address = address,
|
||||
)
|
||||
|
||||
val balanceItems = buildBalanceItems(account, vault)
|
||||
|
||||
return if (balanceItems.isEmpty()) {
|
||||
YieldBalance.Empty(stakingId = stakingId, source = source)
|
||||
} else {
|
||||
YieldBalance.Data(
|
||||
stakingId = stakingId,
|
||||
source = source,
|
||||
balance = YieldBalanceItem(
|
||||
items = balanceItems,
|
||||
integrationId = integrationId,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildBalanceItems(account: P2PEthPoolAccount, vault: P2PEthPoolVault): List<BalanceItem> = buildList {
|
||||
if (account.stake.assets > BigDecimal.ZERO) {
|
||||
add(
|
||||
createBalanceItem(
|
||||
groupId = "p2p-staked",
|
||||
amount = account.stake.assets,
|
||||
type = BalanceType.STAKED,
|
||||
validatorAddress = vault.vaultAddress,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createBalanceItem(
|
||||
groupId: String,
|
||||
amount: BigDecimal,
|
||||
type: BalanceType,
|
||||
validatorAddress: String,
|
||||
): BalanceItem {
|
||||
return BalanceItem(
|
||||
groupId = groupId,
|
||||
token = createEthToken(),
|
||||
type = type,
|
||||
amount = amount,
|
||||
rawCurrencyId = ETH_COINGECKO_ID,
|
||||
validatorAddress = validatorAddress,
|
||||
date = null,
|
||||
pendingActions = emptyList(),
|
||||
pendingActionsConstraints = emptyList(),
|
||||
isPending = false,
|
||||
)
|
||||
}
|
||||
|
||||
private fun createEthToken(): YieldToken {
|
||||
return YieldToken(
|
||||
name = ETH_NAME,
|
||||
network = NetworkType.ETHEREUM,
|
||||
symbol = ETH_SYMBOL,
|
||||
decimals = ETH_DECIMALS,
|
||||
address = null,
|
||||
coinGeckoId = ETH_COINGECKO_ID,
|
||||
logoURI = null,
|
||||
isPoints = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package com.tangem.data.staking.di
|
||||
|
||||
import com.tangem.data.staking.multi.DefaultMultiStakingBalanceFetcher
|
||||
import com.tangem.data.staking.single.DefaultSingleStakingBalanceFetcher
|
||||
import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher
|
||||
import com.tangem.domain.staking.single.SingleStakingBalanceFetcher
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal interface StakingBalanceFetcherModule {
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindSingleStakingBalanceFetcher(impl: DefaultSingleStakingBalanceFetcher): SingleStakingBalanceFetcher
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindMultiStakingBalanceFetcher(impl: DefaultMultiStakingBalanceFetcher): MultiStakingBalanceFetcher
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package com.tangem.data.staking.di
|
||||
|
||||
import com.tangem.data.staking.multi.DefaultMultiStakingBalanceProducer
|
||||
import com.tangem.data.staking.single.DefaultSingleStakingBalanceProducer
|
||||
import com.tangem.domain.staking.multi.MultiStakingBalanceProducer
|
||||
import com.tangem.domain.staking.single.SingleStakingBalanceProducer
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal interface StakingBalanceProducerFactoryModule {
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindSingleStakingBalanceProducerFactory(
|
||||
impl: DefaultSingleStakingBalanceProducer.Factory,
|
||||
): SingleStakingBalanceProducer.Factory
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindMultiStakingBalanceProducerFactory(
|
||||
impl: DefaultMultiStakingBalanceProducer.Factory,
|
||||
): MultiStakingBalanceProducer.Factory
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
package com.tangem.data.staking.di
|
||||
|
||||
import androidx.datastore.core.DataStore
|
||||
import com.tangem.data.staking.store.DefaultP2PBalancesStore
|
||||
import com.tangem.data.staking.store.DefaultStakingBalancesStore
|
||||
import com.tangem.data.staking.store.P2PBalancesStore
|
||||
import com.tangem.data.staking.store.StakingBalancesStore
|
||||
import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolAccountResponse
|
||||
import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO
|
||||
import com.tangem.datasource.local.datastore.RuntimeSharedStore
|
||||
import com.tangem.domain.staking.multi.MultiStakingBalanceProducer
|
||||
import com.tangem.domain.staking.multi.MultiStakingBalanceSupplier
|
||||
import com.tangem.domain.staking.single.SingleStakingBalanceProducer
|
||||
import com.tangem.domain.staking.single.SingleStakingBalanceSupplier
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object StakingBalanceSupplierModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideStakingBalancesStore(
|
||||
persistenceStore: DataStore<Map<String, Set<YieldBalanceWrapperDTO>>>,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): StakingBalancesStore {
|
||||
return DefaultStakingBalancesStore(
|
||||
runtimeStore = RuntimeSharedStore(),
|
||||
persistenceStore = persistenceStore,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideP2PBalancesStore(
|
||||
persistenceStore: DataStore<Map<String, Set<P2PEthPoolAccountResponse>>>,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): P2PBalancesStore {
|
||||
return DefaultP2PBalancesStore(
|
||||
runtimeStore = RuntimeSharedStore(),
|
||||
persistenceStore = persistenceStore,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSingleStakingBalanceSupplier(
|
||||
factory: SingleStakingBalanceProducer.Factory,
|
||||
): SingleStakingBalanceSupplier {
|
||||
return object : SingleStakingBalanceSupplier(
|
||||
factory = factory,
|
||||
keyCreator = { params ->
|
||||
listOf(
|
||||
"single_staking_balance",
|
||||
params.userWalletId.stringValue,
|
||||
params.stakingId.integrationId,
|
||||
params.stakingId.address,
|
||||
)
|
||||
.joinToString(separator = "_")
|
||||
},
|
||||
) {}
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideMultiStakingBalanceSupplier(factory: MultiStakingBalanceProducer.Factory): MultiStakingBalanceSupplier {
|
||||
return object : MultiStakingBalanceSupplier(
|
||||
factory = factory,
|
||||
keyCreator = { "multi_staking_balances_${it.userWalletId.stringValue}" },
|
||||
) {}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler
|
|||
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||
import com.tangem.data.staking.*
|
||||
import com.tangem.data.staking.converters.error.StakeKitErrorConverter
|
||||
import com.tangem.data.staking.store.YieldsBalancesStore
|
||||
import com.tangem.data.staking.store.StakingBalancesStore
|
||||
import com.tangem.data.staking.toggles.DefaultStakingFeatureToggles
|
||||
import com.tangem.data.staking.utils.DefaultStakingCleaner
|
||||
import com.tangem.datasource.api.ethpool.P2PEthPoolApi
|
||||
|
|
@ -55,7 +55,7 @@ internal object StakingDataModule {
|
|||
fun provideStakingRepository(
|
||||
stakeKitRepository: StakeKitRepository,
|
||||
p2pEthPoolRepository: P2PEthPoolRepository,
|
||||
yieldsBalancesStore: YieldsBalancesStore,
|
||||
stakingBalancesStore: StakingBalancesStore,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
getUserWalletUseCase: GetUserWalletUseCase,
|
||||
stakingFeatureToggles: StakingFeatureToggles,
|
||||
|
|
@ -64,7 +64,7 @@ internal object StakingDataModule {
|
|||
return DefaultStakingRepository(
|
||||
stakeKitRepository = stakeKitRepository,
|
||||
p2pEthPoolRepository = p2pEthPoolRepository,
|
||||
stakingBalanceStoreV2 = yieldsBalancesStore,
|
||||
stakingBalanceStoreV2 = stakingBalancesStore,
|
||||
dispatchers = dispatchers,
|
||||
getUserWalletUseCase = getUserWalletUseCase,
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
|
|
@ -134,11 +134,11 @@ internal object StakingDataModule {
|
|||
@Provides
|
||||
@Singleton
|
||||
fun provideStakingCleaner(
|
||||
yieldsBalancesStore: YieldsBalancesStore,
|
||||
stakingBalancesStore: StakingBalancesStore,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): StakingCleaner {
|
||||
return DefaultStakingCleaner(
|
||||
yieldsBalancesStore = yieldsBalancesStore,
|
||||
stakingBalancesStore = stakingBalancesStore,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,24 +0,0 @@
|
|||
package com.tangem.data.staking.di
|
||||
|
||||
import com.tangem.data.staking.multi.DefaultMultiYieldBalanceFetcher
|
||||
import com.tangem.data.staking.single.DefaultSingleYieldBalanceFetcher
|
||||
import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher
|
||||
import com.tangem.domain.staking.single.SingleYieldBalanceFetcher
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal interface YieldBalanceFetcherModule {
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindSingleYieldBalanceFetcher(impl: DefaultSingleYieldBalanceFetcher): SingleYieldBalanceFetcher
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindMultiYieldBalanceFetcher(impl: DefaultMultiYieldBalanceFetcher): MultiYieldBalanceFetcher
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
package com.tangem.data.staking.di
|
||||
|
||||
import com.tangem.data.staking.multi.DefaultMultiYieldBalanceProducer
|
||||
import com.tangem.data.staking.single.DefaultSingleYieldBalanceProducer
|
||||
import com.tangem.domain.staking.multi.MultiYieldBalanceProducer
|
||||
import com.tangem.domain.staking.single.SingleYieldBalanceProducer
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal interface YieldBalanceProducerFactoryModule {
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindSingleYieldBalanceProducerFactory(
|
||||
impl: DefaultSingleYieldBalanceProducer.Factory,
|
||||
): SingleYieldBalanceProducer.Factory
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindMultiYieldBalanceProducerFactory(
|
||||
impl: DefaultMultiYieldBalanceProducer.Factory,
|
||||
): MultiYieldBalanceProducer.Factory
|
||||
}
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
package com.tangem.data.staking.di
|
||||
|
||||
import androidx.datastore.core.DataStore
|
||||
import com.tangem.data.staking.store.DefaultYieldsBalancesStore
|
||||
import com.tangem.data.staking.store.YieldsBalancesStore
|
||||
import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO
|
||||
import com.tangem.datasource.local.datastore.RuntimeSharedStore
|
||||
import com.tangem.domain.staking.multi.MultiYieldBalanceProducer
|
||||
import com.tangem.domain.staking.multi.MultiYieldBalanceSupplier
|
||||
import com.tangem.domain.staking.single.SingleYieldBalanceProducer
|
||||
import com.tangem.domain.staking.single.SingleYieldBalanceSupplier
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object YieldBalanceSupplierModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideYieldsBalancesStore(
|
||||
persistenceStore: DataStore<Map<String, Set<YieldBalanceWrapperDTO>>>,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): YieldsBalancesStore {
|
||||
return DefaultYieldsBalancesStore(
|
||||
runtimeStore = RuntimeSharedStore(),
|
||||
persistenceStore = persistenceStore,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSingleYieldBalanceSupplier(factory: SingleYieldBalanceProducer.Factory): SingleYieldBalanceSupplier {
|
||||
return object : SingleYieldBalanceSupplier(
|
||||
factory = factory,
|
||||
keyCreator = { params ->
|
||||
listOf(
|
||||
"single_yield_balance",
|
||||
params.userWalletId.stringValue,
|
||||
params.stakingId.integrationId,
|
||||
params.stakingId.address,
|
||||
)
|
||||
.joinToString(separator = "_")
|
||||
},
|
||||
) {}
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideMultiYieldBalanceSupplier(factory: MultiYieldBalanceProducer.Factory): MultiYieldBalanceSupplier {
|
||||
return object : MultiYieldBalanceSupplier(
|
||||
factory = factory,
|
||||
keyCreator = { "multi_yields_balances_${it.userWalletId.stringValue}" },
|
||||
) {}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,344 @@
|
|||
package com.tangem.data.staking.multi
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
import arrow.core.toOption
|
||||
import com.tangem.data.common.api.safeApiCall
|
||||
import com.tangem.data.staking.store.P2PBalancesStore
|
||||
import com.tangem.data.staking.store.StakingBalancesStore
|
||||
import com.tangem.data.staking.utils.YieldBalanceRequestBodyFactory
|
||||
import com.tangem.datasource.api.common.response.ApiResponse
|
||||
import com.tangem.datasource.api.ethpool.P2PEthPoolApi
|
||||
import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolAccountResponse
|
||||
import com.tangem.datasource.api.stakekit.StakeKitApi
|
||||
import com.tangem.datasource.api.stakekit.models.request.YieldBalanceRequestBody
|
||||
import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO
|
||||
import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO
|
||||
import com.tangem.datasource.local.token.P2PEthPoolVaultsStore
|
||||
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.StakingIntegrationID
|
||||
import com.tangem.domain.staking.model.ethpool.P2PStakingConfig
|
||||
import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.runSuspendCatching
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Default implementation of [MultiStakingBalanceFetcher]
|
||||
*
|
||||
* Supports both StakeKit and P2P staking providers.
|
||||
*
|
||||
* @property userWalletsStore user wallets store
|
||||
* @property stakingYieldsStore staking yields store
|
||||
* @property stakingBalancesStore staking balances store (StakeKit)
|
||||
* @property p2pBalancesStore P2P balances store
|
||||
* @property stakeKitApi stake kit API
|
||||
* @property p2pApi P2P ETH Pool API
|
||||
* @property p2pVaultsStore P2P vaults store
|
||||
* @property dispatchers dispatchers
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
internal class DefaultMultiStakingBalanceFetcher @Inject constructor(
|
||||
private val userWalletsStore: UserWalletsStore,
|
||||
private val stakingYieldsStore: StakingYieldsStore,
|
||||
private val stakingBalancesStore: StakingBalancesStore,
|
||||
private val p2pBalancesStore: P2PBalancesStore,
|
||||
private val stakeKitApi: StakeKitApi,
|
||||
private val p2pApi: P2PEthPoolApi,
|
||||
private val p2pVaultsStore: P2PEthPoolVaultsStore,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : MultiStakingBalanceFetcher {
|
||||
|
||||
override suspend fun invoke(params: MultiStakingBalanceFetcher.Params): Either<Throwable, Unit> {
|
||||
Timber.i("Start fetching staking balances for params:\n$params")
|
||||
|
||||
val stakingIds = params.stakingIds.ifEmpty {
|
||||
Timber.i("Nothing to fetch, empty stakingIds for ${params.userWalletId}")
|
||||
return Unit.right()
|
||||
}
|
||||
|
||||
checkIsSupportedByWalletOrElse(userWalletId = params.userWalletId) {
|
||||
return it.left()
|
||||
}
|
||||
|
||||
val (stakeKitIds, p2pIds) = stakingIds.partition { stakingId ->
|
||||
val stakingIntegrationID = StakingIntegrationID.entries.find {
|
||||
it.value == stakingId.integrationId
|
||||
}
|
||||
stakingIntegrationID is StakingIntegrationID.StakeKit
|
||||
}
|
||||
|
||||
Timber.i(
|
||||
"""
|
||||
Staking IDs to fetch:
|
||||
- StakeKit: ${stakeKitIds.joinToString()}
|
||||
- P2P: ${p2pIds.joinToString()}
|
||||
""".trimIndent(),
|
||||
)
|
||||
|
||||
return Either.catchOn(dispatchers.default) {
|
||||
coroutineScope {
|
||||
if (stakeKitIds.isNotEmpty()) {
|
||||
launch { fetchStakeKitBalances(params.userWalletId, stakeKitIds.toSet()) }
|
||||
}
|
||||
|
||||
if (p2pIds.isNotEmpty()) {
|
||||
launch { fetchP2PBalances(params.userWalletId, p2pIds.toSet()) }
|
||||
}
|
||||
}
|
||||
}
|
||||
.onLeft { throwable ->
|
||||
Timber.e(throwable, "Unable to fetch staking balances $params")
|
||||
|
||||
if (stakeKitIds.isNotEmpty()) {
|
||||
stakingBalancesStore.storeError(
|
||||
userWalletId = params.userWalletId,
|
||||
stakingIds = stakeKitIds.toSet(),
|
||||
)
|
||||
}
|
||||
if (p2pIds.isNotEmpty()) {
|
||||
p2pBalancesStore.storeError(userWalletId = params.userWalletId, stakingIds = p2pIds.toSet())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchStakeKitBalances(userWalletId: UserWalletId, stakingIds: Set<StakingID>) {
|
||||
stakingBalancesStore.refresh(userWalletId = userWalletId, stakingIds = stakingIds)
|
||||
|
||||
val availableStakingIds = getAvailableStakingIds(
|
||||
userWalletId = userWalletId,
|
||||
stakingIds = stakingIds,
|
||||
)
|
||||
|
||||
fetchFromStakeKit(userWalletId = userWalletId, stakingIds = availableStakingIds)
|
||||
}
|
||||
|
||||
private suspend fun fetchP2PBalances(userWalletId: UserWalletId, stakingIds: Set<StakingID>) {
|
||||
p2pBalancesStore.refresh(userWalletId = userWalletId, stakingIds = stakingIds)
|
||||
|
||||
val vaults = runSuspendCatching { p2pVaultsStore.getSync() }.getOrNull().orEmpty()
|
||||
if (vaults.isEmpty()) {
|
||||
Timber.w("No P2P vaults available for $userWalletId")
|
||||
p2pBalancesStore.storeError(userWalletId = userWalletId, stakingIds = stakingIds)
|
||||
return
|
||||
}
|
||||
|
||||
fetchFromP2P(userWalletId = userWalletId, stakingIds = stakingIds, vaults = vaults)
|
||||
}
|
||||
|
||||
private suspend fun fetchFromP2P(
|
||||
userWalletId: UserWalletId,
|
||||
stakingIds: Set<StakingID>,
|
||||
vaults: List<com.tangem.domain.staking.model.ethpool.P2PEthPoolVault>,
|
||||
) {
|
||||
safeApiCall(
|
||||
call = {
|
||||
val addresses = stakingIds.map { it.address }.toSet()
|
||||
|
||||
val responses = mutableSetOf<P2PEthPoolAccountResponse>()
|
||||
|
||||
for (vault in vaults) {
|
||||
for (address in addresses) {
|
||||
runSuspendCatching {
|
||||
val response = p2pApi.getAccountInfo(
|
||||
network = P2PStakingConfig.activeNetwork.value,
|
||||
delegatorAddress = address,
|
||||
vaultAddress = vault.vaultAddress,
|
||||
)
|
||||
|
||||
when (response) {
|
||||
is ApiResponse.Success -> {
|
||||
val data = response.data
|
||||
if (data.error != null) {
|
||||
Timber.w(
|
||||
"P2P API returned error for vault ${vault.vaultAddress}, " +
|
||||
"address $address: ${data.error ?: "error"}",
|
||||
)
|
||||
} else {
|
||||
val result = requireNotNull(data.result) {
|
||||
"Result is null in successful response"
|
||||
}
|
||||
responses.add(result)
|
||||
}
|
||||
}
|
||||
is ApiResponse.Error -> {
|
||||
Timber.w(
|
||||
response.cause,
|
||||
"Failed to fetch P2P balance for vault ${vault.vaultAddress}, " +
|
||||
"address $address",
|
||||
)
|
||||
}
|
||||
}
|
||||
}.onFailure { error ->
|
||||
Timber.w(
|
||||
error,
|
||||
"Failed to fetch P2P balance for vault ${vault.vaultAddress}, address $address",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timber.i("Successfully fetched ${responses.size} P2P balances for $userWalletId")
|
||||
|
||||
if (responses.isNotEmpty()) {
|
||||
p2pBalancesStore.storeActual(userWalletId = userWalletId, values = responses)
|
||||
|
||||
val missingStakingIds = stakingIds.filter { stakingId ->
|
||||
responses.none { response ->
|
||||
response.delegatorAddress.equals(stakingId.address, ignoreCase = true)
|
||||
}
|
||||
}
|
||||
|
||||
if (missingStakingIds.isNotEmpty()) {
|
||||
Timber.i("Missing responses for ${missingStakingIds.size} staking IDs: $missingStakingIds")
|
||||
p2pBalancesStore.storeError(userWalletId = userWalletId, stakingIds = missingStakingIds.toSet())
|
||||
}
|
||||
} else {
|
||||
Timber.i("No P2P responses received for $userWalletId")
|
||||
p2pBalancesStore.storeError(userWalletId = userWalletId, stakingIds = stakingIds)
|
||||
}
|
||||
},
|
||||
onError = { throwable ->
|
||||
Timber.e(throwable, "Unable to fetch P2P balances $userWalletId")
|
||||
|
||||
p2pBalancesStore.storeError(userWalletId = userWalletId, stakingIds = stakingIds)
|
||||
|
||||
throw throwable
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private inline fun checkIsSupportedByWalletOrElse(userWalletId: UserWalletId, ifNotSupported: (Throwable) -> Unit) {
|
||||
val maybeUserWallet = userWalletsStore.getSyncOrNull(key = userWalletId).toOption()
|
||||
|
||||
val isSupportedByWallet = maybeUserWallet.isSome(UserWallet::isMultiCurrency)
|
||||
|
||||
if (!isSupportedByWallet) {
|
||||
val exception = IllegalStateException("Wallet $userWalletId is not supported: $maybeUserWallet")
|
||||
Timber.e(exception)
|
||||
|
||||
ifNotSupported(exception)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getAvailableStakingIds(userWalletId: UserWalletId, stakingIds: Set<StakingID>): Set<StakingID> {
|
||||
val yieldIds = getYieldsIds(userWalletId = userWalletId)
|
||||
|
||||
// [true] -> available
|
||||
// [false] -> unavailable
|
||||
val groupedStakingIds = stakingIds.groupBy { stakingId ->
|
||||
yieldIds.any { it == stakingId.integrationId }
|
||||
}
|
||||
|
||||
val availableStakingIds = groupedStakingIds[true].orEmpty()
|
||||
val unavailableStakingIds = groupedStakingIds[false].orEmpty()
|
||||
|
||||
Timber.i(
|
||||
"""
|
||||
Available staking IDs: ${availableStakingIds.joinToString()}
|
||||
Unavailable staking IDs: ${unavailableStakingIds.joinToString()}
|
||||
""".trimIndent(),
|
||||
)
|
||||
|
||||
if (unavailableStakingIds.isNotEmpty()) {
|
||||
stakingBalancesStore.storeError(userWalletId = userWalletId, stakingIds = unavailableStakingIds.toSet())
|
||||
}
|
||||
|
||||
return availableStakingIds.toSet().ifEmpty {
|
||||
val exception = IllegalStateException(
|
||||
"""
|
||||
No available yields to fetch yield balances:
|
||||
– userWalletId: $userWalletId
|
||||
– stakingIds: ${stakingIds.joinToString()}
|
||||
""".trimIndent(),
|
||||
)
|
||||
Timber.i(exception)
|
||||
throw exception
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getYieldsIds(userWalletId: UserWalletId): Set<String> {
|
||||
val yieldsIds = stakingYieldsStore.getSyncWithTimeout().orEmpty()
|
||||
.mapNotNullTo(destination = hashSetOf(), transform = YieldDTO::id)
|
||||
|
||||
if (yieldsIds.isEmpty()) {
|
||||
val exception = IllegalStateException("No enabled yields for $userWalletId")
|
||||
Timber.e(exception)
|
||||
|
||||
throw exception
|
||||
}
|
||||
|
||||
return yieldsIds
|
||||
}
|
||||
|
||||
private suspend fun fetchFromStakeKit(userWalletId: UserWalletId, stakingIds: Set<StakingID>) {
|
||||
safeApiCall(
|
||||
call = {
|
||||
val requests = stakingIds.map(YieldBalanceRequestBodyFactory::create)
|
||||
|
||||
val yieldBalances = coroutineScope {
|
||||
requests
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
.awaitAll()
|
||||
.flatten()
|
||||
.toSet()
|
||||
}
|
||||
|
||||
Timber.i(
|
||||
"Successfully fetched staking balances for $userWalletId:\n${yieldBalances.joinToString("\n")}",
|
||||
)
|
||||
stakingBalancesStore.storeActual(userWalletId = userWalletId, values = yieldBalances)
|
||||
|
||||
if (!allResponsesReceived(requests, yieldBalances)) {
|
||||
val values = stakingIds.filter { stakingId ->
|
||||
yieldBalances.none { balanceWrapper ->
|
||||
stakingId.integrationId == balanceWrapper.integrationId &&
|
||||
stakingId.address == balanceWrapper.addresses.address
|
||||
}
|
||||
}
|
||||
|
||||
stakingBalancesStore.storeError(userWalletId = userWalletId, stakingIds = values.toSet())
|
||||
}
|
||||
},
|
||||
onError = { throwable ->
|
||||
Timber.e(throwable, "Unable to fetch staking balances $userWalletId")
|
||||
|
||||
stakingBalancesStore.storeError(userWalletId = userWalletId, stakingIds = stakingIds)
|
||||
|
||||
throw throwable
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun allResponsesReceived(
|
||||
requests: List<YieldBalanceRequestBody>,
|
||||
yieldBalances: Set<YieldBalanceWrapperDTO>,
|
||||
): Boolean {
|
||||
return requests.all { request ->
|
||||
yieldBalances.any { balance ->
|
||||
request.integrationId == balance.integrationId &&
|
||||
request.addresses.address == balance.addresses.address
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
package com.tangem.data.staking.multi
|
||||
|
||||
import arrow.core.Option
|
||||
import arrow.core.some
|
||||
import com.tangem.data.staking.store.P2PBalancesStore
|
||||
import com.tangem.data.staking.store.StakingBalancesStore
|
||||
import com.tangem.domain.models.staking.StakingBalance
|
||||
import com.tangem.domain.staking.multi.MultiStakingBalanceProducer
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.onEmpty
|
||||
|
||||
/**
|
||||
* Default implementation of [MultiStakingBalanceProducer]
|
||||
*
|
||||
* Combines staking balances from both StakeKit and P2P providers.
|
||||
*
|
||||
* @property params params
|
||||
* @property stakingBalancesStore StakeKit staking balances store
|
||||
* @property p2pBalancesStore P2P balances store
|
||||
* @property dispatchers dispatchers
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class DefaultMultiStakingBalanceProducer @AssistedInject constructor(
|
||||
@Assisted val params: MultiStakingBalanceProducer.Params,
|
||||
private val stakingBalancesStore: StakingBalancesStore,
|
||||
private val p2pBalancesStore: P2PBalancesStore,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : MultiStakingBalanceProducer {
|
||||
|
||||
override val fallback: Option<Set<StakingBalance>> = emptySet<StakingBalance>().some()
|
||||
|
||||
override fun produce(): Flow<Set<StakingBalance>> {
|
||||
val stakeKitFlow = stakingBalancesStore.get(userWalletId = params.userWalletId)
|
||||
val p2pFlow = p2pBalancesStore.get(userWalletId = params.userWalletId)
|
||||
|
||||
return combine(stakeKitFlow, p2pFlow) { stakeKitBalances, p2pBalances ->
|
||||
stakeKitBalances + p2pBalances
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.onEmpty { emit(value = hashSetOf()) }
|
||||
.flowOn(dispatchers.default)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : MultiStakingBalanceProducer.Factory {
|
||||
override fun create(params: MultiStakingBalanceProducer.Params): DefaultMultiStakingBalanceProducer
|
||||
}
|
||||
}
|
||||
|
|
@ -1,196 +0,0 @@
|
|||
package com.tangem.data.staking.multi
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.left
|
||||
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.YieldBalanceRequestBodyFactory
|
||||
import com.tangem.datasource.api.stakekit.StakeKitApi
|
||||
import com.tangem.datasource.api.stakekit.models.request.YieldBalanceRequestBody
|
||||
import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO
|
||||
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.multi.MultiYieldBalanceFetcher
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Default implementation of [MultiYieldBalanceFetcher]
|
||||
*
|
||||
* @property userWalletsStore user wallets store
|
||||
* @property stakingYieldsStore staking yields store
|
||||
* @property yieldsBalancesStore yields balances store
|
||||
* @property stakeKitApi stake kit API
|
||||
* @property dispatchers dispatchers
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class DefaultMultiYieldBalanceFetcher @Inject constructor(
|
||||
private val userWalletsStore: UserWalletsStore,
|
||||
private val stakingYieldsStore: StakingYieldsStore,
|
||||
private val yieldsBalancesStore: YieldsBalancesStore,
|
||||
private val stakeKitApi: StakeKitApi,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : MultiYieldBalanceFetcher {
|
||||
|
||||
override suspend fun invoke(params: MultiYieldBalanceFetcher.Params): Either<Throwable, Unit> {
|
||||
Timber.i("Start fetching yield balances for params:\n$params")
|
||||
|
||||
val stakingIds = params.stakingIds.ifEmpty {
|
||||
Timber.i("Nothing to fetch, empty stakingIds for ${params.userWalletId}")
|
||||
return Unit.right()
|
||||
}
|
||||
|
||||
checkIsSupportedByWalletOrElse(userWalletId = params.userWalletId) {
|
||||
return it.left()
|
||||
}
|
||||
|
||||
Timber.i("Staking IDs to fetch:\n${stakingIds.joinToString("\n")}")
|
||||
|
||||
return Either.catchOn(dispatchers.default) {
|
||||
yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = stakingIds)
|
||||
|
||||
val availableStakingIds = getAvailableStakingIds(
|
||||
userWalletId = params.userWalletId,
|
||||
stakingIds = stakingIds,
|
||||
)
|
||||
|
||||
fetch(userWalletId = params.userWalletId, stakingIds = availableStakingIds)
|
||||
}
|
||||
.onLeft { throwable ->
|
||||
Timber.e(throwable, "Unable to fetch yield balances $params")
|
||||
|
||||
yieldsBalancesStore.storeError(userWalletId = params.userWalletId, stakingIds = stakingIds)
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun checkIsSupportedByWalletOrElse(userWalletId: UserWalletId, ifNotSupported: (Throwable) -> Unit) {
|
||||
val maybeUserWallet = userWalletsStore.getSyncOrNull(key = userWalletId).toOption()
|
||||
|
||||
val isSupportedByWallet = maybeUserWallet.isSome(UserWallet::isMultiCurrency)
|
||||
|
||||
if (!isSupportedByWallet) {
|
||||
val exception = IllegalStateException("Wallet $userWalletId is not supported: $maybeUserWallet")
|
||||
Timber.e(exception)
|
||||
|
||||
ifNotSupported(exception)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getAvailableStakingIds(userWalletId: UserWalletId, stakingIds: Set<StakingID>): Set<StakingID> {
|
||||
val yieldIds = getYieldsIds(userWalletId = userWalletId)
|
||||
|
||||
// [true] -> available
|
||||
// [false] -> unavailable
|
||||
val groupedStakingIds = stakingIds.groupBy { stakingId ->
|
||||
yieldIds.any { it == stakingId.integrationId }
|
||||
}
|
||||
|
||||
val availableStakingIds = groupedStakingIds[true].orEmpty()
|
||||
val unavailableStakingIds = groupedStakingIds[false].orEmpty()
|
||||
|
||||
Timber.i(
|
||||
"""
|
||||
Available staking IDs: ${availableStakingIds.joinToString()}
|
||||
Unavailable staking IDs: ${unavailableStakingIds.joinToString()}
|
||||
""".trimIndent(),
|
||||
)
|
||||
|
||||
if (unavailableStakingIds.isNotEmpty()) {
|
||||
yieldsBalancesStore.storeError(userWalletId = userWalletId, stakingIds = unavailableStakingIds.toSet())
|
||||
}
|
||||
|
||||
return availableStakingIds.toSet().ifEmpty {
|
||||
val exception = IllegalStateException(
|
||||
"""
|
||||
No available yields to fetch yield balances:
|
||||
– userWalletId: $userWalletId
|
||||
– stakingIds: ${stakingIds.joinToString()}
|
||||
""".trimIndent(),
|
||||
)
|
||||
Timber.i(exception)
|
||||
throw exception
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getYieldsIds(userWalletId: UserWalletId): Set<String> {
|
||||
val yieldsIds = stakingYieldsStore.getSyncWithTimeout().orEmpty()
|
||||
.mapNotNullTo(destination = hashSetOf(), transform = YieldDTO::id)
|
||||
|
||||
if (yieldsIds.isEmpty()) {
|
||||
val exception = IllegalStateException("No enabled yields for $userWalletId")
|
||||
Timber.e(exception)
|
||||
|
||||
throw exception
|
||||
}
|
||||
|
||||
return yieldsIds
|
||||
}
|
||||
|
||||
private suspend fun fetch(userWalletId: UserWalletId, stakingIds: Set<StakingID>) {
|
||||
safeApiCall(
|
||||
call = {
|
||||
val requests = stakingIds.map(YieldBalanceRequestBodyFactory::create)
|
||||
|
||||
val yieldBalances = coroutineScope {
|
||||
requests
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
.awaitAll()
|
||||
.flatten()
|
||||
.toSet()
|
||||
}
|
||||
|
||||
Timber.i("Successfully fetched yield balances for $userWalletId:\n${yieldBalances.joinToString("\n")}")
|
||||
yieldsBalancesStore.storeActual(userWalletId = userWalletId, values = yieldBalances)
|
||||
|
||||
if (!allResponsesReceived(requests, yieldBalances)) {
|
||||
val values = stakingIds.filter { stakingId ->
|
||||
yieldBalances.none { balanceWrapper ->
|
||||
stakingId.integrationId == balanceWrapper.integrationId &&
|
||||
stakingId.address == balanceWrapper.addresses.address
|
||||
}
|
||||
}
|
||||
|
||||
yieldsBalancesStore.storeError(userWalletId = userWalletId, stakingIds = values.toSet())
|
||||
}
|
||||
},
|
||||
onError = { throwable ->
|
||||
Timber.e(throwable, "Unable to fetch yield balances $userWalletId")
|
||||
|
||||
yieldsBalancesStore.storeError(userWalletId = userWalletId, stakingIds = stakingIds)
|
||||
|
||||
throw throwable
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun allResponsesReceived(
|
||||
requests: List<YieldBalanceRequestBody>,
|
||||
yieldBalances: Set<YieldBalanceWrapperDTO>,
|
||||
): Boolean {
|
||||
return requests.all { request ->
|
||||
yieldBalances.any { balance ->
|
||||
request.integrationId == balance.integrationId &&
|
||||
request.addresses.address == balance.addresses.address
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
package com.tangem.data.staking.multi
|
||||
|
||||
import arrow.core.Option
|
||||
import arrow.core.some
|
||||
import com.tangem.data.staking.store.YieldsBalancesStore
|
||||
import com.tangem.domain.models.staking.YieldBalance
|
||||
import com.tangem.domain.staking.multi.MultiYieldBalanceProducer
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.onEmpty
|
||||
|
||||
/**
|
||||
* Default implementation of [MultiYieldBalanceProducer]
|
||||
*
|
||||
* @property params params
|
||||
* @property yieldsBalancesStore yields balances store
|
||||
* @property dispatchers dispatchers
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class DefaultMultiYieldBalanceProducer @AssistedInject constructor(
|
||||
@Assisted val params: MultiYieldBalanceProducer.Params,
|
||||
private val yieldsBalancesStore: YieldsBalancesStore,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : MultiYieldBalanceProducer {
|
||||
|
||||
override val fallback: Option<Set<YieldBalance>> = emptySet<YieldBalance>().some()
|
||||
|
||||
override fun produce(): Flow<Set<YieldBalance>> {
|
||||
return yieldsBalancesStore.get(userWalletId = params.userWalletId)
|
||||
.distinctUntilChanged()
|
||||
.onEmpty { emit(value = hashSetOf()) }
|
||||
.flowOn(dispatchers.default)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : MultiYieldBalanceProducer.Factory {
|
||||
override fun create(params: MultiYieldBalanceProducer.Params): DefaultMultiYieldBalanceProducer
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package com.tangem.data.staking.single
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher
|
||||
import com.tangem.domain.staking.single.SingleStakingBalanceFetcher
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Default implementation of [SingleStakingBalanceFetcher]
|
||||
*
|
||||
* @property multiStakingBalanceFetcher multi staking balance fetcher
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class DefaultSingleStakingBalanceFetcher @Inject constructor(
|
||||
private val multiStakingBalanceFetcher: MultiStakingBalanceFetcher,
|
||||
) : SingleStakingBalanceFetcher {
|
||||
|
||||
override suspend fun invoke(params: SingleStakingBalanceFetcher.Params): Either<Throwable, Unit> {
|
||||
return multiStakingBalanceFetcher(
|
||||
params = MultiStakingBalanceFetcher.Params(
|
||||
userWalletId = params.userWalletId,
|
||||
stakingIds = setOf(params.stakingId),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -4,10 +4,10 @@ import arrow.core.Option
|
|||
import arrow.core.some
|
||||
import com.tangem.core.analytics.api.AnalyticsExceptionHandler
|
||||
import com.tangem.core.analytics.models.ExceptionAnalyticsEvent
|
||||
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
|
||||
import com.tangem.domain.models.staking.StakingBalance
|
||||
import com.tangem.domain.staking.multi.MultiStakingBalanceProducer
|
||||
import com.tangem.domain.staking.multi.MultiStakingBalanceSupplier
|
||||
import com.tangem.domain.staking.single.SingleStakingBalanceProducer
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.extensions.indexOfFirstOrNull
|
||||
import dagger.assisted.Assisted
|
||||
|
|
@ -20,29 +20,29 @@ import kotlinx.coroutines.flow.mapNotNull
|
|||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Default implementation of [SingleYieldBalanceProducer]
|
||||
* Default implementation of [SingleStakingBalanceProducer]
|
||||
*
|
||||
* @property params params
|
||||
* @property multiYieldBalanceSupplier multi yield balance supplier
|
||||
* @property analyticsExceptionHandler analytics exception handler
|
||||
* @property dispatchers dispatchers
|
||||
* @property params params
|
||||
* @property multiStakingBalanceSupplier multi staking balance supplier
|
||||
* @property analyticsExceptionHandler analytics exception handler
|
||||
* @property dispatchers dispatchers
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class DefaultSingleYieldBalanceProducer @AssistedInject constructor(
|
||||
@Assisted private val params: SingleYieldBalanceProducer.Params,
|
||||
private val multiYieldBalanceSupplier: MultiYieldBalanceSupplier,
|
||||
internal class DefaultSingleStakingBalanceProducer @AssistedInject constructor(
|
||||
@Assisted private val params: SingleStakingBalanceProducer.Params,
|
||||
private val multiStakingBalanceSupplier: MultiStakingBalanceSupplier,
|
||||
private val analyticsExceptionHandler: AnalyticsExceptionHandler,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : SingleYieldBalanceProducer {
|
||||
) : SingleStakingBalanceProducer {
|
||||
|
||||
override val fallback: Option<YieldBalance> = YieldBalance.Error(stakingId = params.stakingId).some()
|
||||
override val fallback: Option<StakingBalance> = StakingBalance.Error(stakingId = params.stakingId).some()
|
||||
|
||||
override fun produce(): Flow<YieldBalance> {
|
||||
Timber.i("Producing yield balance for params:\n$params")
|
||||
override fun produce(): Flow<StakingBalance> {
|
||||
Timber.i("Producing staking balance for params:\n$params")
|
||||
|
||||
return multiYieldBalanceSupplier(
|
||||
params = MultiYieldBalanceProducer.Params(userWalletId = params.userWalletId),
|
||||
return multiStakingBalanceSupplier(
|
||||
params = MultiStakingBalanceProducer.Params(userWalletId = params.userWalletId),
|
||||
)
|
||||
.mapNotNull { balances ->
|
||||
val currentStakingId = params.stakingId
|
||||
|
|
@ -65,7 +65,7 @@ internal class DefaultSingleYieldBalanceProducer @AssistedInject constructor(
|
|||
currentBalances.joinToString("\n"),
|
||||
)
|
||||
|
||||
val dataIndex = currentBalances.indexOfFirstOrNull { it is YieldBalance.Data }
|
||||
val dataIndex = currentBalances.indexOfFirstOrNull { it is StakingBalance.Data }
|
||||
|
||||
if (dataIndex != null) {
|
||||
currentBalances[dataIndex]
|
||||
|
|
@ -75,7 +75,7 @@ internal class DefaultSingleYieldBalanceProducer @AssistedInject constructor(
|
|||
} else {
|
||||
val balance = currentBalances.firstOrNull() ?: return@mapNotNull null
|
||||
|
||||
Timber.i("Yield balance found for $currentStakingId:\n$balance")
|
||||
Timber.i("Staking balance found for $currentStakingId:\n$balance")
|
||||
balance
|
||||
}
|
||||
}
|
||||
|
|
@ -84,7 +84,7 @@ internal class DefaultSingleYieldBalanceProducer @AssistedInject constructor(
|
|||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : SingleYieldBalanceProducer.Factory {
|
||||
override fun create(params: SingleYieldBalanceProducer.Params): DefaultSingleYieldBalanceProducer
|
||||
interface Factory : SingleStakingBalanceProducer.Factory {
|
||||
override fun create(params: SingleStakingBalanceProducer.Params): DefaultSingleStakingBalanceProducer
|
||||
}
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
package com.tangem.data.staking.single
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher
|
||||
import com.tangem.domain.staking.single.SingleYieldBalanceFetcher
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Default implementation of [MultiYieldBalanceFetcher]
|
||||
*
|
||||
* @property multiYieldBalanceFetcher multi yield balance fetcher
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class DefaultSingleYieldBalanceFetcher @Inject constructor(
|
||||
private val multiYieldBalanceFetcher: MultiYieldBalanceFetcher,
|
||||
) : SingleYieldBalanceFetcher {
|
||||
|
||||
override suspend fun invoke(params: SingleYieldBalanceFetcher.Params): Either<Throwable, Unit> {
|
||||
return multiYieldBalanceFetcher(
|
||||
params = MultiYieldBalanceFetcher.Params(
|
||||
userWalletId = params.userWalletId,
|
||||
stakingIds = setOf(params.stakingId),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,191 @@
|
|||
package com.tangem.data.staking.store
|
||||
|
||||
import androidx.datastore.core.DataStore
|
||||
import com.tangem.data.staking.converters.ethpool.P2PStakingBalanceConverter
|
||||
import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolAccountResponse
|
||||
import com.tangem.datasource.local.datastore.RuntimeSharedStore
|
||||
import com.tangem.domain.models.StatusSource
|
||||
import com.tangem.domain.models.staking.StakingBalance
|
||||
import com.tangem.domain.models.staking.StakingID
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.staking.model.StakingIntegrationID
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.extensions.addOrReplace
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
internal typealias WalletIdWithP2PStakingBalances = Map<UserWalletId, Set<StakingBalance>>
|
||||
internal typealias WalletIdWithP2PResponses = Map<String, Set<P2PEthPoolAccountResponse>>
|
||||
|
||||
/**
|
||||
* Default implementation of [P2PBalancesStore]
|
||||
*
|
||||
* Stores P2P ETH Pool staking balances.
|
||||
*
|
||||
* @property runtimeStore runtime store
|
||||
* @property persistenceStore persistence store
|
||||
* @param dispatchers coroutine dispatchers
|
||||
*/
|
||||
internal class DefaultP2PBalancesStore(
|
||||
private val runtimeStore: RuntimeSharedStore<WalletIdWithP2PStakingBalances>,
|
||||
private val persistenceStore: DataStore<WalletIdWithP2PResponses>,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
) : P2PBalancesStore {
|
||||
|
||||
private val scope = CoroutineScope(context = SupervisorJob() + dispatchers.io)
|
||||
|
||||
init {
|
||||
scope.launch {
|
||||
val cachedData = persistenceStore.data.firstOrNull() ?: return@launch
|
||||
|
||||
runtimeStore.store(
|
||||
value = cachedData.map { (stringWalletId, responses) ->
|
||||
val key = UserWalletId(stringWalletId)
|
||||
val value = responses.map { response ->
|
||||
P2PStakingBalanceConverter.convert(
|
||||
response = response,
|
||||
source = StatusSource.CACHE,
|
||||
)
|
||||
}.toSet()
|
||||
|
||||
key to value
|
||||
}.toMap(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun get(userWalletId: UserWalletId): Flow<Set<StakingBalance>> {
|
||||
return runtimeStore.get().map { it[userWalletId].orEmpty() }
|
||||
}
|
||||
|
||||
override suspend fun getSyncOrNull(userWalletId: UserWalletId, stakingId: StakingID): StakingBalance? {
|
||||
return runtimeStore.getSyncOrNull()
|
||||
?.get(userWalletId)
|
||||
?.firstOrNull { it.stakingId == stakingId }
|
||||
}
|
||||
|
||||
override suspend fun getAllSyncOrNull(userWalletId: UserWalletId): Set<StakingBalance>? {
|
||||
return runtimeStore.getSyncOrNull()?.get(userWalletId)
|
||||
}
|
||||
|
||||
override suspend fun refresh(userWalletId: UserWalletId, stakingId: StakingID) {
|
||||
refresh(userWalletId = userWalletId, stakingIds = setOf(stakingId))
|
||||
}
|
||||
|
||||
override suspend fun refresh(userWalletId: UserWalletId, stakingIds: Set<StakingID>) {
|
||||
updateInRuntime(userWalletId = userWalletId, stakingIds = stakingIds) {
|
||||
it.copySealed(source = StatusSource.CACHE)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun storeActual(userWalletId: UserWalletId, values: Set<P2PEthPoolAccountResponse>) {
|
||||
coroutineScope {
|
||||
launch { storeInRuntime(userWalletId = userWalletId, values = values) }
|
||||
launch { storeInPersistence(userWalletId = userWalletId, values = values) }
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun storeError(userWalletId: UserWalletId, stakingIds: Set<StakingID>) {
|
||||
updateInRuntime(
|
||||
userWalletId = userWalletId,
|
||||
stakingIds = stakingIds,
|
||||
ifNotFound = ::createErrorStakingBalance,
|
||||
update = { it.copySealed(source = StatusSource.ONLY_CACHE) },
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun clear(userWalletId: UserWalletId, stakingIds: Set<StakingID>) {
|
||||
coroutineScope {
|
||||
launch { clearInRuntime(userWalletId = userWalletId, stakingIds = stakingIds) }
|
||||
launch { clearInPersistence(userWalletId = userWalletId, stakingIds = stakingIds) }
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun storeInRuntime(userWalletId: UserWalletId, values: Set<P2PEthPoolAccountResponse>) {
|
||||
val newBalances = values.map { response ->
|
||||
P2PStakingBalanceConverter.convert(
|
||||
response = response,
|
||||
source = StatusSource.ACTUAL,
|
||||
)
|
||||
}.toSet()
|
||||
|
||||
runtimeStore.update(default = emptyMap()) { saved ->
|
||||
saved.toMutableMap().apply {
|
||||
this[userWalletId] = saved[userWalletId]
|
||||
?.addOrReplace(newBalances) { old, new -> old.stakingId == new.stakingId }
|
||||
?: newBalances
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun storeInPersistence(userWalletId: UserWalletId, values: Set<P2PEthPoolAccountResponse>) {
|
||||
persistenceStore.updateData { current ->
|
||||
current.toMutableMap().apply {
|
||||
this[userWalletId.stringValue] = this[userWalletId.stringValue]
|
||||
?.addOrReplace(values) { old, new ->
|
||||
old.delegatorAddress == new.delegatorAddress && old.vaultAddress == new.vaultAddress
|
||||
}
|
||||
?: values
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun clearInRuntime(userWalletId: UserWalletId, stakingIds: Set<StakingID>) {
|
||||
runtimeStore.update(default = emptyMap()) { stored ->
|
||||
stored.toMutableMap().apply {
|
||||
this[userWalletId] = this[userWalletId].orEmpty()
|
||||
.filterNot { it.stakingId in stakingIds }
|
||||
.toSet()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun clearInPersistence(userWalletId: UserWalletId, stakingIds: Set<StakingID>) {
|
||||
val integrationIds = stakingIds.map { it.integrationId }.toSet()
|
||||
|
||||
persistenceStore.updateData { current ->
|
||||
current.toMutableMap().apply {
|
||||
this[userWalletId.stringValue] = this[userWalletId.stringValue].orEmpty()
|
||||
.filterNot { response ->
|
||||
StakingIntegrationID.P2P.EthereumPooled.value in integrationIds
|
||||
}
|
||||
.toSet()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun updateInRuntime(
|
||||
userWalletId: UserWalletId,
|
||||
stakingIds: Set<StakingID>,
|
||||
ifNotFound: (StakingID) -> StakingBalance? = { null },
|
||||
update: (StakingBalance) -> StakingBalance,
|
||||
) {
|
||||
runtimeStore.update(default = emptyMap()) { stored ->
|
||||
stored.toMutableMap().apply {
|
||||
val portfolioBalances = stored[userWalletId].orEmpty()
|
||||
|
||||
val balances = stakingIds.mapNotNullTo(hashSetOf()) { stakingId ->
|
||||
val balance = portfolioBalances
|
||||
.firstOrNull { it.stakingId == stakingId }
|
||||
?: ifNotFound(stakingId)
|
||||
?: return@mapNotNullTo null
|
||||
|
||||
update(balance)
|
||||
}
|
||||
|
||||
val updatedBalances = portfolioBalances.addOrReplace(items = balances) { old, new ->
|
||||
old.stakingId == new.stakingId
|
||||
}
|
||||
|
||||
put(key = userWalletId, value = updatedBalances)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createErrorStakingBalance(id: StakingID): StakingBalance = StakingBalance.Error(stakingId = id)
|
||||
}
|
||||
|
|
@ -3,10 +3,10 @@ package com.tangem.data.staking.store
|
|||
import androidx.datastore.core.DataStore
|
||||
import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO
|
||||
import com.tangem.datasource.local.datastore.RuntimeSharedStore
|
||||
import com.tangem.datasource.local.token.converter.YieldBalanceConverter
|
||||
import com.tangem.datasource.local.token.converter.StakingBalanceConverter
|
||||
import com.tangem.domain.models.StatusSource
|
||||
import com.tangem.domain.models.staking.StakingBalance
|
||||
import com.tangem.domain.models.staking.StakingID
|
||||
import com.tangem.domain.models.staking.YieldBalance
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.extensions.addOrReplace
|
||||
|
|
@ -19,10 +19,10 @@ import kotlinx.coroutines.flow.map
|
|||
import kotlinx.coroutines.launch
|
||||
|
||||
internal typealias WalletIdWithWrappers = Map<String, Set<YieldBalanceWrapperDTO>>
|
||||
internal typealias WalletIdWithBalances = Map<UserWalletId, Set<YieldBalance>>
|
||||
internal typealias WalletIdWithStakingBalances = Map<UserWalletId, Set<StakingBalance>>
|
||||
|
||||
/**
|
||||
* Default implementation of [YieldsBalancesStore]
|
||||
* Default implementation of [StakingBalancesStore]
|
||||
*
|
||||
* @property runtimeStore runtime store
|
||||
* @property persistenceStore persistence store
|
||||
|
|
@ -30,11 +30,11 @@ internal typealias WalletIdWithBalances = Map<UserWalletId, Set<YieldBalance>>
|
|||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class DefaultYieldsBalancesStore(
|
||||
private val runtimeStore: RuntimeSharedStore<WalletIdWithBalances>,
|
||||
internal class DefaultStakingBalancesStore(
|
||||
private val runtimeStore: RuntimeSharedStore<WalletIdWithStakingBalances>,
|
||||
private val persistenceStore: DataStore<WalletIdWithWrappers>,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
) : YieldsBalancesStore {
|
||||
) : StakingBalancesStore {
|
||||
|
||||
private val scope = CoroutineScope(context = SupervisorJob() + dispatchers.io)
|
||||
|
||||
|
|
@ -45,7 +45,7 @@ internal class DefaultYieldsBalancesStore(
|
|||
runtimeStore.store(
|
||||
value = cachedStatuses.map { (stringWalletId, wrappers) ->
|
||||
val key = UserWalletId(stringWalletId)
|
||||
val value = YieldBalanceConverter(isCached = true).convertSet(input = wrappers)
|
||||
val value = StakingBalanceConverter(isCached = true).convertSet(input = wrappers)
|
||||
.filterNotNull()
|
||||
.toSet()
|
||||
|
||||
|
|
@ -56,17 +56,17 @@ internal class DefaultYieldsBalancesStore(
|
|||
}
|
||||
}
|
||||
|
||||
override fun get(userWalletId: UserWalletId): Flow<Set<YieldBalance>> {
|
||||
override fun get(userWalletId: UserWalletId): Flow<Set<StakingBalance>> {
|
||||
return runtimeStore.get().map { it[userWalletId].orEmpty() }
|
||||
}
|
||||
|
||||
override suspend fun getSyncOrNull(userWalletId: UserWalletId, stakingId: StakingID): YieldBalance? {
|
||||
override suspend fun getSyncOrNull(userWalletId: UserWalletId, stakingId: StakingID): StakingBalance? {
|
||||
return runtimeStore.getSyncOrNull()
|
||||
?.get(userWalletId)
|
||||
?.firstOrNull { it.stakingId == stakingId }
|
||||
}
|
||||
|
||||
override suspend fun getAllSyncOrNull(userWalletId: UserWalletId): Set<YieldBalance>? {
|
||||
override suspend fun getAllSyncOrNull(userWalletId: UserWalletId): Set<StakingBalance>? {
|
||||
return runtimeStore.getSyncOrNull()?.get(userWalletId)
|
||||
}
|
||||
|
||||
|
|
@ -91,7 +91,7 @@ internal class DefaultYieldsBalancesStore(
|
|||
updateInRuntime(
|
||||
userWalletId = userWalletId,
|
||||
stakingIds = stakingIds,
|
||||
ifNotFound = ::createErrorYieldBalance,
|
||||
ifNotFound = ::createErrorStakingBalance,
|
||||
update = { it.copySealed(source = StatusSource.ONLY_CACHE) },
|
||||
)
|
||||
}
|
||||
|
|
@ -107,7 +107,7 @@ internal class DefaultYieldsBalancesStore(
|
|||
}
|
||||
|
||||
private suspend fun storeInRuntime(userWalletId: UserWalletId, values: Set<YieldBalanceWrapperDTO>) {
|
||||
val newBalances = YieldBalanceConverter(isCached = false).convertSet(input = values)
|
||||
val newBalances = StakingBalanceConverter(isCached = false).convertSet(input = values)
|
||||
.filterNotNull()
|
||||
.toSet()
|
||||
|
||||
|
|
@ -140,8 +140,8 @@ internal class DefaultYieldsBalancesStore(
|
|||
private suspend fun updateInRuntime(
|
||||
userWalletId: UserWalletId,
|
||||
stakingIds: Set<StakingID>,
|
||||
ifNotFound: (StakingID) -> YieldBalance? = { null },
|
||||
update: (YieldBalance) -> YieldBalance,
|
||||
ifNotFound: (StakingID) -> StakingBalance? = { null },
|
||||
update: (StakingBalance) -> StakingBalance,
|
||||
) {
|
||||
runtimeStore.update(default = emptyMap()) { stored ->
|
||||
stored.toMutableMap().apply {
|
||||
|
|
@ -165,7 +165,7 @@ internal class DefaultYieldsBalancesStore(
|
|||
}
|
||||
}
|
||||
|
||||
private fun createErrorYieldBalance(id: StakingID): YieldBalance = YieldBalance.Error(stakingId = id)
|
||||
private fun createErrorStakingBalance(id: StakingID): StakingBalance = StakingBalance.Error(stakingId = id)
|
||||
|
||||
private fun YieldBalanceWrapperDTO.getStakingId(): StakingID? {
|
||||
val integrationId = integrationId
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package com.tangem.data.staking.store
|
||||
|
||||
import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolAccountResponse
|
||||
import com.tangem.domain.models.staking.StakingBalance
|
||||
import com.tangem.domain.models.staking.StakingID
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
* Store for P2P ETH Pool staking balances
|
||||
*/
|
||||
interface P2PBalancesStore {
|
||||
|
||||
fun get(userWalletId: UserWalletId): Flow<Set<StakingBalance>>
|
||||
|
||||
suspend fun getSyncOrNull(userWalletId: UserWalletId, stakingId: StakingID): StakingBalance?
|
||||
|
||||
suspend fun getAllSyncOrNull(userWalletId: UserWalletId): Set<StakingBalance>?
|
||||
|
||||
suspend fun refresh(userWalletId: UserWalletId, stakingId: StakingID)
|
||||
|
||||
suspend fun refresh(userWalletId: UserWalletId, stakingIds: Set<StakingID>)
|
||||
|
||||
suspend fun storeActual(userWalletId: UserWalletId, values: Set<P2PEthPoolAccountResponse>)
|
||||
|
||||
suspend fun storeError(userWalletId: UserWalletId, stakingIds: Set<StakingID>)
|
||||
|
||||
suspend fun clear(userWalletId: UserWalletId, stakingIds: Set<StakingID>)
|
||||
}
|
||||
|
|
@ -1,39 +1,27 @@
|
|||
package com.tangem.data.staking.store
|
||||
|
||||
import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO
|
||||
import com.tangem.domain.models.staking.StakingBalance
|
||||
import com.tangem.domain.models.staking.StakingID
|
||||
import com.tangem.domain.models.staking.YieldBalance
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
* Store of [YieldBalance]'s set
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
interface YieldsBalancesStore {
|
||||
/** Store of StakeKit [StakingBalance] */
|
||||
interface StakingBalancesStore {
|
||||
|
||||
/** Get flow of [YieldBalance]'s set by [userWalletId] */
|
||||
fun get(userWalletId: UserWalletId): Flow<Set<YieldBalance>>
|
||||
fun get(userWalletId: UserWalletId): Flow<Set<StakingBalance>>
|
||||
|
||||
/** Get [YieldBalance] by [userWalletId] and [stakingId] synchronously or null */
|
||||
suspend fun getSyncOrNull(userWalletId: UserWalletId, stakingId: StakingID): YieldBalance?
|
||||
suspend fun getSyncOrNull(userWalletId: UserWalletId, stakingId: StakingID): StakingBalance?
|
||||
|
||||
/** Get all [YieldBalance] by [userWalletId] synchronously or null */
|
||||
suspend fun getAllSyncOrNull(userWalletId: UserWalletId): Set<YieldBalance>?
|
||||
suspend fun getAllSyncOrNull(userWalletId: UserWalletId): Set<StakingBalance>?
|
||||
|
||||
/** Refresh balance of [stakingId] by [userWalletId] */
|
||||
suspend fun refresh(userWalletId: UserWalletId, stakingId: StakingID)
|
||||
|
||||
/** Refresh balances of [stakingIds] by [userWalletId] */
|
||||
suspend fun refresh(userWalletId: UserWalletId, stakingIds: Set<StakingID>)
|
||||
|
||||
/** Store actual [values] by [userWalletId] */
|
||||
suspend fun storeActual(userWalletId: UserWalletId, values: Set<YieldBalanceWrapperDTO>)
|
||||
|
||||
/** Store error by [userWalletId] and [stakingIds] */
|
||||
suspend fun storeError(userWalletId: UserWalletId, stakingIds: Set<StakingID>)
|
||||
|
||||
/** Clear balances of [stakingIds] by [userWalletId] */
|
||||
suspend fun clear(userWalletId: UserWalletId, stakingIds: Set<StakingID>)
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
package com.tangem.data.staking.utils
|
||||
|
||||
import com.tangem.data.staking.store.YieldsBalancesStore
|
||||
import com.tangem.data.staking.store.StakingBalancesStore
|
||||
import com.tangem.domain.models.staking.StakingID
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.staking.utils.StakingCleaner
|
||||
|
|
@ -9,13 +9,13 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
|||
/**
|
||||
* Default implementation of [StakingCleaner].
|
||||
*
|
||||
* @property yieldsBalancesStore Store to manage yields balances.
|
||||
* @property stakingBalancesStore Store to manage staking balances.
|
||||
* @property dispatchers Coroutine dispatchers provider.
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class DefaultStakingCleaner(
|
||||
private val yieldsBalancesStore: YieldsBalancesStore,
|
||||
private val stakingBalancesStore: StakingBalancesStore,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : StakingCleaner {
|
||||
|
||||
|
|
@ -23,7 +23,7 @@ internal class DefaultStakingCleaner(
|
|||
if (stakingIds.isEmpty()) return
|
||||
|
||||
with(dispatchers.default) {
|
||||
yieldsBalancesStore.clear(userWalletId, stakingIds)
|
||||
stakingBalancesStore.clear(userWalletId, stakingIds)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package com.tangem.data.staking
|
||||
|
||||
import com.tangem.data.staking.converters.ethpool.P2PStakingBalanceConverter
|
||||
import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolAccountResponse
|
||||
import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO
|
||||
import com.tangem.datasource.local.token.converter.StakingBalanceConverter
|
||||
import com.tangem.domain.models.StatusSource
|
||||
import com.tangem.domain.models.staking.StakingBalance
|
||||
|
||||
internal fun YieldBalanceWrapperDTO.toDomain(source: StatusSource = StatusSource.CACHE): StakingBalance {
|
||||
return StakingBalanceConverter(isCached = source == StatusSource.CACHE).convert(this)!!
|
||||
}
|
||||
|
||||
internal fun P2PEthPoolAccountResponse.toDomain(source: StatusSource = StatusSource.CACHE): StakingBalance.Data.P2P {
|
||||
return P2PStakingBalanceConverter.convert(
|
||||
response = this,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
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.models.staking.YieldBalance
|
||||
|
||||
internal fun YieldBalanceWrapperDTO.toDomain(source: StatusSource = StatusSource.CACHE): YieldBalance {
|
||||
return YieldBalanceConverter(source = source).convert(this)!!
|
||||
}
|
||||
|
|
@ -4,17 +4,20 @@ import arrow.core.toOption
|
|||
import com.tangem.common.test.data.staking.MockYieldBalanceWrapperDTOFactory
|
||||
import com.tangem.common.test.data.staking.MockYieldDTOFactory
|
||||
import com.tangem.common.test.domain.wallet.MockUserWalletFactory
|
||||
import com.tangem.data.staking.store.YieldsBalancesStore
|
||||
import com.tangem.data.staking.store.P2PBalancesStore
|
||||
import com.tangem.data.staking.store.StakingBalancesStore
|
||||
import com.tangem.data.staking.utils.YieldBalanceRequestBodyFactory
|
||||
import com.tangem.datasource.api.common.response.ApiResponse
|
||||
import com.tangem.datasource.api.common.response.ApiResponseError
|
||||
import com.tangem.datasource.api.ethpool.P2PEthPoolApi
|
||||
import com.tangem.datasource.api.stakekit.StakeKitApi
|
||||
import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO
|
||||
import com.tangem.datasource.local.token.P2PEthPoolVaultsStore
|
||||
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.multi.MultiYieldBalanceFetcher
|
||||
import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher
|
||||
import com.tangem.test.core.assertEitherLeft
|
||||
import com.tangem.test.core.assertEitherRight
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
|
|
@ -28,30 +31,36 @@ import org.junit.jupiter.api.TestInstance
|
|||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class DefaultMultiYieldBalanceFetcherTest {
|
||||
internal class DefaultMultiStakingBalanceFetcherTest {
|
||||
|
||||
private val userWalletsStore: UserWalletsStore = mockk()
|
||||
private val stakingYieldsStore: StakingYieldsStore = mockk()
|
||||
private val yieldsBalancesStore: YieldsBalancesStore = mockk(relaxUnitFun = true)
|
||||
private val stakingBalancesStore: StakingBalancesStore = mockk(relaxUnitFun = true)
|
||||
private val p2pBalancesStore: P2PBalancesStore = mockk(relaxUnitFun = true)
|
||||
private val stakeKitApi: StakeKitApi = mockk()
|
||||
private val p2pApi: P2PEthPoolApi = mockk()
|
||||
private val p2pVaultsStore: P2PEthPoolVaultsStore = mockk()
|
||||
|
||||
private val fetcher = DefaultMultiYieldBalanceFetcher(
|
||||
private val fetcher = DefaultMultiStakingBalanceFetcher(
|
||||
userWalletsStore = userWalletsStore,
|
||||
stakingYieldsStore = stakingYieldsStore,
|
||||
yieldsBalancesStore = yieldsBalancesStore,
|
||||
stakingBalancesStore = stakingBalancesStore,
|
||||
p2pBalancesStore = p2pBalancesStore,
|
||||
stakeKitApi = stakeKitApi,
|
||||
p2pApi = p2pApi,
|
||||
p2pVaultsStore = p2pVaultsStore,
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
)
|
||||
|
||||
@BeforeEach
|
||||
fun resetMocks() {
|
||||
clearMocks(userWalletsStore, stakingYieldsStore, yieldsBalancesStore, stakeKitApi)
|
||||
clearMocks(userWalletsStore, stakingYieldsStore, stakingBalancesStore, stakeKitApi)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fetch yields balances successfully`() = runTest {
|
||||
fun `fetch staking balances successfully`() = runTest {
|
||||
// Arrange
|
||||
val params = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds)
|
||||
val params = MultiStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds)
|
||||
|
||||
coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet
|
||||
|
||||
|
|
@ -72,21 +81,21 @@ internal class DefaultMultiYieldBalanceFetcherTest {
|
|||
// Assert
|
||||
coVerifyOrder {
|
||||
userWalletsStore.getSyncOrNull(params.userWalletId)
|
||||
yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds)
|
||||
stakingBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds)
|
||||
stakingYieldsStore.getSyncWithTimeout()
|
||||
stakeKitApi.getMultipleYieldBalances(requests)
|
||||
yieldsBalancesStore.storeActual(userWalletId = userWalletId, values = result)
|
||||
stakingBalancesStore.storeActual(userWalletId = userWalletId, values = result)
|
||||
}
|
||||
|
||||
coVerify(inverse = true) { yieldsBalancesStore.storeError(any(), any()) }
|
||||
coVerify(inverse = true) { stakingBalancesStore.storeError(any(), any()) }
|
||||
|
||||
assertEitherRight(actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fetch yields balances successfully if one of stakingIds is unavailable`() = runTest {
|
||||
fun `fetch staking balances successfully if one of stakingIds is unavailable`() = runTest {
|
||||
// Arrange
|
||||
val params = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds)
|
||||
val params = MultiStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds)
|
||||
|
||||
coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet
|
||||
|
||||
|
|
@ -104,20 +113,20 @@ internal class DefaultMultiYieldBalanceFetcherTest {
|
|||
// Assert
|
||||
coVerifyOrder {
|
||||
userWalletsStore.getSyncOrNull(params.userWalletId)
|
||||
yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds)
|
||||
stakingBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds)
|
||||
stakingYieldsStore.getSyncWithTimeout()
|
||||
yieldsBalancesStore.storeError(userWalletId = userWalletId, stakingIds = setOf(solanaId))
|
||||
stakingBalancesStore.storeError(userWalletId = userWalletId, stakingIds = setOf(solanaId))
|
||||
stakeKitApi.getMultipleYieldBalances(requests)
|
||||
yieldsBalancesStore.storeActual(userWalletId = userWalletId, values = result)
|
||||
stakingBalancesStore.storeActual(userWalletId = userWalletId, values = result)
|
||||
}
|
||||
|
||||
assertEitherRight(actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fetch yields balances failure if user wallet is not supported`() = runTest {
|
||||
fun `fetch staking balances failure if user wallet is not supported`() = runTest {
|
||||
// Arrange
|
||||
val params = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds)
|
||||
val params = MultiStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds)
|
||||
|
||||
val userWallet = MockUserWalletFactory.create().copy(isMultiCurrency = false)
|
||||
coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet
|
||||
|
|
@ -129,11 +138,11 @@ internal class DefaultMultiYieldBalanceFetcherTest {
|
|||
coVerifyOrder { userWalletsStore.getSyncOrNull(params.userWalletId) }
|
||||
|
||||
coVerify(inverse = true) {
|
||||
yieldsBalancesStore.refresh(userWalletId = any(), stakingIds = any())
|
||||
stakingBalancesStore.refresh(userWalletId = any(), stakingIds = any())
|
||||
stakingYieldsStore.getSyncWithTimeout()
|
||||
stakeKitApi.getSingleYieldBalance(integrationId = any(), body = any())
|
||||
yieldsBalancesStore.storeActual(userWalletId = any(), values = any())
|
||||
yieldsBalancesStore.storeError(userWalletId = any(), stakingIds = any())
|
||||
stakingBalancesStore.storeActual(userWalletId = any(), values = any())
|
||||
stakingBalancesStore.storeError(userWalletId = any(), stakingIds = any())
|
||||
}
|
||||
|
||||
val expected = IllegalStateException("Wallet ${params.userWalletId} is not supported: ${userWallet.toOption()}")
|
||||
|
|
@ -142,9 +151,9 @@ internal class DefaultMultiYieldBalanceFetcherTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `fetch yields balances failure if userWalletsStore returns null`() = runTest {
|
||||
fun `fetch staking balances failure if userWalletsStore returns null`() = runTest {
|
||||
// Arrange
|
||||
val params = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds)
|
||||
val params = MultiStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds)
|
||||
|
||||
coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns null
|
||||
|
||||
|
|
@ -155,11 +164,11 @@ internal class DefaultMultiYieldBalanceFetcherTest {
|
|||
coVerifyOrder { userWalletsStore.getSyncOrNull(params.userWalletId) }
|
||||
|
||||
coVerify(inverse = true) {
|
||||
yieldsBalancesStore.refresh(userWalletId = any(), stakingIds = any())
|
||||
stakingBalancesStore.refresh(userWalletId = any(), stakingIds = any())
|
||||
stakingYieldsStore.getSyncWithTimeout()
|
||||
stakeKitApi.getSingleYieldBalance(integrationId = any(), body = any())
|
||||
yieldsBalancesStore.storeActual(userWalletId = any(), values = any())
|
||||
yieldsBalancesStore.storeError(userWalletId = any(), stakingIds = any())
|
||||
stakingBalancesStore.storeActual(userWalletId = any(), values = any())
|
||||
stakingBalancesStore.storeError(userWalletId = any(), stakingIds = any())
|
||||
}
|
||||
|
||||
val expected = IllegalStateException("Wallet ${params.userWalletId} is not supported: ${null.toOption()}")
|
||||
|
|
@ -168,9 +177,9 @@ internal class DefaultMultiYieldBalanceFetcherTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `fetch yields balances failure if stakingYieldsStore getSyncWithTimeout returns null`() = runTest {
|
||||
fun `fetch staking balances failure if stakingYieldsStore getSyncWithTimeout returns null`() = runTest {
|
||||
// Arrange
|
||||
val params = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds)
|
||||
val params = MultiStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds)
|
||||
|
||||
coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet
|
||||
coEvery { stakingYieldsStore.getSyncWithTimeout() } returns null
|
||||
|
|
@ -181,14 +190,14 @@ internal class DefaultMultiYieldBalanceFetcherTest {
|
|||
// Assert
|
||||
coVerifyOrder {
|
||||
userWalletsStore.getSyncOrNull(params.userWalletId)
|
||||
yieldsBalancesStore.refresh(params.userWalletId, tonAndSolanaIds)
|
||||
stakingBalancesStore.refresh(params.userWalletId, tonAndSolanaIds)
|
||||
stakingYieldsStore.getSyncWithTimeout()
|
||||
yieldsBalancesStore.storeError(userWalletId, tonAndSolanaIds)
|
||||
stakingBalancesStore.storeError(userWalletId, tonAndSolanaIds)
|
||||
}
|
||||
|
||||
coVerify(inverse = true) {
|
||||
stakeKitApi.getMultipleYieldBalances(any())
|
||||
yieldsBalancesStore.storeActual(userWalletId = any(), values = any())
|
||||
stakingBalancesStore.storeActual(userWalletId = any(), values = any())
|
||||
}
|
||||
|
||||
val expected = IllegalStateException("No enabled yields for ${params.userWalletId}")
|
||||
|
|
@ -197,9 +206,9 @@ internal class DefaultMultiYieldBalanceFetcherTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `fetch yields balances failure if stakingYieldsStore getSyncWithTimeout returns empty list`() = runTest {
|
||||
fun `fetch staking balances failure if stakingYieldsStore getSyncWithTimeout returns empty list`() = runTest {
|
||||
// Arrange
|
||||
val params = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds)
|
||||
val params = MultiStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds)
|
||||
|
||||
coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet
|
||||
coEvery { stakingYieldsStore.getSyncWithTimeout() } returns emptyList()
|
||||
|
|
@ -210,14 +219,14 @@ internal class DefaultMultiYieldBalanceFetcherTest {
|
|||
// Assert
|
||||
coVerifyOrder {
|
||||
userWalletsStore.getSyncOrNull(params.userWalletId)
|
||||
yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds)
|
||||
stakingBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds)
|
||||
stakingYieldsStore.getSyncWithTimeout()
|
||||
yieldsBalancesStore.storeError(userWalletId, tonAndSolanaIds)
|
||||
stakingBalancesStore.storeError(userWalletId, tonAndSolanaIds)
|
||||
}
|
||||
|
||||
coVerify(inverse = true) {
|
||||
stakeKitApi.getMultipleYieldBalances(any())
|
||||
yieldsBalancesStore.storeActual(userWalletId = any(), values = any())
|
||||
stakingBalancesStore.storeActual(userWalletId = any(), values = any())
|
||||
}
|
||||
|
||||
val expected = IllegalStateException("No enabled yields for ${params.userWalletId}")
|
||||
|
|
@ -226,9 +235,9 @@ internal class DefaultMultiYieldBalanceFetcherTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `fetch yields balances failure if yields converting is failed`() = runTest {
|
||||
fun `fetch staking balances failure if yields converting is failed`() = runTest {
|
||||
// Arrange
|
||||
val params = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds)
|
||||
val params = MultiStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds)
|
||||
|
||||
coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet
|
||||
|
||||
|
|
@ -244,14 +253,14 @@ internal class DefaultMultiYieldBalanceFetcherTest {
|
|||
// Assert
|
||||
coVerifyOrder {
|
||||
userWalletsStore.getSyncOrNull(params.userWalletId)
|
||||
yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds)
|
||||
stakingBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds)
|
||||
stakingYieldsStore.getSyncWithTimeout()
|
||||
yieldsBalancesStore.storeError(userWalletId, tonAndSolanaIds)
|
||||
stakingBalancesStore.storeError(userWalletId, tonAndSolanaIds)
|
||||
}
|
||||
|
||||
coVerify(inverse = true) {
|
||||
stakeKitApi.getMultipleYieldBalances(any())
|
||||
yieldsBalancesStore.storeActual(userWalletId = any(), values = any())
|
||||
stakingBalancesStore.storeActual(userWalletId = any(), values = any())
|
||||
}
|
||||
|
||||
val expected = IllegalStateException("No enabled yields for ${params.userWalletId}")
|
||||
|
|
@ -260,9 +269,9 @@ internal class DefaultMultiYieldBalanceFetcherTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `fetch yields balances failure if available yields does not contain ids from params`() = runTest {
|
||||
fun `fetch staking balances failure if available yields does not contain ids from params`() = runTest {
|
||||
// Arrange
|
||||
val params = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds)
|
||||
val params = MultiStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds)
|
||||
|
||||
coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet
|
||||
|
||||
|
|
@ -275,14 +284,14 @@ internal class DefaultMultiYieldBalanceFetcherTest {
|
|||
// Assert
|
||||
coVerifyOrder {
|
||||
userWalletsStore.getSyncOrNull(params.userWalletId)
|
||||
yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds)
|
||||
stakingBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds)
|
||||
stakingYieldsStore.getSyncWithTimeout()
|
||||
yieldsBalancesStore.storeError(userWalletId, tonAndSolanaIds)
|
||||
stakingBalancesStore.storeError(userWalletId, tonAndSolanaIds)
|
||||
}
|
||||
|
||||
coVerify(inverse = true) {
|
||||
stakeKitApi.getMultipleYieldBalances(any())
|
||||
yieldsBalancesStore.storeActual(userWalletId = any(), values = any())
|
||||
stakingBalancesStore.storeActual(userWalletId = any(), values = any())
|
||||
}
|
||||
|
||||
val expected = IllegalStateException(
|
||||
|
|
@ -297,9 +306,9 @@ internal class DefaultMultiYieldBalanceFetcherTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `fetch yields balances failure if stakeKitApi getMultipleYieldBalances is failed`() = runTest {
|
||||
fun `fetch staking balances failure if stakeKitApi getMultipleYieldBalances is failed`() = runTest {
|
||||
// Arrange
|
||||
val params = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds)
|
||||
val params = MultiStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds)
|
||||
|
||||
coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet
|
||||
|
||||
|
|
@ -320,13 +329,13 @@ internal class DefaultMultiYieldBalanceFetcherTest {
|
|||
// Assert
|
||||
coVerifyOrder {
|
||||
userWalletsStore.getSyncOrNull(params.userWalletId)
|
||||
yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds)
|
||||
stakingBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds)
|
||||
stakingYieldsStore.getSyncWithTimeout()
|
||||
stakeKitApi.getMultipleYieldBalances(requests)
|
||||
yieldsBalancesStore.storeError(userWalletId = userWalletId, stakingIds = tonAndSolanaIds)
|
||||
stakingBalancesStore.storeError(userWalletId = userWalletId, stakingIds = tonAndSolanaIds)
|
||||
}
|
||||
|
||||
coVerify(inverse = true) { yieldsBalancesStore.storeActual(userWalletId = any(), values = any()) }
|
||||
coVerify(inverse = true) { stakingBalancesStore.storeActual(userWalletId = any(), values = any()) }
|
||||
|
||||
val expected = ApiResponseError.NetworkException()
|
||||
|
||||
|
|
@ -0,0 +1,283 @@
|
|||
package com.tangem.data.staking.multi
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.common.test.data.staking.MockYieldBalanceWrapperDTOFactory
|
||||
import com.tangem.common.test.data.staking.MockP2PEthPoolAccountResponseFactory
|
||||
import com.tangem.data.staking.store.P2PBalancesStore
|
||||
import com.tangem.data.staking.store.StakingBalancesStore
|
||||
import com.tangem.data.staking.toDomain
|
||||
import com.tangem.domain.models.StatusSource
|
||||
import com.tangem.domain.models.staking.*
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.staking.model.StakingIntegrationID
|
||||
import com.tangem.domain.staking.multi.MultiStakingBalanceProducer
|
||||
import com.tangem.test.core.getEmittedValues
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
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
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class DefaultMultiStakingBalanceProducerTest {
|
||||
|
||||
private val params = MultiStakingBalanceProducer.Params(userWalletId = UserWalletId("011"))
|
||||
|
||||
private val stakingBalancesStore = mockk<StakingBalancesStore>()
|
||||
private val p2pBalancesStore = mockk<P2PBalancesStore>()
|
||||
private val dispatchers = TestingCoroutineDispatcherProvider()
|
||||
|
||||
private val producer = DefaultMultiStakingBalanceProducer(
|
||||
params = params,
|
||||
stakingBalancesStore = stakingBalancesStore,
|
||||
p2pBalancesStore = p2pBalancesStore,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `test that flow is mapped for user wallet id from params`() = runTest {
|
||||
val balances = setOf(
|
||||
MockYieldBalanceWrapperDTOFactory.createWithBalance(tonId).toDomain(),
|
||||
MockYieldBalanceWrapperDTOFactory.createWithBalance(solanaId).toDomain(),
|
||||
)
|
||||
|
||||
val networksStatusesFlow = flowOf(balances)
|
||||
|
||||
every { stakingBalancesStore.get(params.userWalletId) } returns networksStatusesFlow
|
||||
every { p2pBalancesStore.get(params.userWalletId) } returns flowOf(emptySet())
|
||||
|
||||
val actual = producer.produce()
|
||||
|
||||
// check after producer.produce()
|
||||
verify { stakingBalancesStore.get(params.userWalletId) }
|
||||
verify { p2pBalancesStore.get(params.userWalletId) }
|
||||
|
||||
val values = getEmittedValues(flow = actual)
|
||||
|
||||
Truth.assertThat(values.size).isEqualTo(1)
|
||||
Truth.assertThat(values.first()).isEqualTo(balances)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test that flow is updated if balances are updated`() = runTest {
|
||||
val networksStatusesFlow = MutableSharedFlow<Set<StakingBalance>>(replay = 2)
|
||||
|
||||
every { stakingBalancesStore.get(params.userWalletId) } returns networksStatusesFlow
|
||||
every { p2pBalancesStore.get(params.userWalletId) } returns flowOf(emptySet())
|
||||
|
||||
val actual = producer.produce()
|
||||
|
||||
// check after producer.produce()
|
||||
verify { stakingBalancesStore.get(params.userWalletId) }
|
||||
verify { p2pBalancesStore.get(params.userWalletId) }
|
||||
|
||||
// first emit
|
||||
val balances = setOf(
|
||||
MockYieldBalanceWrapperDTOFactory.createWithEmptyBalance(tonId).toDomain(),
|
||||
MockYieldBalanceWrapperDTOFactory.createWithEmptyBalance(solanaId).toDomain(),
|
||||
)
|
||||
|
||||
networksStatusesFlow.emit(balances)
|
||||
|
||||
val values1 = getEmittedValues(flow = actual)
|
||||
|
||||
Truth.assertThat(values1.size).isEqualTo(1)
|
||||
Truth.assertThat(values1.first()).isEqualTo(balances)
|
||||
|
||||
// second emit
|
||||
val updatedWrappers = setOf(
|
||||
MockYieldBalanceWrapperDTOFactory.createWithBalance(tonId).toDomain(),
|
||||
MockYieldBalanceWrapperDTOFactory.createWithBalance(solanaId).toDomain(),
|
||||
)
|
||||
|
||||
networksStatusesFlow.emit(updatedWrappers)
|
||||
|
||||
val values2 = getEmittedValues(flow = actual)
|
||||
|
||||
val expected = listOf(balances, updatedWrappers)
|
||||
Truth.assertThat(values2.size).isEqualTo(2)
|
||||
Truth.assertThat(values2).isEqualTo(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test that flow is filtered the same balance`() = runTest {
|
||||
val networksStatusesFlow = MutableSharedFlow<Set<StakingBalance>>(replay = 2)
|
||||
|
||||
every { stakingBalancesStore.get(params.userWalletId) } returns networksStatusesFlow
|
||||
every { p2pBalancesStore.get(params.userWalletId) } returns flowOf(emptySet())
|
||||
|
||||
val actual = producer.produce()
|
||||
|
||||
// check after producer.produce()
|
||||
verify { stakingBalancesStore.get(params.userWalletId) }
|
||||
verify { p2pBalancesStore.get(params.userWalletId) }
|
||||
|
||||
// first emit
|
||||
val wrappers = setOf(
|
||||
MockYieldBalanceWrapperDTOFactory.createWithEmptyBalance(tonId).toDomain(),
|
||||
MockYieldBalanceWrapperDTOFactory.createWithEmptyBalance(solanaId).toDomain(),
|
||||
)
|
||||
|
||||
networksStatusesFlow.emit(wrappers)
|
||||
|
||||
val values1 = getEmittedValues(flow = actual)
|
||||
|
||||
Truth.assertThat(values1.size).isEqualTo(1)
|
||||
Truth.assertThat(values1.first()).isEqualTo(wrappers)
|
||||
|
||||
// second emit
|
||||
networksStatusesFlow.emit(wrappers)
|
||||
|
||||
val values2 = getEmittedValues(flow = actual)
|
||||
|
||||
Truth.assertThat(values2.size).isEqualTo(1)
|
||||
Truth.assertThat(values2.first()).isEqualTo(wrappers)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test if flow throws exception`() = runTest {
|
||||
val exception = IllegalStateException()
|
||||
val balances = setOf(
|
||||
MockYieldBalanceWrapperDTOFactory.createWithBalance(tonId).toDomain(),
|
||||
MockYieldBalanceWrapperDTOFactory.createWithBalance(solanaId).toDomain(),
|
||||
)
|
||||
|
||||
val innerFlow = MutableStateFlow(value = false)
|
||||
val networksStatusesFlow = flow {
|
||||
if (innerFlow.value) {
|
||||
emit(balances)
|
||||
} else {
|
||||
throw exception
|
||||
}
|
||||
}
|
||||
.buffer(capacity = 5)
|
||||
|
||||
every { stakingBalancesStore.get(params.userWalletId) } returns networksStatusesFlow
|
||||
every { p2pBalancesStore.get(params.userWalletId) } returns flowOf(emptySet())
|
||||
|
||||
val actual = producer.produceWithFallback()
|
||||
|
||||
// check after producer.produce()
|
||||
verify { stakingBalancesStore.get(params.userWalletId) }
|
||||
verify { p2pBalancesStore.get(params.userWalletId) }
|
||||
|
||||
val values1 = getEmittedValues(flow = actual)
|
||||
|
||||
Truth.assertThat(values1.size).isEqualTo(1)
|
||||
Truth.assertThat(values1).isEqualTo(listOf(emptySet<StakingBalance>()))
|
||||
|
||||
innerFlow.emit(value = true)
|
||||
|
||||
val values2 = getEmittedValues(flow = actual)
|
||||
|
||||
Truth.assertThat(values2.size).isEqualTo(1)
|
||||
Truth.assertThat(values2).isEqualTo(listOf(balances))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test that flow is empty`() = runTest {
|
||||
every { stakingBalancesStore.get(params.userWalletId) } returns emptyFlow()
|
||||
every { p2pBalancesStore.get(params.userWalletId) } returns emptyFlow()
|
||||
|
||||
val actual = producer.produce()
|
||||
|
||||
// check after producer.produce()
|
||||
verify { stakingBalancesStore.get(params.userWalletId) }
|
||||
verify { p2pBalancesStore.get(params.userWalletId) }
|
||||
|
||||
val values = getEmittedValues(flow = actual)
|
||||
|
||||
Truth.assertThat(values.size).isEqualTo(1)
|
||||
Truth.assertThat(values).isEqualTo(listOf(emptySet<StakingBalance>()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test that StakeKit and P2P balances are combined`() = runTest {
|
||||
val stakeKitBalances = createStakeKitBalances()
|
||||
val p2pBalances = createP2PBalances()
|
||||
|
||||
every { stakingBalancesStore.get(params.userWalletId) } returns flowOf(stakeKitBalances)
|
||||
every { p2pBalancesStore.get(params.userWalletId) } returns flowOf(p2pBalances)
|
||||
|
||||
val actual = producer.produce()
|
||||
|
||||
// check after producer.produce()
|
||||
verify { stakingBalancesStore.get(params.userWalletId) }
|
||||
verify { p2pBalancesStore.get(params.userWalletId) }
|
||||
|
||||
val values = getEmittedValues(flow = actual)
|
||||
|
||||
Truth.assertThat(values.size).isEqualTo(1)
|
||||
Truth.assertThat(values.first()).isEqualTo(stakeKitBalances + p2pBalances)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test that P2P balances are updated independently from StakeKit`() = runTest {
|
||||
val stakeKitBalances = createStakeKitBalancesWithTonOnly()
|
||||
val p2pFlow = MutableSharedFlow<Set<StakingBalance>>(replay = 2)
|
||||
|
||||
every { stakingBalancesStore.get(params.userWalletId) } returns flowOf(stakeKitBalances)
|
||||
every { p2pBalancesStore.get(params.userWalletId) } returns p2pFlow
|
||||
|
||||
val actual = producer.produce()
|
||||
|
||||
// check after producer.produce()
|
||||
verify { stakingBalancesStore.get(params.userWalletId) }
|
||||
verify { p2pBalancesStore.get(params.userWalletId) }
|
||||
|
||||
// first emit - empty P2P
|
||||
p2pFlow.emit(emptySet())
|
||||
|
||||
val values1 = getEmittedValues(flow = actual)
|
||||
|
||||
Truth.assertThat(values1.size).isEqualTo(1)
|
||||
Truth.assertThat(values1.first()).isEqualTo(stakeKitBalances)
|
||||
|
||||
// second emit - with P2P balance
|
||||
val p2pBalances = createP2PBalances()
|
||||
p2pFlow.emit(p2pBalances)
|
||||
|
||||
val values2 = getEmittedValues(flow = actual)
|
||||
|
||||
Truth.assertThat(values2.size).isEqualTo(2)
|
||||
Truth.assertThat(values2.last()).isEqualTo(stakeKitBalances + p2pBalances)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
||||
val tonId = MockYieldBalanceWrapperDTOFactory.defaultStakingId
|
||||
val solanaId = StakingID(
|
||||
integrationId = "solana-sol-native-multivalidator-staking",
|
||||
address = "0x1",
|
||||
)
|
||||
val p2pEthereumId = StakingID(
|
||||
integrationId = StakingIntegrationID.P2P.EthereumPooled.value,
|
||||
address = "0x5aa711F440Eb6d4361148bBD89d03464628ace84",
|
||||
)
|
||||
|
||||
fun createStakeKitBalances(): Set<StakingBalance> {
|
||||
return setOf(
|
||||
MockYieldBalanceWrapperDTOFactory.createWithBalance(tonId).toDomain(),
|
||||
MockYieldBalanceWrapperDTOFactory.createWithBalance(solanaId).toDomain(),
|
||||
)
|
||||
}
|
||||
|
||||
fun createStakeKitBalancesWithTonOnly(): Set<StakingBalance> {
|
||||
return setOf(
|
||||
MockYieldBalanceWrapperDTOFactory.createWithBalance(tonId).toDomain(),
|
||||
)
|
||||
}
|
||||
|
||||
fun createP2PBalances(): Set<StakingBalance> {
|
||||
return setOf(
|
||||
MockP2PEthPoolAccountResponseFactory.createWithBalance(stakingId = p2pEthereumId).toDomain(
|
||||
source = StatusSource.ACTUAL,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,191 +0,0 @@
|
|||
package com.tangem.data.staking.multi
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.common.test.data.staking.MockYieldBalanceWrapperDTOFactory
|
||||
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.multi.MultiYieldBalanceProducer
|
||||
import com.tangem.test.core.getEmittedValues
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
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
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class DefaultMultiYieldBalanceProducerTest {
|
||||
|
||||
private val params = MultiYieldBalanceProducer.Params(userWalletId = UserWalletId("011"))
|
||||
|
||||
private val yieldsBalancesStore = mockk<YieldsBalancesStore>()
|
||||
private val dispatchers = TestingCoroutineDispatcherProvider()
|
||||
|
||||
private val producer = DefaultMultiYieldBalanceProducer(
|
||||
params = params,
|
||||
yieldsBalancesStore = yieldsBalancesStore,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `test that flow is mapped for user wallet id from params`() = runTest {
|
||||
val balances = setOf(
|
||||
MockYieldBalanceWrapperDTOFactory.createWithBalance(tonId).toDomain(),
|
||||
MockYieldBalanceWrapperDTOFactory.createWithBalance(solanaId).toDomain(),
|
||||
)
|
||||
|
||||
val networksStatusesFlow = flowOf(balances)
|
||||
|
||||
every { yieldsBalancesStore.get(params.userWalletId) } returns networksStatusesFlow
|
||||
|
||||
val actual = producer.produce()
|
||||
|
||||
// check after producer.produce()
|
||||
verify { yieldsBalancesStore.get(params.userWalletId) }
|
||||
|
||||
val values = getEmittedValues(flow = actual)
|
||||
|
||||
Truth.assertThat(values.size).isEqualTo(1)
|
||||
Truth.assertThat(values.first()).isEqualTo(balances)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test that flow is updated if balances are updated`() = runTest {
|
||||
val networksStatusesFlow = MutableSharedFlow<Set<YieldBalance>>(replay = 2)
|
||||
|
||||
every { yieldsBalancesStore.get(params.userWalletId) } returns networksStatusesFlow
|
||||
|
||||
val actual = producer.produce()
|
||||
|
||||
// check after producer.produce()
|
||||
verify { yieldsBalancesStore.get(params.userWalletId) }
|
||||
|
||||
// first emit
|
||||
val balances = setOf(
|
||||
MockYieldBalanceWrapperDTOFactory.createWithEmptyBalance(tonId).toDomain(),
|
||||
MockYieldBalanceWrapperDTOFactory.createWithEmptyBalance(solanaId).toDomain(),
|
||||
)
|
||||
|
||||
networksStatusesFlow.emit(balances)
|
||||
|
||||
val values1 = getEmittedValues(flow = actual)
|
||||
|
||||
Truth.assertThat(values1.size).isEqualTo(1)
|
||||
Truth.assertThat(values1.first()).isEqualTo(balances)
|
||||
|
||||
// second emit
|
||||
val updatedWrappers = setOf(
|
||||
MockYieldBalanceWrapperDTOFactory.createWithBalance(tonId).toDomain(),
|
||||
MockYieldBalanceWrapperDTOFactory.createWithBalance(solanaId).toDomain(),
|
||||
)
|
||||
|
||||
networksStatusesFlow.emit(updatedWrappers)
|
||||
|
||||
val values2 = getEmittedValues(flow = actual)
|
||||
|
||||
val expected = listOf(balances, updatedWrappers)
|
||||
Truth.assertThat(values2.size).isEqualTo(2)
|
||||
Truth.assertThat(values2).isEqualTo(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test that flow is filtered the same balance`() = runTest {
|
||||
val networksStatusesFlow = MutableSharedFlow<Set<YieldBalance>>(replay = 2)
|
||||
|
||||
every { yieldsBalancesStore.get(params.userWalletId) } returns networksStatusesFlow
|
||||
|
||||
val actual = producer.produce()
|
||||
|
||||
// check after producer.produce()
|
||||
verify { yieldsBalancesStore.get(params.userWalletId) }
|
||||
|
||||
// first emit
|
||||
val wrappers = setOf(
|
||||
MockYieldBalanceWrapperDTOFactory.createWithEmptyBalance(tonId).toDomain(),
|
||||
MockYieldBalanceWrapperDTOFactory.createWithEmptyBalance(solanaId).toDomain(),
|
||||
)
|
||||
|
||||
networksStatusesFlow.emit(wrappers)
|
||||
|
||||
val values1 = getEmittedValues(flow = actual)
|
||||
|
||||
Truth.assertThat(values1.size).isEqualTo(1)
|
||||
Truth.assertThat(values1.first()).isEqualTo(wrappers)
|
||||
|
||||
// second emit
|
||||
networksStatusesFlow.emit(wrappers)
|
||||
|
||||
val values2 = getEmittedValues(flow = actual)
|
||||
|
||||
Truth.assertThat(values2.size).isEqualTo(1)
|
||||
Truth.assertThat(values2.first()).isEqualTo(wrappers)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test if flow throws exception`() = runTest {
|
||||
val exception = IllegalStateException()
|
||||
val balances = setOf(
|
||||
MockYieldBalanceWrapperDTOFactory.createWithBalance(tonId).toDomain(),
|
||||
MockYieldBalanceWrapperDTOFactory.createWithBalance(solanaId).toDomain(),
|
||||
)
|
||||
|
||||
val innerFlow = MutableStateFlow(value = false)
|
||||
val networksStatusesFlow = flow {
|
||||
if (innerFlow.value) {
|
||||
emit(balances)
|
||||
} else {
|
||||
throw exception
|
||||
}
|
||||
}
|
||||
.buffer(capacity = 5)
|
||||
|
||||
every { yieldsBalancesStore.get(params.userWalletId) } returns networksStatusesFlow
|
||||
|
||||
val actual = producer.produceWithFallback()
|
||||
|
||||
// check after producer.produce()
|
||||
verify { yieldsBalancesStore.get(params.userWalletId) }
|
||||
|
||||
val values1 = getEmittedValues(flow = actual)
|
||||
|
||||
Truth.assertThat(values1.size).isEqualTo(1)
|
||||
Truth.assertThat(values1).isEqualTo(listOf(emptySet<YieldBalance>()))
|
||||
|
||||
innerFlow.emit(value = true)
|
||||
|
||||
val values2 = getEmittedValues(flow = actual)
|
||||
|
||||
Truth.assertThat(values2.size).isEqualTo(1)
|
||||
Truth.assertThat(values2).isEqualTo(listOf(balances))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test that flow is empty`() = runTest {
|
||||
every { yieldsBalancesStore.get(params.userWalletId) } returns emptyFlow()
|
||||
|
||||
val actual = producer.produce()
|
||||
|
||||
// check after producer.produce()
|
||||
verify { yieldsBalancesStore.get(params.userWalletId) }
|
||||
|
||||
val values = getEmittedValues(flow = actual)
|
||||
|
||||
Truth.assertThat(values.size).isEqualTo(1)
|
||||
Truth.assertThat(values).isEqualTo(listOf(emptySet<YieldBalance>()))
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
||||
val tonId = MockYieldBalanceWrapperDTOFactory.defaultStakingId
|
||||
val solanaId = StakingID(
|
||||
integrationId = "solana-sol-native-multivalidator-staking",
|
||||
address = "0x1",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -5,8 +5,8 @@ import arrow.core.right
|
|||
import com.google.common.truth.Truth
|
||||
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
|
||||
import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher
|
||||
import com.tangem.domain.staking.single.SingleStakingBalanceFetcher
|
||||
import io.mockk.clearMocks
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
|
|
@ -20,32 +20,32 @@ import org.junit.jupiter.api.TestInstance
|
|||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class DefaultSingleYieldBalanceFetcherTest {
|
||||
internal class DefaultSingleStakingBalanceFetcherTest {
|
||||
|
||||
private val multiYieldBalanceFetcher: MultiYieldBalanceFetcher = mockk()
|
||||
private val multiStakingBalanceFetcher: MultiStakingBalanceFetcher = mockk()
|
||||
|
||||
private val fetcher = DefaultSingleYieldBalanceFetcher(
|
||||
multiYieldBalanceFetcher = multiYieldBalanceFetcher,
|
||||
private val fetcher = DefaultSingleStakingBalanceFetcher(
|
||||
multiStakingBalanceFetcher = multiStakingBalanceFetcher,
|
||||
)
|
||||
|
||||
@BeforeEach
|
||||
fun resetMocks() {
|
||||
clearMocks(multiYieldBalanceFetcher)
|
||||
clearMocks(multiStakingBalanceFetcher)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fetch yield balance successfully`() = runTest {
|
||||
fun `fetch staking balance successfully`() = runTest {
|
||||
// Arrange
|
||||
val params = SingleYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingId = tonId)
|
||||
val params = SingleStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingId = tonId)
|
||||
|
||||
val multiParams = MultiYieldBalanceFetcher.Params(
|
||||
val multiParams = MultiStakingBalanceFetcher.Params(
|
||||
userWalletId = userWalletId,
|
||||
stakingIds = setOf(tonId),
|
||||
)
|
||||
|
||||
val multiResult = Unit.right()
|
||||
|
||||
coEvery { multiYieldBalanceFetcher(params = multiParams) } returns multiResult
|
||||
coEvery { multiStakingBalanceFetcher(params = multiParams) } returns multiResult
|
||||
|
||||
// Act
|
||||
val actual = fetcher.invoke(params).isRight()
|
||||
|
|
@ -53,26 +53,26 @@ internal class DefaultSingleYieldBalanceFetcherTest {
|
|||
// Assert
|
||||
Truth.assertThat(actual).isTrue()
|
||||
|
||||
coVerify { multiYieldBalanceFetcher(params = multiParams) }
|
||||
coVerify { multiStakingBalanceFetcher(params = multiParams) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fetch yield balance failure`() = runTest {
|
||||
fun `fetch staking balance failure`() = runTest {
|
||||
// Arrange
|
||||
val params = SingleYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingId = tonId)
|
||||
val params = SingleStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingId = tonId)
|
||||
|
||||
val multiParams = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = setOf(tonId))
|
||||
val multiParams = MultiStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = setOf(tonId))
|
||||
|
||||
val multiResult = IllegalStateException().left()
|
||||
|
||||
coEvery { multiYieldBalanceFetcher(params = multiParams) } returns multiResult
|
||||
coEvery { multiStakingBalanceFetcher(params = multiParams) } returns multiResult
|
||||
|
||||
// Act
|
||||
val actual = fetcher.invoke(params)
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).isEqualTo(multiResult)
|
||||
coVerify { multiYieldBalanceFetcher(params = multiParams) }
|
||||
coVerify { multiStakingBalanceFetcher(params = multiParams) }
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
|
@ -4,12 +4,12 @@ import com.google.common.truth.Truth
|
|||
import com.tangem.common.test.data.staking.MockYieldBalanceWrapperDTOFactory
|
||||
import com.tangem.core.analytics.api.AnalyticsExceptionHandler
|
||||
import com.tangem.data.staking.toDomain
|
||||
import com.tangem.domain.models.staking.StakingBalance
|
||||
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.multi.MultiYieldBalanceProducer
|
||||
import com.tangem.domain.staking.multi.MultiYieldBalanceSupplier
|
||||
import com.tangem.domain.staking.single.SingleYieldBalanceProducer
|
||||
import com.tangem.domain.staking.multi.MultiStakingBalanceProducer
|
||||
import com.tangem.domain.staking.multi.MultiStakingBalanceSupplier
|
||||
import com.tangem.domain.staking.single.SingleStakingBalanceProducer
|
||||
import com.tangem.test.core.getEmittedValues
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import io.mockk.clearMocks
|
||||
|
|
@ -26,20 +26,20 @@ import org.junit.jupiter.api.TestInstance
|
|||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class DefaultSingleYieldBalanceProducerTest {
|
||||
internal class DefaultSingleStakingBalanceProducerTest {
|
||||
|
||||
private val params = SingleYieldBalanceProducer.Params(
|
||||
private val params = SingleStakingBalanceProducer.Params(
|
||||
userWalletId = UserWalletId(stringValue = "011"),
|
||||
stakingId = tonId,
|
||||
)
|
||||
|
||||
private val multiNetworkStatusSupplier = mockk<MultiYieldBalanceSupplier>()
|
||||
private val multiNetworkStatusSupplier = mockk<MultiStakingBalanceSupplier>()
|
||||
private val analyticsExceptionHandler = mockk<AnalyticsExceptionHandler>(relaxUnitFun = true)
|
||||
private val dispatchers = TestingCoroutineDispatcherProvider()
|
||||
|
||||
private val producer = DefaultSingleYieldBalanceProducer(
|
||||
private val producer = DefaultSingleStakingBalanceProducer(
|
||||
params = params,
|
||||
multiYieldBalanceSupplier = multiNetworkStatusSupplier,
|
||||
multiStakingBalanceSupplier = multiNetworkStatusSupplier,
|
||||
analyticsExceptionHandler = analyticsExceptionHandler,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
|
|
@ -61,7 +61,7 @@ internal class DefaultSingleYieldBalanceProducerTest {
|
|||
),
|
||||
)
|
||||
|
||||
val multiParams = MultiYieldBalanceProducer.Params(userWalletId = params.userWalletId)
|
||||
val multiParams = MultiStakingBalanceProducer.Params(userWalletId = params.userWalletId)
|
||||
every { multiNetworkStatusSupplier(multiParams) } returns multiFlow
|
||||
|
||||
// Act
|
||||
|
|
@ -74,17 +74,17 @@ internal class DefaultSingleYieldBalanceProducerTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `flow is updated if yield balance is updated`() = runTest {
|
||||
fun `flow is updated if staking balance is updated`() = runTest {
|
||||
// Arrange
|
||||
val multiFlow = MutableSharedFlow<Set<YieldBalance>>(replay = 2, extraBufferCapacity = 1)
|
||||
val multiFlow = MutableSharedFlow<Set<StakingBalance>>(replay = 2, extraBufferCapacity = 1)
|
||||
|
||||
val multiParams = MultiYieldBalanceProducer.Params(userWalletId = params.userWalletId)
|
||||
val multiParams = MultiStakingBalanceProducer.Params(userWalletId = params.userWalletId)
|
||||
every { multiNetworkStatusSupplier(multiParams) } returns multiFlow
|
||||
|
||||
val producerFlow = producer.produceWithFallback()
|
||||
|
||||
val balance = MockYieldBalanceWrapperDTOFactory.createWithBalance(tonId).toDomain()
|
||||
val updatedBalance = YieldBalance.Error(stakingId = tonId)
|
||||
val updatedBalance = StakingBalance.Error(stakingId = tonId)
|
||||
|
||||
// Act (first emit)
|
||||
multiFlow.emit(value = setOf(balance))
|
||||
|
|
@ -108,9 +108,9 @@ internal class DefaultSingleYieldBalanceProducerTest {
|
|||
@Test
|
||||
fun `flow is filtered the same status`() = runTest {
|
||||
// Arrange
|
||||
val multiFlow = MutableSharedFlow<Set<YieldBalance>>(replay = 2, extraBufferCapacity = 1)
|
||||
val multiFlow = MutableSharedFlow<Set<StakingBalance>>(replay = 2, extraBufferCapacity = 1)
|
||||
|
||||
val multiParams = MultiYieldBalanceProducer.Params(userWalletId = params.userWalletId)
|
||||
val multiParams = MultiStakingBalanceProducer.Params(userWalletId = params.userWalletId)
|
||||
every { multiNetworkStatusSupplier(multiParams) } returns multiFlow
|
||||
|
||||
val producerFlow = producer.produceWithFallback()
|
||||
|
|
@ -153,7 +153,7 @@ internal class DefaultSingleYieldBalanceProducerTest {
|
|||
}
|
||||
.buffer(capacity = 5)
|
||||
|
||||
val multiParams = MultiYieldBalanceProducer.Params(userWalletId = params.userWalletId)
|
||||
val multiParams = MultiStakingBalanceProducer.Params(userWalletId = params.userWalletId)
|
||||
every { multiNetworkStatusSupplier(multiParams) } returns multiFlow
|
||||
|
||||
val producerFlow = producer.produceWithFallback()
|
||||
|
|
@ -162,7 +162,7 @@ internal class DefaultSingleYieldBalanceProducerTest {
|
|||
val actual1 = getEmittedValues(flow = producerFlow)
|
||||
|
||||
// Assert (first emit)
|
||||
val fallbackStatus = YieldBalance.Error(stakingId = tonId.copy(address = "0x1"))
|
||||
val fallbackStatus = StakingBalance.Error(stakingId = tonId.copy(address = "0x1"))
|
||||
|
||||
Truth.assertThat(actual1).hasSize(1)
|
||||
Truth.assertThat(actual1).containsExactly(fallbackStatus)
|
||||
|
|
@ -184,7 +184,7 @@ internal class DefaultSingleYieldBalanceProducerTest {
|
|||
|
||||
val multiFlow = flowOf(setOf(balance))
|
||||
|
||||
val multiParams = MultiYieldBalanceProducer.Params(userWalletId = params.userWalletId)
|
||||
val multiParams = MultiStakingBalanceProducer.Params(userWalletId = params.userWalletId)
|
||||
every { multiNetworkStatusSupplier(multiParams) } returns multiFlow
|
||||
|
||||
val producerFlow = producer.produce()
|
||||
|
|
@ -5,7 +5,7 @@ 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.staking.StakingBalance
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.test.core.getEmittedValues
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
|
|
@ -15,12 +15,12 @@ import org.junit.Test
|
|||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class YieldsBalancesStoreGetMethodTest {
|
||||
internal class StakingBalancesStoreGetMethodTest {
|
||||
|
||||
private val runtimeStore = RuntimeSharedStore<WalletIdWithBalances>()
|
||||
private val runtimeStore = RuntimeSharedStore<WalletIdWithStakingBalances>()
|
||||
private val persistenceStore = MockStateDataStore<WalletIdWithWrappers>(default = emptyMap())
|
||||
|
||||
private val store = DefaultYieldsBalancesStore(
|
||||
private val store = DefaultStakingBalancesStore(
|
||||
runtimeStore = runtimeStore,
|
||||
persistenceStore = persistenceStore,
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
|
|
@ -32,7 +32,7 @@ internal class YieldsBalancesStoreGetMethodTest {
|
|||
|
||||
val values = getEmittedValues(flow = actual)
|
||||
|
||||
val expected = listOf(emptySet<YieldBalance>())
|
||||
val expected = listOf(emptySet<StakingBalance>())
|
||||
Truth.assertThat(values).isEqualTo(expected)
|
||||
}
|
||||
|
||||
|
|
@ -44,7 +44,7 @@ internal class YieldsBalancesStoreGetMethodTest {
|
|||
|
||||
val values = getEmittedValues(flow = actual)
|
||||
|
||||
val expected = listOf(emptySet<YieldBalance>())
|
||||
val expected = listOf(emptySet<StakingBalance>())
|
||||
Truth.assertThat(values).isEqualTo(expected)
|
||||
}
|
||||
|
||||
|
|
@ -59,7 +59,7 @@ internal class YieldsBalancesStoreGetMethodTest {
|
|||
val values = getEmittedValues(flow = actual)
|
||||
|
||||
Truth.assertThat(values.size).isEqualTo(1)
|
||||
Truth.assertThat(values).isEqualTo(listOf(emptySet<YieldBalance>()))
|
||||
Truth.assertThat(values).isEqualTo(listOf(emptySet<StakingBalance>()))
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -6,7 +6,7 @@ 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.staking.StakingBalance
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import io.mockk.every
|
||||
|
|
@ -18,16 +18,16 @@ import org.junit.Test
|
|||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class YieldsBalancesStoreInitializationTest {
|
||||
internal class StakingBalancesStoreInitializationTest {
|
||||
|
||||
@Test
|
||||
fun `test initialization if cache store is empty`() = runTest {
|
||||
val runtimeStore = RuntimeSharedStore<WalletIdWithBalances>()
|
||||
val runtimeStore = RuntimeSharedStore<WalletIdWithStakingBalances>()
|
||||
val persistenceStore: DataStore<WalletIdWithWrappers> = mockk()
|
||||
|
||||
every { persistenceStore.data } returns emptyFlow()
|
||||
|
||||
DefaultYieldsBalancesStore(
|
||||
DefaultStakingBalancesStore(
|
||||
runtimeStore = runtimeStore,
|
||||
persistenceStore = persistenceStore,
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
|
|
@ -38,21 +38,21 @@ internal class YieldsBalancesStoreInitializationTest {
|
|||
|
||||
@Test
|
||||
fun `test initialization if cache store contains empty map`() = runTest {
|
||||
val runtimeStore = RuntimeSharedStore<WalletIdWithBalances>()
|
||||
val runtimeStore = RuntimeSharedStore<WalletIdWithStakingBalances>()
|
||||
val persistenceStore = MockStateDataStore<WalletIdWithWrappers>(default = emptyMap())
|
||||
|
||||
DefaultYieldsBalancesStore(
|
||||
DefaultStakingBalancesStore(
|
||||
runtimeStore = runtimeStore,
|
||||
persistenceStore = persistenceStore,
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
)
|
||||
|
||||
Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(emptyMap<String, Set<YieldBalance>>())
|
||||
Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(emptyMap<String, Set<StakingBalance>>())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test initialization if cache store is not empty`() = runTest {
|
||||
val runtimeStore = RuntimeSharedStore<WalletIdWithBalances>()
|
||||
val runtimeStore = RuntimeSharedStore<WalletIdWithStakingBalances>()
|
||||
val persistenceStore = MockStateDataStore<WalletIdWithWrappers>(default = emptyMap())
|
||||
|
||||
val wrapper = MockYieldBalanceWrapperDTOFactory.createWithBalance()
|
||||
|
|
@ -63,7 +63,7 @@ internal class YieldsBalancesStoreInitializationTest {
|
|||
}
|
||||
}
|
||||
|
||||
DefaultYieldsBalancesStore(
|
||||
DefaultStakingBalancesStore(
|
||||
runtimeStore = runtimeStore,
|
||||
persistenceStore = persistenceStore,
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
|
|
@ -7,8 +7,8 @@ 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.StakingBalance
|
||||
import com.tangem.domain.models.staking.StakingID
|
||||
import com.tangem.domain.models.staking.YieldBalance
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
|
|
@ -18,12 +18,12 @@ import org.junit.Test
|
|||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class YieldsBalancesStoreUpdateMethodsTest {
|
||||
internal class StakingBalancesStoreUpdateMethodsTest {
|
||||
|
||||
private val runtimeStore = RuntimeSharedStore<WalletIdWithBalances>()
|
||||
private val runtimeStore = RuntimeSharedStore<WalletIdWithStakingBalances>()
|
||||
private val persistenceStore = MockStateDataStore<WalletIdWithWrappers>(default = emptyMap())
|
||||
|
||||
private val store = DefaultYieldsBalancesStore(
|
||||
private val store = DefaultStakingBalancesStore(
|
||||
runtimeStore = runtimeStore,
|
||||
persistenceStore = persistenceStore,
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
|
|
@ -33,7 +33,7 @@ internal class YieldsBalancesStoreUpdateMethodsTest {
|
|||
fun `refresh the single id if runtime store is empty`() = runTest {
|
||||
store.refresh(userWalletId = userWalletId, stakingId = stakingId)
|
||||
|
||||
val runtimeExpected = mapOf(userWalletId to emptySet<YieldBalance>())
|
||||
val runtimeExpected = mapOf(userWalletId to emptySet<StakingBalance>())
|
||||
|
||||
Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected)
|
||||
Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap<String, Set<YieldBalanceWrapperDTO>>())
|
||||
|
|
@ -63,7 +63,7 @@ internal class YieldsBalancesStoreUpdateMethodsTest {
|
|||
fun `refresh the multi ids if runtime store is empty`() = runTest {
|
||||
store.refresh(userWalletId = userWalletId, stakingIds = stakingIds)
|
||||
|
||||
val runtimeExpected = mapOf(userWalletId to emptySet<YieldBalance>())
|
||||
val runtimeExpected = mapOf(userWalletId to emptySet<StakingBalance>())
|
||||
|
||||
Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected)
|
||||
Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap<String, Set<YieldBalanceWrapperDTO>>())
|
||||
|
|
@ -129,7 +129,7 @@ internal class YieldsBalancesStoreUpdateMethodsTest {
|
|||
store.storeError(userWalletId = userWalletId, stakingIds = setOf(stakingId))
|
||||
|
||||
val runtimeExpected = mapOf(
|
||||
userWalletId to setOf(YieldBalance.Error(stakingId)),
|
||||
userWalletId to setOf(StakingBalance.Error(stakingId)),
|
||||
)
|
||||
|
||||
Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected)
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
package com.tangem.data.staking.utils
|
||||
|
||||
import com.tangem.data.staking.store.YieldsBalancesStore
|
||||
import com.tangem.data.staking.store.StakingBalancesStore
|
||||
import com.tangem.domain.models.staking.StakingID
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.staking.model.StakingIntegrationID
|
||||
|
|
@ -16,9 +16,9 @@ import org.junit.jupiter.api.TestInstance
|
|||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
class DefaultStakingCleanerTest {
|
||||
|
||||
private val yieldsBalancesStore = mockk<YieldsBalancesStore>(relaxed = true)
|
||||
private val stakingBalancesStore = mockk<StakingBalancesStore>(relaxed = true)
|
||||
private val cleaner = DefaultStakingCleaner(
|
||||
yieldsBalancesStore = yieldsBalancesStore,
|
||||
stakingBalancesStore = stakingBalancesStore,
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
)
|
||||
private val userWalletId = UserWalletId("011")
|
||||
|
|
@ -28,7 +28,7 @@ class DefaultStakingCleanerTest {
|
|||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
clearMocks(yieldsBalancesStore)
|
||||
clearMocks(stakingBalancesStore)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -38,7 +38,7 @@ class DefaultStakingCleanerTest {
|
|||
|
||||
// Assert
|
||||
coVerifyOrder {
|
||||
yieldsBalancesStore.clear(userWalletId = userWalletId, stakingIds = stakingIds)
|
||||
stakingBalancesStore.clear(userWalletId = userWalletId, stakingIds = stakingIds)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -49,7 +49,7 @@ class DefaultStakingCleanerTest {
|
|||
|
||||
// Assert
|
||||
coVerifyOrder(inverse = true) {
|
||||
yieldsBalancesStore.clear(userWalletId = any(), stakingIds = any())
|
||||
stakingBalancesStore.clear(userWalletId = any(), stakingIds = any())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
<?xml version="1.0" ?>
|
||||
<SmellBaseline>
|
||||
<ManuallySuppressedIssues/>
|
||||
<CurrentIssues>
|
||||
<ID>MaxChainedCallsOnSameLine:DefaultSwapRepositoryV2.kt$DefaultSwapRepositoryV2$fromCryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value.orEmpty()</ID>
|
||||
<ID>MultilineLambdaItParameter:DefaultSwapRepositoryV2.kt$DefaultSwapRepositoryV2${ Timber.w(it, "Unable to get pairs") throw it }</ID>
|
||||
<ID>MultilineLambdaItParameter:DefaultSwapRepositoryV2.kt$DefaultSwapRepositoryV2${ it.currency.getContractAddress() == pair.from.contractAddress && it.currency.network.backendId == pair.from.network }</ID>
|
||||
<ID>MultilineLambdaItParameter:DefaultSwapRepositoryV2.kt$DefaultSwapRepositoryV2${ it.currency.getContractAddress() == pair.to.contractAddress && it.currency.network.backendId == pair.to.network }</ID>
|
||||
<ID>MultilineLambdaItParameter:DefaultSwapRepositoryV2.kt$DefaultSwapRepositoryV2${ it.getContractAddress() == pair.from.contractAddress && it.network.backendId == pair.from.network }</ID>
|
||||
<ID>MultilineLambdaItParameter:DefaultSwapRepositoryV2.kt$DefaultSwapRepositoryV2${ it.getContractAddress() == pair.to.contractAddress && it.network.backendId == pair.to.network }</ID>
|
||||
<ID>MultilineLambdaItParameter:DefaultSwapTransactionRepository.kt$DefaultSwapTransactionRepository${ it.checkId( checkUserWalletId = userWalletId, fromCurrencyId = fromCryptoCurrency.id, toCurrencyId = toCryptoCurrency.id, ) }</ID>
|
||||
<ID>MultilineLambdaItParameter:DefaultSwapTransactionRepository.kt$DefaultSwapTransactionRepository${ it.userWalletId == userWallet.walletId.stringValue && ( it.toCryptoCurrencyId == cryptoCurrencyId.value || it.fromCryptoCurrencyId == cryptoCurrencyId.value ) }</ID>
|
||||
<ID>MultilineLambdaItParameter:DefaultSwapTransactionRepository.kt$DefaultSwapTransactionRepository${ listConverter.convertBack( value = it, multiAccountList = multiAccountList, userWallet = userWallet, txStatuses = txStatuses, ) }</ID>
|
||||
<ID>MultilineLambdaItParameter:DefaultSwapTransactionRepository.kt$DefaultSwapTransactionRepository${ storeTransactionState( txId = transaction.txId, status = it, accountWithCurrency = fromAccount?.accountId to fromCryptoCurrency, ) }</ID>
|
||||
<ID>MultilineLambdaItParameter:SwapDataConverter.kt$SwapDataConverter${ if (it == "0") { BigDecimal.ZERO } else { requireNotNull(it.toBigDecimalOrNull()) { "wrong amount format, use only digits" } } }</ID>
|
||||
<ID>NoNameShadowing:DefaultSwapRepositoryV2.kt$DefaultSwapRepositoryV2$mappedProviders</ID>
|
||||
<ID>NoNameShadowing:DefaultSwapTransactionRepository.kt$DefaultSwapTransactionRepository${ it.txId == txId }</ID>
|
||||
</CurrentIssues>
|
||||
</SmellBaseline>
|
||||
|
|
@ -79,23 +79,23 @@ internal class DefaultSwapRepositoryV2 @Inject constructor(
|
|||
userWallet = userWallet,
|
||||
filterProviderTypes = filterProviderTypes,
|
||||
)
|
||||
val mappedProviders = providers.associateBy(ExpressProvider::providerId)
|
||||
val expressProviders = providers.associateBy(ExpressProvider::providerId)
|
||||
|
||||
allPairs.map { pair ->
|
||||
async {
|
||||
val statusFrom = cryptoCurrencyStatusList
|
||||
.firstOrNull {
|
||||
it.currency.getContractAddress() == pair.from.contractAddress &&
|
||||
it.currency.network.backendId == pair.from.network
|
||||
.firstOrNull { currencyStatus ->
|
||||
currencyStatus.currency.getContractAddress() == pair.from.contractAddress &&
|
||||
currencyStatus.currency.network.backendId == pair.from.network
|
||||
}
|
||||
val statusTo = cryptoCurrencyStatusList
|
||||
.firstOrNull {
|
||||
it.currency.getContractAddress() == pair.to.contractAddress &&
|
||||
it.currency.network.backendId == pair.to.network
|
||||
.firstOrNull { currencyStatus ->
|
||||
currencyStatus.currency.getContractAddress() == pair.to.contractAddress &&
|
||||
currencyStatus.currency.network.backendId == pair.to.network
|
||||
}
|
||||
|
||||
val mappedProviders = pair.providers.mapNotNull {
|
||||
mappedProviders[it.providerId]
|
||||
expressProviders[it.providerId]
|
||||
}.filterYieldSupplyProvider(statusFrom)
|
||||
|
||||
if (statusFrom != null && statusTo != null && mappedProviders.isNotEmpty()) {
|
||||
|
|
@ -135,16 +135,16 @@ internal class DefaultSwapRepositoryV2 @Inject constructor(
|
|||
async {
|
||||
val statusFromDeferred = async {
|
||||
cryptoCurrencyList
|
||||
.firstOrNull {
|
||||
it.getContractAddress() == pair.from.contractAddress &&
|
||||
it.network.backendId == pair.from.network
|
||||
.firstOrNull { currency ->
|
||||
currency.getContractAddress() == pair.from.contractAddress &&
|
||||
currency.network.backendId == pair.from.network
|
||||
}
|
||||
}
|
||||
val statusToDeferred = async {
|
||||
cryptoCurrencyList
|
||||
.firstOrNull {
|
||||
it.getContractAddress() == pair.to.contractAddress &&
|
||||
it.network.backendId == pair.to.network
|
||||
.firstOrNull { currency ->
|
||||
currency.getContractAddress() == pair.to.contractAddress &&
|
||||
currency.network.backendId == pair.to.network
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -213,27 +213,27 @@ internal class DefaultSwapRepositoryV2 @Inject constructor(
|
|||
expressOperationType: ExpressOperationType,
|
||||
): SwapDataModel = withContext(coroutineDispatcher.io) {
|
||||
val requestId = UUID.randomUUID().toString()
|
||||
val fromCryptoCurrency = fromCryptoCurrencyStatus.currency
|
||||
val (fromCurrency, fromStatus) = fromCryptoCurrencyStatus
|
||||
|
||||
val refundData = when (expressProvider.type) {
|
||||
ExpressProviderType.CEX,
|
||||
ExpressProviderType.DEX_BRIDGE,
|
||||
ExpressProviderType.DEX,
|
||||
-> SwapRefundData(
|
||||
refundAddress = fromCryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value,
|
||||
refundAddress = fromStatus.networkAddress?.defaultAddress?.value,
|
||||
refundExtraId = null, // currently always null
|
||||
)
|
||||
else -> null
|
||||
}
|
||||
|
||||
val response = tangemExpressApi.getExchangeData(
|
||||
fromContractAddress = fromCryptoCurrency.getContractAddress(),
|
||||
fromContractAddress = fromCurrency.getContractAddress(),
|
||||
toContractAddress = toCryptoCurrency.getContractAddress(),
|
||||
fromNetwork = fromCryptoCurrency.network.backendId,
|
||||
fromNetwork = fromCurrency.network.backendId,
|
||||
toNetwork = toCryptoCurrency.network.backendId,
|
||||
fromAddress = fromCryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value.orEmpty(),
|
||||
fromAddress = fromStatus.networkAddress?.defaultAddress?.value.orEmpty(),
|
||||
toAddress = toAddress,
|
||||
fromDecimals = fromCryptoCurrency.decimals,
|
||||
fromDecimals = fromCurrency.decimals,
|
||||
toDecimals = toCryptoCurrency.decimals,
|
||||
fromAmount = fromAmount,
|
||||
providerId = expressProvider.providerId,
|
||||
|
|
@ -277,6 +277,7 @@ internal class DefaultSwapRepositoryV2 @Inject constructor(
|
|||
txHash: String,
|
||||
txExtraId: String?,
|
||||
) {
|
||||
val (currency, status) = fromCryptoCurrencyStatus
|
||||
withContext(coroutineDispatcher.io) {
|
||||
tangemExpressApi.exchangeSent(
|
||||
userWalletId = userWallet.walletId.stringValue,
|
||||
|
|
@ -286,8 +287,8 @@ internal class DefaultSwapRepositoryV2 @Inject constructor(
|
|||
),
|
||||
body = ExchangeSentRequestBody(
|
||||
txId = txId,
|
||||
fromNetwork = fromCryptoCurrencyStatus.currency.network.backendId,
|
||||
fromAddress = fromCryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value.orEmpty(),
|
||||
fromNetwork = currency.network.backendId,
|
||||
fromAddress = status.networkAddress?.defaultAddress?.value.orEmpty(),
|
||||
payinAddress = payInAddress,
|
||||
payinExtraId = txExtraId,
|
||||
txHash = txHash,
|
||||
|
|
@ -364,9 +365,9 @@ internal class DefaultSwapRepositoryV2 @Inject constructor(
|
|||
),
|
||||
).getOrThrow()
|
||||
},
|
||||
onError = {
|
||||
Timber.w(it, "Unable to get pairs")
|
||||
throw it
|
||||
onError = { error ->
|
||||
Timber.w(error, "Unable to get pairs")
|
||||
throw error
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -403,7 +404,7 @@ internal class DefaultSwapRepositoryV2 @Inject constructor(
|
|||
value = NetworkStatus.MissedDerivation, // Caution!!! Do not change this status
|
||||
).some(),
|
||||
maybeQuoteStatus = quoteStatus.toOption(),
|
||||
maybeYieldBalance = none(),
|
||||
maybeStakingBalance = none(),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -64,10 +64,10 @@ internal class DefaultSwapTransactionRepository(
|
|||
toAccount: Account.CryptoPortfolio?,
|
||||
transaction: SwapTransactionModel,
|
||||
) {
|
||||
transaction.status?.let {
|
||||
transaction.status?.let { swapTxList ->
|
||||
storeTransactionState(
|
||||
txId = transaction.txId,
|
||||
status = it,
|
||||
status = swapTxList,
|
||||
accountWithCurrency = fromAccount?.accountId to fromCryptoCurrency,
|
||||
)
|
||||
}
|
||||
|
|
@ -76,8 +76,8 @@ internal class DefaultSwapTransactionRepository(
|
|||
key = PreferencesKeys.SWAP_TRANSACTIONS_KEY,
|
||||
)
|
||||
val tokenTransactions = savedTransactions
|
||||
?.firstOrNull {
|
||||
it.checkId(
|
||||
?.firstOrNull { swapTxList ->
|
||||
swapTxList.checkId(
|
||||
checkUserWalletId = userWalletId,
|
||||
fromCurrencyId = fromCryptoCurrency.id,
|
||||
toCurrencyId = toCryptoCurrency.id,
|
||||
|
|
@ -129,17 +129,17 @@ internal class DefaultSwapTransactionRepository(
|
|||
},
|
||||
) { savedTransactions, txStatuses, multiAccountList ->
|
||||
val currencyTxs = savedTransactions
|
||||
?.filter {
|
||||
it.userWalletId == userWallet.walletId.stringValue &&
|
||||
?.filter { swapTxList ->
|
||||
swapTxList.userWalletId == userWallet.walletId.stringValue &&
|
||||
(
|
||||
it.toCryptoCurrencyId == cryptoCurrencyId.value ||
|
||||
it.fromCryptoCurrencyId == cryptoCurrencyId.value
|
||||
swapTxList.toCryptoCurrencyId == cryptoCurrencyId.value ||
|
||||
swapTxList.fromCryptoCurrencyId == cryptoCurrencyId.value
|
||||
)
|
||||
}
|
||||
|
||||
currencyTxs?.mapNotNull {
|
||||
currencyTxs?.mapNotNull { swapTxList ->
|
||||
listConverter.convertBack(
|
||||
value = it,
|
||||
value = swapTxList,
|
||||
multiAccountList = multiAccountList,
|
||||
userWallet = userWallet,
|
||||
txStatuses = txStatuses,
|
||||
|
|
@ -155,8 +155,8 @@ internal class DefaultSwapTransactionRepository(
|
|||
)
|
||||
val tokenTransactions = savedList
|
||||
?.asSequence()
|
||||
?.map {
|
||||
it.copy(transactions = it.transactions.filterNot { it.txId == txId })
|
||||
?.map { swapTxList ->
|
||||
swapTxList.copy(transactions = swapTxList.transactions.filterNot { swapTx -> swapTx.txId == txId })
|
||||
}?.filterNot { it.transactions.isEmpty() }
|
||||
?.toList()
|
||||
|
||||
|
|
@ -257,8 +257,8 @@ internal class DefaultSwapTransactionRepository(
|
|||
toAccount = toAccount,
|
||||
tokenTransactions = transactions,
|
||||
),
|
||||
predicate = {
|
||||
it.checkId(
|
||||
predicate = { swapTxList ->
|
||||
swapTxList.checkId(
|
||||
checkUserWalletId = userWalletId,
|
||||
fromCurrencyId = fromCryptoCurrency.id,
|
||||
toCurrencyId = toCryptoCurrency.id,
|
||||
|
|
|
|||
|
|
@ -31,11 +31,11 @@ internal class SwapDataConverter : Converter<ExchangeDataResponseWithTxDetails,
|
|||
)
|
||||
|
||||
return if (transactionDto.txType == TxType.SWAP) {
|
||||
val otherNativeFeeWei = transactionDto.otherNativeFee?.let {
|
||||
if (it == "0") {
|
||||
val otherNativeFeeWei = transactionDto.otherNativeFee?.let { otherFee ->
|
||||
if (otherFee == "0") {
|
||||
BigDecimal.ZERO
|
||||
} else {
|
||||
requireNotNull(it.toBigDecimalOrNull()) { "wrong amount format, use only digits" }
|
||||
requireNotNull(otherFee.toBigDecimalOrNull()) { "wrong amount format, use only digits" }
|
||||
}
|
||||
}
|
||||
SwapDataTransactionModel.DEX(
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ 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.staking.StakingBalance
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.staking.utils.getTotalStakingBalance
|
||||
import com.tangem.domain.tokens.model.CurrencyAmount
|
||||
|
|
@ -136,7 +136,7 @@ internal class DefaultCurrencyChecksRepository(
|
|||
|
||||
val rentData = walletManagersFacade.getRentInfo(userWalletId, currencyStatus.currency.network) ?: return null
|
||||
val balanceValue = currencyStatus.value as? CryptoCurrencyStatus.Loaded ?: return null
|
||||
val stakingBalance = balanceValue.yieldBalance as? YieldBalance.Data
|
||||
val stakingBalance = balanceValue.stakingBalance as? StakingBalance.Data
|
||||
val stakingTotalBalance = stakingBalance?.getTotalStakingBalance(
|
||||
blockchainId = currencyStatus.currency.network.rawId,
|
||||
).orZero()
|
||||
|
|
|
|||
|
|
@ -1,11 +0,0 @@
|
|||
<?xml version="1.0" ?>
|
||||
<SmellBaseline>
|
||||
<ManuallySuppressedIssues/>
|
||||
<CurrentIssues>
|
||||
<ID>CastNullableToNonNullableType:DefaultTransactionRepository.kt$DefaultTransactionRepository$as</ID>
|
||||
<ID>NoNameShadowing:DefaultTransactionRepository.kt$DefaultTransactionRepository$amount</ID>
|
||||
<ID>NoNameShadowing:DefaultTransactionRepository.kt$DefaultTransactionRepository$destination</ID>
|
||||
<ID>NullableBooleanCheck:DefaultWalletAddressServiceRepository.kt$DefaultWalletAddressServiceRepository$(walletManager as? NearWalletManager)?.validateAddress(address) ?: false</ID>
|
||||
<ID>NullableToStringCall:DefaultTransactionRepository.kt$DefaultTransactionRepository$${walletManager?.wallet?.blockchain}</ID>
|
||||
</CurrentIssues>
|
||||
</SmellBaseline>
|
||||
|
|
@ -38,7 +38,7 @@ import timber.log.Timber
|
|||
import java.math.BigDecimal
|
||||
import java.math.BigInteger
|
||||
|
||||
@Suppress("LargeClass")
|
||||
@Suppress("LargeClass", "NullableToStringCall")
|
||||
internal class DefaultTransactionRepository(
|
||||
private val tangemTechApi: TangemTechApi,
|
||||
private val walletManagersFacade: WalletManagersFacade,
|
||||
|
|
@ -64,12 +64,12 @@ internal class DefaultTransactionRepository(
|
|||
|
||||
val extras = txExtras ?: getMemoExtras(networkId = network.rawId, memo)
|
||||
|
||||
val destination = if (amount.type is AmountType.TokenYieldSupply) {
|
||||
val patchedDestination = if (amount.type is AmountType.TokenYieldSupply) {
|
||||
walletManager.getYieldModuleAddress()
|
||||
} else {
|
||||
destination
|
||||
}
|
||||
val amount = if (amount.type is AmountType.TokenYieldSupply) {
|
||||
val patchedAmount = if (amount.type is AmountType.TokenYieldSupply) {
|
||||
amount.copy(value = BigDecimal.ZERO)
|
||||
} else {
|
||||
amount
|
||||
|
|
@ -77,17 +77,17 @@ internal class DefaultTransactionRepository(
|
|||
|
||||
return@withContext if (fee != null) {
|
||||
walletManager.createTransaction(
|
||||
amount = amount,
|
||||
amount = patchedAmount,
|
||||
fee = fee,
|
||||
destination = destination,
|
||||
destination = patchedDestination,
|
||||
).copy(
|
||||
extras = extras,
|
||||
)
|
||||
} else {
|
||||
TransactionData.Uncompiled(
|
||||
amount = amount,
|
||||
amount = patchedAmount,
|
||||
sourceAddress = walletManager.wallet.address,
|
||||
destinationAddress = destination,
|
||||
destinationAddress = patchedDestination,
|
||||
extras = extras,
|
||||
fee = null,
|
||||
)
|
||||
|
|
@ -288,7 +288,7 @@ internal class DefaultTransactionRepository(
|
|||
blockchain = blockchain,
|
||||
derivationPath = network.derivationPath.value,
|
||||
)
|
||||
(walletManager as TransactionSender).send(txData, signer)
|
||||
(requireNotNull(walletManager) as TransactionSender).send(txData, signer)
|
||||
}
|
||||
|
||||
override suspend fun sendMultipleTransactions(
|
||||
|
|
@ -304,7 +304,7 @@ internal class DefaultTransactionRepository(
|
|||
blockchain = blockchain,
|
||||
derivationPath = network.derivationPath.value,
|
||||
)
|
||||
(walletManager as TransactionSender).sendMultiple(txsData, signer, sendMode)
|
||||
(requireNotNull(walletManager) as TransactionSender).sendMultiple(txsData, signer, sendMode)
|
||||
}
|
||||
|
||||
override fun createTransactionDataExtras(
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ class DefaultWalletAddressServiceRepository(
|
|||
blockchain = blockchain,
|
||||
derivationPath = network.derivationPath.value,
|
||||
) ?: return@withContext false
|
||||
(walletManager as? NearWalletManager)?.validateAddress(address) ?: false
|
||||
(walletManager as? NearWalletManager)?.validateAddress(address) == true
|
||||
} else {
|
||||
blockchain.validateAddress(address)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,10 +14,8 @@
|
|||
<ID>NullCheckOnMutableProperty:VisaLibLoader.kt$VisaLibLoader$if (config != null) return@withLock requireNotNull(config)</ID>
|
||||
<ID>NullCheckOnMutableProperty:VisaLibLoader.kt$VisaLibLoader$if (provider != null) return@withLock requireNotNull(provider)</ID>
|
||||
<ID>NullableToStringCall:DefaultOnboardingRepository.kt$DefaultOnboardingRepository$${error.message}</ID>
|
||||
<ID>NullableToStringCall:TangemPayRequestPerformer.kt$TangemPayRequestPerformer$${error.message}</ID>
|
||||
<ID>RedundantSuspendModifier:DefaultVisaRepository.kt$DefaultVisaRepository$suspend</ID>
|
||||
<ID>SuspendFunSwallowedCancellation:DefaultVisaRepository.kt$DefaultVisaRepository$runCatching</ID>
|
||||
<ID>SuspendFunSwallowedCancellation:TangemPayRequestPerformer.kt$TangemPayRequestPerformer$runCatching</ID>
|
||||
<ID>SuspendFunSwallowedCancellation:VisaApiRequestMaker.kt$VisaApiRequestMaker$runCatching</ID>
|
||||
<ID>UnreachableCode:VisaApiRequestMaker.kt$VisaApiRequestMaker$if (status is VisaCardActivationStatus.RefreshTokenExpired) { throw RefreshTokenExpiredException() }</ID>
|
||||
<ID>UnreachableCode:VisaApiRequestMaker.kt$VisaApiRequestMaker$return (status as? VisaCardActivationStatus.Activated)?.visaAuthTokens ?: error("Visa card is not activated")</ID>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,94 @@
|
|||
package com.tangem.data.pay
|
||||
|
||||
import com.tangem.common.card.FirmwareVersion
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.isLocked
|
||||
import com.tangem.domain.models.wallet.isMultiCurrency
|
||||
import com.tangem.domain.pay.TangemPayEligibilityManager
|
||||
import com.tangem.domain.pay.repository.OnboardingRepository
|
||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
||||
import com.tangem.features.hotwallet.HotWalletFeatureToggles
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class DefaultTangemPayEligibilityManager @Inject constructor(
|
||||
private val userWalletsListManager: UserWalletsListManager,
|
||||
private val userWalletsListRepository: UserWalletsListRepository,
|
||||
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
|
||||
private val onboardingRepository: OnboardingRepository,
|
||||
) : TangemPayEligibilityManager {
|
||||
|
||||
private var cachedEligibleWallets: List<UserWallet>? = null
|
||||
private var eligibleWalletsDeferred: Deferred<List<UserWallet>>? = null
|
||||
private val loadMutex = Mutex()
|
||||
|
||||
override suspend fun getEligibleWallets(): List<UserWallet> {
|
||||
cachedEligibleWallets?.let { return it }
|
||||
|
||||
return loadMutex.withLock {
|
||||
cachedEligibleWallets?.let { return it }
|
||||
eligibleWalletsDeferred?.let { return it.await() }
|
||||
|
||||
coroutineScope {
|
||||
val deferred = async {
|
||||
getPossibleWalletsForTangemPay()
|
||||
.excludePaeraCustomers()
|
||||
.also { cachedEligibleWallets = it }
|
||||
}
|
||||
eligibleWalletsDeferred = deferred
|
||||
try {
|
||||
deferred.await()
|
||||
} finally {
|
||||
eligibleWalletsDeferred = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getPossibleWalletsForTangemPay(): List<UserWallet> {
|
||||
if (!onboardingRepository.checkCustomerEligibility()) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
val wallets = if (hotWalletFeatureToggles.isHotWalletEnabled) {
|
||||
userWalletsListRepository.userWallets.value
|
||||
} else {
|
||||
userWalletsListManager.userWalletsSync
|
||||
} ?: return emptyList()
|
||||
|
||||
return wallets.filter { wallet ->
|
||||
wallet.isMultiCurrency && !wallet.isLocked && wallet.isCompatible()
|
||||
}
|
||||
}
|
||||
|
||||
private fun UserWallet.isCompatible(): Boolean = when (this) {
|
||||
is UserWallet.Cold ->
|
||||
scanResponse.card.firmwareVersion >= FirmwareVersion.HDWalletAvailable
|
||||
is UserWallet.Hot -> true
|
||||
}
|
||||
|
||||
private suspend fun List<UserWallet>.excludePaeraCustomers(): List<UserWallet> {
|
||||
if (isEmpty()) return this
|
||||
|
||||
return coroutineScope {
|
||||
map { wallet ->
|
||||
async {
|
||||
val isCustomer = onboardingRepository
|
||||
.checkCustomerWallet(wallet.walletId)
|
||||
.getOrNull() == true
|
||||
wallet to isCustomer
|
||||
}
|
||||
}
|
||||
.awaitAll()
|
||||
.mapNotNull { (wallet, isCustomer) ->
|
||||
wallet.takeUnless { isCustomer }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,12 @@
|
|||
package com.tangem.data.pay.di
|
||||
|
||||
import com.tangem.data.pay.DefaultTangemPayCryptoCurrencyFactory
|
||||
import com.tangem.data.pay.DefaultTangemPayEligibilityManager
|
||||
import com.tangem.data.pay.repository.*
|
||||
import com.tangem.data.pay.usecase.DefaultGetTangemPayCurrencyStatusUseCase
|
||||
import com.tangem.data.pay.usecase.DefaultTangemPayWithdrawUseCase
|
||||
import com.tangem.domain.pay.TangemPayCryptoCurrencyFactory
|
||||
import com.tangem.domain.pay.TangemPayEligibilityManager
|
||||
import com.tangem.domain.pay.repository.*
|
||||
import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase
|
||||
import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase
|
||||
|
|
@ -62,6 +64,10 @@ internal interface TangemPayDataModule {
|
|||
@Singleton
|
||||
fun bindTangemPayWithdrawUseCase(impl: DefaultTangemPayWithdrawUseCase): TangemPayWithdrawUseCase
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindTangemPayEligibilityManager(impl: DefaultTangemPayEligibilityManager): TangemPayEligibilityManager
|
||||
|
||||
companion object {
|
||||
@Provides
|
||||
@Singleton
|
||||
|
|
@ -69,11 +75,13 @@ internal interface TangemPayDataModule {
|
|||
repository: OnboardingRepository,
|
||||
customerOrderRepository: CustomerOrderRepository,
|
||||
tangemPayOnboardingRepository: OnboardingRepository,
|
||||
eligibilityManager: TangemPayEligibilityManager,
|
||||
): TangemPayMainScreenCustomerInfoUseCase {
|
||||
return TangemPayMainScreenCustomerInfoUseCase(
|
||||
repository = repository,
|
||||
customerOrderRepository = customerOrderRepository,
|
||||
tangemPayOnboardingRepository = tangemPayOnboardingRepository,
|
||||
eligibilityManager = eligibilityManager,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -84,7 +84,6 @@ internal class DefaultOnboardingRepository @Inject constructor(
|
|||
}
|
||||
|
||||
override suspend fun getCustomerInfo(userWalletId: UserWalletId): Either<VisaApiError, CustomerInfo> {
|
||||
// TODO implement selector
|
||||
return requestHelper.performRequest(userWalletId) { authHeader -> tangemPayApi.getCustomerMe(authHeader) }
|
||||
.map { response -> getCustomerInfo(userWalletId = userWalletId, response = response.result) }
|
||||
}
|
||||
|
|
@ -182,7 +181,7 @@ internal class DefaultOnboardingRepository @Inject constructor(
|
|||
customerWalletId = userWalletId.stringValue,
|
||||
)
|
||||
}.map { response ->
|
||||
val id = response.id
|
||||
val id = response.result?.id
|
||||
val isPaeraCustomer = !id.isNullOrEmpty()
|
||||
tangemPayStorage.storeCheckCustomerWalletResult(userWalletId = userWalletId, isPaeraCustomer)
|
||||
isPaeraCustomer
|
||||
|
|
@ -193,4 +192,19 @@ internal class DefaultOnboardingRepository @Inject constructor(
|
|||
error
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun checkCustomerEligibility(): Boolean {
|
||||
val response = requestHelper.performWithoutToken {
|
||||
tangemPayApi.checkCustomerEligibility()
|
||||
}.getOrNull()
|
||||
return response?.result?.isTangemPayAvailable == true
|
||||
}
|
||||
|
||||
override suspend fun getHideMainOnboardingBanner(userWalletId: UserWalletId): Boolean {
|
||||
return tangemPayStorage.getHideMainOnboardingBanner(userWalletId)
|
||||
}
|
||||
|
||||
override suspend fun setHideMainOnboardingBanner(userWalletId: UserWalletId) {
|
||||
tangemPayStorage.storeHideOnboardingBanner(userWalletId, hide = true)
|
||||
}
|
||||
}
|
||||
|
|
@ -279,6 +279,7 @@ internal class DefaultTangemPayCardDetailsRepository @Inject constructor(
|
|||
ApiEnvironment.DEV_2,
|
||||
ApiEnvironment.DEV_3,
|
||||
ApiEnvironment.STAGE,
|
||||
ApiEnvironment.STAGE_2,
|
||||
ApiEnvironment.MOCK,
|
||||
-> visaLibLoader.getOrCreateConfig().rainRSAPublicKey.dev
|
||||
ApiEnvironment.PROD -> visaLibLoader.getOrCreateConfig().rainRSAPublicKey.prod
|
||||
|
|
|
|||
|
|
@ -89,6 +89,19 @@ internal class TangemPayRequestPerformer @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
suspend fun <T : Any> performWithoutToken(requestBlock: suspend () -> ApiResponse<T>): Either<VisaApiError, T> =
|
||||
withContext(dispatchers.io) {
|
||||
catch(
|
||||
block = {
|
||||
when (val apiResponse = requestBlock()) {
|
||||
is ApiResponse.Error -> errorConverter.convert(apiResponse.cause).left()
|
||||
is ApiResponse.Success<T> -> apiResponse.data.right()
|
||||
}
|
||||
},
|
||||
catch = { errorConverter.convert(it).left() },
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun <T : Any> performRequest(
|
||||
userWalletId: UserWalletId,
|
||||
requestBlock: suspend (header: String) -> ApiResponse<T>,
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ internal class DefaultGetTangemPayCurrencyStatusUseCase @Inject constructor(
|
|||
),
|
||||
sources = CryptoCurrencyStatus.Sources(),
|
||||
pendingTransactions = emptySet(),
|
||||
yieldBalance = null,
|
||||
stakingBalance = null,
|
||||
yieldSupplyStatus = null,
|
||||
hasCurrentNetworkTransactions = false,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ package com.tangem.data.pay.util
|
|||
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.datasource.api.common.response.ApiResponseError
|
||||
import com.tangem.datasource.api.pay.models.response.VisaErrorResponse
|
||||
import com.tangem.datasource.api.pay.models.response.TangemPayErrorResponse
|
||||
import com.tangem.datasource.di.NetworkMoshi
|
||||
import com.tangem.domain.visa.error.VisaApiError
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
|
@ -14,7 +14,7 @@ internal class TangemPayErrorConverter @Inject constructor(
|
|||
@NetworkMoshi moshi: Moshi,
|
||||
) : Converter<Throwable, VisaApiError> {
|
||||
|
||||
private val visaErrorAdapter by lazy { moshi.adapter(VisaErrorResponse::class.java) }
|
||||
private val tangemPayErrorAdapter by lazy { moshi.adapter(TangemPayErrorResponse::class.java) }
|
||||
|
||||
override fun convert(value: Throwable): VisaApiError {
|
||||
return if (value is ApiResponseError.HttpException) {
|
||||
|
|
@ -23,7 +23,7 @@ internal class TangemPayErrorConverter @Inject constructor(
|
|||
|
||||
val errorBody = value.errorBody ?: return VisaApiError.UnknownWithoutCode
|
||||
return runCatching {
|
||||
visaErrorAdapter.fromJson(errorBody)?.error?.code ?: value.code.numericCode
|
||||
tangemPayErrorAdapter.fromJson(errorBody)?.error?.code ?: value.code.numericCode
|
||||
}.map {
|
||||
VisaApiError.fromBackendError(it)
|
||||
}.getOrElse {
|
||||
|
|
|
|||
|
|
@ -1,39 +0,0 @@
|
|||
package com.tangem.data.pay.util
|
||||
|
||||
import com.tangem.common.card.FirmwareVersion
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
||||
import com.tangem.features.hotwallet.HotWalletFeatureToggles
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.first
|
||||
import javax.inject.Inject
|
||||
|
||||
// TODO remove after implement wallet selector in pay
|
||||
class TangemPayWalletsManager @Inject constructor(
|
||||
private val manager: UserWalletsListManager,
|
||||
private val repository: UserWalletsListRepository,
|
||||
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
|
||||
) {
|
||||
|
||||
@Deprecated("Don't use and put userWallet in features that need it")
|
||||
suspend fun getDefaultWalletForTangemPay(): UserWallet.Cold {
|
||||
val userWalletsFlow = if (useNewRepository()) repository.userWallets else manager.userWallets
|
||||
val userWallets = userWalletsFlow.filter { !it.isNullOrEmpty() }.first()
|
||||
return findColdWallet(userWallets)
|
||||
}
|
||||
|
||||
@Deprecated("Don't use and put userWallet in features that need it")
|
||||
fun getDefaultWalletForTangemPayBlocking(): UserWallet.Cold {
|
||||
val userWallets = if (useNewRepository()) repository.userWallets.value else manager.userWalletsSync
|
||||
return findColdWallet(userWallets)
|
||||
}
|
||||
|
||||
private fun useNewRepository(): Boolean = hotWalletFeatureToggles.isHotWalletEnabled
|
||||
|
||||
private fun findColdWallet(userWallets: List<UserWallet>?): UserWallet.Cold {
|
||||
return userWallets?.find {
|
||||
it is UserWallet.Cold && it.scanResponse.card.firmwareVersion >= FirmwareVersion.HDWalletAvailable
|
||||
} as? UserWallet.Cold ?: error("Cannot find cold user wallet")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
package com.tangem.data.visa
|
||||
|
||||
import arrow.core.Either
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.datasource.api.common.response.ApiResponseError
|
||||
import com.tangem.datasource.api.common.response.getOrThrow
|
||||
import com.tangem.datasource.api.pay.TangemPayAuthApi
|
||||
import com.tangem.datasource.api.pay.models.request.GenerateNonceByCustomerWalletRequest
|
||||
import com.tangem.datasource.api.pay.models.request.GetTokenByCustomerWalletRequest
|
||||
import com.tangem.datasource.api.pay.models.response.TangemPayErrorResponse
|
||||
import com.tangem.datasource.di.NetworkMoshi
|
||||
import com.tangem.domain.visa.datasource.TangemPayRemoteDataSource
|
||||
import com.tangem.domain.visa.error.VisaApiError
|
||||
import com.tangem.domain.visa.model.TangemPayAuthTokens
|
||||
import com.tangem.domain.visa.model.VisaAuthChallenge
|
||||
import com.tangem.domain.visa.model.VisaAuthSession
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class DefaultTangemPayRemoteDataSource @Inject constructor(
|
||||
@NetworkMoshi private val moshi: Moshi,
|
||||
private val tangemPayAuthApi: TangemPayAuthApi,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : TangemPayRemoteDataSource {
|
||||
|
||||
private val errorAdapter by lazy { moshi.adapter(TangemPayErrorResponse::class.java) }
|
||||
|
||||
override suspend fun getCustomerWalletAuthChallenge(
|
||||
customerWalletAddress: String,
|
||||
customerWalletId: String,
|
||||
): Either<VisaApiError, VisaAuthChallenge.Wallet> = withContext(dispatchers.io) {
|
||||
request {
|
||||
tangemPayAuthApi.generateNonceByCustomerWallet(
|
||||
request = GenerateNonceByCustomerWalletRequest(
|
||||
customerWalletAddress = customerWalletAddress,
|
||||
customerWalletId = customerWalletId,
|
||||
),
|
||||
).getOrThrow()
|
||||
}.map { response ->
|
||||
VisaAuthChallenge.Wallet(
|
||||
challenge = response.nonce,
|
||||
session = VisaAuthSession(response.sessionId),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getTokenWithCustomerWallet(
|
||||
sessionId: String,
|
||||
signature: String,
|
||||
nonce: String,
|
||||
): Either<VisaApiError, TangemPayAuthTokens> = withContext(dispatchers.io) {
|
||||
request {
|
||||
tangemPayAuthApi.getTokenByCustomerWallet(
|
||||
request = GetTokenByCustomerWalletRequest(
|
||||
authType = "customer_wallet",
|
||||
sessionId = sessionId,
|
||||
signature = signature,
|
||||
messageFormat = "Tangem Pay wants to sign in with your account. Nonce: $nonce",
|
||||
),
|
||||
).getOrThrow()
|
||||
}.map { response ->
|
||||
TangemPayAuthTokens(
|
||||
accessToken = response.accessToken,
|
||||
expiresAt = response.expiresAt,
|
||||
refreshToken = response.refreshToken,
|
||||
refreshExpiresAt = response.refreshExpiresAt,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun <T : Any> request(requestBlock: suspend () -> T): Either<VisaApiError, T> {
|
||||
return runCatching {
|
||||
Either.Right(requestBlock())
|
||||
}.getOrElse { responseError ->
|
||||
if (responseError is ApiResponseError.HttpException &&
|
||||
responseError.errorBody != null
|
||||
) {
|
||||
val errorCode =
|
||||
errorAdapter.fromJson(responseError.errorBody)?.error?.code ?: responseError.code.numericCode
|
||||
return Either.Left(VisaApiError.fromBackendError(errorCode))
|
||||
}
|
||||
|
||||
return Either.Left(VisaApiError.UnknownWithoutCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -10,15 +10,16 @@ import com.tangem.datasource.api.common.config.ApiEnvironment
|
|||
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
|
||||
import com.tangem.datasource.api.common.response.ApiResponseError
|
||||
import com.tangem.datasource.api.common.response.getOrThrow
|
||||
import com.tangem.datasource.api.pay.TangemPayApi
|
||||
import com.tangem.datasource.api.pay.models.request.*
|
||||
import com.tangem.datasource.api.pay.models.response.VisaErrorResponseJsonAdapter
|
||||
import com.tangem.datasource.api.pay.models.request.SetPinCodeRequest
|
||||
import com.tangem.datasource.api.pay.models.response.TangemPayErrorResponse
|
||||
import com.tangem.datasource.api.visa.VisaApi
|
||||
import com.tangem.datasource.api.visa.models.request.*
|
||||
import com.tangem.datasource.di.NetworkMoshi
|
||||
import com.tangem.datasource.local.visa.VisaAuthTokenStorage
|
||||
import com.tangem.domain.visa.datasource.VisaAuthRemoteDataSource
|
||||
import com.tangem.domain.visa.error.VisaApiError
|
||||
import com.tangem.domain.visa.model.*
|
||||
import com.tangem.domain.visa.repository.VisaActivationRepository
|
||||
import com.tangem.domain.visa.datasource.VisaAuthRemoteDataSource
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
|
|
@ -29,7 +30,7 @@ import kotlinx.coroutines.withContext
|
|||
internal class DefaultVisaActivationRepository @AssistedInject constructor(
|
||||
@Assisted private val visaCardId: VisaCardId,
|
||||
@NetworkMoshi private val moshi: Moshi,
|
||||
private val visaApi: TangemPayApi,
|
||||
private val visaApi: VisaApi,
|
||||
private val dispatcherProvider: CoroutineDispatcherProvider,
|
||||
private val visaAuthTokenStorage: VisaAuthTokenStorage,
|
||||
private val visaAuthRemoteDataSource: VisaAuthRemoteDataSource,
|
||||
|
|
@ -37,7 +38,7 @@ internal class DefaultVisaActivationRepository @AssistedInject constructor(
|
|||
private val apiConfigsManager: ApiConfigsManager,
|
||||
) : VisaActivationRepository {
|
||||
|
||||
private val visaErrorAdapter = VisaErrorResponseJsonAdapter(moshi)
|
||||
private val errorAdapter by lazy { moshi.adapter(TangemPayErrorResponse::class.java) }
|
||||
|
||||
override suspend fun getActivationRemoteState(): Either<VisaApiError, VisaActivationRemoteState> =
|
||||
withContext(dispatcherProvider.io) {
|
||||
|
|
@ -171,6 +172,7 @@ internal class DefaultVisaActivationRepository @AssistedInject constructor(
|
|||
ApiEnvironment.DEV_2,
|
||||
ApiEnvironment.DEV_3,
|
||||
ApiEnvironment.STAGE,
|
||||
ApiEnvironment.STAGE_2,
|
||||
ApiEnvironment.MOCK,
|
||||
-> rsaPublicKey.dev
|
||||
ApiEnvironment.PROD -> rsaPublicKey.prod
|
||||
|
|
@ -186,7 +188,7 @@ internal class DefaultVisaActivationRepository @AssistedInject constructor(
|
|||
responseError.errorBody != null
|
||||
) {
|
||||
val errorCode =
|
||||
visaErrorAdapter.fromJson(responseError.errorBody!!)?.error?.code ?: responseError.code.numericCode
|
||||
errorAdapter.fromJson(responseError.errorBody!!)?.error?.code ?: responseError.code.numericCode
|
||||
return Either.Left(VisaApiError.fromBackendError(errorCode))
|
||||
}
|
||||
|
||||
|
|
@ -206,7 +208,7 @@ internal class DefaultVisaActivationRepository @AssistedInject constructor(
|
|||
}.getOrElse { responseError ->
|
||||
if (responseError is ApiResponseError.HttpException && responseError.errorBody != null) {
|
||||
val errorCode =
|
||||
visaErrorAdapter.fromJson(responseError.errorBody!!)?.error?.code
|
||||
errorAdapter.fromJson(responseError.errorBody!!)?.error?.code
|
||||
?: responseError.code.numericCode
|
||||
Either.Left(VisaApiError.fromBackendError(errorCode))
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -4,33 +4,35 @@ import arrow.core.Either
|
|||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.datasource.api.common.response.ApiResponseError
|
||||
import com.tangem.datasource.api.common.response.getOrThrow
|
||||
import com.tangem.datasource.api.pay.TangemPayApi
|
||||
import com.tangem.datasource.api.pay.TangemPayAuthApi
|
||||
import com.tangem.datasource.api.pay.models.request.*
|
||||
import com.tangem.datasource.api.pay.models.response.VisaErrorResponseJsonAdapter
|
||||
import com.tangem.datasource.api.pay.models.request.RefreshTokenByCardIdRequest
|
||||
import com.tangem.datasource.api.pay.models.response.TangemPayErrorResponse
|
||||
import com.tangem.datasource.api.visa.VisaApi
|
||||
import com.tangem.datasource.api.visa.models.request.*
|
||||
import com.tangem.datasource.di.NetworkMoshi
|
||||
import com.tangem.domain.visa.datasource.VisaAuthRemoteDataSource
|
||||
import com.tangem.domain.visa.error.VisaApiError
|
||||
import com.tangem.domain.visa.model.*
|
||||
import com.tangem.domain.visa.model.VisaAuthChallenge
|
||||
import com.tangem.domain.visa.model.VisaAuthSession
|
||||
import com.tangem.domain.visa.model.VisaAuthSignedChallenge
|
||||
import com.tangem.domain.visa.model.VisaAuthTokens
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class DefaultVisaAuthRemoteDataSource @Inject constructor(
|
||||
@NetworkMoshi private val moshi: Moshi,
|
||||
private val visaAuthApi: TangemPayApi,
|
||||
private val tangemPayAuthApi: TangemPayAuthApi,
|
||||
private val visaApi: VisaApi,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : VisaAuthRemoteDataSource {
|
||||
|
||||
private val visaErrorAdapter = VisaErrorResponseJsonAdapter(moshi)
|
||||
private val errorAdapter by lazy { moshi.adapter(TangemPayErrorResponse::class.java) }
|
||||
|
||||
override suspend fun getCardAuthChallenge(
|
||||
cardId: String,
|
||||
cardPublicKey: String,
|
||||
): Either<VisaApiError, VisaAuthChallenge.Card> = withContext(dispatchers.io) {
|
||||
request {
|
||||
visaAuthApi.generateNonceByCardId(
|
||||
visaApi.generateNonceByCardId(
|
||||
GenerateNoneByCardIdRequest(
|
||||
cardId = cardId,
|
||||
cardPublicKey = cardPublicKey,
|
||||
|
|
@ -49,7 +51,7 @@ internal class DefaultVisaAuthRemoteDataSource @Inject constructor(
|
|||
cardWalletAddress: String,
|
||||
): Either<VisaApiError, VisaAuthChallenge.Wallet> = withContext(dispatchers.io) {
|
||||
request {
|
||||
visaAuthApi.generateNonceByCardWallet(
|
||||
visaApi.generateNonceByCardWallet(
|
||||
GenerateNoneByCardWalletRequest(
|
||||
cardWalletAddress = cardWalletAddress,
|
||||
cardId = cardId,
|
||||
|
|
@ -63,56 +65,13 @@ internal class DefaultVisaAuthRemoteDataSource @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun getCustomerWalletAuthChallenge(
|
||||
customerWalletAddress: String,
|
||||
customerWalletId: String,
|
||||
): Either<VisaApiError, VisaAuthChallenge.Wallet> = withContext(dispatchers.io) {
|
||||
request {
|
||||
tangemPayAuthApi.generateNonceByCustomerWallet(
|
||||
request = GenerateNonceByCustomerWalletRequest(
|
||||
customerWalletAddress = customerWalletAddress,
|
||||
customerWalletId = customerWalletId,
|
||||
),
|
||||
).getOrThrow()
|
||||
}.map { response ->
|
||||
VisaAuthChallenge.Wallet(
|
||||
challenge = response.nonce,
|
||||
session = VisaAuthSession(response.sessionId),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getTokenWithCustomerWallet(
|
||||
sessionId: String,
|
||||
signature: String,
|
||||
nonce: String,
|
||||
): Either<VisaApiError, TangemPayAuthTokens> = withContext(dispatchers.io) {
|
||||
request {
|
||||
tangemPayAuthApi.getTokenByCustomerWallet(
|
||||
request = GetTokenByCustomerWalletRequest(
|
||||
authType = "customer_wallet",
|
||||
sessionId = sessionId,
|
||||
signature = signature,
|
||||
messageFormat = "Tangem Pay wants to sign in with your account. Nonce: $nonce",
|
||||
),
|
||||
).getOrThrow()
|
||||
}.map { response ->
|
||||
TangemPayAuthTokens(
|
||||
accessToken = response.accessToken,
|
||||
expiresAt = response.expiresAt,
|
||||
refreshToken = response.refreshToken,
|
||||
refreshExpiresAt = response.refreshExpiresAt,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getAccessTokens(
|
||||
signedChallenge: VisaAuthSignedChallenge,
|
||||
): Either<VisaApiError, VisaAuthTokens> = withContext(dispatchers.io) {
|
||||
request {
|
||||
when (signedChallenge) {
|
||||
is VisaAuthSignedChallenge.ByCardPublicKey -> {
|
||||
visaAuthApi.getAccessTokenByCardId(
|
||||
visaApi.getAccessTokenByCardId(
|
||||
GetAccessTokenByCardIdRequest(
|
||||
sessionId = signedChallenge.challenge.session.sessionId,
|
||||
signature = signedChallenge.signature,
|
||||
|
|
@ -121,7 +80,7 @@ internal class DefaultVisaAuthRemoteDataSource @Inject constructor(
|
|||
).getOrThrow()
|
||||
}
|
||||
is VisaAuthSignedChallenge.ByWallet -> {
|
||||
visaAuthApi.getAccessTokenByCardWallet(
|
||||
visaApi.getAccessTokenByCardWallet(
|
||||
GetAccessTokenByCardWalletRequest(
|
||||
sessionId = signedChallenge.challenge.session.sessionId,
|
||||
signature = signedChallenge.signature,
|
||||
|
|
@ -150,11 +109,11 @@ internal class DefaultVisaAuthRemoteDataSource @Inject constructor(
|
|||
request {
|
||||
when (refreshToken.authType) {
|
||||
VisaAuthTokens.RefreshToken.Type.CardId ->
|
||||
visaAuthApi.refreshCardIdAccessToken(
|
||||
visaApi.refreshCardIdAccessToken(
|
||||
RefreshTokenByCardIdRequest(refreshToken = refreshToken.value),
|
||||
)
|
||||
VisaAuthTokens.RefreshToken.Type.CardWallet ->
|
||||
visaAuthApi.refreshCardIdAccessToken(
|
||||
visaApi.refreshCardIdAccessToken(
|
||||
RefreshTokenByCardIdRequest(refreshToken = refreshToken.value),
|
||||
)
|
||||
}.getOrThrow()
|
||||
|
|
@ -169,7 +128,7 @@ internal class DefaultVisaAuthRemoteDataSource @Inject constructor(
|
|||
override suspend fun exchangeAccessToken(tokens: VisaAuthTokens): Either<VisaApiError, VisaAuthTokens> =
|
||||
withContext(dispatchers.io) {
|
||||
request {
|
||||
visaAuthApi.exchangeAccessToken(
|
||||
visaApi.exchangeAccessToken(
|
||||
ExchangeAccessTokenRequest(
|
||||
accessToken = tokens.accessToken,
|
||||
refreshToken = tokens.refreshToken.value,
|
||||
|
|
@ -194,7 +153,7 @@ internal class DefaultVisaAuthRemoteDataSource @Inject constructor(
|
|||
responseError.errorBody != null
|
||||
) {
|
||||
val errorCode =
|
||||
visaErrorAdapter.fromJson(responseError.errorBody!!)?.error?.code ?: responseError.code.numericCode
|
||||
errorAdapter.fromJson(responseError.errorBody!!)?.error?.code ?: responseError.code.numericCode
|
||||
return Either.Left(VisaApiError.fromBackendError(errorCode))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,8 +14,8 @@ import com.tangem.data.common.cache.CacheRegistry
|
|||
import com.tangem.data.common.quote.QuotesFetcher
|
||||
import com.tangem.data.visa.config.VisaLibLoader
|
||||
import com.tangem.data.visa.utils.*
|
||||
import com.tangem.datasource.api.pay.TangemPayApi
|
||||
import com.tangem.datasource.api.pay.models.response.VisaTxHistoryResponse
|
||||
import com.tangem.datasource.api.visa.VisaApi
|
||||
import com.tangem.datasource.api.visa.models.response.VisaTxHistoryResponse
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.card.common.util.cardTypesResolver
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
|
|
@ -43,7 +43,7 @@ internal class DefaultVisaRepository @Inject constructor(
|
|||
private val userWalletsStore: UserWalletsStore,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val visaApiRequestMaker: VisaApiRequestMaker,
|
||||
private val visaApi: TangemPayApi,
|
||||
private val visaApi: VisaApi,
|
||||
private val visaCurrencyFactory: VisaCurrencyFactory,
|
||||
) : VisaRepository {
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
package com.tangem.data.visa.converter
|
||||
|
||||
import com.tangem.datasource.api.pay.models.response.CardActivationRemoteStateResponse
|
||||
import com.tangem.datasource.api.visa.models.response.CardActivationRemoteStateResponse
|
||||
import com.tangem.domain.visa.model.VisaActivationOrderInfo
|
||||
import com.tangem.domain.visa.model.VisaActivationRemoteState
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
package com.tangem.data.visa.di
|
||||
|
||||
import com.tangem.data.pay.datasource.DefaultTangemPayAuthDataSource
|
||||
import com.tangem.data.visa.DefaultTangemPayRemoteDataSource
|
||||
import com.tangem.data.visa.DefaultVisaActivationRepository
|
||||
import com.tangem.data.visa.DefaultVisaAuthRemoteDataSource
|
||||
import com.tangem.data.visa.MockVisaRepository
|
||||
import com.tangem.domain.pay.datasource.TangemPayAuthDataSource
|
||||
import com.tangem.domain.visa.datasource.TangemPayRemoteDataSource
|
||||
import com.tangem.domain.visa.repository.VisaActivationRepository
|
||||
import com.tangem.domain.visa.datasource.VisaAuthRemoteDataSource
|
||||
import com.tangem.domain.visa.repository.VisaRepository
|
||||
|
|
@ -22,22 +24,16 @@ internal interface VisaDataModule {
|
|||
@Singleton
|
||||
fun bindVisaAuthRemoteDataSource(repository: DefaultVisaAuthRemoteDataSource): VisaAuthRemoteDataSource
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindTangemPayRemoteDataSource(impl: DefaultTangemPayRemoteDataSource): TangemPayRemoteDataSource
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindVisaActivationRepositoryFactory(
|
||||
repository: DefaultVisaActivationRepository.Factory,
|
||||
): VisaActivationRepository.Factory
|
||||
|
||||
// Mocked
|
||||
// @Binds
|
||||
// @Singleton
|
||||
// fun bindVisaActivationRepositoryFactory(
|
||||
// repository: MockVisaActivationRepository.Factory,
|
||||
// ): VisaActivationRepository.Factory
|
||||
|
||||
// @Binds
|
||||
// fun bindVisaRepository(repository: DefaultVisaRepository): VisaRepository
|
||||
|
||||
// Mocked
|
||||
@Binds
|
||||
fun bindVisaRepository(repository: MockVisaRepository): VisaRepository
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ import com.tangem.data.visa.model.AccessCodeData
|
|||
import com.tangem.datasource.api.common.response.ApiResponse
|
||||
import com.tangem.datasource.api.common.response.ApiResponseError
|
||||
import com.tangem.datasource.api.common.response.getOrThrow
|
||||
import com.tangem.datasource.api.pay.TangemPayApi
|
||||
import com.tangem.datasource.api.pay.models.request.RefreshTokenByCardWalletRequest
|
||||
import com.tangem.datasource.api.visa.VisaApi
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.card.common.util.cardTypesResolver
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
|
|
@ -24,7 +24,7 @@ typealias VisaAuthorizationHeader = String
|
|||
|
||||
internal class VisaApiRequestMaker @Inject constructor(
|
||||
private val userWalletsStore: UserWalletsStore,
|
||||
private val visaAuthApi: TangemPayApi,
|
||||
private val visaAuthApi: VisaApi,
|
||||
private val accessCodeDataConverter: AccessCodeDataConverter,
|
||||
private val dispatcherProvider: CoroutineDispatcherProvider,
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ package com.tangem.data.visa.utils
|
|||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.externallinkprovider.TxExploreState
|
||||
import com.tangem.datasource.api.pay.models.response.VisaTxHistoryResponse
|
||||
import com.tangem.datasource.api.visa.models.response.VisaTxHistoryResponse
|
||||
import com.tangem.domain.visa.model.VisaTxDetails
|
||||
|
||||
internal class VisaTxDetailsFactory {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
package com.tangem.data.visa.utils
|
||||
|
||||
import com.tangem.datasource.api.pay.models.response.VisaTxHistoryResponse
|
||||
import com.tangem.datasource.api.visa.models.response.VisaTxHistoryResponse
|
||||
import com.tangem.domain.visa.model.VisaTxHistoryItem
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ package com.tangem.data.visa.utils
|
|||
import androidx.paging.PagingSource
|
||||
import androidx.paging.PagingState
|
||||
import com.tangem.data.common.cache.CacheRegistry
|
||||
import com.tangem.datasource.api.pay.models.response.VisaTxHistoryResponse
|
||||
import com.tangem.datasource.api.visa.models.response.VisaTxHistoryResponse
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.visa.model.VisaTxHistoryItem
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
|
|
|
|||
|
|
@ -47,7 +47,6 @@
|
|||
<ID>SuspendFunSwallowedCancellation:DefaultWcPairUseCase.kt$DefaultWcPairUseCase$runCatching</ID>
|
||||
<ID>UseAnyOrNoneInsteadOfFind:DefaultWcSessionsManager.kt$DefaultWcSessionsManager$find { it.sdkModel.topic == dto.topic }</ID>
|
||||
<ID>UseEmptyCounterpart:AssociateNetworksDelegate.kt$AssociateNetworksDelegate.Companion$listOf()</ID>
|
||||
<ID>UseEmptyCounterpart:DefaultWcRequestService.kt$DefaultWcRequestService$setOf()</ID>
|
||||
<ID>UseEmptyCounterpart:WcAppMetaDataConverter.kt$WcAppMetaDataConverter$listOf()</ID>
|
||||
<ID>UseEmptyCounterpart:WcNetworksConverter.kt$WcNetworksConverter$listOf()</ID>
|
||||
<ID>UseEmptyCounterpart:WcSdkSessionConverter.kt$WcSdkSessionConverter$listOf()</ID>
|
||||
|
|
|
|||
|
|
@ -166,7 +166,6 @@ internal class DefaultWcPairUseCase @AssistedInject constructor(
|
|||
}
|
||||
|
||||
override fun approve(sessionForApprove: WcSessionApprove) {
|
||||
analytics.send(WcAnalyticEvents.PairButtonConnect)
|
||||
onCallTerminalAction.trySend(TerminalAction.Approve(sessionForApprove))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,10 +2,7 @@
|
|||
<SmellBaseline>
|
||||
<ManuallySuppressedIssues/>
|
||||
<CurrentIssues>
|
||||
<ID>MultilineLambdaItParameter:DefaultWalletManagersFacade.kt$DefaultWalletManagersFacade${ Token( name = it.name, symbol = it.symbol, contractAddress = it.contractAddress, decimals = it.decimals, id = it.id, ) }</ID>
|
||||
<ID>MultilineLambdaItParameter:UpdateWalletManagerResultFactory.kt$UpdateWalletManagerResultFactory${ createCurrencyTransaction( txHistoryItemConverter = txHistoryItemConverter, data = it, ) }</ID>
|
||||
<ID>NamedArguments:DefaultWalletManagersFacade.kt$DefaultWalletManagersFacade$getAndUpdateWalletManager(userWallet, blockchain, derivationPath, extraTokens)</ID>
|
||||
<ID>UnnecessaryLet:DefaultWalletManagersFacade.kt$DefaultWalletManagersFacade$let(txHistoryStateConverter::convert)</ID>
|
||||
<ID>UnsafeCallOnNullableType:WalletManagerFactory.kt$blockchain.getTestnetVersion()!!</ID>
|
||||
<ID>UnsafeCallOnNullableType:WalletManagerFactory.kt$scanResponse.secondTwinPublicKey!!</ID>
|
||||
</CurrentIssues>
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue