Updated on 2026-08-14

This commit is contained in:
Tangem 2026-02-16 19:18:15 +04:00
parent 1cdece6ae7
commit d7a120fc86
24 changed files with 486 additions and 160 deletions

View file

@ -2,6 +2,9 @@ package com.tangem.tap.data
import android.content.Context
import com.squareup.moshi.Moshi
import com.tangem.data.pay.entity.WithdrawStoreData
import com.tangem.data.pay.util.WithdrawStateConverter
import com.tangem.data.pay.util.WithdrawStoreDataConverter
import com.tangem.datasource.di.NetworkMoshi
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
@ -11,6 +14,7 @@ import com.tangem.datasource.local.preferences.utils.getSyncOrNull
import com.tangem.datasource.local.preferences.utils.store
import com.tangem.datasource.local.visa.TangemPayStorage
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.TangemPayWithdrawState
import com.tangem.domain.visa.model.TangemPayAuthTokens
import com.tangem.sdk.storage.AndroidSecureStorageV2
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -41,6 +45,8 @@ internal class DefaultTangemPayStorage @Inject constructor(
}
private val tokensAdapter by lazy { moshi.adapter(TangemPayAuthTokens::class.java) }
private val withdrawStoreDataConverter by lazy { WithdrawStoreDataConverter() }
private val withdrawStateConverter by lazy { WithdrawStateConverter() }
override suspend fun storeCustomerWalletAddress(userWalletId: UserWalletId, customerWalletAddress: String) {
withContext(dispatcherProvider.io) {
@ -131,10 +137,12 @@ internal class DefaultTangemPayStorage @Inject constructor(
return appPreferencesStore.getSyncOrNull(PreferencesKeys.getTangemPayCheckCustomerByWalletId(userWalletId))
}
override suspend fun storeWithdrawOrder(userWalletId: UserWalletId, orderId: String) {
override suspend fun storeWithdrawOrder(userWalletId: UserWalletId, data: TangemPayWithdrawState) {
appPreferencesStore.editData { mutablePreferences ->
val orders = mutablePreferences.getObjectMap<String>(PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY)
.plus(createWithdrawOrderIdKey(userWalletId) to orderId)
val orders = mutablePreferences.getObjectMap<TangemPayWithdrawState>(
PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY,
)
.plus(createWithdrawOrderIdKey(userWalletId) to withdrawStoreDataConverter.convert(data))
mutablePreferences.setObjectMap(
key = PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY,
value = orders,
@ -142,14 +150,19 @@ internal class DefaultTangemPayStorage @Inject constructor(
}
}
override suspend fun getWithdrawOrderId(userWalletId: UserWalletId): String? {
val orders = appPreferencesStore.getObjectMapSync<String>(PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY)
return orders[createWithdrawOrderIdKey(userWalletId)]
override suspend fun getWithdrawOrder(userWalletId: UserWalletId): TangemPayWithdrawState? {
val orders = appPreferencesStore.getObjectMapSync<WithdrawStoreData>(
PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY,
)
val data = orders[createWithdrawOrderIdKey(userWalletId)] ?: return null
return withdrawStateConverter.convert(data)
}
override suspend fun deleteWithdrawOrder(userWalletId: UserWalletId) {
appPreferencesStore.editData { mutablePreferences ->
val orders = mutablePreferences.getObjectMap<String>(PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY)
val orders = mutablePreferences.getObjectMap<WithdrawStoreData>(
PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY,
)
.minus(createWithdrawOrderIdKey(userWalletId))
mutablePreferences.setObjectMap(
key = PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY,

View file

@ -27,6 +27,7 @@ data class OrderResponse(
@Json(name = "emboss_name") val embossName: String?,
@Json(name = "product_instance_id") val productInstanceId: String?,
@Json(name = "payment_account_id") val paymentAccountId: String?,
@Json(name = "transaction_hash") val transactionHash: String?,
)
}
}

View file

@ -1,6 +1,7 @@
package com.tangem.datasource.local.visa
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.TangemPayWithdrawState
import com.tangem.domain.visa.model.TangemPayAuthTokens
@Suppress("TooManyFunctions")
@ -28,9 +29,9 @@ interface TangemPayStorage {
suspend fun storeCheckCustomerWalletResult(userWalletId: UserWalletId, isPaeraCustomer: Boolean)
suspend fun checkCustomerWalletResult(userWalletId: UserWalletId): Boolean?
suspend fun storeWithdrawOrder(userWalletId: UserWalletId, orderId: String)
suspend fun storeWithdrawOrder(userWalletId: UserWalletId, data: TangemPayWithdrawState)
suspend fun getWithdrawOrderId(userWalletId: UserWalletId): String?
suspend fun getWithdrawOrder(userWalletId: UserWalletId): TangemPayWithdrawState?
suspend fun deleteWithdrawOrder(userWalletId: UserWalletId)

View file

@ -36,6 +36,7 @@ dependencies {
implementation(projects.domain.walletManager)
implementation(projects.domain.quotes)
implementation(projects.domain.common)
implementation(projects.features.swap.domain)
/** Feature API - remove after removing [HotWalletFeatureToggles] */
implementation(projects.features.hotWallet.api)

View file

@ -45,7 +45,7 @@ internal interface TangemPayDataModule {
@Binds
@Singleton
fun bindTangemPaySwapRepository(repository: DefaultTangemPaySwapRepository): TangemPaySwapRepository
fun bindTangemPaySwapRepository(repository: DefaultTangemPayWithdrawRepository): TangemPayWithdrawRepository
@Binds
@Singleton

View file

@ -0,0 +1,19 @@
package com.tangem.data.pay.entity
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = false)
data class WithdrawStoreData(
@Json(name = "orderId") val orderId: String,
@Json(name = "exchangeData") val exchangeData: ExchangeStoreData?,
)
@JsonClass(generateAdapter = false)
data class ExchangeStoreData(
@Json(name = "txId") val txId: String,
@Json(name = "fromNetwork") val fromNetwork: String,
@Json(name = "fromAddress") val fromAddress: String,
@Json(name = "payInAddress") val payInAddress: String,
@Json(name = "payInExtraId") val payInExtraId: String?,
)

View file

@ -2,8 +2,8 @@ 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.OrderData
import com.tangem.domain.pay.model.OrderStatus
import com.tangem.domain.pay.repository.CustomerOrderRepository
import com.tangem.domain.visa.error.VisaApiError
@ -12,35 +12,23 @@ 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> {
override suspend fun getOrderData(userWalletId: UserWalletId, orderId: String): Either<VisaApiError, OrderData> {
return requestHelper.performRequest(userWalletId) { authHeader ->
tangemPayApi.getOrder(authHeader = authHeader, orderId = orderId)
}.map { response ->
when (response.result?.status) {
val status = 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
}
OrderData(
status = status,
withdrawTxHash = response.result?.data?.transactionHash?.ifEmpty { null },
)
}
}
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

@ -1,99 +0,0 @@
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.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.pay.WithdrawalResult
import com.tangem.domain.pay.WithdrawalSignatureResult
import com.tangem.domain.pay.datasource.TangemPayAuthDataSource
import com.tangem.domain.pay.repository.TangemPaySwapRepository
import com.tangem.domain.visa.error.VisaApiError
import com.tangem.utils.extensions.addHexPrefix
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 quotesFetcher: QuotesFetcher,
private val tangemPayStorage: TangemPayStorage,
) : TangemPaySwapRepository {
override suspend fun withdraw(
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(userWallet.walletId) { authHeader ->
val request = WithdrawDataRequest(amountInCents = amountInCents, recipientAddress = receiverAddress)
tangemPayApi.getWithdrawData(authHeader = authHeader, body = request)
}.map { data ->
val result = data.result ?: return VisaApiError.WithdrawalDataError.left()
val signatureResult = authDataSource.getWithdrawalSignature(
userWallet = userWallet,
hash = result.hash,
).getOrNull()
return when (signatureResult) {
is WithdrawalSignatureResult.Cancelled -> {
Either.Right(WithdrawalResult.Cancelled)
}
is WithdrawalSignatureResult.Success -> {
requestHelper.performRequest(userWallet.walletId) { authHeader ->
val request = WithdrawRequest(
amountInCents = amountInCents,
recipientAddress = receiverAddress,
adminSalt = result.salt,
senderAddress = result.senderAddress,
adminSignature = signatureResult.signature.addHexPrefix(),
)
tangemPayApi.withdraw(authHeader = authHeader, body = request)
}
.mapLeft { return Either.Left(VisaApiError.WithdrawError) }
.map { response ->
val orderId = response.result?.orderId
if (orderId != null) tangemPayStorage.storeWithdrawOrder(userWallet.walletId, orderId)
WithdrawalResult.Success
}
}
null -> return Either.Left(VisaApiError.SignWithdrawError)
}
}
}
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
}
}

View file

@ -0,0 +1,301 @@
package com.tangem.data.pay.repository
import arrow.core.Either
import arrow.core.left
import arrow.core.right
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.api.pay.models.response.WithdrawResponse
import com.tangem.datasource.local.visa.TangemPayStorage
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.pay.TangemPayWithdrawExchangeState
import com.tangem.domain.pay.TangemPayWithdrawState
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.OrderData
import com.tangem.domain.pay.model.OrderStatus
import com.tangem.domain.pay.repository.CustomerOrderRepository
import com.tangem.domain.pay.repository.TangemPayWithdrawRepository
import com.tangem.domain.visa.error.VisaApiError
import com.tangem.feature.swap.domain.api.SwapRepository
import com.tangem.feature.swap.domain.models.ExpressDataError
import com.tangem.utils.extensions.addHexPrefix
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import timber.log.Timber
import java.math.BigDecimal
import java.math.RoundingMode
import java.util.Currency
import java.util.Locale
import javax.inject.Inject
import kotlin.collections.set
import kotlin.coroutines.cancellation.CancellationException
import kotlin.time.Duration.Companion.seconds
private const val TAG = "TangemPaySwapRepository"
@Suppress("LongParameterList")
internal class DefaultTangemPayWithdrawRepository @Inject constructor(
private val tangemPayApi: TangemPayApi,
private val requestHelper: TangemPayRequestPerformer,
private val authDataSource: TangemPayAuthDataSource,
private val quotesFetcher: QuotesFetcher,
private val tangemPayStorage: TangemPayStorage,
private val swapRepository: SwapRepository,
private val orderRepository: CustomerOrderRepository,
) : TangemPayWithdrawRepository {
private val withdrawPollingScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private val withdrawPollingJobs = mutableMapOf<String, Job>()
private val withdrawPollingMutex = Mutex()
override suspend fun withdraw(
userWallet: UserWallet,
receiverAddress: String,
cryptoAmount: BigDecimal,
cryptoCurrencyId: CryptoCurrency.RawID,
exchangeData: TangemPayWithdrawExchangeState,
): Either<UniversalError, WithdrawalResult> {
val amountInCents = getAmountInCents(cryptoAmount, cryptoCurrencyId)
if (amountInCents.isNullOrEmpty()) return Either.Left(VisaApiError.WithdrawalDataError)
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 ?: return VisaApiError.WithdrawalDataError.left()
val signatureResult = authDataSource.getWithdrawalSignature(
userWallet = userWallet,
hash = result.hash,
).getOrNull()
return when (signatureResult) {
is WithdrawalSignatureResult.Cancelled -> {
Either.Right(WithdrawalResult.Cancelled)
}
is WithdrawalSignatureResult.Success -> {
requestHelper.performRequest(userWallet.walletId) { authHeader ->
val request = WithdrawRequest(
amountInCents = amountInCents,
recipientAddress = receiverAddress,
adminSalt = result.salt,
senderAddress = result.senderAddress,
adminSignature = signatureResult.signature.addHexPrefix(),
)
tangemPayApi.withdraw(authHeader = authHeader, body = request)
}
.mapLeft { return Either.Left(VisaApiError.WithdrawError) }
.map { response ->
processWithdrawResult(response, userWallet, exchangeData)
WithdrawalResult.Success
}
}
null -> return Either.Left(VisaApiError.SignWithdrawError)
}
}
}
private suspend fun processWithdrawResult(
response: WithdrawResponse,
userWallet: UserWallet,
exchangeData: TangemPayWithdrawExchangeState,
) {
val orderId = response.result?.orderId
if (orderId != null) {
val orderData = orderRepository
.getOrderData(userWalletId = userWallet.walletId, orderId = orderId).getOrNull()
val withdrawTxHash = orderData?.withdrawTxHash
val storeData = TangemPayWithdrawState(
orderId = orderId,
exchangeData = exchangeData,
)
if (orderData != null && !withdrawTxHash.isNullOrEmpty()) {
finalizeWithdraw(
userWallet = userWallet,
withdrawTxHash = withdrawTxHash,
orderId = orderId,
exchangeData = exchangeData,
order = orderData,
).onLeft {
tangemPayStorage.storeWithdrawOrder(userWalletId = userWallet.walletId, data = storeData)
}
} else {
tangemPayStorage.storeWithdrawOrder(userWalletId = userWallet.walletId, data = storeData)
}
}
}
private suspend fun finalizeWithdraw(
userWallet: UserWallet,
withdrawTxHash: String,
orderId: String,
exchangeData: TangemPayWithdrawExchangeState,
order: OrderData,
): Either<ExpressDataError, Unit> {
return swapRepository.exchangeSent(
userWallet = userWallet,
txId = exchangeData.txId,
fromNetwork = exchangeData.fromNetwork,
fromAddress = exchangeData.fromAddress,
payInAddress = exchangeData.payInAddress,
txHash = withdrawTxHash,
payInExtraId = exchangeData.payInExtraId,
)
.onRight {
val isActive = order.status == OrderStatus.NEW || order.status == OrderStatus.PROCESSING
if (!isActive) {
tangemPayStorage.deleteWithdrawOrder(userWalletId = userWallet.walletId)
} else {
tangemPayStorage.storeWithdrawOrder(
userWalletId = userWallet.walletId,
data = TangemPayWithdrawState(orderId = orderId, exchangeData = null),
)
}
}
.onLeft { error ->
Timber.tag(TAG).e(error.toString())
}
}
override suspend fun hasWithdrawOrder(userWallet: UserWallet): Boolean {
val orderExchangeData = tangemPayStorage.getWithdrawOrder(userWallet.walletId)
if (orderExchangeData == null) return false
val exchangeData = orderExchangeData.exchangeData
val orderData = orderRepository
.getOrderData(userWallet.walletId, orderId = orderExchangeData.orderId).getOrNull()
val withdrawTxHash = orderData?.withdrawTxHash
if (exchangeData != null && orderData != null && withdrawTxHash != null) {
finalizeWithdraw(
userWallet = userWallet,
withdrawTxHash = withdrawTxHash,
orderId = orderExchangeData.orderId,
exchangeData = exchangeData,
order = orderData,
)
}
return orderData?.status == OrderStatus.NEW || orderData?.status == OrderStatus.PROCESSING
}
override suspend fun pollWithdrawOrderIfNeeds(userWallet: UserWallet): Either<VisaApiError, Unit> {
val storeData = tangemPayStorage.getWithdrawOrder(userWallet.walletId) ?: return Unit.right()
val exchangeData = storeData.exchangeData ?: return Unit.right()
val orderId = storeData.orderId
val order = orderRepository
.getOrderData(userWalletId = userWallet.walletId, orderId = orderId).getOrNull()
?: return Unit.right()
val txHash = order.withdrawTxHash
if (!txHash.isNullOrEmpty()) {
finalizeWithdraw(
userWallet = userWallet,
withdrawTxHash = txHash,
orderId = storeData.orderId,
exchangeData = exchangeData,
order = order,
).onLeft {
startWithdrawOrderPolling(
userWallet = userWallet,
orderId = orderId,
storeData = storeData,
exchangeData = exchangeData,
)
}
} else {
startWithdrawOrderPolling(
userWallet = userWallet,
orderId = orderId,
storeData = storeData,
exchangeData = exchangeData,
)
}
return Unit.right()
}
private suspend fun startWithdrawOrderPolling(
userWallet: UserWallet,
orderId: String,
storeData: TangemPayWithdrawState,
exchangeData: TangemPayWithdrawExchangeState,
) {
withdrawPollingMutex.withLock {
if (withdrawPollingJobs.containsKey(orderId)) return
val pollingJob = withdrawPollingScope.launch {
try {
while (isActive && withdrawPollingJobs.containsKey(orderId)) {
delay(duration = 5.seconds)
val orderData = orderRepository
.getOrderData(userWalletId = userWallet.walletId, orderId = orderId)
orderData.onRight { order ->
if (order.status != OrderStatus.NEW && order.status != OrderStatus.PROCESSING) {
tangemPayStorage.deleteWithdrawOrder(userWallet.walletId)
withdrawPollingJobs.remove(key = orderId)
return@launch
}
val txHash = order.withdrawTxHash
if (!txHash.isNullOrEmpty()) {
finalizeWithdraw(
userWallet = userWallet,
withdrawTxHash = txHash,
orderId = storeData.orderId,
exchangeData = exchangeData,
order = order,
).onRight {
withdrawPollingJobs.remove(key = orderId)
return@launch
}
}
}.onLeft { error ->
Timber.tag(TAG).e("error ${error.errorCode}")
}
}
} catch (exception: CancellationException) {
throw exception
} catch (exception: Exception) {
Timber.tag(TAG).e(exception)
withdrawPollingMutex.withLock { withdrawPollingJobs.remove(orderId) }
}
}
withdrawPollingJobs[orderId] = pollingJob
}
}
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
}
}

View file

@ -4,14 +4,15 @@ import arrow.core.Either
import com.tangem.core.error.UniversalError
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.pay.TangemPayWithdrawExchangeState
import com.tangem.domain.pay.WithdrawalResult
import com.tangem.domain.pay.repository.TangemPaySwapRepository
import com.tangem.domain.pay.repository.TangemPayWithdrawRepository
import com.tangem.domain.tangempay.TangemPayWithdrawUseCase
import java.math.BigDecimal
import javax.inject.Inject
internal class DefaultTangemPayWithdrawUseCase @Inject constructor(
private val repository: TangemPaySwapRepository,
private val repository: TangemPayWithdrawRepository,
) : TangemPayWithdrawUseCase {
override suspend fun invoke(
@ -19,12 +20,14 @@ internal class DefaultTangemPayWithdrawUseCase @Inject constructor(
cryptoAmount: BigDecimal,
cryptoCurrencyId: CryptoCurrency.RawID,
receiverCexAddress: String,
exchangeData: TangemPayWithdrawExchangeState,
): Either<UniversalError, WithdrawalResult> {
return repository.withdraw(
userWallet = userWallet,
cryptoAmount = cryptoAmount,
receiverAddress = receiverCexAddress,
cryptoCurrencyId = cryptoCurrencyId,
exchangeData = exchangeData,
)
}
}

View file

@ -0,0 +1,22 @@
package com.tangem.data.pay.util
import com.tangem.data.pay.entity.WithdrawStoreData
import com.tangem.domain.pay.TangemPayWithdrawExchangeState
import com.tangem.domain.pay.TangemPayWithdrawState
import com.tangem.utils.converter.Converter
class WithdrawStateConverter : Converter<WithdrawStoreData, TangemPayWithdrawState> {
override fun convert(value: WithdrawStoreData): TangemPayWithdrawState = TangemPayWithdrawState(
orderId = value.orderId,
exchangeData = value.exchangeData?.let { exchangeData ->
TangemPayWithdrawExchangeState(
txId = exchangeData.txId,
fromNetwork = exchangeData.fromNetwork,
fromAddress = exchangeData.fromAddress,
payInAddress = exchangeData.payInAddress,
payInExtraId = exchangeData.payInExtraId,
)
},
)
}

View file

@ -0,0 +1,22 @@
package com.tangem.data.pay.util
import com.tangem.data.pay.entity.ExchangeStoreData
import com.tangem.data.pay.entity.WithdrawStoreData
import com.tangem.domain.pay.TangemPayWithdrawState
import com.tangem.utils.converter.Converter
class WithdrawStoreDataConverter : Converter<TangemPayWithdrawState, WithdrawStoreData> {
override fun convert(value: TangemPayWithdrawState): WithdrawStoreData = WithdrawStoreData(
orderId = value.orderId,
exchangeData = value.exchangeData?.let { exchangeData ->
ExchangeStoreData(
txId = exchangeData.txId,
fromNetwork = exchangeData.fromNetwork,
fromAddress = exchangeData.fromAddress,
payInAddress = exchangeData.payInAddress,
payInExtraId = exchangeData.payInExtraId,
)
},
)
}

View file

@ -24,6 +24,7 @@ dependencies {
implementation(projects.domain.core)
implementation(projects.domain.tokens.models)
implementation(projects.domain.wallets.models)
implementation(projects.features.swap.domain)
/** Feature API - remove after removing [TangemPayFeatureToggles] */
implementation(projects.features.tangempay.details.api)

View file

@ -0,0 +1,14 @@
package com.tangem.domain.pay
data class TangemPayWithdrawState(
val orderId: String,
val exchangeData: TangemPayWithdrawExchangeState?,
)
data class TangemPayWithdrawExchangeState(
val txId: String,
val fromNetwork: String,
val fromAddress: String,
val payInAddress: String,
val payInExtraId: String?,
)

View file

@ -0,0 +1,6 @@
package com.tangem.domain.pay.model
data class OrderData(
val status: OrderStatus,
val withdrawTxHash: String?,
)

View file

@ -2,12 +2,10 @@ package com.tangem.domain.pay.repository
import arrow.core.Either
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.model.OrderStatus
import com.tangem.domain.pay.model.OrderData
import com.tangem.domain.visa.error.VisaApiError
interface CustomerOrderRepository {
suspend fun getOrderStatus(userWalletId: UserWalletId, orderId: String): Either<VisaApiError, OrderStatus>
suspend fun hasWithdrawOrder(userWalletId: UserWalletId): Boolean
suspend fun getOrderData(userWalletId: UserWalletId, orderId: String): Either<VisaApiError, OrderData>
}

View file

@ -4,15 +4,22 @@ import arrow.core.Either
import com.tangem.core.error.UniversalError
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.pay.TangemPayWithdrawExchangeState
import com.tangem.domain.pay.WithdrawalResult
import com.tangem.domain.visa.error.VisaApiError
import java.math.BigDecimal
interface TangemPaySwapRepository {
interface TangemPayWithdrawRepository {
suspend fun withdraw(
userWallet: UserWallet,
receiverAddress: String,
cryptoAmount: BigDecimal,
cryptoCurrencyId: CryptoCurrency.RawID,
exchangeData: TangemPayWithdrawExchangeState,
): Either<UniversalError, WithdrawalResult>
suspend fun hasWithdrawOrder(userWallet: UserWallet): Boolean
suspend fun pollWithdrawOrderIfNeeds(userWallet: UserWallet): Either<VisaApiError, Unit>
}

View file

@ -137,13 +137,13 @@ class TangemPayMainScreenCustomerInfoUseCase(
userWalletId: UserWalletId,
orderId: String,
): Either<TangemPayCustomerInfoError, MainScreenCustomerInfo> {
return customerOrderRepository.getOrderStatus(userWalletId, orderId = orderId)
return customerOrderRepository.getOrderData(userWalletId, orderId = orderId)
.fold(
ifLeft = { error ->
error.mapErrorForCustomer().left()
},
ifRight = { orderStatus ->
when (orderStatus) {
ifRight = { orderData ->
when (orderData.status) {
// Kyc is passed and user waits for order creation -> no need to get customer info
OrderStatus.NEW,
OrderStatus.PROCESSING,
@ -154,7 +154,7 @@ class TangemPayMainScreenCustomerInfoUseCase(
kycStatus = CustomerInfo.KycStatus.APPROVED,
cardInfo = null,
),
orderStatus = orderStatus,
orderStatus = orderData.status,
).right()
// Order was created/cancelled -> clear order id and get customer info
@ -164,11 +164,11 @@ class TangemPayMainScreenCustomerInfoUseCase(
-> {
onboardingRepository.clearOrderId(userWalletId)
// If order was cancelled -> start order creation
if (orderStatus == OrderStatus.CANCELED) onboardingRepository.createOrder(userWalletId)
if (orderData.status == OrderStatus.CANCELED) onboardingRepository.createOrder(userWalletId)
onboardingRepository.getCustomerInfo(userWalletId = userWalletId)
.mapLeft { it.mapErrorForCustomer() }
.map { customerInfo ->
MainScreenCustomerInfo(info = customerInfo, orderStatus = orderStatus)
MainScreenCustomerInfo(info = customerInfo, orderStatus = orderData.status)
}
}
}

View file

@ -4,6 +4,7 @@ import arrow.core.Either
import com.tangem.core.error.UniversalError
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.pay.TangemPayWithdrawExchangeState
import com.tangem.domain.pay.WithdrawalResult
import java.math.BigDecimal
@ -14,5 +15,6 @@ interface TangemPayWithdrawUseCase {
cryptoAmount: BigDecimal,
cryptoCurrencyId: CryptoCurrency.RawID,
receiverCexAddress: String,
exchangeData: TangemPayWithdrawExchangeState,
): Either<UniversalError, WithdrawalResult>
}

View file

@ -38,6 +38,7 @@ dependencies {
implementation(projects.domain.express.models)
implementation(projects.domain.account)
implementation(projects.domain.account.status)
implementation(projects.domain.visa.models)
implementation(projects.features.swap.domain.api)
implementation(projects.features.swap.domain.models)

View file

@ -36,6 +36,7 @@ import com.tangem.domain.models.network.Network
import com.tangem.domain.models.quote.QuoteStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.TangemPayWithdrawExchangeState
import com.tangem.domain.quotes.QuotesRepository
import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher
import com.tangem.domain.tokens.*
@ -1030,6 +1031,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
exchangeData.transaction as? ExpressTransactionModel.CEX ?: return SwapTransactionState.Error.UnknownError
if (isTangemPayWithdrawal) {
val networkAddress = currencyToSend.value.networkAddress
return SwapTransactionState.TangemPayWithdrawalData(
cryptoAmount = amount.value,
cryptoCurrencyId = requireNotNull(currencyToSend.currency.id.rawCurrencyId),
@ -1056,6 +1058,13 @@ internal class SwapInteractorImpl @AssistedInject constructor(
txExternalId = exchangeDataCex.externalTxId,
averageDuration = null,
),
exchangeData = TangemPayWithdrawExchangeState(
txId = exchangeDataCex.txId,
fromNetwork = currencyToSend.currency.network.backendId,
fromAddress = networkAddress?.defaultAddress?.value.orEmpty(),
payInAddress = exchangeData.transaction.txTo,
payInExtraId = exchangeDataCex.txExtraId,
),
)
}

View file

@ -3,6 +3,7 @@ package com.tangem.feature.swap.domain.models.ui
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.pay.TangemPayWithdrawExchangeState
import com.tangem.domain.transaction.error.SendTransactionError
import com.tangem.feature.swap.domain.models.ExpressDataError
import com.tangem.feature.swap.domain.models.SwapAmount
@ -31,6 +32,7 @@ sealed class SwapTransactionState {
val toAmount: String?,
val toAmountValue: BigDecimal?,
val storeData: StoreTransactionData,
val exchangeData: TangemPayWithdrawExchangeState,
) : SwapTransactionState() {
data class StoreTransactionData(

View file

@ -1136,6 +1136,7 @@ internal class SwapModel @Inject constructor(
cryptoAmount = swapTransactionState.cryptoAmount,
cryptoCurrencyId = swapTransactionState.cryptoCurrencyId,
receiverCexAddress = swapTransactionState.cexAddress,
exchangeData = swapTransactionState.exchangeData,
)
.onLeft {
startLoadingQuotesFromLastState()

View file

@ -26,8 +26,8 @@ import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.pay.TangemPayCryptoCurrencyFactory
import com.tangem.domain.pay.model.TangemPayCardBalance
import com.tangem.domain.pay.model.TangemPayTopUpData
import com.tangem.domain.pay.repository.CustomerOrderRepository
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
import com.tangem.domain.pay.repository.TangemPayWithdrawRepository
import com.tangem.domain.tangempay.TangemPayAnalyticsEvents
import com.tangem.domain.visa.model.TangemPayCardFrozenState
import com.tangem.domain.visa.model.TangemPayTxHistoryItem
@ -79,7 +79,7 @@ internal class TangemPayDetailsModel @Inject constructor(
private val cardDetailsEventListener: CardDetailsEventListener,
private val txHistoryUpdateListener: TangemPayTxHistoryUpdateListener,
private val tangemPayCryptoCurrencyFactory: TangemPayCryptoCurrencyFactory,
private val orderRepository: CustomerOrderRepository,
private val tangemPayWithdrawRepository: TangemPayWithdrawRepository,
private val getUserWalletUseCase: GetUserWalletUseCase,
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
private val expressTransactionsEventListener: ExpressTransactionsEventListener,
@ -124,6 +124,7 @@ internal class TangemPayDetailsModel @Inject constructor(
modelScope.launch {
expressTransactionsEventListener.send(ExpressTransactionsEvent.Update)
}
subscribeToWithdrawOrder()
}
fun onPause() {
@ -148,6 +149,14 @@ internal class TangemPayDetailsModel @Inject constructor(
.launchIn(modelScope)
}
private fun subscribeToWithdrawOrder() {
modelScope.launch {
val userWallet = userWallet ?: getUserWalletUseCase(params.userWalletId).getOrNull()
?: return@launch
tangemPayWithdrawRepository.pollWithdrawOrderIfNeeds(userWallet)
}
}
override fun onClickPinCode() {
analytics.send(TangemPayAnalyticsEvents.PinCodeClicked())
if (!params.config.isPinSet) {
@ -278,24 +287,28 @@ internal class TangemPayDetailsModel @Inject constructor(
if (currentBalance == null || depositAddress == null) {
showBottomSheetError(TangemPayDetailsErrorType.Withdraw)
} else {
modelScope.launch {
val hasActiveWithdrawal = orderRepository.hasWithdrawOrder(userWalletId = params.userWalletId)
if (hasActiveWithdrawal) {
showBottomSheetError(TangemPayDetailsErrorType.WithdrawInProgress)
} else {
val userWallet = userWallet ?: getUserWalletUseCase(params.userWalletId).getOrNull()
val currency = cryptoCurrency ?: userWallet?.let {
tangemPayCryptoCurrencyFactory.create(userWallet = userWallet, chainId = params.config.chainId)
.getOrNull()
}
if (currency != null) {
uiMessageSender.send(
message = TangemPayMessagesFactory.createWithdrawWarning(
onGotItClick = { onConfirmWithdrawal(currency, currentBalance, depositAddress) },
),
)
val userWallet = userWallet ?: getUserWalletUseCase(params.userWalletId).getOrNull()
if (userWallet == null) {
showBottomSheetError(TangemPayDetailsErrorType.Withdraw)
} else {
modelScope.launch {
val hasActiveWithdrawal = tangemPayWithdrawRepository.hasWithdrawOrder(userWallet = userWallet)
if (hasActiveWithdrawal) {
showBottomSheetError(TangemPayDetailsErrorType.WithdrawInProgress)
} else {
showBottomSheetError(TangemPayDetailsErrorType.Withdraw)
val currency = cryptoCurrency ?: tangemPayCryptoCurrencyFactory.create(
userWallet = userWallet,
chainId = params.config.chainId,
).getOrNull()
if (currency != null) {
uiMessageSender.send(
message = TangemPayMessagesFactory.createWithdrawWarning(
onGotItClick = { onConfirmWithdrawal(currency, currentBalance, depositAddress) },
),
)
} else {
showBottomSheetError(TangemPayDetailsErrorType.Withdraw)
}
}
}
}