Updated on 2026-08-14

This commit is contained in:
Tangem 2025-06-24 16:31:20 +05:00
parent 64626bbcbc
commit 3e2de9fd6b
12 changed files with 812 additions and 0 deletions

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,
)