Updated on 2026-08-14
This commit is contained in:
parent
330ae73357
commit
e7efe7b76a
4636 changed files with 234864 additions and 63507 deletions
1
data/visa/.gitignore
vendored
Normal file
1
data/visa/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
52
data/visa/build.gradle.kts
Normal file
52
data/visa/build.gradle.kts
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import com.tangem.plugin.configuration.configurations.extension.kaptForObfuscatingVariants
|
||||
|
||||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
alias(deps.plugins.kotlin.kapt)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.tangem.data.visa"
|
||||
}
|
||||
|
||||
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)
|
||||
implementation(projects.libs.blockchainSdk)
|
||||
|
||||
/** Project - Libs */
|
||||
debugImplementation(projects.libs.visa)
|
||||
|
||||
/** Libs - Other */
|
||||
implementation(deps.kotlin.coroutines)
|
||||
implementation(deps.arrow.core)
|
||||
implementation(deps.arrow.fx)
|
||||
implementation(deps.jodatime)
|
||||
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(tangemDeps.blockchain)
|
||||
implementation(tangemDeps.card.core)
|
||||
|
||||
/** DI */
|
||||
implementation(deps.hilt.core)
|
||||
kapt(deps.hilt.kapt)
|
||||
}
|
||||
|
|
@ -0,0 +1,272 @@
|
|||
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.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.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
|
||||
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
|
||||
|
||||
@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) {
|
||||
VisaCurrencyFactory()
|
||||
}
|
||||
private val txDetailsFactory by lazy(mode = LazyThreadSafetyMode.NONE) {
|
||||
VisaTxDetailsFactory()
|
||||
}
|
||||
|
||||
private val fetchedCurrencies = MutableStateFlow(
|
||||
value = hashMapOf<String, VisaCurrency>(),
|
||||
)
|
||||
private val fetchedHistoryItems = MutableStateFlow(
|
||||
value = emptyMap<String, List<VisaTxHistoryResponse.Transaction>>(),
|
||||
)
|
||||
|
||||
override suspend fun getVisaCurrency(userWalletId: UserWalletId, isRefresh: Boolean): VisaCurrency {
|
||||
val address = makeAddress(userWalletId)
|
||||
|
||||
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 = getVisaCurrencyKey(address),
|
||||
skipCache = isRefresh,
|
||||
block = { fetchVisaCurrency(address) },
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun fetchVisaCurrency(address: String) {
|
||||
val contractInfoProvider = visaLibLoader.getOrCreateProvider()
|
||||
|
||||
parZip(
|
||||
dispatchers.io,
|
||||
{ contractInfoProvider.getContractInfo(address) },
|
||||
{ getFiatRate() },
|
||||
{ contractInfo, fiatRate ->
|
||||
fetchedCurrencies.update { value ->
|
||||
value.apply {
|
||||
put(address, currencyFactory.create(contractInfo, fiatRate))
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getTxHistory(
|
||||
userWalletId: UserWalletId,
|
||||
pageSize: Int,
|
||||
isRefresh: Boolean,
|
||||
): Flow<PagingData<VisaTxHistoryItem>> {
|
||||
val userWallet = findVisaUserWallet(userWalletId)
|
||||
val cardPubKey = getCardPubKey(userWallet)
|
||||
val api = visaLibLoader.getOrCreateApi()
|
||||
val pager = Pager(
|
||||
config = PagingConfig(
|
||||
pageSize = pageSize,
|
||||
initialLoadSize = pageSize,
|
||||
),
|
||||
pagingSourceFactory = {
|
||||
VisaTxHistoryPagingSource(
|
||||
params = VisaTxHistoryPagingSource.Params(
|
||||
cardPublicKey = cardPubKey,
|
||||
pageSize = pageSize,
|
||||
isRefresh = isRefresh,
|
||||
userWallet = userWallet,
|
||||
),
|
||||
cacheRegistry = cacheRegistry,
|
||||
fetchedItems = fetchedHistoryItems,
|
||||
dispatchers = dispatchers,
|
||||
requestTxHistory = { offset, pageSize -> getTxHistory(api, userWalletId, offset, pageSize) },
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
return pager.flow
|
||||
}
|
||||
|
||||
override suspend fun getTxDetails(userWalletId: UserWalletId, txId: String): VisaTxDetails {
|
||||
return withContext(dispatchers.io) {
|
||||
val userWallet = findVisaUserWallet(userWalletId)
|
||||
val cardPubKey = getCardPubKey(userWallet)
|
||||
val transaction = fetchedHistoryItems.value[cardPubKey]?.firstOrNull {
|
||||
it.transactionId.toString() == txId
|
||||
}
|
||||
requireNotNull(transaction) { "Transaction not found: $txId" }
|
||||
|
||||
txDetailsFactory.create(
|
||||
transaction = transaction,
|
||||
walletBlockchain = userWallet.scanResponse.cardTypesResolver.getBlockchain(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
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 suspend fun getFiatRate(): BigDecimal? {
|
||||
val fiatCurrencyId = VisaConstants.fiatCurrency.code.lowercase()
|
||||
val quotes = tangemTechApi.getQuotes(
|
||||
currencyId = fiatCurrencyId,
|
||||
coinIds = VisaConstants.TOKEN_ID,
|
||||
).getOrThrow()
|
||||
|
||||
return quotes.quotes[VisaConstants.TOKEN_ID]?.price
|
||||
}
|
||||
|
||||
private fun makeWalletAddresses(userWallet: UserWallet): Set<Address> {
|
||||
val walletBlockchain = userWallet.scanResponse.cardTypesResolver.getBlockchain()
|
||||
|
||||
return walletBlockchain.makeAddresses(getCardPubKey(userWallet).hexToBytes())
|
||||
}
|
||||
|
||||
private fun getCardPubKey(userWallet: UserWallet): String {
|
||||
if (VisaConstants.IS_DEMO_MODE_ENABLED) return getDemoPublicKey()
|
||||
|
||||
val cardWallet = userWallet.scanResponse.card.wallets.firstOrNull {
|
||||
it.curve == EllipticCurve.Secp256k1
|
||||
}
|
||||
requireNotNull(cardWallet) { "Visa card wallet not found" }
|
||||
|
||||
return cardWallet.publicKey.toHexString()
|
||||
}
|
||||
|
||||
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 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")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
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,
|
||||
@Json(name = "mainnet")
|
||||
val mainnet: Addresses,
|
||||
@Json(name = "txHistoryAPIAdditionalHeaders")
|
||||
val header: Header,
|
||||
) {
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Addresses(
|
||||
@Json(name = "paymentAccountRegistry")
|
||||
val paymentAccountRegistry: String,
|
||||
@Json(name = "bridgeProcessor")
|
||||
val bridgeProcessor: String,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Header(
|
||||
@Json(name = "x-asn")
|
||||
val xAsn: String,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
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
|
||||
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 var config: VisaConfig? = null
|
||||
|
||||
private var provider: VisaContractInfoProvider? = null
|
||||
private var api: VisaApi? = null
|
||||
|
||||
suspend fun getOrCreateProvider(): VisaContractInfoProvider = provider ?: createProvider()
|
||||
|
||||
suspend fun getOrCreateApi(): VisaApi = api ?: createApi()
|
||||
|
||||
private suspend fun createProvider(): VisaContractInfoProvider = createMutex.withLock {
|
||||
val config = getOrLoadConfig()
|
||||
|
||||
provider = VisaContractInfoProvider.Builder(
|
||||
useTestnetRpc = VisaConstants.USE_TEST_ENV,
|
||||
bridgeProcessorAddress = if (VisaConstants.USE_TEST_ENV) {
|
||||
config.testnet.bridgeProcessor
|
||||
} else {
|
||||
config.mainnet.bridgeProcessor
|
||||
},
|
||||
paymentAccountRegistryAddress = if (VisaConstants.USE_TEST_ENV) {
|
||||
config.testnet.paymentAccountRegistry
|
||||
} else {
|
||||
config.mainnet.paymentAccountRegistry
|
||||
},
|
||||
isNetworkLoggingEnabled = BuildConfig.LOG_ENABLED,
|
||||
dispatchers = dispatchers,
|
||||
).build()
|
||||
|
||||
return requireNotNull(provider) {
|
||||
"Visa provider is not created"
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
config = assetLoader.load<VisaConfig>(VISA_CONFIG_FILE_NAME)
|
||||
|
||||
return requireNotNull(config) {
|
||||
"Visa config is not found"
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val VISA_CONFIG_FILE_NAME = "tangem-app-config/visa_config"
|
||||
private const val X_ASN_HEADER_NAME = "x-asn"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
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
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package com.tangem.data.visa.utils
|
||||
|
||||
import java.util.Currency
|
||||
|
||||
internal fun findCurrencyByNumericCode(code: Int) =
|
||||
Currency.getAvailableCurrencies().firstOrNull { it.numericCode == code }
|
||||
?: Currency.getInstance(VisaConstants.fiatCurrency.code)
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
package com.tangem.data.visa.utils
|
||||
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
|
||||
internal object VisaConstants {
|
||||
|
||||
const val NETWORK_NAME = "Polygon PoS"
|
||||
|
||||
const val TOKEN_ID = "tether"
|
||||
|
||||
val fiatCurrency = AppCurrency(
|
||||
code = "EUR",
|
||||
name = "Euro",
|
||||
symbol = "€",
|
||||
)
|
||||
|
||||
/*
|
||||
* 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
|
||||
|
||||
const val DEMO_TESTNET_ADDRESS = "0x51d034eb1563d0d2e66379ef37756d3c14936c44"
|
||||
const val DEMO_TESTNET_PUBLIC_KEY = "03FA1122B809079F79C4E0F657FE11337FEC88C3FB3C6341B2CE2E4F5D9241DD86"
|
||||
|
||||
const val DEMO_MAINNET_ADDRESS = "0x927e3ef2b3d85bacf9e520379f64f6627d323fcd"
|
||||
const val DEMO_MAINNET_PUBLIC_KEY = "02AC61CD57B8011BEE8BB489FB744845CC113AD379132C56015EE70528B6A88E92"
|
||||
}
|
||||
|
||||
internal fun getDemoAddress(): String {
|
||||
return if (VisaConstants.USE_TEST_ENV) {
|
||||
VisaConstants.DEMO_TESTNET_ADDRESS
|
||||
} else {
|
||||
VisaConstants.DEMO_MAINNET_ADDRESS
|
||||
}
|
||||
}
|
||||
|
||||
internal fun getDemoPublicKey(): String {
|
||||
return if (VisaConstants.USE_TEST_ENV) {
|
||||
VisaConstants.DEMO_TESTNET_PUBLIC_KEY
|
||||
} else VisaConstants.DEMO_MAINNET_PUBLIC_KEY
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
package com.tangem.data.visa.utils
|
||||
|
||||
import com.tangem.domain.visa.model.VisaCurrency
|
||||
import com.tangem.lib.visa.model.VisaContractInfo
|
||||
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(contractInfo: VisaContractInfo, fiatRate: BigDecimal?): VisaCurrency {
|
||||
val now = Instant.now()
|
||||
val currentLimit = if (contractInfo.limitsChangeDate > now) {
|
||||
contractInfo.oldLimits
|
||||
} else {
|
||||
contractInfo.newLimits
|
||||
}
|
||||
val remainingOtpLimit = getRemainingOtp(currentLimit, now)
|
||||
|
||||
return VisaCurrency(
|
||||
symbol = contractInfo.token.symbol,
|
||||
networkName = VisaConstants.NETWORK_NAME,
|
||||
decimals = contractInfo.token.decimals,
|
||||
fiatRate = fiatRate,
|
||||
fiatCurrency = VisaConstants.fiatCurrency,
|
||||
balances = with(contractInfo) {
|
||||
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 = remainingOtpLimit,
|
||||
remainingNoOtp = minOf(remainingOtpLimit, getRemainingNoOtp(currentLimit, now)),
|
||||
singleTransaction = currentLimit.singleTransactionLimit,
|
||||
expirationDate = getLimitsExpirationDate(currentLimit, now),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun getRemainingOtp(currentLimit: VisaContractInfo.Limits, now: Instant): BigDecimal {
|
||||
if (currentLimit.expirationDate >= now) {
|
||||
return currentLimit.spendLimit.limit - currentLimit.spendLimit.spent
|
||||
}
|
||||
|
||||
return currentLimit.spendLimit.limit
|
||||
}
|
||||
|
||||
private fun getRemainingNoOtp(currentLimit: VisaContractInfo.Limits, now: Instant): BigDecimal {
|
||||
if (currentLimit.expirationDate >= now) {
|
||||
return currentLimit.noOtpLimit.limit - currentLimit.noOtpLimit.spent
|
||||
}
|
||||
|
||||
return currentLimit.noOtpLimit.limit
|
||||
}
|
||||
|
||||
private fun getLimitsExpirationDate(currentLimits: VisaContractInfo.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
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
package com.tangem.data.visa.utils
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.externallinkprovider.TxExploreState
|
||||
import com.tangem.domain.visa.model.VisaTxDetails
|
||||
import com.tangem.lib.visa.model.VisaTxHistoryResponse
|
||||
|
||||
internal class VisaTxDetailsFactory {
|
||||
|
||||
fun create(transaction: VisaTxHistoryResponse.Transaction, walletBlockchain: Blockchain): VisaTxDetails {
|
||||
return VisaTxDetails(
|
||||
id = transaction.transactionId.toString(),
|
||||
type = transaction.transactionType,
|
||||
status = transaction.transactionStatus,
|
||||
blockchainAmount = transaction.blockchainAmount,
|
||||
blockchainFee = transaction.blockchainFee,
|
||||
transactionAmount = transaction.transactionAmount,
|
||||
transactionCurrencyCode = transaction.transactionCurrencyCode,
|
||||
merchantName = transaction.merchantName,
|
||||
merchantCity = transaction.merchantCity,
|
||||
merchantCountryCode = transaction.merchantCountryCode,
|
||||
merchantCategoryCode = transaction.merchantCategoryCode,
|
||||
fiatCurrency = findCurrencyByNumericCode(transaction.transactionCurrencyCode),
|
||||
requests = transaction.requests.map { createRequest(it, walletBlockchain) },
|
||||
)
|
||||
}
|
||||
|
||||
private fun createRequest(
|
||||
request: VisaTxHistoryResponse.Transaction.Request,
|
||||
walletBlockchain: Blockchain,
|
||||
): VisaTxDetails.Request {
|
||||
return VisaTxDetails.Request(
|
||||
billingAmount = request.billingAmount,
|
||||
billingCurrencyCode = request.billingCurrencyCode,
|
||||
blockchainAmount = request.blockchainAmount,
|
||||
blockchainFee = request.blockchainFee,
|
||||
errorCode = request.errorCode,
|
||||
requestDate = request.requestDt,
|
||||
requestStatus = request.requestStatus,
|
||||
requestType = request.requestType,
|
||||
transactionAmount = request.transactionAmount,
|
||||
txCurrencyCode = request.transactionCurrencyCode,
|
||||
id = request.transactionRequestId.toString(),
|
||||
txHash = request.txHash,
|
||||
txStatus = request.txStatus,
|
||||
fiatCurrency = findCurrencyByNumericCode(request.transactionCurrencyCode),
|
||||
exploreUrl = request.txHash?.let {
|
||||
when (val txUrl = walletBlockchain.getExploreTxUrl(it)) {
|
||||
is TxExploreState.Url -> txUrl.url
|
||||
is TxExploreState.Unsupported -> ""
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
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
|
||||
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 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()
|
||||
|
||||
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 = requestTxHistory(offset, pageSize)
|
||||
|
||||
fetchedItems.update {
|
||||
it.toMutableMap().apply {
|
||||
this[cardPublicKey] = this[cardPublicKey].orEmpty() + response.transactions
|
||||
}
|
||||
}
|
||||
|
||||
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 userWallet: UserWallet,
|
||||
val cardPublicKey: String,
|
||||
val pageSize: Int,
|
||||
val isRefresh: Boolean,
|
||||
)
|
||||
|
||||
private companion object {
|
||||
const val INITIAL_OFFSET = 0
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,220 @@
|
|||
package com.tangem.data.visa
|
||||
|
||||
import com.tangem.data.visa.converter.AccessCodeDataConverter
|
||||
import com.tangem.data.visa.converter.VisaActivationStatusConverter
|
||||
import com.tangem.datasource.api.common.response.ApiResponseError
|
||||
import com.tangem.datasource.api.common.response.getOrThrow
|
||||
import com.tangem.datasource.api.visa.TangemVisaApi
|
||||
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.local.visa.VisaAuthTokenStorage
|
||||
import com.tangem.domain.visa.exception.RefreshTokenExpiredException
|
||||
import com.tangem.domain.visa.model.*
|
||||
import com.tangem.domain.visa.repository.VisaActivationRepository
|
||||
import com.tangem.domain.visa.repository.VisaAuthRepository
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
internal class DefaultVisaActivationRepository @AssistedInject constructor(
|
||||
@Assisted private val visaCardId: VisaCardId,
|
||||
private val visaApi: TangemVisaApi,
|
||||
private val dispatcherProvider: CoroutineDispatcherProvider,
|
||||
private val visaActivationStatusConverter: VisaActivationStatusConverter,
|
||||
private val visaAuthTokenStorage: VisaAuthTokenStorage,
|
||||
private val accessCodeDataConverter: AccessCodeDataConverter,
|
||||
private val visaAuthRepository: VisaAuthRepository,
|
||||
) : VisaActivationRepository {
|
||||
|
||||
override suspend fun getActivationRemoteState(): VisaActivationRemoteState = withContext(dispatcherProvider.io) {
|
||||
val result = request {
|
||||
val authTokens =
|
||||
checkNotNull(visaAuthTokenStorage.get(visaCardId.cardId)) { "Visa auth tokens are not stored" }
|
||||
val accessCodeData = accessCodeDataConverter.convert(authTokens)
|
||||
|
||||
visaApi.getRemoteActivationStatus(
|
||||
authHeader = authTokens.getAuthHeader(),
|
||||
customerId = accessCodeData.customerId,
|
||||
productInstanceId = accessCodeData.productInstanceId,
|
||||
cardId = visaCardId.cardId,
|
||||
cardPublicKey = visaCardId.cardPublicKey,
|
||||
).getOrThrow()
|
||||
}
|
||||
|
||||
visaActivationStatusConverter.convert(result)
|
||||
}
|
||||
|
||||
override suspend fun getActivationRemoteStateLongPoll(): VisaActivationRemoteState =
|
||||
withContext(dispatcherProvider.io) {
|
||||
val result = request {
|
||||
val authTokens =
|
||||
checkNotNull(visaAuthTokenStorage.get(visaCardId.cardId)) { "Visa auth tokens are not stored" }
|
||||
val accessCodeData = accessCodeDataConverter.convert(authTokens)
|
||||
|
||||
visaApi.getRemoteActivationStatusLongPoll(
|
||||
authHeader = authTokens.getAuthHeader(),
|
||||
customerId = accessCodeData.customerId,
|
||||
productInstanceId = accessCodeData.productInstanceId,
|
||||
cardId = visaCardId.cardId,
|
||||
cardPublicKey = visaCardId.cardPublicKey,
|
||||
).getOrThrow()
|
||||
}
|
||||
|
||||
visaActivationStatusConverter.convert(result)
|
||||
}
|
||||
|
||||
override suspend fun getCardWalletAcceptanceData(
|
||||
request: VisaCardWalletDataToSignRequest,
|
||||
): VisaDataToSignByCardWallet = withContext(dispatcherProvider.io) {
|
||||
val result = request {
|
||||
val authTokens =
|
||||
checkNotNull(visaAuthTokenStorage.get(visaCardId.cardId)) { "Visa auth tokens are not stored" }
|
||||
val accessCodeData = accessCodeDataConverter.convert(authTokens)
|
||||
|
||||
visaApi.getCardWalletAcceptance(
|
||||
authHeader = authTokens.getAuthHeader(),
|
||||
customerId = accessCodeData.customerId,
|
||||
productInstanceId = accessCodeData.productInstanceId,
|
||||
activationOrderId = request.orderId,
|
||||
customerWalletAddress = request.customerWalletAddress,
|
||||
).getOrThrow()
|
||||
}
|
||||
|
||||
VisaDataToSignByCardWallet(
|
||||
request = request,
|
||||
hashToSign = result.dataForCardWallet.hash,
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getCustomerWalletAcceptanceData(
|
||||
request: VisaCustomerWalletDataToSignRequest,
|
||||
): VisaDataToSignByCustomerWallet = withContext(dispatcherProvider.io) {
|
||||
val result = request {
|
||||
val authTokens =
|
||||
checkNotNull(visaAuthTokenStorage.get(visaCardId.cardId)) { "Visa auth tokens are not stored" }
|
||||
val accessCodeData = accessCodeDataConverter.convert(authTokens)
|
||||
|
||||
visaApi.getCustomerWalletAcceptance(
|
||||
authHeader = authTokens.getAuthHeader(),
|
||||
customerId = accessCodeData.customerId,
|
||||
productInstanceId = accessCodeData.productInstanceId,
|
||||
activationOrderId = request.orderId,
|
||||
cardWalletAddress = request.cardWalletAddress,
|
||||
).getOrThrow()
|
||||
}
|
||||
|
||||
VisaDataToSignByCustomerWallet(
|
||||
request = request,
|
||||
hashToSign = result.dataForCardWallet.hash,
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun activateCard(signedData: VisaSignedActivationDataByCardWallet) {
|
||||
withContext(dispatcherProvider.io) {
|
||||
request {
|
||||
val authTokens =
|
||||
checkNotNull(visaAuthTokenStorage.get(visaCardId.cardId)) { "Visa auth tokens are not stored" }
|
||||
val accessCodeData = accessCodeDataConverter.convert(authTokens)
|
||||
|
||||
visaApi.activateByCardWallet(
|
||||
authHeader = authTokens.getAuthHeader(),
|
||||
body = ActivationByCardWalletRequest(
|
||||
customerId = accessCodeData.customerId,
|
||||
productInstanceId = accessCodeData.productInstanceId,
|
||||
activationOrderId = signedData.dataToSign.request.orderId,
|
||||
data = ActivationByCardWalletRequest.Data(
|
||||
cardWallet = ActivationByCardWalletRequest.CardWallet(
|
||||
address = signedData.cardWalletAddress,
|
||||
cardWalletConfirmation = null, // for second iteration
|
||||
deployAcceptanceSignature = signedData.signature,
|
||||
),
|
||||
otp = ActivationByCardWalletRequest.Otp(
|
||||
rootOtp = signedData.rootOTP,
|
||||
counter = signedData.otpCounter,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun approveByCustomerWallet(signedData: VisaSignedDataByCustomerWallet) {
|
||||
withContext(dispatcherProvider.io) {
|
||||
request {
|
||||
val authTokens =
|
||||
checkNotNull(visaAuthTokenStorage.get(visaCardId.cardId)) { "Visa auth tokens are not stored" }
|
||||
val accessCodeData = accessCodeDataConverter.convert(authTokens)
|
||||
|
||||
visaApi.activateByCustomerWallet(
|
||||
authHeader = authTokens.getAuthHeader(),
|
||||
body = ActivationByCustomerWalletRequest(
|
||||
customerId = accessCodeData.customerId,
|
||||
productInstanceId = accessCodeData.productInstanceId,
|
||||
activationOrderId = signedData.dataToSign.request.orderId,
|
||||
data = ActivationByCustomerWalletRequest.Data(
|
||||
customerWallet = ActivationByCustomerWalletRequest.CustomerWallet(
|
||||
address = signedData.customerWalletAddress,
|
||||
deployAcceptanceSignature = signedData.signature,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun sendPinCode(pinCode: VisaEncryptedPinCode) {
|
||||
withContext(dispatcherProvider.io) {
|
||||
request {
|
||||
val authTokens =
|
||||
checkNotNull(visaAuthTokenStorage.get(visaCardId.cardId)) { "Visa auth tokens are not stored" }
|
||||
val accessCodeData = accessCodeDataConverter.convert(authTokens)
|
||||
|
||||
visaApi.setPinCode(
|
||||
authHeader = authTokens.getAuthHeader(),
|
||||
body = SetPinCodeRequest(
|
||||
customerId = accessCodeData.customerId,
|
||||
productInstanceId = accessCodeData.productInstanceId,
|
||||
activationOrderId = pinCode.activationOrderId,
|
||||
data = SetPinCodeRequest.Data(
|
||||
sessionKey = pinCode.sessionId,
|
||||
iv = pinCode.iv,
|
||||
encryptedPin = pinCode.encryptedPin,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun <T : Any> request(requestBlock: suspend () -> T): T {
|
||||
return runCatching {
|
||||
requestBlock()
|
||||
}.getOrElse { responseError ->
|
||||
if (responseError !is ApiResponseError.HttpException ||
|
||||
responseError.code != ApiResponseError.HttpException.Code.UNAUTHORIZED
|
||||
) {
|
||||
throw responseError
|
||||
}
|
||||
|
||||
val authTokens = visaAuthTokenStorage.get(visaCardId.cardId) ?: error("Auth tokens are not stored")
|
||||
val newTokens = runCatching {
|
||||
visaAuthRepository.refreshAccessTokens(authTokens.refreshToken)
|
||||
}.getOrElse { throw RefreshTokenExpiredException() }
|
||||
|
||||
visaAuthTokenStorage.store(visaCardId.cardId, newTokens)
|
||||
|
||||
requestBlock()
|
||||
}
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : VisaActivationRepository.Factory {
|
||||
override fun create(cardId: VisaCardId): DefaultVisaActivationRepository
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
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
|
||||
import com.tangem.domain.visa.model.VisaAuthSignedChallenge
|
||||
import com.tangem.domain.visa.model.VisaAuthTokens
|
||||
import com.tangem.domain.visa.repository.VisaAuthRepository
|
||||
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,
|
||||
) : VisaAuthRepository {
|
||||
|
||||
override suspend fun getCardAuthChallenge(cardId: String, cardPublicKey: String): VisaAuthChallenge.Card =
|
||||
withContext(dispatchers.io) {
|
||||
// val response = visaAuthApi.generateNonceByCard(
|
||||
// cardId = cardId,
|
||||
// cardPublicKey = cardPublicKey,
|
||||
// )
|
||||
//
|
||||
// VisaAuthChallenge.Card(
|
||||
// challenge = response.nonce,
|
||||
// session = VisaAuthSession(response.sessionId),
|
||||
// )
|
||||
|
||||
VisaAuthChallenge.Card(
|
||||
challenge = CryptoUtils.generateRandomBytes(length = 16).toHexString(),
|
||||
session = VisaAuthSession("session"),
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getCardWalletAuthChallenge(cardWalletAddress: String): VisaAuthChallenge.Wallet =
|
||||
withContext(dispatchers.io) {
|
||||
// val response = visaAuthApi.generateNonceByWalletAddress(
|
||||
// customerId = cardId,
|
||||
// customerWalletAddress = walletPublicKey,
|
||||
// )
|
||||
//
|
||||
// VisaAuthChallenge.Wallet(
|
||||
// challenge = response.nonce,
|
||||
// session = VisaAuthSession(response.sessionId),
|
||||
// )
|
||||
VisaAuthChallenge.Wallet(
|
||||
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,
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// VisaAuthTokens(
|
||||
// accessToken = response.accessToken,
|
||||
// refreshToken = response.refreshToken,
|
||||
// )
|
||||
VisaAuthTokens(
|
||||
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"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package com.tangem.data.visa
|
||||
|
||||
import androidx.paging.PagingData
|
||||
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.UserWalletId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
internal class DummyVisaRepository : VisaRepository {
|
||||
|
||||
override suspend fun getVisaCurrency(userWalletId: UserWalletId, isRefresh: Boolean): VisaCurrency {
|
||||
TODO("Not implemented for this build type")
|
||||
}
|
||||
|
||||
override suspend fun getTxHistory(
|
||||
userWalletId: UserWalletId,
|
||||
pageSize: Int,
|
||||
isRefresh: Boolean,
|
||||
): Flow<PagingData<VisaTxHistoryItem>> {
|
||||
TODO("Not implemented for this build type")
|
||||
}
|
||||
|
||||
override suspend fun getTxDetails(userWalletId: UserWalletId, txId: String): VisaTxDetails {
|
||||
TODO("Not implemented for this build type")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
package com.tangem.data.visa
|
||||
|
||||
import com.tangem.common.extensions.toHexString
|
||||
import com.tangem.crypto.CryptoUtils
|
||||
import com.tangem.domain.visa.model.*
|
||||
import com.tangem.domain.visa.repository.VisaActivationRepository
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
class MockVisaActivationRepository @AssistedInject constructor(
|
||||
@Assisted private val visaCardId: VisaCardId,
|
||||
) : VisaActivationRepository {
|
||||
|
||||
override suspend fun getActivationRemoteState(): VisaActivationRemoteState {
|
||||
return VisaActivationRemoteState.PaymentAccountDeploying
|
||||
}
|
||||
|
||||
override suspend fun getActivationRemoteStateLongPoll(): VisaActivationRemoteState {
|
||||
return VisaActivationRemoteState.PaymentAccountDeploying
|
||||
}
|
||||
|
||||
override suspend fun getCardWalletAcceptanceData(
|
||||
request: VisaCardWalletDataToSignRequest,
|
||||
): VisaDataToSignByCardWallet {
|
||||
return VisaDataToSignByCardWallet(
|
||||
request = request,
|
||||
hashToSign = CryptoUtils.generateRandomBytes(length = 32).toHexString(),
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getCustomerWalletAcceptanceData(
|
||||
request: VisaCustomerWalletDataToSignRequest,
|
||||
): VisaDataToSignByCustomerWallet {
|
||||
return VisaDataToSignByCustomerWallet(
|
||||
request = request,
|
||||
CryptoUtils.generateRandomBytes(length = 32).toHexString(),
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun activateCard(signedData: VisaSignedActivationDataByCardWallet) {}
|
||||
|
||||
override suspend fun approveByCustomerWallet(signedData: VisaSignedDataByCustomerWallet) {}
|
||||
|
||||
override suspend fun sendPinCode(pinCode: VisaEncryptedPinCode) {}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : VisaActivationRepository.Factory {
|
||||
override fun create(cardId: VisaCardId): MockVisaActivationRepository
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package com.tangem.data.visa.converter
|
||||
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.data.visa.model.AccessCodeData
|
||||
import com.tangem.datasource.di.NetworkMoshi
|
||||
import com.tangem.domain.visa.model.VisaAuthTokens
|
||||
import com.tangem.utils.converter.Converter
|
||||
import okio.ByteString.Companion.decodeBase64
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
private const val JWT_PAYLOAD_INDEX = 1
|
||||
|
||||
@Singleton
|
||||
internal class AccessCodeDataConverter @Inject constructor(
|
||||
@NetworkMoshi private val moshi: Moshi,
|
||||
) : Converter<VisaAuthTokens, AccessCodeData> {
|
||||
|
||||
private val adapter = moshi.adapter(AccessCodeData::class.java)
|
||||
|
||||
override fun convert(value: VisaAuthTokens): AccessCodeData {
|
||||
val payloadBase64 = value.accessToken.split(".").getOrNull(JWT_PAYLOAD_INDEX)
|
||||
val decodedString = payloadBase64?.decodeBase64()?.utf8()
|
||||
val data = decodedString?.let { adapter.fromJson(it) }
|
||||
requireNotNull(data) { "Invalid access token" }
|
||||
return data
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.data.visa.converter
|
||||
|
||||
import com.tangem.datasource.api.visa.models.response.CardActivationRemoteStateResponse
|
||||
import com.tangem.domain.visa.model.VisaActivationRemoteState
|
||||
import com.tangem.utils.converter.Converter
|
||||
import javax.inject.Inject
|
||||
|
||||
class VisaActivationStatusConverter @Inject constructor() :
|
||||
Converter<CardActivationRemoteStateResponse, VisaActivationRemoteState> {
|
||||
|
||||
override fun convert(value: CardActivationRemoteStateResponse): VisaActivationRemoteState {
|
||||
// TODO Will be implemented in the future
|
||||
return VisaActivationRemoteState.Activated
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem.data.visa.di
|
||||
|
||||
import javax.inject.Qualifier
|
||||
|
||||
@Qualifier
|
||||
internal annotation class ImplementedVisaRepository
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
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
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
package com.tangem.data.visa.di
|
||||
|
||||
import com.tangem.data.visa.DefaultVisaAuthRepository
|
||||
import com.tangem.data.visa.DummyVisaRepository
|
||||
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 {
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindVisaAuthRepository(repository: DefaultVisaAuthRepository): VisaAuthRepository
|
||||
|
||||
// @Binds
|
||||
// @Singleton
|
||||
// fun bindVisaActivationRepositoryFactory(
|
||||
// repository: DefaultVisaActivationRepository.Factory,
|
||||
// ): VisaActivationRepository.Factory
|
||||
|
||||
// Mocked
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindVisaActivationRepositoryFactory(
|
||||
repository: MockVisaActivationRepository.Factory,
|
||||
): VisaActivationRepository.Factory
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.data.visa.model
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class AccessCodeData(
|
||||
@Json(name = "pid") val productInstanceId: String,
|
||||
@Json(name = "sub") val customerId: String,
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue