Updated on 2026-08-14

This commit is contained in:
Tangem 2025-02-17 21:44:49 +03:00
parent 6d56af68f5
commit 3d9e937ee9
30 changed files with 258 additions and 344 deletions

View file

@ -1,19 +0,0 @@
package com.tangem.tap.network.auth
import com.tangem.datasource.api.common.visa.TangemVisaAuthProvider
import com.tangem.domain.visa.model.VisaCardActivationStatus
import com.tangem.domain.visa.model.getAuthHeader
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import javax.inject.Inject
internal class DefaultVisaAuthProvider @Inject constructor(
private val userWalletsListManager: UserWalletsListManager,
) : TangemVisaAuthProvider {
override suspend fun getAuthHeader(cardId: String): String {
val card = userWalletsListManager.userWalletsSync.firstOrNull { it.cardId == cardId }
val status = card?.scanResponse?.visaCardActivationStatus as? VisaCardActivationStatus.Activated
?: return "Error in the app!"
return status.visaAuthTokens.getAuthHeader()
}
}

View file

@ -1,21 +1,17 @@
package com.tangem.tap.network.auth.di
import com.tangem.datasource.api.common.AuthProvider
import com.tangem.datasource.api.common.visa.TangemVisaAuthProvider
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.lib.auth.ExpressAuthProvider
import com.tangem.lib.auth.StakeKitAuthProvider
import com.tangem.tap.network.auth.*
import com.tangem.tap.network.auth.DefaultAppVersionProvider
import com.tangem.tap.network.auth.DefaultAuthProvider
import com.tangem.tap.network.auth.DefaultExpressAuthProvider
import com.tangem.tap.network.auth.DefaultStakeKitAuthProvider
import com.tangem.tap.network.auth.DefaultVisaAuthProvider
import com.tangem.utils.version.AppVersionProvider
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -55,13 +51,4 @@ internal class AuthModule {
fun provideAppVersionProvider(): AppVersionProvider {
return DefaultAppVersionProvider()
}
}
@Module
@InstallIn(SingletonComponent::class)
internal interface AuthBindModule {
@Binds
@Singleton
fun bindVisaAuthProvider(authStorage: DefaultVisaAuthProvider): TangemVisaAuthProvider
}

View file

@ -1,6 +0,0 @@
package com.tangem.datasource.api.common.visa
interface TangemVisaAuthProvider {
suspend fun getAuthHeader(cardId: String): String
}

View file

@ -5,9 +5,7 @@ import com.tangem.datasource.api.utils.ReadTimeout
import com.tangem.datasource.api.visa.models.request.ActivationByCardWalletRequest
import com.tangem.datasource.api.visa.models.request.ActivationByCustomerWalletRequest
import com.tangem.datasource.api.visa.models.request.SetPinCodeRequest
import com.tangem.datasource.api.visa.models.response.CardActivationRemoteStateResponse
import com.tangem.datasource.api.visa.models.response.CardWalletDataToSignResponse
import com.tangem.datasource.api.visa.models.response.CustomerWalletDataToSignResponse
import com.tangem.datasource.api.visa.models.response.*
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.Header
@ -71,4 +69,19 @@ interface TangemVisaApi {
@Header("Authorization") authHeader: String,
@Body body: SetPinCodeRequest,
): ApiResponse<Unit>
@GET("customer/info")
suspend fun getCustomerInfo(
@Header("Authorization") authHeader: String,
@Query("card_id") cardId: String,
): ApiResponse<VisaCustomerInfo>
@GET("product_instance/transactions")
suspend fun getTxHistory(
@Header("Authorization") authHeader: String,
@Query("customer_id") customerId: String,
@Query("product_instance_id") productInstanceId: String,
@Query("limit") limit: Int,
@Query("offset") offset: Int,
): ApiResponse<VisaTxHistoryResponse>
}

View file

@ -1,5 +1,6 @@
package com.tangem.datasource.api.visa
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.visa.models.response.GenerateNonceResponse
import com.tangem.datasource.api.visa.models.response.JWTResponse
import retrofit2.http.Field
@ -27,5 +28,5 @@ interface TangemVisaAuthApi {
): JWTResponse
@POST("auth/refresh_token")
suspend fun refreshAccessToken(@Field("refresh_token") refreshToken: String): JWTResponse
suspend fun refreshAccessToken(@Field("refresh_token") refreshToken: String): ApiResponse<JWTResponse>
}

