Updated on 2026-08-14
This commit is contained in:
commit
cef36091da
379 changed files with 8948 additions and 8976 deletions
|
|
@ -0,0 +1,14 @@
|
|||
package com.tangem.data.tokens.converters
|
||||
|
||||
import com.tangem.domain.tokens.model.blockchains.UtxoAmountLimit
|
||||
import com.tangem.utils.converter.Converter
|
||||
import com.tangem.blockchain.common.UtxoAmountLimit as BlockchainUtxoAmountLimit
|
||||
|
||||
internal class UtxoConverter : Converter<BlockchainUtxoAmountLimit, UtxoAmountLimit> {
|
||||
override fun convert(value: BlockchainUtxoAmountLimit): UtxoAmountLimit {
|
||||
return UtxoAmountLimit(
|
||||
maxLimit = value.maxLimit,
|
||||
maxAmount = value.maxAmount,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -113,4 +113,10 @@ internal object TokensDataModule {
|
|||
): NetworksCompatibilityRepository {
|
||||
return DefaultNetworksCompatibilityRepository(userWalletsStore = userWalletsStore, dispatchers = dispatchers)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideCurrencyChecksRepository(walletManagersFacade: WalletManagersFacade): CurrencyChecksRepository {
|
||||
return DefaultCurrencyChecksRepository(walletManagersFacade = walletManagersFacade)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
package com.tangem.data.tokens.repository
|
||||
|
||||
import com.tangem.blockchain.blockchains.polkadot.ExistentialDepositProvider
|
||||
import com.tangem.blockchain.common.ReserveAmountProvider
|
||||
import com.tangem.blockchain.common.UtxoAmountLimitProvider
|
||||
import com.tangem.data.tokens.converters.UtxoConverter
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.tokens.model.blockchains.UtxoAmountLimit
|
||||
import com.tangem.domain.tokens.repository.CurrencyChecksRepository
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal class DefaultCurrencyChecksRepository(
|
||||
private val walletManagersFacade: WalletManagersFacade,
|
||||
) : CurrencyChecksRepository {
|
||||
override suspend fun getExistentialDeposit(userWalletId: UserWalletId, network: Network): BigDecimal? {
|
||||
val manager = walletManagersFacade.getOrCreateWalletManager(
|
||||
userWalletId = userWalletId,
|
||||
network = network,
|
||||
)
|
||||
|
||||
return if (manager is ExistentialDepositProvider) manager.getExistentialDeposit() else null
|
||||
}
|
||||
|
||||
override suspend fun getDustValue(userWalletId: UserWalletId, network: Network): BigDecimal? {
|
||||
val manager = walletManagersFacade.getOrCreateWalletManager(
|
||||
userWalletId = userWalletId,
|
||||
network = network,
|
||||
)
|
||||
|
||||
return manager?.dustValue
|
||||
}
|
||||
|
||||
override suspend fun getReserveAmount(userWalletId: UserWalletId, network: Network): BigDecimal? {
|
||||
val manager = walletManagersFacade.getOrCreateWalletManager(
|
||||
userWalletId = userWalletId,
|
||||
network = network,
|
||||
)
|
||||
|
||||
return if (manager is ReserveAmountProvider) manager.getReserveAmount() else null
|
||||
}
|
||||
|
||||
override suspend fun checkIfAccountFunded(userWalletId: UserWalletId, network: Network, address: String): Boolean {
|
||||
val manager = walletManagersFacade.getOrCreateWalletManager(
|
||||
userWalletId = userWalletId,
|
||||
network = network,
|
||||
)
|
||||
|
||||
return if (manager is ReserveAmountProvider) manager.isAccountFunded(address) else true
|
||||
}
|
||||
|
||||
override suspend fun checkUtxoAmountLimit(
|
||||
userWalletId: UserWalletId,
|
||||
network: Network,
|
||||
amount: BigDecimal,
|
||||
fee: BigDecimal,
|
||||
): UtxoAmountLimit? {
|
||||
val manager = walletManagersFacade.getOrCreateWalletManager(
|
||||
userWalletId = userWalletId,
|
||||
network = network,
|
||||
)
|
||||
val utxoAmount = if (manager is UtxoAmountLimitProvider) {
|
||||
manager.checkUtxoAmountLimit(amount, fee)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
return utxoAmount?.let(UtxoConverter()::convert)
|
||||
}
|
||||
}
|
||||
|
|
@ -11,9 +11,35 @@ android {
|
|||
|
||||
dependencies {
|
||||
|
||||
/** Project - Data */
|
||||
implementation(projects.core.datasource)
|
||||
implementation(projects.data.common)
|
||||
|
||||
/** Project - Domain */
|
||||
implementation(projects.domain.visa)
|
||||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.wallets.models)
|
||||
implementation(projects.domain.appCurrency.models)
|
||||
|
||||
/** Project - Utils */
|
||||
implementation(projects.core.utils)
|
||||
implementation(projects.domain.legacy)
|
||||
|
||||
/** Project - Libs */
|
||||
implementation(projects.libs.visa)
|
||||
|
||||
/** Libs - Other */
|
||||
implementation(deps.kotlin.coroutines)
|
||||
implementation(deps.arrow.core)
|
||||
implementation(deps.arrow.fx)
|
||||
implementation(deps.jodatime)
|
||||
implementation(deps.timber)
|
||||
implementation(deps.androidx.paging.runtime)
|
||||
implementation(deps.moshi.kotlin)
|
||||
|
||||
/** Libs - Tangem */
|
||||
implementation(deps.tangem.blockchain)
|
||||
implementation(deps.tangem.card.core)
|
||||
|
||||
/** DI */
|
||||
implementation(deps.hilt.core)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,190 @@
|
|||
package com.tangem.data.visa
|
||||
|
||||
import androidx.paging.Pager
|
||||
import androidx.paging.PagingConfig
|
||||
import androidx.paging.PagingData
|
||||
import arrow.fx.coroutines.parZip
|
||||
import com.tangem.blockchain.common.address.Address
|
||||
import com.tangem.blockchain.common.address.AddressType
|
||||
import com.tangem.common.card.EllipticCurve
|
||||
import com.tangem.common.extensions.toHexString
|
||||
import com.tangem.data.common.cache.CacheRegistry
|
||||
import com.tangem.data.visa.utils.VisaConfig
|
||||
import com.tangem.data.visa.utils.VisaCurrencyFactory
|
||||
import com.tangem.data.visa.utils.VisaTxDetailsFactory
|
||||
import com.tangem.data.visa.utils.VisaTxHistoryPagingSource
|
||||
import com.tangem.datasource.api.common.response.getOrThrow
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.common.util.cardTypesResolver
|
||||
import com.tangem.domain.visa.model.VisaCurrency
|
||||
import com.tangem.domain.visa.model.VisaTxDetails
|
||||
import com.tangem.domain.visa.model.VisaTxHistoryItem
|
||||
import com.tangem.domain.visa.repository.VisaRepository
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.lib.visa.VisaContractInfoProvider
|
||||
import com.tangem.lib.visa.api.VisaApi
|
||||
import com.tangem.lib.visa.model.VisaTxHistoryResponse
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal class DefaultVisaRepository(
|
||||
private val visaContractInfoProvider: VisaContractInfoProvider,
|
||||
private val tangemTechApi: TangemTechApi,
|
||||
private val visaApi: VisaApi,
|
||||
private val cacheRegistry: CacheRegistry,
|
||||
private val userWalletsStore: UserWalletsStore,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : VisaRepository {
|
||||
|
||||
private val currencyFactory by lazy(mode = LazyThreadSafetyMode.NONE) {
|
||||
VisaCurrencyFactory()
|
||||
}
|
||||
private val txDetailsFactory by lazy(mode = LazyThreadSafetyMode.NONE) {
|
||||
VisaTxDetailsFactory()
|
||||
}
|
||||
|
||||
private val fetchedCurrencies = MutableStateFlow(
|
||||
value = hashMapOf<String, VisaCurrency>(),
|
||||
)
|
||||
private val fetchedHistoryItems = MutableStateFlow(
|
||||
value = emptyMap<String, List<VisaTxHistoryResponse.Transaction>>(),
|
||||
)
|
||||
|
||||
override suspend fun getVisaCurrency(userWalletId: UserWalletId, isRefresh: Boolean): VisaCurrency {
|
||||
val address = makeAddress(userWalletId)
|
||||
// val address = "0x143fe062a538176aa0bf162f13d390208f90898f" // for testing
|
||||
|
||||
fetchVisaCurrencyIfExpired(address, isRefresh)
|
||||
|
||||
return requireNotNull(fetchedCurrencies.value[address]) {
|
||||
"Unable to find VISA currency for $address"
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchVisaCurrencyIfExpired(address: String, isRefresh: Boolean) {
|
||||
cacheRegistry.invokeOnExpire(
|
||||
key = getBalancesAndLimitsKey(address),
|
||||
skipCache = isRefresh,
|
||||
block = { fetchVisaCurrency(address) },
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun fetchVisaCurrency(address: String) {
|
||||
parZip(
|
||||
dispatchers.io,
|
||||
{ visaContractInfoProvider.getBalancesAndLimits(address) },
|
||||
{ getFiatRate() },
|
||||
{ balancesAndLimits, fiatRate ->
|
||||
fetchedCurrencies.update { value ->
|
||||
value.apply {
|
||||
put(address, currencyFactory.create(balancesAndLimits, fiatRate))
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getTxHistory(
|
||||
userWalletId: UserWalletId,
|
||||
pageSize: Int,
|
||||
isRefresh: Boolean,
|
||||
): Flow<PagingData<VisaTxHistoryItem>> {
|
||||
val userWallet = findVisaUserWallet(userWalletId)
|
||||
val cardPubKey = getCardPubKey(userWallet).toHexString()
|
||||
// val cardPubKey = "03DEF02B1FECC8BD3CFD52CE93235194479E1DE931EF0F55DC194967E7CCC3D12C" // for testing
|
||||
val pager = Pager(
|
||||
config = PagingConfig(
|
||||
pageSize = pageSize,
|
||||
initialLoadSize = pageSize,
|
||||
),
|
||||
pagingSourceFactory = {
|
||||
VisaTxHistoryPagingSource(
|
||||
params = VisaTxHistoryPagingSource.Params(
|
||||
cardPublicKey = cardPubKey,
|
||||
pageSize = pageSize,
|
||||
isRefresh = isRefresh,
|
||||
),
|
||||
cacheRegistry = cacheRegistry,
|
||||
visaApi = visaApi,
|
||||
fetchedItems = fetchedHistoryItems,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
return pager.flow
|
||||
}
|
||||
|
||||
override suspend fun getTxDetails(userWalletId: UserWalletId, txId: String): VisaTxDetails {
|
||||
return withContext(dispatchers.io) {
|
||||
val userWallet = findVisaUserWallet(userWalletId)
|
||||
val cardPubKey = getCardPubKey(userWallet).toHexString()
|
||||
// val cardPubKey = "03DEF02B1FECC8BD3CFD52CE93235194479E1DE931EF0F55DC194967E7CCC3D12C" // for testing
|
||||
val transaction = fetchedHistoryItems.value[cardPubKey]?.firstOrNull {
|
||||
it.transactionId.toString() == txId
|
||||
}
|
||||
requireNotNull(transaction) { "Transaction not found: $txId" }
|
||||
|
||||
txDetailsFactory.create(
|
||||
transaction = transaction,
|
||||
walletBlockchain = userWallet.scanResponse.cardTypesResolver.getBlockchain(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun makeAddress(userWalletId: UserWalletId): String {
|
||||
val userWallet = findVisaUserWallet(userWalletId)
|
||||
val walletAddresses = makeWalletAddresses(userWallet)
|
||||
val walletAddress = walletAddresses.firstOrNull { it.type == AddressType.Default }
|
||||
|
||||
return requireNotNull(walletAddress?.value) {
|
||||
"Unable to find wallet address"
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getFiatRate(): BigDecimal? {
|
||||
val fiatCurrencyId = VisaConfig.fiatCurrency.code.lowercase()
|
||||
val quotes = tangemTechApi.getQuotes(
|
||||
currencyId = fiatCurrencyId,
|
||||
coinIds = VisaConfig.TOKEN_ID,
|
||||
).getOrThrow()
|
||||
|
||||
return quotes.quotes[VisaConfig.TOKEN_ID]?.price
|
||||
}
|
||||
|
||||
private fun makeWalletAddresses(userWallet: UserWallet): Set<Address> {
|
||||
val walletBlockchain = userWallet.scanResponse.cardTypesResolver.getBlockchain()
|
||||
|
||||
return walletBlockchain.makeAddresses(getCardPubKey(userWallet))
|
||||
}
|
||||
|
||||
private fun getCardPubKey(userWallet: UserWallet): ByteArray {
|
||||
val cardWallet = userWallet.scanResponse.card.wallets.firstOrNull {
|
||||
it.curve == EllipticCurve.Secp256k1
|
||||
}
|
||||
requireNotNull(cardWallet) { "Secp256k1 card wallet not found" }
|
||||
|
||||
return cardWallet.publicKey
|
||||
}
|
||||
|
||||
private suspend fun findVisaUserWallet(userWalletId: UserWalletId): UserWallet {
|
||||
val userWallet = requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) {
|
||||
"No user wallet found: $userWalletId"
|
||||
}
|
||||
if (!userWallet.scanResponse.cardTypesResolver.isVisaWallet()) {
|
||||
error("VISA wallet required: $userWalletId")
|
||||
}
|
||||
|
||||
return userWallet
|
||||
}
|
||||
|
||||
private fun getBalancesAndLimitsKey(address: String): String {
|
||||
return "visa_balances_and_limits_$address"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
package com.tangem.data.visa
|
||||
|
||||
import com.tangem.domain.visa.model.VisaCurrency
|
||||
import com.tangem.domain.visa.repository.VisaRepository
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
|
||||
internal class DummyVisaRepository : VisaRepository {
|
||||
|
||||
override suspend fun getVisaCurrency(userWalletId: UserWalletId, isRefresh: Boolean): VisaCurrency {
|
||||
TODO(reason = "Implement in [REDACTED_JIRA]")
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,16 @@
|
|||
package com.tangem.data.visa.di
|
||||
|
||||
import com.tangem.data.visa.DummyVisaRepository
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.data.common.cache.CacheRegistry
|
||||
import com.tangem.data.visa.BuildConfig
|
||||
import com.tangem.data.visa.DefaultVisaRepository
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.di.NetworkMoshi
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.visa.repository.VisaRepository
|
||||
import com.tangem.lib.visa.VisaContractInfoProvider
|
||||
import com.tangem.lib.visa.api.VisaApiBuilder
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -14,7 +23,30 @@ internal object VisaDataModule {
|
|||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideVisaRepository(): VisaRepository {
|
||||
return DummyVisaRepository()
|
||||
fun provideVisaRepository(
|
||||
@NetworkMoshi moshi: Moshi,
|
||||
tangemTechApi: TangemTechApi,
|
||||
cacheRegistry: CacheRegistry,
|
||||
userWalletsStore: UserWalletsStore,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): VisaRepository {
|
||||
val contractInfoProvider = VisaContractInfoProvider.Builder(
|
||||
isNetworkLoggingEnabled = BuildConfig.LOG_ENABLED,
|
||||
dispatchers = dispatchers,
|
||||
).build()
|
||||
val visaApi = VisaApiBuilder(
|
||||
useDevApi = true,
|
||||
isNetworkLoggingEnabled = BuildConfig.LOG_ENABLED,
|
||||
moshi = moshi,
|
||||
).build()
|
||||
|
||||
return DefaultVisaRepository(
|
||||
contractInfoProvider,
|
||||
tangemTechApi,
|
||||
visaApi,
|
||||
cacheRegistry,
|
||||
userWalletsStore,
|
||||
dispatchers,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.data.visa.utils
|
||||
|
||||
import android.os.Build
|
||||
import timber.log.Timber
|
||||
import java.util.Currency
|
||||
|
||||
internal fun findCurrencyByNumericCode(code: Int): Currency {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
Currency.getAvailableCurrencies().firstOrNull { it.numericCode == code }
|
||||
?: Currency.getInstance(VisaConfig.fiatCurrency.code)
|
||||
} else {
|
||||
Timber.w("Unable to get currency by numeric code on API level ${Build.VERSION.SDK_INT}")
|
||||
Currency.getInstance(VisaConfig.fiatCurrency.code)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.tangem.data.visa.utils
|
||||
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
|
||||
internal object VisaConfig {
|
||||
|
||||
const val NETWORK_NAME = "Polygon PoS"
|
||||
|
||||
const val TOKEN_SYMBOL = "USDT"
|
||||
const val TOKEN_ID = "tether"
|
||||
const val TOKEN_DECIMALS = 8
|
||||
|
||||
val fiatCurrency = AppCurrency(
|
||||
code = "EUR",
|
||||
name = "Euro",
|
||||
symbol = "€",
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
package com.tangem.data.visa.utils
|
||||
|
||||
import com.tangem.domain.visa.model.VisaCurrency
|
||||
import com.tangem.lib.visa.model.VisaBalancesAndLimits
|
||||
import org.joda.time.DateTime
|
||||
import org.joda.time.DateTimeZone
|
||||
import org.joda.time.Instant
|
||||
import java.math.BigDecimal
|
||||
import java.math.BigInteger
|
||||
|
||||
internal class VisaCurrencyFactory {
|
||||
|
||||
fun create(balancesAndLimits: VisaBalancesAndLimits, fiatRate: BigDecimal?): VisaCurrency {
|
||||
val now = Instant.now()
|
||||
val currentLimit = if (balancesAndLimits.limitsChangeDate > now) {
|
||||
balancesAndLimits.oldLimits
|
||||
} else {
|
||||
balancesAndLimits.newLimits
|
||||
}
|
||||
|
||||
return VisaCurrency(
|
||||
symbol = VisaConfig.TOKEN_SYMBOL,
|
||||
networkName = VisaConfig.NETWORK_NAME,
|
||||
decimals = VisaConfig.TOKEN_DECIMALS,
|
||||
fiatRate = fiatRate,
|
||||
fiatCurrency = VisaConfig.fiatCurrency,
|
||||
balances = with(balancesAndLimits) {
|
||||
VisaCurrency.Balances(
|
||||
total = balances.total,
|
||||
verified = balances.verified,
|
||||
available = balances.available.forPayment,
|
||||
blocked = balances.blocked,
|
||||
debt = balances.debt,
|
||||
pendingRefund = balances.pendingRefund,
|
||||
)
|
||||
},
|
||||
limits = VisaCurrency.Limits(
|
||||
remainingOtp = getRemainingOtp(currentLimit, now),
|
||||
remainingNoOtp = getRemainingNoOtp(currentLimit, now),
|
||||
singleTransaction = currentLimit.singleTransactionLimit,
|
||||
expirationDate = getLimitsExpirationDate(currentLimit, now),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun getRemainingOtp(currentLimit: VisaBalancesAndLimits.Limits, now: Instant): BigDecimal {
|
||||
if (currentLimit.expirationDate >= now) {
|
||||
return currentLimit.spendLimit.limit - currentLimit.spendLimit.spent
|
||||
}
|
||||
|
||||
return currentLimit.spendLimit.limit
|
||||
}
|
||||
|
||||
private fun getRemainingNoOtp(currentLimit: VisaBalancesAndLimits.Limits, now: Instant): BigDecimal {
|
||||
if (currentLimit.expirationDate >= now) {
|
||||
return currentLimit.noOtpLimit.limit - currentLimit.noOtpLimit.spent
|
||||
}
|
||||
|
||||
return currentLimit.noOtpLimit.limit
|
||||
}
|
||||
|
||||
private fun getLimitsExpirationDate(currentLimits: VisaBalancesAndLimits.Limits, now: Instant): DateTime {
|
||||
val expirationDate = if (currentLimits.expirationDate >= now) {
|
||||
currentLimits.expirationDate.toDateTime()
|
||||
} else {
|
||||
val spendPeriodDays = currentLimits.spendPeriodSeconds
|
||||
.div(BigInteger.valueOf(SECONDS_IN_DAY))
|
||||
.toInt()
|
||||
|
||||
now.toDateTime().plusDays(spendPeriodDays)
|
||||
}
|
||||
|
||||
return expirationDate.withZone(DateTimeZone.getDefault())
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val SECONDS_IN_DAY = 86_400L
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
package com.tangem.data.visa.utils
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.domain.visa.model.VisaTxDetails
|
||||
import com.tangem.lib.visa.model.VisaTxHistoryResponse
|
||||
|
||||
internal class VisaTxDetailsFactory {
|
||||
|
||||
fun create(transaction: VisaTxHistoryResponse.Transaction, walletBlockchain: Blockchain): VisaTxDetails {
|
||||
return VisaTxDetails(
|
||||
id = transaction.transactionId.toString(),
|
||||
type = transaction.transactionType,
|
||||
status = transaction.transactionStatus,
|
||||
blockchainAmount = transaction.blockchainAmount,
|
||||
blockchainFee = transaction.blockchainFee,
|
||||
transactionAmount = transaction.transactionAmount,
|
||||
transactionCurrencyCode = transaction.transactionCurrencyCode,
|
||||
merchantName = transaction.merchantName,
|
||||
merchantCity = transaction.merchantCity,
|
||||
merchantCountryCode = transaction.merchantCountryCode,
|
||||
merchantCategoryCode = transaction.merchantCategoryCode,
|
||||
fiatCurrency = findCurrencyByNumericCode(transaction.transactionCurrencyCode),
|
||||
requests = transaction.requests.map { createRequest(it, walletBlockchain) },
|
||||
)
|
||||
}
|
||||
|
||||
private fun createRequest(
|
||||
request: VisaTxHistoryResponse.Transaction.Request,
|
||||
walletBlockchain: Blockchain,
|
||||
): VisaTxDetails.Request {
|
||||
return VisaTxDetails.Request(
|
||||
billingAmount = request.billingAmount,
|
||||
billingCurrencyCode = request.billingCurrencyCode,
|
||||
blockchainAmount = request.blockchainAmount,
|
||||
blockchainFee = request.blockchainFee,
|
||||
errorCode = request.errorCode,
|
||||
requestDate = request.requestDt,
|
||||
requestStatus = request.requestStatus,
|
||||
requestType = request.requestType,
|
||||
transactionAmount = request.transactionAmount,
|
||||
txCurrencyCode = request.transactionCurrencyCode,
|
||||
id = request.transactionRequestId.toString(),
|
||||
txHash = request.txHash,
|
||||
txStatus = request.txStatus,
|
||||
fiatCurrency = findCurrencyByNumericCode(request.transactionCurrencyCode),
|
||||
exploreUrl = request.txHash?.let(walletBlockchain::getExploreTxUrl),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package com.tangem.data.visa.utils
|
||||
|
||||
import com.tangem.domain.visa.model.VisaTxHistoryItem
|
||||
import com.tangem.lib.visa.model.VisaTxHistoryResponse
|
||||
|
||||
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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
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.common.response.getOrThrow
|
||||
import com.tangem.domain.visa.model.VisaTxHistoryItem
|
||||
import com.tangem.lib.visa.api.VisaApi
|
||||
import com.tangem.lib.visa.model.VisaTxHistoryResponse
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
|
||||
internal class VisaTxHistoryPagingSource(
|
||||
params: Params,
|
||||
private val cacheRegistry: CacheRegistry,
|
||||
private val visaApi: VisaApi,
|
||||
private val fetchedItems: MutableStateFlow<Map<String, List<VisaTxHistoryResponse.Transaction>>>,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : PagingSource<Int, VisaTxHistoryItem>() {
|
||||
|
||||
private val itemsFactory = VisaTxHistoryItemFactory()
|
||||
|
||||
private val cardPublicKey = params.cardPublicKey
|
||||
private val pageSize = params.pageSize
|
||||
private val isRefresh = params.isRefresh
|
||||
|
||||
private val pagedItems = MutableStateFlow<Map<Int, List<VisaTxHistoryItem>>>(
|
||||
value = emptyMap(),
|
||||
)
|
||||
|
||||
override fun getRefreshKey(state: PagingState<Int, VisaTxHistoryItem>): Int? {
|
||||
return state.anchorPosition?.let { anchorPosition ->
|
||||
val anchorPage = state.closestPageToPosition(anchorPosition)
|
||||
|
||||
anchorPage?.prevKey?.plus(pageSize) ?: anchorPage?.nextKey?.minus(pageSize)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, VisaTxHistoryItem> {
|
||||
val offsetToLoad = params.key ?: INITIAL_OFFSET
|
||||
|
||||
return try {
|
||||
fetchItemsIfExpired(offsetToLoad, pageSize, isRefresh = isRefresh && params is LoadParams.Refresh)
|
||||
|
||||
val items = pagedItems.value[offsetToLoad].orEmpty()
|
||||
val prevOffset = when {
|
||||
items.isEmpty() -> null
|
||||
offsetToLoad > INITIAL_OFFSET -> offsetToLoad - pageSize
|
||||
else -> null
|
||||
}
|
||||
val nextOffset = when {
|
||||
items.isEmpty() -> INITIAL_OFFSET
|
||||
items.size % pageSize == 0 -> offsetToLoad + pageSize
|
||||
else -> null
|
||||
}
|
||||
|
||||
LoadResult.Page(items, prevOffset, nextOffset)
|
||||
} catch (e: Throwable) {
|
||||
Timber.e(e, "Unable to load the transaction history for the requested offset: $offsetToLoad")
|
||||
LoadResult.Error(e)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchItemsIfExpired(offset: Int, pageSize: Int, isRefresh: Boolean) {
|
||||
cacheRegistry.invokeOnExpire(
|
||||
key = getCacheKey(offset),
|
||||
skipCache = isRefresh,
|
||||
block = { fetchItems(offset, pageSize) },
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun fetchItems(offset: Int, pageSize: Int) = withContext(dispatchers.io) {
|
||||
val response = visaApi.getTxHistory(
|
||||
cardPublicKey = cardPublicKey,
|
||||
limit = pageSize,
|
||||
offset = offset,
|
||||
).getOrThrow()
|
||||
|
||||
fetchedItems.update {
|
||||
it.toMutableMap().apply {
|
||||
this[cardPublicKey] = this[cardPublicKey].orEmpty() + response.transactions
|
||||
}
|
||||
}
|
||||
|
||||
pagedItems.update {
|
||||
it.toMutableMap().apply {
|
||||
this[offset] = response.transactions.map(itemsFactory::create)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getCacheKey(offset: Int): String {
|
||||
return "visa_tx_history_${cardPublicKey}_$offset"
|
||||
}
|
||||
|
||||
class Params(
|
||||
val cardPublicKey: String,
|
||||
val pageSize: Int,
|
||||
val isRefresh: Boolean,
|
||||
)
|
||||
|
||||
private companion object {
|
||||
const val INITIAL_OFFSET = 0
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue