Updated on 2026-08-14

This commit is contained in:
Tangem 2025-06-26 11:04:35 +05:00
commit 62e4838d2f
22 changed files with 1171 additions and 0 deletions

View file

@ -2,7 +2,11 @@ package com.tangem.tap.di.domain
import com.tangem.domain.swap.SwapErrorResolver
import com.tangem.domain.swap.SwapRepositoryV2
import com.tangem.domain.swap.SwapTransactionRepository
import com.tangem.domain.swap.usecase.GetSwapPairsUseCase
import com.tangem.domain.swap.usecase.GetSwapQuoteUseCase
import com.tangem.domain.swap.usecase.GetSwapSupportedPairsUseCase
import com.tangem.domain.swap.usecase.SelectInitialPairUseCase
import com.tangem.feature.swap.domain.GetAvailablePairsUseCase
import dagger.Module
import dagger.Provides
@ -35,4 +39,38 @@ internal object SwapDomainModule {
swapErrorResolver = swapErrorResolver,
)
}
@Provides
@Singleton
fun provideGetSwapPairsUseCase(
swapRepositoryV2: SwapRepositoryV2,
swapErrorResolver: SwapErrorResolver,
): GetSwapPairsUseCase {
return GetSwapPairsUseCase(
swapRepositoryV2 = swapRepositoryV2,
swapErrorResolver = swapErrorResolver,
)
}
@Provides
@Singleton
fun provideSelectInitialPairUseCase(
swapTransactionRepository: SwapTransactionRepository,
): SelectInitialPairUseCase {
return SelectInitialPairUseCase(
swapTransactionRepository = swapTransactionRepository,
)
}
@Provides
@Singleton
fun provideGetSwapQuoteUseCase(
swapRepositoryV2: SwapRepositoryV2,
swapErrorResolver: SwapErrorResolver,
): GetSwapQuoteUseCase {
return GetSwapQuoteUseCase(
swapRepositoryV2 = swapRepositoryV2,
swapErrorResolver = swapErrorResolver,
)
}
}

View file

