Updated on 2026-08-14
This commit is contained in:
commit
9b53fd4f0c
909 changed files with 14900 additions and 6085 deletions
|
|
@ -0,0 +1,132 @@
|
|||
package com.tangem.data.pay
|
||||
|
||||
import com.tangem.common.card.FirmwareVersion
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.isLocked
|
||||
import com.tangem.domain.models.wallet.isMultiCurrency
|
||||
import com.tangem.domain.pay.TangemPayEligibilityManager
|
||||
import com.tangem.domain.pay.repository.OnboardingRepository
|
||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
||||
import com.tangem.features.hotwallet.HotWalletFeatureToggles
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class DefaultTangemPayEligibilityManager @Inject constructor(
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
private val userWalletsListManager: UserWalletsListManager,
|
||||
private val userWalletsListRepository: UserWalletsListRepository,
|
||||
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
|
||||
private val onboardingRepository: OnboardingRepository,
|
||||
) : TangemPayEligibilityManager {
|
||||
|
||||
private var cachedEligibleWallets: List<UserWalletData>? = null
|
||||
private var eligibleWalletsDeferred: Deferred<List<UserWalletData>>? = null
|
||||
private val loadMutex = Mutex()
|
||||
private val coroutineScope = CoroutineScope(SupervisorJob() + dispatchers.default)
|
||||
|
||||
init {
|
||||
resetDataWhenWalletsUpdate()
|
||||
}
|
||||
|
||||
override suspend fun getEligibleWallets(shouldExcludePaeraCustomers: Boolean): List<UserWallet> {
|
||||
return getUserWalletsData().mapNotNull {
|
||||
if (!it.isPaeraCustomer || !shouldExcludePaeraCustomers) it.userWallet else null
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getUserWalletsData(): List<UserWalletData> {
|
||||
cachedEligibleWallets?.let { return it }
|
||||
|
||||
return loadMutex.withLock {
|
||||
cachedEligibleWallets?.let { return it }
|
||||
eligibleWalletsDeferred?.let { return it.await() }
|
||||
|
||||
coroutineScope {
|
||||
val deferred = async {
|
||||
getPossibleWalletsForTangemPay()
|
||||
.addPaeraCustomersData()
|
||||
.also { cachedEligibleWallets = it }
|
||||
}
|
||||
eligibleWalletsDeferred = deferred
|
||||
try {
|
||||
deferred.await()
|
||||
} finally {
|
||||
eligibleWalletsDeferred = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getPossibleWalletsForTangemPay(): List<UserWallet> {
|
||||
if (!onboardingRepository.checkCustomerEligibility()) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
val wallets = if (hotWalletFeatureToggles.isHotWalletEnabled) {
|
||||
userWalletsListRepository.userWallets.value
|
||||
} else {
|
||||
userWalletsListManager.userWalletsSync
|
||||
} ?: return emptyList()
|
||||
|
||||
return wallets.filter { wallet ->
|
||||
wallet.isMultiCurrency && !wallet.isLocked && wallet.isCompatible()
|
||||
}
|
||||
}
|
||||
|
||||
private fun UserWallet.isCompatible(): Boolean = when (this) {
|
||||
is UserWallet.Cold ->
|
||||
scanResponse.card.firmwareVersion >= FirmwareVersion.HDWalletAvailable
|
||||
is UserWallet.Hot -> true
|
||||
}
|
||||
|
||||
private suspend fun List<UserWallet>.addPaeraCustomersData(): List<UserWalletData> {
|
||||
if (isEmpty()) return emptyList()
|
||||
|
||||
return coroutineScope {
|
||||
map { wallet ->
|
||||
async {
|
||||
val isCustomer = onboardingRepository
|
||||
.checkCustomerWallet(wallet.walletId)
|
||||
.getOrNull() == true
|
||||
wallet to isCustomer
|
||||
}
|
||||
}
|
||||
.awaitAll()
|
||||
.map { (userWallet, isPaeraCustomer) ->
|
||||
UserWalletData(userWallet, isPaeraCustomer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun resetDataWhenWalletsUpdate() {
|
||||
coroutineScope.launch {
|
||||
if (hotWalletFeatureToggles.isHotWalletEnabled) {
|
||||
userWalletsListRepository.userWallets.collectLatest { reset() }
|
||||
} else {
|
||||
userWalletsListManager.userWallets.collectLatest { reset() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun reset() {
|
||||
cachedEligibleWallets = null
|
||||
eligibleWalletsDeferred?.cancel()
|
||||
eligibleWalletsDeferred = null
|
||||
}
|
||||
|
||||
private data class UserWalletData(
|
||||
val userWallet: UserWallet,
|
||||
val isPaeraCustomer: Boolean,
|
||||
)
|
||||
}
|
||||
|
|
@ -1,42 +1,34 @@
|
|||
package com.tangem.data.pay.datasource
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.pay.WithdrawalSignatureResult
|
||||
import com.tangem.domain.pay.datasource.TangemPayAuthDataSource
|
||||
import com.tangem.domain.pay.model.WithdrawalSignatureResult
|
||||
import com.tangem.domain.visa.model.TangemPayInitialCredentials
|
||||
import com.tangem.sdk.api.TangemSdkManager
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class DefaultTangemPayAuthDataSource @Inject constructor(
|
||||
private val tangemSdkManager: TangemSdkManager,
|
||||
private val tangemPayHotSdkManager: TangemPayHotSdkManager,
|
||||
) : TangemPayAuthDataSource {
|
||||
|
||||
override suspend fun produceInitialCredentials(cardId: String): Either<Throwable, TangemPayInitialCredentials> {
|
||||
return when (val initialCredentials = tangemSdkManager.tangemPayProduceInitialCredentials(cardId = cardId)) {
|
||||
is CompletionResult.Failure<*> -> initialCredentials.error.left()
|
||||
is CompletionResult.Success<TangemPayInitialCredentials> -> initialCredentials.data.right()
|
||||
override suspend fun produceInitialCredentials(
|
||||
userWallet: UserWallet,
|
||||
): Either<Throwable, TangemPayInitialCredentials> {
|
||||
return when (userWallet) {
|
||||
is UserWallet.Cold -> tangemSdkManager.tangemPayProduceInitialCredentials(cardId = userWallet.cardId)
|
||||
is UserWallet.Hot -> tangemPayHotSdkManager.produceInitialCredentials(userWallet)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getWithdrawalSignature(
|
||||
cardId: String,
|
||||
userWallet: UserWallet,
|
||||
hash: String,
|
||||
): Either<Throwable, WithdrawalSignatureResult> {
|
||||
return when (val signResult = tangemSdkManager.getWithdrawalSignature(cardId, hash)) {
|
||||
is CompletionResult.Failure<*> -> {
|
||||
if (signResult.error is TangemSdkError.UserCancelled) {
|
||||
WithdrawalSignatureResult.Cancelled.right()
|
||||
} else {
|
||||
signResult.error.left()
|
||||
}
|
||||
}
|
||||
is CompletionResult.Success<String> -> {
|
||||
WithdrawalSignatureResult.Success(signResult.data).right()
|
||||
}
|
||||
return when (userWallet) {
|
||||
is UserWallet.Cold -> tangemSdkManager.getWithdrawalSignature(cardId = userWallet.cardId, hash = hash)
|
||||
is UserWallet.Hot -> tangemPayHotSdkManager.getWithdrawalSignature(hotWallet = userWallet, hash = hash)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
package com.tangem.data.pay.datasource
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.getOrElse
|
||||
import arrow.core.raise.Raise
|
||||
import arrow.core.raise.either
|
||||
import com.tangem.common.extensions.hexToBytes
|
||||
import com.tangem.core.error.ext.tangemError
|
||||
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
|
||||
import com.tangem.domain.card.common.visa.VisaUtilities
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.pay.WithdrawalSignatureResult
|
||||
import com.tangem.domain.visa.datasource.TangemPayRemoteDataSource
|
||||
import com.tangem.domain.visa.error.VisaActivationError
|
||||
import com.tangem.domain.visa.error.VisaCardScanError
|
||||
import com.tangem.domain.visa.model.TangemPayInitialCredentials
|
||||
import com.tangem.domain.wallets.hot.HotWalletAccessor
|
||||
import com.tangem.hot.sdk.TangemHotSdk
|
||||
import com.tangem.hot.sdk.model.DataToSign
|
||||
import com.tangem.hot.sdk.model.DeriveWalletRequest
|
||||
import com.tangem.hot.sdk.model.UnlockHotWallet
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class TangemPayHotSdkManager @Inject constructor(
|
||||
private val hotWalletAccessor: HotWalletAccessor,
|
||||
private val tangemHotSdk: TangemHotSdk,
|
||||
private val tangemPayAuthRemoteDataSource: TangemPayRemoteDataSource,
|
||||
) {
|
||||
|
||||
suspend fun produceInitialCredentials(hotWallet: UserWallet.Hot): Either<Throwable, TangemPayInitialCredentials> =
|
||||
withUnlockedHotWallet(hotWallet) { unlockHotWallet ->
|
||||
val extendedPublicKey = getExtendedPublicKey(unlockHotWallet = unlockHotWallet)
|
||||
val address = VisaUtilities.generateAddressFromExtendedKey(extendedPublicKey)
|
||||
val challenge = tangemPayAuthRemoteDataSource.getCustomerWalletAuthChallenge(
|
||||
customerWalletAddress = address,
|
||||
customerWalletId = hotWallet.walletId.stringValue,
|
||||
).getOrElse { raise(it.tangemError) }
|
||||
|
||||
val content = VisaUtilities.signWithNonceMessage(challenge.challenge)
|
||||
val hash = VisaUtilities.hashPersonalMessage(content.toByteArray(Charsets.UTF_8))
|
||||
val signature = getSignature(
|
||||
unlockHotWallet = unlockHotWallet,
|
||||
hash = hash,
|
||||
extendedPublicKey = extendedPublicKey,
|
||||
)
|
||||
|
||||
val authTokens = tangemPayAuthRemoteDataSource.getTokenWithCustomerWallet(
|
||||
sessionId = challenge.session.sessionId,
|
||||
signature = signature,
|
||||
nonce = challenge.challenge,
|
||||
).getOrElse { raise(VisaActivationError.FailedRemoteState.tangemError) }
|
||||
|
||||
TangemPayInitialCredentials(
|
||||
customerWalletAddress = address,
|
||||
authTokens = authTokens,
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun getWithdrawalSignature(
|
||||
hotWallet: UserWallet.Hot,
|
||||
hash: String,
|
||||
): Either<Throwable, WithdrawalSignatureResult> = withUnlockedHotWallet(hotWallet) { unlockHotWallet ->
|
||||
val signature = getSignature(
|
||||
unlockHotWallet = unlockHotWallet,
|
||||
hash = hash.hexToBytes(),
|
||||
extendedPublicKey = getExtendedPublicKey(unlockHotWallet = unlockHotWallet),
|
||||
)
|
||||
|
||||
WithdrawalSignatureResult.Success(signature)
|
||||
}
|
||||
|
||||
private suspend fun Raise<Throwable>.getExtendedPublicKey(unlockHotWallet: UnlockHotWallet): ExtendedPublicKey {
|
||||
val publicKeyResponse = tangemHotSdk.derivePublicKey(
|
||||
unlockHotWallet = unlockHotWallet,
|
||||
request = DeriveWalletRequest(
|
||||
requests = listOf(
|
||||
DeriveWalletRequest.Request(
|
||||
curve = VisaUtilities.curve,
|
||||
paths = listOf(VisaUtilities.customDerivationPath),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
return publicKeyResponse.responses
|
||||
.firstOrNull { it.curve == VisaUtilities.curve }
|
||||
?.publicKeys[VisaUtilities.customDerivationPath]
|
||||
?: raise(VisaActivationError.MissingWallet.tangemError)
|
||||
}
|
||||
|
||||
private suspend fun Raise<Throwable>.getSignature(
|
||||
unlockHotWallet: UnlockHotWallet,
|
||||
hash: ByteArray,
|
||||
extendedPublicKey: ExtendedPublicKey,
|
||||
): String {
|
||||
val signedHashes = tangemHotSdk.signHashes(
|
||||
unlockHotWallet = unlockHotWallet,
|
||||
dataToSign = listOf(
|
||||
DataToSign(
|
||||
curve = VisaUtilities.curve,
|
||||
derivationPath = VisaUtilities.customDerivationPath,
|
||||
hashes = listOf(hash),
|
||||
),
|
||||
),
|
||||
)
|
||||
val signature = signedHashes
|
||||
.firstOrNull { it.curve == VisaUtilities.curve }
|
||||
?.signatures
|
||||
?.firstOrNull()
|
||||
?: raise(VisaCardScanError.FailedToSignChallenge.tangemError)
|
||||
|
||||
return VisaUtilities.unmarshallSignature(
|
||||
signature = signature,
|
||||
hash = hash,
|
||||
extendedPublicKey = extendedPublicKey,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend inline fun <Error, T> withUnlockedHotWallet(
|
||||
hotWallet: UserWallet.Hot,
|
||||
block: Raise<Error>.(UnlockHotWallet) -> T,
|
||||
): Either<Error, T> = either {
|
||||
try {
|
||||
val unlockHotWallet = hotWalletAccessor.getContextualUnlock(hotWallet.hotWalletId)
|
||||
?: hotWalletAccessor.unlockContextual(hotWallet.hotWalletId)
|
||||
block(unlockHotWallet)
|
||||
} finally {
|
||||
hotWalletAccessor.clearContextualUnlock(hotWallet.hotWalletId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +1,19 @@
|
|||
package com.tangem.data.pay.di
|
||||
|
||||
import com.tangem.data.pay.DefaultTangemPayCryptoCurrencyFactory
|
||||
import com.tangem.data.pay.DefaultTangemPayEligibilityManager
|
||||
import com.tangem.data.pay.repository.*
|
||||
import com.tangem.data.pay.usecase.DefaultGetTangemPayCurrencyStatusUseCase
|
||||
import com.tangem.data.pay.usecase.DefaultTangemPayWithdrawUseCase
|
||||
import com.tangem.domain.pay.TangemPayCryptoCurrencyFactory
|
||||
import com.tangem.domain.pay.TangemPayEligibilityManager
|
||||
import com.tangem.domain.pay.repository.*
|
||||
import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase
|
||||
import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase
|
||||
import com.tangem.domain.tangempay.GetTangemPayCurrencyStatusUseCase
|
||||
import com.tangem.domain.tangempay.TangemPayWithdrawUseCase
|
||||
import com.tangem.domain.tangempay.repository.TangemPayTxHistoryRepository
|
||||
import com.tangem.security.DeviceSecurityInfoProvider
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
|
|
@ -62,6 +65,10 @@ internal interface TangemPayDataModule {
|
|||
@Singleton
|
||||
fun bindTangemPayWithdrawUseCase(impl: DefaultTangemPayWithdrawUseCase): TangemPayWithdrawUseCase
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindTangemPayEligibilityManager(impl: DefaultTangemPayEligibilityManager): TangemPayEligibilityManager
|
||||
|
||||
companion object {
|
||||
@Provides
|
||||
@Singleton
|
||||
|
|
@ -69,11 +76,14 @@ internal interface TangemPayDataModule {
|
|||
repository: OnboardingRepository,
|
||||
customerOrderRepository: CustomerOrderRepository,
|
||||
tangemPayOnboardingRepository: OnboardingRepository,
|
||||
eligibilityManager: TangemPayEligibilityManager,
|
||||
deviceSecurity: DeviceSecurityInfoProvider,
|
||||
): TangemPayMainScreenCustomerInfoUseCase {
|
||||
return TangemPayMainScreenCustomerInfoUseCase(
|
||||
repository = repository,
|
||||
onboardingRepository = repository,
|
||||
customerOrderRepository = customerOrderRepository,
|
||||
tangemPayOnboardingRepository = tangemPayOnboardingRepository,
|
||||
eligibilityManager = eligibilityManager,
|
||||
deviceSecurity = deviceSecurity,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ internal class DefaultOnboardingRepository @Inject constructor(
|
|||
return requestHelper.performWithStaticToken {
|
||||
tangemPayApi.validateDeeplink(body = DeeplinkValidityRequest(link = link))
|
||||
}.map { response ->
|
||||
response.result?.status == VALID_STATUS
|
||||
response.result?.status.equals(VALID_STATUS, ignoreCase = true)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -65,15 +65,16 @@ internal class DefaultOnboardingRepository @Inject constructor(
|
|||
|
||||
override suspend fun produceInitialData(userWalletId: UserWalletId) {
|
||||
withContext(dispatcherProvider.io) {
|
||||
val initialCredentials = authDataSource.produceInitialCredentials(cardId = getCardId(userWalletId))
|
||||
val userWallet = getUserWallet(userWalletId)
|
||||
val initialCredentials = authDataSource.produceInitialCredentials(userWallet)
|
||||
.fold(
|
||||
ifLeft = { error -> error("Can not produce initial data: ${error.message}") },
|
||||
ifRight = { it },
|
||||
)
|
||||
// should storeCheckCustomerWalletResult because we already know this
|
||||
tangemPayStorage.storeCheckCustomerWalletResult(userWalletId, true)
|
||||
tangemPayStorage.storeCheckCustomerWalletResult(userWallet.walletId, true)
|
||||
tangemPayStorage.storeCustomerWalletAddress(
|
||||
userWalletId = userWalletId,
|
||||
userWalletId = userWallet.walletId,
|
||||
customerWalletAddress = initialCredentials.customerWalletAddress,
|
||||
)
|
||||
tangemPayStorage.storeAuthTokens(
|
||||
|
|
@ -84,7 +85,6 @@ internal class DefaultOnboardingRepository @Inject constructor(
|
|||
}
|
||||
|
||||
override suspend fun getCustomerInfo(userWalletId: UserWalletId): Either<VisaApiError, CustomerInfo> {
|
||||
// TODO implement selector
|
||||
return requestHelper.performRequest(userWalletId) { authHeader -> tangemPayApi.getCustomerMe(authHeader) }
|
||||
.map { response -> getCustomerInfo(userWalletId = userWalletId, response = response.result) }
|
||||
}
|
||||
|
|
@ -121,17 +121,13 @@ internal class DefaultOnboardingRepository @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun getCardId(userWalletId: UserWalletId): String {
|
||||
private fun getUserWallet(userWalletId: UserWalletId): UserWallet {
|
||||
val userWallet = if (hotWalletFeatureToggles.isHotWalletEnabled) {
|
||||
userWalletsListRepository.userWallets.value?.firstOrNull { it.walletId == userWalletId }
|
||||
} else {
|
||||
userWalletsListManager.userWalletsSync.firstOrNull { it.walletId == userWalletId }
|
||||
} ?: error("no userWallet found")
|
||||
return if (userWallet is UserWallet.Cold) {
|
||||
userWallet.cardId
|
||||
} else {
|
||||
TODO("[REDACTED_JIRA]")
|
||||
}
|
||||
return userWallet
|
||||
}
|
||||
|
||||
private suspend fun getCustomerInfo(
|
||||
|
|
@ -148,6 +144,7 @@ internal class DefaultOnboardingRepository @Inject constructor(
|
|||
currencyCode = fiatBalance.currency,
|
||||
customerWalletAddress = paymentAccount.customerWalletAddress,
|
||||
depositAddress = response.depositAddress,
|
||||
isPinSet = response.card?.isPinSet == true,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
|
|
@ -182,7 +179,7 @@ internal class DefaultOnboardingRepository @Inject constructor(
|
|||
customerWalletId = userWalletId.stringValue,
|
||||
)
|
||||
}.map { response ->
|
||||
val id = response.id
|
||||
val id = response.result?.id
|
||||
val isPaeraCustomer = !id.isNullOrEmpty()
|
||||
tangemPayStorage.storeCheckCustomerWalletResult(userWalletId = userWalletId, isPaeraCustomer)
|
||||
isPaeraCustomer
|
||||
|
|
@ -193,4 +190,19 @@ internal class DefaultOnboardingRepository @Inject constructor(
|
|||
error
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun checkCustomerEligibility(): Boolean {
|
||||
val response = requestHelper.performWithoutToken {
|
||||
tangemPayApi.checkCustomerEligibility()
|
||||
}.getOrNull()
|
||||
return response?.result?.isTangemPayAvailable == true
|
||||
}
|
||||
|
||||
override suspend fun getHideMainOnboardingBanner(userWalletId: UserWalletId): Boolean {
|
||||
return tangemPayStorage.getHideMainOnboardingBanner(userWalletId)
|
||||
}
|
||||
|
||||
override suspend fun setHideMainOnboardingBanner(userWalletId: UserWalletId) {
|
||||
tangemPayStorage.storeHideOnboardingBanner(userWalletId, hide = true)
|
||||
}
|
||||
}
|
||||
|
|
@ -82,12 +82,14 @@ internal class DefaultTangemPayCardDetailsRepository @Inject constructor(
|
|||
block = {
|
||||
val publicKeyBase64 = getPublicKeyBase64()
|
||||
val (secretKeyBytes, sessionId) = rainCryptoUtil.generateSecretKeyAndSessionId(publicKeyBase64)
|
||||
val result = requestHelper.performRequest(userWalletId = userWalletId) { authHeader ->
|
||||
tangemPayApi.revealCardDetails(
|
||||
authHeader = authHeader,
|
||||
body = CardDetailsRequest(sessionId = sessionId),
|
||||
)
|
||||
}.getOrNull()?.result ?: error("Cannot reveal card details")
|
||||
val result = requireNotNull(
|
||||
requestHelper.performRequest(userWalletId = userWalletId) { authHeader ->
|
||||
tangemPayApi.revealCardDetails(
|
||||
authHeader = authHeader,
|
||||
body = CardDetailsRequest(sessionId = sessionId),
|
||||
)
|
||||
}.getOrNull()?.result,
|
||||
)
|
||||
|
||||
val pan = rainCryptoUtil.decryptSecret(
|
||||
base64Secret = result.pan.secret,
|
||||
|
|
@ -113,6 +115,38 @@ internal class DefaultTangemPayCardDetailsRepository @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
override suspend fun getPin(userWalletId: UserWalletId, cardId: String): Either<UniversalError, String?> {
|
||||
return catch(
|
||||
block = {
|
||||
val publicKeyBase64 = getPublicKeyBase64()
|
||||
val (secretKeyBytes, sessionId) = rainCryptoUtil.generateSecretKeyAndSessionId(publicKeyBase64)
|
||||
val result = requireNotNull(
|
||||
requestHelper.performRequest(userWalletId = userWalletId) { authHeader ->
|
||||
tangemPayApi.revealCardDetails(
|
||||
authHeader = authHeader,
|
||||
body = CardDetailsRequest(sessionId = sessionId),
|
||||
)
|
||||
}.getOrNull()?.result,
|
||||
)
|
||||
|
||||
val encryptedPin = result.pin
|
||||
val pin = if (encryptedPin != null) {
|
||||
rainCryptoUtil.decryptPin(
|
||||
base64Secret = encryptedPin.secret,
|
||||
base64Iv = encryptedPin.iv,
|
||||
secretKeyBytes = secretKeyBytes,
|
||||
).takeIf { !it.isNullOrEmpty() }
|
||||
} else {
|
||||
null
|
||||
}
|
||||
secretKeyBytes.fill(0)
|
||||
|
||||
pin.right()
|
||||
},
|
||||
catch = ::catchException,
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun setPin(userWalletId: UserWalletId, pin: String): Either<UniversalError, SetPinResult> {
|
||||
return requestHelper.runWithErrorLogs(TAG) {
|
||||
val publicKeyBase64 = getPublicKeyBase64()
|
||||
|
|
@ -120,16 +154,18 @@ internal class DefaultTangemPayCardDetailsRepository @Inject constructor(
|
|||
val encryptedData = rainCryptoUtil.encryptPin(pin = pin, secretKeyBytes = secretKeyBytes)
|
||||
secretKeyBytes.fill(0)
|
||||
|
||||
val status = requestHelper.request(userWalletId) { authHeader ->
|
||||
tangemPayApi.setPin(
|
||||
authHeader = authHeader,
|
||||
body = SetPinRequest(
|
||||
sessionId = sessionId,
|
||||
pin = encryptedData.encryptedBase64,
|
||||
iv = encryptedData.ivBase64,
|
||||
),
|
||||
)
|
||||
}.result?.result ?: error("Cannot set pin code")
|
||||
val status = requireNotNull(
|
||||
requestHelper.request(userWalletId) { authHeader ->
|
||||
tangemPayApi.setPin(
|
||||
authHeader = authHeader,
|
||||
body = SetPinRequest(
|
||||
sessionId = sessionId,
|
||||
pin = encryptedData.encryptedBase64,
|
||||
iv = encryptedData.ivBase64,
|
||||
),
|
||||
)
|
||||
}.result?.result,
|
||||
)
|
||||
when (status) {
|
||||
SetPinResult.SUCCESS.name -> SetPinResult.SUCCESS
|
||||
SetPinResult.PIN_TOO_WEAK.name -> SetPinResult.PIN_TOO_WEAK
|
||||
|
|
@ -288,6 +324,7 @@ internal class DefaultTangemPayCardDetailsRepository @Inject constructor(
|
|||
ApiEnvironment.DEV_2,
|
||||
ApiEnvironment.DEV_3,
|
||||
ApiEnvironment.STAGE,
|
||||
ApiEnvironment.STAGE_2,
|
||||
ApiEnvironment.MOCK,
|
||||
-> visaLibLoader.getOrCreateConfig().rainRSAPublicKey.dev
|
||||
ApiEnvironment.PROD -> visaLibLoader.getOrCreateConfig().rainRSAPublicKey.prod
|
||||
|
|
|
|||
|
|
@ -1,23 +1,20 @@
|
|||
package com.tangem.data.pay.repository
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.left
|
||||
import com.tangem.core.error.UniversalError
|
||||
import com.tangem.data.common.quote.QuotesFetcher
|
||||
import com.tangem.datasource.api.pay.TangemPayApi
|
||||
import com.tangem.datasource.api.pay.models.request.WithdrawDataRequest
|
||||
import com.tangem.datasource.api.pay.models.request.WithdrawRequest
|
||||
import com.tangem.datasource.local.visa.TangemPayStorage
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.pay.WithdrawalResult
|
||||
import com.tangem.domain.pay.WithdrawalSignatureResult
|
||||
import com.tangem.domain.pay.datasource.TangemPayAuthDataSource
|
||||
import com.tangem.domain.pay.model.WithdrawalSignatureResult
|
||||
import com.tangem.domain.pay.repository.TangemPaySwapRepository
|
||||
import com.tangem.domain.visa.error.VisaApiError
|
||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
||||
import com.tangem.features.hotwallet.HotWalletFeatureToggles
|
||||
import com.tangem.utils.extensions.addHexPrefix
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
|
|
@ -30,29 +27,25 @@ internal class DefaultTangemPaySwapRepository @Inject constructor(
|
|||
private val tangemPayApi: TangemPayApi,
|
||||
private val requestHelper: TangemPayRequestPerformer,
|
||||
private val authDataSource: TangemPayAuthDataSource,
|
||||
private val userWalletsListManager: UserWalletsListManager,
|
||||
private val userWalletsListRepository: UserWalletsListRepository,
|
||||
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
|
||||
private val quotesFetcher: QuotesFetcher,
|
||||
private val tangemPayStorage: TangemPayStorage,
|
||||
) : TangemPaySwapRepository {
|
||||
|
||||
override suspend fun withdraw(
|
||||
userWalletId: UserWalletId,
|
||||
userWallet: UserWallet,
|
||||
receiverAddress: String,
|
||||
cryptoAmount: BigDecimal,
|
||||
cryptoCurrencyId: CryptoCurrency.RawID,
|
||||
): Either<UniversalError, WithdrawalResult> {
|
||||
val amountInCents = getAmountInCents(cryptoAmount, cryptoCurrencyId)
|
||||
if (amountInCents.isNullOrEmpty()) return Either.Left(VisaApiError.WithdrawalDataError)
|
||||
return requestHelper.performRequest(userWalletId) { authHeader ->
|
||||
return requestHelper.performRequest(userWallet.walletId) { authHeader ->
|
||||
val request = WithdrawDataRequest(amountInCents = amountInCents, recipientAddress = receiverAddress)
|
||||
tangemPayApi.getWithdrawData(authHeader = authHeader, body = request)
|
||||
}.map { data ->
|
||||
val result = data.result
|
||||
if (result == null) return Either.Left(VisaApiError.WithdrawalDataError)
|
||||
val result = data.result ?: return VisaApiError.WithdrawalDataError.left()
|
||||
val signatureResult = authDataSource.getWithdrawalSignature(
|
||||
cardId = getCardId(userWalletId),
|
||||
userWallet = userWallet,
|
||||
hash = result.hash,
|
||||
).getOrNull()
|
||||
|
||||
|
|
@ -61,7 +54,7 @@ internal class DefaultTangemPaySwapRepository @Inject constructor(
|
|||
Either.Right(WithdrawalResult.Cancelled)
|
||||
}
|
||||
is WithdrawalSignatureResult.Success -> {
|
||||
requestHelper.performRequest(userWalletId) { authHeader ->
|
||||
requestHelper.performRequest(userWallet.walletId) { authHeader ->
|
||||
val request = WithdrawRequest(
|
||||
amountInCents = amountInCents,
|
||||
recipientAddress = receiverAddress,
|
||||
|
|
@ -74,7 +67,7 @@ internal class DefaultTangemPaySwapRepository @Inject constructor(
|
|||
.mapLeft { return Either.Left(VisaApiError.WithdrawError) }
|
||||
.map { response ->
|
||||
val orderId = response.result?.orderId
|
||||
if (orderId != null) tangemPayStorage.storeWithdrawOrder(userWalletId, orderId)
|
||||
if (orderId != null) tangemPayStorage.storeWithdrawOrder(userWallet.walletId, orderId)
|
||||
WithdrawalResult.Success
|
||||
}
|
||||
}
|
||||
|
|
@ -103,17 +96,4 @@ internal class DefaultTangemPaySwapRepository @Inject constructor(
|
|||
|
||||
return quotes?.quotes[cryptoCurrencyId.value]?.price
|
||||
}
|
||||
|
||||
private fun getCardId(userWalletId: UserWalletId): String {
|
||||
val userWallet = if (hotWalletFeatureToggles.isHotWalletEnabled) {
|
||||
userWalletsListRepository.userWallets.value?.firstOrNull { it.walletId == userWalletId }
|
||||
} else {
|
||||
userWalletsListManager.userWalletsSync.firstOrNull { it.walletId == userWalletId }
|
||||
} ?: error("No User Wallet found")
|
||||
return if (userWallet is UserWallet.Cold) {
|
||||
userWallet.cardId
|
||||
} else {
|
||||
TODO("[REDACTED_JIRA]")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -89,6 +89,19 @@ internal class TangemPayRequestPerformer @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
suspend fun <T : Any> performWithoutToken(requestBlock: suspend () -> ApiResponse<T>): Either<VisaApiError, T> =
|
||||
withContext(dispatchers.io) {
|
||||
catch(
|
||||
block = {
|
||||
when (val apiResponse = requestBlock()) {
|
||||
is ApiResponse.Error -> errorConverter.convert(apiResponse.cause).left()
|
||||
is ApiResponse.Success<T> -> apiResponse.data.right()
|
||||
}
|
||||
},
|
||||
catch = { errorConverter.convert(it).left() },
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun <T : Any> performRequest(
|
||||
userWalletId: UserWalletId,
|
||||
requestBlock: suspend (header: String) -> ApiResponse<T>,
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ internal class DefaultGetTangemPayCurrencyStatusUseCase @Inject constructor(
|
|||
),
|
||||
sources = CryptoCurrencyStatus.Sources(),
|
||||
pendingTransactions = emptySet(),
|
||||
yieldBalance = null,
|
||||
stakingBalance = null,
|
||||
yieldSupplyStatus = null,
|
||||
hasCurrentNetworkTransactions = false,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ package com.tangem.data.pay.usecase
|
|||
import arrow.core.Either
|
||||
import com.tangem.core.error.UniversalError
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.pay.WithdrawalResult
|
||||
import com.tangem.domain.pay.repository.TangemPaySwapRepository
|
||||
import com.tangem.domain.tangempay.TangemPayWithdrawUseCase
|
||||
|
|
@ -15,13 +15,13 @@ internal class DefaultTangemPayWithdrawUseCase @Inject constructor(
|
|||
) : TangemPayWithdrawUseCase {
|
||||
|
||||
override suspend fun invoke(
|
||||
userWalletId: UserWalletId,
|
||||
userWallet: UserWallet,
|
||||
cryptoAmount: BigDecimal,
|
||||
cryptoCurrencyId: CryptoCurrency.RawID,
|
||||
receiverCexAddress: String,
|
||||
): Either<UniversalError, WithdrawalResult> {
|
||||
return repository.withdraw(
|
||||
userWalletId = userWalletId,
|
||||
userWallet = userWallet,
|
||||
cryptoAmount = cryptoAmount,
|
||||
receiverAddress = receiverCexAddress,
|
||||
cryptoCurrencyId = cryptoCurrencyId,
|
||||
|
|
|
|||
|
|
@ -37,6 +37,15 @@ internal class RainCryptoUtil @Inject constructor(
|
|||
secretKeyBytes to sessionId
|
||||
}
|
||||
|
||||
suspend fun decryptPin(base64Secret: String, base64Iv: String, secretKeyBytes: ByteArray): String? {
|
||||
val pinBlock = decryptSecret(
|
||||
base64Secret = base64Secret,
|
||||
base64Iv = base64Iv,
|
||||
secretKeyBytes = secretKeyBytes,
|
||||
)
|
||||
return extractPinFromPinBlock(pinBlock)
|
||||
}
|
||||
|
||||
suspend fun encryptPin(pin: String, secretKeyBytes: ByteArray): EncryptedData = withContext(dispatchers.default) {
|
||||
val bytes = pinBlockByteArray(pin)
|
||||
try {
|
||||
|
|
@ -113,6 +122,17 @@ internal class RainCryptoUtil @Inject constructor(
|
|||
return hex.toByteArray(StandardCharsets.UTF_8)
|
||||
}
|
||||
|
||||
private suspend fun extractPinFromPinBlock(pinBlock: String): String? = withContext(dispatchers.default) {
|
||||
val pinLength = pinBlock[1].digitToIntOrNull()
|
||||
require(pinLength == PIN_LENGTH) { "Unexpected PIN length: ${pinBlock[1]}" }
|
||||
|
||||
val pinStartIndex = 2
|
||||
val pinEndIndex = pinStartIndex + pinLength
|
||||
val pin = pinBlock.substring(pinStartIndex, pinEndIndex)
|
||||
|
||||
pin.takeIf { value -> value.all { it.isDigit() } && value.length == PIN_LENGTH }
|
||||
}
|
||||
|
||||
private fun ByteArray.clear() {
|
||||
for (i in indices) this[i] = 0
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ package com.tangem.data.pay.util
|
|||
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.datasource.api.common.response.ApiResponseError
|
||||
import com.tangem.datasource.api.pay.models.response.VisaErrorResponse
|
||||
import com.tangem.datasource.api.pay.models.response.TangemPayErrorResponse
|
||||
import com.tangem.datasource.di.NetworkMoshi
|
||||
import com.tangem.domain.visa.error.VisaApiError
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
|
@ -14,7 +14,7 @@ internal class TangemPayErrorConverter @Inject constructor(
|
|||
@NetworkMoshi moshi: Moshi,
|
||||
) : Converter<Throwable, VisaApiError> {
|
||||
|
||||
private val visaErrorAdapter by lazy { moshi.adapter(VisaErrorResponse::class.java) }
|
||||
private val tangemPayErrorAdapter by lazy { moshi.adapter(TangemPayErrorResponse::class.java) }
|
||||
|
||||
override fun convert(value: Throwable): VisaApiError {
|
||||
return if (value is ApiResponseError.HttpException) {
|
||||
|
|
@ -24,7 +24,7 @@ internal class TangemPayErrorConverter @Inject constructor(
|
|||
|
||||
val errorBody = value.errorBody ?: return VisaApiError.UnknownWithoutCode
|
||||
return runCatching {
|
||||
visaErrorAdapter.fromJson(errorBody)?.error?.code ?: value.code.numericCode
|
||||
tangemPayErrorAdapter.fromJson(errorBody)?.error?.code ?: value.code.numericCode
|
||||
}.map {
|
||||
VisaApiError.fromBackendError(it)
|
||||
}.getOrElse {
|
||||
|
|
|
|||
|
|
@ -1,39 +0,0 @@
|
|||
package com.tangem.data.pay.util
|
||||
|
||||
import com.tangem.common.card.FirmwareVersion
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
||||
import com.tangem.features.hotwallet.HotWalletFeatureToggles
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.first
|
||||
import javax.inject.Inject
|
||||
|
||||
// TODO remove after implement wallet selector in pay
|
||||
class TangemPayWalletsManager @Inject constructor(
|
||||
private val manager: UserWalletsListManager,
|
||||
private val repository: UserWalletsListRepository,
|
||||
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
|
||||
) {
|
||||
|
||||
@Deprecated("Don't use and put userWallet in features that need it")
|
||||
suspend fun getDefaultWalletForTangemPay(): UserWallet.Cold {
|
||||
val userWalletsFlow = if (useNewRepository()) repository.userWallets else manager.userWallets
|
||||
val userWallets = userWalletsFlow.filter { !it.isNullOrEmpty() }.first()
|
||||
return findColdWallet(userWallets)
|
||||
}
|
||||
|
||||
@Deprecated("Don't use and put userWallet in features that need it")
|
||||
fun getDefaultWalletForTangemPayBlocking(): UserWallet.Cold {
|
||||
val userWallets = if (useNewRepository()) repository.userWallets.value else manager.userWalletsSync
|
||||
return findColdWallet(userWallets)
|
||||
}
|
||||
|
||||
private fun useNewRepository(): Boolean = hotWalletFeatureToggles.isHotWalletEnabled
|
||||
|
||||
private fun findColdWallet(userWallets: List<UserWallet>?): UserWallet.Cold {
|
||||
return userWallets?.find {
|
||||
it is UserWallet.Cold && it.scanResponse.card.firmwareVersion >= FirmwareVersion.HDWalletAvailable
|
||||
} as? UserWallet.Cold ?: error("Cannot find cold user wallet")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
package com.tangem.data.visa
|
||||
|
||||
import arrow.core.Either
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.datasource.api.common.response.ApiResponseError
|
||||
import com.tangem.datasource.api.common.response.getOrThrow
|
||||
import com.tangem.datasource.api.pay.TangemPayAuthApi
|
||||
import com.tangem.datasource.api.pay.models.request.GenerateNonceByCustomerWalletRequest
|
||||
import com.tangem.datasource.api.pay.models.request.GetTokenByCustomerWalletRequest
|
||||
import com.tangem.datasource.api.pay.models.response.TangemPayErrorResponse
|
||||
import com.tangem.datasource.di.NetworkMoshi
|
||||
import com.tangem.domain.card.common.visa.VisaUtilities
|
||||
import com.tangem.domain.visa.datasource.TangemPayRemoteDataSource
|
||||
import com.tangem.domain.visa.error.VisaApiError
|
||||
import com.tangem.domain.visa.model.TangemPayAuthTokens
|
||||
import com.tangem.domain.visa.model.VisaAuthChallenge
|
||||
import com.tangem.domain.visa.model.VisaAuthSession
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class DefaultTangemPayRemoteDataSource @Inject constructor(
|
||||
@NetworkMoshi private val moshi: Moshi,
|
||||
private val tangemPayAuthApi: TangemPayAuthApi,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : TangemPayRemoteDataSource {
|
||||
|
||||
private val errorAdapter by lazy { moshi.adapter(TangemPayErrorResponse::class.java) }
|
||||
|
||||
override suspend fun getCustomerWalletAuthChallenge(
|
||||
customerWalletAddress: String,
|
||||
customerWalletId: String,
|
||||
): Either<VisaApiError, VisaAuthChallenge.Wallet> = withContext(dispatchers.io) {
|
||||
request {
|
||||
tangemPayAuthApi.generateNonceByCustomerWallet(
|
||||
request = GenerateNonceByCustomerWalletRequest(
|
||||
customerWalletAddress = customerWalletAddress,
|
||||
customerWalletId = customerWalletId,
|
||||
),
|
||||
).getOrThrow()
|
||||
}.map { response ->
|
||||
VisaAuthChallenge.Wallet(
|
||||
challenge = response.nonce,
|
||||
session = VisaAuthSession(response.sessionId),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getTokenWithCustomerWallet(
|
||||
sessionId: String,
|
||||
signature: String,
|
||||
nonce: String,
|
||||
): Either<VisaApiError, TangemPayAuthTokens> = withContext(dispatchers.io) {
|
||||
request {
|
||||
tangemPayAuthApi.getTokenByCustomerWallet(
|
||||
request = GetTokenByCustomerWalletRequest(
|
||||
authType = "customer_wallet",
|
||||
sessionId = sessionId,
|
||||
signature = signature,
|
||||
messageFormat = VisaUtilities.signWithNonceMessage(nonce),
|
||||
),
|
||||
).getOrThrow()
|
||||
}.map { response ->
|
||||
TangemPayAuthTokens(
|
||||
accessToken = response.accessToken,
|
||||
expiresAt = response.expiresAt,
|
||||
refreshToken = response.refreshToken,
|
||||
refreshExpiresAt = response.refreshExpiresAt,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun <T : Any> request(requestBlock: suspend () -> T): Either<VisaApiError, T> {
|
||||
return runCatching {
|
||||
Either.Right(requestBlock())
|
||||
}.getOrElse { responseError ->
|
||||
if (responseError is ApiResponseError.HttpException &&
|
||||
responseError.errorBody != null
|
||||
) {
|
||||
val errorCode =
|
||||
errorAdapter.fromJson(responseError.errorBody)?.error?.code ?: responseError.code.numericCode
|
||||
return Either.Left(VisaApiError.fromBackendError(errorCode))
|
||||
}
|
||||
|
||||
return Either.Left(VisaApiError.UnknownWithoutCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -10,15 +10,16 @@ import com.tangem.datasource.api.common.config.ApiEnvironment
|
|||
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
|
||||
import com.tangem.datasource.api.common.response.ApiResponseError
|
||||
import com.tangem.datasource.api.common.response.getOrThrow
|
||||
import com.tangem.datasource.api.pay.TangemPayApi
|
||||
import com.tangem.datasource.api.pay.models.request.*
|
||||
import com.tangem.datasource.api.pay.models.response.VisaErrorResponseJsonAdapter
|
||||
import com.tangem.datasource.api.pay.models.request.SetPinCodeRequest
|
||||
import com.tangem.datasource.api.pay.models.response.TangemPayErrorResponse
|
||||
import com.tangem.datasource.api.visa.VisaApi
|
||||
import com.tangem.datasource.api.visa.models.request.*
|
||||
import com.tangem.datasource.di.NetworkMoshi
|
||||
import com.tangem.datasource.local.visa.VisaAuthTokenStorage
|
||||
import com.tangem.domain.visa.datasource.VisaAuthRemoteDataSource
|
||||
import com.tangem.domain.visa.error.VisaApiError
|
||||
import com.tangem.domain.visa.model.*
|
||||
import com.tangem.domain.visa.repository.VisaActivationRepository
|
||||
import com.tangem.domain.visa.datasource.VisaAuthRemoteDataSource
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
|
|
@ -29,7 +30,7 @@ import kotlinx.coroutines.withContext
|
|||
internal class DefaultVisaActivationRepository @AssistedInject constructor(
|
||||
@Assisted private val visaCardId: VisaCardId,
|
||||
@NetworkMoshi private val moshi: Moshi,
|
||||
private val visaApi: TangemPayApi,
|
||||
private val visaApi: VisaApi,
|
||||
private val dispatcherProvider: CoroutineDispatcherProvider,
|
||||
private val visaAuthTokenStorage: VisaAuthTokenStorage,
|
||||
private val visaAuthRemoteDataSource: VisaAuthRemoteDataSource,
|
||||
|
|
@ -37,7 +38,7 @@ internal class DefaultVisaActivationRepository @AssistedInject constructor(
|
|||
private val apiConfigsManager: ApiConfigsManager,
|
||||
) : VisaActivationRepository {
|
||||
|
||||
private val visaErrorAdapter = VisaErrorResponseJsonAdapter(moshi)
|
||||
private val errorAdapter by lazy { moshi.adapter(TangemPayErrorResponse::class.java) }
|
||||
|
||||
override suspend fun getActivationRemoteState(): Either<VisaApiError, VisaActivationRemoteState> =
|
||||
withContext(dispatcherProvider.io) {
|
||||
|
|
@ -171,6 +172,7 @@ internal class DefaultVisaActivationRepository @AssistedInject constructor(
|
|||
ApiEnvironment.DEV_2,
|
||||
ApiEnvironment.DEV_3,
|
||||
ApiEnvironment.STAGE,
|
||||
ApiEnvironment.STAGE_2,
|
||||
ApiEnvironment.MOCK,
|
||||
-> rsaPublicKey.dev
|
||||
ApiEnvironment.PROD -> rsaPublicKey.prod
|
||||
|
|
@ -186,7 +188,7 @@ internal class DefaultVisaActivationRepository @AssistedInject constructor(
|
|||
responseError.errorBody != null
|
||||
) {
|
||||
val errorCode =
|
||||
visaErrorAdapter.fromJson(responseError.errorBody!!)?.error?.code ?: responseError.code.numericCode
|
||||
errorAdapter.fromJson(responseError.errorBody!!)?.error?.code ?: responseError.code.numericCode
|
||||
return Either.Left(VisaApiError.fromBackendError(errorCode))
|
||||
}
|
||||
|
||||
|
|
@ -206,7 +208,7 @@ internal class DefaultVisaActivationRepository @AssistedInject constructor(
|
|||
}.getOrElse { responseError ->
|
||||
if (responseError is ApiResponseError.HttpException && responseError.errorBody != null) {
|
||||
val errorCode =
|
||||
visaErrorAdapter.fromJson(responseError.errorBody!!)?.error?.code
|
||||
errorAdapter.fromJson(responseError.errorBody!!)?.error?.code
|
||||
?: responseError.code.numericCode
|
||||
Either.Left(VisaApiError.fromBackendError(errorCode))
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -4,33 +4,35 @@ import arrow.core.Either
|
|||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.datasource.api.common.response.ApiResponseError
|
||||
import com.tangem.datasource.api.common.response.getOrThrow
|
||||
import com.tangem.datasource.api.pay.TangemPayApi
|
||||
import com.tangem.datasource.api.pay.TangemPayAuthApi
|
||||
import com.tangem.datasource.api.pay.models.request.*
|
||||
import com.tangem.datasource.api.pay.models.response.VisaErrorResponseJsonAdapter
|
||||
import com.tangem.datasource.api.pay.models.request.RefreshTokenByCardIdRequest
|
||||
import com.tangem.datasource.api.pay.models.response.TangemPayErrorResponse
|
||||
import com.tangem.datasource.api.visa.VisaApi
|
||||
import com.tangem.datasource.api.visa.models.request.*
|
||||
import com.tangem.datasource.di.NetworkMoshi
|
||||
import com.tangem.domain.visa.datasource.VisaAuthRemoteDataSource
|
||||
import com.tangem.domain.visa.error.VisaApiError
|
||||
import com.tangem.domain.visa.model.*
|
||||
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.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class DefaultVisaAuthRemoteDataSource @Inject constructor(
|
||||
@NetworkMoshi private val moshi: Moshi,
|
||||
private val visaAuthApi: TangemPayApi,
|
||||
private val tangemPayAuthApi: TangemPayAuthApi,
|
||||
private val visaApi: VisaApi,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : VisaAuthRemoteDataSource {
|
||||
|
||||
private val visaErrorAdapter = VisaErrorResponseJsonAdapter(moshi)
|
||||
private val errorAdapter by lazy { moshi.adapter(TangemPayErrorResponse::class.java) }
|
||||
|
||||
override suspend fun getCardAuthChallenge(
|
||||
cardId: String,
|
||||
cardPublicKey: String,
|
||||
): Either<VisaApiError, VisaAuthChallenge.Card> = withContext(dispatchers.io) {
|
||||
request {
|
||||
visaAuthApi.generateNonceByCardId(
|
||||
visaApi.generateNonceByCardId(
|
||||
GenerateNoneByCardIdRequest(
|
||||
cardId = cardId,
|
||||
cardPublicKey = cardPublicKey,
|
||||
|
|
@ -49,7 +51,7 @@ internal class DefaultVisaAuthRemoteDataSource @Inject constructor(
|
|||
cardWalletAddress: String,
|
||||
): Either<VisaApiError, VisaAuthChallenge.Wallet> = withContext(dispatchers.io) {
|
||||
request {
|
||||
visaAuthApi.generateNonceByCardWallet(
|
||||
visaApi.generateNonceByCardWallet(
|
||||
GenerateNoneByCardWalletRequest(
|
||||
cardWalletAddress = cardWalletAddress,
|
||||
cardId = cardId,
|
||||
|
|
@ -63,56 +65,13 @@ internal class DefaultVisaAuthRemoteDataSource @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun getCustomerWalletAuthChallenge(
|
||||
customerWalletAddress: String,
|
||||
customerWalletId: String,
|
||||
): Either<VisaApiError, VisaAuthChallenge.Wallet> = withContext(dispatchers.io) {
|
||||
request {
|
||||
tangemPayAuthApi.generateNonceByCustomerWallet(
|
||||
request = GenerateNonceByCustomerWalletRequest(
|
||||
customerWalletAddress = customerWalletAddress,
|
||||
customerWalletId = customerWalletId,
|
||||
),
|
||||
).getOrThrow()
|
||||
}.map { response ->
|
||||
VisaAuthChallenge.Wallet(
|
||||
challenge = response.nonce,
|
||||
session = VisaAuthSession(response.sessionId),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getTokenWithCustomerWallet(
|
||||
sessionId: String,
|
||||
signature: String,
|
||||
nonce: String,
|
||||
): Either<VisaApiError, TangemPayAuthTokens> = withContext(dispatchers.io) {
|
||||
request {
|
||||
tangemPayAuthApi.getTokenByCustomerWallet(
|
||||
request = GetTokenByCustomerWalletRequest(
|
||||
authType = "customer_wallet",
|
||||
sessionId = sessionId,
|
||||
signature = signature,
|
||||
messageFormat = "Tangem Pay wants to sign in with your account. Nonce: $nonce",
|
||||
),
|
||||
).getOrThrow()
|
||||
}.map { response ->
|
||||
TangemPayAuthTokens(
|
||||
accessToken = response.accessToken,
|
||||
expiresAt = response.expiresAt,
|
||||
refreshToken = response.refreshToken,
|
||||
refreshExpiresAt = response.refreshExpiresAt,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getAccessTokens(
|
||||
signedChallenge: VisaAuthSignedChallenge,
|
||||
): Either<VisaApiError, VisaAuthTokens> = withContext(dispatchers.io) {
|
||||
request {
|
||||
when (signedChallenge) {
|
||||
is VisaAuthSignedChallenge.ByCardPublicKey -> {
|
||||
visaAuthApi.getAccessTokenByCardId(
|
||||
visaApi.getAccessTokenByCardId(
|
||||
GetAccessTokenByCardIdRequest(
|
||||
sessionId = signedChallenge.challenge.session.sessionId,
|
||||
signature = signedChallenge.signature,
|
||||
|
|
@ -121,7 +80,7 @@ internal class DefaultVisaAuthRemoteDataSource @Inject constructor(
|
|||
).getOrThrow()
|
||||
}
|
||||
is VisaAuthSignedChallenge.ByWallet -> {
|
||||
visaAuthApi.getAccessTokenByCardWallet(
|
||||
visaApi.getAccessTokenByCardWallet(
|
||||
GetAccessTokenByCardWalletRequest(
|
||||
sessionId = signedChallenge.challenge.session.sessionId,
|
||||
signature = signedChallenge.signature,
|
||||
|
|
@ -150,11 +109,11 @@ internal class DefaultVisaAuthRemoteDataSource @Inject constructor(
|
|||
request {
|
||||
when (refreshToken.authType) {
|
||||
VisaAuthTokens.RefreshToken.Type.CardId ->
|
||||
visaAuthApi.refreshCardIdAccessToken(
|
||||
visaApi.refreshCardIdAccessToken(
|
||||
RefreshTokenByCardIdRequest(refreshToken = refreshToken.value),
|
||||
)
|
||||
VisaAuthTokens.RefreshToken.Type.CardWallet ->
|
||||
visaAuthApi.refreshCardIdAccessToken(
|
||||
visaApi.refreshCardIdAccessToken(
|
||||
RefreshTokenByCardIdRequest(refreshToken = refreshToken.value),
|
||||
)
|
||||
}.getOrThrow()
|
||||
|
|
@ -169,7 +128,7 @@ internal class DefaultVisaAuthRemoteDataSource @Inject constructor(
|
|||
override suspend fun exchangeAccessToken(tokens: VisaAuthTokens): Either<VisaApiError, VisaAuthTokens> =
|
||||
withContext(dispatchers.io) {
|
||||
request {
|
||||
visaAuthApi.exchangeAccessToken(
|
||||
visaApi.exchangeAccessToken(
|
||||
ExchangeAccessTokenRequest(
|
||||
accessToken = tokens.accessToken,
|
||||
refreshToken = tokens.refreshToken.value,
|
||||
|
|
@ -194,7 +153,7 @@ internal class DefaultVisaAuthRemoteDataSource @Inject constructor(
|
|||
responseError.errorBody != null
|
||||
) {
|
||||
val errorCode =
|
||||
visaErrorAdapter.fromJson(responseError.errorBody!!)?.error?.code ?: responseError.code.numericCode
|
||||
errorAdapter.fromJson(responseError.errorBody!!)?.error?.code ?: responseError.code.numericCode
|
||||
return Either.Left(VisaApiError.fromBackendError(errorCode))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,8 +14,8 @@ import com.tangem.data.common.cache.CacheRegistry
|
|||
import com.tangem.data.common.quote.QuotesFetcher
|
||||
import com.tangem.data.visa.config.VisaLibLoader
|
||||
import com.tangem.data.visa.utils.*
|
||||
import com.tangem.datasource.api.pay.TangemPayApi
|
||||
import com.tangem.datasource.api.pay.models.response.VisaTxHistoryResponse
|
||||
import com.tangem.datasource.api.visa.VisaApi
|
||||
import com.tangem.datasource.api.visa.models.response.VisaTxHistoryResponse
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.card.common.util.cardTypesResolver
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
|
|
@ -43,7 +43,7 @@ internal class DefaultVisaRepository @Inject constructor(
|
|||
private val userWalletsStore: UserWalletsStore,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val visaApiRequestMaker: VisaApiRequestMaker,
|
||||
private val visaApi: TangemPayApi,
|
||||
private val visaApi: VisaApi,
|
||||
private val visaCurrencyFactory: VisaCurrencyFactory,
|
||||
) : VisaRepository {
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
package com.tangem.data.visa.converter
|
||||
|
||||
import com.tangem.datasource.api.pay.models.response.CardActivationRemoteStateResponse
|
||||
import com.tangem.datasource.api.visa.models.response.CardActivationRemoteStateResponse
|
||||
import com.tangem.domain.visa.model.VisaActivationOrderInfo
|
||||
import com.tangem.domain.visa.model.VisaActivationRemoteState
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
package com.tangem.data.visa.di
|
||||
|
||||
import com.tangem.data.pay.datasource.DefaultTangemPayAuthDataSource
|
||||
import com.tangem.data.visa.DefaultTangemPayRemoteDataSource
|
||||
import com.tangem.data.visa.DefaultVisaActivationRepository
|
||||
import com.tangem.data.visa.DefaultVisaAuthRemoteDataSource
|
||||
import com.tangem.data.visa.MockVisaRepository
|
||||
import com.tangem.domain.pay.datasource.TangemPayAuthDataSource
|
||||
import com.tangem.domain.visa.datasource.TangemPayRemoteDataSource
|
||||
import com.tangem.domain.visa.repository.VisaActivationRepository
|
||||
import com.tangem.domain.visa.datasource.VisaAuthRemoteDataSource
|
||||
import com.tangem.domain.visa.repository.VisaRepository
|
||||
|
|
@ -22,22 +24,16 @@ internal interface VisaDataModule {
|
|||
@Singleton
|
||||
fun bindVisaAuthRemoteDataSource(repository: DefaultVisaAuthRemoteDataSource): VisaAuthRemoteDataSource
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindTangemPayRemoteDataSource(impl: DefaultTangemPayRemoteDataSource): TangemPayRemoteDataSource
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindVisaActivationRepositoryFactory(
|
||||
repository: DefaultVisaActivationRepository.Factory,
|
||||
): VisaActivationRepository.Factory
|
||||
|
||||
// Mocked
|
||||
// @Binds
|
||||
// @Singleton
|
||||
// fun bindVisaActivationRepositoryFactory(
|
||||
// repository: MockVisaActivationRepository.Factory,
|
||||
// ): VisaActivationRepository.Factory
|
||||
|
||||
// @Binds
|
||||
// fun bindVisaRepository(repository: DefaultVisaRepository): VisaRepository
|
||||
|
||||
// Mocked
|
||||
@Binds
|
||||
fun bindVisaRepository(repository: MockVisaRepository): VisaRepository
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ 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.pay.TangemPayApi
|
||||
import com.tangem.datasource.api.pay.models.request.RefreshTokenByCardWalletRequest
|
||||
import com.tangem.datasource.api.visa.VisaApi
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.card.common.util.cardTypesResolver
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
|
|
@ -24,7 +24,7 @@ typealias VisaAuthorizationHeader = String
|
|||
|
||||
internal class VisaApiRequestMaker @Inject constructor(
|
||||
private val userWalletsStore: UserWalletsStore,
|
||||
private val visaAuthApi: TangemPayApi,
|
||||
private val visaAuthApi: VisaApi,
|
||||
private val accessCodeDataConverter: AccessCodeDataConverter,
|
||||
private val dispatcherProvider: CoroutineDispatcherProvider,
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ package com.tangem.data.visa.utils
|
|||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.externallinkprovider.TxExploreState
|
||||
import com.tangem.datasource.api.pay.models.response.VisaTxHistoryResponse
|
||||
import com.tangem.datasource.api.visa.models.response.VisaTxHistoryResponse
|
||||
import com.tangem.domain.visa.model.VisaTxDetails
|
||||
|
||||
internal class VisaTxDetailsFactory {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
package com.tangem.data.visa.utils
|
||||
|
||||
import com.tangem.datasource.api.pay.models.response.VisaTxHistoryResponse
|
||||
import com.tangem.datasource.api.visa.models.response.VisaTxHistoryResponse
|
||||
import com.tangem.domain.visa.model.VisaTxHistoryItem
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ 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.pay.models.response.VisaTxHistoryResponse
|
||||
import com.tangem.datasource.api.visa.models.response.VisaTxHistoryResponse
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.visa.model.VisaTxHistoryItem
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue