From b646b4f01080aa5a16435652878d4fd80b9f7dbb Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 14 May 2026 14:22:23 +0500 Subject: [PATCH] Updated on 2026-08-14 --- .../SwapInteractorImplLoadFeeForDexTest.kt | 745 ------------------ .../SwapInteractorImplOtherNativeFeeTest.kt | 359 --------- .../feature/swap/StateBuilderFeeStateTest.kt | 376 --------- ...SwapNotificationsFactoryFeeWarningsTest.kt | 475 ----------- 4 files changed, 1955 deletions(-) delete mode 100644 features/swap/domain/src/test/kotlin/com/tangem/feature/swap/domain/SwapInteractorImplLoadFeeForDexTest.kt delete mode 100644 features/swap/domain/src/test/kotlin/com/tangem/feature/swap/domain/SwapInteractorImplOtherNativeFeeTest.kt delete mode 100644 features/swap/impl/src/test/java/com/tangem/feature/swap/StateBuilderFeeStateTest.kt delete mode 100644 features/swap/impl/src/test/java/com/tangem/feature/swap/SwapNotificationsFactoryFeeWarningsTest.kt diff --git a/features/swap/domain/src/test/kotlin/com/tangem/feature/swap/domain/SwapInteractorImplLoadFeeForDexTest.kt b/features/swap/domain/src/test/kotlin/com/tangem/feature/swap/domain/SwapInteractorImplLoadFeeForDexTest.kt deleted file mode 100644 index 7fe9e7cf76..0000000000 --- a/features/swap/domain/src/test/kotlin/com/tangem/feature/swap/domain/SwapInteractorImplLoadFeeForDexTest.kt +++ /dev/null @@ -1,745 +0,0 @@ -package com.tangem.feature.swap.domain - -import android.util.Base64 -import arrow.core.left -import arrow.core.right -import com.google.common.truth.Truth.assertThat -import com.tangem.blockchain.blockchains.solana.SolanaTransactionHelper -import com.tangem.blockchain.common.Amount -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.TransactionData -import com.tangem.blockchain.common.TransactionExtras -import com.tangem.blockchain.common.transaction.Fee -import com.tangem.blockchain.common.transaction.TransactionFee -import com.tangem.blockchainsdk.utils.toNetworkId -import com.tangem.domain.models.StatusSource -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.quote.QuoteStatus -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.swap.models.SwapCurrencyStatus -import com.tangem.domain.tokens.model.FeePaidCurrency -import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck -import com.tangem.domain.transaction.error.GetFeeError -import com.tangem.domain.transaction.models.AllowanceInfo -import com.tangem.feature.swap.domain.models.ExpressDataError -import com.tangem.feature.swap.domain.models.SwapAmount -import com.tangem.feature.swap.domain.models.domain.ExchangeProviderType -import com.tangem.feature.swap.domain.models.domain.ExpressTransactionModel -import com.tangem.feature.swap.domain.models.domain.SwapDataModel -import com.tangem.feature.swap.domain.models.ui.SwapState -import io.mockk.* -import kotlinx.coroutines.test.runTest -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Disabled -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.TestInstance -import java.math.BigDecimal -import java.math.BigInteger - -/** - * Characterization tests for the private fee-loading paths of [SwapInteractorImpl] reached by - * the DEX provider branch: - * - * - `loadFeeForDex` - * - `getFeeDataForDexSwap` (EVM) - * - `getFeeDataForSolanaDexSwap` (Solana) - * - the `patchTransactionFeeForSwap` 12% gas-limit bump applied on EVM DEX - * - * Driven through the public [SwapInteractorImpl.findBestQuote] entry point with carefully - * stubbed dependencies so the DEX-fee branch executes deterministically. - * - * [REDACTED_TASK_KEY] — these tests are intentionally pinned to the **current** behavior so that the - * upcoming refactor (extraction into `DexSwapFeeCalculator`) is provably equivalent. - */ -/** - * [REDACTED_TASK_KEY] Phase 4 — `findBestQuote` no longer loads fees. The legacy `loadDexSwapData` is gone, - * replaced by `loadDexSwapDataNoFee` (no fee calls inside). Fee-loading characterization that was - * previously exercised via `findBestQuote` is now covered by: - * - `DexSwapFeeCalculatorTest` — for the raw fee strategy (EVM, Solana, fallback, size guard) - * - `SwapInteractorImplLoadSwapFeeTest` — for the unified entry point through `loadSwapFee` - * - `SwapInteractorImplApplySwapFeeTest` — for fee → state patching semantics - * - * This class is kept in source for reference and disabled. Phase 5 removes it. - */ -@Disabled( - "[REDACTED_TASK_KEY] Phase 4: findBestQuote no longer loads fees. " + - "See DexSwapFeeCalculatorTest, SwapInteractorImplLoadSwapFeeTest, SwapInteractorImplApplySwapFeeTest.", -) -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -internal class SwapInteractorImplLoadFeeForDexTest : SwapInteractorImplTestBase() { - - private val ethNetwork = Blockchain.Ethereum.toNetworkId() - private val solanaNetwork = Blockchain.Solana.toNetworkId() - private val btcNetwork = Blockchain.Bitcoin.toNetworkId() - - @BeforeEach - fun setup() { - coEvery { currenciesRepository.getFeePaidCurrency(any(), any()) } returns FeePaidCurrency.Coin - coEvery { walletManagersFacade.getNativeTokenBalance(any(), any(), any()) } returns BigDecimal("10") - coEvery { - getCurrencyCheckUseCase.invoke( - userWalletId = any(), - currencyStatus = any(), - feeCurrencyStatus = any(), - amount = any(), - fee = any(), - feeCurrencyBalanceAfterTransaction = any(), - recipientAddress = any(), - ) - } returns CryptoCurrencyCheck( - dustValue = null, - reserveAmount = null, - minimumSendAmount = null, - existentialDeposit = null, - utxoAmountLimit = null, - isAccountFunded = true, - rentWarning = null, - isMemoRequired = false, - ) - coEvery { - validateTransactionUseCase.invoke( - amount = any(), - fee = any(), - memo = any(), - destination = any(), - userWalletId = any(), - network = any(), - ) - } returns Unit.right() - coEvery { quotesRepository.getMultiQuoteSyncOrNull(any()) } answers { - firstArg>().map { rawId -> - QuoteStatus( - rawCurrencyId = rawId, - value = QuoteStatus.Data( - source = StatusSource.ACTUAL, - fiatRate = BigDecimal.ONE, - fiatRateUSD = BigDecimal.ONE, - priceChange = BigDecimal.ZERO, - ), - ) - }.toSet() - } - coEvery { multiQuoteStatusFetcher.invoke(any()) } returns Unit.right() - coEvery { getFeePaidCryptoCurrencyStatusSyncUseCase.invoke(any(), any()) } returns null.right() - coEvery { multiWalletCryptoCurrenciesSupplier.getSyncOrNull(any()) } returns null - coEvery { currenciesRepository.createCoinCurrency(any()) } returns buildCoinCurrency() - every { allowPermissionsHandler.isAddressAllowanceInProgress(any()) } returns false - coEvery { - getAllowanceInfoUseCase.invoke( - userWalletId = any(), - cryptoCurrency = any(), - spenderAddress = any(), - requiredAmount = any(), - ) - } returns (AllowanceInfo.Enough(allowance = BigDecimal("1000")) as AllowanceInfo).right() - every { createTransactionExtrasUseCase.invoke(data = any(), network = any()) } returns - mockk(relaxed = true).right() - } - - @Test - fun `EVM DEX swap propagates extras destinationAddress sourceAddress and amount to getFeeUseCase`() = runTest { - // Given - val dexProvider = buildSwapProvider(ExchangeProviderType.DEX) - val fromStatus = buildSwapCurrencyStatus( - networkRawId = ethNetwork, - isCoin = true, - amount = BigDecimal("10"), - ) - val toStatus = buildSwapCurrencyStatus(networkRawId = btcNetwork) - val quoteModel = buildQuoteModel() - val swapData = buildSwapDataModelDex( - txValue = "1000000000000000", // 0.001 ETH - txTo = "0xRecipient", - txFrom = "0xSender", - txData = "0xPayload", - ) - - coEvery { - repository.findBestQuote( - userWallet = any(), - fromContractAddress = any(), - fromNetwork = any(), - toContractAddress = any(), - toNetwork = any(), - fromAmount = any(), - fromDecimals = any(), - toDecimals = any(), - providerId = dexProvider.providerId, - rateType = any(), - ) - } returns quoteModel.right() - - coEvery { - repository.getExchangeData( - userWallet = any(), - fromContractAddress = any(), - fromNetwork = any(), - toContractAddress = any(), - fromAddress = any(), - toNetwork = any(), - fromAmount = any(), - fromDecimals = any(), - toDecimals = any(), - providerId = dexProvider.providerId, - rateType = any(), - toAddress = any(), - expressOperationType = any(), - refundAddress = any(), - ) - } returns swapData.right() - - val capturedTxData = slot() - coEvery { - getFeeUseCase.invoke( - userWallet = any(), - network = any(), - transactionData = capture(capturedTxData), - ) - } returns mockk(relaxed = true).right() - - // When - sut.findBestQuote( - fromSwapCurrencyStatus = fromStatus, - toSwapCurrencyStatus = toStatus, - providers = listOf(dexProvider), - amountToSwap = "1.0", - reduceBalanceBy = BigDecimal.ZERO, - - ) - - // Then — captured TransactionData carries the values from ExpressTransactionModel.DEX - assertThat(capturedTxData.isCaptured).isTrue() - val uncompiled = capturedTxData.captured as TransactionData.Uncompiled - assertThat(uncompiled.destinationAddress).isEqualTo("0xRecipient") - assertThat(uncompiled.sourceAddress).isEqualTo("0xSender") - // amount.value is the txValue moved-point-left by native decimals (18 for ETH) → 0.001 - // Use compareTo-equivalence to ignore the BigDecimal scale (0.001 vs 0.001000000000000000). - assertThat(uncompiled.amount.value).isEquivalentAccordingToCompareTo(BigDecimal("0.001")) - // extras came from createTransactionExtrasUseCase - assertThat(uncompiled.extras).isNotNull() - } - - @Test - fun `EVM DEX swap with native balance ZERO surfaces SwapError UnknownError`() = runTest { - // Given — zero native balance triggers the early-raise in getFeeDataForDexSwap - val dexProvider = buildSwapProvider(ExchangeProviderType.DEX) - val fromStatus = buildSwapCurrencyStatus( - networkRawId = ethNetwork, - isCoin = true, - amount = BigDecimal("10"), - ) - val toStatus = buildSwapCurrencyStatus(networkRawId = btcNetwork) - val quoteModel = buildQuoteModel() - val swapData = buildSwapDataModelDex(txValue = "0") - - coEvery { walletManagersFacade.getNativeTokenBalance(any(), any(), any()) } returns BigDecimal.ZERO - - coEvery { - repository.findBestQuote( - userWallet = any(), - fromContractAddress = any(), - fromNetwork = any(), - toContractAddress = any(), - toNetwork = any(), - fromAmount = any(), - fromDecimals = any(), - toDecimals = any(), - providerId = dexProvider.providerId, - rateType = any(), - ) - } returns quoteModel.right() - coEvery { - repository.getExchangeData( - userWallet = any(), - fromContractAddress = any(), - fromNetwork = any(), - toContractAddress = any(), - fromAddress = any(), - toNetwork = any(), - fromAmount = any(), - fromDecimals = any(), - toDecimals = any(), - providerId = dexProvider.providerId, - rateType = any(), - toAddress = any(), - expressOperationType = any(), - refundAddress = any(), - ) - } returns swapData.right() - - // When - val result = sut.findBestQuote( - fromSwapCurrencyStatus = fromStatus, - toSwapCurrencyStatus = toStatus, - providers = listOf(dexProvider), - amountToSwap = "1.0", - reduceBalanceBy = BigDecimal.ZERO, - - ) - - // Then — native-balance == 0 raises ExpressDataError.UnknownError up to SwapError - val state = result[dexProvider] - assertThat(state).isInstanceOf(SwapState.SwapError::class.java) - val swapError = state as SwapState.SwapError - assertThat(swapError.error).isEqualTo(ExpressDataError.UnknownError) - // getFeeUseCase should NOT have been invoked because the balance check short-circuits first - coVerify(exactly = 0) { - getFeeUseCase.invoke(userWallet = any(), network = any(), transactionData = any()) - } - } - - @Test - fun `EVM DEX swap falls back to getEthSpecificFeeUseCase when txValue is null`() = runTest { - // Given — null txValue forces error("unable to get txValue") → IllegalStateException → fallback - val dexProvider = buildSwapProvider(ExchangeProviderType.DEX) - val fromStatus = buildSwapCurrencyStatus( - networkRawId = ethNetwork, - isCoin = true, - amount = BigDecimal("10"), - ) - val toStatus = buildSwapCurrencyStatus(networkRawId = btcNetwork) - val quoteModel = buildQuoteModel() - val gas = BigInteger.valueOf(150_000L) - val swapData = buildSwapDataModelDex(txValue = null, gas = gas) - - coEvery { - repository.findBestQuote( - userWallet = any(), - fromContractAddress = any(), - fromNetwork = any(), - toContractAddress = any(), - toNetwork = any(), - fromAmount = any(), - fromDecimals = any(), - toDecimals = any(), - providerId = dexProvider.providerId, - rateType = any(), - ) - } returns quoteModel.right() - coEvery { - repository.getExchangeData( - userWallet = any(), - fromContractAddress = any(), - fromNetwork = any(), - toContractAddress = any(), - fromAddress = any(), - toNetwork = any(), - fromAmount = any(), - fromDecimals = any(), - toDecimals = any(), - providerId = dexProvider.providerId, - rateType = any(), - toAddress = any(), - expressOperationType = any(), - refundAddress = any(), - ) - } returns swapData.right() - - coEvery { - getEthSpecificFeeUseCase.invoke( - userWallet = any(), - cryptoCurrency = any(), - gasLimit = any(), - gasPrice = any(), - ) - } returns mockk(relaxed = true).right() - - // When - sut.findBestQuote( - fromSwapCurrencyStatus = fromStatus, - toSwapCurrencyStatus = toStatus, - providers = listOf(dexProvider), - amountToSwap = "1.0", - reduceBalanceBy = BigDecimal.ZERO, - - ) - - // Then — fallback path is invoked with the gas from the express transaction model - coVerify(exactly = 1) { - getEthSpecificFeeUseCase.invoke( - userWallet = any(), - cryptoCurrency = any(), - gasLimit = gas, - gasPrice = any(), - ) - } - } - - @Test - fun `EVM DEX swap falls back to getEthSpecificFeeUseCase when createTransactionExtrasUseCase returns null`() = - runTest { - // Given — null extras → error("unable to create extras") → IllegalStateException → fallback - val dexProvider = buildSwapProvider(ExchangeProviderType.DEX) - val fromStatus = buildSwapCurrencyStatus( - networkRawId = ethNetwork, - isCoin = true, - amount = BigDecimal("10"), - ) - val toStatus = buildSwapCurrencyStatus(networkRawId = btcNetwork) - val quoteModel = buildQuoteModel() - val gas = BigInteger.valueOf(75_000L) - val swapData = buildSwapDataModelDex(txValue = "1000000000000000", gas = gas) - - coEvery { - repository.findBestQuote( - userWallet = any(), - fromContractAddress = any(), - fromNetwork = any(), - toContractAddress = any(), - toNetwork = any(), - fromAmount = any(), - fromDecimals = any(), - toDecimals = any(), - providerId = dexProvider.providerId, - rateType = any(), - ) - } returns quoteModel.right() - coEvery { - repository.getExchangeData( - userWallet = any(), - fromContractAddress = any(), - fromNetwork = any(), - toContractAddress = any(), - fromAddress = any(), - toNetwork = any(), - fromAmount = any(), - fromDecimals = any(), - toDecimals = any(), - providerId = dexProvider.providerId, - rateType = any(), - toAddress = any(), - expressOperationType = any(), - refundAddress = any(), - ) - } returns swapData.right() - - // Force createTransactionExtrasUseCase to return null → triggers the fallback path. - // The use case signature is Either; pass a Throwable Left. - every { - createTransactionExtrasUseCase.invoke(data = any(), network = any()) - } returns IllegalStateException("forced fail").left() - - coEvery { - getEthSpecificFeeUseCase.invoke( - userWallet = any(), - cryptoCurrency = any(), - gasLimit = any(), - gasPrice = any(), - ) - } returns mockk(relaxed = true).right() - - // When - sut.findBestQuote( - fromSwapCurrencyStatus = fromStatus, - toSwapCurrencyStatus = toStatus, - providers = listOf(dexProvider), - amountToSwap = "1.0", - reduceBalanceBy = BigDecimal.ZERO, - - ) - - // Then - coVerify(exactly = 1) { - getEthSpecificFeeUseCase.invoke( - userWallet = any(), - cryptoCurrency = any(), - gasLimit = gas, - gasPrice = any(), - ) - } - } - - @Test - fun `EVM DEX swap falls back to getEthSpecificFeeUseCase when getFeeUseCase returns null`() = runTest { - // Given — getFeeUseCase Left → getOrNull() == null → error("unable to calculate fee") → fallback - val dexProvider = buildSwapProvider(ExchangeProviderType.DEX) - val fromStatus = buildSwapCurrencyStatus( - networkRawId = ethNetwork, - isCoin = true, - amount = BigDecimal("10"), - ) - val toStatus = buildSwapCurrencyStatus(networkRawId = btcNetwork) - val quoteModel = buildQuoteModel() - val gas = BigInteger.valueOf(50_000L) - val swapData = buildSwapDataModelDex(txValue = "1000000000000000", gas = gas) - - coEvery { - repository.findBestQuote( - userWallet = any(), - fromContractAddress = any(), - fromNetwork = any(), - toContractAddress = any(), - toNetwork = any(), - fromAmount = any(), - fromDecimals = any(), - toDecimals = any(), - providerId = dexProvider.providerId, - rateType = any(), - ) - } returns quoteModel.right() - coEvery { - repository.getExchangeData( - userWallet = any(), - fromContractAddress = any(), - fromNetwork = any(), - toContractAddress = any(), - fromAddress = any(), - toNetwork = any(), - fromAmount = any(), - fromDecimals = any(), - toDecimals = any(), - providerId = dexProvider.providerId, - rateType = any(), - toAddress = any(), - expressOperationType = any(), - refundAddress = any(), - ) - } returns swapData.right() - - coEvery { - getFeeUseCase.invoke(userWallet = any(), network = any(), transactionData = any()) - } returns GetFeeError.UnknownError.left() - - coEvery { - getEthSpecificFeeUseCase.invoke( - userWallet = any(), - cryptoCurrency = any(), - gasLimit = any(), - gasPrice = any(), - ) - } returns mockk(relaxed = true).right() - - // When - sut.findBestQuote( - fromSwapCurrencyStatus = fromStatus, - toSwapCurrencyStatus = toStatus, - providers = listOf(dexProvider), - amountToSwap = "1.0", - reduceBalanceBy = BigDecimal.ZERO, - - ) - - // Then - coVerify(exactly = 1) { - getEthSpecificFeeUseCase.invoke( - userWallet = any(), - cryptoCurrency = any(), - gasLimit = gas, - gasPrice = any(), - ) - } - } - - @Test - fun `Solana DEX uses TransactionData Compiled and skips the 12 percent gas patch`() = runTest { - // Given — Solana network forces the Compiled path. We capture the TransactionData and - // assert that the resulting fee value is the raw return of getFeeUseCase (no 1.12x scaling). - mockkStatic(Base64::class) - every { Base64.decode(any(), any()) } returns ByteArray(64) - mockkObject(SolanaTransactionHelper) - every { SolanaTransactionHelper.removeSignaturesPlaceholders(any()) } returns ByteArray(64) - - val dexProvider = buildSwapProvider(ExchangeProviderType.DEX) - val fromStatus = buildSwapCurrencyStatus( - networkRawId = solanaNetwork, - isCoin = true, - amount = BigDecimal("10"), - ) - val toStatus = buildSwapCurrencyStatus(networkRawId = solanaNetwork) - val quoteModel = buildQuoteModel() - val swapData = buildSwapDataModelDex(txData = "U29sYW5h") - - coEvery { - repository.findBestQuote( - userWallet = any(), - fromContractAddress = any(), - fromNetwork = any(), - toContractAddress = any(), - toNetwork = any(), - fromAmount = any(), - fromDecimals = any(), - toDecimals = any(), - providerId = dexProvider.providerId, - rateType = any(), - ) - } returns quoteModel.right() - coEvery { - repository.getExchangeData( - userWallet = any(), - fromContractAddress = any(), - fromNetwork = any(), - toContractAddress = any(), - fromAddress = any(), - toNetwork = any(), - fromAmount = any(), - fromDecimals = any(), - toDecimals = any(), - providerId = dexProvider.providerId, - rateType = any(), - toAddress = any(), - expressOperationType = any(), - refundAddress = any(), - ) - } returns swapData.right() - - // Construct a deterministic Solana fee — Fee.Common with a known amount value. - val rawFeeAmount = BigDecimal("0.005000") - val rawFee: Fee = Fee.Common( - amount = Amount( - currencySymbol = "SOL", - value = rawFeeAmount, - decimals = 9, - ), - ) - val txFee = TransactionFee.Single(normal = rawFee) - val capturedTxData = slot() - coEvery { - getFeeUseCase.invoke( - userWallet = any(), - network = any(), - transactionData = capture(capturedTxData), - ) - } returns txFee.right() - - // When - sut.findBestQuote( - fromSwapCurrencyStatus = fromStatus, - toSwapCurrencyStatus = toStatus, - providers = listOf(dexProvider), - amountToSwap = "1.0", - reduceBalanceBy = BigDecimal.ZERO, - - ) - - // Then — TransactionData passed to getFeeUseCase is Compiled (not Uncompiled) - assertThat(capturedTxData.isCaptured).isTrue() - assertThat(capturedTxData.captured).isInstanceOf(TransactionData.Compiled::class.java) - // No gas-patch is applied on the Solana path; the raw amount is preserved. - // Pinning behavior: Fee.Common is not a Fee.Ethereum, so increaseEthGasLimitInNeeded - // returns it unchanged → no 1.12x scaling. - assertThat(rawFee.amount.value).isEqualTo(rawFeeAmount) - } - - @Test - fun `Solana DEX size guard raises TooLargeSolanaTransactionError when formatted hash exceeds 1232 bytes on Cold wallet`() = - runTest { - // Given — formatted hash > 1232 bytes on a Cold wallet → SwapError(TooLargeSolanaTransactionError) - mockkStatic(Base64::class) - val oversizedBytes = ByteArray(1300) - every { Base64.decode(any(), any()) } returns oversizedBytes - mockkObject(SolanaTransactionHelper) - every { SolanaTransactionHelper.removeSignaturesPlaceholders(any()) } returns oversizedBytes - - val dexProvider = buildSwapProvider(ExchangeProviderType.DEX) - val coldWallet = mockk(relaxed = true) - val fromStatus = buildSwapCurrencyStatus( - networkRawId = solanaNetwork, - isCoin = true, - amount = BigDecimal("10"), - ).let { status -> - SwapCurrencyStatus( - userWallet = coldWallet, - status = status.status, - account = status.account, - ) - } - val toStatus = buildSwapCurrencyStatus(networkRawId = solanaNetwork) - val quoteModel = buildQuoteModel() - val swapData = buildSwapDataModelDex(txData = "very-long-base64-content==") - - coEvery { - repository.findBestQuote( - userWallet = any(), - fromContractAddress = any(), - fromNetwork = any(), - toContractAddress = any(), - toNetwork = any(), - fromAmount = any(), - fromDecimals = any(), - toDecimals = any(), - providerId = dexProvider.providerId, - rateType = any(), - ) - } returns quoteModel.right() - coEvery { - repository.getExchangeData( - userWallet = any(), - fromContractAddress = any(), - fromNetwork = any(), - toContractAddress = any(), - fromAddress = any(), - toNetwork = any(), - fromAmount = any(), - fromDecimals = any(), - toDecimals = any(), - providerId = dexProvider.providerId, - rateType = any(), - toAddress = any(), - expressOperationType = any(), - refundAddress = any(), - ) - } returns swapData.right() - - // When - val result = sut.findBestQuote( - fromSwapCurrencyStatus = fromStatus, - toSwapCurrencyStatus = toStatus, - providers = listOf(dexProvider), - amountToSwap = "1.0", - reduceBalanceBy = BigDecimal.ZERO, - - ) - - // Then - val state = result[dexProvider] - assertThat(state).isInstanceOf(SwapState.SwapError::class.java) - val swapError = state as SwapState.SwapError - assertThat(swapError.error).isEqualTo(ExpressDataError.TooLargeSolanaTransactionError) - } - - /** - * Surprising current behavior pinned here for the redesign: - * - * In `loadDexSwapData`, the local `txFeeState` produced by `loadFeeForDex(...).toTxFeeState(...)` - * is computed but NEVER PROPAGATED to `QuotesLoadedState.txFee`. The latter is sourced from - * the input `txFeeSealedState` parameter via `updateBalances`. This means the 12% gas patch - * is applied (the side-effect runs) but the patched value is then discarded for state - * purposes; only the side effects of `loadFeeForDex` (raising on Solana size limit, balance=0, - * etc.) survive. - * - * The 12% gas-patch math itself is fully covered by the planned Phase-2 PatchEthGasLimitForSwapTest; - * pinning it through the public API here would only assert the discarded result. - * - * [REDACTED_TASK_KEY] — flagged for Phase-2 author awareness; the refactor MUST decide whether to: - * (a) preserve the dead-store (unlikely), or - * (b) actually wire the loaded fee into the resulting state (the intended fix). - */ - - // region — local builders - - private fun buildSwapDataModelDex( - txData: String = "dGVzdA==", - txValue: String? = "0", - toAmount: BigDecimal = BigDecimal("0.5"), - otherNativeFeeWei: BigDecimal? = null, - gas: BigInteger = BigInteger.valueOf(21_000L), - txTo: String = "0xRecipient", - txFrom: String = "0xSender", - ): SwapDataModel = SwapDataModel( - toTokenAmount = SwapAmount(toAmount, 18), - transaction = ExpressTransactionModel.DEX( - fromAmount = SwapAmount(BigDecimal.ONE, 18), - toAmount = SwapAmount(toAmount, 18), - txValue = txValue, - txId = "tx-id-123", - txTo = txTo, - txExtraId = null, - txFrom = txFrom, - txData = txData, - otherNativeFeeWei = otherNativeFeeWei, - gas = gas, - ), - ) - - // endregion -} \ No newline at end of file diff --git a/features/swap/domain/src/test/kotlin/com/tangem/feature/swap/domain/SwapInteractorImplOtherNativeFeeTest.kt b/features/swap/domain/src/test/kotlin/com/tangem/feature/swap/domain/SwapInteractorImplOtherNativeFeeTest.kt deleted file mode 100644 index 93db5c79f7..0000000000 --- a/features/swap/domain/src/test/kotlin/com/tangem/feature/swap/domain/SwapInteractorImplOtherNativeFeeTest.kt +++ /dev/null @@ -1,359 +0,0 @@ -package com.tangem.feature.swap.domain - -import arrow.core.right -import com.google.common.truth.Truth.assertThat -import com.tangem.blockchain.common.Amount -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.TransactionExtras -import com.tangem.blockchain.common.transaction.Fee -import com.tangem.blockchain.common.transaction.TransactionFee -import com.tangem.blockchainsdk.utils.toNetworkId -import com.tangem.domain.models.StatusSource -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.quote.QuoteStatus -import com.tangem.domain.tokens.model.FeePaidCurrency -import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck -import com.tangem.domain.transaction.models.AllowanceInfo -import com.tangem.feature.swap.domain.models.SwapAmount -import com.tangem.feature.swap.domain.models.domain.ExchangeProviderType -import com.tangem.feature.swap.domain.models.domain.ExpressTransactionModel -import com.tangem.feature.swap.domain.models.domain.SwapDataModel -import com.tangem.feature.swap.domain.models.domain.SwapFeeState -import com.tangem.feature.swap.domain.models.ui.SwapState -import com.tangem.feature.swap.domain.models.ui.TxFeeState -import io.mockk.coEvery -import io.mockk.every -import io.mockk.mockk -import kotlinx.coroutines.test.runTest -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Disabled -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.TestInstance -import java.math.BigDecimal -import java.math.BigInteger - -/** - * Characterization tests for the bridge-fee field `otherNativeFeeWei` flowing into the - * resulting [TxFeeState]. - * - * Pinned behavior: - * - `otherNativeFee` (BigDecimal) = `transaction.otherNativeFeeWei` shifted left by native - * decimals (18 for ETH). - * - `feeIncludeOtherNativeFee` of the resulting `TxFee.Legacy` equals `feeValue + otherNativeFee`. - * - When `otherNativeFeeWei == null`, `feeIncludeOtherNativeFee == feeValue`. - * - The `feeToCheckFunds` (the value used by `getFeeState`) equals - * `feeByPriority + otherNativeFee`. We assert this indirectly: when the native balance is - * BETWEEN `feeByPriority` and `feeByPriority + otherNativeFee`, the resulting - * `SwapFeeState` is `NotEnough` (not `Enough`). - * - * [REDACTED_TASK_KEY] — these tests exist to guarantee that the upcoming refactor does not silently - * drop the bridge protocol fee for DEX_BRIDGE providers. - */ -/** - * [REDACTED_TASK_KEY] Phase 4 — bridge `otherNativeFee` no longer flows through `findBestQuote` (fees aren't - * computed during quotes). The bridge-fee balance check is now exercised by - * `SwapInteractorImplApplySwapFeeTest` where `SwapFee.otherNativeFee` feeds the recomputed - * `feeToCheck = swapFee.fee + otherNativeFee`. The raw propagation from - * `ExpressTransactionModel.DEX.otherNativeFeeWei` is covered by `DexSwapFeeCalculatorTest`. - * - * This class is kept in source for reference and disabled. Phase 5 removes it. - */ -@Disabled( - "[REDACTED_TASK_KEY] Phase 4: otherNativeFee no longer flows through findBestQuote. " + - "See SwapInteractorImplApplySwapFeeTest and DexSwapFeeCalculatorTest.", -) -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -internal class SwapInteractorImplOtherNativeFeeTest : SwapInteractorImplTestBase() { - - private val ethNetwork = Blockchain.Ethereum.toNetworkId() - private val btcNetwork = Blockchain.Bitcoin.toNetworkId() - - @BeforeEach - fun setup() { - coEvery { currenciesRepository.getFeePaidCurrency(any(), any()) } returns FeePaidCurrency.Coin - // Default native balance is large; specific tests override. - coEvery { walletManagersFacade.getNativeTokenBalance(any(), any(), any()) } returns BigDecimal("10") - coEvery { - getCurrencyCheckUseCase.invoke( - userWalletId = any(), - currencyStatus = any(), - feeCurrencyStatus = any(), - amount = any(), - fee = any(), - feeCurrencyBalanceAfterTransaction = any(), - recipientAddress = any(), - ) - } returns CryptoCurrencyCheck( - dustValue = null, - reserveAmount = null, - minimumSendAmount = null, - existentialDeposit = null, - utxoAmountLimit = null, - isAccountFunded = true, - rentWarning = null, - isMemoRequired = false, - ) - coEvery { - validateTransactionUseCase.invoke( - amount = any(), - fee = any(), - memo = any(), - destination = any(), - userWalletId = any(), - network = any(), - ) - } returns Unit.right() - coEvery { quotesRepository.getMultiQuoteSyncOrNull(any()) } answers { - firstArg>().map { rawId -> - QuoteStatus( - rawCurrencyId = rawId, - value = QuoteStatus.Data( - source = StatusSource.ACTUAL, - fiatRate = BigDecimal.ONE, - fiatRateUSD = BigDecimal.ONE, - priceChange = BigDecimal.ZERO, - ), - ) - }.toSet() - } - coEvery { multiQuoteStatusFetcher.invoke(any()) } returns Unit.right() - coEvery { getFeePaidCryptoCurrencyStatusSyncUseCase.invoke(any(), any()) } returns null.right() - coEvery { multiWalletCryptoCurrenciesSupplier.getSyncOrNull(any()) } returns null - coEvery { currenciesRepository.createCoinCurrency(any()) } returns buildCoinCurrency() - every { allowPermissionsHandler.isAddressAllowanceInProgress(any()) } returns false - coEvery { - getAllowanceInfoUseCase.invoke( - userWalletId = any(), - cryptoCurrency = any(), - spenderAddress = any(), - requiredAmount = any(), - ) - } returns (AllowanceInfo.Enough(allowance = BigDecimal("1000")) as AllowanceInfo).right() - every { createTransactionExtrasUseCase.invoke(data = any(), network = any()) } returns - mockk(relaxed = true).right() - } - - /** - * Surprising current behavior (caught while writing this test): - * - * In `loadDexSwapData`, the local `txFeeState` produced by `loadFeeForDex(...).toTxFeeState(...)` - * is computed but **never used** for the resulting `QuotesLoadedState.txFee`. The actual - * `txFee` field of the resulting state is populated from the input `txFeeSealedState` - * parameter via `updateBalances` → which means the `feeIncludeOtherNativeFee` etc. on - * the returned state come from whatever the caller passes in, NOT from the loaded fee. - * - * What IS observable through the public API: - * - `feeByPriority + otherNativeFee` enters `feeToCheckFunds` and drives `feeState` - * (Enough vs NotEnough). This is verified in the two tests below. - * - * The "feeIncludeOtherNativeFee on the result.txFee" assertion is intentionally NOT - * pinned here — that field is sourced from the caller's `txFeeSealedState` and a refactor - * that fixes this dead-store will not break this test class. - * - * [REDACTED_TASK_KEY] — flagged for discussion before Phase 2. - */ - - @Test - fun `bridge provider with non-zero otherNativeFeeWei loads exchange data and reaches getFeeUseCase`() = runTest { - // Given — DEX_BRIDGE with otherNativeFeeWei = 5e15 wei = 0.005 ETH - val dexBridgeProvider = buildSwapProvider(ExchangeProviderType.DEX_BRIDGE) - val fromStatus = buildSwapCurrencyStatus( - networkRawId = ethNetwork, - isCoin = true, - amount = BigDecimal("10"), - ) - val toStatus = buildSwapCurrencyStatus(networkRawId = btcNetwork) - val quoteModel = buildQuoteModel() - val swapData = buildSwapDataModelDex( - txValue = "1000000000000000", - otherNativeFeeWei = BigDecimal("5000000000000000"), - ) - stubExchangeData(dexBridgeProvider, quoteModel, swapData) - - val rawFee: Fee = Fee.Common( - amount = Amount(currencySymbol = "ETH", value = BigDecimal("0.001"), decimals = 18), - ) - coEvery { - getFeeUseCase.invoke(userWallet = any(), network = any(), transactionData = any()) - } returns TransactionFee.Single(normal = rawFee).right() - - // When - val result = sut.findBestQuote( - fromSwapCurrencyStatus = fromStatus, - toSwapCurrencyStatus = toStatus, - providers = listOf(dexBridgeProvider), - amountToSwap = "1.0", - reduceBalanceBy = BigDecimal.ZERO, - - ) - - // Then — the bridge provider produces a QuotesLoadedState (no SwapError) - // and the swap data carries the otherNativeFeeWei. - val state = result[dexBridgeProvider] - assertThat(state).isInstanceOf(SwapState.QuotesLoadedState::class.java) - val loaded = state as SwapState.QuotesLoadedState - val transaction = loaded.swapDataModel?.transaction as? ExpressTransactionModel.DEX - assertThat(transaction?.otherNativeFeeWei).isEqualTo(BigDecimal("5000000000000000")) - } - - @Test - fun `feeToCheckFunds includes otherNativeFee — NotEnough fires when balance covers fee but not fee plus otherNativeFee`() = - runTest { - // Given — DEX_BRIDGE swap, native balance = 0.002 ETH - // base fee = 0.001 ETH, otherNativeFee = 0.005 ETH → feeToCheck = 0.006 ETH > balance - // For a Coin swap the feeState branch checks: nativeBalance - spendAmount > fee - // We swap a Token (so fromToken != Coin) → branch becomes: nativeBalance > fee - val dexBridgeProvider = buildSwapProvider(ExchangeProviderType.DEX_BRIDGE) - val fromTokenStatus = buildSwapCurrencyStatus( - networkRawId = ethNetwork, - contractAddress = "0xToken", - isCoin = false, - amount = BigDecimal("100"), - ) - val toStatus = buildSwapCurrencyStatus(networkRawId = btcNetwork) - val quoteModel = buildQuoteModel() - val swapData = buildSwapDataModelDex( - txValue = "1000000000000000", - otherNativeFeeWei = BigDecimal("5000000000000000"), // 0.005 ETH - ) - stubExchangeData(dexBridgeProvider, quoteModel, swapData) - - // Native balance: 0.002 ETH — enough for base fee (0.001) but NOT for combined (0.006). - coEvery { walletManagersFacade.getNativeTokenBalance(any(), any(), any()) } returns BigDecimal("0.002") - - val baseFeeValue = BigDecimal("0.001") - val rawFee: Fee = Fee.Common( - amount = Amount(currencySymbol = "ETH", value = baseFeeValue, decimals = 18), - ) - coEvery { - getFeeUseCase.invoke(userWallet = any(), network = any(), transactionData = any()) - } returns TransactionFee.Single(normal = rawFee).right() - - // When - val result = sut.findBestQuote( - fromSwapCurrencyStatus = fromTokenStatus, - toSwapCurrencyStatus = toStatus, - providers = listOf(dexBridgeProvider), - amountToSwap = "10", - reduceBalanceBy = BigDecimal.ZERO, - - ) - - // Then — feeToCheckFunds (0.006) > nativeBalance (0.002) → NotEnough - val state = result[dexBridgeProvider] - assertThat(state).isInstanceOf(SwapState.QuotesLoadedState::class.java) - val loaded = state as SwapState.QuotesLoadedState - assertThat(loaded.preparedSwapConfigState.feeState).isInstanceOf(SwapFeeState.NotEnough::class.java) - } - - @Test - fun `feeToCheckFunds excluding otherNativeFee would have been Enough — pinning the inclusion`() = runTest { - // Given — same shape as above but native balance = 0.002 ETH and otherNativeFee = 0 - // Verifies the contrapositive: with otherNativeFee == 0, balance covers the fee → Enough. - val dexBridgeProvider = buildSwapProvider(ExchangeProviderType.DEX_BRIDGE) - val fromTokenStatus = buildSwapCurrencyStatus( - networkRawId = ethNetwork, - contractAddress = "0xToken", - isCoin = false, - amount = BigDecimal("100"), - ) - val toStatus = buildSwapCurrencyStatus(networkRawId = btcNetwork) - val quoteModel = buildQuoteModel() - val swapData = buildSwapDataModelDex( - txValue = "1000000000000000", - otherNativeFeeWei = null, - ) - stubExchangeData(dexBridgeProvider, quoteModel, swapData) - - coEvery { walletManagersFacade.getNativeTokenBalance(any(), any(), any()) } returns BigDecimal("0.002") - - val baseFeeValue = BigDecimal("0.001") - val rawFee: Fee = Fee.Common( - amount = Amount(currencySymbol = "ETH", value = baseFeeValue, decimals = 18), - ) - coEvery { - getFeeUseCase.invoke(userWallet = any(), network = any(), transactionData = any()) - } returns TransactionFee.Single(normal = rawFee).right() - - // When - val result = sut.findBestQuote( - fromSwapCurrencyStatus = fromTokenStatus, - toSwapCurrencyStatus = toStatus, - providers = listOf(dexBridgeProvider), - amountToSwap = "10", - reduceBalanceBy = BigDecimal.ZERO, - - ) - - // Then — without otherNativeFee, the same balance is now sufficient. - val state = result[dexBridgeProvider] - assertThat(state).isInstanceOf(SwapState.QuotesLoadedState::class.java) - val loaded = state as SwapState.QuotesLoadedState - assertThat(loaded.preparedSwapConfigState.feeState).isInstanceOf(SwapFeeState.Enough::class.java) - } - - // region — local helpers - - private fun stubExchangeData( - provider: com.tangem.feature.swap.domain.models.domain.SwapProvider, - quoteModel: com.tangem.feature.swap.domain.models.domain.QuoteModel, - swapData: SwapDataModel, - ) { - coEvery { - repository.findBestQuote( - userWallet = any(), - fromContractAddress = any(), - fromNetwork = any(), - toContractAddress = any(), - toNetwork = any(), - fromAmount = any(), - fromDecimals = any(), - toDecimals = any(), - providerId = provider.providerId, - rateType = any(), - ) - } returns quoteModel.right() - coEvery { - repository.getExchangeData( - userWallet = any(), - fromContractAddress = any(), - fromNetwork = any(), - toContractAddress = any(), - fromAddress = any(), - toNetwork = any(), - fromAmount = any(), - fromDecimals = any(), - toDecimals = any(), - providerId = provider.providerId, - rateType = any(), - toAddress = any(), - expressOperationType = any(), - refundAddress = any(), - ) - } returns swapData.right() - } - - private fun buildSwapDataModelDex( - txData: String = "dGVzdA==", - txValue: String? = "0", - toAmount: BigDecimal = BigDecimal("0.5"), - otherNativeFeeWei: BigDecimal? = null, - gas: BigInteger = BigInteger.valueOf(21_000L), - ): SwapDataModel = SwapDataModel( - toTokenAmount = SwapAmount(toAmount, 18), - transaction = ExpressTransactionModel.DEX( - fromAmount = SwapAmount(BigDecimal.ONE, 18), - toAmount = SwapAmount(toAmount, 18), - txValue = txValue, - txId = "tx-id-bridge", - txTo = "0xRecipient", - txExtraId = null, - txFrom = "0xSender", - txData = txData, - otherNativeFeeWei = otherNativeFeeWei, - gas = gas, - ), - ) - - // endregion -} \ No newline at end of file diff --git a/features/swap/impl/src/test/java/com/tangem/feature/swap/StateBuilderFeeStateTest.kt b/features/swap/impl/src/test/java/com/tangem/feature/swap/StateBuilderFeeStateTest.kt deleted file mode 100644 index 7af4b86dd9..0000000000 --- a/features/swap/impl/src/test/java/com/tangem/feature/swap/StateBuilderFeeStateTest.kt +++ /dev/null @@ -1,376 +0,0 @@ -package com.tangem.feature.swap - -import com.google.common.truth.Truth.assertThat -import com.tangem.blockchain.common.transaction.Fee -import com.tangem.core.ui.extensions.stringReference -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.transaction.usecase.gasless.IsGaslessFeeSupportedForNetwork -import com.tangem.feature.swap.domain.models.SwapAmount -import com.tangem.feature.swap.domain.models.domain.* -import com.tangem.feature.swap.domain.models.ui.* -import com.tangem.feature.swap.models.SwapStateHolder -import com.tangem.feature.swap.models.UiActions -import com.tangem.feature.swap.models.states.FeeItemState -import com.tangem.feature.swap.ui.StateBuilder -import com.tangem.utils.Provider -import io.mockk.every -import io.mockk.mockk -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import java.math.BigDecimal - -/** - * Characterization tests for `StateBuilder.createFeeState` (private), exercised through the - * public `createQuotesLoadedState`. - * - * Pinned behavior: - * - `TxFeeState.Empty` → `FeeItemState.Empty` - * - `TxFeeState.SingleFeeState` → `FeeItemState.Content` with `isClickable = false` - * - `TxFeeState.MultipleFeeState` + `selectedFeeType = NORMAL` → uses normal fee values, isClickable = true - * - `TxFeeState.MultipleFeeState` + `selectedFeeType = PRIORITY` → uses priority fee values, isClickable = true - * - `hideFee = true` → always `FeeItemState.Empty` regardless of `txFee` - * - `feeCryptoFormattedWithNative` is what populates `FeeItemState.Content.amountCrypto`, - * NOT the plain `feeCryptoFormatted`. Same for fiat. (This pins the bridge-fee - * "display fee with native as workaround for okx" pathway.) - * - * [REDACTED_TASK_KEY] — these exist to guarantee the redesign's `FeeSelectorBlockComponent` carries - * the same display semantics across the cutover. - */ -internal class StateBuilderFeeStateTest { - - private val actions: UiActions = mockk(relaxed = true) - private val isBalanceHiddenProvider: Provider = mockk() - private val appCurrencyProvider: Provider = mockk() - private val isAccountsModeProvider: Provider = mockk() - private val isGaslessFeeSupportedForNetwork: IsGaslessFeeSupportedForNetwork = mockk() - - private lateinit var sut: StateBuilder - - private val userWalletId = UserWalletId("aabbccdd") - private val coldWallet: UserWallet.Cold = mockk(relaxed = true) { - every { walletId } returns userWalletId - } - - @BeforeEach - fun setup() { - every { isBalanceHiddenProvider() } returns false - every { appCurrencyProvider() } returns AppCurrency.Default - every { isAccountsModeProvider() } returns false - every { isGaslessFeeSupportedForNetwork(any()) } returns false - - sut = StateBuilder( - actions = actions, - isBalanceHiddenProvider = isBalanceHiddenProvider, - appCurrencyProvider = appCurrencyProvider, - isAccountsModeProvider = isAccountsModeProvider, - isGaslessFeeSupportedForNetwork = isGaslessFeeSupportedForNetwork, - shouldShowAbMenu = false, - ) - } - - @Test - fun `GIVEN TxFeeState Empty WHEN hideFee false THEN fee is FeeItemState Empty`() { - val baseState = buildReadyState() - val quoteModel = buildQuoteModel(txFeeState = TxFeeState.Empty) - - val result = sut.createQuotesLoadedState( - uiStateHolder = baseState, - quoteModel = quoteModel, - feeCryptoCurrencyStatus = null, - swapProvider = buildSwapProvider(), - bestRatedProviderId = "provider-id", - isNeedBestRateBadge = false, - selectedFeeType = FeeType.NORMAL, - needApplyFCARestrictions = false, - hideFee = false, - ) - - assertThat(result.fee).isInstanceOf(FeeItemState.Empty::class.java) - } - - @Test - fun `GIVEN SingleFeeState WHEN hideFee false THEN fee Content is not clickable`() { - val baseState = buildReadyState() - val singleFee = buildLegacyFee( - feeType = FeeType.NORMAL, - cryptoFormatted = "0.001 ETH", - cryptoFormattedWithNative = "0.001 ETH", - fiatFormatted = "$2.00", - fiatFormattedWithNative = "$2.00", - ) - val quoteModel = buildQuoteModel(txFeeState = TxFeeState.SingleFeeState(singleFee)) - - val result = sut.createQuotesLoadedState( - uiStateHolder = baseState, - quoteModel = quoteModel, - feeCryptoCurrencyStatus = null, - swapProvider = buildSwapProvider(), - bestRatedProviderId = "provider-id", - isNeedBestRateBadge = false, - selectedFeeType = FeeType.NORMAL, - needApplyFCARestrictions = false, - hideFee = false, - ) - - val feeContent = result.fee as FeeItemState.Content - assertThat(feeContent.isClickable).isFalse() - // Field source pinning: amountCrypto/fiatFormatted come from the *WithNative variants. - assertThat(feeContent.amountCrypto).isEqualTo("0.001 ETH") - assertThat(feeContent.amountFiatFormatted).isEqualTo("$2.00") - } - - @Test - fun `GIVEN MultipleFeeState WHEN selectedFeeType NORMAL THEN fee Content has normal fee values and is clickable`() { - val baseState = buildReadyState() - val normalFee = buildLegacyFee( - feeType = FeeType.NORMAL, - cryptoFormatted = "0.001 ETH", - cryptoFormattedWithNative = "0.001 ETH", - fiatFormatted = "$2.00", - fiatFormattedWithNative = "$2.00", - ) - val priorityFee = buildLegacyFee( - feeType = FeeType.PRIORITY, - cryptoFormatted = "0.005 ETH", - cryptoFormattedWithNative = "0.005 ETH", - fiatFormatted = "$10.00", - fiatFormattedWithNative = "$10.00", - ) - val quoteModel = buildQuoteModel( - txFeeState = TxFeeState.MultipleFeeState(normalFee = normalFee, priorityFee = priorityFee), - ) - - val result = sut.createQuotesLoadedState( - uiStateHolder = baseState, - quoteModel = quoteModel, - feeCryptoCurrencyStatus = null, - swapProvider = buildSwapProvider(), - bestRatedProviderId = "provider-id", - isNeedBestRateBadge = false, - selectedFeeType = FeeType.NORMAL, - needApplyFCARestrictions = false, - hideFee = false, - ) - - val feeContent = result.fee as FeeItemState.Content - assertThat(feeContent.isClickable).isTrue() - assertThat(feeContent.feeType).isEqualTo(FeeType.NORMAL) - assertThat(feeContent.amountCrypto).isEqualTo("0.001 ETH") - assertThat(feeContent.amountFiatFormatted).isEqualTo("$2.00") - } - - @Test - fun `GIVEN MultipleFeeState WHEN selectedFeeType PRIORITY THEN fee Content has priority fee values and is clickable`() { - val baseState = buildReadyState() - val normalFee = buildLegacyFee( - feeType = FeeType.NORMAL, - cryptoFormatted = "0.001 ETH", - cryptoFormattedWithNative = "0.001 ETH", - fiatFormatted = "$2.00", - fiatFormattedWithNative = "$2.00", - ) - val priorityFee = buildLegacyFee( - feeType = FeeType.PRIORITY, - cryptoFormatted = "0.005 ETH", - cryptoFormattedWithNative = "0.005 ETH", - fiatFormatted = "$10.00", - fiatFormattedWithNative = "$10.00", - ) - val quoteModel = buildQuoteModel( - txFeeState = TxFeeState.MultipleFeeState(normalFee = normalFee, priorityFee = priorityFee), - ) - - val result = sut.createQuotesLoadedState( - uiStateHolder = baseState, - quoteModel = quoteModel, - feeCryptoCurrencyStatus = null, - swapProvider = buildSwapProvider(), - bestRatedProviderId = "provider-id", - isNeedBestRateBadge = false, - selectedFeeType = FeeType.PRIORITY, - needApplyFCARestrictions = false, - hideFee = false, - ) - - val feeContent = result.fee as FeeItemState.Content - assertThat(feeContent.isClickable).isTrue() - assertThat(feeContent.feeType).isEqualTo(FeeType.PRIORITY) - assertThat(feeContent.amountCrypto).isEqualTo("0.005 ETH") - assertThat(feeContent.amountFiatFormatted).isEqualTo("$10.00") - } - - @Test - fun `GIVEN hideFee true WHEN any TxFeeState THEN fee is Empty`() { - val baseState = buildReadyState() - val singleFee = buildLegacyFee( - feeType = FeeType.NORMAL, - cryptoFormatted = "0.001 ETH", - cryptoFormattedWithNative = "0.001 ETH", - fiatFormatted = "$2.00", - fiatFormattedWithNative = "$2.00", - ) - val quoteModel = buildQuoteModel(txFeeState = TxFeeState.SingleFeeState(singleFee)) - - val result = sut.createQuotesLoadedState( - uiStateHolder = baseState, - quoteModel = quoteModel, - feeCryptoCurrencyStatus = null, - swapProvider = buildSwapProvider(), - bestRatedProviderId = "provider-id", - isNeedBestRateBadge = false, - selectedFeeType = FeeType.NORMAL, - needApplyFCARestrictions = false, - hideFee = true, - ) - - assertThat(result.fee).isInstanceOf(FeeItemState.Empty::class.java) - } - - @Test - fun `GIVEN otherNativeFee greater than feeValue WHEN SingleFeeState THEN amountCrypto reflects the With-Native variant`() { - // Bridge fee scenario: the WithNative formatted strings differ from the plain ones. - // StateBuilder.createFeeState picks `feeCryptoFormattedWithNative` (and fiat) — pinning that. - val baseState = buildReadyState() - val singleFee = buildLegacyFee( - feeType = FeeType.NORMAL, - cryptoFormatted = "0.001 ETH", - cryptoFormattedWithNative = "0.006 ETH", // includes 0.005 bridge native fee - fiatFormatted = "$2.00", - fiatFormattedWithNative = "$12.00", - ) - val quoteModel = buildQuoteModel(txFeeState = TxFeeState.SingleFeeState(singleFee)) - - val result = sut.createQuotesLoadedState( - uiStateHolder = baseState, - quoteModel = quoteModel, - feeCryptoCurrencyStatus = null, - swapProvider = buildSwapProvider(), - bestRatedProviderId = "provider-id", - isNeedBestRateBadge = false, - selectedFeeType = FeeType.NORMAL, - needApplyFCARestrictions = false, - hideFee = false, - ) - - val feeContent = result.fee as FeeItemState.Content - assertThat(feeContent.amountCrypto).isEqualTo("0.006 ETH") - assertThat(feeContent.amountFiatFormatted).isEqualTo("$12.00") - } - - @Test - fun `GIVEN otherNativeFee equal to feeValue WHEN SingleFeeState THEN amountCrypto equals plain feeCryptoFormatted`() { - // No bridge fee → WithNative strings happen to equal the plain strings. - val baseState = buildReadyState() - val singleFee = buildLegacyFee( - feeType = FeeType.NORMAL, - cryptoFormatted = "0.0007 ETH", - cryptoFormattedWithNative = "0.0007 ETH", - fiatFormatted = "$1.40", - fiatFormattedWithNative = "$1.40", - ) - val quoteModel = buildQuoteModel(txFeeState = TxFeeState.SingleFeeState(singleFee)) - - val result = sut.createQuotesLoadedState( - uiStateHolder = baseState, - quoteModel = quoteModel, - feeCryptoCurrencyStatus = null, - swapProvider = buildSwapProvider(), - bestRatedProviderId = "provider-id", - isNeedBestRateBadge = false, - selectedFeeType = FeeType.NORMAL, - needApplyFCARestrictions = false, - hideFee = false, - ) - - val feeContent = result.fee as FeeItemState.Content - assertThat(feeContent.amountCrypto).isEqualTo("0.0007 ETH") - assertThat(feeContent.amountFiatFormatted).isEqualTo("$1.40") - } - - // region — local fixtures - - private fun buildReadyState(): SwapStateHolder { - val fromStatus = buildSwapCurrencyStatus(coldWallet) - val toStatus = buildSwapCurrencyStatus(coldWallet) - return sut.createInitialReadyState( - uiStateHolder = sut.createInitialLoadingState(), - emptyAmountState = SwapState.EmptyAmountState( - zeroAmountEquivalent = stringReference("$0.00"), - ), - fromSwapCurrencyStatus = fromStatus, - toSwapCurrencyStatus = toStatus, - ) - } - - private fun buildQuoteModel( - txFeeState: TxFeeState, - ): SwapState.QuotesLoadedState { - val fromStatus = buildSwapCurrencyStatus(coldWallet) - val toStatus = buildSwapCurrencyStatus(coldWallet) - val fromInfo = TokenSwapInfo( - tokenAmount = SwapAmount(BigDecimal("1.0"), 18), - amountFiat = BigDecimal("100.00"), - swapCurrencyStatus = fromStatus, - ) - val toInfo = TokenSwapInfo( - tokenAmount = SwapAmount(BigDecimal("0.05"), 18), - amountFiat = BigDecimal("100.00"), - swapCurrencyStatus = toStatus, - ) - return SwapState.QuotesLoadedState( - fromTokenInfo = fromInfo, - toTokenInfo = toInfo, - priceImpact = PriceImpact.Empty, - preparedSwapConfigState = PreparedSwapConfigState( - isBalanceEnough = true, - feeState = SwapFeeState.Enough, - hasOutgoingTransaction = false, - includeFeeInAmount = IncludeFeeInAmount.Excluded, - ), - permissionState = PermissionDataState.Empty, - txFee = txFeeState, - currencyCheck = null, - validationResult = null, - minAdaValue = null, - swapProvider = buildSwapProvider(), - ) - } - - private fun buildSwapProvider(): SwapProvider = SwapProvider( - providerId = "provider-id", - rateTypes = listOf(RateType.FLOAT), - name = "TestProvider", - type = ExchangeProviderType.DEX, - imageLarge = "https://example.com/icon.png", - termsOfUse = null, - privacyPolicy = null, - isRecommended = false, - slippage = null, - isExtraIdSupported = false, - ) - - private fun buildLegacyFee( - feeType: FeeType, - cryptoFormatted: String, - cryptoFormattedWithNative: String, - fiatFormatted: String, - fiatFormattedWithNative: String, - ): TxFee.Legacy { - val fee: Fee = mockk(relaxed = true) - return TxFee.Legacy( - feeValue = BigDecimal("0.001"), - feeFiatFormatted = fiatFormatted, - feeCryptoFormatted = cryptoFormatted, - feeIncludeOtherNativeFee = BigDecimal.ZERO, - feeFiatFormattedWithNative = fiatFormattedWithNative, - feeCryptoFormattedWithNative = cryptoFormattedWithNative, - cryptoSymbol = "ETH", - feeType = feeType, - fee = fee, - ) - } - - // endregion -} \ No newline at end of file diff --git a/features/swap/impl/src/test/java/com/tangem/feature/swap/SwapNotificationsFactoryFeeWarningsTest.kt b/features/swap/impl/src/test/java/com/tangem/feature/swap/SwapNotificationsFactoryFeeWarningsTest.kt deleted file mode 100644 index 02fae0c38b..0000000000 --- a/features/swap/impl/src/test/java/com/tangem/feature/swap/SwapNotificationsFactoryFeeWarningsTest.kt +++ /dev/null @@ -1,475 +0,0 @@ -package com.tangem.feature.swap - -import com.google.common.truth.Truth.assertThat -import com.tangem.blockchain.common.transaction.Fee -import com.tangem.common.ui.notifications.NotificationUM -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.network.NetworkAddress -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.swap.models.SwapCurrencyStatus -import com.tangem.domain.transaction.usecase.gasless.IsGaslessFeeSupportedForNetwork -import com.tangem.feature.swap.domain.models.SwapAmount -import com.tangem.feature.swap.domain.models.domain.ExchangeProviderType -import com.tangem.feature.swap.domain.models.domain.IncludeFeeInAmount -import com.tangem.feature.swap.domain.models.domain.PreparedSwapConfigState -import com.tangem.feature.swap.domain.models.domain.RateType -import com.tangem.feature.swap.domain.models.domain.SwapFeeState -import com.tangem.feature.swap.domain.models.domain.SwapProvider -import com.tangem.feature.swap.domain.models.ui.FeeType -import com.tangem.feature.swap.domain.models.ui.PermissionDataState -import com.tangem.feature.swap.domain.models.ui.PriceImpact -import com.tangem.feature.swap.domain.models.ui.SwapState -import com.tangem.feature.swap.domain.models.ui.TokenSwapInfo -import com.tangem.feature.swap.domain.models.ui.TxFee -import com.tangem.feature.swap.domain.models.ui.TxFeeState -import com.tangem.feature.swap.model.SwapNotificationsFactory -import com.tangem.feature.swap.models.UiActions -import com.tangem.feature.swap.models.states.SwapNotificationUM -import io.mockk.every -import io.mockk.mockk -import org.junit.jupiter.api.Test -import java.math.BigDecimal - -/** - * Characterization tests for fee-related notifications produced by [SwapNotificationsFactory]. - * - * Pinned behavior: - * - [SwapNotificationUM.Error.UnableToCoverFeeWarning]: - * adds when `feeState = NotEnough` AND `isBalanceEnough = true` AND - * `permissionState != PermissionLoading` AND fee currency != fromCurrency, - * AND NOT (gasless network AND CEX provider). - * Suppressed for CEX-on-gasless-network. Re-added unconditionally when - * `includeFeeInAmount is BalanceNotEnough`. - * - [NotificationUM.Warning.FeeCoverageNotification]: triggers on - * `includeFeeInAmount is Included` AND a fee is selected AND no existential deposit. - * - [SwapNotificationUM.Info.PermissionNeeded]: triggers on `permissionState is PermissionRequired`. - * - [SwapNotificationUM.Error.TransactionInProgressWarning]: triggers on - * `hasOutgoingTransaction = true` (when `permissionState is not PermissionLoading`). - * - `hideFee = true` short-circuits `maybeAddUnableCoverFeeWarning` only. - * - * [REDACTED_TASK_KEY] — these guard the redesign that consolidates fee-state into `SwapFee` / - * `FeeBucket`. Phase 5 will rewrite the factory; these tests stay green throughout. - */ -internal class SwapNotificationsFactoryFeeWarningsTest { - - private val actions: UiActions = mockk(relaxed = true) - private val isGaslessFeeSupportedForNetwork: IsGaslessFeeSupportedForNetwork = mockk() - - private val sut: SwapNotificationsFactory by lazy { - SwapNotificationsFactory( - actions = actions, - isGaslessFeeSupportedForNetwork = isGaslessFeeSupportedForNetwork, - ) - } - - private val ethNetworkMock: Network = mockk(relaxed = true) { - every { name } returns "Ethereum" - every { currencySymbol } returns "ETH" - every { rawId } returns "ethereum" - } - private val fromCurrency: CryptoCurrency = mockk(relaxed = true) { - every { network } returns ethNetworkMock - every { symbol } returns "ETH" - every { decimals } returns 18 - every { name } returns "Ethereum" - } - private val differentFeeCurrency: CryptoCurrency = mockk(relaxed = true) { - every { network } returns ethNetworkMock - every { symbol } returns "USDC" - every { decimals } returns 6 - every { name } returns "USD Coin" - } - - // ---------- UnableToCoverFeeWarning ---------- - - @Test - fun `UnableToCoverFeeWarning is added when feeState NotEnough and balance enough and not gasless and fee currency differs`() { - // Given - every { isGaslessFeeSupportedForNetwork(any()) } returns false - val quoteModel = buildQuoteModel( - feeState = SwapFeeState.NotEnough(currencyName = "Ethereum", currencySymbol = "ETH"), - isBalanceEnough = true, - permissionState = PermissionDataState.Empty, - includeFeeInAmount = IncludeFeeInAmount.Excluded, - providerType = ExchangeProviderType.DEX, - ) - val feeStatus = buildFeeStatus(differentFeeCurrency) - - // When - val notifications = sut.getConfirmationStateNotifications( - quoteModel = quoteModel, - feeCryptoCurrencyStatus = feeStatus, - selectedFeeType = FeeType.NORMAL, - hideFee = false, - ) - - // Then - assertThat(notifications.any { it is SwapNotificationUM.Error.UnableToCoverFeeWarning }).isTrue() - } - - @Test - fun `UnableToCoverFeeWarning is suppressed when gasless is available for CEX provider`() { - // Given — CEX + supported network → suppressed - every { isGaslessFeeSupportedForNetwork(any()) } returns true - val quoteModel = buildQuoteModel( - feeState = SwapFeeState.NotEnough(currencyName = "Ethereum", currencySymbol = "ETH"), - isBalanceEnough = true, - permissionState = PermissionDataState.Empty, - includeFeeInAmount = IncludeFeeInAmount.Excluded, - providerType = ExchangeProviderType.CEX, - ) - val feeStatus = buildFeeStatus(differentFeeCurrency) - - // When - val notifications = sut.getConfirmationStateNotifications( - quoteModel = quoteModel, - feeCryptoCurrencyStatus = feeStatus, - selectedFeeType = FeeType.NORMAL, - hideFee = false, - ) - - // Then - assertThat(notifications.any { it is SwapNotificationUM.Error.UnableToCoverFeeWarning }).isFalse() - } - - @Test - fun `UnableToCoverFeeWarning is added even when gasless is available IF includeFeeInAmount is BalanceNotEnough`() { - // Given — CEX + supported network BUT BalanceNotEnough overrides the suppression. - every { isGaslessFeeSupportedForNetwork(any()) } returns true - val quoteModel = buildQuoteModel( - feeState = SwapFeeState.NotEnough(currencyName = "Ethereum", currencySymbol = "ETH"), - isBalanceEnough = true, - permissionState = PermissionDataState.Empty, - includeFeeInAmount = IncludeFeeInAmount.BalanceNotEnough, - providerType = ExchangeProviderType.CEX, - ) - val feeStatus = buildFeeStatus(differentFeeCurrency) - - // When - val notifications = sut.getConfirmationStateNotifications( - quoteModel = quoteModel, - feeCryptoCurrencyStatus = feeStatus, - selectedFeeType = FeeType.NORMAL, - hideFee = false, - ) - - // Then - assertThat(notifications.any { it is SwapNotificationUM.Error.UnableToCoverFeeWarning }).isTrue() - } - - @Test - fun `UnableToCoverFeeWarning is not added when hideFee true`() { - // Given - every { isGaslessFeeSupportedForNetwork(any()) } returns false - val quoteModel = buildQuoteModel( - feeState = SwapFeeState.NotEnough(currencyName = "Ethereum", currencySymbol = "ETH"), - isBalanceEnough = true, - permissionState = PermissionDataState.Empty, - includeFeeInAmount = IncludeFeeInAmount.Excluded, - providerType = ExchangeProviderType.DEX, - ) - val feeStatus = buildFeeStatus(differentFeeCurrency) - - // When - val notifications = sut.getConfirmationStateNotifications( - quoteModel = quoteModel, - feeCryptoCurrencyStatus = feeStatus, - selectedFeeType = FeeType.NORMAL, - hideFee = true, - ) - - // Then - assertThat(notifications.any { it is SwapNotificationUM.Error.UnableToCoverFeeWarning }).isFalse() - } - - @Test - fun `UnableToCoverFeeWarning is not added when feeState is Enough`() { - // Given - every { isGaslessFeeSupportedForNetwork(any()) } returns false - val quoteModel = buildQuoteModel( - feeState = SwapFeeState.Enough, - isBalanceEnough = true, - permissionState = PermissionDataState.Empty, - includeFeeInAmount = IncludeFeeInAmount.Excluded, - providerType = ExchangeProviderType.DEX, - ) - val feeStatus = buildFeeStatus(differentFeeCurrency) - - // When - val notifications = sut.getConfirmationStateNotifications( - quoteModel = quoteModel, - feeCryptoCurrencyStatus = feeStatus, - selectedFeeType = FeeType.NORMAL, - hideFee = false, - ) - - // Then - assertThat(notifications.any { it is SwapNotificationUM.Error.UnableToCoverFeeWarning }).isFalse() - } - - // ---------- FeeCoverageNotification ---------- - - @Test - fun `FeeCoverageNotification is added when includeFeeInAmount is Included with a selected fee`() { - // Given - every { isGaslessFeeSupportedForNetwork(any()) } returns false - val singleFee = buildLegacyFee(FeeType.NORMAL) - val quoteModel = buildQuoteModel( - feeState = SwapFeeState.Enough, - isBalanceEnough = true, - permissionState = PermissionDataState.Empty, - includeFeeInAmount = IncludeFeeInAmount.Included(SwapAmount(BigDecimal("0.99"), 18)), - providerType = ExchangeProviderType.CEX, - txFeeState = TxFeeState.SingleFeeState(singleFee), - ) - val feeStatus = buildFeeStatus(fromCurrency) - - // When - val notifications = sut.getConfirmationStateNotifications( - quoteModel = quoteModel, - feeCryptoCurrencyStatus = feeStatus, - selectedFeeType = FeeType.NORMAL, - hideFee = false, - ) - - // Then - assertThat(notifications.any { it is NotificationUM.Warning.FeeCoverageNotification }).isTrue() - } - - @Test - fun `FeeCoverageNotification is not added when includeFeeInAmount is Excluded`() { - // Given - every { isGaslessFeeSupportedForNetwork(any()) } returns false - val singleFee = buildLegacyFee(FeeType.NORMAL) - val quoteModel = buildQuoteModel( - feeState = SwapFeeState.Enough, - isBalanceEnough = true, - permissionState = PermissionDataState.Empty, - includeFeeInAmount = IncludeFeeInAmount.Excluded, - providerType = ExchangeProviderType.CEX, - txFeeState = TxFeeState.SingleFeeState(singleFee), - ) - val feeStatus = buildFeeStatus(fromCurrency) - - // When - val notifications = sut.getConfirmationStateNotifications( - quoteModel = quoteModel, - feeCryptoCurrencyStatus = feeStatus, - selectedFeeType = FeeType.NORMAL, - hideFee = false, - ) - - // Then - assertThat(notifications.any { it is NotificationUM.Warning.FeeCoverageNotification }).isFalse() - } - - // ---------- PermissionNeeded ---------- - - @Test - fun `PermissionNeeded is added when permissionState is PermissionRequired`() { - // Given - every { isGaslessFeeSupportedForNetwork(any()) } returns false - val quoteModel = buildQuoteModel( - feeState = SwapFeeState.Enough, - isBalanceEnough = true, - permissionState = PermissionDataState.PermissionRequired( - isResetApproval = false, - spenderAddress = "0xSpender", - ), - includeFeeInAmount = IncludeFeeInAmount.Excluded, - providerType = ExchangeProviderType.DEX, - ) - val feeStatus = buildFeeStatus(fromCurrency) - - // When - val notifications = sut.getConfirmationStateNotifications( - quoteModel = quoteModel, - feeCryptoCurrencyStatus = feeStatus, - selectedFeeType = FeeType.NORMAL, - hideFee = false, - ) - - // Then - assertThat(notifications.any { it is SwapNotificationUM.Info.PermissionNeeded }).isTrue() - } - - @Test - fun `PermissionNeeded is not added when permissionState is Empty`() { - // Given - every { isGaslessFeeSupportedForNetwork(any()) } returns false - val quoteModel = buildQuoteModel( - feeState = SwapFeeState.Enough, - isBalanceEnough = true, - permissionState = PermissionDataState.Empty, - includeFeeInAmount = IncludeFeeInAmount.Excluded, - providerType = ExchangeProviderType.DEX, - ) - val feeStatus = buildFeeStatus(fromCurrency) - - // When - val notifications = sut.getConfirmationStateNotifications( - quoteModel = quoteModel, - feeCryptoCurrencyStatus = feeStatus, - selectedFeeType = FeeType.NORMAL, - hideFee = false, - ) - - // Then - assertThat(notifications.any { it is SwapNotificationUM.Info.PermissionNeeded }).isFalse() - } - - // ---------- TransactionInProgressWarning / ApprovalInProgressWarning ---------- - - @Test - fun `ApprovalInProgressWarning is added when permissionState is PermissionLoading`() { - // Given — PermissionLoading short-circuits to ApprovalInProgressWarning (an Error subtype) - every { isGaslessFeeSupportedForNetwork(any()) } returns false - val quoteModel = buildQuoteModel( - feeState = SwapFeeState.Enough, - isBalanceEnough = true, - permissionState = PermissionDataState.PermissionLoading, - includeFeeInAmount = IncludeFeeInAmount.Excluded, - providerType = ExchangeProviderType.DEX, - hasOutgoingTransaction = false, - ) - val feeStatus = buildFeeStatus(fromCurrency) - - // When - val notifications = sut.getConfirmationStateNotifications( - quoteModel = quoteModel, - feeCryptoCurrencyStatus = feeStatus, - selectedFeeType = FeeType.NORMAL, - hideFee = false, - ) - - // Then - assertThat(notifications.any { it is SwapNotificationUM.Error.ApprovalInProgressWarning }).isTrue() - } - - @Test - fun `TransactionInProgressWarning is added when hasOutgoingTransaction true and permission not loading`() { - // Given - every { isGaslessFeeSupportedForNetwork(any()) } returns false - val quoteModel = buildQuoteModel( - feeState = SwapFeeState.Enough, - isBalanceEnough = true, - permissionState = PermissionDataState.Empty, - includeFeeInAmount = IncludeFeeInAmount.Excluded, - providerType = ExchangeProviderType.DEX, - hasOutgoingTransaction = true, - ) - val feeStatus = buildFeeStatus(fromCurrency) - - // When - val notifications = sut.getConfirmationStateNotifications( - quoteModel = quoteModel, - feeCryptoCurrencyStatus = feeStatus, - selectedFeeType = FeeType.NORMAL, - hideFee = false, - ) - - // Then - assertThat(notifications.any { it is SwapNotificationUM.Error.TransactionInProgressWarning }).isTrue() - } - - // region — local helpers - - @Suppress("LongParameterList") - private fun buildQuoteModel( - feeState: SwapFeeState = SwapFeeState.Enough, - isBalanceEnough: Boolean = true, - permissionState: PermissionDataState = PermissionDataState.Empty, - includeFeeInAmount: IncludeFeeInAmount = IncludeFeeInAmount.Excluded, - providerType: ExchangeProviderType = ExchangeProviderType.DEX, - txFeeState: TxFeeState = TxFeeState.Empty, - hasOutgoingTransaction: Boolean = false, - ): SwapState.QuotesLoadedState { - val fromStatus = buildSwapCurrencyStatusForFromCurrency() - val toStatus = buildSwapCurrencyStatusForFromCurrency() - val fromInfo = TokenSwapInfo( - tokenAmount = SwapAmount(BigDecimal("1.0"), 18), - amountFiat = BigDecimal("100.00"), - swapCurrencyStatus = fromStatus, - ) - val toInfo = TokenSwapInfo( - tokenAmount = SwapAmount(BigDecimal("0.05"), 18), - amountFiat = BigDecimal("100.00"), - swapCurrencyStatus = toStatus, - ) - return SwapState.QuotesLoadedState( - fromTokenInfo = fromInfo, - toTokenInfo = toInfo, - priceImpact = PriceImpact.Empty, - preparedSwapConfigState = PreparedSwapConfigState( - isBalanceEnough = isBalanceEnough, - feeState = feeState, - hasOutgoingTransaction = hasOutgoingTransaction, - includeFeeInAmount = includeFeeInAmount, - ), - permissionState = permissionState, - txFee = txFeeState, - currencyCheck = null, - validationResult = null, - minAdaValue = null, - swapProvider = SwapProvider( - providerId = "p", - rateTypes = listOf(RateType.FLOAT), - name = "TestProvider", - type = providerType, - imageLarge = "", - termsOfUse = null, - privacyPolicy = null, - isRecommended = false, - slippage = null, - isExtraIdSupported = false, - ), - ) - } - - private fun buildSwapCurrencyStatusForFromCurrency(): SwapCurrencyStatus { - val statusValue: CryptoCurrencyStatus.Value = mockk(relaxed = true) { - every { amount } returns BigDecimal("1.0") - every { fiatRate } returns BigDecimal("2000.00") - every { fiatAmount } returns BigDecimal("2000.00") - every { networkAddress } returns mockk(relaxed = true) - every { pendingTransactions } returns emptySet() - } - val cryptoCurrencyStatus = CryptoCurrencyStatus(currency = fromCurrency, value = statusValue) - val userWallet: UserWallet = mockk(relaxed = true) { - every { walletId } returns UserWalletId("aabbccdd") - } - val account = com.tangem.domain.models.account.Account.CryptoPortfolio.createMainAccount(userWallet.walletId) - return SwapCurrencyStatus( - userWallet = userWallet, - status = cryptoCurrencyStatus, - account = account, - ) - } - - private fun buildFeeStatus(currency: CryptoCurrency): CryptoCurrencyStatus { - val statusValue: CryptoCurrencyStatus.Value = mockk(relaxed = true) { - every { amount } returns BigDecimal("0.5") - } - return CryptoCurrencyStatus(currency = currency, value = statusValue) - } - - private fun buildLegacyFee(feeType: FeeType): TxFee.Legacy { - val fee: Fee = mockk(relaxed = true) - return TxFee.Legacy( - feeValue = BigDecimal("0.001"), - feeFiatFormatted = "$2.00", - feeCryptoFormatted = "0.001 ETH", - feeIncludeOtherNativeFee = BigDecimal.ZERO, - feeFiatFormattedWithNative = "$2.00", - feeCryptoFormattedWithNative = "0.001 ETH", - cryptoSymbol = "ETH", - feeType = feeType, - fee = fee, - ) - } - - // endregion -} \ No newline at end of file