Updated on 2026-08-14
This commit is contained in:
parent
53b02cfb10
commit
2be7445212
81 changed files with 4444 additions and 3434 deletions
|
|
@ -13,6 +13,7 @@ import com.tangem.data.swap.converter.TokenInfoConverter
|
|||
import com.tangem.datasource.api.common.response.getOrThrow
|
||||
import com.tangem.datasource.api.express.TangemExpressApi
|
||||
import com.tangem.datasource.api.express.models.request.ExchangeSentRequestBody
|
||||
import com.tangem.datasource.api.express.models.request.LeastTokenInfo
|
||||
import com.tangem.datasource.api.express.models.request.PairsRequestBody
|
||||
import com.tangem.datasource.api.express.models.response.ExchangeDataResponseWithTxDetails
|
||||
import com.tangem.datasource.api.express.models.response.RateType
|
||||
|
|
@ -34,7 +35,6 @@ import com.tangem.domain.quotes.single.SingleQuoteStatusProducer
|
|||
import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier
|
||||
import com.tangem.domain.swap.SwapRepositoryV2
|
||||
import com.tangem.domain.swap.models.*
|
||||
import com.tangem.domain.swap.models.SwapAmountType
|
||||
import com.tangem.domain.tokens.operations.CryptoCurrencyStatusFactory
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
|
|
@ -44,6 +44,7 @@ import kotlinx.coroutines.awaitAll
|
|||
import kotlinx.coroutines.withContext
|
||||
import java.io.IOException
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -64,6 +65,76 @@ internal class DefaultSwapRepositoryV2 @Inject constructor(
|
|||
private val exchangeStatusConverter = SwapStatusConverter()
|
||||
private val txDetailsMoshiAdapter = moshi.adapter(TxDetails::class.java)
|
||||
|
||||
override suspend fun getPairs(
|
||||
primarySwapCurrencyStatus: SwapCurrencyStatus,
|
||||
secondarySwapCurrencyStatus: SwapCurrencyStatus,
|
||||
filterProviderTypes: List<ExpressProviderType>,
|
||||
swapTxType: SwapTxType,
|
||||
): List<SwapPairModel> = withContext(coroutineDispatcher.default) {
|
||||
val primaryCurrency = primarySwapCurrencyStatus.currency
|
||||
val secondaryCurrency = secondarySwapCurrencyStatus.currency
|
||||
val primaryUserWallet = primarySwapCurrencyStatus.userWallet
|
||||
val secondaryUserWallet = secondarySwapCurrencyStatus.userWallet
|
||||
|
||||
val pairs = awaitAll(
|
||||
// original pairs
|
||||
async {
|
||||
invokePairRequest(
|
||||
userWallet = primaryUserWallet,
|
||||
from = listOf(primaryCurrency),
|
||||
to = listOf(secondaryCurrency),
|
||||
)
|
||||
},
|
||||
// reversed pairs
|
||||
async {
|
||||
invokePairRequest(
|
||||
userWallet = secondaryUserWallet,
|
||||
from = listOf(secondaryCurrency),
|
||||
to = listOf(primaryCurrency),
|
||||
)
|
||||
},
|
||||
).flatten()
|
||||
|
||||
val expressProviders = expressRepository.getFilteredProviders(
|
||||
userWallet = primaryUserWallet,
|
||||
filterProviderTypes = filterProviderTypes,
|
||||
swapTxType = swapTxType,
|
||||
).associateBy(ExpressProvider::providerId)
|
||||
|
||||
fun checkPair(currency: CryptoCurrency, pair: LeastTokenInfo): Boolean {
|
||||
return currency.getContractAddress() == pair.contractAddress &&
|
||||
currency.network.rawId == pair.network
|
||||
}
|
||||
|
||||
pairs.mapNotNull { pair ->
|
||||
val fromCurrencyStatus = when {
|
||||
checkPair(primaryCurrency, pair.from) -> primarySwapCurrencyStatus.status
|
||||
checkPair(secondaryCurrency, pair.from) -> secondarySwapCurrencyStatus.status
|
||||
else -> null
|
||||
}
|
||||
|
||||
val toCurrencyStatus = when {
|
||||
checkPair(primaryCurrency, pair.to) -> primarySwapCurrencyStatus.status
|
||||
checkPair(secondaryCurrency, pair.to) -> secondarySwapCurrencyStatus.status
|
||||
else -> null
|
||||
}
|
||||
|
||||
val mappedProviders = pair.providers
|
||||
.mapNotNull { it.withExpressProvider(expressProviders) }
|
||||
.filterYieldSupplyProvider(fromCurrencyStatus)
|
||||
|
||||
if (fromCurrencyStatus != null && toCurrencyStatus != null && mappedProviders.isNotEmpty()) {
|
||||
SwapPairModel(
|
||||
from = fromCurrencyStatus,
|
||||
to = toCurrencyStatus,
|
||||
providers = mappedProviders,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getPairs(
|
||||
userWallet: UserWallet,
|
||||
initialCurrency: CryptoCurrency,
|
||||
|
|
@ -185,16 +256,12 @@ internal class DefaultSwapRepositoryV2 @Inject constructor(
|
|||
): SwapQuoteModel = withContext(coroutineDispatcher.io) {
|
||||
val response = tangemExpressApi.getExchangeQuote(
|
||||
fromAmount = if (amountType == SwapAmountType.From) {
|
||||
amount.movePointRight(
|
||||
fromCryptoCurrency.decimals,
|
||||
).toString()
|
||||
amount.toStringWithRightOffset(fromCryptoCurrency.decimals)
|
||||
} else {
|
||||
null
|
||||
},
|
||||
toAmount = if (amountType == SwapAmountType.To) {
|
||||
amount.movePointRight(
|
||||
toCryptoCurrency.decimals,
|
||||
).toString()
|
||||
amount.toStringWithRightOffset(toCryptoCurrency.decimals)
|
||||
} else {
|
||||
null
|
||||
},
|
||||
|
|
@ -229,7 +296,7 @@ internal class DefaultSwapRepositoryV2 @Inject constructor(
|
|||
userWallet: UserWallet,
|
||||
fromCryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
toCryptoCurrency: CryptoCurrency,
|
||||
amount: String,
|
||||
amount: BigDecimal,
|
||||
amountType: SwapAmountType,
|
||||
toAddress: String,
|
||||
toExtraId: String?,
|
||||
|
|
@ -261,8 +328,16 @@ internal class DefaultSwapRepositoryV2 @Inject constructor(
|
|||
toAddress = toAddress,
|
||||
fromDecimals = fromCurrency.decimals,
|
||||
toDecimals = toCryptoCurrency.decimals,
|
||||
fromAmount = if (amountType == SwapAmountType.From) amount else null,
|
||||
toAmount = if (amountType == SwapAmountType.To) amount else null,
|
||||
fromAmount = if (amountType == SwapAmountType.From) {
|
||||
amount.toStringWithRightOffset(fromCurrency.decimals)
|
||||
} else {
|
||||
null
|
||||
},
|
||||
toAmount = if (amountType == SwapAmountType.To) {
|
||||
amount.toStringWithRightOffset(toCryptoCurrency.decimals)
|
||||
} else {
|
||||
null
|
||||
},
|
||||
providerId = expressProvider.providerId,
|
||||
rateType = rateType.name.lowercase(),
|
||||
requestId = requestId,
|
||||
|
|
@ -465,6 +540,10 @@ internal class DefaultSwapRepositoryV2 @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun BigDecimal.toStringWithRightOffset(decimals: Int): String {
|
||||
return setScale(decimals, RoundingMode.HALF_DOWN).movePointRight(decimals).toPlainString()
|
||||
}
|
||||
|
||||
private fun List<ExpressProvider>.filterYieldSupplyProvider(cryptoCurrencyStatus: CryptoCurrencyStatus?) =
|
||||
filter { provider ->
|
||||
// !!!WARNING!!! Filter out dex provider if yield supply is active
|
||||
|
|
|
|||
|
|
@ -54,7 +54,8 @@ internal class DefaultSwapTransactionRepository(
|
|||
private val userTokensResponseFactory = UserTokensResponseFactory()
|
||||
|
||||
override suspend fun storeTransaction(
|
||||
userWalletId: UserWalletId,
|
||||
fromUserWalletId: UserWalletId,
|
||||
toUserWalletId: UserWalletId,
|
||||
fromCryptoCurrency: CryptoCurrency,
|
||||
toCryptoCurrency: CryptoCurrency,
|
||||
fromAccount: Account?,
|
||||
|
|
@ -75,7 +76,10 @@ internal class DefaultSwapTransactionRepository(
|
|||
val tokenTransactions = savedTransactions
|
||||
?.firstOrNull { swapTxList ->
|
||||
swapTxList.checkId(
|
||||
checkUserWalletId = userWalletId,
|
||||
fromUserWalletId = fromUserWalletId,
|
||||
toUserWalletId = toUserWalletId,
|
||||
fromAccountId = fromAccount?.accountId,
|
||||
toAccountId = toAccount?.accountId,
|
||||
fromCurrencyId = fromCryptoCurrency.id,
|
||||
toCurrencyId = toCryptoCurrency.id,
|
||||
)
|
||||
|
|
@ -89,7 +93,8 @@ internal class DefaultSwapTransactionRepository(
|
|||
mutablePreferences.setObject(
|
||||
key = PreferencesKeys.SWAP_TRANSACTIONS_KEY,
|
||||
value = savedTransactions?.updateList(
|
||||
userWalletId = userWalletId,
|
||||
fromUserWalletId = fromUserWalletId,
|
||||
toUserWalletId = toUserWalletId,
|
||||
fromCryptoCurrency = fromCryptoCurrency,
|
||||
toCryptoCurrency = toCryptoCurrency,
|
||||
fromAccount = fromAccount,
|
||||
|
|
@ -97,7 +102,8 @@ internal class DefaultSwapTransactionRepository(
|
|||
transactions = tokenTransactions,
|
||||
) ?: listOf(
|
||||
listConverter.default(
|
||||
userWalletId = userWalletId,
|
||||
fromUserWalletId = fromUserWalletId,
|
||||
toUserWalletId = toUserWalletId,
|
||||
fromCryptoCurrency = fromCryptoCurrency,
|
||||
toCryptoCurrency = toCryptoCurrency,
|
||||
fromAccount = fromAccount,
|
||||
|
|
@ -123,7 +129,7 @@ internal class DefaultSwapTransactionRepository(
|
|||
) { savedTransactions, txStatuses, multiAccountList ->
|
||||
val currencyTxs = savedTransactions
|
||||
?.filter { swapTxList ->
|
||||
swapTxList.userWalletId == userWallet.walletId.stringValue &&
|
||||
swapTxList.fromUserWalletId == userWallet.walletId.stringValue &&
|
||||
(
|
||||
swapTxList.toCryptoCurrencyId == cryptoCurrencyId.value ||
|
||||
swapTxList.fromCryptoCurrencyId == cryptoCurrencyId.value
|
||||
|
|
@ -222,19 +228,27 @@ internal class DefaultSwapTransactionRepository(
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
private fun SwapTransactionListDTO.checkId(
|
||||
checkUserWalletId: UserWalletId,
|
||||
fromUserWalletId: UserWalletId,
|
||||
toUserWalletId: UserWalletId,
|
||||
fromAccountId: AccountId?,
|
||||
toAccountId: AccountId?,
|
||||
fromCurrencyId: CryptoCurrency.ID,
|
||||
toCurrencyId: CryptoCurrency.ID,
|
||||
): Boolean {
|
||||
return userWalletId == checkUserWalletId.stringValue &&
|
||||
toCryptoCurrencyId == toCurrencyId.value &&
|
||||
fromCryptoCurrencyId == fromCurrencyId.value
|
||||
return this.fromUserWalletId == fromUserWalletId.stringValue &&
|
||||
this.toUserWalletId == toUserWalletId.stringValue &&
|
||||
this.fromTokensResponse?.accountId == fromAccountId?.value &&
|
||||
this.toTokensResponse?.accountId == toAccountId?.value &&
|
||||
fromCryptoCurrencyId == fromCurrencyId.value &&
|
||||
toCryptoCurrencyId == toCurrencyId.value
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
private fun List<SwapTransactionListDTO>.updateList(
|
||||
userWalletId: UserWalletId,
|
||||
fromUserWalletId: UserWalletId,
|
||||
toUserWalletId: UserWalletId,
|
||||
fromCryptoCurrency: CryptoCurrency,
|
||||
toCryptoCurrency: CryptoCurrency,
|
||||
fromAccount: Account?,
|
||||
|
|
@ -243,7 +257,8 @@ internal class DefaultSwapTransactionRepository(
|
|||
): List<SwapTransactionListDTO> {
|
||||
return addOrReplace(
|
||||
item = listConverter.default(
|
||||
userWalletId = userWalletId,
|
||||
fromUserWalletId = fromUserWalletId,
|
||||
toUserWalletId = toUserWalletId,
|
||||
fromCryptoCurrency = fromCryptoCurrency,
|
||||
toCryptoCurrency = toCryptoCurrency,
|
||||
fromAccount = fromAccount,
|
||||
|
|
@ -252,7 +267,10 @@ internal class DefaultSwapTransactionRepository(
|
|||
),
|
||||
predicate = { swapTxList ->
|
||||
swapTxList.checkId(
|
||||
checkUserWalletId = userWalletId,
|
||||
fromUserWalletId = fromUserWalletId,
|
||||
toUserWalletId = toUserWalletId,
|
||||
fromAccountId = fromAccount?.accountId,
|
||||
toAccountId = toAccount?.accountId,
|
||||
fromCurrencyId = fromCryptoCurrency.id,
|
||||
toCurrencyId = toCryptoCurrency.id,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -31,7 +31,8 @@ internal class SavedSwapTransactionListConverter(
|
|||
}
|
||||
|
||||
override fun convert(value: SwapTransactionListModel) = SwapTransactionListDTO(
|
||||
userWalletId = value.userWalletId,
|
||||
fromUserWalletId = value.fromUserWalletId,
|
||||
toUserWalletId = value.toUserWalletId,
|
||||
fromCryptoCurrencyId = value.fromCryptoCurrencyId,
|
||||
toCryptoCurrencyId = value.toCryptoCurrencyId,
|
||||
fromTokensResponse = userTokensResponseFactory.createResponseToken(
|
||||
|
|
@ -79,7 +80,8 @@ internal class SavedSwapTransactionListConverter(
|
|||
txStatuses = txStatuses,
|
||||
)
|
||||
},
|
||||
userWalletId = value.userWalletId,
|
||||
fromUserWalletId = value.fromUserWalletId,
|
||||
toUserWalletId = value.toUserWalletId,
|
||||
fromCryptoCurrencyId = value.fromCryptoCurrencyId,
|
||||
toCryptoCurrencyId = value.toCryptoCurrencyId,
|
||||
fromCryptoCurrency = fromCryptoCurrency,
|
||||
|
|
@ -98,14 +100,16 @@ internal class SavedSwapTransactionListConverter(
|
|||
|
||||
@Suppress("LongParameterList")
|
||||
fun default(
|
||||
userWalletId: UserWalletId,
|
||||
fromUserWalletId: UserWalletId,
|
||||
toUserWalletId: UserWalletId,
|
||||
fromCryptoCurrency: CryptoCurrency,
|
||||
toCryptoCurrency: CryptoCurrency,
|
||||
fromAccount: Account?,
|
||||
toAccount: Account?,
|
||||
tokenTransactions: List<SwapTransactionDTO>,
|
||||
) = SwapTransactionListDTO(
|
||||
userWalletId = userWalletId.stringValue,
|
||||
fromUserWalletId = fromUserWalletId.stringValue,
|
||||
toUserWalletId = toUserWalletId.stringValue,
|
||||
fromCryptoCurrencyId = fromCryptoCurrency.id.value,
|
||||
toCryptoCurrencyId = toCryptoCurrency.id.value,
|
||||
fromTokensResponse = userTokensResponseFactory.createResponseToken(
|
||||
|
|
|
|||
|
|
@ -9,7 +9,9 @@ import java.math.BigDecimal
|
|||
@JsonClass(generateAdapter = true)
|
||||
internal data class SwapTransactionListDTO(
|
||||
@Json(name = "userWalletId")
|
||||
val userWalletId: String,
|
||||
val fromUserWalletId: String,
|
||||
@Json(name = "toUserWalletId")
|
||||
val toUserWalletId: String = fromUserWalletId,
|
||||
@Json(name = "fromCryptoCurrencyId")
|
||||
val fromCryptoCurrencyId: String,
|
||||
@Json(name = "toCryptoCurrencyId")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,709 @@
|
|||
package com.tangem.data.swap
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.common.test.domain.wallet.MockUserWalletFactory
|
||||
import com.tangem.datasource.api.common.response.ApiResponse
|
||||
import com.tangem.datasource.api.express.TangemExpressApi
|
||||
import com.tangem.datasource.api.express.models.request.LeastTokenInfo
|
||||
import com.tangem.datasource.api.express.models.request.PairsRequestBody
|
||||
import com.tangem.datasource.api.express.models.response.*
|
||||
import com.tangem.datasource.crypto.DataSignatureVerifier
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.domain.express.ExpressRepository
|
||||
import com.tangem.domain.express.models.*
|
||||
import com.tangem.domain.express.models.ExpressError
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.yield.supply.YieldSupplyStatus
|
||||
import com.tangem.domain.quotes.single.SingleQuoteStatusFetcher
|
||||
import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier
|
||||
import com.tangem.domain.swap.models.SwapAmountType
|
||||
import com.tangem.domain.swap.models.SwapCurrencyStatus
|
||||
import com.tangem.domain.swap.models.SwapStatus
|
||||
import com.tangem.domain.swap.models.SwapTxType
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import io.mockk.*
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import org.junit.jupiter.api.assertThrows
|
||||
import java.math.BigDecimal
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class DefaultSwapRepositoryV2Test {
|
||||
|
||||
private val tangemExpressApi: TangemExpressApi = mockk()
|
||||
private val expressRepository: ExpressRepository = mockk()
|
||||
private val appPreferencesStore: AppPreferencesStore = mockk(relaxed = true)
|
||||
private val dataSignatureVerifier: DataSignatureVerifier = mockk()
|
||||
private val singleQuoteStatusSupplier: SingleQuoteStatusSupplier = mockk()
|
||||
private val singleQuoteStatusFetcher: SingleQuoteStatusFetcher = mockk()
|
||||
private val moshi: Moshi = Moshi.Builder().build()
|
||||
|
||||
private val repository = DefaultSwapRepositoryV2(
|
||||
tangemExpressApi = tangemExpressApi,
|
||||
expressRepository = expressRepository,
|
||||
coroutineDispatcher = TestingCoroutineDispatcherProvider(),
|
||||
appPreferencesStore = appPreferencesStore,
|
||||
dataSignatureVerifier = dataSignatureVerifier,
|
||||
singleQuoteStatusSupplier = singleQuoteStatusSupplier,
|
||||
singleQuoteStatusFetcher = singleQuoteStatusFetcher,
|
||||
moshi = moshi,
|
||||
)
|
||||
|
||||
@BeforeEach
|
||||
fun resetMocks() {
|
||||
clearMocks(
|
||||
tangemExpressApi,
|
||||
expressRepository,
|
||||
appPreferencesStore,
|
||||
dataSignatureVerifier,
|
||||
singleQuoteStatusSupplier,
|
||||
singleQuoteStatusFetcher,
|
||||
)
|
||||
}
|
||||
|
||||
// region getPairs(SwapCurrencyStatus, SwapCurrencyStatus)
|
||||
|
||||
@Test
|
||||
fun `getPairs with SwapCurrencyStatus returns mapped pairs when providers match`() = runTest {
|
||||
// Arrange
|
||||
val primaryStatus = createCryptoCurrencyStatus(primaryCoin)
|
||||
val secondaryStatus = createCryptoCurrencyStatus(secondaryCoin)
|
||||
val primarySwapCurrencyStatus = SwapCurrencyStatus(
|
||||
userWallet = userWallet,
|
||||
status = primaryStatus,
|
||||
account = mockk(),
|
||||
)
|
||||
val secondarySwapCurrencyStatus = SwapCurrencyStatus(
|
||||
userWallet = userWallet,
|
||||
status = secondaryStatus,
|
||||
account = mockk(),
|
||||
)
|
||||
|
||||
val swapPair = SwapPair(
|
||||
from = LeastTokenInfo(contractAddress = "0", network = ETH_BACKEND_ID),
|
||||
to = LeastTokenInfo(contractAddress = "0", network = BTC_BACKEND_ID),
|
||||
providers = listOf(SwapPairProvider(providerId = PROVIDER_ID, rateTypes = listOf(RateType.FLOAT))),
|
||||
)
|
||||
|
||||
coEvery {
|
||||
tangemExpressApi.getPairs(any(), any(), any())
|
||||
} returns ApiResponse.Success(listOf(swapPair))
|
||||
|
||||
coEvery {
|
||||
expressRepository.getProviders(any(), any())
|
||||
} returns listOf(expressProvider)
|
||||
|
||||
// Act
|
||||
val result = repository.getPairs(
|
||||
primarySwapCurrencyStatus = primarySwapCurrencyStatus,
|
||||
secondarySwapCurrencyStatus = secondarySwapCurrencyStatus,
|
||||
filterProviderTypes = emptyList(),
|
||||
swapTxType = SwapTxType.Swap,
|
||||
)
|
||||
|
||||
// Assert
|
||||
assertThat(result).hasSize(2)
|
||||
assertThat(result.first().from).isEqualTo(primaryStatus)
|
||||
assertThat(result.first().to).isEqualTo(secondaryStatus)
|
||||
assertThat(result.first().providers).hasSize(1)
|
||||
assertThat(result.first().providers.first().providerId).isEqualTo(PROVIDER_ID)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getPairs with SwapCurrencyStatus returns empty when no providers match`() = runTest {
|
||||
// Arrange
|
||||
val primaryStatus = createCryptoCurrencyStatus(primaryCoin)
|
||||
val secondaryStatus = createCryptoCurrencyStatus(secondaryCoin)
|
||||
val primarySwapCurrencyStatus = SwapCurrencyStatus(
|
||||
userWallet = userWallet,
|
||||
status = primaryStatus,
|
||||
account = mockk(),
|
||||
)
|
||||
val secondarySwapCurrencyStatus = SwapCurrencyStatus(
|
||||
userWallet = userWallet,
|
||||
status = secondaryStatus,
|
||||
account = mockk(),
|
||||
)
|
||||
|
||||
val swapPair = SwapPair(
|
||||
from = LeastTokenInfo(contractAddress = "0", network = ETH_BACKEND_ID),
|
||||
to = LeastTokenInfo(contractAddress = "0", network = BTC_BACKEND_ID),
|
||||
providers = listOf(SwapPairProvider(providerId = "unknown-provider", rateTypes = listOf(RateType.FLOAT))),
|
||||
)
|
||||
|
||||
coEvery {
|
||||
tangemExpressApi.getPairs(any(), any(), any())
|
||||
} returns ApiResponse.Success(listOf(swapPair))
|
||||
|
||||
coEvery {
|
||||
expressRepository.getProviders(any(), any())
|
||||
} returns listOf(expressProvider)
|
||||
|
||||
// Act
|
||||
val result = repository.getPairs(
|
||||
primarySwapCurrencyStatus = primarySwapCurrencyStatus,
|
||||
secondarySwapCurrencyStatus = secondarySwapCurrencyStatus,
|
||||
filterProviderTypes = emptyList(),
|
||||
swapTxType = SwapTxType.Swap,
|
||||
)
|
||||
|
||||
// Assert
|
||||
assertThat(result).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getPairs with SwapCurrencyStatus returns empty when API returns empty pairs`() = runTest {
|
||||
// Arrange
|
||||
val primarySwapCurrencyStatus = SwapCurrencyStatus(
|
||||
userWallet = userWallet,
|
||||
status = createCryptoCurrencyStatus(primaryCoin),
|
||||
account = mockk(),
|
||||
)
|
||||
val secondarySwapCurrencyStatus = SwapCurrencyStatus(
|
||||
userWallet = userWallet,
|
||||
status = createCryptoCurrencyStatus(secondaryCoin),
|
||||
account = mockk(),
|
||||
)
|
||||
|
||||
coEvery {
|
||||
tangemExpressApi.getPairs(any(), any(), any())
|
||||
} returns ApiResponse.Success(emptyList())
|
||||
|
||||
coEvery {
|
||||
expressRepository.getProviders(any(), any())
|
||||
} returns listOf(expressProvider)
|
||||
|
||||
// Act
|
||||
val result = repository.getPairs(
|
||||
primarySwapCurrencyStatus = primarySwapCurrencyStatus,
|
||||
secondarySwapCurrencyStatus = secondarySwapCurrencyStatus,
|
||||
filterProviderTypes = emptyList(),
|
||||
swapTxType = SwapTxType.Swap,
|
||||
)
|
||||
|
||||
// Assert
|
||||
assertThat(result).isEmpty()
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region getPairs(UserWallet, CryptoCurrency, List<CryptoCurrencyStatus>)
|
||||
|
||||
@Test
|
||||
fun `getPairs with currency status list returns mapped pairs`() = runTest {
|
||||
// Arrange
|
||||
val primaryStatus = createCryptoCurrencyStatus(primaryCoin)
|
||||
val secondaryStatus = createCryptoCurrencyStatus(secondaryCoin)
|
||||
|
||||
val swapPair = SwapPair(
|
||||
from = LeastTokenInfo(contractAddress = "0", network = ETH_BACKEND_ID),
|
||||
to = LeastTokenInfo(contractAddress = "0", network = BTC_BACKEND_ID),
|
||||
providers = listOf(SwapPairProvider(providerId = PROVIDER_ID, rateTypes = listOf(RateType.FLOAT))),
|
||||
)
|
||||
|
||||
coEvery {
|
||||
tangemExpressApi.getPairs(any(), any(), any())
|
||||
} returns ApiResponse.Success(listOf(swapPair))
|
||||
|
||||
coEvery {
|
||||
expressRepository.getProviders(any(), any())
|
||||
} returns listOf(expressProvider)
|
||||
|
||||
// Act
|
||||
val result = repository.getPairs(
|
||||
userWallet = userWallet,
|
||||
initialCurrency = primaryCoin,
|
||||
cryptoCurrencyStatusList = listOf(primaryStatus, secondaryStatus),
|
||||
filterProviderTypes = emptyList(),
|
||||
swapTxType = SwapTxType.Swap,
|
||||
)
|
||||
|
||||
// Assert
|
||||
assertThat(result).hasSize(2)
|
||||
assertThat(result.first().from).isEqualTo(primaryStatus)
|
||||
assertThat(result.first().to).isEqualTo(secondaryStatus)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getPairs with SendWithSwap only fetches forward pairs`() = runTest {
|
||||
// Arrange
|
||||
val primaryStatus = createCryptoCurrencyStatus(primaryCoin)
|
||||
val secondaryStatus = createCryptoCurrencyStatus(secondaryCoin)
|
||||
|
||||
val swapPair = SwapPair(
|
||||
from = LeastTokenInfo(contractAddress = "0", network = ETH_BACKEND_ID),
|
||||
to = LeastTokenInfo(contractAddress = "0", network = BTC_BACKEND_ID),
|
||||
providers = listOf(SwapPairProvider(providerId = PROVIDER_ID, rateTypes = listOf(RateType.FLOAT))),
|
||||
)
|
||||
|
||||
coEvery {
|
||||
tangemExpressApi.getPairs(any(), any(), any())
|
||||
} returns ApiResponse.Success(listOf(swapPair))
|
||||
|
||||
coEvery {
|
||||
expressRepository.getProviders(any(), any())
|
||||
} returns listOf(expressProvider)
|
||||
|
||||
// Act
|
||||
val result = repository.getPairs(
|
||||
userWallet = userWallet,
|
||||
initialCurrency = primaryCoin,
|
||||
cryptoCurrencyStatusList = listOf(primaryStatus, secondaryStatus),
|
||||
filterProviderTypes = emptyList(),
|
||||
swapTxType = SwapTxType.SendWithSwap,
|
||||
)
|
||||
|
||||
// Assert
|
||||
assertThat(result).hasSize(1)
|
||||
|
||||
// SendWithSwap should call getPairs only once (forward), not twice (forward + reverse)
|
||||
coVerify(exactly = 1) { tangemExpressApi.getPairs(any(), any(), any()) }
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region getSwapQuote
|
||||
|
||||
@Test
|
||||
fun `getSwapQuote returns correct quote model for fromAmount`() = runTest {
|
||||
// Arrange
|
||||
val quoteResponse = ExchangeQuoteResponse(
|
||||
fromAmount = "1000000000000000000",
|
||||
fromDecimals = 18,
|
||||
toAmount = "100000000",
|
||||
toDecimals = 8,
|
||||
allowanceContract = "0xAllowance",
|
||||
minAmount = BigDecimal.ONE,
|
||||
quoteId = "quote-123",
|
||||
)
|
||||
|
||||
coEvery {
|
||||
tangemExpressApi.getExchangeQuote(
|
||||
fromAmount = any(),
|
||||
toAmount = any(),
|
||||
fromNetwork = any(),
|
||||
fromContractAddress = any(),
|
||||
fromDecimals = any(),
|
||||
toNetwork = any(),
|
||||
toContractAddress = any(),
|
||||
toDecimals = any(),
|
||||
providerId = any(),
|
||||
rateType = any(),
|
||||
userWalletId = any(),
|
||||
refCode = any(),
|
||||
)
|
||||
} returns ApiResponse.Success(quoteResponse)
|
||||
|
||||
// Act
|
||||
val result = repository.getSwapQuote(
|
||||
userWallet = userWallet,
|
||||
fromCryptoCurrency = primaryCoin,
|
||||
toCryptoCurrency = secondaryCoin,
|
||||
amount = BigDecimal.ONE,
|
||||
amountType = SwapAmountType.From,
|
||||
provider = expressProvider,
|
||||
rateType = ExpressRateType.Float,
|
||||
)
|
||||
|
||||
// Assert
|
||||
assertThat(result.provider).isEqualTo(expressProvider)
|
||||
assertThat(result.toTokenAmount).isEqualTo(BigDecimal("1.00000000"))
|
||||
assertThat(result.fromTokenAmount).isEqualTo(BigDecimal("1.000000000000000000"))
|
||||
assertThat(result.allowanceContract).isEqualTo("0xAllowance")
|
||||
assertThat(result.quoteId).isEqualTo("quote-123")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getSwapQuote sends fromAmount when amountType is From`() = runTest {
|
||||
// Arrange
|
||||
val quoteResponse = ExchangeQuoteResponse(
|
||||
fromAmount = "1000000000000000000",
|
||||
fromDecimals = 18,
|
||||
toAmount = "100000000",
|
||||
toDecimals = 8,
|
||||
allowanceContract = null,
|
||||
minAmount = BigDecimal.ONE,
|
||||
quoteId = null,
|
||||
)
|
||||
|
||||
coEvery {
|
||||
tangemExpressApi.getExchangeQuote(
|
||||
fromAmount = any(),
|
||||
toAmount = any(),
|
||||
fromNetwork = any(),
|
||||
fromContractAddress = any(),
|
||||
fromDecimals = any(),
|
||||
toNetwork = any(),
|
||||
toContractAddress = any(),
|
||||
toDecimals = any(),
|
||||
providerId = any(),
|
||||
rateType = any(),
|
||||
userWalletId = any(),
|
||||
refCode = any(),
|
||||
)
|
||||
} returns ApiResponse.Success(quoteResponse)
|
||||
|
||||
// Act
|
||||
repository.getSwapQuote(
|
||||
userWallet = userWallet,
|
||||
fromCryptoCurrency = primaryCoin,
|
||||
toCryptoCurrency = secondaryCoin,
|
||||
amount = BigDecimal("2.5"),
|
||||
amountType = SwapAmountType.From,
|
||||
provider = expressProvider,
|
||||
rateType = ExpressRateType.Float,
|
||||
)
|
||||
|
||||
// Assert — fromAmount is set, toAmount is null
|
||||
coVerify {
|
||||
tangemExpressApi.getExchangeQuote(
|
||||
fromAmount = "2500000000000000000",
|
||||
toAmount = null,
|
||||
fromNetwork = ETH_BACKEND_ID,
|
||||
fromContractAddress = "0",
|
||||
fromDecimals = 18,
|
||||
toNetwork = BTC_BACKEND_ID,
|
||||
toContractAddress = "0",
|
||||
toDecimals = 8,
|
||||
providerId = PROVIDER_ID,
|
||||
rateType = "float",
|
||||
userWalletId = any(),
|
||||
refCode = any(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getSwapQuote sends toAmount when amountType is To`() = runTest {
|
||||
// Arrange
|
||||
val quoteResponse = ExchangeQuoteResponse(
|
||||
fromAmount = "1000000000000000000",
|
||||
fromDecimals = 18,
|
||||
toAmount = "100000000",
|
||||
toDecimals = 8,
|
||||
allowanceContract = null,
|
||||
minAmount = BigDecimal.ONE,
|
||||
quoteId = null,
|
||||
)
|
||||
|
||||
coEvery {
|
||||
tangemExpressApi.getExchangeQuote(
|
||||
fromAmount = any(),
|
||||
toAmount = any(),
|
||||
fromNetwork = any(),
|
||||
fromContractAddress = any(),
|
||||
fromDecimals = any(),
|
||||
toNetwork = any(),
|
||||
toContractAddress = any(),
|
||||
toDecimals = any(),
|
||||
providerId = any(),
|
||||
rateType = any(),
|
||||
userWalletId = any(),
|
||||
refCode = any(),
|
||||
)
|
||||
} returns ApiResponse.Success(quoteResponse)
|
||||
|
||||
// Act
|
||||
repository.getSwapQuote(
|
||||
userWallet = userWallet,
|
||||
fromCryptoCurrency = primaryCoin,
|
||||
toCryptoCurrency = secondaryCoin,
|
||||
amount = BigDecimal("1.5"),
|
||||
amountType = SwapAmountType.To,
|
||||
provider = expressProvider,
|
||||
rateType = ExpressRateType.Fixed,
|
||||
)
|
||||
|
||||
// Assert — toAmount is set, fromAmount is null
|
||||
coVerify {
|
||||
tangemExpressApi.getExchangeQuote(
|
||||
fromAmount = null,
|
||||
toAmount = "150000000",
|
||||
fromNetwork = ETH_BACKEND_ID,
|
||||
fromContractAddress = "0",
|
||||
fromDecimals = 18,
|
||||
toNetwork = BTC_BACKEND_ID,
|
||||
toContractAddress = "0",
|
||||
toDecimals = 8,
|
||||
providerId = PROVIDER_ID,
|
||||
rateType = "fixed",
|
||||
userWalletId = any(),
|
||||
refCode = any(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region getExchangeStatus
|
||||
|
||||
@Test
|
||||
fun `getExchangeStatus returns converted status model`() = runTest {
|
||||
// Arrange
|
||||
val statusResponse = ExchangeStatusResponse(
|
||||
providerId = PROVIDER_ID,
|
||||
status = ExchangeStatus.Finished,
|
||||
externalTxId = "ext-tx-1",
|
||||
externalTxUrl = "https://example.com/tx/1",
|
||||
error = null,
|
||||
)
|
||||
|
||||
coEvery {
|
||||
tangemExpressApi.getExchangeStatus(any(), any(), any())
|
||||
} returns ApiResponse.Success(statusResponse)
|
||||
|
||||
// Act
|
||||
val result = repository.getExchangeStatus(userWallet = userWallet, txId = "tx-123")
|
||||
|
||||
// Assert
|
||||
assertThat(result.providerId).isEqualTo(PROVIDER_ID)
|
||||
assertThat(result.status).isEqualTo(SwapStatus.Finished)
|
||||
assertThat(result.txId).isEqualTo("ext-tx-1")
|
||||
assertThat(result.txExternalUrl).isEqualTo("https://example.com/tx/1")
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region swapTransactionSent
|
||||
|
||||
@Test
|
||||
fun `swapTransactionSent calls exchangeSent API`() = runTest {
|
||||
// Arrange
|
||||
val fromStatus = createCryptoCurrencyStatus(primaryCoin)
|
||||
|
||||
coEvery {
|
||||
tangemExpressApi.exchangeSent(any(), any(), any())
|
||||
} returns ApiResponse.Success(ExchangeSentResponseBody(txId = "tx-1", status = "ok"))
|
||||
|
||||
// Act
|
||||
repository.swapTransactionSent(
|
||||
userWallet = userWallet,
|
||||
fromCryptoCurrencyStatus = fromStatus,
|
||||
payInAddress = "0xPayIn",
|
||||
txId = "tx-1",
|
||||
txHash = "0xHash",
|
||||
txExtraId = null,
|
||||
)
|
||||
|
||||
// Assert
|
||||
coVerify {
|
||||
tangemExpressApi.exchangeSent(
|
||||
userWalletId = any(),
|
||||
refCode = any(),
|
||||
body = match { body ->
|
||||
body.txId == "tx-1" &&
|
||||
body.txHash == "0xHash" &&
|
||||
body.payinAddress == "0xPayIn" &&
|
||||
body.payinExtraId == null
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region filterYieldSupplyProvider
|
||||
|
||||
@Test
|
||||
fun `getPairs filters out DEX providers when yield supply is active`() = runTest {
|
||||
// Arrange
|
||||
val primaryStatus = createCryptoCurrencyStatusWithActiveYield(primaryCoin)
|
||||
val secondaryStatus = createCryptoCurrencyStatus(secondaryCoin)
|
||||
val primarySwapCurrencyStatus = SwapCurrencyStatus(
|
||||
userWallet = userWallet,
|
||||
status = primaryStatus,
|
||||
account = mockk(),
|
||||
)
|
||||
val secondarySwapCurrencyStatus = SwapCurrencyStatus(
|
||||
userWallet = userWallet,
|
||||
status = secondaryStatus,
|
||||
account = mockk(),
|
||||
)
|
||||
|
||||
val swapPair = SwapPair(
|
||||
from = LeastTokenInfo(contractAddress = "0", network = ETH_BACKEND_ID),
|
||||
to = LeastTokenInfo(contractAddress = "0", network = BTC_BACKEND_ID),
|
||||
providers = listOf(
|
||||
SwapPairProvider(providerId = PROVIDER_ID, rateTypes = listOf(RateType.FLOAT)),
|
||||
SwapPairProvider(providerId = CEX_PROVIDER_ID, rateTypes = listOf(RateType.FLOAT)),
|
||||
),
|
||||
)
|
||||
|
||||
coEvery {
|
||||
tangemExpressApi.getPairs(any(), any(), any())
|
||||
} returns ApiResponse.Success(listOf(swapPair))
|
||||
|
||||
coEvery {
|
||||
expressRepository.getProviders(any(), any())
|
||||
} returns listOf(dexProvider, cexProvider)
|
||||
|
||||
// Act
|
||||
val result = repository.getPairs(
|
||||
primarySwapCurrencyStatus = primarySwapCurrencyStatus,
|
||||
secondarySwapCurrencyStatus = secondarySwapCurrencyStatus,
|
||||
filterProviderTypes = emptyList(),
|
||||
swapTxType = SwapTxType.Swap,
|
||||
)
|
||||
|
||||
// Assert — only CEX provider should remain
|
||||
assertThat(result).hasSize(2)
|
||||
val providers = result.first().providers
|
||||
assertThat(providers).hasSize(1)
|
||||
assertThat(providers.first().type).isEqualTo(ExpressProviderType.CEX)
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region getSwapData
|
||||
|
||||
@Test
|
||||
fun `getSwapData throws InvalidSignatureError when signature verification fails`() = runTest {
|
||||
// Arrange
|
||||
val fromStatus = createCryptoCurrencyStatus(primaryCoin)
|
||||
val exchangeDataResponse = ExchangeDataResponse(
|
||||
fromAmount = "1000000000000000000",
|
||||
fromDecimals = 18,
|
||||
toAmount = "100000000",
|
||||
toDecimals = 8,
|
||||
txId = "tx-123",
|
||||
txDetailsJson = "{}",
|
||||
signature = "invalid-sig",
|
||||
)
|
||||
|
||||
coEvery {
|
||||
tangemExpressApi.getExchangeData(
|
||||
fromContractAddress = any(),
|
||||
toContractAddress = any(),
|
||||
fromNetwork = any(),
|
||||
toNetwork = any(),
|
||||
fromAddress = any(),
|
||||
toAddress = any(),
|
||||
fromDecimals = any(),
|
||||
toDecimals = any(),
|
||||
fromAmount = any(),
|
||||
toAmount = any(),
|
||||
providerId = any(),
|
||||
rateType = any(),
|
||||
requestId = any(),
|
||||
refundAddress = any(),
|
||||
refundExtraId = any(),
|
||||
userWalletId = any(),
|
||||
partnerOperationType = any(),
|
||||
refCode = any(),
|
||||
toExtraId = any(),
|
||||
quoteId = any(),
|
||||
)
|
||||
} returns ApiResponse.Success(exchangeDataResponse)
|
||||
|
||||
every { dataSignatureVerifier.verifySignature(any(), any()) } returns false
|
||||
|
||||
// Act & Assert
|
||||
assertThrows<ExpressError.InvalidSignatureError> {
|
||||
repository.getSwapData(
|
||||
userWallet = userWallet,
|
||||
fromCryptoCurrencyStatus = fromStatus,
|
||||
toCryptoCurrency = secondaryCoin,
|
||||
amount = BigDecimal.ONE,
|
||||
amountType = SwapAmountType.From,
|
||||
toAddress = "0xToAddress",
|
||||
toExtraId = null,
|
||||
expressProvider = cexProvider,
|
||||
rateType = ExpressRateType.Float,
|
||||
expressOperationType = ExpressOperationType.SWAP,
|
||||
quoteId = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
private companion object {
|
||||
const val ETH_BACKEND_ID = "ethereum"
|
||||
const val BTC_BACKEND_ID = "bitcoin"
|
||||
const val PROVIDER_ID = "dex-provider-1"
|
||||
const val CEX_PROVIDER_ID = "cex-provider-1"
|
||||
|
||||
val userWallet: UserWallet = MockUserWalletFactory.create()
|
||||
|
||||
val ethNetwork: Network = mockk(relaxed = true) {
|
||||
every { rawId } returns ETH_BACKEND_ID
|
||||
}
|
||||
|
||||
val btcNetwork: Network = mockk(relaxed = true) {
|
||||
every { rawId } returns BTC_BACKEND_ID
|
||||
}
|
||||
|
||||
val primaryCoin: CryptoCurrency.Coin = mockk(relaxed = true) {
|
||||
every { network } returns ethNetwork
|
||||
every { decimals } returns 18
|
||||
every { id } returns mockk(relaxed = true) {
|
||||
every { rawCurrencyId } returns CryptoCurrency.RawID("ethereum")
|
||||
}
|
||||
}
|
||||
|
||||
val secondaryCoin: CryptoCurrency.Coin = mockk(relaxed = true) {
|
||||
every { network } returns btcNetwork
|
||||
every { decimals } returns 8
|
||||
every { id } returns mockk(relaxed = true) {
|
||||
every { rawCurrencyId } returns CryptoCurrency.RawID("bitcoin")
|
||||
}
|
||||
}
|
||||
|
||||
val expressProvider = ExpressProvider(
|
||||
providerId = PROVIDER_ID,
|
||||
rateTypes = listOf(ExpressRateType.Float),
|
||||
name = "Test DEX",
|
||||
type = ExpressProviderType.DEX,
|
||||
imageLarge = "",
|
||||
termsOfUse = null,
|
||||
privacyPolicy = null,
|
||||
slippage = null,
|
||||
)
|
||||
|
||||
val dexProvider = expressProvider
|
||||
|
||||
val cexProvider = ExpressProvider(
|
||||
providerId = CEX_PROVIDER_ID,
|
||||
rateTypes = listOf(ExpressRateType.Float),
|
||||
name = "Test CEX",
|
||||
type = ExpressProviderType.CEX,
|
||||
imageLarge = "",
|
||||
termsOfUse = null,
|
||||
privacyPolicy = null,
|
||||
slippage = null,
|
||||
)
|
||||
|
||||
fun createCryptoCurrencyStatus(currency: CryptoCurrency): CryptoCurrencyStatus {
|
||||
val value: CryptoCurrencyStatus.Value = mockk(relaxed = true) {
|
||||
every { yieldSupplyStatus } returns null
|
||||
every { networkAddress } returns mockk(relaxed = true) {
|
||||
every { defaultAddress } returns mockk(relaxed = true) {
|
||||
every { value } returns "0xAddress"
|
||||
}
|
||||
}
|
||||
}
|
||||
return CryptoCurrencyStatus(currency = currency, value = value)
|
||||
}
|
||||
|
||||
fun createCryptoCurrencyStatusWithActiveYield(currency: CryptoCurrency): CryptoCurrencyStatus {
|
||||
val yieldStatus: YieldSupplyStatus = mockk {
|
||||
every { isActive } returns true
|
||||
}
|
||||
val value: CryptoCurrencyStatus.Value = mockk(relaxed = true) {
|
||||
every { yieldSupplyStatus } returns yieldStatus
|
||||
every { networkAddress } returns mockk(relaxed = true) {
|
||||
every { defaultAddress } returns mockk(relaxed = true) {
|
||||
every { value } returns "0xAddress"
|
||||
}
|
||||
}
|
||||
}
|
||||
return CryptoCurrencyStatus(currency = currency, value = value)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue