From 88dbdbc7f6a07ca81638e442f2a6dd383f4b87f6 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 7 Feb 2024 12:15:41 +0300 Subject: [PATCH] Updated on 2026-08-14 --- data/visa/build.gradle.kts | 23 ++++ .../tangem/data/visa/DefaultVisaRepository.kt | 123 ++++++++++++++++++ .../tangem/data/visa/DummyVisaRepository.kt | 12 -- .../com/tangem/data/visa/di/VisaDataModule.kt | 28 +++- .../com/tangem/data/visa/utils/VisaConfig.kt | 18 +++ .../data/visa/utils/VisaCurrencyFactory.kt | 79 +++++++++++ 6 files changed, 268 insertions(+), 15 deletions(-) create mode 100644 data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt delete mode 100644 data/visa/src/main/kotlin/com/tangem/data/visa/DummyVisaRepository.kt create mode 100644 data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaConfig.kt create mode 100644 data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaCurrencyFactory.kt diff --git a/data/visa/build.gradle.kts b/data/visa/build.gradle.kts index e57696df79..26f4c9abe7 100644 --- a/data/visa/build.gradle.kts +++ b/data/visa/build.gradle.kts @@ -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) diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt new file mode 100644 index 0000000000..9ba7dd179b --- /dev/null +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt @@ -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(), + ) + + 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
{ + 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" + } +} \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/DummyVisaRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/DummyVisaRepository.kt deleted file mode 100644 index 40815fac3c..0000000000 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/DummyVisaRepository.kt +++ /dev/null @@ -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]") - } -} \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/di/VisaDataModule.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/di/VisaDataModule.kt index ab68249dd9..4204ec5430 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/di/VisaDataModule.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/di/VisaDataModule.kt @@ -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, + ) } } \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaConfig.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaConfig.kt new file mode 100644 index 0000000000..e450a769cc --- /dev/null +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaConfig.kt @@ -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 = "€", + ) +} \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaCurrencyFactory.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaCurrencyFactory.kt new file mode 100644 index 0000000000..d2555bdf84 --- /dev/null +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaCurrencyFactory.kt @@ -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 + } +} \ No newline at end of file