@ -6,6 +6,7 @@ import com.tangem.datasource.api.express.models.response.ExpressErrorResponse
import com.tangem.domain.onramp.model.error.OnrampError
import com.tangem.utils.converter.Converter
@Deprecated("Use ExpressErrorConverter")
internal class OnrampErrorConverter(
private val jsonAdapter: JsonAdapter<ExpressErrorResponse>,
) : Converter<String, OnrampError> {

View file

@ -9,9 +9,11 @@ import com.tangem.datasource.exchangeservice.swap.ExpressUtils
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.domain.express.ExpressRepository
import com.tangem.domain.express.models.ExpressProvider
import com.tangem.domain.express.models.ExpressRateType
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.swap.SwapRepositoryV2
import com.tangem.domain.swap.models.SwapPairModel
import com.tangem.domain.swap.models.SwapQuoteModel
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations
import com.tangem.domain.wallets.models.UserWallet
@ -22,6 +24,7 @@ import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.math.BigDecimal
import javax.inject.Inject
@Suppress("LongParameterList")
@ -34,6 +37,7 @@ internal class DefaultSwapRepositoryV2 @Inject constructor(
) : SwapRepositoryV2 {
private val tokenInfoConverter = TokenInfoConverter()
override suspend fun getPairs(
userWallet: UserWallet,
initialCurrency: CryptoCurrency,
@ -115,6 +119,40 @@ internal class DefaultSwapRepositoryV2 @Inject constructor(
}.awaitAll().filterNotNull()
}
override suspend fun getSwapQuote(
userWallet: UserWallet,
fromCryptoCurrency: CryptoCurrency,
toCryptoCurrency: CryptoCurrency,
fromAmount: BigDecimal,
provider: ExpressProvider,
rateType: ExpressRateType,
): SwapQuoteModel = withContext(coroutineDispatcher.io) {
val response = tangemExpressApi.getExchangeQuote(
fromAmount = fromAmount.movePointRight(fromCryptoCurrency.decimals).toString(),
fromNetwork = fromCryptoCurrency.network.backendId,
fromContractAddress = fromCryptoCurrency.getContractAddress(),
fromDecimals = fromCryptoCurrency.decimals,
toNetwork = toCryptoCurrency.network.backendId,
toContractAddress = toCryptoCurrency.getContractAddress(),
toDecimals = toCryptoCurrency.decimals,
providerId = provider.providerId,
rateType = rateType.name.lowercase(),
userWalletId = userWallet.walletId.stringValue,
refCode = ExpressUtils.getRefCode(
userWallet = userWallet,
appPreferencesStore = appPreferencesStore,
),
).getOrThrow()
val toTokenAmount = requireNotNull(response.toAmount.toBigDecimalOrNull()?.movePointLeft(response.toDecimals))
return@withContext SwapQuoteModel(
provider = provider,
toTokenAmount = toTokenAmount,
allowanceContract = response.allowanceContract,
)
}
private suspend fun CoroutineScope.getPairsInternal(
userWallet: UserWallet,
initialCurrency: CryptoCurrency,

View file

@ -0,0 +1,289 @@
package com.tangem.data.swap
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.data.common.currency.UserTokensResponseFactory
import com.tangem.data.swap.converter.transaction.SavedSwapStatusConverter
import com.tangem.data.swap.converter.transaction.SavedSwapTransactionConverter
import com.tangem.data.swap.converter.transaction.SavedSwapTransactionListConverter
import com.tangem.data.swap.models.LastSwappedCryptoCurrencyDTO
import com.tangem.data.swap.models.SwapStatusDTO
import com.tangem.data.swap.models.SwapTransactionDTO
import com.tangem.data.swap.models.SwapTransactionListDTO
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getObjectList
import com.tangem.datasource.local.preferences.utils.getObjectListSync
import com.tangem.datasource.local.preferences.utils.getObjectMapSync
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.swap.SwapTransactionRepository
import com.tangem.domain.swap.models.SwapStatusModel
import com.tangem.domain.swap.models.SwapTransactionListModel
import com.tangem.domain.swap.models.SwapTransactionModel
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.models.requireColdWallet
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.extensions.addOrReplace
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
internal class DefaultSwapTransactionRepository(
private val appPreferencesStore: AppPreferencesStore,
private val dispatchers: CoroutineDispatcherProvider,
excludedBlockchains: ExcludedBlockchains,
) : SwapTransactionRepository {
private val listConverter by lazy(LazyThreadSafetyMode.NONE) {
SavedSwapTransactionListConverter(excludedBlockchains)
}
private val converter by lazy(LazyThreadSafetyMode.NONE) {
SavedSwapTransactionConverter(excludedBlockchains)
}
private val savedStatusConverter by lazy(LazyThreadSafetyMode.NONE) {
SavedSwapStatusConverter()
}
private val userTokensResponseFactory = UserTokensResponseFactory()
override suspend fun storeTransaction(
userWalletId: UserWalletId,
fromCryptoCurrency: CryptoCurrency,
toCryptoCurrency: CryptoCurrency,
transaction: SwapTransactionModel,
) {
transaction.status?.let {
storeTransactionState(
txId = transaction.txId,
status = it,
refundTokenCurrency = null,
)
}
appPreferencesStore.editData { mutablePreferences ->
val savedTransactions: List<SwapTransactionListDTO>? = mutablePreferences.getObjectList(
key = PreferencesKeys.SWAP_TRANSACTIONS_KEY,
)
val tokenTransactions = savedTransactions
?.firstOrNull {
it.checkId(
checkUserWalletId = userWalletId,
fromCurrencyId = fromCryptoCurrency.id,
toCurrencyId = toCryptoCurrency.id,
)
}
?.transactions
?.addOrReplace(
item = converter.convert(transaction),
predicate = { it.txId == transaction.txId },
) ?: listOf(converter.convert(transaction))
mutablePreferences.setObject(
key = PreferencesKeys.SWAP_TRANSACTIONS_KEY,
value = savedTransactions?.updateList(
userWalletId = userWalletId,
fromCryptoCurrency = fromCryptoCurrency,
toCryptoCurrency = toCryptoCurrency,
transactions = tokenTransactions,
) ?: listOf(
listConverter.default(
userWalletId = userWalletId,
fromCryptoCurrency = fromCryptoCurrency,
toCryptoCurrency = toCryptoCurrency,
tokenTransactions = listOf(converter.convert(transaction)),
),
),
)
}
}
override suspend fun getTransactions(
userWallet: UserWallet,
cryptoCurrencyId: CryptoCurrency.ID,
): Flow<List<SwapTransactionListModel>?> {
return withContext(dispatchers.io) {
val txStatuses = appPreferencesStore.getObjectMapSync<SwapStatusDTO>(
key = PreferencesKeys.SWAP_TRANSACTIONS_STATUSES_KEY,
)
appPreferencesStore.getObjectList<SwapTransactionListDTO>(
key = PreferencesKeys.SWAP_TRANSACTIONS_KEY,
).map { savedTransactions ->
val currencyTxs = savedTransactions
?.filter {
it.userWalletId == userWallet.walletId.stringValue &&
(
it.toCryptoCurrencyId == cryptoCurrencyId.value ||
it.fromCryptoCurrencyId == cryptoCurrencyId.value
)
}
currencyTxs?.mapNotNull {
listConverter.convertBack(
value = it,
scanResponse = userWallet.requireColdWallet().scanResponse, // TODO [REDACTED_TASK_KEY]
txStatuses = txStatuses,
)
}
}.flowOn(dispatchers.io)
}
}
override suspend fun removeTransaction(
userWalletId: UserWalletId,
fromCryptoCurrency: CryptoCurrency,
toCryptoCurrency: CryptoCurrency,
txId: String,
) {
clearTransactionsStatuses(txId = txId)
appPreferencesStore.editData { mutablePreferences ->
val savedList: List<SwapTransactionListDTO>? = mutablePreferences.getObjectList(
key = PreferencesKeys.SWAP_TRANSACTIONS_KEY,
)
val tokenTransactions = savedList
?.firstOrNull {
it.checkId(
checkUserWalletId = userWalletId,
fromCurrencyId = fromCryptoCurrency.id,
toCurrencyId = toCryptoCurrency.id,
)
}
?.transactions
?.filterNot { it.txId == txId }
val editedList =
if (tokenTransactions.isNullOrEmpty()) {
savedList?.filterNot {
it.checkId(
checkUserWalletId = userWalletId,
fromCurrencyId = fromCryptoCurrency.id,
toCurrencyId = toCryptoCurrency.id,
)
}
} else {
savedList.updateList(
userWalletId = userWalletId,
fromCryptoCurrency = fromCryptoCurrency,
toCryptoCurrency = toCryptoCurrency,
transactions = tokenTransactions,
)
}
if (editedList.isNullOrEmpty()) {
mutablePreferences.remove(key = PreferencesKeys.SWAP_TRANSACTIONS_KEY)
} else {
mutablePreferences.setObject(
key = PreferencesKeys.SWAP_TRANSACTIONS_KEY,
value = editedList,
)
}
}
}
override suspend fun storeTransactionState(
txId: String,
status: SwapStatusModel,
refundTokenCurrency: CryptoCurrency?,
) {
appPreferencesStore.editData { mutablePreferences ->
val savedMap = mutablePreferences.getObjectMap<SwapStatusDTO>(
key = PreferencesKeys.SWAP_TRANSACTIONS_STATUSES_KEY,
)
val updatesMap = savedMap.toMutableMap()
updatesMap[txId] = savedStatusConverter.convertBack(
status.copy(
refundTokensResponse = refundTokenCurrency?.let {
userTokensResponseFactory.createResponseToken(refundTokenCurrency)
},
),
)
mutablePreferences.setObjectMap(
key = PreferencesKeys.SWAP_TRANSACTIONS_STATUSES_KEY,
value = updatesMap,
)
}
}
override suspend fun getLastSwappedCryptoCurrencyId(userWalletId: UserWalletId): String? {
val lastSwappedCurrencies = appPreferencesStore.getObjectListSync<LastSwappedCryptoCurrencyDTO>(
key = PreferencesKeys.LAST_SWAPPED_CRYPTOCURRENCY_ID_KEY,
)
return lastSwappedCurrencies.find { userWalletId.stringValue == it.userWalletId }?.cryptoCurrencyId
}
override suspend fun storeLastSwappedCryptoCurrencyId(
userWalletId: UserWalletId,
cryptoCurrencyId: CryptoCurrency.ID,
) {
appPreferencesStore.editData { mutablePreferences ->
val lastSwappedCryptoCurrencies: List<LastSwappedCryptoCurrencyDTO>? = mutablePreferences.getObjectList(
key = PreferencesKeys.LAST_SWAPPED_CRYPTOCURRENCY_ID_KEY,
)
val newList = if (lastSwappedCryptoCurrencies != null) {
lastSwappedCryptoCurrencies.filter {
it.userWalletId != userWalletId.stringValue
} + LastSwappedCryptoCurrencyDTO(userWalletId.stringValue, cryptoCurrencyId.value)
} else {
listOf(LastSwappedCryptoCurrencyDTO(userWalletId.stringValue, cryptoCurrencyId.value))
}
mutablePreferences.setObjectList(
key = PreferencesKeys.LAST_SWAPPED_CRYPTOCURRENCY_ID_KEY,
value = newList,
)
}
}
private fun SwapTransactionListDTO.checkId(
checkUserWalletId: UserWalletId,
fromCurrencyId: CryptoCurrency.ID,
toCurrencyId: CryptoCurrency.ID,
): Boolean {
return userWalletId == checkUserWalletId.stringValue &&
toCryptoCurrencyId == toCurrencyId.value &&
fromCryptoCurrencyId == fromCurrencyId.value
}
private fun List<SwapTransactionListDTO>.updateList(
userWalletId: UserWalletId,
fromCryptoCurrency: CryptoCurrency,
toCryptoCurrency: CryptoCurrency,
transactions: List<SwapTransactionDTO>,
): List<SwapTransactionListDTO> {
return addOrReplace(
item = listConverter.default(
userWalletId = userWalletId,
fromCryptoCurrency = fromCryptoCurrency,
toCryptoCurrency = toCryptoCurrency,
tokenTransactions = transactions,
),
predicate = {
it.checkId(
checkUserWalletId = userWalletId,
fromCurrencyId = fromCryptoCurrency.id,
toCurrencyId = toCryptoCurrency.id,
)
},
)
}
private suspend fun clearTransactionsStatuses(txId: String) {
appPreferencesStore.editData { mutablePreferences ->
val savedList = mutablePreferences.getObjectMap<SwapStatusModel>(
key = PreferencesKeys.SWAP_TRANSACTIONS_STATUSES_KEY,
)
val editedList = savedList.filterNot { it.key == txId }
if (editedList.isEmpty()) {
mutablePreferences.remove(key = PreferencesKeys.SWAP_TRANSACTIONS_STATUSES_KEY)
} else {
mutablePreferences.setObjectMap(
key = PreferencesKeys.SWAP_TRANSACTIONS_STATUSES_KEY,
value = editedList,
)
}
}
}
}

View file

@ -0,0 +1,24 @@
package com.tangem.data.swap.converter
import com.tangem.datasource.api.express.models.response.ExchangeStatusResponse
import com.tangem.domain.swap.models.SwapStatus
import com.tangem.domain.swap.models.SwapStatusModel
import com.tangem.utils.converter.Converter
internal class SwapStatusConverter : Converter<ExchangeStatusResponse, SwapStatusModel> {
override fun convert(value: ExchangeStatusResponse): SwapStatusModel {
return SwapStatusModel(
providerId = value.providerId,
status = SwapStatus.entries.firstOrNull {
it.name.lowercase() == value.status.name.lowercase()
},
txId = value.externalTxId,
txExternalUrl = value.externalTxUrl,
txExternalId = value.externalTxId,
refundNetwork = value.refundNetwork,
refundContractAddress = value.refundContractAddress,
createdAt = value.createdAt,
averageDuration = value.averageDuration,
)
}
}

View file

@ -0,0 +1,38 @@
package com.tangem.data.swap.converter.transaction
import com.tangem.data.swap.models.SavedSwapStatus
import com.tangem.data.swap.models.SwapStatusDTO
import com.tangem.domain.swap.models.SwapStatus
import com.tangem.domain.swap.models.SwapStatusModel
import com.tangem.utils.converter.TwoWayConverter
internal class SavedSwapStatusConverter : TwoWayConverter<SwapStatusDTO, SwapStatusModel> {
override fun convert(value: SwapStatusDTO) = SwapStatusModel(
providerId = value.providerId,
status = SwapStatus.entries.firstOrNull {
it.name.lowercase() == value.status?.name?.lowercase()
},
txId = value.txExternalId,
txExternalUrl = value.txExternalUrl,
txExternalId = value.txExternalId,
refundNetwork = value.refundNetwork,
refundContractAddress = value.refundContractAddress,
createdAt = value.createdAt,
averageDuration = value.averageDuration,
)
override fun convertBack(value: SwapStatusModel) = SwapStatusDTO(
providerId = value.providerId,
status = SavedSwapStatus.entries.firstOrNull {
it.name.lowercase() == value.status?.name?.lowercase()
},
txId = value.txExternalId,
txExternalUrl = value.txExternalUrl,
txExternalId = value.txExternalId,
refundNetwork = value.refundNetwork,
refundContractAddress = value.refundContractAddress,
createdAt = value.createdAt,
averageDuration = value.averageDuration,
)
}

View file

@ -0,0 +1,60 @@
package com.tangem.data.swap.converter.transaction
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory
import com.tangem.data.swap.models.SwapStatusDTO
import com.tangem.data.swap.models.SwapTransactionDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.swap.models.SwapTransactionModel
import com.tangem.utils.converter.TwoWayConverter
internal class SavedSwapTransactionConverter(
excludedBlockchains: ExcludedBlockchains,
) : TwoWayConverter<SwapTransactionModel, SwapTransactionDTO> {
private val responseCryptoCurrenciesFactory = ResponseCryptoCurrenciesFactory(excludedBlockchains)
private val statusConverter by lazy(LazyThreadSafetyMode.NONE) {
SavedSwapStatusConverter()
}
override fun convert(value: SwapTransactionModel) = SwapTransactionDTO(
txId = value.txId,
timestamp = value.timestamp,
fromCryptoAmount = value.fromCryptoAmount,
toCryptoAmount = value.toCryptoAmount,
provider = value.provider,
status = value.status,
)
override fun convertBack(value: SwapTransactionDTO) = SwapTransactionModel(
txId = value.txId,
timestamp = value.timestamp,
fromCryptoAmount = value.fromCryptoAmount,
toCryptoAmount = value.toCryptoAmount,
provider = value.provider,
status = value.status,
)
fun convertBack(
value: SwapTransactionDTO,
scanResponse: ScanResponse,
txStatuses: Map<String, SwapStatusDTO>,
): SwapTransactionModel {
val status = txStatuses[value.txId]
val refundCurrency = status?.refundTokensResponse?.let { id ->
responseCryptoCurrenciesFactory.createCurrency(
responseToken = id,
scanResponse = scanResponse,
)
}
val statusWithRefundCurrency = status?.copy(refundCurrency = refundCurrency)
return SwapTransactionModel(
txId = value.txId,
timestamp = value.timestamp,
fromCryptoAmount = value.fromCryptoAmount,
toCryptoAmount = value.toCryptoAmount,
provider = value.provider,
status = statusWithRefundCurrency?.let(statusConverter::convert),
)
}
}

View file

@ -0,0 +1,83 @@
package com.tangem.data.swap.converter.transaction
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory
import com.tangem.data.common.currency.UserTokensResponseFactory
import com.tangem.data.swap.models.SwapStatusDTO
import com.tangem.data.swap.models.SwapTransactionDTO
import com.tangem.data.swap.models.SwapTransactionListDTO
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.swap.models.SwapTransactionListModel
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.converter.Converter
internal class SavedSwapTransactionListConverter(
excludedBlockchains: ExcludedBlockchains,
) : Converter<SwapTransactionListModel, SwapTransactionListDTO> {
private val responseCryptoCurrenciesFactory = ResponseCryptoCurrenciesFactory(excludedBlockchains)
private val userTokensResponseFactory = UserTokensResponseFactory()
private val savedSwapTransactionConverter by lazy(LazyThreadSafetyMode.NONE) {
SavedSwapTransactionConverter(excludedBlockchains)
}
override fun convert(value: SwapTransactionListModel) = SwapTransactionListDTO(
userWalletId = value.userWalletId,
fromCryptoCurrencyId = value.fromCryptoCurrencyId,
toCryptoCurrencyId = value.toCryptoCurrencyId,
fromTokensResponse = userTokensResponseFactory.createResponseToken(
value.fromCryptoCurrency,
),
toTokensResponse = userTokensResponseFactory.createResponseToken(
value.toCryptoCurrency,
),
transactions = savedSwapTransactionConverter.convertList(value.transactions),
)
fun convertBack(
value: SwapTransactionListDTO,
scanResponse: ScanResponse,
txStatuses: Map<String, SwapStatusDTO>,
): SwapTransactionListModel? {
val fromToken = value.fromTokensResponse
val toToken = value.toTokensResponse
return if (fromToken == null || toToken == null) {
null
} else {
val fromCryptoCurrency = responseCryptoCurrenciesFactory.createCurrency(
responseToken = fromToken,
scanResponse = scanResponse,
) ?: return null
val toCryptoCurrency = responseCryptoCurrenciesFactory.createCurrency(
responseToken = toToken,
scanResponse = scanResponse,
) ?: return null
return SwapTransactionListModel(
transactions = value.transactions.map { tx ->
savedSwapTransactionConverter.convertBack(tx, scanResponse, txStatuses)
},
userWalletId = value.userWalletId,
fromCryptoCurrencyId = value.fromCryptoCurrencyId,
toCryptoCurrencyId = value.toCryptoCurrencyId,
fromCryptoCurrency = fromCryptoCurrency,
toCryptoCurrency = toCryptoCurrency,
)
}
}
fun default(
userWalletId: UserWalletId,
fromCryptoCurrency: CryptoCurrency,
toCryptoCurrency: CryptoCurrency,
tokenTransactions: List<SwapTransactionDTO>,
) = SwapTransactionListDTO(
userWalletId = userWalletId.stringValue,
fromCryptoCurrencyId = fromCryptoCurrency.id.value,
toCryptoCurrencyId = toCryptoCurrency.id.value,
fromTokensResponse = userTokensResponseFactory.createResponseToken(fromCryptoCurrency),
toTokensResponse = userTokensResponseFactory.createResponseToken(toCryptoCurrency),
transactions = tokenTransactions,
)
}

View file

@ -1,9 +1,11 @@
package com.tangem.data.swap.di
import com.squareup.moshi.Moshi
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.data.express.converter.ExpressErrorConverter
import com.tangem.data.swap.DefaultSwapErrorResolver
import com.tangem.data.swap.DefaultSwapRepositoryV2
import com.tangem.data.swap.DefaultSwapTransactionRepository
import com.tangem.datasource.api.express.TangemExpressApi
import com.tangem.datasource.api.express.models.response.ExpressErrorResponse
import com.tangem.datasource.di.NetworkMoshi
@ -11,6 +13,7 @@ import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.domain.express.ExpressRepository
import com.tangem.domain.swap.SwapErrorResolver
import com.tangem.domain.swap.SwapRepositoryV2
import com.tangem.domain.swap.SwapTransactionRepository
import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
@ -49,4 +52,18 @@ internal object SwapDataModule {
currencyStatusOperations = currencyStatusOperations,
)
}
@Provides
@Singleton
fun provideSwapTransactionRepository(
coroutineDispatcher: CoroutineDispatcherProvider,
appPreferencesStore: AppPreferencesStore,
excludedBlockchains: ExcludedBlockchains,
): SwapTransactionRepository {
return DefaultSwapTransactionRepository(
appPreferencesStore = appPreferencesStore,
dispatchers = coroutineDispatcher,
excludedBlockchains = excludedBlockchains,
)
}
}

View file

@ -0,0 +1,12 @@
package com.tangem.data.swap.models
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class LastSwappedCryptoCurrencyDTO(
@Json(name = "userWalletId")
val userWalletId: String,
@Json(name = "cryptoCurrencyId")
val cryptoCurrencyId: String,
)

View file

@ -0,0 +1,78 @@
package com.tangem.data.swap.models
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.domain.models.currency.CryptoCurrency
import org.joda.time.DateTime
@JsonClass(generateAdapter = true)
internal data class SwapStatusDTO(
@Json(name = "providerId")
val providerId: String,
@Json(name = "status")
val status: SavedSwapStatus? = null,
@Json(name = "txId")
val txId: String? = null,
@Json(name = "txExternalUrl")
val txExternalUrl: String? = null,
@Json(name = "txExternalId")
val txExternalId: String? = null,
@Json(name = "refundNetwork")
val refundNetwork: String? = null,
@Json(name = "refundContractAddress")
val refundContractAddress: String? = null,
@Json(name = "refundTokensResponse")
val refundTokensResponse: UserTokensResponse.Token? = null,
@Json(ignore = true)
val refundCurrency: CryptoCurrency? = null,
@Json(name = "createdAt")
val createdAt: DateTime? = null,
@Json(name = "averageDuration")
val averageDuration: Int? = null,
)
@JsonClass(generateAdapter = false)
internal enum class SavedSwapStatus {
@Json(name = "New")
New,
@Json(name = "Waiting")
Waiting,
@Json(name = "WaitingTxHash")
WaitingTxHash,
@Json(name = "Confirming")
Confirming,
@Json(name = "Verifying")
Verifying,
@Json(name = "Exchanging")
Exchanging,
@Json(name = "Failed")
Failed,
@Json(name = "Sending")
Sending,
@Json(name = "Finished")
Finished,
@Json(name = "Refunded")
Refunded,
@Json(name = "Cancelled")
Cancelled,
@Json(name = "TxFailed")
TxFailed,
@Json(name = "Unknown")
Unknown,
@Json(name = "Paused")
Paused,
}

View file

@ -0,0 +1,40 @@
package com.tangem.data.swap.models
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.domain.express.models.ExpressProvider
import com.tangem.domain.swap.models.SwapStatusModel
import java.math.BigDecimal
@JsonClass(generateAdapter = true)
internal data class SwapTransactionListDTO(
@Json(name = "userWalletId")
val userWalletId: String,
@Json(name = "fromCryptoCurrencyId")
val fromCryptoCurrencyId: String,
@Json(name = "toCryptoCurrencyId")
val toCryptoCurrencyId: String,
@Json(name = "fromTokensResponse")
val fromTokensResponse: UserTokensResponse.Token? = null,
@Json(name = "toTokensResponse")
val toTokensResponse: UserTokensResponse.Token? = null,
@Json(name = "transactions")
val transactions: List<SwapTransactionDTO>,
)
@JsonClass(generateAdapter = true)
internal data class SwapTransactionDTO(
@Json(name = "txId")
val txId: String,
@Json(name = "timestamp")
val timestamp: Long,
@Json(name = "fromCryptoAmount")
val fromCryptoAmount: BigDecimal,
@Json(name = "toCryptoAmount")
val toCryptoAmount: BigDecimal,
@Json(name = "provider")
val provider: ExpressProvider,
@Json(name = "status")
val status: SwapStatusModel? = null,
)

View file

@ -3,17 +3,43 @@ package com.tangem.domain.swap.models
import com.tangem.domain.express.models.ExpressProvider
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
/**
* Model of currencies available to swap
*
* @param fromGroup group of currencies available swap from
* @param toGroup group of currencies available swap to
*/
data class SwapCurrencies(
val fromGroup: SwapCurrenciesGroup,
val toGroup: SwapCurrenciesGroup,
)
/**
* Return swap group depending on [swapDirection]
*/
fun SwapCurrencies.getGroupWithReverse(swapDirection: SwapDirection): SwapCurrenciesGroup {
return when (swapDirection) {
SwapDirection.Reverse -> fromGroup
SwapDirection.Direct -> toGroup
}
}
/**
* Swap group model
*
* @param available list of available currencies to swap
* @param available list of unavailable currencies to swap
* @param afterSearch flag indicates whether user searched token
*/
data class SwapCurrenciesGroup(
val available: List<SwapCryptoCurrency>,
val unavailable: List<SwapCryptoCurrency>,
val afterSearch: Boolean,
)
/**
* Crypto currency with providers to swap
*/
data class SwapCryptoCurrency(
val currencyStatus: CryptoCurrencyStatus,
val providers: List<ExpressProvider>,

View file

@ -0,0 +1,16 @@
package com.tangem.domain.swap.models
import com.tangem.domain.swap.models.SwapDirection.Direct
import com.tangem.domain.swap.models.SwapDirection.Reverse
/**
* Swap direction
*
* Initial currency can be swap to or from.
* If swap being swap from direction is [Direct],
* Otherwise direction is [Reverse]
*/
enum class SwapDirection {
Direct,
Reverse,
}

View file

@ -0,0 +1,17 @@
package com.tangem.domain.swap.models
import com.tangem.domain.express.models.ExpressProvider
import java.math.BigDecimal
/**
* Quote model holds data about current amounts of swaps and fees
*
* @property provider swap provider
* @property toTokenAmount amount of token you want to receive
* @property allowanceContract whether swap occurs via third token
*/
data class SwapQuoteModel(
val provider: ExpressProvider,
val toTokenAmount: BigDecimal,
val allowanceContract: String?,
)

View file

@ -0,0 +1,62 @@
package com.tangem.domain.swap.models
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.domain.models.currency.CryptoCurrency
import org.joda.time.DateTime
/**
* Swapped status model
*/
data class SwapStatusModel(
val providerId: String,
val status: SwapStatus? = null,
val txId: String? = null,
val txExternalUrl: String? = null,
val txExternalId: String? = null,
val refundNetwork: String? = null,
val refundContractAddress: String? = null,
val refundTokensResponse: UserTokensResponse.Token? = null,
val refundCurrency: CryptoCurrency? = null,
val createdAt: DateTime? = null,
val averageDuration: Int? = null,
) {
val hasLongTime: Boolean
get() = if (createdAt != null && averageDuration != null) {
DateTime.now().minusSeconds(averageDuration * 5) > createdAt
} else {
false
}
}
enum class SwapStatus {
New,
Waiting,
WaitingTxHash,
Confirming,
Verifying,
Exchanging,
Failed,
Sending,
Finished,
Refunded,
Cancelled,
TxFailed,
Unknown,
Paused,
;
val isTerminal: Boolean
get() = this == Refunded ||
this == Finished ||
this == Cancelled ||
this == TxFailed ||
this == Paused ||
this == Unknown
val isAutoDisposable: Boolean
get() = this == Finished
companion object {
fun SwapStatus?.isFailed(): Boolean = this == Failed || this == TxFailed
}
}

View file

@ -0,0 +1,29 @@
package com.tangem.domain.swap.models
import com.tangem.domain.express.models.ExpressProvider
import com.tangem.domain.models.currency.CryptoCurrency
import java.math.BigDecimal
/**
* List of saved swap transactions
*/
data class SwapTransactionListModel(
val userWalletId: String,
val fromCryptoCurrencyId: String,
val toCryptoCurrencyId: String,
val fromCryptoCurrency: CryptoCurrency,
val toCryptoCurrency: CryptoCurrency,
val transactions: List<SwapTransactionModel>,
)
/**
* Saved swap transactions
*/
data class SwapTransactionModel(
val txId: String,
val timestamp: Long,
val fromCryptoAmount: BigDecimal,
val toCryptoAmount: BigDecimal,
val provider: ExpressProvider,
val status: SwapStatusModel? = null,
)

View file

@ -1,10 +1,17 @@
package com.tangem.domain.swap
import com.tangem.domain.express.models.ExpressProvider
import com.tangem.domain.express.models.ExpressRateType
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.swap.models.SwapPairModel
import com.tangem.domain.swap.models.SwapQuoteModel
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.wallets.models.UserWallet
import java.math.BigDecimal
/**
* Swap repository
*/
interface SwapRepositoryV2 {
/**
@ -26,4 +33,24 @@ interface SwapRepositoryV2 {
initialCurrency: CryptoCurrency,
cryptoCurrencyList: List<CryptoCurrency>,
): List<SwapPairModel>
/**
* Returns swap quotes on selected pair
*
* @param userWallet selected user wallet
* @param fromCryptoCurrency currency being swapped from
* @param toCryptoCurrency currency being swapped to
* @param fromAmount swap amount
* @param provider selected express provider
* @param rateType rate type
*/
@Suppress("LongParameterList")
suspend fun getSwapQuote(
userWallet: UserWallet,
fromCryptoCurrency: CryptoCurrency,
toCryptoCurrency: CryptoCurrency,
fromAmount: BigDecimal,
provider: ExpressProvider,
rateType: ExpressRateType,
): SwapQuoteModel
}

View file

@ -0,0 +1,80 @@
package com.tangem.domain.swap
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.swap.models.SwapStatusModel
import com.tangem.domain.swap.models.SwapTransactionListModel
import com.tangem.domain.swap.models.SwapTransactionModel
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.Flow
/**
* Swap repository for statuses
*/
interface SwapTransactionRepository {
/**
* Store new swap transaction
*
* @param userWalletId selected user wallet id
* @param fromCryptoCurrency currency swap from
* @param toCryptoCurrency currency swap to
* @param transaction swap transaction
*/
suspend fun storeTransaction(
userWalletId: UserWalletId,
fromCryptoCurrency: CryptoCurrency,
toCryptoCurrency: CryptoCurrency,
transaction: SwapTransactionModel,
)
/**
* Get list of swap transactions
*
* @param userWallet selected user wallet
* @param cryptoCurrencyId transactions for specific crypto currency
*/
suspend fun getTransactions(
userWallet: UserWallet,
cryptoCurrencyId: CryptoCurrency.ID,
): Flow<List<SwapTransactionListModel>?>
/**
* Remove stores swap transaction
*
* @param userWalletId selected user wallet id
* @param fromCryptoCurrency currency swap from
* @param toCryptoCurrency currency swap to
* @param txId transaction id to remove
*/
suspend fun removeTransaction(
userWalletId: UserWalletId,
fromCryptoCurrency: CryptoCurrency,
toCryptoCurrency: CryptoCurrency,
txId: String,
)
/**
* Update swap transaction
*
* @param txId transaction id to update
* @param status new transaction status
* @param refundTokenCurrency refund token
*/
suspend fun storeTransactionState(txId: String, status: SwapStatusModel, refundTokenCurrency: CryptoCurrency?)
/**
* Save last swapped crypto currency token
*
* @param userWalletId selected user wallet id
* @param cryptoCurrencyId last swapped currency id
*/
suspend fun storeLastSwappedCryptoCurrencyId(userWalletId: UserWalletId, cryptoCurrencyId: CryptoCurrency.ID)
/**
* Get last swapped crypto currency token
*
* @param userWalletId selected user wallet id
*/
suspend fun getLastSwappedCryptoCurrencyId(userWalletId: UserWalletId): String?
}

View file

@ -0,0 +1,80 @@
package com.tangem.domain.swap.usecase
import arrow.core.Either
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.swap.SwapErrorResolver
import com.tangem.domain.swap.SwapRepositoryV2
import com.tangem.domain.swap.models.SwapCryptoCurrency
import com.tangem.domain.swap.models.SwapCurrencies
import com.tangem.domain.swap.models.SwapCurrenciesGroup
import com.tangem.domain.swap.models.SwapPairModel
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.wallets.models.UserWallet
/**
* Get list of swap pairs
*/
class GetSwapPairsUseCase(
private val swapRepositoryV2: SwapRepositoryV2,
private val swapErrorResolver: SwapErrorResolver,
) {
/**
* @param userWallet selected user wallet
* @param initialCurrency initial currency to swap (to or from)
* @param cryptoCurrencyStatusList list of added cryptocurrencies
*/
suspend operator fun invoke(
userWallet: UserWallet,
initialCurrency: CryptoCurrency,
cryptoCurrencyStatusList: List<CryptoCurrencyStatus>,
) = Either.catch {
val pairs = swapRepositoryV2.getPairs(
userWallet = userWallet,
initialCurrency = initialCurrency,
cryptoCurrencyStatusList = cryptoCurrencyStatusList,
)
val fromGroup = pairs.groupPairs(
initialCurrency = initialCurrency,
filteringCurrency = { it.from },
groupingCurrency = { it.to },
cryptoCurrencyStatusList = cryptoCurrencyStatusList,
)
val toGroup = pairs.groupPairs(
initialCurrency = initialCurrency,
filteringCurrency = { it.to },
groupingCurrency = { it.from },
cryptoCurrencyStatusList = cryptoCurrencyStatusList,
)
SwapCurrencies(
fromGroup = fromGroup,
toGroup = toGroup,
)
}.mapLeft(swapErrorResolver::resolve)
private fun List<SwapPairModel>.groupPairs(
initialCurrency: CryptoCurrency,
filteringCurrency: (SwapPairModel) -> CryptoCurrencyStatus,
groupingCurrency: (SwapPairModel) -> CryptoCurrencyStatus,
cryptoCurrencyStatusList: List<CryptoCurrencyStatus>,
): SwapCurrenciesGroup {
val availableCryptoCurrencies = filter { pair -> filteringCurrency(pair).currency.id == initialCurrency.id }
// Search available to swap currency
.filter { pair ->
cryptoCurrencyStatusList.any { currencyStatus ->
currencyStatus.currency.id == pair.to.currency.id
}
}.map { pair -> SwapCryptoCurrency(groupingCurrency(pair), pair.providers) }
val unavailableCryptoCurrencies =
cryptoCurrencyStatusList - availableCryptoCurrencies.map { it.currencyStatus }.toSet()
return SwapCurrenciesGroup(
available = availableCryptoCurrencies,
unavailable = unavailableCryptoCurrencies.map { pair -> SwapCryptoCurrency(pair, emptyList()) },
afterSearch = false,
)
}
}

View file

@ -0,0 +1,45 @@
package com.tangem.domain.swap.usecase
import arrow.core.Either
import com.tangem.domain.express.models.ExpressError
import com.tangem.domain.express.models.ExpressProvider
import com.tangem.domain.express.models.ExpressRateType
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.swap.SwapErrorResolver
import com.tangem.domain.swap.SwapRepositoryV2
import com.tangem.domain.swap.models.SwapQuoteModel
import com.tangem.domain.wallets.models.UserWallet
import java.math.BigDecimal
/**
* Get swap quote for selected pair
*/
class GetSwapQuoteUseCase(
private val swapRepositoryV2: SwapRepositoryV2,
private val swapErrorResolver: SwapErrorResolver,
) {
/**
* @param userWallet selected user wallet
* @param fromCryptoCurrency currency swap from
* @param toCryptoCurrency currency swap to
* @param fromAmount swap amount
* @param provider swap provider
*/
suspend operator fun invoke(
userWallet: UserWallet,
fromCryptoCurrency: CryptoCurrency,
toCryptoCurrency: CryptoCurrency,
fromAmount: BigDecimal,
provider: ExpressProvider,
): Either<ExpressError, SwapQuoteModel> = Either.catch {
swapRepositoryV2.getSwapQuote(
userWallet = userWallet,
fromCryptoCurrency = fromCryptoCurrency,
toCryptoCurrency = toCryptoCurrency,
fromAmount = fromAmount,
provider = provider,
rateType = ExpressRateType.Float, // todo rate type
)
}.mapLeft(swapErrorResolver::resolve)
}

View file

@ -0,0 +1,71 @@
package com.tangem.domain.swap.usecase
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.swap.SwapTransactionRepository
import com.tangem.domain.swap.models.SwapCurrencies
import com.tangem.domain.swap.models.SwapCurrenciesGroup
import com.tangem.domain.swap.models.SwapDirection
import com.tangem.domain.swap.models.getGroupWithReverse
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.utils.extensions.orZero
/**
* Select initial pair to swap
*/
class SelectInitialPairUseCase(
private val swapTransactionRepository: SwapTransactionRepository,
) {
/**
* @param userWallet selected user wallet
* @param primaryCryptoCurrency initial currency
* @param secondaryCryptoCurrency selected currency
* @param swapCurrencies list of currencies with providers
* @param swapDirection swap direction
*/
suspend operator fun invoke(
userWallet: UserWallet,
primaryCryptoCurrency: CryptoCurrency,
secondaryCryptoCurrency: CryptoCurrency?,
swapCurrencies: SwapCurrencies,
swapDirection: SwapDirection,
): CryptoCurrencyStatus? {
val swapCurrenciesGroup = swapCurrencies.getGroupWithReverse(swapDirection)
return tryToGetAlreadySelectedCurrency(secondaryCryptoCurrency, swapCurrenciesGroup)
?: tryGetFromCache(userWallet, primaryCryptoCurrency, swapCurrenciesGroup)
?: tryGetWithMaxAmount(swapCurrenciesGroup)
?: swapCurrenciesGroup.available.firstOrNull()?.currencyStatus
}
private fun tryToGetAlreadySelectedCurrency(
secondaryCryptoCurrency: CryptoCurrency?,
swapCurrenciesGroup: SwapCurrenciesGroup,
): CryptoCurrencyStatus? {
return secondaryCryptoCurrency?.let {
swapCurrenciesGroup.available.firstOrNull {
secondaryCryptoCurrency.id == it.currencyStatus.currency.id
}?.currencyStatus
}
}
private suspend fun tryGetFromCache(
userWallet: UserWallet,
primaryCryptoCurrency: CryptoCurrency,
swapCurrenciesGroup: SwapCurrenciesGroup,
): CryptoCurrencyStatus? {
val id = swapTransactionRepository.getLastSwappedCryptoCurrencyId(userWallet.walletId) ?: return null
return if (id != primaryCryptoCurrency.id.value) {
swapCurrenciesGroup.available.find { it.currencyStatus.currency.id.value == id }?.currencyStatus
} else {
null
}
}
private fun tryGetWithMaxAmount(swapCurrenciesGroup: SwapCurrenciesGroup): CryptoCurrencyStatus? {
return swapCurrenciesGroup.available.maxByOrNull {
it.currencyStatus.value.fiatAmount.orZero()
}?.currencyStatus
}
}