Updated on 2026-08-14

This commit is contained in:
Tangem 2023-11-29 12:10:09 +03:00
parent 3c3af4cb34
commit 6a5cb8f8d5
14 changed files with 341 additions and 48 deletions

View file

@ -49,6 +49,6 @@ interface TangemExpressApi {
@Query("toAddress") toAddress: String,
): ApiResponse<ExchangeDataResponse>
@GET("exchange-result")
suspend fun getExchangeResults(@Query("txId") txId: String): ApiResponse<ExchangeResultsResponse>
@GET("exchange-status")
suspend fun getExchangeStatus(@Query("txId") txId: String): ApiResponse<ExchangeStatusResponse>
}

View file

@ -1,42 +0,0 @@
package com.tangem.datasource.api.express.models.response
import com.squareup.moshi.Json
data class ExchangeResultsResponse(
@Json(name = "status")
val status: ExchangeResultsStatus,
@Json(name = "externalStatus")
val externalStatus: String,
@Json(name = "externalTxUrl")
val externalTxUrl: String,
@Json(name = "error")
val error: ExchangeResultsError?,
)
enum class ExchangeResultsStatus {
@Json(name = "processing")
PROCESSING,
@Json(name = "done")
DONE,
@Json(name = "failed")
FAILED,
@Json(name = "refunded")
REFUNDED,
@Json(name = "verificationRequired")
VERIFICATION_REQUIRED,
}
data class ExchangeResultsError(
@Json(name = "code")
val code: Int,
@Json(name = "description")
val description: String,
)

View file

@ -0,0 +1,59 @@
package com.tangem.datasource.api.express.models.response
import com.squareup.moshi.Json
data class ExchangeStatusResponse(
@Json(name = "providerId")
val providerId: String,
@Json(name = "externalTxId")
val externalTxId: String,
@Json(name = "externalTxStatus")
val externalStatus: ExchangeStatus,
@Json(name = "externalTxUrl")
val externalTxUrl: String,
@Json(name = "error")
val error: ExchangeStatusError?,
)
enum class ExchangeStatus {
@Json(name = "new")
NEW,
@Json(name = "waiting")
WAITING,
@Json(name = "confirming")
CONFIRMING,
@Json(name = "exchanging")
EXCHANGING,
@Json(name = "sending")
SENDING,
@Json(name = "finished")
FINISHED,
@Json(name = "failed")
FAILED,
@Json(name = "refunded")
REFUNDED,
@Json(name = "verifying")
VERIFYING,
}
data class ExchangeStatusError(
@Json(name = "code")
val code: Int,
@Json(name = "description")
val description: String,
)

View file

@ -32,6 +32,8 @@ object PreferencesKeys {
val SELECTED_APP_CURRENCY_KEY by lazy { stringPreferencesKey(name = "selectedAppCurrency") }
val BALANCE_HIDING_SETTINGS_KEY by lazy { stringPreferencesKey(name = "balanceHidingSettings") }
val SWAP_TRANSACTIONS_KEY by lazy { stringPreferencesKey(name = "swapTransactions") }
}
/** Preferences keys set that should be migrated from "PreferencesDataSource" to a new DataStore<Preferences> */

View file

