Updated on 2026-08-14

This commit is contained in:
Tangem 2024-02-07 19:27:16 +03:00
parent 710e944eae
commit c22dc2df3b
11 changed files with 259 additions and 7 deletions

View file

@ -33,6 +33,9 @@ dependencies {
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)

View file

@ -1,22 +1,30 @@
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.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.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.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import java.math.BigDecimal
@ -24,6 +32,7 @@ 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,
@ -71,14 +80,34 @@ internal class DefaultVisaRepository(
)
}
private suspend fun getFiatRate(): BigDecimal? {
val fiatCurrencyId = VisaConfig.fiatCurrency.code.lowercase()
val quotes = tangemTechApi.getQuotes(
currencyId = fiatCurrencyId,
coinIds = VisaConfig.TOKEN_ID,
).getOrThrow()
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,
dispatchers = dispatchers,
)
},
)
return quotes.quotes[VisaConfig.TOKEN_ID]?.price
return pager.flow
}
private suspend fun makeAddress(userWalletId: UserWalletId): String {
@ -91,6 +120,16 @@ internal class DefaultVisaRepository(
}
}
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()

View file

@ -1,12 +1,15 @@
package com.tangem.data.visa.di
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
@ -21,6 +24,7 @@ internal object VisaDataModule {
@Provides
@Singleton
fun provideVisaRepository(
@NetworkMoshi moshi: Moshi,
tangemTechApi: TangemTechApi,
cacheRegistry: CacheRegistry,
userWalletsStore: UserWalletsStore,
@ -30,10 +34,16 @@ internal object VisaDataModule {
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,

View file

@ -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)
}
}

View file

@ -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),
)
}
}

View file

@ -0,0 +1,100 @@
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.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 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()
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
}
}