Updated on 2026-08-14

This commit is contained in:
Tangem 2025-01-29 21:44:25 +03:00
commit bf0cb06cb3
297 changed files with 4771 additions and 2376 deletions

View file

@ -294,6 +294,8 @@ private fun Blockchain.getSupportedTransactionExtras(): Network.TransactionExtra
Blockchain.VanarChainTestnet,
Blockchain.OdysseyChain, Blockchain.OdysseyChainTestnet,
Blockchain.Bitrock, Blockchain.BitrockTestnet,
Blockchain.Sonic, Blockchain.SonicTestnet,
Blockchain.ApeChain, Blockchain.ApeChainTestnet,
-> Network.TransactionExtrasType.NONE
// endregion
}

View file

@ -1,3 +1,5 @@
import com.tangem.plugin.configuration.configurations.extension.kaptForObfuscatingVariants
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
@ -44,4 +46,6 @@ dependencies {
/** Other */
implementation(deps.moshi.kotlin)
implementation(deps.timber)
kaptForObfuscatingVariants(deps.moshi.kotlin.codegen)
kaptForObfuscatingVariants(deps.retrofit.response.type.keeper)
}

View file

@ -1,3 +1,5 @@
import com.tangem.plugin.configuration.configurations.extension.kaptForObfuscatingVariants
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
@ -39,5 +41,7 @@ dependencies {
implementation(deps.moshi.kotlin)
implementation(deps.timber)
implementation(deps.tangem.blockchain)
kaptForObfuscatingVariants(deps.moshi.kotlin.codegen)
kaptForObfuscatingVariants(deps.retrofit.response.type.keeper)
// endregion
}

View file

@ -1,3 +1,5 @@
import com.tangem.plugin.configuration.configurations.extension.kaptForObfuscatingVariants
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
@ -24,6 +26,7 @@ dependencies {
implementation(projects.domain.onramp)
implementation(projects.domain.legacy)
implementation(projects.domain.appTheme.models)
implementation(projects.domain.models)
// region DI
@ -41,6 +44,10 @@ dependencies {
implementation(deps.moshi)
implementation(deps.moshi.kotlin)
implementation(deps.timber)
kaptForObfuscatingVariants(deps.moshi.kotlin.codegen)
kaptForObfuscatingVariants(deps.retrofit.response.type.keeper)
implementation(deps.kotlin.serialization)
implementation(projects.libs.blockchainSdk)
implementation(projects.libs.crypto)

View file

@ -0,0 +1,41 @@
package com.tangem.data.onramp
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.data.onramp.converters.HotCryptoCurrencyConverter
import com.tangem.datasource.exchangeservice.hotcrypto.HotCryptoResponseStore
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.onramp.model.HotCryptoCurrency
import com.tangem.domain.onramp.repositories.HotCryptoRepository
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
/**
* Default implementation of [HotCryptoRepository]
*
* @property excludedBlockchains excluded blockchains
* @property hotCryptoResponseStore store of `HotCryptoResponse`
* @property userWalletsStore store of `UserWallet`
*
[REDACTED_AUTHOR]
*/
internal class DefaultHotCryptoRepository(
private val excludedBlockchains: ExcludedBlockchains,
private val hotCryptoResponseStore: HotCryptoResponseStore,
private val userWalletsStore: UserWalletsStore,
) : HotCryptoRepository {
override fun getCurrencies(userWalletId: UserWalletId): Flow<List<HotCryptoCurrency>> {
return hotCryptoResponseStore.get()
.map {
val userWallet = userWalletsStore.getSyncOrNull(userWalletId)
?: error("UserWalletId [$userWalletId] not found")
HotCryptoCurrencyConverter(
scanResponse = userWallet.scanResponse,
imageHost = it.imageHost,
excludedBlockchains = excludedBlockchains,
).convertList(input = it.tokens)
}
}
}

View file

@ -0,0 +1,78 @@
package com.tangem.data.onramp.converters
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.data.common.currency.CryptoCurrencyFactory
import com.tangem.data.common.currency.getNetwork
import com.tangem.datasource.api.tangemTech.models.HotCryptoResponse
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.onramp.model.HotCryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.utils.converter.Converter
/**
* Converter from [HotCryptoResponse.Token] to [HotCryptoCurrency]
*
* @property scanResponse scan response
* @property imageHost image host
* @property excludedBlockchains excluded blockchains
*
[REDACTED_AUTHOR]
*/
internal class HotCryptoCurrencyConverter(
private val scanResponse: ScanResponse,
private val imageHost: String?,
private val excludedBlockchains: ExcludedBlockchains,
) : Converter<HotCryptoResponse.Token, HotCryptoCurrency> {
private val cryptoCurrencyFactory by lazy { CryptoCurrencyFactory(excludedBlockchains) }
override fun convert(value: HotCryptoResponse.Token): HotCryptoCurrency {
val network = createNetwork(value.networkId)
val contractAddress = value.contractAddress
val decimals = value.decimalCount
val currency = if (contractAddress != null && decimals != null) {
cryptoCurrencyFactory.createToken(
network = network,
rawId = value.id,
name = value.name,
symbol = value.symbol,
decimals = decimals,
contractAddress = contractAddress,
)
} else {
cryptoCurrencyFactory.createCoin(network = network)
}
return HotCryptoCurrency(
cryptoCurrency = currency.setupIconUrl(id = value.id),
fiatRate = value.currentPrice,
priceChange = value.priceChangePercentage,
)
}
private fun createNetwork(networkId: String): Network {
return requireNotNull(
getNetwork(
blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)),
extraDerivationPath = null,
scanResponse = scanResponse,
excludedBlockchains = excludedBlockchains,
),
)
}
private fun CryptoCurrency.setupIconUrl(id: String): CryptoCurrency {
imageHost ?: return this
val iconUrl = "${imageHost}large/$id.png"
return when (this) {
is CryptoCurrency.Coin -> copy(iconUrl = iconUrl)
is CryptoCurrency.Token -> copy(iconUrl = iconUrl)
}
}
}

