Updated on 2026-08-14

This commit is contained in:
Tangem 2024-02-07 12:15:41 +03:00
parent 6d7042f24c
commit 88dbdbc7f6
6 changed files with 268 additions and 15 deletions

View file

@ -11,9 +11,32 @@ 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)
/** Libs - Tangem */
implementation(deps.tangem.blockchain)
implementation(deps.tangem.card.core)
/** DI */
implementation(deps.hilt.core)

View file

@ -0,0 +1,123 @@
package com.tangem.data.visa
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.data.common.cache.CacheRegistry
import com.tangem.data.visa.utils.VisaConfig
import com.tangem.data.visa.utils.VisaCurrencyFactory
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.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.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import java.math.BigDecimal
internal class DefaultVisaRepository(
private val visaContractInfoProvider: VisaContractInfoProvider,
private val tangemTechApi: TangemTechApi,
private val cacheRegistry: CacheRegistry,
private val userWalletsStore: UserWalletsStore,
private val dispatchers: CoroutineDispatcherProvider,
) : VisaRepository {
private val currencyFactory by lazy(mode = LazyThreadSafetyMode.NONE) {
VisaCurrencyFactory()
}
private val fetchedCurrencies = MutableStateFlow(
value = hashMapOf<String, VisaCurrency>(),
)
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))
}
}
},
)
}
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 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 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"
}
}

View file

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

View file

@ -1,7 +1,13 @@
package com.tangem.data.visa.di
import com.tangem.data.visa.DummyVisaRepository
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.local.userwallet.UserWalletsStore
import com.tangem.domain.visa.repository.VisaRepository
import com.tangem.lib.visa.VisaContractInfoProvider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -14,7 +20,23 @@ internal object VisaDataModule {
@Provides
@Singleton
fun provideVisaRepository(): VisaRepository {
return DummyVisaRepository()
fun provideVisaRepository(
tangemTechApi: TangemTechApi,
cacheRegistry: CacheRegistry,
userWalletsStore: UserWalletsStore,
dispatchers: CoroutineDispatcherProvider,
): VisaRepository {
val contractInfoProvider = VisaContractInfoProvider.Builder(
isNetworkLoggingEnabled = BuildConfig.LOG_ENABLED,
dispatchers = dispatchers,
).build()
return DefaultVisaRepository(
contractInfoProvider,
tangemTechApi,
cacheRegistry,
userWalletsStore,
dispatchers,
)
}
}

View file

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

View file

@ -0,0 +1,79 @@
package com.tangem.data.visa.utils
import com.tangem.domain.visa.model.VisaCurrency
import com.tangem.lib.visa.model.BalancesAndLimits
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: BalancesAndLimits, 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: BalancesAndLimits.Limits, now: Instant): BigDecimal {
if (currentLimit.expirationDate >= now) {
return currentLimit.spendLimit.limit - currentLimit.spendLimit.spent
}
return currentLimit.spendLimit.limit
}
private fun getRemainingNoOtp(currentLimit: BalancesAndLimits.Limits, now: Instant): BigDecimal {
if (currentLimit.expirationDate >= now) {
return currentLimit.noOtpLimit.limit - currentLimit.noOtpLimit.spent
}
return currentLimit.noOtpLimit.limit
}
private fun getLimitsExpirationDate(currentLimits: BalancesAndLimits.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
}
}