Updated on 2026-08-14

This commit is contained in:
Tangem 2025-12-19 17:25:04 +05:00
parent 71e4d3a6ed
commit 0594e1b680
23 changed files with 298 additions and 208 deletions

View file

@ -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)
}
}
}

View file

@ -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)
}
}
}

View file

@ -80,9 +80,8 @@ internal interface TangemPayDataModule {
deviceSecurity: DeviceSecurityInfoProvider,
): TangemPayMainScreenCustomerInfoUseCase {
return TangemPayMainScreenCustomerInfoUseCase(
repository = repository,
onboardingRepository = repository,
customerOrderRepository = customerOrderRepository,
tangemPayOnboardingRepository = tangemPayOnboardingRepository,
eligibilityManager = eligibilityManager,
deviceSecurity = deviceSecurity,
)

View file

@ -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(
@ -120,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(

View file

@ -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]")
}
}
}

View file

@ -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,

View file

@ -9,6 +9,7 @@ import com.tangem.datasource.api.pay.models.request.GenerateNonceByCustomerWalle
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
@ -56,7 +57,7 @@ internal class DefaultTangemPayRemoteDataSource @Inject constructor(
authType = "customer_wallet",
sessionId = sessionId,
signature = signature,
messageFormat = "Tangem Pay wants to sign in with your account. Nonce: $nonce",
messageFormat = VisaUtilities.signWithNonceMessage(nonce),
),
).getOrThrow()
}.map { response ->