Updated on 2026-08-14

This commit is contained in:
Tangem 2025-11-28 18:13:46 +04:00
parent 9163a956aa
commit b70933a1e0
47 changed files with 848 additions and 74 deletions

View file

@ -16,7 +16,7 @@ import com.tangem.domain.models.ReceiveAddressModel.NameService
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.pay.TangemPayTopUpData
import com.tangem.domain.pay.TangemPayTopUpDataFactory
import com.tangem.domain.pay.TangemPaySwapDataFactory
import timber.log.Timber
import java.math.BigDecimal
import javax.inject.Inject
@ -30,11 +30,11 @@ private const val TOKEN_NAME = "USDC"
private const val TOKEN_CONTRACT_ADDRESS = "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359"
private const val TOKEN_DECIMALS = 6
internal class DefaultTangemPayTopUpDataFactory @Inject constructor(
internal class DefaultTangemPaySwapDataFactory @Inject constructor(
@NetworkMoshi moshi: Moshi,
private val tangemPayWalletsManager: TangemPayWalletsManager,
excludedBlockchains: ExcludedBlockchains,
) : TangemPayTopUpDataFactory {
) : TangemPaySwapDataFactory {
private val cryptoCurrencyFactory by lazy(mode = LazyThreadSafetyMode.NONE) {
CryptoCurrencyFactory(excludedBlockchains)

View file

@ -27,4 +27,13 @@ internal class DefaultTangemPayAuthDataSource @Inject constructor(
.mapLeft { IllegalStateException("TangemPay token refresh failed. Error code: ${it.errorCode}") }
.bind()
}
override suspend fun getWithdrawalSignature(cardId: String, hash: String): Either<Throwable, String> {
return when (val signResult = tangemSdkManager.getWithdrawalSignature(cardId, hash)) {
is CompletionResult.Failure<*> -> Either.Left(
IllegalStateException("TangemPay sign hash failed: ${signResult.error}"),
)
is CompletionResult.Success<String> -> Either.Right(signResult.data)
}
}
}

View file

@ -1,18 +1,15 @@
package com.tangem.data.pay.di
import com.tangem.data.pay.DefaultTangemPayTopUpDataFactory
import com.tangem.data.pay.repository.DefaultKycRepository
import com.tangem.data.pay.repository.DefaultOnboardingRepository
import com.tangem.data.pay.repository.DefaultTangemPayCardDetailsRepository
import com.tangem.data.pay.repository.DefaultTangemPayTxHistoryRepository
import com.tangem.data.pay.DefaultTangemPaySwapDataFactory
import com.tangem.data.pay.repository.*
import com.tangem.data.pay.usecase.DefaultGetTangemPayCurrencyStatusUseCase
import com.tangem.domain.pay.TangemPayTopUpDataFactory
import com.tangem.domain.pay.repository.KycRepository
import com.tangem.domain.pay.repository.OnboardingRepository
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
import com.tangem.data.pay.usecase.DefaultTangemPayWithdrawUseCase
import com.tangem.domain.pay.TangemPaySwapDataFactory
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 dagger.Binds
import dagger.Module
@ -43,7 +40,15 @@ internal interface TangemPayDataModule {
@Binds
@Singleton
fun bindDataForReceiveFactory(factory: DefaultTangemPayTopUpDataFactory): TangemPayTopUpDataFactory
fun bindTangemPaySwapRepository(repository: DefaultTangemPaySwapRepository): TangemPaySwapRepository
@Binds
@Singleton
fun bindCustomerOrderRepository(repository: DefaultCustomerOrderRepository): CustomerOrderRepository
@Binds
@Singleton
fun bindTangemPaySwapDataFactory(factory: DefaultTangemPaySwapDataFactory): TangemPaySwapDataFactory
@Binds
@Singleton
@ -51,13 +56,21 @@ internal interface TangemPayDataModule {
impl: DefaultGetTangemPayCurrencyStatusUseCase,
): GetTangemPayCurrencyStatusUseCase
@Binds
@Singleton
fun bindTangemPayWithdrawUseCase(impl: DefaultTangemPayWithdrawUseCase): TangemPayWithdrawUseCase
companion object {
@Provides
@Singleton
fun provideTangemPayMainScreenCustomerInfoUseCase(
repository: OnboardingRepository,
customerOrderRepository: CustomerOrderRepository,
): TangemPayMainScreenCustomerInfoUseCase {
return TangemPayMainScreenCustomerInfoUseCase(repository = repository)
return TangemPayMainScreenCustomerInfoUseCase(
repository = repository,
customerOrderRepository = customerOrderRepository,
)
}
@Provides

View file

@ -0,0 +1,46 @@
package com.tangem.data.pay.repository
import arrow.core.Either
import com.tangem.datasource.api.pay.TangemPayApi
import com.tangem.datasource.local.visa.TangemPayStorage
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.model.OrderStatus
import com.tangem.domain.pay.repository.CustomerOrderRepository
import com.tangem.domain.visa.error.VisaApiError
import javax.inject.Inject
internal class DefaultCustomerOrderRepository @Inject constructor(
private val tangemPayApi: TangemPayApi,
private val requestHelper: TangemPayRequestPerformer,
private val tangemPayStorage: TangemPayStorage,
) : CustomerOrderRepository {
override suspend fun getOrderStatus(
userWalletId: UserWalletId,
orderId: String,
): Either<VisaApiError, OrderStatus> {
return requestHelper.performRequest(userWalletId) { authHeader ->
tangemPayApi.getOrder(authHeader = authHeader, orderId = orderId)
}.map { response ->
when (response.result?.status) {
null -> OrderStatus.UNKNOWN
OrderStatus.NEW.apiName -> OrderStatus.NEW
OrderStatus.PROCESSING.apiName -> OrderStatus.PROCESSING
OrderStatus.COMPLETED.apiName -> OrderStatus.COMPLETED
else -> OrderStatus.CANCELED
}
}
}
override suspend fun hasWithdrawOrder(userWalletId: UserWalletId): Boolean {
val orderId = tangemPayStorage.getWithdrawOrderId(userWalletId)
if (orderId == null) return false
val status = getOrderStatus(userWalletId, orderId).getOrNull()
val hasActiveOrder = status == OrderStatus.NEW || status == OrderStatus.PROCESSING
if (!hasActiveOrder) tangemPayStorage.deleteWithdrawOrder(userWalletId)
return hasActiveOrder
}
}

View file

@ -14,7 +14,6 @@ import com.tangem.domain.pay.datasource.TangemPayAuthDataSource
import com.tangem.domain.pay.model.CustomerInfo
import com.tangem.domain.pay.model.CustomerInfo.CardInfo
import com.tangem.domain.pay.model.CustomerInfo.ProductInstance
import com.tangem.domain.pay.model.OrderStatus
import com.tangem.domain.pay.repository.OnboardingRepository
import com.tangem.domain.visa.error.VisaApiError
import com.tangem.domain.visa.model.TangemPayCardFrozenState
@ -170,23 +169,6 @@ internal class DefaultOnboardingRepository @Inject constructor(
}
}
override suspend fun getOrderStatus(
userWalletId: UserWalletId,
orderId: String,
): Either<VisaApiError, OrderStatus> {
return requestHelper.performRequest(userWalletId) { authHeader ->
tangemPayApi.getOrder(authHeader = authHeader, orderId = orderId)
}.map { response ->
when (response.result?.status) {
null -> OrderStatus.UNKNOWN
OrderStatus.NEW.apiName -> OrderStatus.NEW
OrderStatus.PROCESSING.apiName -> OrderStatus.PROCESSING
OrderStatus.COMPLETED.apiName -> OrderStatus.COMPLETED
else -> OrderStatus.CANCELED
}
}
}
override suspend fun checkCustomerWallet(userWalletId: UserWalletId): Either<VisaApiError, Boolean> {
val hasTangemPay = tangemPayStorage.checkCustomerWalletResult(userWalletId)
if (hasTangemPay == true) {

View file

@ -42,8 +42,12 @@ internal class DefaultTangemPayCardDetailsRepository @Inject constructor(
tangemPayApi.getCardBalance(authHeader)
}.result ?: error("Cannot get card balance")
TangemPayCardBalance(
balance = result.fiat.availableBalance,
fiatBalance = result.fiat.availableBalance,
currencyCode = result.fiat.currency,
cryptoBalance = result.crypto.balance,
chainId = result.crypto.chainId,
depositAddress = result.crypto.depositAddress,
contractAddress = result.crypto.tokenContractAddress,
)
}
}

View file

@ -0,0 +1,106 @@
package com.tangem.data.pay.repository
import arrow.core.Either
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.datasource.TangemPayAuthDataSource
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 java.math.BigDecimal
import java.math.RoundingMode
import java.util.Currency
import java.util.Locale
import javax.inject.Inject
@Suppress("LongParameterList")
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,
receiverAddress: String,
cryptoAmount: BigDecimal,
cryptoCurrencyId: CryptoCurrency.RawID,
): Either<UniversalError, Unit> {
val amountInCents = getAmountInCents(cryptoAmount, cryptoCurrencyId)
if (amountInCents.isNullOrEmpty()) return Either.Left(VisaApiError.WithdrawalDataError)
return requestHelper.makeSafeRequest(userWalletId) { 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 signature = authDataSource.getWithdrawalSignature(cardId = getCardId(userWalletId), hash = result.hash)
.getOrNull()
if (signature == null) return Either.Left(VisaApiError.SignWithdrawError)
requestHelper.makeSafeRequest(userWalletId) { authHeader ->
val request = WithdrawRequest(
amountInCents = amountInCents,
recipientAddress = receiverAddress,
adminSalt = result.salt,
senderAddress = result.senderAddress,
adminSignature = signature,
)
tangemPayApi.withdraw(authHeader = authHeader, body = request)
}
.mapLeft { return Either.Left(VisaApiError.WithdrawError) }
.map { response ->
val orderId = response.result?.orderId
if (orderId != null) tangemPayStorage.storeWithdrawOrder(userWalletId, orderId)
}
}
}
private suspend fun getAmountInCents(cryptoAmount: BigDecimal, cryptoCurrencyId: CryptoCurrency.RawID): String? {
val fiatRate = getFiatRate(cryptoCurrencyId) ?: return null
val amountInDollars = cryptoAmount.multiply(fiatRate)
val defaultFractionDigits = Currency.getInstance(Locale.US).defaultFractionDigits
return amountInDollars
.setScale(defaultFractionDigits, RoundingMode.HALF_UP)
.movePointRight(defaultFractionDigits)
.longValueExact()
.toString()
}
private suspend fun getFiatRate(cryptoCurrencyId: CryptoCurrency.RawID): BigDecimal? {
val quotes = quotesFetcher.fetch(
fiatCurrencyId = Currency.getInstance(Locale.US).currencyCode,
currencyId = cryptoCurrencyId.value,
field = QuotesFetcher.Field.PRICE,
).getOrNull()
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

@ -0,0 +1,29 @@
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.pay.repository.TangemPaySwapRepository
import com.tangem.domain.tangempay.TangemPayWithdrawUseCase
import java.math.BigDecimal
import javax.inject.Inject
internal class DefaultTangemPayWithdrawUseCase @Inject constructor(
private val repository: TangemPaySwapRepository,
) : TangemPayWithdrawUseCase {
override suspend fun invoke(
userWalletId: UserWalletId,
cryptoAmount: BigDecimal,
cryptoCurrencyId: CryptoCurrency.RawID,
receiverCexAddress: String,
): Either<UniversalError, Unit> {
return repository.withdraw(
userWalletId = userWalletId,
cryptoAmount = cryptoAmount,
receiverAddress = receiverCexAddress,
cryptoCurrencyId = cryptoCurrencyId,
)
}
}