diff --git a/app/src/main/java/com/tangem/tap/di/domain/SwapDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/SwapDomainModule.kt index a392d9c30e..f0ba15b856 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/SwapDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/SwapDomainModule.kt @@ -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, + ) + } } \ No newline at end of file diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/converters/error/OnrampErrorConverter.kt b/data/onramp/src/main/java/com/tangem/data/onramp/converters/error/OnrampErrorConverter.kt index 895da30c18..1abd3b4d3d 100644 --- a/data/onramp/src/main/java/com/tangem/data/onramp/converters/error/OnrampErrorConverter.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/converters/error/OnrampErrorConverter.kt @@ -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, ) : Converter { diff --git a/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt b/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt index 1931f346f2..9adca6af89 100644 --- a/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt +++ b/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt @@ -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, diff --git a/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapTransactionRepository.kt b/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapTransactionRepository.kt new file mode 100644 index 0000000000..5329db524a --- /dev/null +++ b/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapTransactionRepository.kt @@ -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? = 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?> { + return withContext(dispatchers.io) { + val txStatuses = appPreferencesStore.getObjectMapSync( + key = PreferencesKeys.SWAP_TRANSACTIONS_STATUSES_KEY, + ) + appPreferencesStore.getObjectList( + 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? = 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( + 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( + 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? = 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.updateList( + userWalletId: UserWalletId, + fromCryptoCurrency: CryptoCurrency, + toCryptoCurrency: CryptoCurrency, + transactions: List, + ): List { + 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( + 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, + ) + } + } + } +} \ No newline at end of file diff --git a/data/swap/src/main/java/com/tangem/data/swap/converter/SwapStatusConverter.kt b/data/swap/src/main/java/com/tangem/data/swap/converter/SwapStatusConverter.kt new file mode 100644 index 0000000000..33ae4fa064 --- /dev/null +++ b/data/swap/src/main/java/com/tangem/data/swap/converter/SwapStatusConverter.kt @@ -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 { + 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, + ) + } +} \ No newline at end of file diff --git a/data/swap/src/main/java/com/tangem/data/swap/converter/transaction/SavedSwapStatusConverter.kt b/data/swap/src/main/java/com/tangem/data/swap/converter/transaction/SavedSwapStatusConverter.kt new file mode 100644 index 0000000000..c3d8f36903 --- /dev/null +++ b/data/swap/src/main/java/com/tangem/data/swap/converter/transaction/SavedSwapStatusConverter.kt @@ -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 { + + 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, + ) +} \ No newline at end of file diff --git a/data/swap/src/main/java/com/tangem/data/swap/converter/transaction/SavedSwapTransactionConverter.kt b/data/swap/src/main/java/com/tangem/data/swap/converter/transaction/SavedSwapTransactionConverter.kt new file mode 100644 index 0000000000..ab7b21e61d --- /dev/null +++ b/data/swap/src/main/java/com/tangem/data/swap/converter/transaction/SavedSwapTransactionConverter.kt @@ -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 { + 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, + ): 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), + ) + } +} \ No newline at end of file diff --git a/data/swap/src/main/java/com/tangem/data/swap/converter/transaction/SavedSwapTransactionListConverter.kt b/data/swap/src/main/java/com/tangem/data/swap/converter/transaction/SavedSwapTransactionListConverter.kt new file mode 100644 index 0000000000..176c1eb8f6 --- /dev/null +++ b/data/swap/src/main/java/com/tangem/data/swap/converter/transaction/SavedSwapTransactionListConverter.kt @@ -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 { + + 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, + ): 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, + ) = SwapTransactionListDTO( + userWalletId = userWalletId.stringValue, + fromCryptoCurrencyId = fromCryptoCurrency.id.value, + toCryptoCurrencyId = toCryptoCurrency.id.value, + fromTokensResponse = userTokensResponseFactory.createResponseToken(fromCryptoCurrency), + toTokensResponse = userTokensResponseFactory.createResponseToken(toCryptoCurrency), + transactions = tokenTransactions, + ) +} \ No newline at end of file diff --git a/data/swap/src/main/java/com/tangem/data/swap/di/SwapDataModule.kt b/data/swap/src/main/java/com/tangem/data/swap/di/SwapDataModule.kt index 0ee1971b25..bd75b9d1ad 100644 --- a/data/swap/src/main/java/com/tangem/data/swap/di/SwapDataModule.kt +++ b/data/swap/src/main/java/com/tangem/data/swap/di/SwapDataModule.kt @@ -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, + ) + } } \ No newline at end of file diff --git a/data/swap/src/main/java/com/tangem/data/swap/models/LastSwappedCryptoCurrencyDTO.kt b/data/swap/src/main/java/com/tangem/data/swap/models/LastSwappedCryptoCurrencyDTO.kt new file mode 100644 index 0000000000..61d738acd0 --- /dev/null +++ b/data/swap/src/main/java/com/tangem/data/swap/models/LastSwappedCryptoCurrencyDTO.kt @@ -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, +) \ No newline at end of file diff --git a/data/swap/src/main/java/com/tangem/data/swap/models/SwapStatusDTO.kt b/data/swap/src/main/java/com/tangem/data/swap/models/SwapStatusDTO.kt new file mode 100644 index 0000000000..45c37a2005 --- /dev/null +++ b/data/swap/src/main/java/com/tangem/data/swap/models/SwapStatusDTO.kt @@ -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, +} \ No newline at end of file diff --git a/data/swap/src/main/java/com/tangem/data/swap/models/SwapTransactionListDTO.kt b/data/swap/src/main/java/com/tangem/data/swap/models/SwapTransactionListDTO.kt new file mode 100644 index 0000000000..e5249da91f --- /dev/null +++ b/data/swap/src/main/java/com/tangem/data/swap/models/SwapTransactionListDTO.kt @@ -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, +) + +@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, +) \ No newline at end of file diff --git a/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapCurrencies.kt b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapCurrencies.kt index 6a1f16be09..ef559c44e9 100644 --- a/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapCurrencies.kt +++ b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapCurrencies.kt @@ -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, val unavailable: List, val afterSearch: Boolean, ) +/** + * Crypto currency with providers to swap + */ data class SwapCryptoCurrency( val currencyStatus: CryptoCurrencyStatus, val providers: List, diff --git a/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapDirection.kt b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapDirection.kt new file mode 100644 index 0000000000..9c60cc2ec0 --- /dev/null +++ b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapDirection.kt @@ -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, +} \ No newline at end of file diff --git a/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapQuoteModel.kt b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapQuoteModel.kt new file mode 100644 index 0000000000..a808f07e67 --- /dev/null +++ b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapQuoteModel.kt @@ -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?, +) \ No newline at end of file diff --git a/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapStatusModel.kt b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapStatusModel.kt new file mode 100644 index 0000000000..53cfd45e13 --- /dev/null +++ b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapStatusModel.kt @@ -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 + } +} \ No newline at end of file diff --git a/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapTransactionListModel.kt b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapTransactionListModel.kt new file mode 100644 index 0000000000..e296ca6fe3 --- /dev/null +++ b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapTransactionListModel.kt @@ -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, +) + +/** + * 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, +) \ No newline at end of file diff --git a/domain/swap/src/main/java/com/tangem/domain/swap/SwapRepositoryV2.kt b/domain/swap/src/main/java/com/tangem/domain/swap/SwapRepositoryV2.kt index b2c768cdc7..7bb2c091ba 100644 --- a/domain/swap/src/main/java/com/tangem/domain/swap/SwapRepositoryV2.kt +++ b/domain/swap/src/main/java/com/tangem/domain/swap/SwapRepositoryV2.kt @@ -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, ): List + + /** + * 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 } \ No newline at end of file diff --git a/domain/swap/src/main/java/com/tangem/domain/swap/SwapTransactionRepository.kt b/domain/swap/src/main/java/com/tangem/domain/swap/SwapTransactionRepository.kt new file mode 100644 index 0000000000..5efa512937 --- /dev/null +++ b/domain/swap/src/main/java/com/tangem/domain/swap/SwapTransactionRepository.kt @@ -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?> + + /** + * 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? +} \ No newline at end of file diff --git a/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapPairsUseCase.kt b/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapPairsUseCase.kt new file mode 100644 index 0000000000..d0a1cd0bf2 --- /dev/null +++ b/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapPairsUseCase.kt @@ -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, + ) = 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.groupPairs( + initialCurrency: CryptoCurrency, + filteringCurrency: (SwapPairModel) -> CryptoCurrencyStatus, + groupingCurrency: (SwapPairModel) -> CryptoCurrencyStatus, + cryptoCurrencyStatusList: List, + ): 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, + ) + } +} \ No newline at end of file diff --git a/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapQuoteUseCase.kt b/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapQuoteUseCase.kt new file mode 100644 index 0000000000..f0594c8f02 --- /dev/null +++ b/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapQuoteUseCase.kt @@ -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 = Either.catch { + swapRepositoryV2.getSwapQuote( + userWallet = userWallet, + fromCryptoCurrency = fromCryptoCurrency, + toCryptoCurrency = toCryptoCurrency, + fromAmount = fromAmount, + provider = provider, + rateType = ExpressRateType.Float, // todo rate type + ) + }.mapLeft(swapErrorResolver::resolve) +} \ No newline at end of file diff --git a/domain/swap/src/main/java/com/tangem/domain/swap/usecase/SelectInitialPairUseCase.kt b/domain/swap/src/main/java/com/tangem/domain/swap/usecase/SelectInitialPairUseCase.kt new file mode 100644 index 0000000000..f348bdc52a --- /dev/null +++ b/domain/swap/src/main/java/com/tangem/domain/swap/usecase/SelectInitialPairUseCase.kt @@ -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 + } +} \ No newline at end of file