View file

@ -1,6 +1,8 @@
package com.tangem.data.onramp.di
import com.squareup.moshi.Moshi
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.data.onramp.DefaultHotCryptoRepository
import com.tangem.data.onramp.DefaultOnrampErrorResolver
import com.tangem.data.onramp.DefaultOnrampRepository
import com.tangem.data.onramp.DefaultOnrampTransactionRepository
@ -10,12 +12,15 @@ import com.tangem.datasource.api.express.models.response.ExpressErrorResponse
import com.tangem.datasource.api.onramp.OnrampApi
import com.tangem.datasource.crypto.DataSignatureVerifier
import com.tangem.datasource.di.NetworkMoshi
import com.tangem.datasource.exchangeservice.hotcrypto.HotCryptoResponseStore
import com.tangem.datasource.local.onramp.countries.OnrampCountriesStore
import com.tangem.datasource.local.onramp.currencies.OnrampCurrenciesStore
import com.tangem.datasource.local.onramp.pairs.OnrampPairsStore
import com.tangem.datasource.local.onramp.paymentmethods.OnrampPaymentMethodsStore
import com.tangem.datasource.local.onramp.quotes.OnrampQuotesStore
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.onramp.repositories.HotCryptoRepository
import com.tangem.domain.onramp.repositories.OnrampErrorResolver
import com.tangem.domain.onramp.repositories.OnrampRepository
import com.tangem.domain.onramp.repositories.OnrampTransactionRepository
@ -83,4 +88,18 @@ internal object OnrampDataModule {
OnrampErrorConverter(jsonAdapter),
)
}
@Provides
@Singleton
fun provideHotCryptoRepository(
excludedBlockchains: ExcludedBlockchains,
hotCryptoResponseStore: HotCryptoResponseStore,
userWalletsStore: UserWalletsStore,
): HotCryptoRepository {
return DefaultHotCryptoRepository(
excludedBlockchains = excludedBlockchains,
hotCryptoResponseStore = hotCryptoResponseStore,
userWalletsStore = userWalletsStore,
)
}
}

View file