View file

@ -0,0 +1,15 @@
package com.tangem.datasource.api.visa.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class VisaCustomerInfo(
@Json(name = "payment_accounts") val paymentAccounts: List<PaymentAccount>,
) {
data class PaymentAccount(
@Json(name = "id") val id: String,
@Json(name = "customer_wallet_address") val customerWalletAddress: String,
@Json(name = "payment_account_address") val paymentAccountAddress: String,
)
}

View file

@ -1,4 +1,4 @@
package com.tangem.lib.visa.model
package com.tangem.datasource.api.visa.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass

View file

@ -1,25 +0,0 @@
package com.tangem.data.visa.di
import com.tangem.data.common.cache.CacheRegistry
import com.tangem.data.visa.DefaultVisaRepository
import com.tangem.data.visa.config.VisaLibLoader
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
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal interface ImplementedVisaDataModule {
@Binds
@Singleton
@ImplementedVisaRepository
fun provideVisaRepository(impl: DefaultVisaRepository): VisaRepository
}

View file

@ -1,5 +1,6 @@
package com.tangem.data.visa
import com.tangem.data.visa.config.VisaLibLoader
import com.tangem.data.visa.converter.AccessCodeDataConverter
import com.tangem.data.visa.converter.VisaActivationStatusConverter
import com.tangem.datasource.api.common.response.ApiResponseError
@ -28,6 +29,7 @@ internal class DefaultVisaActivationRepository @AssistedInject constructor(
private val visaAuthTokenStorage: VisaAuthTokenStorage,
private val accessCodeDataConverter: AccessCodeDataConverter,
private val visaAuthRepository: VisaAuthRepository,
private val visaLibLoader: VisaLibLoader,
) : VisaActivationRepository {
override suspend fun getActivationRemoteState(): VisaActivationRemoteState = withContext(dispatcherProvider.io) {
@ -192,6 +194,10 @@ internal class DefaultVisaActivationRepository @AssistedInject constructor(
}
}
override suspend fun getPinCodeRsaEncryptionPublicKey(): String {
return visaLibLoader.getOrCreateConfig().rsaPublicKey
}
private suspend fun <T : Any> request(requestBlock: suspend () -> T): T {
return runCatching {
requestBlock()

View file

@ -12,21 +12,18 @@ 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.api.visa.TangemVisaApi
import com.tangem.datasource.api.visa.models.response.VisaTxHistoryResponse
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.common.util.cardTypesResolver
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.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.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
@ -35,8 +32,8 @@ import kotlinx.coroutines.withContext
import java.math.BigDecimal
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.jvm.Throws
@Suppress("LongParameterList")
@Singleton
internal class DefaultVisaRepository @Inject constructor(
private val visaLibLoader: VisaLibLoader,
@ -44,8 +41,8 @@ internal class DefaultVisaRepository @Inject constructor(
private val cacheRegistry: CacheRegistry,
private val userWalletsStore: UserWalletsStore,
private val dispatchers: CoroutineDispatcherProvider,
private val visaAuthProvider: TangemVisaAuthProvider,
private val visaAuthRepository: VisaAuthRepository,
private val visaApiRequestMaker: VisaApiRequestMaker,
private val visaApi: TangemVisaApi,
) : VisaRepository {
private val currencyFactory by lazy(mode = LazyThreadSafetyMode.NONE) {
@ -65,27 +62,32 @@ internal class DefaultVisaRepository @Inject constructor(
override suspend fun getVisaCurrency(userWalletId: UserWalletId, isRefresh: Boolean): VisaCurrency {
val address = makeAddress(userWalletId)
fetchVisaCurrencyIfExpired(address, isRefresh)
fetchVisaCurrencyIfExpired(userWalletId, address, isRefresh)
return requireNotNull(fetchedCurrencies.value[address]) {
"Unable to find VISA currency for $address"
}
}
private suspend fun fetchVisaCurrencyIfExpired(address: String, isRefresh: Boolean) {
private suspend fun fetchVisaCurrencyIfExpired(userWalletId: UserWalletId, address: String, isRefresh: Boolean) {
cacheRegistry.invokeOnExpire(
key = getVisaCurrencyKey(address),
skipCache = isRefresh,
block = { fetchVisaCurrency(address) },
block = { fetchVisaCurrency(userWalletId, address) },
)
}
private suspend fun fetchVisaCurrency(address: String) {
private suspend fun fetchVisaCurrency(userWalletId: UserWalletId, address: String) {
val contractInfoProvider = visaLibLoader.getOrCreateProvider()
parZip(
dispatchers.io,
{ contractInfoProvider.getContractInfo(address) },
{
contractInfoProvider.getContractInfo(
walletAddress = address,
paymentAccountAddress = getPaymentAccountAddress(userWalletId),
)
},
{ getFiatRate() },
{ contractInfo, fiatRate ->
fetchedCurrencies.update { value ->
@ -104,7 +106,7 @@ internal class DefaultVisaRepository @Inject constructor(
): Flow<PagingData<VisaTxHistoryItem>> {
val userWallet = findVisaUserWallet(userWalletId)
val cardPubKey = getCardPubKey(userWallet)
val api = visaLibLoader.getOrCreateApi()
val pager = Pager(
config = PagingConfig(
pageSize = pageSize,
@ -121,7 +123,7 @@ internal class DefaultVisaRepository @Inject constructor(
cacheRegistry = cacheRegistry,
fetchedItems = fetchedHistoryItems,
dispatchers = dispatchers,
requestTxHistory = { offset, pageSize -> getTxHistory(api, userWalletId, offset, pageSize) },
requestTxHistory = { offset, pageSize -> getTxHistory(userWalletId, offset, pageSize) },
)
},
)
@ -145,22 +147,31 @@ internal class DefaultVisaRepository @Inject constructor(
}
}
private suspend fun getTxHistory(
api: VisaApi,
userWalletId: UserWalletId,
offset: Int,
pageSize: Int,
): VisaTxHistoryResponse = withContext(dispatchers.io) {
private suspend fun getPaymentAccountAddress(userWalletId: UserWalletId): String? = runCatching {
val userWallet = findVisaUserWallet(userWalletId)
val cardPubKey = getCardPubKey(userWallet)
request(userWalletId = userWalletId) {
api.getTxHistory(
authorizationHeader = visaAuthProvider.getAuthHeader(userWallet.cardId),
cardPublicKey = cardPubKey,
val customerInfo = visaApiRequestMaker.request(userWalletId) { authHeader, _ ->
visaApi.getCustomerInfo(
authHeader = authHeader,
cardId = userWallet.scanResponse.card.cardId,
)
}
// TODO select correct account when multiple accounts are available (will be implemented when backend is ready)
customerInfo.paymentAccounts.firstOrNull()?.paymentAccountAddress
}.getOrNull()
private suspend fun getTxHistory(userWalletId: UserWalletId, offset: Int, pageSize: Int): VisaTxHistoryResponse {
return visaApiRequestMaker.request(
userWalletId = userWalletId,
) { authHeader, accessCodeData ->
visaApi.getTxHistory(
authHeader = authHeader,
customerId = accessCodeData.customerId,
productInstanceId = accessCodeData.productInstanceId,
limit = pageSize,
offset = offset,
).getOrThrow()
)
}
}
@ -217,56 +228,4 @@ internal class DefaultVisaRepository @Inject constructor(
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

@ -45,6 +45,10 @@ class MockVisaActivationRepository @AssistedInject constructor(
override suspend fun sendPinCode(pinCode: VisaEncryptedPinCode) {}
override suspend fun getPinCodeRsaEncryptionPublicKey(): String {
return CryptoUtils.generateRandomBytes(length = 32).toHexString()
}
@AssistedFactory
interface Factory : VisaActivationRepository.Factory {
override fun create(cardId: VisaCardId): MockVisaActivationRepository

View file

@ -7,8 +7,9 @@ import com.tangem.domain.visa.model.VisaTxHistoryItem
import com.tangem.domain.visa.repository.VisaRepository
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject
internal class DummyVisaRepository : VisaRepository {
internal class MockVisaRepository @Inject constructor() : VisaRepository {
override suspend fun getVisaCurrency(userWalletId: UserWalletId, isRefresh: Boolean): VisaCurrency {
TODO("Not implemented for this build type")

View file

@ -11,6 +11,8 @@ internal data class VisaConfig(
val mainnet: Addresses,
@Json(name = "txHistoryAPIAdditionalHeaders")
val header: Header,
@Json(name = "rsaPublicKey")
val rsaPublicKey: String,
) {
@JsonClass(generateAdapter = true)

View file

@ -1,13 +1,9 @@
package com.tangem.data.visa.config
import com.squareup.moshi.Moshi
import com.tangem.data.visa.BuildConfig
import com.tangem.data.visa.utils.VisaConstants
import com.tangem.datasource.asset.loader.AssetLoader
import com.tangem.datasource.di.NetworkMoshi
import com.tangem.lib.visa.VisaContractInfoProvider
import com.tangem.lib.visa.api.VisaApi
import com.tangem.lib.visa.api.VisaApiBuilder
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
@ -15,21 +11,19 @@ import javax.inject.Inject
internal class VisaLibLoader @Inject constructor(
private val assetLoader: AssetLoader,
@NetworkMoshi private val moshi: Moshi,
private val dispatchers: CoroutineDispatcherProvider,
) {
private val createMutex = Mutex()
private val createMutex2 = Mutex()
private var config: VisaConfig? = null
private var provider: VisaContractInfoProvider? = null
private var api: VisaApi? = null
suspend fun getOrCreateConfig(): VisaConfig = config ?: getOrLoadConfig()
suspend fun getOrCreateProvider(): VisaContractInfoProvider = provider ?: createProvider()
suspend fun getOrCreateApi(): VisaApi = api ?: createApi()
private suspend fun createProvider(): VisaContractInfoProvider = createMutex.withLock {
val config = getOrLoadConfig()
@ -54,24 +48,7 @@ internal class VisaLibLoader @Inject constructor(
}
}
private suspend fun createApi(): VisaApi = createMutex.withLock {
val config = getOrLoadConfig()
api = VisaApiBuilder(
useDevApi = VisaConstants.USE_TEST_ENV,
isNetworkLoggingEnabled = BuildConfig.LOG_ENABLED,
moshi = moshi,
headers = mapOf(
X_ASN_HEADER_NAME to config.header.xAsn,
),
).build()
return requireNotNull(api) {
"Visa API is not created"
}
}
private suspend fun getOrLoadConfig(): VisaConfig {
private suspend fun getOrLoadConfig(): VisaConfig = createMutex2.withLock {
config = assetLoader.load<VisaConfig>(VISA_CONFIG_FILE_NAME)
return requireNotNull(config) {
@ -81,6 +58,5 @@ internal class VisaLibLoader @Inject constructor(
companion object {
private const val VISA_CONFIG_FILE_NAME = "tangem-app-config/visa_config"
private const val X_ASN_HEADER_NAME = "x-asn"
}
}

View file

@ -1,6 +0,0 @@
package com.tangem.data.visa.di
import javax.inject.Qualifier
@Qualifier
internal annotation class ImplementedVisaRepository

View file

@ -1,16 +0,0 @@
package com.tangem.data.visa.di
import com.tangem.domain.visa.repository.VisaRepository
import dagger.BindsOptionalOf
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
@Module
@InstallIn(SingletonComponent::class)
internal interface ImplementedVisaRepositoryModule {
@BindsOptionalOf
@ImplementedVisaRepository
fun bindImplementedVisaRepository(): VisaRepository
}

View file

@ -1,36 +1,20 @@
package com.tangem.data.visa.di
import com.tangem.data.visa.DefaultVisaAuthRepository
import com.tangem.data.visa.DummyVisaRepository
import com.tangem.data.visa.MockVisaRepository
import com.tangem.data.visa.MockVisaActivationRepository
import com.tangem.domain.visa.repository.VisaActivationRepository
import com.tangem.domain.visa.repository.VisaAuthRepository
import com.tangem.domain.visa.repository.VisaRepository
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import java.util.Optional
import javax.inject.Singleton
import kotlin.jvm.optionals.getOrNull
@Module
@InstallIn(SingletonComponent::class)
internal object VisaDataModule {
@Provides
@Singleton
fun provideVisaRepository(
@ImplementedVisaRepository implementedVisaRepository: Optional<VisaRepository>,
): VisaRepository {
return implementedVisaRepository.getOrNull() ?: DummyVisaRepository()
}
}
@Module
@InstallIn(SingletonComponent::class)
internal interface VisaDataBindsModule {
internal interface VisaDataModule {
@Binds
@Singleton
@ -48,4 +32,11 @@ internal interface VisaDataBindsModule {
fun bindVisaActivationRepositoryFactory(
repository: MockVisaActivationRepository.Factory,
): VisaActivationRepository.Factory
// @Binds
// fun bindVisaRepository(repository: DefaultVisaRepository): VisaRepository
// Mocked
@Binds
fun bindVisaRepository(repository: MockVisaRepository): VisaRepository
}

View file

@ -0,0 +1,107 @@
package com.tangem.data.visa.utils
import com.tangem.data.visa.converter.AccessCodeDataConverter
import com.tangem.data.visa.model.AccessCodeData
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.common.response.ApiResponseError
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.visa.TangemVisaAuthApi
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.visa.exception.RefreshTokenExpiredException
import com.tangem.domain.visa.model.VisaAuthTokens
import com.tangem.domain.visa.model.VisaCardActivationStatus
import com.tangem.domain.visa.model.getAuthHeader
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext
import javax.inject.Inject
import kotlin.jvm.Throws
typealias VisaAuthorizationHeader = String
internal class VisaApiRequestMaker @Inject constructor(
private val userWalletsStore: UserWalletsStore,
private val visaAuthApi: TangemVisaAuthApi,
private val accessCodeDataConverter: AccessCodeDataConverter,
private val dispatcherProvider: CoroutineDispatcherProvider,
) {
suspend fun <T : Any> request(
userWalletId: UserWalletId,
requestBlock: suspend (header: VisaAuthorizationHeader, accessCodeData: AccessCodeData) -> ApiResponse<T>,
): T = withContext(dispatcherProvider.io) {
val authTokens = getAuthTokens(userWalletId)
val authHeader = authTokens.getAuthHeader()
val accessCodeData = accessCodeDataConverter.convert(authTokens)
runCatching {
requestBlock(authHeader, accessCodeData).getOrThrow()
}.getOrElse { responseError ->
if (responseError !is ApiResponseError.HttpException ||
responseError.code != ApiResponseError.HttpException.Code.UNAUTHORIZED
) {
throw responseError
}
val newTokens = runCatching {
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,
),
),
)
}
val newAuthHeader = newTokens.getAuthHeader()
val newAccessCodeData = accessCodeDataConverter.convert(newTokens)
requestBlock(newAuthHeader, newAccessCodeData).getOrThrow()
}
}
private suspend fun refreshAccessTokens(refreshToken: VisaAuthTokens.RefreshToken): VisaAuthTokens {
val result = visaAuthApi.refreshAccessToken(refreshToken.value).getOrThrow()
return VisaAuthTokens(
accessToken = result.accessToken,
refreshToken = VisaAuthTokens.RefreshToken(result.refreshToken),
)
}
@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")
}
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
}
}

View file

@ -15,9 +15,9 @@ internal object VisaConstants {
)
/*
* Must be `false` in production
* Don't forget to change CardTypesResolver.isVisaWallet
* */
* Must be `false` in production
* Don't forget to change CardTypesResolver.isVisaWallet
* */
const val IS_DEMO_MODE_ENABLED = false
const val USE_TEST_ENV = true
@ -40,5 +40,7 @@ internal fun getDemoAddress(): String {
internal fun getDemoPublicKey(): String {
return if (VisaConstants.USE_TEST_ENV) {
VisaConstants.DEMO_TESTNET_PUBLIC_KEY
} else VisaConstants.DEMO_MAINNET_PUBLIC_KEY
} else {
VisaConstants.DEMO_MAINNET_PUBLIC_KEY
}
}

View file

@ -2,8 +2,8 @@ package com.tangem.data.visa.utils
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.externallinkprovider.TxExploreState
import com.tangem.datasource.api.visa.models.response.VisaTxHistoryResponse
import com.tangem.domain.visa.model.VisaTxDetails
import com.tangem.lib.visa.model.VisaTxHistoryResponse
internal class VisaTxDetailsFactory {

View file

@ -1,7 +1,7 @@
package com.tangem.data.visa.utils
import com.tangem.datasource.api.visa.models.response.VisaTxHistoryResponse
import com.tangem.domain.visa.model.VisaTxHistoryItem
import com.tangem.lib.visa.model.VisaTxHistoryResponse
internal class VisaTxHistoryItemFactory {

View file

@ -3,13 +3,9 @@ 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.datasource.api.visa.models.response.VisaTxHistoryResponse
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
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update

View file

@ -18,8 +18,6 @@ import javax.crypto.spec.SecretKeySpec
private const val KEY_SIZE = 256
private const val RSA_PUB_KEY = "TODO" // TODO add RSA public key [REDACTED_TASK_KEY]
class SetVisaPinCodeUseCase(
private val visaActivationRepositoryFactory: VisaActivationRepository.Factory,
) {
@ -30,9 +28,10 @@ class SetVisaPinCodeUseCase(
pinCode: String,
): Either<Throwable, Unit> = Either.catch {
val visaActivationRepository = visaActivationRepositoryFactory.create(visaCardId)
val rsaPublicKey = visaActivationRepository.getPinCodeRsaEncryptionPublicKey()
val sessionKey = generateSessionKey()
val sessionId = getSessionId(RSA_PUB_KEY, sessionKey)
val sessionId = getSessionId(rsaPublicKey, sessionKey)
val secureRandom = SecureRandom()
val iv = ByteArray(size = 16)

View file

@ -20,6 +20,8 @@ interface VisaActivationRepository {
suspend fun sendPinCode(pinCode: VisaEncryptedPinCode)
suspend fun getPinCodeRsaEncryptionPublicKey(): String
interface Factory {
fun create(cardId: VisaCardId): VisaActivationRepository
}

View file

@ -20,27 +20,38 @@ internal class DefaultVisaContractInfoProvider(
private val dispatchers: CoroutineDispatcherProvider,
) : VisaContractInfoProvider {
override suspend fun getContractInfo(walletAddress: String): VisaContractInfo {
override suspend fun getContractInfo(walletAddress: String, paymentAccountAddress: String?): VisaContractInfo {
return parZip(
dispatchers.io,
{ loadPaymentAccount(walletAddress) },
{ loadPaymentAccount(walletAddress = walletAddress, paymentAccountAddress = paymentAccountAddress) },
{ loadPaymentTokenInfo() },
{ paymentAccount, paymentToken ->
fetchBalancesAndLimits(paymentAccount, paymentToken)
fetchBalancesAndLimits(
paymentAccount = paymentAccount,
paymentToken = paymentToken,
walletAddress = walletAddress,
)
},
)
}
private fun loadPaymentAccount(walletAddress: String): TangemPaymentAccount {
private fun loadPaymentAccount(walletAddress: String, paymentAccountAddress: String?): TangemPaymentAccount {
return TangemPaymentAccount.load(
/* contractAddress = */ paymentAccountAddress ?: getPaymentAccountAddressFromRegistry(walletAddress),
/* web3j = */ web3j,
/* transactionManager = */ transactionManager,
/* contractGasProvider = */ gasProvider,
)
}
private fun getPaymentAccountAddressFromRegistry(walletAddress: String): String {
val paymentAccountRegistry = TangemPaymentAccountRegistry.load(
/* contractAddress = */ paymentAccountRegistryAddress,
/* web3j = */ web3j,
/* transactionManager = */ transactionManager,
/* contractGasProvider = */ gasProvider,
)
val paymentAccountAddress = paymentAccountRegistry.paymentAccountByCard(walletAddress).send()
return TangemPaymentAccount.load(paymentAccountAddress, web3j, transactionManager, gasProvider)
return paymentAccountRegistry.paymentAccountByCard(walletAddress).send()
}
private fun loadPaymentTokenInfo(): PaymentTokenInfo {
@ -63,11 +74,12 @@ internal class DefaultVisaContractInfoProvider(
private suspend fun fetchBalancesAndLimits(
paymentAccount: TangemPaymentAccount,
paymentToken: PaymentTokenInfo,
walletAddress: String,
): VisaContractInfo = parZip(
dispatchers.io,
{ fetchToken(paymentAccount) },
{ fetchBalances(paymentAccount, paymentToken) },
{ fetchLimits(paymentAccount, paymentToken) },
{ fetchLimits(paymentAccount, paymentToken, walletAddress) },
{ token, balances, (oldLimit, newLimit, changeDate) ->
VisaContractInfo(token, balances, oldLimit, newLimit, changeDate)
},
@ -123,19 +135,15 @@ internal class DefaultVisaContractInfoProvider(
private fun fetchLimits(
paymentAccount: TangemPaymentAccount,
paymentToken: PaymentTokenInfo,
walletAddress: String,
): Triple<Limits, Limits, Instant> {
TODO() // TODO: [REDACTED_TASK_KEY]
// val (
// oldLimit,
// newLimit,
// changeDateSeconds,
// ) = paymentAccount.cards("").send().component5()
//
// return Triple(
// first = getLimits(oldLimit, paymentToken),
// second = getLimits(newLimit, paymentToken),
// third = changeDateSeconds.toInstant(),
// )
val limits = paymentAccount.cards(walletAddress).send().component5()
return Triple(
first = getLimits(limits.oldValue, paymentToken),
second = getLimits(limits.newValue, paymentToken),
third = limits.changeTimestamp.toInstant(),
)
}
@Suppress("UnusedPrivateMember")

View file

@ -21,7 +21,14 @@ import java.util.concurrent.TimeUnit
interface VisaContractInfoProvider {
suspend fun getContractInfo(walletAddress: String): VisaContractInfo
/**
* Fetches Visa contract info for the given wallet address.
*
* @param walletAddress Wallet address to fetch contract info for.
* @param paymentAccountAddress Payment account address to fetch data from. If null,
* it will be fetched from the registry.
*/
suspend fun getContractInfo(walletAddress: String, paymentAccountAddress: String?): VisaContractInfo
class Builder(
private val useTestnetRpc: Boolean,

View file

@ -1,18 +0,0 @@
package com.tangem.lib.visa.api
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.lib.visa.model.VisaTxHistoryResponse
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.Query
interface VisaApi {
@GET("transaction")
suspend fun getTxHistory(
@Header("Authorization") authorizationHeader: String,
@Query("card_public_key") cardPublicKey: String,
@Query("limit") limit: Int,
@Query("offset") offset: Int,
): ApiResponse<VisaTxHistoryResponse>
}

View file

@ -1,72 +0,0 @@
package com.tangem.lib.visa.api
import android.util.Log
import com.ihsanbal.logging.Level
import com.ihsanbal.logging.LoggingInterceptor
import com.squareup.moshi.Moshi
import com.tangem.datasource.api.common.response.ApiResponseCallAdapterFactory
import com.tangem.lib.visa.utils.Constants
import com.tangem.lib.visa.utils.Constants.NETWORK_LOGS_TAG
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import java.util.concurrent.TimeUnit
class VisaApiBuilder(
private val useDevApi: Boolean,
private val isNetworkLoggingEnabled: Boolean,
private val moshi: Moshi,
private val headers: Map<String, String>,
private val networkTimeoutSeconds: Long = Constants.NETWORK_TIMEOUT_SECONDS,
) {
fun build(): VisaApi {
val okHttpClient = createOkHttpClient()
val retrofit = createRetrofit(okHttpClient)
return retrofit.create(VisaApi::class.java)
}
private fun createOkHttpClient(): OkHttpClient {
val builder = OkHttpClient.Builder().apply {
connectTimeout(networkTimeoutSeconds, TimeUnit.SECONDS)
readTimeout(networkTimeoutSeconds, TimeUnit.SECONDS)
writeTimeout(networkTimeoutSeconds, TimeUnit.SECONDS)
if (isNetworkLoggingEnabled) {
addInterceptor(createNetworkLoggingInterceptor())
}
if (headers.isNotEmpty()) {
addInterceptor { chain ->
val request = chain.request().newBuilder().apply {
headers.forEach { (key, value) -> addHeader(key, value) }
}.build()
chain.proceed(request)
}
}
}
return builder.build()
}
private fun createRetrofit(okHttpClient: OkHttpClient): Retrofit {
val baseUrl = if (useDevApi) Constants.VISA_API_DEV_URL else Constants.VISA_API_PROD_URL
return Retrofit.Builder()
.addConverterFactory(MoshiConverterFactory.create(moshi))
.addCallAdapterFactory(ApiResponseCallAdapterFactory.create())
.baseUrl(baseUrl)
.client(okHttpClient)
.build()
}
}
private fun createNetworkLoggingInterceptor(): Interceptor {
return LoggingInterceptor.Builder()
.setLevel(Level.BODY)
.log(Log.VERBOSE)
.tag(NETWORK_LOGS_TAG)
.build()
}