Updated on 2026-08-14

This commit is contained in:
Tangem 2026-06-29 02:46:34 +04:00
parent 3405bea182
commit 257351398c
2 changed files with 88 additions and 5 deletions

View file

@ -835,14 +835,11 @@ internal class SwapInteractorImpl @Inject constructor(
val payInAddress = if (isYieldSwap && fromCurrency is CryptoCurrency.Token) {
swapData.transaction.txTo
} else if (txData is TransactionData.Uncompiled) {
getPayoutAddress(txData)
} else {
swapData.transaction.txTo
getPayoutAddress(txData)
}
return if (integratedApproval != null) {
// TODO YIELD payInAddress [REDACTED_TASK_KEY]
sendIntegratedApproveAndSwap(
fromSwapCurrencyStatus = fromSwapCurrencyStatus,
toSwapCurrencyStatus = toSwapCurrencyStatus,
@ -852,6 +849,7 @@ internal class SwapInteractorImpl @Inject constructor(
swapTxData = txData,
swapFee = swapFee,
integratedApproval = integratedApproval,
payInAddress = payInAddress,
)
} else {
handleSwapResult(
@ -884,6 +882,7 @@ internal class SwapInteractorImpl @Inject constructor(
swapTxData: TransactionData.Uncompiled,
swapFee: SwapFee,
integratedApproval: IntegratedApprovalData,
payInAddress: String,
): SwapTransactionState {
val approvalFee = selectFeeForBucket(integratedApproval.approvalFee, swapFee.feeBucket)
val approvalTx = integratedApproval.approvalTransaction.copy(fee = approvalFee)
@ -908,7 +907,7 @@ internal class SwapInteractorImpl @Inject constructor(
swapData = swapData,
amount = amount,
txHash = swapTxHash,
payInAddress = getPayoutAddress(swapTxData),
payInAddress = payInAddress,
)
}

View file

@ -168,6 +168,53 @@ internal class SwapInteractorImplOnSwapTest : SwapInteractorImplTestBase() {
assertThat(result).isInstanceOf(SwapTransactionState.Error.TransactionError::class.java)
}
@Test
fun `GIVEN yield integratedApproval WHEN onSwap THEN exchangeSent uses dex router txTo as payInAddress not yield proxy`() =
runTest {
// Arrange — yield-active token swap is routed through the yield module proxy: the swap tx is
// addressed to the proxy, but the Express status must be tracked by the original dex router (txTo).
// [REDACTED_TASK_KEY] / [REDACTED_TASK_KEY]: otherwise the "Supplying to Aave" status never resolves.
every { swapFeatureToggles.isYieldSwapEnabled } returns true
coEvery { yieldModuleAddressProvider.getOrFetch(any(), any()) } returns YIELD_PROXY
coEvery {
createTransactionExtrasUseCase(callData = any(), network = any(), gasLimit = any())
} returns mockk<TransactionExtras>(relaxed = true).right()
coEvery {
createTransactionUseCase(
amount = any(), fee = any(), memo = any(),
destination = any(), userWalletId = any(), network = any(), txExtras = any(),
)
} returns yieldSwapTxUncompiled().right()
coEvery {
sendTransactionUseCase(txsData = any(), userWallet = any(), network = any(), sendMode = any())
} returns listOf(APPROVAL_HASH, SWAP_HASH).right()
val payInSlot = slot<String>()
coEvery {
repository.exchangeSent(
userWallet = any(), txId = any(), fromNetwork = any(), fromAddress = any(),
payInAddress = capture(payInSlot), txHash = any(), payInExtraId = any(),
)
} returns Unit.right()
// Act
val result = sut.onSwap(
fromSwapCurrencyStatus = yieldTokenStatus(),
toSwapCurrencyStatus = hotStatus(),
swapProvider = buildSwapProvider(ExchangeProviderType.DEX),
swapData = yieldDexSwapData(),
amountToSwap = "1.0",
balanceStatus = SwapBalanceStatus.Sufficient,
fee = buildSwapFee(),
expressOperationType = ExpressOperationType.SWAP,
isTangemPayWithdrawal = false,
integratedApproval = integratedApproval(approvalFee = singleFee()),
)
// Assert — backend receives the original dex router address, not the yield module proxy.
assertThat(result).isInstanceOf(SwapTransactionState.TxSent::class.java)
assertThat(payInSlot.captured).isEqualTo(DEX_ROUTER)
}
@Test
fun `GIVEN Choosable approval fee AND SLOW bucket THEN approval tx fee is the minimum`() = runTest {
assertApprovalFeeBucket(
@ -270,6 +317,41 @@ internal class SwapInteractorImplOnSwapTest : SwapInteractorImplTestBase() {
destinationAddress = "0xTo",
)
/** Yield-swap tx is addressed to the yield module proxy, not to the dex router. */
private fun yieldSwapTxUncompiled(): TransactionData.Uncompiled = TransactionData.Uncompiled(
amount = realAmount(),
fee = NORMAL_FEE,
sourceAddress = "0xFrom",
destinationAddress = YIELD_PROXY,
)
private fun yieldTokenStatus(): SwapCurrencyStatus {
val hotWallet = mockk<UserWallet.Hot>(relaxed = true)
return buildSwapCurrencyStatus(
networkRawId = ethNetwork,
contractAddress = "0xTokenContract",
isCoin = false,
yieldSupplyActive = true,
).let { SwapCurrencyStatus(userWallet = hotWallet, status = it.status, account = it.account) }
}
private fun yieldDexSwapData(): SwapDataModel = SwapDataModel(
toTokenAmount = SwapAmount(BigDecimal("0.5"), 18),
transaction = ExpressTransactionModel.DEX(
fromAmount = SwapAmount(BigDecimal("1.0"), 18),
toAmount = SwapAmount(BigDecimal("0.5"), 18),
txValue = "0",
txId = "tx-id",
txTo = DEX_ROUTER,
txExtraId = null,
txFrom = "0xFrom",
txData = "0xdata",
otherNativeFeeWei = null,
gas = BigInteger.valueOf(21_000L),
allowanceContract = "0xSpender",
),
)
private fun realAmount(): Amount = Amount(
currencySymbol = "ETH",
value = BigDecimal.ONE,
@ -369,6 +451,8 @@ internal class SwapInteractorImplOnSwapTest : SwapInteractorImplTestBase() {
private companion object {
const val APPROVAL_HASH = "0xApprovalHash"
const val SWAP_HASH = "0xSwapHash"
const val DEX_ROUTER = "0xDexRouter"
const val YIELD_PROXY = "0xYieldProxy"
val MIN_FEE: Fee = feeOf(BigDecimal("0.001"))
val NORMAL_FEE: Fee = feeOf(BigDecimal("0.002"))