@ -8,15 +8,19 @@ plugins {
dependencies {
/** AndroidX */
implementation(deps.androidx.datastore)
/** Project*/
implementation(project(":core:datasource"))
implementation(project(":core:utils"))
implementation(project(":features:swap:domain"))
implementation(projects.core.datasource)
implementation(projects.core.utils)
implementation(projects.features.swap.domain)
/** Network */
implementation(deps.retrofit)
implementation(deps.moshi)
implementation(deps.moshi.kotlin)
implementation(deps.arrow.core)
/** Domain */
implementation(projects.domain.tokens.models)

View file

@ -0,0 +1,163 @@
package com.tangem.feature.swap
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.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.swap.domain.SwapTransactionRepository
import com.tangem.feature.swap.domain.models.domain.SavedSwapTransactionListModel
import com.tangem.feature.swap.domain.models.domain.SavedSwapTransactionModel
import com.tangem.utils.extensions.addOrReplace
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
class DefaultSwapTransactionRepository(
private val appPreferencesStore: AppPreferencesStore,
) : SwapTransactionRepository {
override suspend fun storeTransaction(
userWalletId: UserWalletId,
fromCryptoCurrencyId: CryptoCurrency.ID,
toCryptoCurrencyId: CryptoCurrency.ID,
transaction: SavedSwapTransactionModel,
) {
appPreferencesStore.editData { mutablePreferences ->
val savedTransactions: List<SavedSwapTransactionListModel>? = mutablePreferences.getObjectList(
key = PreferencesKeys.SWAP_TRANSACTIONS_KEY,
)
val tokenTransactions = savedTransactions
?.firstOrNull {
it.checkId(
checkUserWalletId = userWalletId,
fromCurrencyId = fromCryptoCurrencyId,
toCurrencyId = toCryptoCurrencyId,
)
}
?.transactions
?.addOrReplace(
item = transaction,
predicate = { it.txId == transaction.txId },
) ?: listOf(transaction)
mutablePreferences.setObject(
key = PreferencesKeys.SWAP_TRANSACTIONS_KEY,
value = savedTransactions?.updateList(
userWalletId = userWalletId,
fromCryptoCurrencyId = fromCryptoCurrencyId,
toCryptoCurrencyId = toCryptoCurrencyId,
transactions = tokenTransactions,
) ?: listOf(
SavedSwapTransactionListModel(
userWalletId = userWalletId.stringValue,
fromCryptoCurrencyId = fromCryptoCurrencyId.value,
toCryptoCurrencyId = toCryptoCurrencyId.value,
transactions = tokenTransactions,
),
),
)
}
}
override fun getTransactions(
userWalletId: UserWalletId,
cryptoCurrencyId: CryptoCurrency.ID,
): Flow<List<SavedSwapTransactionListModel>?> {
return appPreferencesStore.getObjectList<SavedSwapTransactionListModel>(
key = PreferencesKeys.SWAP_TRANSACTIONS_KEY,
).map { savedTransactions ->
savedTransactions
?.filter {
it.userWalletId == userWalletId.stringValue &&
(
it.toCryptoCurrencyId == cryptoCurrencyId.value ||
it.fromCryptoCurrencyId == cryptoCurrencyId.value
)
}
}
}
override suspend fun removeTransaction(
userWalletId: UserWalletId,
fromCryptoCurrencyId: CryptoCurrency.ID,
toCryptoCurrencyId: CryptoCurrency.ID,
txId: String,
) {
appPreferencesStore.editData { mutablePreferences ->
val savedList: List<SavedSwapTransactionListModel>? = mutablePreferences.getObjectList(
key = PreferencesKeys.SWAP_TRANSACTIONS_KEY,
)
val tokenTransactions = savedList
?.first {
it.checkId(
checkUserWalletId = userWalletId,
fromCurrencyId = fromCryptoCurrencyId,
toCurrencyId = toCryptoCurrencyId,
)
}
?.transactions
?.dropWhile { it.txId == txId }
val editedList =
if (tokenTransactions.isNullOrEmpty()) {
savedList?.dropWhile {
it.checkId(
checkUserWalletId = userWalletId,
fromCurrencyId = fromCryptoCurrencyId,
toCurrencyId = toCryptoCurrencyId,
)
}
} else {
savedList.updateList(
userWalletId = userWalletId,
fromCryptoCurrencyId = fromCryptoCurrencyId,
toCryptoCurrencyId = toCryptoCurrencyId,
transactions = tokenTransactions,
)
}
if (editedList.isNullOrEmpty()) {
mutablePreferences.remove(key = PreferencesKeys.SWAP_TRANSACTIONS_KEY)
} else {
mutablePreferences.setObject(
key = PreferencesKeys.SWAP_TRANSACTIONS_KEY,
value = editedList,
)
}
}
}
private fun SavedSwapTransactionListModel.checkId(
checkUserWalletId: UserWalletId,
fromCurrencyId: CryptoCurrency.ID,
toCurrencyId: CryptoCurrency.ID,
): Boolean {
return userWalletId == checkUserWalletId.stringValue &&
toCryptoCurrencyId == toCurrencyId.value &&
fromCryptoCurrencyId == fromCurrencyId.value
}
private fun List<SavedSwapTransactionListModel>.updateList(
userWalletId: UserWalletId,
fromCryptoCurrencyId: CryptoCurrency.ID,
toCryptoCurrencyId: CryptoCurrency.ID,
transactions: List<SavedSwapTransactionModel>,
): List<SavedSwapTransactionListModel> {
return addOrReplace(
item = SavedSwapTransactionListModel(
userWalletId = userWalletId.stringValue,
fromCryptoCurrencyId = fromCryptoCurrencyId.value,
toCryptoCurrencyId = toCryptoCurrencyId.value,
transactions = transactions,
),
predicate = {
it.checkId(
checkUserWalletId = userWalletId,
fromCurrencyId = fromCryptoCurrencyId,
toCurrencyId = toCryptoCurrencyId,
)
},
)
}
}

View file

@ -1,5 +1,8 @@
package com.tangem.feature.swap
import arrow.core.Either
import arrow.core.raise.catch
import arrow.core.raise.either
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.Approver
import com.tangem.blockchain.common.Blockchain
@ -52,6 +55,7 @@ internal class SwapRepositoryImpl @Inject constructor(
private val leastTokenInfoConverter = LeastTokenInfoConverter()
private val swapPairInfoConverter = SwapPairInfoConverter()
private val cryptoCurrencyFactory = CryptoCurrencyFactory()
private val exchangeStatusConverter = ExchangeStatusConverter()
override suspend fun getPairs(
initialCurrency: LeastTokenInfo,
@ -103,6 +107,25 @@ internal class SwapRepositoryImpl @Inject constructor(
).getOrThrow()
}
override suspend fun getExchangeStatus(txId: String): Either<UnknownError, ExchangeStatusModel> {
return withContext(coroutineDispatcher.io) {
either {
catch(
{
exchangeStatusConverter.convert(
tangemExpressApi
.getExchangeStatus(txId)
.getOrThrow(),
)
},
{
raise(UnknownError(it.message))
},
)
}
}
}
override suspend fun getRates(currencyId: String, tokenIds: List<String>): Map<String, Double> {
// workaround cause backend do not return arbitrum and optimism rates
val addedTokens = if (tokenIds.contains(OPTIMISM_ID) || tokenIds.contains(ARBITRUM_ID)) {

View file

@ -0,0 +1,19 @@
package com.tangem.feature.swap.converters
import com.tangem.datasource.api.express.models.response.ExchangeStatusResponse
import com.tangem.feature.swap.domain.models.domain.ExchangeStatus
import com.tangem.feature.swap.domain.models.domain.ExchangeStatusModel
import com.tangem.utils.converter.Converter
internal class ExchangeStatusConverter : Converter<ExchangeStatusResponse, ExchangeStatusModel> {
override fun convert(value: ExchangeStatusResponse): ExchangeStatusModel {
return ExchangeStatusModel(
providerId = value.providerId,
status = ExchangeStatus.values().firstOrNull {
it.name.lowercase() == value.externalStatus.name.lowercase()
},
txId = value.externalTxId,
txUrl = value.externalTxUrl,
)
}
}

View file

@ -7,11 +7,14 @@ import com.tangem.datasource.api.oneinch.OneInchApiFactory
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.config.ConfigManager
import com.tangem.datasource.di.NetworkMoshi
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.legacy.WalletsStateHolder
import com.tangem.feature.swap.SwapRepositoryImpl
import com.tangem.feature.swap.converters.ErrorsDataConverter
import com.tangem.feature.swap.DefaultSwapTransactionRepository
import com.tangem.feature.swap.domain.SwapRepository
import com.tangem.feature.swap.domain.SwapTransactionRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
@ -47,6 +50,14 @@ internal class SwapDataModule {
)
}
@Provides
@Singleton
fun provideSwapTransactionRepository(appPreferencesStore: AppPreferencesStore): SwapTransactionRepository {
return DefaultSwapTransactionRepository(
appPreferencesStore = appPreferencesStore,
)
}
@Provides
@Singleton
internal fun provideErrorsConverter(@NetworkMoshi moshi: Moshi): ErrorsDataConverter {

View file

@ -27,9 +27,11 @@ dependencies {
implementation(projects.domain.legacy)
implementation(projects.domain.demo)
implementation(projects.domain.card)
implementation(projects.domain.appCurrency.models)
/** Core modules */
implementation(projects.core.utils)
implementation(projects.core.ui)
/** Feature Apis */
implementation(projects.features.wallet.api)
@ -40,4 +42,5 @@ dependencies {
implementation(deps.arrow.core)
implementation(deps.timber)
implementation(deps.tangem.blockchain)
implementation(deps.moshi)
}

View file

@ -1,5 +1,6 @@
package com.tangem.feature.swap.domain
import arrow.core.Either
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.swap.domain.models.data.AggregatedSwapDataModel
@ -14,6 +15,8 @@ interface SwapRepository {
suspend fun getExchangeableTokens(networkId: String): List<Currency>
suspend fun getExchangeStatus(txId: String): Either<UnknownError, ExchangeStatusModel>
@Suppress("LongParameterList")
suspend fun findBestQuote(
fromContractAddress: String,

View file

@ -0,0 +1,29 @@
package com.tangem.feature.swap.domain
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.swap.domain.models.domain.SavedSwapTransactionListModel
import com.tangem.feature.swap.domain.models.domain.SavedSwapTransactionModel
import kotlinx.coroutines.flow.Flow
interface SwapTransactionRepository {
suspend fun storeTransaction(
userWalletId: UserWalletId,
fromCryptoCurrencyId: CryptoCurrency.ID,
toCryptoCurrencyId: CryptoCurrency.ID,
transaction: SavedSwapTransactionModel,
)
fun getTransactions(
userWalletId: UserWalletId,
cryptoCurrencyId: CryptoCurrency.ID,
): Flow<List<SavedSwapTransactionListModel>?>
suspend fun removeTransaction(
userWalletId: UserWalletId,
fromCryptoCurrencyId: CryptoCurrency.ID,
toCryptoCurrencyId: CryptoCurrency.ID,
txId: String,
)
}

View file

@ -1,7 +1,7 @@
package com.tangem.feature.swap.domain.models.domain
data class ExchangeStatusModel(
val providerId: Int,
val providerId: String,
val status: ExchangeStatus? = null,
val txId: String? = null,
val txUrl: String? = null,

View file

@ -0,0 +1,19 @@
package com.tangem.feature.swap.domain.models.domain
data class SavedSwapTransactionListModel(
val userWalletId: String,
val fromCryptoCurrencyId: String,
val toCryptoCurrencyId: String,
val transactions: List<SavedSwapTransactionModel>,
)
data class SavedSwapTransactionModel(
val txId: String,
val timestamp: Long,
val fromCryptoAmount: String,
val toCryptoAmount: String,
val provider: SwapProvider,
val toFiatAmount: String? = null,
val fromFiatAmount: String? = null,
val status: ExchangeStatusModel? = null,
)