Updated on 2026-08-14
This commit is contained in:
commit
808cd4a301
503 changed files with 12306 additions and 3036 deletions
|
|
@ -29,6 +29,5 @@ interface ETagsStore {
|
|||
enum class Key {
|
||||
WalletAccounts,
|
||||
UserTokens,
|
||||
;
|
||||
}
|
||||
}
|
||||
|
|
@ -47,7 +47,6 @@ interface QuotesFetcher {
|
|||
value = setOf(PRICE, PRICE_CHANGE_24H, PRICE_CHANGE_1W, PRICE_CHANGE_30D).combine(),
|
||||
),
|
||||
LAST_UPDATED_AT(value = "lastUpdatedAt"),
|
||||
;
|
||||
}
|
||||
|
||||
sealed interface Error {
|
||||
|
|
|
|||
|
|
@ -2,11 +2,13 @@ package com.tangem.data.notifications
|
|||
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys
|
||||
import com.tangem.datasource.local.preferences.utils.get
|
||||
import com.tangem.datasource.local.preferences.utils.getObjectMapSync
|
||||
import com.tangem.datasource.local.preferences.utils.getSyncOrDefault
|
||||
import com.tangem.datasource.local.preferences.utils.getSyncOrNull
|
||||
import com.tangem.datasource.local.preferences.utils.store
|
||||
import com.tangem.domain.notifications.repository.NotificationsRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import javax.inject.Inject
|
||||
|
||||
class DefaultNotificationsRepository @Inject constructor(
|
||||
|
|
@ -17,6 +19,10 @@ class DefaultNotificationsRepository @Inject constructor(
|
|||
return appPreferencesStore.getSyncOrDefault(PreferencesKeys.getShouldShowNotificationKey(key), true)
|
||||
}
|
||||
|
||||
override fun getShouldShowNotification(key: String): Flow<Boolean> {
|
||||
return appPreferencesStore.get(PreferencesKeys.getShouldShowNotificationKey(key), true)
|
||||
}
|
||||
|
||||
override suspend fun setShouldShowNotifications(key: String, value: Boolean) {
|
||||
appPreferencesStore.store(PreferencesKeys.getShouldShowNotificationKey(key), value)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
package com.tangem.data.swap
|
||||
|
||||
import arrow.core.right
|
||||
import arrow.core.none
|
||||
import arrow.core.some
|
||||
import arrow.core.toOption
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.data.common.api.safeApiCall
|
||||
import com.tangem.data.swap.converter.SwapDataConverter
|
||||
|
|
@ -28,7 +30,7 @@ import com.tangem.domain.quotes.single.SingleQuoteStatusProducer
|
|||
import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier
|
||||
import com.tangem.domain.swap.SwapRepositoryV2
|
||||
import com.tangem.domain.swap.models.*
|
||||
import com.tangem.domain.tokens.utils.CurrencyStatusProxyCreator
|
||||
import com.tangem.domain.tokens.operations.CryptoCurrencyStatusFactory
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.async
|
||||
|
|
@ -49,7 +51,6 @@ internal class DefaultSwapRepositoryV2 @Inject constructor(
|
|||
private val dataSignatureVerifier: DataSignatureVerifier,
|
||||
private val singleQuoteStatusSupplier: SingleQuoteStatusSupplier,
|
||||
private val singleQuoteStatusFetcher: SingleQuoteStatusFetcher,
|
||||
private val currencyStatusProxyCreator: CurrencyStatusProxyCreator,
|
||||
@NetworkMoshi moshi: Moshi,
|
||||
) : SwapRepositoryV2 {
|
||||
|
||||
|
|
@ -391,17 +392,19 @@ internal class DefaultSwapRepositoryV2 @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
return currencyStatusProxyCreator.createCurrencyStatus(
|
||||
val quoteStatus = quote ?: singleQuoteStatusSupplier.getSyncOrNull(
|
||||
params = SingleQuoteStatusProducer.Params(rawCurrencyId = rawCurrencyId),
|
||||
)
|
||||
|
||||
return CryptoCurrencyStatusFactory.create(
|
||||
currency = cryptoCurrency,
|
||||
maybeQuoteStatus = quote?.right() ?: singleQuoteStatusSupplier.getSyncOrNull(
|
||||
params = SingleQuoteStatusProducer.Params(rawCurrencyId = rawCurrencyId),
|
||||
).right(),
|
||||
maybeNetworkStatus = NetworkStatus(
|
||||
network = cryptoCurrency.network,
|
||||
value = NetworkStatus.MissedDerivation, // Caution!!! Do not change this status
|
||||
).right(),
|
||||
maybeYieldBalance = null,
|
||||
).getOrNull()
|
||||
).some(),
|
||||
maybeQuoteStatus = quoteStatus.toOption(),
|
||||
maybeYieldBalance = none(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun parseTxDetails(txDetailsJson: String): TxDetails? {
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier
|
|||
import com.tangem.domain.swap.SwapErrorResolver
|
||||
import com.tangem.domain.swap.SwapRepositoryV2
|
||||
import com.tangem.domain.swap.SwapTransactionRepository
|
||||
import com.tangem.domain.tokens.utils.CurrencyStatusProxyCreator
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
|
|
@ -59,7 +58,6 @@ internal object SwapDataModule {
|
|||
moshi = moshi,
|
||||
singleQuoteStatusSupplier = singleQuoteStatusSupplier,
|
||||
singleQuoteStatusFetcher = singleQuoteStatusFetcher,
|
||||
currencyStatusProxyCreator = CurrencyStatusProxyCreator(),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import com.tangem.data.tokens.repository.DefaultCurrenciesRepository
|
|||
import com.tangem.data.tokens.repository.DefaultCurrencyChecksRepository
|
||||
import com.tangem.data.tokens.repository.DefaultPolkadotAccountHealthCheckRepository
|
||||
import com.tangem.data.tokens.repository.DefaultTokenReceiveWarningsViewedRepository
|
||||
import com.tangem.data.tokens.repository.DefaultYieldSupplyWarningsViewedRepository
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.exchangeservice.swap.ExpressServiceLoader
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
|
|
@ -19,6 +20,7 @@ import com.tangem.domain.tokens.repository.CurrenciesRepository
|
|||
import com.tangem.domain.tokens.repository.CurrencyChecksRepository
|
||||
import com.tangem.domain.tokens.repository.PolkadotAccountHealthCheckRepository
|
||||
import com.tangem.domain.tokens.repository.TokenReceiveWarningsViewedRepository
|
||||
import com.tangem.domain.tokens.repository.YieldSupplyWarningsViewedRepository
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
|
|
@ -96,4 +98,16 @@ internal object TokensDataModule {
|
|||
tokenReceiveWarningActionStore = tokenReceiveWarningActionStore,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideDefaultYieldSupplyWarningsViewedRepository(
|
||||
appPreferencesStore: AppPreferencesStore,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): YieldSupplyWarningsViewedRepository {
|
||||
return DefaultYieldSupplyWarningsViewedRepository(
|
||||
appPreferencesStore = appPreferencesStore,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
package com.tangem.data.tokens.repository
|
||||
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys
|
||||
import com.tangem.datasource.local.preferences.utils.getObjectSet
|
||||
import com.tangem.domain.tokens.repository.YieldSupplyWarningsViewedRepository
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
internal class DefaultYieldSupplyWarningsViewedRepository(
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : YieldSupplyWarningsViewedRepository {
|
||||
|
||||
override suspend fun getViewedWarnings(): Set<String> = withContext(dispatchers.io) {
|
||||
appPreferencesStore.getObjectSet<String>(PreferencesKeys.YIELD_SUPPLY_WARNINGS_STATES_KEY).firstOrNull()
|
||||
?: emptySet()
|
||||
}
|
||||
|
||||
override suspend fun view(symbol: String) = withContext(dispatchers.io) {
|
||||
appPreferencesStore.editData { mutablePreferences ->
|
||||
val stored = mutablePreferences.getObjectSet<String>(
|
||||
PreferencesKeys.YIELD_SUPPLY_WARNINGS_STATES_KEY,
|
||||
) ?: mutableSetOf()
|
||||
|
||||
val updated = stored + symbol
|
||||
|
||||
mutablePreferences.setObjectSet<String>(
|
||||
key = PreferencesKeys.YIELD_SUPPLY_WARNINGS_STATES_KEY,
|
||||
value = updated,
|
||||
)
|
||||
}
|
||||
return@withContext
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,15 @@
|
|||
package com.tangem.data.pay.di
|
||||
|
||||
import com.tangem.data.pay.repository.DefaultKycRepository
|
||||
import com.tangem.data.pay.repository.DefaultTangemPayTxHistoryRepository
|
||||
import com.tangem.data.pay.repository.DefaultOnboardingRepository
|
||||
import com.tangem.domain.pay.repository.KycRepository
|
||||
import com.tangem.domain.pay.repository.OnboardingRepository
|
||||
import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase
|
||||
import com.tangem.domain.tangempay.repository.TangemPayTxHistoryRepository
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
|
@ -21,4 +25,18 @@ internal interface TangemPayDataModule {
|
|||
@Binds
|
||||
@Singleton
|
||||
fun bindOnboardingRepository(repository: DefaultOnboardingRepository): OnboardingRepository
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindTangemPayTxHistoryRepository(repository: DefaultTangemPayTxHistoryRepository): TangemPayTxHistoryRepository
|
||||
|
||||
companion object {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideTangemPayMainScreenCustomerInfoUseCase(
|
||||
repository: OnboardingRepository,
|
||||
): TangemPayMainScreenCustomerInfoUseCase {
|
||||
return TangemPayMainScreenCustomerInfoUseCase(repository = repository)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
package com.tangem.data.pay.repository
|
||||
|
||||
import com.tangem.datasource.api.common.response.getOrThrow
|
||||
import com.tangem.datasource.api.pay.TangemPayApi
|
||||
import com.tangem.domain.pay.KycStartInfo
|
||||
import com.tangem.domain.pay.repository.KycRepository
|
||||
|
|
@ -16,9 +15,9 @@ internal class DefaultKycRepository @Inject constructor(
|
|||
|
||||
override suspend fun getKycStartInfo() = withContext(dispatchers.io) {
|
||||
requestHelper.request { authHeader ->
|
||||
tangemPayApi.getKycAccess(authHeader = authHeader).getOrThrow().result
|
||||
tangemPayApi.getKycAccess(authHeader = authHeader)
|
||||
}.map {
|
||||
KycStartInfo(token = it.token, locale = it.locale)
|
||||
KycStartInfo(token = it.result.token, locale = it.result.locale)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,13 +3,13 @@ package com.tangem.data.pay.repository
|
|||
import arrow.core.Either
|
||||
import arrow.core.raise.either
|
||||
import com.tangem.core.error.UniversalError
|
||||
import com.tangem.datasource.api.common.response.getOrThrow
|
||||
import com.tangem.datasource.api.pay.TangemPayApi
|
||||
import com.tangem.datasource.api.pay.models.request.DeeplinkValidityRequest
|
||||
import com.tangem.datasource.api.pay.models.response.CustomerMeResponse
|
||||
import com.tangem.domain.pay.model.CustomerInfo.CardInfo
|
||||
import com.tangem.domain.pay.model.CustomerInfo
|
||||
import com.tangem.domain.pay.model.ProductInstance
|
||||
import com.tangem.domain.pay.model.CustomerInfo.ProductInstance
|
||||
import com.tangem.domain.pay.repository.OnboardingRepository
|
||||
import com.tangem.domain.visa.error.VisaApiError
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val VALID_STATUS = "valid"
|
||||
|
|
@ -21,20 +21,38 @@ internal class DefaultOnboardingRepository @Inject constructor(
|
|||
|
||||
override suspend fun validateDeeplink(link: String): Either<UniversalError, Boolean> = either {
|
||||
return requestHelper.request {
|
||||
tangemPayApi.validateDeeplink(DeeplinkValidityRequest(link)).getOrThrow().result
|
||||
?: raise(VisaApiError.UnknownWithoutCode)
|
||||
}.map { result -> result.status == VALID_STATUS }
|
||||
tangemPayApi.validateDeeplink(DeeplinkValidityRequest(link))
|
||||
}.map { it.result?.status == VALID_STATUS }
|
||||
}
|
||||
|
||||
override suspend fun getCustomerInfo(): Either<UniversalError, CustomerInfo> = either {
|
||||
return requestHelper.request { authHeader ->
|
||||
val response = tangemPayApi.getCustomerMe(authHeader).getOrThrow()
|
||||
response.result ?: raise(VisaApiError.UnknownWithoutCode)
|
||||
}.map { result ->
|
||||
CustomerInfo(
|
||||
productInstance = result.productInstance?.let { ProductInstance(id = it.id, status = it.status) },
|
||||
kycStatus = result.kyc?.status,
|
||||
tangemPayApi.getCustomerMe(authHeader)
|
||||
}.map { getCustomerInfo(it.result) }
|
||||
}
|
||||
|
||||
override suspend fun getMainScreenCustomerInfo(): Either<UniversalError, CustomerInfo> = either {
|
||||
return requestHelper.requestWithPersistedToken { authHeader ->
|
||||
tangemPayApi.getCustomerMe(authHeader)
|
||||
}.map { getCustomerInfo(it.result) }
|
||||
}
|
||||
|
||||
private fun getCustomerInfo(response: CustomerMeResponse.Result?): CustomerInfo {
|
||||
val card = response?.card
|
||||
val balance = response?.balance
|
||||
val cardInfo = if (card != null && balance != null) {
|
||||
CardInfo(
|
||||
lastFourDigits = card.cardNumberEnd,
|
||||
balance = balance.availableBalance,
|
||||
currencyCode = balance.currency,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
return CustomerInfo(
|
||||
productInstance = response?.productInstance?.let { ProductInstance(id = it.id, status = it.status) },
|
||||
kycStatus = response?.kyc?.status,
|
||||
cardInfo = cardInfo,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
package com.tangem.data.pay.repository
|
||||
|
||||
import com.tangem.data.common.cache.CacheRegistry
|
||||
import com.tangem.data.visa.utils.TangemPayTxHistoryItemConverter
|
||||
import com.tangem.datasource.api.pay.TangemPayApi
|
||||
import com.tangem.datasource.local.visa.TangemPayTxHistoryItemsStore
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.tangempay.model.TangemPayTxHistoryListBatchFlow
|
||||
import com.tangem.domain.tangempay.model.TangemPayTxHistoryListBatchingContext
|
||||
import com.tangem.domain.tangempay.model.TangemPayTxHistoryListConfig
|
||||
import com.tangem.domain.tangempay.repository.TangemPayTxHistoryRepository
|
||||
import com.tangem.domain.visa.model.TangemPayTxHistoryItem
|
||||
import com.tangem.pagination.BatchFetchResult
|
||||
import com.tangem.pagination.BatchListSource
|
||||
import com.tangem.pagination.fetcher.BatchFetcher
|
||||
import com.tangem.pagination.fetcher.CursorBatchFetcher
|
||||
import com.tangem.pagination.toBatchFlow
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val INITIAL_CURSOR = "initial_cursor_key"
|
||||
|
||||
internal class DefaultTangemPayTxHistoryRepository @Inject constructor(
|
||||
private val requestPerformer: TangemPayRequestPerformer,
|
||||
private val visaApi: TangemPayApi,
|
||||
private val cacheRegistry: CacheRegistry,
|
||||
private val txHistoryItemsStore: TangemPayTxHistoryItemsStore,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : TangemPayTxHistoryRepository {
|
||||
|
||||
override fun getTxHistoryBatchFlow(
|
||||
batchSize: Int,
|
||||
context: TangemPayTxHistoryListBatchingContext,
|
||||
): TangemPayTxHistoryListBatchFlow {
|
||||
return BatchListSource(
|
||||
fetchDispatcher = dispatchers.io,
|
||||
context = context,
|
||||
generateNewKey = { keys -> keys.lastOrNull()?.inc() ?: 0 },
|
||||
batchFetcher = createFetcher(batchSize),
|
||||
).toBatchFlow()
|
||||
}
|
||||
|
||||
private fun createFetcher(
|
||||
batchSize: Int,
|
||||
): BatchFetcher<TangemPayTxHistoryListConfig, List<TangemPayTxHistoryItem>> {
|
||||
return CursorBatchFetcher(
|
||||
prefetchDistance = batchSize,
|
||||
batchSize = batchSize,
|
||||
subFetcher = { request, _, _ ->
|
||||
val items = loadItems(config = request.params, cursor = request.cursor, limit = request.limit)
|
||||
BatchFetchResult.Success(
|
||||
data = items,
|
||||
last = items.size < request.limit,
|
||||
empty = items.isEmpty(),
|
||||
)
|
||||
},
|
||||
cursorFromItem = { item -> item.id }, // last item’s id becomes next cursor
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun loadItems(
|
||||
config: TangemPayTxHistoryListConfig,
|
||||
cursor: String?,
|
||||
limit: Int,
|
||||
): List<TangemPayTxHistoryItem> {
|
||||
cacheRegistry.invokeOnExpire(
|
||||
key = getCacheKey(userWalletId = config.userWalletId, cursor = cursor),
|
||||
skipCache = config.refresh,
|
||||
block = { fetch(userWalletId = config.userWalletId, cursor = cursor, pageSize = limit) },
|
||||
)
|
||||
|
||||
return txHistoryItemsStore.getSyncOrNull(
|
||||
key = config.userWalletId,
|
||||
cursor = cursor ?: INITIAL_CURSOR,
|
||||
).orEmpty()
|
||||
}
|
||||
|
||||
private fun getCacheKey(userWalletId: UserWalletId, cursor: String?): String {
|
||||
return "tangem_pay_tx_history_${userWalletId}_${cursor ?: INITIAL_CURSOR}"
|
||||
}
|
||||
|
||||
private suspend fun fetch(userWalletId: UserWalletId, cursor: String?, pageSize: Int) {
|
||||
val response = requestPerformer.request { authHeader ->
|
||||
visaApi.getTangemPayTxHistory(
|
||||
authHeader = authHeader,
|
||||
limit = pageSize,
|
||||
cursor = cursor,
|
||||
)
|
||||
}.getOrNull()
|
||||
response?.let {
|
||||
val items = TangemPayTxHistoryItemConverter.convertList(response.result.transactions)
|
||||
txHistoryItemsStore.store(key = userWalletId, cursor = cursor ?: INITIAL_CURSOR, value = items)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,22 +4,26 @@ import arrow.core.Either
|
|||
import arrow.core.raise.either
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.core.error.UniversalError
|
||||
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.models.response.VisaErrorResponseJsonAdapter
|
||||
import com.tangem.datasource.di.NetworkMoshi
|
||||
import com.tangem.datasource.local.visa.TangemPayStorage
|
||||
import com.tangem.domain.core.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.pay.datasource.TangemPayAuthDataSource
|
||||
import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase
|
||||
import com.tangem.domain.visa.error.VisaApiError
|
||||
import com.tangem.domain.visa.model.VisaAuthTokens
|
||||
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
|
|
@ -33,9 +37,9 @@ internal class TangemPayRequestPerformer @Inject constructor(
|
|||
@NetworkMoshi moshi: Moshi,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val tangemPayStorage: TangemPayStorage,
|
||||
private val userWalletsRepository: UserWalletsListRepository,
|
||||
private val getCurrencyUseCase: GetSingleCryptoCurrencyStatusUseCase,
|
||||
private val authDataSource: TangemPayAuthDataSource,
|
||||
private val getWalletsUseCase: GetWalletsUseCase,
|
||||
) {
|
||||
private val visaErrorAdapter = VisaErrorResponseJsonAdapter(moshi)
|
||||
|
||||
|
|
@ -44,24 +48,42 @@ internal class TangemPayRequestPerformer @Inject constructor(
|
|||
private val refreshTokensMutex = Mutex()
|
||||
private var refreshTokensJob: Deferred<Either<UniversalError, VisaAuthTokens>>? = null
|
||||
|
||||
suspend fun <T : Any> request(requestBlock: suspend (header: String) -> T): Either<UniversalError, T> = either {
|
||||
suspend fun <T : Any> request(requestBlock: suspend (header: String) -> ApiResponse<T>): Either<UniversalError, T> =
|
||||
either {
|
||||
withContext(dispatchers.io) {
|
||||
performRequest(
|
||||
requestBlock = requestBlock,
|
||||
getTokens = ::getAccessTokens,
|
||||
refreshTokens = ::refreshAuthTokens,
|
||||
).bind()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun <T : Any> requestWithPersistedToken(
|
||||
requestBlock: suspend (header: String) -> ApiResponse<T>,
|
||||
): Either<UniversalError, T> = either {
|
||||
withContext(dispatchers.io) {
|
||||
performRequest(requestBlock = requestBlock, refreshTokens = ::refreshAuthTokens).bind()
|
||||
performRequest(
|
||||
requestBlock = requestBlock,
|
||||
getTokens = ::getAccessTokensIfSaved,
|
||||
refreshTokens = ::refreshAuthTokens,
|
||||
).bind()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun <T : Any> performRequest(
|
||||
requestBlock: suspend (header: String) -> T,
|
||||
requestBlock: suspend (header: String) -> ApiResponse<T>,
|
||||
getTokens: (suspend () -> Either<UniversalError, VisaAuthTokens>),
|
||||
refreshTokens: (suspend () -> Either<UniversalError, VisaAuthTokens>)? = null,
|
||||
): Either<UniversalError, T> = either {
|
||||
runCatching {
|
||||
requestBlock("Bearer ${getAccessTokens().bind().accessToken}")
|
||||
requestBlock("Bearer ${getTokens().bind().accessToken}").getOrThrow()
|
||||
}.getOrElse { error ->
|
||||
when (error) {
|
||||
is ApiResponseError.HttpException -> {
|
||||
if (refreshTokens != null && error.code == ApiResponseError.HttpException.Code.UNAUTHORIZED) {
|
||||
refreshOrJoin(refreshTokens).bind()
|
||||
performRequest(requestBlock, refreshTokens = null).bind()
|
||||
performRequest(requestBlock, refreshTokens = null, getTokens = getTokens).bind()
|
||||
} else {
|
||||
raise(mapHttpError(error))
|
||||
}
|
||||
|
|
@ -100,9 +122,7 @@ internal class TangemPayRequestPerformer @Inject constructor(
|
|||
}
|
||||
|
||||
private suspend fun getCustomerWalletAddress(): Either<UniversalError, String> = either {
|
||||
customerWalletAddress
|
||||
?: tangemPayStorage.getCustomerWalletAddress()
|
||||
?: fetchAuthInputData().bind().address
|
||||
customerWalletAddress ?: fetchAuthInputData().bind().address
|
||||
}
|
||||
|
||||
private suspend fun getAccessTokens(): Either<UniversalError, VisaAuthTokens> = either {
|
||||
|
|
@ -110,6 +130,11 @@ internal class TangemPayRequestPerformer @Inject constructor(
|
|||
tangemPayStorage.getAuthTokens(address) ?: fetchTokens().bind()
|
||||
}
|
||||
|
||||
private suspend fun getAccessTokensIfSaved(): Either<UniversalError, VisaAuthTokens> = either {
|
||||
tangemPayStorage.getAuthTokens(getCustomerWalletAddress().bind())
|
||||
?: raise(VisaApiError.UnknownWithoutCode)
|
||||
}
|
||||
|
||||
private fun mapHttpError(throwable: ApiResponseError.HttpException): UniversalError {
|
||||
val errorBody = throwable.errorBody ?: return VisaApiError.UnknownWithoutCode
|
||||
return runCatching {
|
||||
|
|
@ -122,14 +147,16 @@ internal class TangemPayRequestPerformer @Inject constructor(
|
|||
}
|
||||
|
||||
private suspend fun fetchAuthInputData(): Either<UniversalError, AuthInputData> = either {
|
||||
val wallet = userWalletsRepository.userWalletsSync().find { it is UserWallet.Cold } as? UserWallet.Cold
|
||||
val userWallets = getWalletsUseCase()
|
||||
.filter { it.isNotEmpty() }
|
||||
.first()
|
||||
val wallet = userWallets.find { it is UserWallet.Cold } as? UserWallet.Cold
|
||||
?: raise(VisaApiError.UnknownWithoutCode)
|
||||
|
||||
val address = getCurrencyUseCase.invokeMultiWalletSync(wallet.walletId, CryptoCurrency.ID.fromValue(POL_VALUE))
|
||||
.getOrNull()?.value?.networkAddress?.defaultAddress?.value ?: raise(VisaApiError.UnknownWithoutCode)
|
||||
|
||||
customerWalletAddress = address
|
||||
tangemPayStorage.storeCustomerWalletAddress(address)
|
||||
|
||||
AuthInputData(address, wallet.cardId)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,51 @@
|
|||
package com.tangem.data.visa.utils
|
||||
|
||||
import com.tangem.datasource.api.pay.models.response.TangemPayTxHistoryResponse
|
||||
import com.tangem.domain.visa.model.TangemPayTxHistoryItem
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
internal object TangemPayTxHistoryItemConverter :
|
||||
Converter<TangemPayTxHistoryResponse.Transaction, TangemPayTxHistoryItem> {
|
||||
|
||||
@Suppress("CyclomaticComplexMethod")
|
||||
override fun convert(value: TangemPayTxHistoryResponse.Transaction): TangemPayTxHistoryItem {
|
||||
val spend = value.spend
|
||||
val collateral = value.collateral
|
||||
val payment = value.payment
|
||||
val fee = value.fee
|
||||
|
||||
return TangemPayTxHistoryItem(
|
||||
id = value.id,
|
||||
date = when {
|
||||
spend != null -> spend.postedAt
|
||||
collateral != null -> collateral.postedAt
|
||||
payment != null -> payment.postedAt
|
||||
fee != null -> fee.postedAt
|
||||
else -> null
|
||||
},
|
||||
amount = when {
|
||||
spend != null -> spend.amount
|
||||
collateral != null -> collateral.amount
|
||||
payment != null -> payment.amount
|
||||
fee != null -> fee.amount
|
||||
else -> null
|
||||
},
|
||||
merchantName = when {
|
||||
spend != null -> spend.merchantName
|
||||
else -> null
|
||||
},
|
||||
status = when {
|
||||
spend != null -> spend.status
|
||||
payment != null -> payment.status
|
||||
else -> null
|
||||
},
|
||||
currency = when {
|
||||
spend != null -> spend.currency
|
||||
collateral != null -> collateral.currency
|
||||
payment != null -> payment.currency
|
||||
fee != null -> fee.currency
|
||||
else -> null
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.data.visa.utils
|
||||
|
||||
import com.tangem.datasource.api.pay.models.response.VisaTxHistoryResponse
|
||||
import com.tangem.domain.visa.model.VisaTxHistoryItem
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
internal object VisaTxHistoryItemConverter : Converter<VisaTxHistoryResponse.Transaction, VisaTxHistoryItem> {
|
||||
|
||||
override fun convert(value: VisaTxHistoryResponse.Transaction): VisaTxHistoryItem {
|
||||
return VisaTxHistoryItem(
|
||||
id = value.transactionId.toString(),
|
||||
date = value.transactionDt,
|
||||
amount = value.blockchainAmount,
|
||||
fiatAmount = value.transactionAmount,
|
||||
merchantName = value.merchantName,
|
||||
status = value.transactionStatus,
|
||||
fiatCurrency = findCurrencyByNumericCode(value.transactionCurrencyCode),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
package com.tangem.data.visa.utils
|
||||
|
||||
import com.tangem.datasource.api.pay.models.response.VisaTxHistoryResponse
|
||||
import com.tangem.domain.visa.model.VisaTxHistoryItem
|
||||
|
||||
internal class VisaTxHistoryItemFactory {
|
||||
|
||||
fun create(transaction: VisaTxHistoryResponse.Transaction): VisaTxHistoryItem {
|
||||
return VisaTxHistoryItem(
|
||||
id = transaction.transactionId.toString(),
|
||||
date = transaction.transactionDt,
|
||||
amount = transaction.blockchainAmount,
|
||||
fiatAmount = transaction.transactionAmount,
|
||||
merchantName = transaction.merchantName,
|
||||
status = transaction.transactionStatus,
|
||||
fiatCurrency = findCurrencyByNumericCode(transaction.transactionCurrencyCode),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -20,8 +20,6 @@ internal class VisaTxHistoryPagingSource(
|
|||
val requestTxHistory: suspend (offset: Int, pageSize: Int) -> VisaTxHistoryResponse,
|
||||
) : PagingSource<Int, VisaTxHistoryItem>() {
|
||||
|
||||
private val itemsFactory = VisaTxHistoryItemFactory()
|
||||
|
||||
private val cardPublicKey = params.cardPublicKey
|
||||
private val pageSize = params.pageSize
|
||||
private val isRefresh = params.isRefresh
|
||||
|
|
@ -82,7 +80,7 @@ internal class VisaTxHistoryPagingSource(
|
|||
|
||||
pagedItems.update {
|
||||
it.toMutableMap().apply {
|
||||
this[offset] = response.transactions.map(itemsFactory::create)
|
||||
this[offset] = response.transactions.map(VisaTxHistoryItemConverter::convert)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -250,7 +250,9 @@ internal class UpdateWalletManagerResultFactoryTest {
|
|||
),
|
||||
),
|
||||
currenciesAmounts = setOf(
|
||||
UpdateWalletManagerResult.CryptoCurrencyAmount.Coin(value = BigDecimal.ZERO), // default for demo
|
||||
UpdateWalletManagerResult.CryptoCurrencyAmount.Coin(
|
||||
value = BigDecimal.ZERO,
|
||||
), // default for demo
|
||||
),
|
||||
currentTransactions = emptySet(),
|
||||
),
|
||||
|
|
@ -272,7 +274,9 @@ internal class UpdateWalletManagerResultFactoryTest {
|
|||
),
|
||||
),
|
||||
currenciesAmounts = setOf(
|
||||
UpdateWalletManagerResult.CryptoCurrencyAmount.Coin(value = BigDecimal.ONE), // used demo amount
|
||||
UpdateWalletManagerResult.CryptoCurrencyAmount.Coin(
|
||||
value = BigDecimal.ONE,
|
||||
), // used demo amount
|
||||
),
|
||||
currentTransactions = emptySet(),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -415,6 +415,6 @@ internal class DefaultWalletsRepository(
|
|||
else -> ActivatePromoCodeError.ActivationFailed
|
||||
}
|
||||
return@fold error.left()
|
||||
},)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -114,62 +114,64 @@ class DefaultWalletsRepositoryTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN API returns wallets WHEN getWalletsInfo THEN should return converted wallets and update cache if requested`() = runTest {
|
||||
// GIVEN
|
||||
val applicationId = "test_app_id"
|
||||
val wallet1Id = "1234567890abcdef"
|
||||
val wallet2Id = "fedcba0987654321"
|
||||
val walletResponses = listOf(
|
||||
WalletResponse(
|
||||
id = wallet1Id,
|
||||
notifyStatus = true,
|
||||
),
|
||||
WalletResponse(
|
||||
id = wallet2Id,
|
||||
notifyStatus = false,
|
||||
),
|
||||
)
|
||||
coEvery { tangemTechApi.getWallets(applicationId) } returns ApiResponse.Success(walletResponses)
|
||||
coEvery { preferencesDataStore.updateData(any()) } returns mockk()
|
||||
fun `GIVEN API returns wallets WHEN getWalletsInfo THEN should return converted wallets and update cache if requested`() =
|
||||
runTest {
|
||||
// GIVEN
|
||||
val applicationId = "test_app_id"
|
||||
val wallet1Id = "1234567890abcdef"
|
||||
val wallet2Id = "fedcba0987654321"
|
||||
val walletResponses = listOf(
|
||||
WalletResponse(
|
||||
id = wallet1Id,
|
||||
notifyStatus = true,
|
||||
),
|
||||
WalletResponse(
|
||||
id = wallet2Id,
|
||||
notifyStatus = false,
|
||||
),
|
||||
)
|
||||
coEvery { tangemTechApi.getWallets(applicationId) } returns ApiResponse.Success(walletResponses)
|
||||
coEvery { preferencesDataStore.updateData(any()) } returns mockk()
|
||||
|
||||
// WHEN
|
||||
val result = repository.getWalletsInfo(applicationId, updateCache = true)
|
||||
// WHEN
|
||||
val result = repository.getWalletsInfo(applicationId, updateCache = true)
|
||||
|
||||
// THEN
|
||||
assertThat(result).hasSize(2)
|
||||
assertThat(result[0].walletId.stringValue).isEqualTo(wallet1Id)
|
||||
assertThat(result[0].isNotificationsEnabled).isTrue()
|
||||
assertThat(result[1].walletId.stringValue).isEqualTo(wallet2Id)
|
||||
assertThat(result[1].isNotificationsEnabled).isFalse()
|
||||
// THEN
|
||||
assertThat(result).hasSize(2)
|
||||
assertThat(result[0].walletId.stringValue).isEqualTo(wallet1Id)
|
||||
assertThat(result[0].isNotificationsEnabled).isTrue()
|
||||
assertThat(result[1].walletId.stringValue).isEqualTo(wallet2Id)
|
||||
assertThat(result[1].isNotificationsEnabled).isFalse()
|
||||
|
||||
coVerify(exactly = 1) { tangemTechApi.getWallets(applicationId) }
|
||||
coVerify(exactly = 2) { preferencesDataStore.updateData(any()) }
|
||||
}
|
||||
coVerify(exactly = 1) { tangemTechApi.getWallets(applicationId) }
|
||||
coVerify(exactly = 2) { preferencesDataStore.updateData(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN API returns wallets WHEN getWalletsInfo with updateCache false THEN should return converted wallets without updating cache`() = runTest {
|
||||
// GIVEN
|
||||
val applicationId = "test_app_id"
|
||||
val wallet1Id = "1234567890abcdef"
|
||||
val walletResponses = listOf(
|
||||
WalletResponse(
|
||||
id = wallet1Id,
|
||||
notifyStatus = true,
|
||||
),
|
||||
)
|
||||
coEvery { tangemTechApi.getWallets(applicationId) } returns ApiResponse.Success(walletResponses)
|
||||
fun `GIVEN API returns wallets WHEN getWalletsInfo with updateCache false THEN should return converted wallets without updating cache`() =
|
||||
runTest {
|
||||
// GIVEN
|
||||
val applicationId = "test_app_id"
|
||||
val wallet1Id = "1234567890abcdef"
|
||||
val walletResponses = listOf(
|
||||
WalletResponse(
|
||||
id = wallet1Id,
|
||||
notifyStatus = true,
|
||||
),
|
||||
)
|
||||
coEvery { tangemTechApi.getWallets(applicationId) } returns ApiResponse.Success(walletResponses)
|
||||
|
||||
// WHEN
|
||||
val result = repository.getWalletsInfo(applicationId, updateCache = false)
|
||||
// WHEN
|
||||
val result = repository.getWalletsInfo(applicationId, updateCache = false)
|
||||
|
||||
// THEN
|
||||
assertThat(result).hasSize(1)
|
||||
assertThat(result[0].walletId.stringValue).isEqualTo(wallet1Id)
|
||||
assertThat(result[0].isNotificationsEnabled).isTrue()
|
||||
// THEN
|
||||
assertThat(result).hasSize(1)
|
||||
assertThat(result[0].walletId.stringValue).isEqualTo(wallet1Id)
|
||||
assertThat(result[0].isNotificationsEnabled).isTrue()
|
||||
|
||||
coVerify(exactly = 1) { tangemTechApi.getWallets(applicationId) }
|
||||
coVerify(exactly = 0) { preferencesDataStore.updateData(any()) }
|
||||
}
|
||||
coVerify(exactly = 1) { tangemTechApi.getWallets(applicationId) }
|
||||
coVerify(exactly = 0) { preferencesDataStore.updateData(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN user wallets and application ID WHEN associateWallets THEN should convert and send to API`() = runTest {
|
||||
|
|
@ -271,8 +273,8 @@ class DefaultWalletsRepositoryTest {
|
|||
// GIVEN
|
||||
coEvery { tangemTechApi.activatePromoCode(any()) } returns
|
||||
ApiResponse.Error(
|
||||
HttpException(code = HttpException.Code.NOT_FOUND, message = null, errorBody = null),
|
||||
) as ApiResponse<PromocodeActivationResponse>
|
||||
HttpException(code = HttpException.Code.NOT_FOUND, message = null, errorBody = null),
|
||||
) as ApiResponse<PromocodeActivationResponse>
|
||||
|
||||
// WHEN
|
||||
val result = repository.activatePromoCode(promoCode = "PROMO", bitcoinAddress = "addr")
|
||||
|
|
@ -288,8 +290,8 @@ class DefaultWalletsRepositoryTest {
|
|||
// GIVEN
|
||||
coEvery { tangemTechApi.activatePromoCode(any()) } returns
|
||||
ApiResponse.Error(
|
||||
HttpException(code = HttpException.Code.CONFLICT, message = null, errorBody = null),
|
||||
) as ApiResponse<PromocodeActivationResponse>
|
||||
HttpException(code = HttpException.Code.CONFLICT, message = null, errorBody = null),
|
||||
) as ApiResponse<PromocodeActivationResponse>
|
||||
|
||||
// WHEN
|
||||
val result = repository.activatePromoCode(promoCode = "PROMO", bitcoinAddress = "addr")
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import com.tangem.blockchainsdk.utils.toBlockchain
|
|||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.models.yield.supply.YieldSupplyStatus
|
||||
import com.tangem.domain.utils.convertToSdkAmount
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.domain.yield.supply.YieldSupplyTransactionRepository
|
||||
|
|
@ -30,6 +29,7 @@ internal class DefaultYieldSupplyTransactionRepository(
|
|||
override suspend fun createEnterTransactions(
|
||||
userWalletId: UserWalletId,
|
||||
cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
maxNetworkFee: BigDecimal,
|
||||
): List<TransactionData.Uncompiled> {
|
||||
val cryptoCurrency = cryptoCurrencyStatus.currency
|
||||
|
||||
|
|
@ -51,27 +51,23 @@ internal class DefaultYieldSupplyTransactionRepository(
|
|||
cryptoCurrency = cryptoCurrency,
|
||||
) ?: error("Calculated yield contract address is null")
|
||||
|
||||
val yieldTokenStatus = cryptoCurrencyStatus.value.yieldSupplyStatus ?: getYieldTokenStatus(
|
||||
walletManager = walletManager,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
)
|
||||
val maxNetworkFee = maxNetworkFee.convertToSdkAmount(cryptoCurrencyStatus)
|
||||
|
||||
return buildEnterTransactions(
|
||||
walletManager = walletManager,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
existingYieldContractAddress = existingYieldContractAddress,
|
||||
calculatedYieldContractAddress = calculatedYieldContractAddress,
|
||||
yieldTokenStatus = yieldTokenStatus,
|
||||
maxNetworkFee = maxNetworkFee,
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun createExitTransaction(
|
||||
userWalletId: UserWalletId,
|
||||
cryptoCurrency: CryptoCurrency,
|
||||
yieldSupplyStatus: YieldSupplyStatus,
|
||||
cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
fee: Fee?,
|
||||
): TransactionData.Uncompiled = withContext(dispatchers.io) {
|
||||
require(cryptoCurrency is CryptoCurrency.Token)
|
||||
val cryptoCurrency = cryptoCurrencyStatus.currency as CryptoCurrency.Token
|
||||
|
||||
val walletManager = walletManagersFacade.getOrCreateWalletManager(
|
||||
userWalletId = userWalletId,
|
||||
|
|
@ -88,19 +84,24 @@ internal class DefaultYieldSupplyTransactionRepository(
|
|||
cryptoCurrency = cryptoCurrency,
|
||||
callData = callData,
|
||||
destinationAddress = walletManager.getYieldContract(),
|
||||
yieldSupplyStatus = yieldSupplyStatus,
|
||||
amount = BigDecimal.ZERO.convertToSdkAmount(cryptoCurrencyStatus),
|
||||
fee = fee,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
private fun buildEnterTransactions(
|
||||
walletManager: WalletManager,
|
||||
cryptoCurrency: CryptoCurrency.Token,
|
||||
cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
existingYieldContractAddress: String?,
|
||||
calculatedYieldContractAddress: String,
|
||||
yieldTokenStatus: YieldSupplyStatus?,
|
||||
maxNetworkFee: Amount,
|
||||
): MutableList<TransactionData.Uncompiled> {
|
||||
val enterTransactions = mutableListOf<TransactionData.Uncompiled>()
|
||||
val cryptoCurrency = cryptoCurrencyStatus.currency as CryptoCurrency.Token
|
||||
val yieldSupplyStatus = cryptoCurrencyStatus.value.yieldSupplyStatus
|
||||
|
||||
val amount = BigDecimal.ZERO.convertToSdkAmount(cryptoCurrencyStatus)
|
||||
|
||||
when {
|
||||
existingYieldContractAddress == null || existingYieldContractAddress == EthereumUtils.ZERO_ADDRESS -> {
|
||||
|
|
@ -108,30 +109,34 @@ internal class DefaultYieldSupplyTransactionRepository(
|
|||
createDeployTransaction(
|
||||
walletManager = walletManager,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
amount = amount,
|
||||
maxNetworkFee = maxNetworkFee,
|
||||
),
|
||||
)
|
||||
}
|
||||
yieldTokenStatus == null -> error("Yield token status is null")
|
||||
!yieldTokenStatus.isInitialized -> enterTransactions.add(
|
||||
yieldSupplyStatus == null -> error("Yield token status is null")
|
||||
!yieldSupplyStatus.isInitialized -> enterTransactions.add(
|
||||
createInitTokenTransaction(
|
||||
walletManager = walletManager,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
yieldSupplyStatus = yieldTokenStatus,
|
||||
yieldContractAddress = calculatedYieldContractAddress,
|
||||
amount = amount,
|
||||
maxNetworkFee = maxNetworkFee,
|
||||
),
|
||||
)
|
||||
!yieldTokenStatus.isActive -> enterTransactions.add(
|
||||
!yieldSupplyStatus.isActive -> enterTransactions.add(
|
||||
createReactivateTokenTransaction(
|
||||
walletManager = walletManager,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
yieldSupplyStatus = yieldTokenStatus,
|
||||
yieldContractAddress = calculatedYieldContractAddress,
|
||||
amount = amount,
|
||||
maxNetworkFee = maxNetworkFee,
|
||||
),
|
||||
)
|
||||
else -> Unit
|
||||
}
|
||||
|
||||
if (yieldTokenStatus?.isAllowedToSpend == false) {
|
||||
if (yieldSupplyStatus?.isAllowedToSpend != true) {
|
||||
enterTransactions.add(
|
||||
createTransaction(
|
||||
walletManager = walletManager,
|
||||
|
|
@ -141,7 +146,7 @@ internal class DefaultYieldSupplyTransactionRepository(
|
|||
amount = null,
|
||||
),
|
||||
destinationAddress = cryptoCurrency.contractAddress,
|
||||
yieldSupplyStatus = yieldTokenStatus,
|
||||
amount = amount,
|
||||
fee = null,
|
||||
),
|
||||
)
|
||||
|
|
@ -151,7 +156,7 @@ internal class DefaultYieldSupplyTransactionRepository(
|
|||
createEnterTransaction(
|
||||
walletManager = walletManager,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
yieldSupplyStatus = yieldTokenStatus,
|
||||
amount = amount,
|
||||
yieldContractAddress = calculatedYieldContractAddress,
|
||||
),
|
||||
)
|
||||
|
|
@ -171,11 +176,10 @@ internal class DefaultYieldSupplyTransactionRepository(
|
|||
derivationPath = cryptoCurrency.network.derivationPath.value,
|
||||
) ?: error("Wallet manager not found")
|
||||
walletManager.calculateYieldContract()
|
||||
}.onFailure(Timber::e)
|
||||
.getOrNull()
|
||||
}.onFailure(Timber::e).getOrNull()
|
||||
}
|
||||
|
||||
private suspend fun getYieldContractAddress(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): String? =
|
||||
override suspend fun getYieldContractAddress(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): String? =
|
||||
withContext(dispatchers.io) {
|
||||
require(cryptoCurrency is CryptoCurrency.Token)
|
||||
runCatching {
|
||||
|
|
@ -185,35 +189,19 @@ internal class DefaultYieldSupplyTransactionRepository(
|
|||
derivationPath = cryptoCurrency.network.derivationPath.value,
|
||||
) ?: error("Wallet manager not found")
|
||||
walletManager.getYieldContract()
|
||||
}.onFailure(Timber::e)
|
||||
.getOrNull()
|
||||
}.onFailure(Timber::e).getOrNull()
|
||||
}
|
||||
|
||||
private suspend fun getYieldTokenStatus(
|
||||
walletManager: WalletManager,
|
||||
cryptoCurrency: CryptoCurrency,
|
||||
): YieldSupplyStatus? = withContext(dispatchers.io) {
|
||||
require(cryptoCurrency is CryptoCurrency.Token)
|
||||
runCatching {
|
||||
val sdkSupplyStatus = walletManager.getYieldSupplyStatus(cryptoCurrency.contractAddress)
|
||||
val isAllowedToSpend = walletManager.isAllowedToSpend(cryptoCurrency.contractAddress)
|
||||
|
||||
YieldSupplyStatus(
|
||||
isActive = sdkSupplyStatus?.isActive == true,
|
||||
isInitialized = sdkSupplyStatus?.isInitialized == true,
|
||||
isAllowedToSpend = isAllowedToSpend,
|
||||
)
|
||||
}.onFailure(Timber::e).getOrNull()
|
||||
}
|
||||
|
||||
private fun createDeployTransaction(
|
||||
walletManager: WalletManager,
|
||||
cryptoCurrency: CryptoCurrency.Token,
|
||||
amount: Amount,
|
||||
maxNetworkFee: Amount,
|
||||
): TransactionData.Uncompiled {
|
||||
val callData = YieldSupplyContractCallDataProviderFactory.getDeployCallData(
|
||||
tokenContractAddress = cryptoCurrency.contractAddress,
|
||||
walletAddress = walletManager.wallet.address,
|
||||
maxNetworkFee = MAX_NETWORK_FEE.convertToSdkAmount(cryptoCurrency),
|
||||
maxNetworkFee = maxNetworkFee,
|
||||
)
|
||||
|
||||
val factoryContractAddress = walletManager.getYieldSupplyContractAddresses()?.factoryContractAddress
|
||||
|
|
@ -224,7 +212,7 @@ internal class DefaultYieldSupplyTransactionRepository(
|
|||
cryptoCurrency = cryptoCurrency,
|
||||
callData = callData,
|
||||
destinationAddress = factoryContractAddress,
|
||||
yieldSupplyStatus = null,
|
||||
amount = amount,
|
||||
fee = null,
|
||||
)
|
||||
}
|
||||
|
|
@ -233,11 +221,12 @@ internal class DefaultYieldSupplyTransactionRepository(
|
|||
walletManager: WalletManager,
|
||||
cryptoCurrency: CryptoCurrency.Token,
|
||||
yieldContractAddress: String,
|
||||
yieldSupplyStatus: YieldSupplyStatus,
|
||||
amount: Amount,
|
||||
maxNetworkFee: Amount,
|
||||
): TransactionData.Uncompiled {
|
||||
val callData = YieldSupplyContractCallDataProviderFactory.getInitTokenCallData(
|
||||
tokenContractAddress = cryptoCurrency.contractAddress,
|
||||
maxNetworkFee = MAX_NETWORK_FEE.convertToSdkAmount(cryptoCurrency),
|
||||
maxNetworkFee = maxNetworkFee,
|
||||
)
|
||||
|
||||
return createTransaction(
|
||||
|
|
@ -245,7 +234,7 @@ internal class DefaultYieldSupplyTransactionRepository(
|
|||
cryptoCurrency = cryptoCurrency,
|
||||
callData = callData,
|
||||
destinationAddress = yieldContractAddress,
|
||||
yieldSupplyStatus = yieldSupplyStatus,
|
||||
amount = amount,
|
||||
fee = null,
|
||||
)
|
||||
}
|
||||
|
|
@ -254,11 +243,12 @@ internal class DefaultYieldSupplyTransactionRepository(
|
|||
walletManager: WalletManager,
|
||||
cryptoCurrency: CryptoCurrency.Token,
|
||||
yieldContractAddress: String,
|
||||
yieldSupplyStatus: YieldSupplyStatus,
|
||||
amount: Amount,
|
||||
maxNetworkFee: Amount,
|
||||
): TransactionData.Uncompiled {
|
||||
val callData = YieldSupplyContractCallDataProviderFactory.getReactivateTokenCallData(
|
||||
tokenContractAddress = cryptoCurrency.contractAddress,
|
||||
maxNetworkFee = MAX_NETWORK_FEE.convertToSdkAmount(cryptoCurrency),
|
||||
maxNetworkFee = maxNetworkFee,
|
||||
)
|
||||
|
||||
return createTransaction(
|
||||
|
|
@ -266,7 +256,7 @@ internal class DefaultYieldSupplyTransactionRepository(
|
|||
cryptoCurrency = cryptoCurrency,
|
||||
callData = callData,
|
||||
destinationAddress = yieldContractAddress,
|
||||
yieldSupplyStatus = yieldSupplyStatus,
|
||||
amount = amount,
|
||||
fee = null,
|
||||
)
|
||||
}
|
||||
|
|
@ -274,7 +264,7 @@ internal class DefaultYieldSupplyTransactionRepository(
|
|||
private fun createEnterTransaction(
|
||||
walletManager: WalletManager,
|
||||
cryptoCurrency: CryptoCurrency.Token,
|
||||
yieldSupplyStatus: YieldSupplyStatus?,
|
||||
amount: Amount,
|
||||
yieldContractAddress: String,
|
||||
): TransactionData.Uncompiled {
|
||||
val callData = YieldSupplyContractCallDataProviderFactory.getEnterCallData(
|
||||
|
|
@ -286,7 +276,7 @@ internal class DefaultYieldSupplyTransactionRepository(
|
|||
cryptoCurrency = cryptoCurrency,
|
||||
callData = callData,
|
||||
destinationAddress = yieldContractAddress,
|
||||
yieldSupplyStatus = yieldSupplyStatus,
|
||||
amount = amount,
|
||||
fee = null,
|
||||
)
|
||||
}
|
||||
|
|
@ -297,7 +287,7 @@ internal class DefaultYieldSupplyTransactionRepository(
|
|||
cryptoCurrency: CryptoCurrency,
|
||||
callData: SmartContractCallData,
|
||||
destinationAddress: String,
|
||||
yieldSupplyStatus: YieldSupplyStatus?,
|
||||
amount: Amount,
|
||||
fee: Fee?,
|
||||
): TransactionData.Uncompiled {
|
||||
requireNotNull(cryptoCurrency as? CryptoCurrency.Token)
|
||||
|
|
@ -308,8 +298,6 @@ internal class DefaultYieldSupplyTransactionRepository(
|
|||
blockchain = blockchain,
|
||||
)
|
||||
|
||||
val amount = getYieldSupplyAmount(cryptoCurrency, yieldSupplyStatus)
|
||||
|
||||
return if (fee != null) {
|
||||
walletManager.createTransaction(
|
||||
amount = amount,
|
||||
|
|
@ -349,23 +337,4 @@ internal class DefaultYieldSupplyTransactionRepository(
|
|||
else -> error("Data extras not supported for $blockchain")
|
||||
}
|
||||
}
|
||||
|
||||
private fun getYieldSupplyAmount(cryptoCurrency: CryptoCurrency.Token, yieldSupplyStatus: YieldSupplyStatus?) =
|
||||
BigDecimal.ZERO.convertToSdkAmount(
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
amountType = AmountType.TokenYieldSupply(
|
||||
token = Token(
|
||||
symbol = cryptoCurrency.symbol,
|
||||
contractAddress = cryptoCurrency.contractAddress,
|
||||
decimals = cryptoCurrency.decimals,
|
||||
),
|
||||
isActive = yieldSupplyStatus?.isActive ?: false,
|
||||
isInitialized = yieldSupplyStatus?.isInitialized ?: false,
|
||||
isAllowedToSpend = yieldSupplyStatus?.isAllowedToSpend ?: false,
|
||||
),
|
||||
)
|
||||
|
||||
private companion object {
|
||||
val MAX_NETWORK_FEE: BigDecimal = BigDecimal.TEN // TODO for TESTNET only
|
||||
}
|
||||
}
|
||||
|
|
@ -16,7 +16,6 @@ import com.tangem.domain.models.yield.supply.YieldSupplyStatus
|
|||
import com.tangem.domain.utils.convertToSdkAmount
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import com.tangem.blockchain.yieldsupply.providers.YieldSupplyStatus as SDKYieldSupplyStatus
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
|
|
@ -26,6 +25,7 @@ import org.junit.jupiter.api.BeforeEach
|
|||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import java.math.BigDecimal
|
||||
import com.tangem.blockchain.yieldsupply.providers.YieldSupplyStatus as SDKYieldSupplyStatus
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
class DefaultYieldSupplyTransactionRepositoryTest {
|
||||
|
|
@ -74,7 +74,11 @@ class DefaultYieldSupplyTransactionRepositoryTest {
|
|||
coEvery { walletManager.isAllowedToSpend(any()) } returns false
|
||||
coEvery { walletManager.calculateYieldContract() } returns yieldContractAddress
|
||||
|
||||
val result = repository.createEnterTransactions(userWalletId, cryptoCurrencyStatus)
|
||||
val result = repository.createEnterTransactions(
|
||||
userWalletId = userWalletId,
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
maxNetworkFee = BigDecimal.TEN,
|
||||
)
|
||||
|
||||
// Assert that 3 transactions are returned: deploy, approve, enter
|
||||
Truth.assertThat(result).isNotNull()
|
||||
|
|
@ -84,7 +88,7 @@ class DefaultYieldSupplyTransactionRepositoryTest {
|
|||
val firstExpectedCallData = YieldSupplyContractCallDataProviderFactory.getDeployCallData(
|
||||
walletAddress = walletManager.wallet.address,
|
||||
tokenContractAddress = mockedContractAddress,
|
||||
maxNetworkFee = BigDecimal.TEN.convertToSdkAmount(cryptoCurrency),
|
||||
maxNetworkFee = BigDecimal.TEN.convertToSdkAmount(cryptoCurrencyStatus),
|
||||
)
|
||||
val firstTransaction = result.first()
|
||||
|
||||
|
|
@ -117,13 +121,18 @@ class DefaultYieldSupplyTransactionRepositoryTest {
|
|||
fun `createEnterTransactions returns init-approve-enter transactions`() = runTest {
|
||||
coEvery { walletManager.getYieldContract() } returns yieldContractAddress
|
||||
coEvery { walletManager.calculateYieldContract() } returns yieldContractAddress
|
||||
coEvery { walletManager.isAllowedToSpend(any()) } returns false
|
||||
coEvery { walletManager.getYieldSupplyStatus(any()) } returns SDKYieldSupplyStatus(
|
||||
isActive = false,
|
||||
isInitialized = false,
|
||||
maxNetworkFee = BigDecimal.TEN,
|
||||
)
|
||||
|
||||
val result = repository.createEnterTransactions(userWalletId, cryptoCurrencyStatus)
|
||||
val result = repository.createEnterTransactions(
|
||||
userWalletId = userWalletId,
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
maxNetworkFee = BigDecimal.TEN,
|
||||
)
|
||||
|
||||
// Assert that 3 transactions are returned: init token, approve, enter
|
||||
Truth.assertThat(result).isNotNull()
|
||||
|
|
@ -132,7 +141,7 @@ class DefaultYieldSupplyTransactionRepositoryTest {
|
|||
// Check transaction - init token
|
||||
val firstExpectedCallData = YieldSupplyContractCallDataProviderFactory.getInitTokenCallData(
|
||||
tokenContractAddress = mockedContractAddress,
|
||||
maxNetworkFee = BigDecimal.TEN.convertToSdkAmount(cryptoCurrency),
|
||||
maxNetworkFee = BigDecimal.TEN.convertToSdkAmount(cryptoCurrencyStatus),
|
||||
)
|
||||
val firstTransaction = result.first()
|
||||
|
||||
|
|
@ -165,13 +174,18 @@ class DefaultYieldSupplyTransactionRepositoryTest {
|
|||
fun `createEnterTransactions returns reactivate-approve-enter transactions`() = runTest {
|
||||
coEvery { walletManager.getYieldContract() } returns yieldContractAddress
|
||||
coEvery { walletManager.calculateYieldContract() } returns yieldContractAddress
|
||||
coEvery { walletManager.isAllowedToSpend(any()) } returns false
|
||||
coEvery { walletManager.getYieldSupplyStatus(any()) } returns SDKYieldSupplyStatus(
|
||||
isActive = false,
|
||||
isInitialized = true,
|
||||
maxNetworkFee = BigDecimal.TEN,
|
||||
)
|
||||
|
||||
val result = repository.createEnterTransactions(userWalletId, cryptoCurrencyStatus)
|
||||
val result = repository.createEnterTransactions(
|
||||
userWalletId = userWalletId,
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
maxNetworkFee = BigDecimal.TEN,
|
||||
)
|
||||
|
||||
// Assert that 3 transactions are returned: reactivate token, approve, enter
|
||||
Truth.assertThat(result).isNotNull()
|
||||
|
|
@ -180,7 +194,7 @@ class DefaultYieldSupplyTransactionRepositoryTest {
|
|||
// Check transaction - reactivate token
|
||||
val firstExpectedCallData = YieldSupplyContractCallDataProviderFactory.getReactivateTokenCallData(
|
||||
tokenContractAddress = mockedContractAddress,
|
||||
maxNetworkFee = BigDecimal.TEN.convertToSdkAmount(cryptoCurrency),
|
||||
maxNetworkFee = BigDecimal.TEN.convertToSdkAmount(cryptoCurrencyStatus),
|
||||
)
|
||||
val firstTransaction = result.first()
|
||||
|
||||
|
|
@ -220,7 +234,11 @@ class DefaultYieldSupplyTransactionRepositoryTest {
|
|||
)
|
||||
coEvery { walletManager.isAllowedToSpend(any()) } returns true
|
||||
|
||||
val result = repository.createEnterTransactions(userWalletId, cryptoCurrencyStatus)
|
||||
val result = repository.createEnterTransactions(
|
||||
userWalletId = userWalletId,
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
maxNetworkFee = BigDecimal.TEN,
|
||||
)
|
||||
|
||||
// Assert that 2 transactions are returned: approve, enter
|
||||
Truth.assertThat(result).isNotNull()
|
||||
|
|
@ -229,7 +247,7 @@ class DefaultYieldSupplyTransactionRepositoryTest {
|
|||
// Check transaction - reactivate token
|
||||
val firstExpectedCallData = YieldSupplyContractCallDataProviderFactory.getReactivateTokenCallData(
|
||||
tokenContractAddress = mockedContractAddress,
|
||||
maxNetworkFee = BigDecimal.TEN.convertToSdkAmount(cryptoCurrency),
|
||||
maxNetworkFee = BigDecimal.TEN.convertToSdkAmount(cryptoCurrencyStatus),
|
||||
)
|
||||
val firstTransaction = result.first()
|
||||
|
||||
|
|
@ -257,7 +275,11 @@ class DefaultYieldSupplyTransactionRepositoryTest {
|
|||
)
|
||||
coEvery { walletManager.isAllowedToSpend(any()) } returns true
|
||||
|
||||
val result = repository.createEnterTransactions(userWalletId, cryptoCurrencyStatus)
|
||||
val result = repository.createEnterTransactions(
|
||||
userWalletId = userWalletId,
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
maxNetworkFee = BigDecimal.TEN,
|
||||
)
|
||||
|
||||
// Assert that transaction is returned: enter
|
||||
Truth.assertThat(result).isNotNull()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue