Updated on 2026-08-14
This commit is contained in:
parent
c3740a70c0
commit
312f241f7c
4 changed files with 177 additions and 2 deletions
|
|
@ -476,6 +476,16 @@ internal class DefaultWalletManagersFacade @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun isSwapSpenderAllowed(
|
||||||
|
userWalletId: UserWalletId,
|
||||||
|
network: Network,
|
||||||
|
spenderAddress: String,
|
||||||
|
): Boolean = withContext(dispatchers.io) {
|
||||||
|
val walletManager = getOrCreateWalletManager(userWalletId = userWalletId, network = network)
|
||||||
|
?: return@withContext false
|
||||||
|
walletManager.isSwapSpenderAllowed(spenderAddress)
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun getDynamicAddressesReceiveAddress(userWalletId: UserWalletId, network: Network): String? {
|
override suspend fun getDynamicAddressesReceiveAddress(userWalletId: UserWalletId, network: Network): String? {
|
||||||
val dynamicAddressesManager = getEnabledDynamicAddressesManagerOrNull(userWalletId, network) ?: return null
|
val dynamicAddressesManager = getEnabledDynamicAddressesManagerOrNull(userWalletId, network) ?: return null
|
||||||
return dynamicAddressesManager.findFirstUnusedReceiveAddress()?.address
|
return dynamicAddressesManager.findFirstUnusedReceiveAddress()?.address
|
||||||
|
|
|
||||||
|
|
@ -246,6 +246,8 @@ interface WalletManagersFacade {
|
||||||
*/
|
*/
|
||||||
suspend fun getPsbtFee(userWalletId: UserWalletId, network: Network, psbtBase64: String): BigDecimal?
|
suspend fun getPsbtFee(userWalletId: UserWalletId, network: Network, psbtBase64: String): BigDecimal?
|
||||||
|
|
||||||
|
suspend fun isSwapSpenderAllowed(userWalletId: UserWalletId, network: Network, spenderAddress: String): Boolean
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get requirements for asset(currency)
|
* Get requirements for asset(currency)
|
||||||
* @return null if there's no requirement, otherwise [AssetRequirementsCondition].
|
* @return null if there's no requirement, otherwise [AssetRequirementsCondition].
|
||||||
|
|
|
||||||
|
|
@ -131,11 +131,29 @@ internal class SwapInteractorImpl @Inject constructor(
|
||||||
ConcurrentHashMap<IntegratedApprovalFallbackKey, Boolean>(),
|
ConcurrentHashMap<IntegratedApprovalFallbackKey, Boolean>(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private val yieldSwapAllowedRouters = newSetFromMap(ConcurrentHashMap<String, Boolean>())
|
||||||
|
|
||||||
private fun hasIntegratedApprovalFallenBack(fromSwapCurrencyStatus: SwapCurrencyStatus, spenderAddress: String?) =
|
private fun hasIntegratedApprovalFallenBack(fromSwapCurrencyStatus: SwapCurrencyStatus, spenderAddress: String?) =
|
||||||
integratedApprovalFallbackContexts.contains(
|
integratedApprovalFallbackContexts.contains(
|
||||||
IntegratedApprovalFallbackKey.of(fromSwapCurrencyStatus, spenderAddress),
|
IntegratedApprovalFallbackKey.of(fromSwapCurrencyStatus, spenderAddress),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private suspend fun isYieldSwapRouterAllowed(
|
||||||
|
fromSwapCurrencyStatus: SwapCurrencyStatus,
|
||||||
|
routerAddress: String,
|
||||||
|
): Boolean {
|
||||||
|
val network = fromSwapCurrencyStatus.currency.network
|
||||||
|
val key = "${network.rawId}:${routerAddress.lowercase()}"
|
||||||
|
if (yieldSwapAllowedRouters.contains(key)) return true
|
||||||
|
val isAllowed = walletManagersFacade.isSwapSpenderAllowed(
|
||||||
|
userWalletId = fromSwapCurrencyStatus.userWalletId,
|
||||||
|
network = network,
|
||||||
|
spenderAddress = routerAddress,
|
||||||
|
)
|
||||||
|
if (isAllowed) yieldSwapAllowedRouters.add(key)
|
||||||
|
return isAllowed
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun getPair(
|
override suspend fun getPair(
|
||||||
fromSwapCurrencyStatus: SwapCurrencyStatus,
|
fromSwapCurrencyStatus: SwapCurrencyStatus,
|
||||||
toSwapCurrencyStatus: SwapCurrencyStatus,
|
toSwapCurrencyStatus: SwapCurrencyStatus,
|
||||||
|
|
@ -304,7 +322,7 @@ internal class SwapInteractorImpl @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.awaitAll().toMap()
|
}.awaitAll().filterNotNull().toMap()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -316,7 +334,7 @@ internal class SwapInteractorImpl @Inject constructor(
|
||||||
amount: SwapAmount,
|
amount: SwapAmount,
|
||||||
reduceBalanceBy: BigDecimal,
|
reduceBalanceBy: BigDecimal,
|
||||||
expressOperationType: ExpressOperationType,
|
expressOperationType: ExpressOperationType,
|
||||||
): Pair<SwapProvider, SwapState> {
|
): Pair<SwapProvider, SwapState>? {
|
||||||
if (fromSwapCurrencyStatus.status.value.yieldSupplyStatus?.isActive == true &&
|
if (fromSwapCurrencyStatus.status.value.yieldSupplyStatus?.isActive == true &&
|
||||||
!swapFeatureToggles.isYieldSwapEnabled
|
!swapFeatureToggles.isYieldSwapEnabled
|
||||||
) {
|
) {
|
||||||
|
|
@ -366,6 +384,12 @@ internal class SwapInteractorImpl @Inject constructor(
|
||||||
|
|
||||||
val dexRouterSpenderAddress = maybeQuote.getOrNull()?.allowanceContract
|
val dexRouterSpenderAddress = maybeQuote.getOrNull()?.allowanceContract
|
||||||
|
|
||||||
|
if (isYieldSwap && dexRouterSpenderAddress != null &&
|
||||||
|
!isYieldSwapRouterAllowed(fromSwapCurrencyStatus, dexRouterSpenderAddress)
|
||||||
|
) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
val allowanceInfo = spenderAddress?.let { allowanceContract ->
|
val allowanceInfo = spenderAddress?.let { allowanceContract ->
|
||||||
getAllowanceInfoUseCase(
|
getAllowanceInfoUseCase(
|
||||||
userWalletId = fromSwapCurrencyStatus.userWalletId,
|
userWalletId = fromSwapCurrencyStatus.userWalletId,
|
||||||
|
|
|
||||||
|
|
@ -888,6 +888,7 @@ internal class SwapInteractorImplFindBestQuoteTest : SwapInteractorImplTestBase(
|
||||||
coEvery {
|
coEvery {
|
||||||
yieldModuleAddressProvider.getOrFetch(any(), any())
|
yieldModuleAddressProvider.getOrFetch(any(), any())
|
||||||
} returns yieldProxyAddress
|
} returns yieldProxyAddress
|
||||||
|
coEvery { walletManagersFacade.isSwapSpenderAllowed(any(), any(), any()) } returns true
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -1131,6 +1132,144 @@ internal class SwapInteractorImplFindBestQuoteTest : SwapInteractorImplTestBase(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||||
|
inner class YieldSwapRouterAllowlist {
|
||||||
|
|
||||||
|
private val yieldProxyAddress = "0xYieldModuleProxy"
|
||||||
|
private val yieldTokenContract = "0xTokenContract"
|
||||||
|
private val notAllowedRouter = "0xMoonPayRouter"
|
||||||
|
private val allowedRouter = "0xOneInchRouter"
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
fun enableYieldSwap() {
|
||||||
|
every { swapFeatureToggles.isYieldSwapEnabled } returns true
|
||||||
|
coEvery { yieldModuleAddressProvider.getOrFetch(any(), any()) } returns yieldProxyAddress
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun yieldTokenStatus(yieldActive: Boolean = true) = buildSwapCurrencyStatus(
|
||||||
|
networkRawId = ethNetwork,
|
||||||
|
contractAddress = yieldTokenContract,
|
||||||
|
isCoin = false,
|
||||||
|
amount = BigDecimal("10"),
|
||||||
|
yieldSupplyActive = yieldActive,
|
||||||
|
yieldSupplyAllowedToSpend = true,
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun stubDexQuote(providerId: String, router: String) {
|
||||||
|
coEvery {
|
||||||
|
repository.findBestQuote(
|
||||||
|
userWallet = any(), fromContractAddress = any(), fromNetwork = any(),
|
||||||
|
toContractAddress = any(), toNetwork = any(), fromAmount = any(),
|
||||||
|
fromDecimals = any(), toDecimals = any(),
|
||||||
|
providerId = providerId, rateType = any(),
|
||||||
|
)
|
||||||
|
} returns buildQuoteModel(allowanceContract = router).right()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun stubExchangeData(providerId: String) {
|
||||||
|
coEvery {
|
||||||
|
repository.getExchangeData(
|
||||||
|
userWallet = any(), fromContractAddress = any(), fromNetwork = any(),
|
||||||
|
toContractAddress = any(), fromAddress = any(), toNetwork = any(),
|
||||||
|
fromAmount = any(), fromDecimals = any(), toDecimals = any(),
|
||||||
|
providerId = providerId, rateType = any(), toAddress = any(),
|
||||||
|
expressOperationType = any(), refundAddress = any(),
|
||||||
|
)
|
||||||
|
} returns buildSwapDataModelDex().right()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `should hide yield-swap DEX provider whose router is not allowed by the registry`() = runTest {
|
||||||
|
// Given — yield active, router NOT in the SwapExecutionRegistry (MoonPay/swaps.xyz)
|
||||||
|
val dexProvider = buildSwapProvider(ExchangeProviderType.DEX)
|
||||||
|
stubDexQuote(dexProvider.providerId, notAllowedRouter)
|
||||||
|
coEvery { walletManagersFacade.isSwapSpenderAllowed(any(), any(), notAllowedRouter) } returns false
|
||||||
|
|
||||||
|
// When
|
||||||
|
val result = sut.findBestQuote(
|
||||||
|
fromSwapCurrencyStatus = yieldTokenStatus(),
|
||||||
|
toSwapCurrencyStatus = buildSwapCurrencyStatus(networkRawId = btcNetwork),
|
||||||
|
providers = listOf(dexProvider),
|
||||||
|
amountToSwap = "1.0",
|
||||||
|
reduceBalanceBy = BigDecimal.ZERO,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Then — provider is absent from the list
|
||||||
|
assertThat(result.containsKey(dexProvider)).isFalse()
|
||||||
|
assertThat(result).isEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `should keep yield-swap DEX provider whose router is allowed by the registry`() = runTest {
|
||||||
|
// Given — yield active, router whitelisted (1inch)
|
||||||
|
val dexProvider = buildSwapProvider(ExchangeProviderType.DEX)
|
||||||
|
stubDexQuote(dexProvider.providerId, allowedRouter)
|
||||||
|
stubExchangeData(dexProvider.providerId)
|
||||||
|
coEvery { walletManagersFacade.isSwapSpenderAllowed(any(), any(), allowedRouter) } returns true
|
||||||
|
|
||||||
|
// When
|
||||||
|
val result = sut.findBestQuote(
|
||||||
|
fromSwapCurrencyStatus = yieldTokenStatus(),
|
||||||
|
toSwapCurrencyStatus = buildSwapCurrencyStatus(networkRawId = btcNetwork),
|
||||||
|
providers = listOf(dexProvider),
|
||||||
|
amountToSwap = "1.0",
|
||||||
|
reduceBalanceBy = BigDecimal.ZERO,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertThat(result.containsKey(dexProvider)).isTrue()
|
||||||
|
assertThat(result[dexProvider]).isNotNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `should hide only the not-allowed router and keep the allowed one for yield swaps`() = runTest {
|
||||||
|
// Given — two DEX providers, only one router whitelisted
|
||||||
|
val allowedProvider = buildSwapProvider(ExchangeProviderType.DEX, "dex-allowed")
|
||||||
|
val blockedProvider = buildSwapProvider(ExchangeProviderType.DEX, "dex-blocked")
|
||||||
|
stubDexQuote(allowedProvider.providerId, allowedRouter)
|
||||||
|
stubDexQuote(blockedProvider.providerId, notAllowedRouter)
|
||||||
|
stubExchangeData(allowedProvider.providerId)
|
||||||
|
coEvery { walletManagersFacade.isSwapSpenderAllowed(any(), any(), allowedRouter) } returns true
|
||||||
|
coEvery { walletManagersFacade.isSwapSpenderAllowed(any(), any(), notAllowedRouter) } returns false
|
||||||
|
|
||||||
|
// When
|
||||||
|
val result = sut.findBestQuote(
|
||||||
|
fromSwapCurrencyStatus = yieldTokenStatus(),
|
||||||
|
toSwapCurrencyStatus = buildSwapCurrencyStatus(networkRawId = btcNetwork),
|
||||||
|
providers = listOf(allowedProvider, blockedProvider),
|
||||||
|
amountToSwap = "1.0",
|
||||||
|
reduceBalanceBy = BigDecimal.ZERO,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertThat(result.containsKey(allowedProvider)).isTrue()
|
||||||
|
assertThat(result.containsKey(blockedProvider)).isFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `should not apply the registry filter to regular non-yield swaps`() = runTest {
|
||||||
|
// Given — yield NOT active; the registry verdict must be irrelevant for plain DEX swaps
|
||||||
|
val dexProvider = buildSwapProvider(ExchangeProviderType.DEX)
|
||||||
|
stubDexQuote(dexProvider.providerId, notAllowedRouter)
|
||||||
|
stubExchangeData(dexProvider.providerId)
|
||||||
|
coEvery { walletManagersFacade.isSwapSpenderAllowed(any(), any(), any()) } returns false
|
||||||
|
|
||||||
|
// When
|
||||||
|
val result = sut.findBestQuote(
|
||||||
|
fromSwapCurrencyStatus = yieldTokenStatus(yieldActive = false),
|
||||||
|
toSwapCurrencyStatus = buildSwapCurrencyStatus(networkRawId = btcNetwork),
|
||||||
|
providers = listOf(dexProvider),
|
||||||
|
amountToSwap = "1.0",
|
||||||
|
reduceBalanceBy = BigDecimal.ZERO,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Then — provider stays; the on-chain allowlist does not gate non-yield swaps
|
||||||
|
assertThat(result.containsKey(dexProvider)).isTrue()
|
||||||
|
coVerify(exactly = 0) { walletManagersFacade.isSwapSpenderAllowed(any(), any(), any()) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Regular (non-yield) DEX swap with the integrated-approve toggle ON: the
|
* Regular (non-yield) DEX swap with the integrated-approve toggle ON: the
|
||||||
* `isAllowanceSatisfied` matrix in `manageDex`.
|
* `isAllowanceSatisfied` matrix in `manageDex`.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue