Updated on 2026-08-14

This commit is contained in:
Tangem 2026-05-26 19:25:07 +04:00
parent a73dfc58af
commit 5ce5d3dc6c
20 changed files with 877 additions and 11 deletions

View file

@ -13,6 +13,10 @@ android {
namespace = "com.tangem.feature.swap.data"
}
tasks.withType<Test>().configureEach {
useJUnitPlatform()
}
dependencies {
/** AndroidX */
@ -61,4 +65,8 @@ dependencies {
implementation(deps.hilt.android)
kapt(deps.hilt.kapt)
/** Test */
testImplementation(projects.test.core)
testRuntimeOnly(deps.test.junit5.engine)
}

View file

@ -293,6 +293,7 @@ internal class DefaultSwapRepository(
QuoteModel(
toTokenAmount = createFromAmountWithOffset(response.toAmount, response.toDecimals),
allowanceContract = response.allowanceContract,
txType = response.txType?.toDomain(),
).right()
} catch (ex: Exception) {
getDataError(ex).left()

View file

@ -42,7 +42,9 @@ internal class ExpressDataConverter : Converter<ExchangeDataResponseWithTxDetail
txData = requireNotNull(transactionDto.txData),
txExtraId = transactionDto.txExtraId,
otherNativeFeeWei = otherNativeFeeWei,
gas = transactionDto.gas?.toBigIntegerOrNull() ?: error("gas is empty"),
// Nullable: providers may omit gas; the fee-fallback path handles null.
gas = transactionDto.gas?.toBigIntegerOrNull(),
allowanceContract = transactionDto.allowanceContract,
)
} else {
ExpressTransactionModel.CEX(

View file

@ -0,0 +1,9 @@
package com.tangem.feature.swap.converters
import com.tangem.datasource.api.express.models.response.TxType
import com.tangem.feature.swap.domain.models.domain.ExpressTxType
internal fun TxType.toDomain(): ExpressTxType = when (this) {
TxType.SEND -> ExpressTxType.SEND
TxType.SWAP -> ExpressTxType.SWAP
}

View file

@ -0,0 +1,169 @@
package com.tangem.feature.swap.converters
import com.google.common.truth.Truth.assertThat
import com.tangem.datasource.api.express.models.response.ExchangeDataResponse
import com.tangem.datasource.api.express.models.response.ExchangeDataResponseWithTxDetails
import com.tangem.datasource.api.express.models.response.TxDetails
import com.tangem.datasource.api.express.models.response.TxType
import com.tangem.feature.swap.domain.models.domain.ExpressTransactionModel
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import java.math.BigDecimal
import java.math.BigInteger
/**
* Unit tests for [ExpressDataConverter].
*
* Covered:
* - SWAP -> DEX with all fields propagated (allowanceContract, gas).
* - SWAP with gas null -> DEX without throwing.
* - SWAP with allowanceContract null -> DEX with allowanceContract null.
* - otherNativeFee "0" -> BigDecimal.ZERO parse path.
* - SEND -> CEX with externalTxId/externalTxUrl preserved.
*/
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
internal class ExpressDataConverterTest {
private val sut = ExpressDataConverter()
@Test
fun `GIVEN txType SWAP with allowanceContract and gas WHEN convert THEN returns DEX with all fields`() {
val dataResponse = buildDataResponse(fromAmount = "1000000000000000000", toAmount = "500000000000000000")
val txDetails = buildTxDetails(
txType = TxType.SWAP,
txFrom = "0xFrom",
txTo = "0xSwapContract",
txData = "0xdeadbeef",
txValue = "0",
gas = "21000",
allowanceContract = "0xSpender",
)
val result = sut.convert(ExchangeDataResponseWithTxDetails(dataResponse, txDetails))
assertThat(result.transaction).isInstanceOf(ExpressTransactionModel.DEX::class.java)
val dex = result.transaction as ExpressTransactionModel.DEX
assertThat(dex.txFrom).isEqualTo("0xFrom")
assertThat(dex.txTo).isEqualTo("0xSwapContract")
assertThat(dex.txData).isEqualTo("0xdeadbeef")
assertThat(dex.txValue).isEqualTo("0")
assertThat(dex.gas).isEqualTo(BigInteger.valueOf(21_000L))
assertThat(dex.allowanceContract).isEqualTo("0xSpender")
}
@Test
fun `GIVEN txType SWAP with gas null WHEN convert THEN returns DEX with gas null without throwing`() {
// Regression guard: the converter must accept a null gas value instead of raising.
val dataResponse = buildDataResponse()
val txDetails = buildTxDetails(txType = TxType.SWAP, gas = null)
val result = sut.convert(ExchangeDataResponseWithTxDetails(dataResponse, txDetails))
assertThat(result.transaction).isInstanceOf(ExpressTransactionModel.DEX::class.java)
val dex = result.transaction as ExpressTransactionModel.DEX
assertThat(dex.gas).isNull()
}
@Test
fun `GIVEN txType SWAP with allowanceContract null WHEN convert THEN returns DEX with allowanceContract null`() {
// Native EVM transfer / pre-approved scenario — no allowance required.
val dataResponse = buildDataResponse()
val txDetails = buildTxDetails(txType = TxType.SWAP, allowanceContract = null)
val result = sut.convert(ExchangeDataResponseWithTxDetails(dataResponse, txDetails))
assertThat(result.transaction).isInstanceOf(ExpressTransactionModel.DEX::class.java)
val dex = result.transaction as ExpressTransactionModel.DEX
assertThat(dex.allowanceContract).isNull()
}
@Test
fun `GIVEN txType SWAP with otherNativeFee zero string WHEN convert THEN returns DEX with otherNativeFeeWei zero`() {
// "0" string must round-trip to BigDecimal.ZERO without parse errors.
val dataResponse = buildDataResponse()
val txDetails = buildTxDetails(txType = TxType.SWAP, otherNativeFee = "0")
val result = sut.convert(ExchangeDataResponseWithTxDetails(dataResponse, txDetails))
val dex = result.transaction as ExpressTransactionModel.DEX
assertThat(dex.otherNativeFeeWei).isEquivalentAccordingToCompareTo(BigDecimal.ZERO)
}
@Test
fun `GIVEN txType SEND with externalTxId and externalTxUrl WHEN convert THEN returns CEX`() {
val dataResponse = buildDataResponse()
val txDetails = buildTxDetails(
txType = TxType.SEND,
txFrom = null,
txTo = "0xCexDepositAddress",
txData = null,
externalTxId = "ext-tx-id-1",
externalTxUrl = "https://explorer.example/tx/ext-tx-id-1",
txExtraIdName = "memo",
txExtraId = "12345",
)
val result = sut.convert(ExchangeDataResponseWithTxDetails(dataResponse, txDetails))
assertThat(result.transaction).isInstanceOf(ExpressTransactionModel.CEX::class.java)
val cex = result.transaction as ExpressTransactionModel.CEX
assertThat(cex.txTo).isEqualTo("0xCexDepositAddress")
assertThat(cex.externalTxId).isEqualTo("ext-tx-id-1")
assertThat(cex.externalTxUrl).isEqualTo("https://explorer.example/tx/ext-tx-id-1")
assertThat(cex.txExtraIdName).isEqualTo("memo")
assertThat(cex.txExtraId).isEqualTo("12345")
}
// -------------------------------------------------------------------------
// Builders
// -------------------------------------------------------------------------
private fun buildDataResponse(
fromAmount: String = "1000000000000000000",
fromDecimals: Int = 18,
toAmount: String = "500000",
toDecimals: Int = 6,
txId: String = "inner-tx-id",
): ExchangeDataResponse = ExchangeDataResponse(
fromAmount = fromAmount,
fromDecimals = fromDecimals,
toAmount = toAmount,
toDecimals = toDecimals,
txId = txId,
txDetailsJson = "{}",
signature = "sig",
)
@Suppress("LongParameterList")
private fun buildTxDetails(
txType: TxType = TxType.SWAP,
payoutAddress: String = "0xPayout",
requestId: String = "req-1",
txFrom: String? = "0xFrom",
txTo: String = "0xTo",
txData: String? = "0xdata",
txValue: String? = "0",
otherNativeFee: String? = null,
externalTxId: String? = null,
externalTxUrl: String? = null,
txExtraIdName: String? = null,
txExtraId: String? = null,
gas: String? = "21000",
allowanceContract: String? = null,
): TxDetails = TxDetails(
payoutAddress = payoutAddress,
requestId = requestId,
txType = txType,
txFrom = txFrom,
txTo = txTo,
txData = txData,
txValue = txValue,
otherNativeFee = otherNativeFee,
externalTxId = externalTxId,
externalTxUrl = externalTxUrl,
txExtraIdName = txExtraIdName,
txExtraId = txExtraId,
gas = gas,
allowanceContract = allowanceContract,
)
}