@ -1,94 +1,32 @@
package com.tangem.data.promo
import com.tangem.data.promo.converters.PromoResponseConverter
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys.ADDED_WALLETS_WITH_RING_KEY
import com.tangem.datasource.local.preferences.PreferencesKeys.IS_TOKEN_SWAP_PROMO_OKX_SHOW_KEY
import com.tangem.datasource.local.preferences.PreferencesKeys.IS_WALLET_SWAP_PROMO_OKX_SHOW_KEY
import com.tangem.datasource.local.preferences.PreferencesKeys.SHOULD_SHOW_RING_PROMO_KEY
import com.tangem.datasource.local.preferences.PreferencesKeys.SHOULD_SHOW_SWAP_STORIES_KEY
import com.tangem.datasource.local.preferences.utils.get
import com.tangem.datasource.local.preferences.utils.getSyncOrDefault
import com.tangem.datasource.local.preferences.utils.store
import com.tangem.domain.promo.PromoRepository
import com.tangem.domain.promo.models.PromoBanner
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.runCatching
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.flowOf
internal class DefaultPromoRepository(
private val tangemApi: TangemTechApi,
private val appPreferencesStore: AppPreferencesStore,
private val dispatchers: CoroutineDispatcherProvider,
) : PromoRepository {
private val promoResponseConverter = PromoResponseConverter()
override suspend fun getChangellyPromoBanner(): PromoBanner? {
return runCatching(dispatchers.io) {
promoResponseConverter.convert(
tangemApi.getPromotionInfo(CHANGELLY_NAME)
.getOrThrow(),
)
}.getOrNull()
}
override suspend fun getOkxPromoBanner(): PromoBanner? {
return runCatching(dispatchers.io) {
promoResponseConverter.convert(
tangemApi.getPromotionInfo(OKX)
.getOrThrow(),
)
}.getOrNull()
}
override suspend fun getRingPromoBanner(): PromoBanner? {
return runCatching(dispatchers.io) {
promoResponseConverter.convert(
tangemApi.getPromotionInfo(RING)
.getOrThrow(),
)
}.getOrNull()
}
override fun isReadyToShowWalletSwapPromo(): Flow<Boolean> {
return appPreferencesStore.get(IS_WALLET_SWAP_PROMO_OKX_SHOW_KEY, true)
return flowOf(false) // Use it on new promo action
}
override fun isReadyToShowTokenSwapPromo(): Flow<Boolean> {
return appPreferencesStore.get(IS_TOKEN_SWAP_PROMO_OKX_SHOW_KEY, true)
return flowOf(false) // Use it on new promo action
}
override suspend fun setNeverToShowWalletSwapPromo() {
appPreferencesStore.store(
key = IS_WALLET_SWAP_PROMO_OKX_SHOW_KEY,
value = false,
)
// Use it on new promo action
}
override suspend fun setNeverToShowTokenSwapPromo() {
appPreferencesStore.store(
key = IS_TOKEN_SWAP_PROMO_OKX_SHOW_KEY,
value = false,
)
}
override fun isReadyToShowRingPromo(userWalletId: UserWalletId): Flow<Boolean> {
return combine(
flow = appPreferencesStore
.get(key = ADDED_WALLETS_WITH_RING_KEY, default = emptySet())
.map { userWalletId.stringValue in it },
flow2 = appPreferencesStore.get(key = SHOULD_SHOW_RING_PROMO_KEY, default = true),
transform = { isRingAdded, shouldShowRingPromo -> isRingAdded && shouldShowRingPromo },
)
}
override suspend fun setNeverToShowRingPromo() {
appPreferencesStore.store(key = SHOULD_SHOW_RING_PROMO_KEY, value = false)
// Use it on new promo action
}
override fun isReadyToShowSwapStories(): Flow<Boolean> {
@ -105,10 +43,4 @@ internal class DefaultPromoRepository(
value = false,
)
}
private companion object {
private const val CHANGELLY_NAME = "changelly"
private const val OKX = "okx"
private const val RING = "ring"
}
}

View file

