From 9b0305efe960d3256834108014337054f91c3317 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 3 Jun 2026 16:24:00 +0400 Subject: [PATCH] Updated on 2026-08-14 --- .../feature/swap/domain/SwapInteractorImpl.kt | 10 ++- .../SwapInteractorImplFindBestQuoteTest.kt | 74 ++++++++++++++++--- 2 files changed, 69 insertions(+), 15 deletions(-) diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index ae014fda50..a85cd3bf4d 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -297,7 +297,6 @@ internal class SwapInteractorImpl @Inject constructor( val fromTokenAddress = getTokenAddress(fromSwapCurrencyStatus.currency) - // TODO CHECK YIELD APPROVE val isYieldSwap = fromSwapCurrencyStatus.isYieldSwapActive && fromSwapCurrencyStatus.currency is CryptoCurrency.Token @@ -310,6 +309,8 @@ internal class SwapInteractorImpl @Inject constructor( maybeQuote.getOrNull()?.allowanceContract } + val dexRouterSpenderAddress = maybeQuote.getOrNull()?.allowanceContract + val allowanceInfo = spenderAddress?.let { allowanceContract -> getAllowanceInfoUseCase( userWalletId = fromSwapCurrencyStatus.userWalletId, @@ -353,6 +354,7 @@ internal class SwapInteractorImpl @Inject constructor( expressOperationType = expressOperationType, allowanceInfo = allowanceInfo, spenderAddress = spenderAddress, + dexRouterSpenderAddress = dexRouterSpenderAddress, ) } else { val quoteBalanceStatus = if (isBalanceWithoutFeeEnough) { @@ -417,6 +419,7 @@ internal class SwapInteractorImpl @Inject constructor( expressOperationType = expressOperationType, allowanceInfo = null, spenderAddress = null, + dexRouterSpenderAddress = null, ) } else { provider to getQuotesState( @@ -1621,6 +1624,7 @@ internal class SwapInteractorImpl @Inject constructor( expressOperationType: ExpressOperationType, allowanceInfo: AllowanceInfo?, spenderAddress: String?, + dexRouterSpenderAddress: String?, ): SwapState { val fromNetworkAddress = fromSwapCurrencyStatus.status.value.networkAddress val dexFromAddress = fromNetworkAddress?.defaultAddress?.value.orEmpty() @@ -1643,8 +1647,8 @@ internal class SwapInteractorImpl @Inject constructor( expressOperationType = expressOperationType, ).map { swapData -> val dexTx = swapData.transaction as? ExpressTransactionModel.DEX - if (dexTx != null && spenderAddress != null && dexTx.allowanceContract == null) { - swapData.copy(transaction = dexTx.copy(allowanceContract = spenderAddress)) + if (dexTx != null && dexRouterSpenderAddress != null && dexTx.allowanceContract == null) { + swapData.copy(transaction = dexTx.copy(allowanceContract = dexRouterSpenderAddress)) } else { swapData } diff --git a/features/swap/domain/src/test/kotlin/com/tangem/feature/swap/domain/SwapInteractorImplFindBestQuoteTest.kt b/features/swap/domain/src/test/kotlin/com/tangem/feature/swap/domain/SwapInteractorImplFindBestQuoteTest.kt index bd60f7cfb8..4d3d80a5b4 100644 --- a/features/swap/domain/src/test/kotlin/com/tangem/feature/swap/domain/SwapInteractorImplFindBestQuoteTest.kt +++ b/features/swap/domain/src/test/kotlin/com/tangem/feature/swap/domain/SwapInteractorImplFindBestQuoteTest.kt @@ -130,7 +130,7 @@ internal class SwapInteractorImplFindBestQuoteTest : SwapInteractorImplTestBase( amountToSwap = "0", reduceBalanceBy = BigDecimal.ZERO, - ) + ) // Then assertThat(result).hasSize(2) @@ -153,7 +153,7 @@ internal class SwapInteractorImplFindBestQuoteTest : SwapInteractorImplTestBase( amountToSwap = "not-a-number", reduceBalanceBy = BigDecimal.ZERO, - ) + ) // Then assertThat(result).hasSize(1) @@ -174,7 +174,7 @@ internal class SwapInteractorImplFindBestQuoteTest : SwapInteractorImplTestBase( amountToSwap = "1.0", reduceBalanceBy = BigDecimal.ZERO, - ) + ) // Then assertThat(result).isEmpty() @@ -268,7 +268,7 @@ internal class SwapInteractorImplFindBestQuoteTest : SwapInteractorImplTestBase( amountToSwap = "1.0", reduceBalanceBy = BigDecimal.ZERO, - ) + ) // Then assertThat(result).hasSize(1) @@ -315,7 +315,7 @@ internal class SwapInteractorImplFindBestQuoteTest : SwapInteractorImplTestBase( amountToSwap = "1.0", reduceBalanceBy = BigDecimal.ZERO, - ) + ) // Then assertThat(result).hasSize(1) @@ -431,7 +431,7 @@ internal class SwapInteractorImplFindBestQuoteTest : SwapInteractorImplTestBase( amountToSwap = "1.0", reduceBalanceBy = BigDecimal.ZERO, - ) + ) // Then assertThat(result).hasSize(1) @@ -500,7 +500,7 @@ internal class SwapInteractorImplFindBestQuoteTest : SwapInteractorImplTestBase( amountToSwap = "1.0", reduceBalanceBy = BigDecimal.ZERO, - ) + ) // Then assertThat(result).hasSize(1) @@ -622,7 +622,7 @@ internal class SwapInteractorImplFindBestQuoteTest : SwapInteractorImplTestBase( amountToSwap = "1000.0", reduceBalanceBy = BigDecimal.ZERO, - ) + ) // Then assertThat(result).hasSize(1) @@ -668,7 +668,7 @@ internal class SwapInteractorImplFindBestQuoteTest : SwapInteractorImplTestBase( amountToSwap = "1.0", reduceBalanceBy = BigDecimal.ZERO, - ) + ) // Then assertThat(result).hasSize(1) @@ -711,7 +711,7 @@ internal class SwapInteractorImplFindBestQuoteTest : SwapInteractorImplTestBase( amountToSwap = "1.0", reduceBalanceBy = BigDecimal.ZERO, - ) + ) // Then assertThat(result).hasSize(1) @@ -793,7 +793,7 @@ internal class SwapInteractorImplFindBestQuoteTest : SwapInteractorImplTestBase( amountToSwap = "1.0", reduceBalanceBy = BigDecimal.ZERO, - ) + ) // Then — both providers have an entry assertThat(result).hasSize(2) @@ -862,7 +862,7 @@ internal class SwapInteractorImplFindBestQuoteTest : SwapInteractorImplTestBase( amountToSwap = "1.0", reduceBalanceBy = BigDecimal.ZERO, - ) + ) // Then — all three providers are dispatched and each has an entry assertThat(result).hasSize(3) @@ -939,6 +939,56 @@ internal class SwapInteractorImplFindBestQuoteTest : SwapInteractorImplTestBase( assertThat(loaded.permissionState).isEqualTo(PermissionDataState.Empty) } + @Test + fun `yield swap on-chain spender is DEX router from quote, not yield-module proxy`() = runTest { + val dexRouter = "0xDexRouterFromQuote" + val dexProvider = buildSwapProvider(ExchangeProviderType.DEX) + val fromStatus = buildSwapCurrencyStatus( + networkRawId = ethNetwork, + contractAddress = yieldTokenContract, + isCoin = false, + amount = BigDecimal("10"), + yieldSupplyActive = true, + yieldSupplyAllowedToSpend = true, + ) + val toStatus = buildSwapCurrencyStatus(networkRawId = btcNetwork) + val quoteModel = buildQuoteModel(allowanceContract = dexRouter) + val swapData = buildSwapDataModelDex() // transaction.allowanceContract == null (OKX) + + 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 — swap data spender is the DEX router from the quote, NOT the yield-module proxy + val loaded = result[dexProvider] as SwapState.QuotesLoadedState + val dexTx = loaded.swapDataModel?.transaction as ExpressTransactionModel.DEX + assertThat(dexTx.allowanceContract).isEqualTo(dexRouter) + assertThat(dexTx.allowanceContract).isNotEqualTo(yieldProxyAddress) + } + @Test fun `should request approval to yield-module proxy when isAllowedToSpend is false`() = runTest { // Given — yield active, approve to proxy revoked → flow must surface PermissionRequired