Updated on 2026-08-14
This commit is contained in:
parent
9163a956aa
commit
b70933a1e0
47 changed files with 848 additions and 74 deletions
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue