Updated on 2026-08-14

This commit is contained in:
Tangem 2025-04-24 11:42:34 +03:00
commit c8024b166d
656 changed files with 18270 additions and 4656 deletions

View file

@ -21,9 +21,12 @@ import com.tangem.datasource.api.express.models.response.SwapPair
import com.tangem.datasource.api.express.models.response.SwapPairsWithProviders
import com.tangem.datasource.api.express.models.response.TxDetails
import com.tangem.datasource.crypto.DataSignatureVerifier
import com.tangem.datasource.exchangeservice.swap.ExpressUtils
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.swap.converters.*
import com.tangem.feature.swap.domain.api.SwapRepository
@ -48,6 +51,7 @@ internal class DefaultSwapRepository(
private val userWalletsListManager: UserWalletsListManager,
private val errorsDataConverter: ErrorsDataConverter,
private val dataSignatureVerifier: DataSignatureVerifier,
private val appPreferencesStore: AppPreferencesStore,
moshi: Moshi,
excludedBlockchains: ExcludedBlockchains,
) : SwapRepository {
@ -60,6 +64,7 @@ internal class DefaultSwapRepository(
private val txDetailsMoshiAdapter = moshi.adapter(TxDetails::class.java)
override suspend fun getPairs(
userWallet: UserWallet,
initialCurrency: LeastTokenInfo,
currencyList: List<CryptoCurrency>,
): PairsWithProviders {
@ -73,6 +78,7 @@ internal class DefaultSwapRepository(
val pairsDeferred = async {
getPairsInternal(
userWallet = userWallet,
from = arrayListOf(initial),
to = currenciesList,
)
@ -80,6 +86,7 @@ internal class DefaultSwapRepository(
val reversedPairsDeferred = async {
getPairsInternal(
userWallet = userWallet,
from = currenciesList,
to = arrayListOf(initial),
)
@ -90,7 +97,13 @@ internal class DefaultSwapRepository(
val allPairs = pairs + reversedPairs
val providers = tangemExpressApi.getProviders().getOrThrow()
val providers = tangemExpressApi.getProviders(
userWalletId = userWallet.walletId.stringValue,
refCode = ExpressUtils.getRefCode(
userWallet = userWallet,
appPreferencesStore = appPreferencesStore,
),
).getOrThrow()
return@withContext swapPairInfoConverter.convert(
SwapPairsWithProviders(
@ -109,6 +122,7 @@ internal class DefaultSwapRepository(
}
override suspend fun getPairsOnly(
userWallet: UserWallet,
initialCurrency: LeastTokenInfo,
currencyList: List<CryptoCurrency>,
): PairsWithProviders {
@ -122,6 +136,7 @@ internal class DefaultSwapRepository(
val pairsDeferred = async {
getPairsInternal(
userWallet = userWallet,
from = arrayListOf(initial),
to = currenciesList,
)
@ -129,6 +144,7 @@ internal class DefaultSwapRepository(
val reversedPairsDeferred = async {
getPairsInternal(
userWallet = userWallet,
from = currenciesList,
to = arrayListOf(initial),
)
@ -156,25 +172,42 @@ internal class DefaultSwapRepository(
}
private suspend fun getPairsInternal(
userWallet: UserWallet,
from: List<NetworkLeastTokenInfo>,
to: List<NetworkLeastTokenInfo>,
): ApiResponse<List<SwapPair>> {
return tangemExpressApi.getPairs(
PairsRequestBody(
userWalletId = userWallet.walletId.stringValue,
refCode = ExpressUtils.getRefCode(
userWallet = userWallet,
appPreferencesStore = appPreferencesStore,
),
body = PairsRequestBody(
from = from,
to = to,
),
)
}
override suspend fun getExchangeStatus(txId: String): Either<UnknownError, ExchangeStatusModel> {
override suspend fun getExchangeStatus(
userWallet: UserWallet,
txId: String,
): Either<UnknownError,
ExchangeStatusModel,> {
return withContext(coroutineDispatcher.io) {
either {
catch(
block = {
exchangeStatusConverter.convert(
tangemExpressApi
.getExchangeStatus(txId)
.getExchangeStatus(
userWalletId = userWallet.walletId.stringValue,
refCode = ExpressUtils.getRefCode(
userWallet = userWallet,
appPreferencesStore = appPreferencesStore,
),
txId = txId,
)
.getOrThrow(),
)
},
@ -188,7 +221,7 @@ internal class DefaultSwapRepository(
}
override suspend fun findBestQuote(
userWalletId: UserWalletId,
userWallet: UserWallet,
fromContractAddress: String,
fromNetwork: String,
toContractAddress: String,
@ -211,6 +244,11 @@ internal class DefaultSwapRepository(
toDecimals = toDecimals,
providerId = providerId,
rateType = rateType.name.lowercase(),
userWalletId = userWallet.walletId.stringValue,
refCode = ExpressUtils.getRefCode(
userWallet = userWallet,
appPreferencesStore = appPreferencesStore,
),
).getOrThrow()
QuoteModel(
toTokenAmount = createFromAmountWithOffset(response.toAmount, response.toDecimals),
@ -223,6 +261,7 @@ internal class DefaultSwapRepository(
}
override suspend fun getExchangeData(
userWallet: UserWallet,
fromContractAddress: String,
fromNetwork: String,
toContractAddress: String,
@ -255,6 +294,11 @@ internal class DefaultSwapRepository(
requestId = requestId,
refundAddress = refundAddress,
refundExtraId = refundExtraId,
userWalletId = userWallet.walletId.stringValue,
refCode = ExpressUtils.getRefCode(
userWallet = userWallet,
appPreferencesStore = appPreferencesStore,
),
).getOrThrow()
if (dataSignatureVerifier.verifySignature(response.signature, response.txDetailsJson)) {
val txDetails = parseTxDetails(response.txDetailsJson)
@ -281,6 +325,7 @@ internal class DefaultSwapRepository(
}
override suspend fun exchangeSent(
userWallet: UserWallet,
txId: String,
fromNetwork: String,
fromAddress: String,
@ -290,7 +335,12 @@ internal class DefaultSwapRepository(
): Either<ExpressDataError, Unit> = withContext(coroutineDispatcher.io) {
try {
tangemExpressApi.exchangeSent(
ExchangeSentRequestBody(
userWalletId = userWallet.walletId.stringValue,
refCode = ExpressUtils.getRefCode(
userWallet = userWallet,
appPreferencesStore = appPreferencesStore,
),
body = ExchangeSentRequestBody(
txId = txId,
fromNetwork = fromNetwork,
fromAddress = fromAddress,
@ -344,47 +394,6 @@ internal class DefaultSwapRepository(
)
}
override suspend fun getApproveData(
userWalletId: UserWalletId,
networkId: String,
derivationPath: String?,
currency: CryptoCurrency,
amount: BigDecimal?,
spenderAddress: String,
): String {
val blockchain =
requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" }
val walletManager = walletManagersFacade.getOrCreateWalletManager(
userWalletId = userWalletId,
blockchain = blockchain,
derivationPath = derivationPath,
)
return (walletManager as? Approver)?.getApproveData(
spenderAddress,
amount?.let { convertToAmount(it, currency) },
) ?: error("Cannot cast to Approver")
}
private fun convertToAmount(amount: BigDecimal, currency: CryptoCurrency): Amount {
return Amount(
currencySymbol = currency.symbol,
value = amount,
decimals = currency.decimals,
type = if (currency is CryptoCurrency.Token) {
AmountType.Token(
Token(
symbol = currency.symbol,
contractAddress = currency.contractAddress,
decimals = currency.decimals,
),
)
} else {
AmountType.Coin
},
)
}
override fun getNativeTokenForNetwork(networkId: String): CryptoCurrency {
val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" }

View file

@ -7,8 +7,8 @@ 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.scan.ScanResponse
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.swap.converters.SavedSwapTransactionListConverter
import com.tangem.feature.swap.domain.SwapTransactionRepository
@ -80,9 +80,8 @@ internal class DefaultSwapTransactionRepository(
}
override suspend fun getTransactions(
userWalletId: UserWalletId,
userWallet: UserWallet,
cryptoCurrencyId: CryptoCurrency.ID,
scanResponse: ScanResponse,
): Flow<List<SavedSwapTransactionListModel>?> {
return withContext(dispatchers.io) {
val txStatuses = appPreferencesStore.getObjectMapSync<ExchangeStatusModel>(
@ -93,7 +92,7 @@ internal class DefaultSwapTransactionRepository(
).map { savedTransactions ->
val currencyTxs = savedTransactions
?.filter {
it.userWalletId == userWalletId.stringValue &&
it.userWalletId == userWallet.walletId.stringValue &&
(
it.toCryptoCurrencyId == cryptoCurrencyId.value ||
it.fromCryptoCurrencyId == cryptoCurrencyId.value
@ -103,7 +102,7 @@ internal class DefaultSwapTransactionRepository(
currencyTxs?.mapNotNull {
converter.convertBack(
value = it,
scanResponse = scanResponse,
scanResponse = userWallet.scanResponse,
txStatuses = txStatuses,
)
}

View file

@ -36,6 +36,7 @@ internal class SwapDataModule {
errorsDataConverter: ErrorsDataConverter,
@NetworkMoshi moshi: Moshi,
excludedBlockchains: ExcludedBlockchains,
appPreferencesStore: AppPreferencesStore,
): SwapRepository {
return DefaultSwapRepository(
tangemExpressApi = tangemExpressApi,
@ -46,6 +47,7 @@ internal class SwapDataModule {
dataSignatureVerifier = dataSignature,
moshi = moshi,
excludedBlockchains = excludedBlockchains,
appPreferencesStore = appPreferencesStore,
)
}

View file

@ -2,6 +2,7 @@ package com.tangem.feature.swap.domain.api
import arrow.core.Either
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.swap.domain.models.ExpressDataError
import com.tangem.feature.swap.domain.models.domain.*
@ -9,16 +10,24 @@ import java.math.BigDecimal
interface SwapRepository {
suspend fun getPairs(initialCurrency: LeastTokenInfo, currencyList: List<CryptoCurrency>): PairsWithProviders
suspend fun getPairs(
userWallet: UserWallet,
initialCurrency: LeastTokenInfo,
currencyList: List<CryptoCurrency>,
): PairsWithProviders
/** Express getPairs request variant without providers request */
suspend fun getPairsOnly(initialCurrency: LeastTokenInfo, currencyList: List<CryptoCurrency>): PairsWithProviders
suspend fun getPairsOnly(
userWallet: UserWallet,
initialCurrency: LeastTokenInfo,
currencyList: List<CryptoCurrency>,
): PairsWithProviders
suspend fun getExchangeStatus(txId: String): Either<UnknownError, ExchangeStatusModel>
suspend fun getExchangeStatus(userWallet: UserWallet, txId: String): Either<UnknownError, ExchangeStatusModel>
@Suppress("LongParameterList")
suspend fun findBestQuote(
userWalletId: UserWalletId,
userWallet: UserWallet,
fromContractAddress: String,
fromNetwork: String,
toContractAddress: String,
@ -41,19 +50,9 @@ interface SwapRepository {
spenderAddress: String,
): BigDecimal
@Suppress("LongParameterList")
@Throws(IllegalStateException::class)
suspend fun getApproveData(
userWalletId: UserWalletId,
networkId: String,
derivationPath: String?,
currency: CryptoCurrency,
amount: BigDecimal?,
spenderAddress: String,
): String
@Suppress("LongParameterList")
suspend fun getExchangeData(
userWallet: UserWallet,
fromContractAddress: String,
fromNetwork: String,
toContractAddress: String,
@ -72,6 +71,7 @@ interface SwapRepository {
// TODO: Add target error handling, remove either ([REDACTED_JIRA])
@Suppress("LongParameterList")
suspend fun exchangeSent(
userWallet: UserWallet,
txId: String,
fromNetwork: String,
fromAddress: String,

View file

@ -1,5 +1,3 @@
import com.tangem.plugin.configuration.configurations.extension.kaptForObfuscatingVariants
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
@ -23,22 +21,24 @@ dependencies {
/** Domain */
implementation(projects.domain.appCurrency)
implementation(projects.domain.appCurrency.models)
implementation(projects.domain.card)
implementation(projects.domain.demo)
implementation(projects.domain.legacy)
implementation(projects.domain.models)
implementation(projects.domain.quotes)
implementation(projects.domain.staking)
implementation(projects.domain.tokens)
implementation(projects.domain.tokens.models)
implementation(projects.domain.transaction)
implementation(projects.domain.transaction.models)
implementation(projects.domain.txhistory.models)
implementation(projects.domain.wallets)
implementation(projects.domain.wallets.models)
implementation(projects.domain.transaction)
implementation(projects.domain.legacy)
implementation(projects.libs.blockchainSdk)
implementation(projects.domain.demo)
implementation(projects.domain.card)
implementation(projects.domain.appCurrency.models)
implementation(projects.domain.txhistory.models)
implementation(projects.domain.transaction.models)
implementation(projects.features.swap.domain.api)
implementation(projects.features.swap.domain.models)
implementation(projects.domain.staking)
implementation(projects.libs.blockchainSdk)
/** Core modules */
implementation(projects.core.utils)

View file

@ -19,6 +19,7 @@ dependencies {
implementation(projects.core.datasource)
/** Other Libraries **/
implementation(tangemDeps.blockchain)
implementation(deps.kotlin.serialization)
implementation(deps.arrow.core)
implementation(deps.moshi.kotlin)

View file

@ -1,5 +1,6 @@
package com.tangem.feature.swap.domain.models.ui
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck
import com.tangem.feature.swap.domain.models.ExpressDataError
@ -82,16 +83,10 @@ data class TokenSwapInfo(
data class RequestApproveStateData(
val fee: TxFeeState,
val approveData: String,
val fromTokenAmount: SwapAmount,
val spenderAddress: String,
)
// data class SwapStateData(
// val fee: TxFeeState,
// val swapModel: SwapDataModel,
// )
sealed class TxFeeState {
data class MultipleFeeState(
val normalFee: TxFee,
@ -115,34 +110,15 @@ sealed class TxFeeState {
data class TxFee(
val feeValue: BigDecimal,
val gasLimit: Int,
val feeFiatFormatted: String,
val feeCryptoFormatted: String,
val feeIncludeOtherNativeFee: BigDecimal,
val feeFiatFormattedWithNative: String,
val feeCryptoFormattedWithNative: String,
val decimals: Int,
val cryptoSymbol: String,
val feeType: FeeType,
val params: Params?,
) {
sealed class Params {
data class Filecoin(
val gasPremium: Long,
) : Params()
data class Sui(
val gasBudget: Long,
val gasPrice: Long,
) : Params()
data class Hedera(
val additionalHBARFee: BigDecimal,
) : Params()
}
}
val fee: Fee,
)
enum class FeeType {
NORMAL, PRIORITY

View file

@ -1,15 +0,0 @@
package com.tangem.feature.swap.domain
import com.tangem.feature.swap.domain.models.domain.NetworkInfo
interface BlockchainInteractor {
/**
* In app blockchain id, actual in blockchain sdk, not the same as networkId
*
* workaround till not use backend only and not integrated server vs sdk
*/
fun getBlockchainInfo(networkId: String): NetworkInfo
fun getExplorerTransactionLink(networkId: String, txHash: String): String
}

View file

@ -1,23 +0,0 @@
package com.tangem.feature.swap.domain
import com.tangem.feature.swap.domain.models.domain.NetworkInfo
import com.tangem.lib.crypto.TransactionManager
import javax.inject.Inject
internal class DefaultBlockchainInteractor @Inject constructor(
private val transactionManager: TransactionManager,
) : BlockchainInteractor {
override fun getBlockchainInfo(networkId: String): NetworkInfo {
return transactionManager.getBlockchainInfo(networkId).let {
NetworkInfo(
name = it.name,
blockchainId = it.blockchainId,
)
}
}
override fun getExplorerTransactionLink(networkId: String, txHash: String): String {
return transactionManager.getExplorerTransactionLink(networkId, txHash)
}
}

View file

@ -1,6 +1,7 @@
package com.tangem.feature.swap.domain
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.feature.swap.domain.api.SwapRepository
import com.tangem.feature.swap.domain.models.domain.LeastTokenInfo
import com.tangem.feature.swap.domain.models.domain.SwapPairLeast
@ -10,9 +11,14 @@ class GetAvailablePairsUseCase(
) {
suspend operator fun invoke(
userWallet: UserWallet,
initialCurrency: LeastTokenInfo,
currencies: List<CryptoCurrency>,
): List<SwapPairLeast> {
return swapRepository.getPairsOnly(initialCurrency, currencies).pairs
return swapRepository.getPairsOnly(
userWallet = userWallet,
initialCurrency = initialCurrency,
currencyList = currencies,
).pairs
}
}

View file

@ -1,7 +1,7 @@
package com.tangem.feature.swap.domain
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.swap.domain.models.domain.ExchangeStatusModel
import com.tangem.feature.swap.domain.models.domain.SavedSwapTransactionListModel
@ -18,9 +18,8 @@ interface SwapTransactionRepository {
)
suspend fun getTransactions(
userWalletId: UserWalletId,
userWallet: UserWallet,
cryptoCurrencyId: CryptoCurrency.ID,
scanResponse: ScanResponse,
): Flow<List<SavedSwapTransactionListModel>?>
suspend fun removeTransaction(

View file

@ -3,7 +3,6 @@ package com.tangem.feature.swap.domain.di
import com.tangem.domain.tokens.GetCryptoCurrencyStatusesSyncUseCase
import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations
import com.tangem.feature.swap.domain.*
import com.tangem.lib.crypto.TransactionManager
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -25,14 +24,6 @@ internal class SwapDomainModule {
return factory
}
@Provides
@Singleton
fun provideBlockchainInteractor(transactionManager: TransactionManager): BlockchainInteractor {
return DefaultBlockchainInteractor(
transactionManager = transactionManager,
)
}
@Provides
@Singleton
fun providesGetCryptoCurrencyStatusUseCase(

View file

@ -40,6 +40,8 @@ dependencies {
implementation(projects.domain.feedback)
implementation(projects.domain.promo)
implementation(projects.domain.promo.models)
implementation(projects.domain.txhistory)
implementation(projects.domain.txhistory.models)
/** Feature modules */
implementation(projects.features.swap.domain)

View file

@ -35,12 +35,12 @@ import com.tangem.domain.tokens.*
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.feature.swap.analytics.StoriesEvents
import com.tangem.feature.swap.analytics.SwapEvents
import com.tangem.feature.swap.domain.BlockchainInteractor
import com.tangem.feature.swap.domain.SwapInteractor
import com.tangem.feature.swap.domain.models.ExpressDataError
import com.tangem.feature.swap.domain.models.ExpressException
@ -80,7 +80,6 @@ typealias SuccessLoadedSwapData = Map<SwapProvider, SwapState.QuotesLoadedState>
internal class SwapModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val blockchainInteractor: BlockchainInteractor,
private val analyticsEventHandler: AnalyticsEventHandler,
private val analyticsErrorEventHandler: AnalyticsErrorHandler,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
@ -93,6 +92,7 @@ internal class SwapModel @Inject constructor(
private val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase,
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
private val getMinimumTransactionAmountSyncUseCase: GetMinimumTransactionAmountSyncUseCase,
private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase,
private val shouldShowStoriesUseCase: ShouldShowStoriesUseCase,
private val getStoryContentUseCase: GetStoryContentUseCase,
getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
@ -135,7 +135,7 @@ internal class SwapModel @Inject constructor(
stateBuilder.createInitialLoadingState(
initialCurrencyFrom = initialCurrencyFrom,
initialCurrencyTo = initialCurrencyTo,
fromNetworkInfo = blockchainInteractor.getBlockchainInfo(initialCurrencyFrom.network.backendId),
fromNetworkInfo = initialCurrencyFrom.getNetworkInfo(),
),
)
private set
@ -261,9 +261,7 @@ internal class SwapModel @Inject constructor(
uiState = stateBuilder.createInitialLoadingState(
initialCurrencyFrom = initialCurrencyFrom,
initialCurrencyTo = initialCurrencyTo,
fromNetworkInfo = blockchainInteractor.getBlockchainInfo(
initialCurrencyFrom.network.backendId,
),
fromNetworkInfo = initialCurrencyFrom.getNetworkInfo(),
)
initTokens(isReverseFromTo)
}
@ -603,10 +601,14 @@ internal class SwapModel @Inject constructor(
when (it) {
is SwapTransactionState.TxSent -> {
sendSuccessSwapEvent(fromCurrency.currency, fee.feeType)
val url = blockchainInteractor.getExplorerTransactionLink(
networkId = fromCurrency.currency.network.backendId,
val url = getExplorerTransactionUrlUseCase(
txHash = it.txHash,
)
networkId = fromCurrency.currency.network.id,
).getOrElse {
Timber.i("tx hash explore not supported")
""
}
updateWalletBalance()
uiState = stateBuilder.createSuccessState(
uiState = uiState,
@ -1356,6 +1358,13 @@ internal class SwapModel @Inject constructor(
}
}
private fun CryptoCurrency.getNetworkInfo(): NetworkInfo {
return NetworkInfo(
name = this.network.name,
blockchainId = this.network.id.value,
)
}
private companion object {
const val INITIAL_AMOUNT = ""
const val UPDATE_DELAY = 10000L