@ -1,10 +1,8 @@
package com.tangem.data.promo.di
import com.tangem.data.promo.DefaultPromoRepository
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.domain.promo.PromoRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -17,11 +15,7 @@ internal object PromoDataModule {
@Provides
@Singleton
fun providePromoRepository(
tangemTechApi: TangemTechApi,
appPreferencesStore: AppPreferencesStore,
dispatchers: CoroutineDispatcherProvider,
): PromoRepository {
return DefaultPromoRepository(tangemTechApi, appPreferencesStore, dispatchers)
fun providePromoRepository(appPreferencesStore: AppPreferencesStore): PromoRepository {
return DefaultPromoRepository(appPreferencesStore)
}
}

View file

@ -1,3 +1,5 @@
import com.tangem.plugin.configuration.configurations.extension.kaptForObfuscatingVariants
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
@ -31,5 +33,7 @@ dependencies {
implementation(deps.moshi)
implementation(deps.moshi.kotlin)
implementation(deps.timber)
kaptForObfuscatingVariants(deps.moshi.kotlin.codegen)
kaptForObfuscatingVariants(deps.retrofit.response.type.keeper)
// endregion
}

View file

@ -1,3 +1,5 @@
import com.tangem.plugin.configuration.configurations.extension.kaptForObfuscatingVariants
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
@ -45,6 +47,8 @@ dependencies {
implementation(deps.moshi)
implementation(deps.moshi.kotlin)
implementation(deps.timber)
kaptForObfuscatingVariants(deps.moshi.kotlin.codegen)
kaptForObfuscatingVariants(deps.retrofit.response.type.keeper)
implementation(projects.libs.blockchainSdk)
implementation(projects.libs.crypto)

View file

@ -1,3 +1,5 @@
import com.tangem.plugin.configuration.configurations.extension.kaptForObfuscatingVariants
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
@ -48,4 +50,6 @@ dependencies {
implementation(deps.timber)
implementation(deps.retrofit) // For HttpException
implementation(deps.androidx.paging.runtime)
kaptForObfuscatingVariants(deps.moshi.kotlin.codegen)
kaptForObfuscatingVariants(deps.retrofit.response.type.keeper)
}

View file

@ -4,6 +4,7 @@ import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.data.common.cache.CacheRegistry
import com.tangem.data.tokens.repository.*
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.exchangeservice.hotcrypto.HotCryptoLoader
import com.tangem.datasource.exchangeservice.swap.ExpressServiceLoader
import com.tangem.datasource.local.network.NetworksStatusesStore
import com.tangem.datasource.local.preferences.AppPreferencesStore
@ -32,6 +33,7 @@ internal object TokensDataModule {
cacheRegistry: CacheRegistry,
dispatchers: CoroutineDispatcherProvider,
expressServiceLoader: ExpressServiceLoader,
hotCryptoLoader: HotCryptoLoader,
excludedBlockchains: ExcludedBlockchains,
): CurrenciesRepository {
return DefaultCurrenciesRepository(
@ -42,6 +44,7 @@ internal object TokensDataModule {
appPreferencesStore = appPreferencesStore,
dispatchers = dispatchers,
expressServiceLoader = expressServiceLoader,
hotCryptoLoader = hotCryptoLoader,
excludedBlockchains = excludedBlockchains,
)
}

View file

@ -15,6 +15,7 @@ import com.tangem.datasource.api.express.models.TangemExpressValues.EMPTY_CONTRA
import com.tangem.datasource.api.express.models.request.LeastTokenInfo
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.exchangeservice.hotcrypto.HotCryptoLoader
import com.tangem.datasource.exchangeservice.swap.ExpressServiceLoader
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
@ -33,10 +34,8 @@ import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.plus
import kotlinx.coroutines.withContext
import timber.log.Timber
import com.tangem.blockchain.common.FeePaidCurrency as FeePaidSdkCurrency
@ -48,6 +47,7 @@ internal class DefaultCurrenciesRepository(
private val cacheRegistry: CacheRegistry,
private val appPreferencesStore: AppPreferencesStore,
private val expressServiceLoader: ExpressServiceLoader,
private val hotCryptoLoader: HotCryptoLoader,
private val dispatchers: CoroutineDispatcherProvider,
excludedBlockchains: ExcludedBlockchains,
) : CurrenciesRepository {
@ -601,7 +601,11 @@ internal class DefaultCurrenciesRepository(
network = token.networkId,
)
}
expressServiceLoader.update(userWalletId, tokens)
coroutineScope {
launch { expressServiceLoader.update(userWalletId, tokens) }
launch { hotCryptoLoader.fetch(tokens = userTokens.tokens) }
}
}
private suspend fun fetchExpressAssetsByNetworkIds(
@ -619,7 +623,12 @@ internal class DefaultCurrenciesRepository(
cacheRegistry.invokeOnExpire(
key = getAssetsCacheKey(userWalletId),
skipCache = refresh,
block = { expressServiceLoader.update(userWalletId, tokens) },
block = {
coroutineScope {
launch { expressServiceLoader.update(userWalletId, tokens) }
launch { hotCryptoLoader.update(cryptoCurrencies) }
}
},
)
}

View file

@ -1,3 +1,5 @@
import com.tangem.plugin.configuration.configurations.extension.kaptForObfuscatingVariants
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
@ -37,6 +39,8 @@ dependencies {
implementation(deps.timber)
implementation(deps.androidx.paging.runtime)
implementation(deps.moshi.kotlin)
kaptForObfuscatingVariants(deps.moshi.kotlin.codegen)
kaptForObfuscatingVariants(deps.retrofit.response.type.keeper)
/** Libs - Tangem */
implementation(deps.tangem.blockchain)

View file

@ -6,22 +6,25 @@ 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.hexToBytes
import com.tangem.common.extensions.toHexString
import com.tangem.data.common.cache.CacheRegistry
import com.tangem.data.visa.config.VisaLibLoader
import com.tangem.data.visa.utils.*
import com.tangem.datasource.api.common.response.ApiResponseError
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.common.visa.TangemVisaAuthProvider
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.common.visa.VisaUtilities
import com.tangem.domain.visa.exception.RefreshTokenExpiredException
import com.tangem.domain.visa.model.*
import com.tangem.domain.visa.repository.VisaAuthRepository
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.api.VisaApi
import com.tangem.lib.visa.model.VisaTxHistoryResponse
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.Flow
@ -29,13 +32,19 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.withContext
import java.math.BigDecimal
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.jvm.Throws
internal class DefaultVisaRepository(
@Singleton
internal class DefaultVisaRepository @Inject constructor(
private val visaLibLoader: VisaLibLoader,
private val tangemTechApi: TangemTechApi,
private val cacheRegistry: CacheRegistry,
private val userWalletsStore: UserWalletsStore,
private val dispatchers: CoroutineDispatcherProvider,
private val visaAuthProvider: TangemVisaAuthProvider,
private val visaAuthRepository: VisaAuthRepository,
) : VisaRepository {
private val currencyFactory by lazy(mode = LazyThreadSafetyMode.NONE) {
@ -106,11 +115,12 @@ internal class DefaultVisaRepository(
cardPublicKey = cardPubKey,
pageSize = pageSize,
isRefresh = isRefresh,
userWallet = userWallet,
),
cacheRegistry = cacheRegistry,
visaApi = api,
fetchedItems = fetchedHistoryItems,
dispatchers = dispatchers,
requestTxHistory = { offset, pageSize -> getTxHistory(api, userWalletId, offset, pageSize) },
)
},
)
@ -134,6 +144,25 @@ internal class DefaultVisaRepository(
}
}
private suspend fun getTxHistory(
api: VisaApi,
userWalletId: UserWalletId,
offset: Int,
pageSize: Int,
): VisaTxHistoryResponse = withContext(dispatchers.io) {
val userWallet = findVisaUserWallet(userWalletId)
val cardPubKey = getCardPubKey(userWallet)
request(userWalletId = userWalletId) {
api.getTxHistory(
authorizationHeader = visaAuthProvider.getAuthHeader(userWallet.cardId),
cardPublicKey = cardPubKey,
limit = pageSize,
offset = offset,
).getOrThrow()
}
}
private suspend fun makeAddress(userWalletId: UserWalletId): String {
if (VisaConstants.IS_DEMO_MODE_ENABLED) return getDemoAddress()
@ -166,9 +195,9 @@ internal class DefaultVisaRepository(
if (VisaConstants.IS_DEMO_MODE_ENABLED) return getDemoPublicKey()
val cardWallet = userWallet.scanResponse.card.wallets.firstOrNull {
it.curve == EllipticCurve.Secp256k1
it.curve == VisaUtilities.mandatoryCurve
}
requireNotNull(cardWallet) { "Secp256k1 card wallet not found" }
requireNotNull(cardWallet) { "Visa card wallet not found" }
return cardWallet.publicKey.toHexString()
}
@ -187,4 +216,56 @@ internal class DefaultVisaRepository(
private fun getVisaCurrencyKey(address: String): String {
return "visa_currency_$address"
}
private suspend fun <T : Any> request(
userWalletId: UserWalletId,
requestBlock: suspend () -> T,
): T {
return runCatching {
requestBlock()
}.getOrElse { responseError ->
if (responseError !is ApiResponseError.HttpException ||
responseError.code != ApiResponseError.HttpException.Code.UNAUTHORIZED
) {
throw responseError
}
val authTokens = getAuthTokens(userWalletId)
val newTokens = runCatching {
visaAuthRepository.refreshAccessTokens(authTokens.refreshToken)
}.getOrElse {
if (it is ApiResponseError.HttpException &&
it.code == ApiResponseError.HttpException.Code.UNAUTHORIZED
) {
userWalletsStore.update(userWalletId) { userWallet ->
userWallet.copy(
scanResponse = userWallet.scanResponse.copy(
visaCardActivationStatus = VisaCardActivationStatus.RefreshTokenExpired
)
)
}
}
throw RefreshTokenExpiredException()
}
userWalletsStore.update(userWalletId) { userWallet ->
userWallet.copy(
scanResponse = userWallet.scanResponse.copy(
visaCardActivationStatus = VisaCardActivationStatus.Activated(
visaAuthTokens = newTokens
)
)
)
}
requestBlock()
}
}
@Throws
private suspend fun getAuthTokens(userWalletId: UserWalletId): VisaAuthTokens {
val userWallet = findVisaUserWallet(userWalletId)
val status = userWallet.scanResponse.visaCardActivationStatus ?: error("Visa card activation status not found")
return (status as? VisaCardActivationStatus.Activated)?.visaAuthTokens ?: error("Visa card is not activated")
}
}

View file

@ -1,7 +1,9 @@
package com.tangem.data.visa.config
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
internal data class VisaConfig(
@Json(name = "testnet")
val testnet: Addresses,
@ -11,6 +13,7 @@ internal data class VisaConfig(
val header: Header,
) {
@JsonClass(generateAdapter = true)
data class Addresses(
@Json(name = "paymentAccountRegistry")
val paymentAccountRegistry: String,
@ -18,6 +21,7 @@ internal data class VisaConfig(
val bridgeProcessor: String,
)
@JsonClass(generateAdapter = true)
data class Header(
@Json(name = "x-asn")
val xAsn: String,

View file

@ -7,6 +7,7 @@ import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.visa.repository.VisaRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -15,22 +16,10 @@ import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object ImplementedVisaDataModule {
internal interface ImplementedVisaDataModule {
@Provides
@Binds
@Singleton
@ImplementedVisaRepository
fun provideVisaRepository(
visaLibLoader: VisaLibLoader,
tangemTechApi: TangemTechApi,
cacheRegistry: CacheRegistry,
userWalletsStore: UserWalletsStore,
dispatchers: CoroutineDispatcherProvider,
): VisaRepository = DefaultVisaRepository(
visaLibLoader,
tangemTechApi,
cacheRegistry,
userWalletsStore,
dispatchers,
)
fun provideVisaRepository(impl: DefaultVisaRepository): VisaRepository
}

View file

@ -3,8 +3,11 @@ 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.ApiResponseError
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.common.visa.TangemVisaAuthProvider
import com.tangem.domain.visa.model.VisaTxHistoryItem
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.lib.visa.api.VisaApi
import com.tangem.lib.visa.model.VisaTxHistoryResponse
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -16,9 +19,9 @@ 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,
val requestTxHistory: suspend (offset: Int, pageSize: Int) -> VisaTxHistoryResponse,
) : PagingSource<Int, VisaTxHistoryItem>() {
private val itemsFactory = VisaTxHistoryItemFactory()
@ -73,11 +76,7 @@ internal class VisaTxHistoryPagingSource(
}
private suspend fun fetchItems(offset: Int, pageSize: Int) = withContext(dispatchers.io) {
val response = visaApi.getTxHistory(
cardPublicKey = cardPublicKey,
limit = pageSize,
offset = offset,
).getOrThrow()
val response = requestTxHistory(offset, pageSize)
fetchedItems.update {
it.toMutableMap().apply {
@ -97,6 +96,7 @@ internal class VisaTxHistoryPagingSource(
}
class Params(
val userWallet: UserWallet,
val cardPublicKey: String,
val pageSize: Int,
val isRefresh: Boolean,

View file

@ -1,28 +1,49 @@
package com.tangem.data.visa
import com.tangem.common.extensions.toHexString
import com.tangem.crypto.CryptoUtils
import com.tangem.data.visa.converter.VisaActivationStatusConverter
import com.tangem.datasource.api.common.visa.TangemVisaAuthProvider
import com.tangem.datasource.api.visa.TangemVisaApi
import com.tangem.domain.visa.model.ActivationOrder
import com.tangem.domain.visa.model.VisaActivationRemoteState
import com.tangem.domain.visa.repository.VisaActivationRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.withContext
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
internal class DefaultVisaActivationRepository @Inject constructor(
@Suppress("UnusedPrivateMember")
internal class DefaultVisaActivationRepository @AssistedInject constructor(
@Assisted private val cardId: String,
private val visaApi: TangemVisaApi,
private val dispatcherProvider: CoroutineDispatcherProvider,
private val visaActivationStatusConverter: VisaActivationStatusConverter,
private val visaAuthProvider: TangemVisaAuthProvider,
) : VisaActivationRepository {
override suspend fun getActivationRemoteState(): VisaActivationRemoteState = withContext(dispatcherProvider.io) {
visaActivationStatusConverter.convert(visaApi.getRemoteActivationStatus())
// visaActivationStatusConverter.convert(visaApi.getRemoteActivationStatus(visaAuthProvider.getAuthHeader(cardId)))
VisaActivationRemoteState.CardWalletSignatureRequired // mock
// TODO implement refreshing access token if it's expired
}
override suspend fun getActivationOrderToSign(): ActivationOrder {
return ActivationOrder("TODO implement")
override suspend fun getActivationRemoteStateLongPoll(): VisaActivationRemoteState =
withContext(dispatcherProvider.io) {
// visaActivationStatusConverter.convert(
// visaApi.getRemoteActivationStatusLongPoll(visaAuthProvider.getAuthHeader(cardId)),
// )
VisaActivationRemoteState.WaitingPinCode
}
override suspend fun getActivationOrderToSign(): ActivationOrder = withContext(dispatcherProvider.io) {
ActivationOrder(CryptoUtils.generateRandomBytes(length = 32).toHexString())
}
@AssistedFactory
interface Factory : VisaActivationRepository.Factory {
override fun create(cardId: String): DefaultVisaActivationRepository
}
}

View file

@ -1,5 +1,7 @@
package com.tangem.data.visa
import com.tangem.common.extensions.toHexString
import com.tangem.crypto.CryptoUtils
import com.tangem.datasource.api.visa.TangemVisaAuthApi
import com.tangem.domain.visa.model.VisaAuthChallenge
import com.tangem.domain.visa.model.VisaAuthSession
@ -10,6 +12,7 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext
import javax.inject.Inject
@Suppress("UnusedPrivateMember")
internal class DefaultVisaAuthRepository @Inject constructor(
private val visaAuthApi: TangemVisaAuthApi,
private val dispatchers: CoroutineDispatcherProvider,
@ -17,14 +20,19 @@ internal class DefaultVisaAuthRepository @Inject constructor(
override suspend fun getCardAuthChallenge(cardId: String, cardPublicKey: String): VisaAuthChallenge.Card =
withContext(dispatchers.io) {
val response = visaAuthApi.generateNonceByCard(
cardId = cardId,
cardPublicKey = cardPublicKey,
)
// val response = visaAuthApi.generateNonceByCard(
// cardId = cardId,
// cardPublicKey = cardPublicKey,
// )
//
// VisaAuthChallenge.Card(
// challenge = response.nonce,
// session = VisaAuthSession(response.sessionId),
// )
VisaAuthChallenge.Card(
challenge = response.nonce,
session = VisaAuthSession(response.sessionId),
challenge = CryptoUtils.generateRandomBytes(length = 16).toHexString(),
session = VisaAuthSession("session"),
)
}
@ -32,39 +40,56 @@ internal class DefaultVisaAuthRepository @Inject constructor(
cardId: String,
walletPublicKey: String,
): VisaAuthChallenge.Wallet = withContext(dispatchers.io) {
val response = visaAuthApi.generateNonceByWalletAddress(
customerId = cardId,
customerWalletAddress = walletPublicKey,
)
// val response = visaAuthApi.generateNonceByWalletAddress(
// customerId = cardId,
// customerWalletAddress = walletPublicKey,
// )
//
// VisaAuthChallenge.Wallet(
// challenge = response.nonce,
// session = VisaAuthSession(response.sessionId),
// )
VisaAuthChallenge.Wallet(
challenge = response.nonce,
session = VisaAuthSession(response.sessionId),
challenge = CryptoUtils.generateRandomBytes(length = 32).toHexString(),
session = VisaAuthSession("session"),
)
}
override suspend fun getAccessTokens(signedChallenge: VisaAuthSignedChallenge): VisaAuthTokens =
withContext(dispatchers.io) {
val response = when (signedChallenge) {
is VisaAuthSignedChallenge.ByCardPublicKey -> {
visaAuthApi.getAccessToken(
sessionId = signedChallenge.challenge.session.sessionId,
signature = signedChallenge.signature,
salt = signedChallenge.salt,
)
}
is VisaAuthSignedChallenge.ByWallet -> {
visaAuthApi.getAccessToken(
sessionId = signedChallenge.challenge.session.sessionId,
signature = signedChallenge.signature,
salt = null,
)
}
}
// val response = when (signedChallenge) {
// is VisaAuthSignedChallenge.ByCardPublicKey -> {
// visaAuthApi.getAccessToken(
// sessionId = signedChallenge.challenge.session.sessionId,
// signature = signedChallenge.signature,
// salt = signedChallenge.salt,
// )
// }
// is VisaAuthSignedChallenge.ByWallet -> {
// visaAuthApi.getAccessToken(
// sessionId = signedChallenge.challenge.session.sessionId,
// signature = signedChallenge.signature,
// salt = null,
// )
// }
// }
//
// VisaAuthTokens(
// accessToken = response.accessToken,
// refreshToken = response.refreshToken,
// )
VisaAuthTokens(
accessToken = response.accessToken,
refreshToken = response.refreshToken,
accessToken = "accessToken",
refreshToken = VisaAuthTokens.RefreshToken("refreshToken"),
)
}
override suspend fun refreshAccessTokens(refreshToken: VisaAuthTokens.RefreshToken): VisaAuthTokens =
withContext(dispatchers.io) {
// TODO
VisaAuthTokens(
accessToken = "accessToken",
refreshToken = VisaAuthTokens.RefreshToken("new refreshToken"),
)
}
}

View file

@ -38,5 +38,7 @@ internal interface VisaDataBindsModule {
@Binds
@Singleton
fun bindVisaActivationRepository(repository: DefaultVisaActivationRepository): VisaActivationRepository
fun bindVisaActivationRepositoryFactory(
repository: DefaultVisaActivationRepository.Factory,
): VisaActivationRepository.Factory
}