From 5cdf4c75493fee5b51e73b0feeb40dd39c8049f5 Mon Sep 17 00:00:00 2001 From: antoncoding Date: Wed, 22 Apr 2026 09:42:08 +0800 Subject: [PATCH 1/7] feat: add SELL quote from velora to support bigger quotes --- src/hooks/useLeverageQuote.ts | 198 ++++++++++++++++++++++++++++------ 1 file changed, 166 insertions(+), 32 deletions(-) diff --git a/src/hooks/useLeverageQuote.ts b/src/hooks/useLeverageQuote.ts index 894b112c..165b61d9 100644 --- a/src/hooks/useLeverageQuote.ts +++ b/src/hooks/useLeverageQuote.ts @@ -6,6 +6,10 @@ import { fetchVeloraPriceRoute, type VeloraPriceRoute } from '@/features/swap/ap import { computeFlashCollateralAmount, computeLeveragedExtraAmount, withSlippageFloor } from './leverage/math'; import type { LeverageRoute } from './leverage/types'; +const SELL_QUOTE_TARGET_BUFFER_BPS = 10_020n; +const SELL_QUOTE_MAX_ATTEMPTS = 4; +const BPS_SCALE = 10_000n; + type UseLeverageQuoteParams = { chainId: number; route: LeverageRoute | null; @@ -50,6 +54,119 @@ export type LeverageQuote = { swapPriceRoute: VeloraPriceRoute | null; }; +const scaleRawAmountCeil = (amount: bigint, fromDecimals: number, toDecimals: number): bigint => { + if (amount <= 0n) return 0n; + if (fromDecimals === toDecimals) return amount; + + if (toDecimals > fromDecimals) { + return amount * 10n ** BigInt(toDecimals - fromDecimals); + } + + const divisor = 10n ** BigInt(fromDecimals - toDecimals); + return (amount + divisor - 1n) / divisor; +}; + +const withTargetBuffer = (amount: bigint): bigint => { + if (amount <= 0n) return 1n; + return (amount * SELL_QUOTE_TARGET_BUFFER_BPS + BPS_SCALE - 1n) / BPS_SCALE; +}; + +const getNextSellAmountForTargetCollateral = ({ + currentSellAmount, + quotedCollateralAmount, + targetCollateralAmount, +}: { + currentSellAmount: bigint; + quotedCollateralAmount: bigint; + targetCollateralAmount: bigint; +}): bigint => { + if (quotedCollateralAmount <= 0n) return withTargetBuffer(currentSellAmount * 2n); + + const proportionalSellAmount = (currentSellAmount * targetCollateralAmount + quotedCollateralAmount - 1n) / quotedCollateralAmount; + const bufferedSellAmount = withTargetBuffer(proportionalSellAmount); + if (bufferedSellAmount > currentSellAmount) return bufferedSellAmount; + + return currentSellAmount + (currentSellAmount / 5n || 1n); +}; + +const quoteVeloraSellRouteForTargetCollateral = async ({ + chainId, + loanTokenAddress, + loanTokenDecimals, + collateralTokenAddress, + collateralTokenDecimals, + targetCollateralTokenAmount, + slippageBps, + swapExecutionAddress, +}: { + chainId: number; + loanTokenAddress: string; + loanTokenDecimals: number; + collateralTokenAddress: string; + collateralTokenDecimals: number; + targetCollateralTokenAmount: bigint; + slippageBps: number; + swapExecutionAddress: `0x${string}`; +}): Promise<{ + flashLoanAssetAmount: bigint; + flashLegCollateralTokenAmount: bigint; + priceRoute: VeloraPriceRoute; +}> => { + let sellAmount = scaleRawAmountCeil(targetCollateralTokenAmount, collateralTokenDecimals, loanTokenDecimals); + if (sellAmount <= 0n) sellAmount = 1n; + + let latestQuote: { + flashLoanAssetAmount: bigint; + flashLegCollateralTokenAmount: bigint; + quotedCollateralTokenAmount: bigint; + priceRoute: VeloraPriceRoute; + } | null = null; + + for (let attempt = 0; attempt < SELL_QUOTE_MAX_ATTEMPTS; attempt += 1) { + const sellRoute = await fetchVeloraPriceRoute({ + srcToken: loanTokenAddress, + srcDecimals: loanTokenDecimals, + destToken: collateralTokenAddress, + destDecimals: collateralTokenDecimals, + amount: sellAmount, + network: chainId, + userAddress: swapExecutionAddress, + side: 'SELL', + }); + const quotedCollateralTokenAmount = BigInt(sellRoute.destAmount); + + latestQuote = { + flashLoanAssetAmount: sellAmount, + flashLegCollateralTokenAmount: withSlippageFloor(quotedCollateralTokenAmount, slippageBps), + quotedCollateralTokenAmount, + priceRoute: sellRoute, + }; + + if (quotedCollateralTokenAmount >= targetCollateralTokenAmount) { + break; + } + + sellAmount = getNextSellAmountForTargetCollateral({ + currentSellAmount: sellAmount, + quotedCollateralAmount: quotedCollateralTokenAmount, + targetCollateralAmount: targetCollateralTokenAmount, + }); + } + + if (!latestQuote) { + throw new Error('Failed to quote Velora sell route for leverage.'); + } + if (latestQuote.quotedCollateralTokenAmount < targetCollateralTokenAmount) { + throw new Error('Failed to size Velora sell route for target leverage. Try a lower multiplier or refresh the quote.'); + } + + return { + flashLoanAssetAmount: latestQuote.flashLoanAssetAmount, + flashLegCollateralTokenAmount: latestQuote.flashLegCollateralTokenAmount, + priceRoute: latestQuote.priceRoute, + }; +}; + /** * Converts user leverage intent into deterministic route amounts. * @@ -142,44 +259,61 @@ export function useLeverageQuote({ ], enabled: route?.kind === 'swap' && !isLoanAssetInput && targetFlashCollateralTokenAmount > 0n && !!userAddress, queryFn: async () => { - const buyRoute = await fetchVeloraPriceRoute({ - srcToken: loanTokenAddress, - srcDecimals: loanTokenDecimals, - destToken: collateralTokenAddress, - destDecimals: collateralTokenDecimals, - amount: targetFlashCollateralTokenAmount, - network: chainId, - userAddress: swapExecutionAddress as `0x${string}`, - side: 'BUY', - }); + try { + const buyRoute = await fetchVeloraPriceRoute({ + srcToken: loanTokenAddress, + srcDecimals: loanTokenDecimals, + destToken: collateralTokenAddress, + destDecimals: collateralTokenDecimals, + amount: targetFlashCollateralTokenAmount, + network: chainId, + userAddress: swapExecutionAddress as `0x${string}`, + side: 'BUY', + }); + + const borrowAssets = BigInt(buyRoute.srcAmount); + if (borrowAssets <= 0n) { + return { + flashLoanAssetAmount: 0n, + flashLegCollateralTokenAmount: 0n, + priceRoute: null, + }; + } + + const sellRoute = await fetchVeloraPriceRoute({ + srcToken: loanTokenAddress, + srcDecimals: loanTokenDecimals, + destToken: collateralTokenAddress, + destDecimals: collateralTokenDecimals, + amount: borrowAssets, + network: chainId, + userAddress: swapExecutionAddress as `0x${string}`, + side: 'SELL', + }); - const borrowAssets = BigInt(buyRoute.srcAmount); - if (borrowAssets <= 0n) { return { - flashLoanAssetAmount: 0n, - flashLegCollateralTokenAmount: 0n, - priceRoute: null, + flashLoanAssetAmount: borrowAssets, + // Quote preview uses the requested sell size as authoritative. The built calldata + // still has to prove that exact sell amount before leverage execution can proceed. + flashLegCollateralTokenAmount: withSlippageFloor(BigInt(sellRoute.destAmount), slippageBps), + priceRoute: sellRoute, }; + } catch { + // Velora's exact-out BUY solver can fail for large routes even when + // exact-in SELL liquidity is available. Execution already uses sell calldata, so + // size a SELL quote directly before surfacing a quote error. } - const sellRoute = await fetchVeloraPriceRoute({ - srcToken: loanTokenAddress, - srcDecimals: loanTokenDecimals, - destToken: collateralTokenAddress, - destDecimals: collateralTokenDecimals, - amount: borrowAssets, - network: chainId, - userAddress: swapExecutionAddress as `0x${string}`, - side: 'SELL', + return quoteVeloraSellRouteForTargetCollateral({ + chainId, + loanTokenAddress, + loanTokenDecimals, + collateralTokenAddress, + collateralTokenDecimals, + targetCollateralTokenAmount: targetFlashCollateralTokenAmount, + slippageBps, + swapExecutionAddress: swapExecutionAddress as `0x${string}`, }); - - return { - flashLoanAssetAmount: borrowAssets, - // Quote preview uses the requested sell size as authoritative. The built calldata - // still has to prove that exact sell amount before leverage execution can proceed. - flashLegCollateralTokenAmount: withSlippageFloor(BigInt(sellRoute.destAmount), slippageBps), - priceRoute: sellRoute, - }; }, }); From 4a40874ffff570ecd84088813cdb9671bc23e01f Mon Sep 17 00:00:00 2001 From: antoncoding Date: Wed, 22 Apr 2026 10:14:39 +0800 Subject: [PATCH 2/7] chore: comments --- src/hooks/useLeverageQuote.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/hooks/useLeverageQuote.ts b/src/hooks/useLeverageQuote.ts index 165b61d9..f798eafc 100644 --- a/src/hooks/useLeverageQuote.ts +++ b/src/hooks/useLeverageQuote.ts @@ -3,12 +3,11 @@ import { useQuery } from '@tanstack/react-query'; import { useReadContract } from 'wagmi'; import { erc4626Abi } from '@/abis/erc4626'; import { fetchVeloraPriceRoute, type VeloraPriceRoute } from '@/features/swap/api/velora'; -import { computeFlashCollateralAmount, computeLeveragedExtraAmount, withSlippageFloor } from './leverage/math'; +import { BPS_SCALE, computeFlashCollateralAmount, computeLeveragedExtraAmount, withSlippageFloor } from './leverage/math'; import type { LeverageRoute } from './leverage/types'; const SELL_QUOTE_TARGET_BUFFER_BPS = 10_020n; const SELL_QUOTE_MAX_ATTEMPTS = 4; -const BPS_SCALE = 10_000n; type UseLeverageQuoteParams = { chainId: number; @@ -54,6 +53,11 @@ export type LeverageQuote = { swapPriceRoute: VeloraPriceRoute | null; }; +/** + * Collateral-input leverage targets a collateral output, but the executable Velora calldata is exact-in SELL. + * When Velora cannot solve exact-out BUY at size, iterate exact-in SELL quotes until one route reaches the + * target collateral amount; only the final route is submitted for transaction calldata. + */ const scaleRawAmountCeil = (amount: bigint, fromDecimals: number, toDecimals: number): bigint => { if (amount <= 0n) return 0n; if (fromDecimals === toDecimals) return amount; @@ -301,7 +305,7 @@ export function useLeverageQuote({ } catch { // Velora's exact-out BUY solver can fail for large routes even when // exact-in SELL liquidity is available. Execution already uses sell calldata, so - // size a SELL quote directly before surfacing a quote error. + // fall back to sizing one final SELL route for the transaction. } return quoteVeloraSellRouteForTargetCollateral({ From 35c76232b110e71a2d082968a6bb0a9e1d333824 Mon Sep 17 00:00:00 2001 From: antoncoding Date: Wed, 22 Apr 2026 10:41:14 +0800 Subject: [PATCH 3/7] chore: cleanup --- src/hooks/useDeleverageQuote.ts | 1 + src/hooks/useLeverageQuote.ts | 67 ++++++++------------------------- 2 files changed, 17 insertions(+), 51 deletions(-) diff --git a/src/hooks/useDeleverageQuote.ts b/src/hooks/useDeleverageQuote.ts index ffad9898..1cebf6c3 100644 --- a/src/hooks/useDeleverageQuote.ts +++ b/src/hooks/useDeleverageQuote.ts @@ -135,6 +135,7 @@ export function useDeleverageQuote({ userAddress: swapExecutionAddress as `0x${string}`, side: 'BUY', }); + const quotedDebtCloseAmount = BigInt(buyRoute.destAmount); if (quotedDebtCloseAmount !== bufferedBorrowAssets) { throw new Error('Failed to resolve the exact full-close collateral bound. Refresh the quote and try again.'); diff --git a/src/hooks/useLeverageQuote.ts b/src/hooks/useLeverageQuote.ts index f798eafc..5c59cc70 100644 --- a/src/hooks/useLeverageQuote.ts +++ b/src/hooks/useLeverageQuote.ts @@ -8,6 +8,7 @@ import type { LeverageRoute } from './leverage/types'; const SELL_QUOTE_TARGET_BUFFER_BPS = 10_020n; const SELL_QUOTE_MAX_ATTEMPTS = 4; +const shouldLogLeverageQuoteDebug = () => process.env.NODE_ENV !== 'production'; type UseLeverageQuoteParams = { chainId: number; @@ -55,8 +56,8 @@ export type LeverageQuote = { /** * Collateral-input leverage targets a collateral output, but the executable Velora calldata is exact-in SELL. - * When Velora cannot solve exact-out BUY at size, iterate exact-in SELL quotes until one route reaches the - * target collateral amount; only the final route is submitted for transaction calldata. + * Iterate exact-in SELL quotes to size the loan-token input; earlier quotes are discarded and only the final + * route is submitted for transaction calldata. */ const scaleRawAmountCeil = (amount: bigint, fromDecimals: number, toDecimals: number): bigint => { if (amount <= 0n) return 0n; @@ -163,6 +164,16 @@ const quoteVeloraSellRouteForTargetCollateral = async ({ if (latestQuote.quotedCollateralTokenAmount < targetCollateralTokenAmount) { throw new Error('Failed to size Velora sell route for target leverage. Try a lower multiplier or refresh the quote.'); } + if (shouldLogLeverageQuoteDebug()) { + console.info('[leverage quote] Velora SELL route sized', { + chainId, + sellAmount: latestQuote.flashLoanAssetAmount.toString(), + quotedCollateralAmount: latestQuote.quotedCollateralTokenAmount.toString(), + minCollateralAmount: latestQuote.flashLegCollateralTokenAmount.toString(), + targetCollateralAmount: targetCollateralTokenAmount.toString(), + slippageBps, + }); + } return { flashLoanAssetAmount: latestQuote.flashLoanAssetAmount, @@ -262,53 +273,8 @@ export function useLeverageQuote({ userAddress ?? null, ], enabled: route?.kind === 'swap' && !isLoanAssetInput && targetFlashCollateralTokenAmount > 0n && !!userAddress, - queryFn: async () => { - try { - const buyRoute = await fetchVeloraPriceRoute({ - srcToken: loanTokenAddress, - srcDecimals: loanTokenDecimals, - destToken: collateralTokenAddress, - destDecimals: collateralTokenDecimals, - amount: targetFlashCollateralTokenAmount, - network: chainId, - userAddress: swapExecutionAddress as `0x${string}`, - side: 'BUY', - }); - - const borrowAssets = BigInt(buyRoute.srcAmount); - if (borrowAssets <= 0n) { - return { - flashLoanAssetAmount: 0n, - flashLegCollateralTokenAmount: 0n, - priceRoute: null, - }; - } - - const sellRoute = await fetchVeloraPriceRoute({ - srcToken: loanTokenAddress, - srcDecimals: loanTokenDecimals, - destToken: collateralTokenAddress, - destDecimals: collateralTokenDecimals, - amount: borrowAssets, - network: chainId, - userAddress: swapExecutionAddress as `0x${string}`, - side: 'SELL', - }); - - return { - flashLoanAssetAmount: borrowAssets, - // Quote preview uses the requested sell size as authoritative. The built calldata - // still has to prove that exact sell amount before leverage execution can proceed. - flashLegCollateralTokenAmount: withSlippageFloor(BigInt(sellRoute.destAmount), slippageBps), - priceRoute: sellRoute, - }; - } catch { - // Velora's exact-out BUY solver can fail for large routes even when - // exact-in SELL liquidity is available. Execution already uses sell calldata, so - // fall back to sizing one final SELL route for the transaction. - } - - return quoteVeloraSellRouteForTargetCollateral({ + queryFn: async () => + quoteVeloraSellRouteForTargetCollateral({ chainId, loanTokenAddress, loanTokenDecimals, @@ -317,8 +283,7 @@ export function useLeverageQuote({ targetCollateralTokenAmount: targetFlashCollateralTokenAmount, slippageBps, swapExecutionAddress: swapExecutionAddress as `0x${string}`, - }); - }, + }), }); const swapLoanInputCombinedQuoteQuery = useQuery({ From fe4054901f4f6636950a8a3bf7ad456e935ad33e Mon Sep 17 00:00:00 2001 From: Stark Sama Date: Wed, 22 Apr 2026 11:50:11 +0800 Subject: [PATCH 4/7] fix: seed collateral-input sell quotes with price-aware bootstrap --- src/hooks/useLeverageQuote.ts | 67 +++++++++++++++++++++++++++++++---- 1 file changed, 61 insertions(+), 6 deletions(-) diff --git a/src/hooks/useLeverageQuote.ts b/src/hooks/useLeverageQuote.ts index 5c59cc70..94b18f3b 100644 --- a/src/hooks/useLeverageQuote.ts +++ b/src/hooks/useLeverageQuote.ts @@ -94,7 +94,56 @@ const getNextSellAmountForTargetCollateral = ({ return currentSellAmount + (currentSellAmount / 5n || 1n); }; -const quoteVeloraSellRouteForTargetCollateral = async ({ +const resolveInitialSellAmountForTargetCollateral = async ({ + chainId, + loanTokenAddress, + loanTokenDecimals, + collateralTokenAddress, + collateralTokenDecimals, + targetCollateralTokenAmount, + swapExecutionAddress, +}: { + chainId: number; + loanTokenAddress: string; + loanTokenDecimals: number; + collateralTokenAddress: string; + collateralTokenDecimals: number; + targetCollateralTokenAmount: bigint; + swapExecutionAddress: `0x${string}`; +}): Promise => { + try { + // Prefer a price-aware bootstrap. When exact-out BUY is available, its required source amount is a + // much better first SELL guess than a raw decimal conversion, especially for routes like USDC -> cbBTC. + const buyRoute = await fetchVeloraPriceRoute({ + srcToken: loanTokenAddress, + srcDecimals: loanTokenDecimals, + destToken: collateralTokenAddress, + destDecimals: collateralTokenDecimals, + amount: targetCollateralTokenAmount, + network: chainId, + userAddress: swapExecutionAddress, + side: 'BUY', + }); + + const initialSellAmount = BigInt(buyRoute.srcAmount); + if (initialSellAmount > 0n && BigInt(buyRoute.destAmount) >= targetCollateralTokenAmount) { + return initialSellAmount; + } + } catch (error) { + if (shouldLogLeverageQuoteDebug()) { + console.info('[leverage quote] Exact-out BUY bootstrap unavailable, falling back to estimated SELL seed', { + chainId, + targetCollateralAmount: targetCollateralTokenAmount.toString(), + error: error instanceof Error ? error.message : String(error), + }); + } + } + + const estimatedSellAmount = scaleRawAmountCeil(targetCollateralTokenAmount, collateralTokenDecimals, loanTokenDecimals); + return estimatedSellAmount > 0n ? estimatedSellAmount : 1n; +}; + +const quoteVeloraCollateralInputRoute = async ({ chainId, loanTokenAddress, loanTokenDecimals, @@ -117,8 +166,15 @@ const quoteVeloraSellRouteForTargetCollateral = async ({ flashLegCollateralTokenAmount: bigint; priceRoute: VeloraPriceRoute; }> => { - let sellAmount = scaleRawAmountCeil(targetCollateralTokenAmount, collateralTokenDecimals, loanTokenDecimals); - if (sellAmount <= 0n) sellAmount = 1n; + let sellAmount = await resolveInitialSellAmountForTargetCollateral({ + chainId, + loanTokenAddress, + loanTokenDecimals, + collateralTokenAddress, + collateralTokenDecimals, + targetCollateralTokenAmount, + swapExecutionAddress, + }); let latestQuote: { flashLoanAssetAmount: bigint; @@ -165,7 +221,7 @@ const quoteVeloraSellRouteForTargetCollateral = async ({ throw new Error('Failed to size Velora sell route for target leverage. Try a lower multiplier or refresh the quote.'); } if (shouldLogLeverageQuoteDebug()) { - console.info('[leverage quote] Velora SELL route sized', { + console.info('[leverage quote] Velora collateral-input route sized', { chainId, sellAmount: latestQuote.flashLoanAssetAmount.toString(), quotedCollateralAmount: latestQuote.quotedCollateralTokenAmount.toString(), @@ -181,7 +237,6 @@ const quoteVeloraSellRouteForTargetCollateral = async ({ priceRoute: latestQuote.priceRoute, }; }; - /** * Converts user leverage intent into deterministic route amounts. * @@ -274,7 +329,7 @@ export function useLeverageQuote({ ], enabled: route?.kind === 'swap' && !isLoanAssetInput && targetFlashCollateralTokenAmount > 0n && !!userAddress, queryFn: async () => - quoteVeloraSellRouteForTargetCollateral({ + quoteVeloraCollateralInputRoute({ chainId, loanTokenAddress, loanTokenDecimals, From 507527d929520cb353c226d6a651d78f9ab550ce Mon Sep 17 00:00:00 2001 From: Stark Sama Date: Wed, 22 Apr 2026 12:42:47 +0800 Subject: [PATCH 5/7] fix: make Velora quote errors clearer --- src/hooks/leverage/velora-quote-errors.ts | 39 +++++++++++++++++++++++ src/hooks/useDeleverageQuote.ts | 3 +- src/hooks/useLeverageQuote.ts | 3 +- 3 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 src/hooks/leverage/velora-quote-errors.ts diff --git a/src/hooks/leverage/velora-quote-errors.ts b/src/hooks/leverage/velora-quote-errors.ts new file mode 100644 index 00000000..bdea9a91 --- /dev/null +++ b/src/hooks/leverage/velora-quote-errors.ts @@ -0,0 +1,39 @@ +const normalizeMessage = (error: unknown): string => { + if (error instanceof Error) return error.message; + if (typeof error === 'string') return error; + return ''; +}; + +const isPairValidationError = (message: string): boolean => + message.includes('validation failed') && (message.includes('srctoken') || message.includes('desttoken')); + +export const toUserFacingVeloraQuoteError = ({ error, action }: { error: unknown; action: 'leverage' | 'deleverage' }): string => { + const rawMessage = normalizeMessage(error); + const message = rawMessage.toLowerCase(); + + if (message.includes('no routes found with enough liquidity')) { + return 'No swap route is available for this size right now. Try a smaller amount or a different market.'; + } + + if (message.includes('estimated_loss_greater_than_max_impact') || message.includes('max impact')) { + return 'This swap is too expensive right now. Try a smaller amount or a different market.'; + } + + if (message.includes('failed to size velora sell route for target leverage')) { + return 'Could not find a swap route that reaches this size. Try a smaller amount or a lower multiplier.'; + } + + if (isPairValidationError(message)) { + return 'This pair is not available through the swap router right now.'; + } + + if (message.includes('failed to fetch velora api response')) { + return 'Could not reach the swap router. Please try again.'; + } + + if (!rawMessage) { + return action === 'leverage' ? 'Failed to quote swap-backed leverage route.' : 'Failed to quote swap-backed deleverage route.'; + } + + return rawMessage; +}; diff --git a/src/hooks/useDeleverageQuote.ts b/src/hooks/useDeleverageQuote.ts index 1cebf6c3..5fb6e4a7 100644 --- a/src/hooks/useDeleverageQuote.ts +++ b/src/hooks/useDeleverageQuote.ts @@ -5,6 +5,7 @@ import { useReadContract } from 'wagmi'; import { erc4626Abi } from '@/abis/erc4626'; import { fetchVeloraPriceRoute, type VeloraPriceRoute } from '@/features/swap/api/velora'; import { withSlippageCeil, withSlippageFloor } from './leverage/math'; +import { toUserFacingVeloraQuoteError } from './leverage/velora-quote-errors'; import type { LeverageRoute } from './leverage/types'; type UseDeleverageQuoteParams = { @@ -266,7 +267,7 @@ export function useDeleverageQuote({ } const routeError = withdrawCollateralAmount > 0n ? swapRepayQuoteQuery.error : null; if (!routeError) return null; - return routeError instanceof Error ? routeError.message : 'Failed to quote Velora swap route for deleverage.'; + return toUserFacingVeloraQuoteError({ error: routeError, action: 'deleverage' }); } const routeError = erc4626PreviewRedeemError; if (!routeError) return null; diff --git a/src/hooks/useLeverageQuote.ts b/src/hooks/useLeverageQuote.ts index 94b18f3b..b27487e6 100644 --- a/src/hooks/useLeverageQuote.ts +++ b/src/hooks/useLeverageQuote.ts @@ -4,6 +4,7 @@ import { useReadContract } from 'wagmi'; import { erc4626Abi } from '@/abis/erc4626'; import { fetchVeloraPriceRoute, type VeloraPriceRoute } from '@/features/swap/api/velora'; import { BPS_SCALE, computeFlashCollateralAmount, computeLeveragedExtraAmount, withSlippageFloor } from './leverage/math'; +import { toUserFacingVeloraQuoteError } from './leverage/velora-quote-errors'; import type { LeverageRoute } from './leverage/types'; const SELL_QUOTE_TARGET_BUFFER_BPS = 10_020n; @@ -452,7 +453,7 @@ export function useLeverageQuote({ if (!userAddress && initialCapitalInputAmount > 0n) return 'Connect wallet to fetch swap-backed leverage route.'; const routeError = isLoanAssetInput ? swapLoanInputCombinedQuoteQuery.error : swapCollateralInputQuoteQuery.error; if (!routeError) return null; - return routeError instanceof Error ? routeError.message : 'Failed to quote Velora swap route for leverage.'; + return toUserFacingVeloraQuoteError({ error: routeError, action: 'leverage' }); } const erc4626RouteError = erc4626DepositError ?? erc4626MintError; if (!erc4626RouteError) return null; From 6e1322e49f6350a9c2eef7f68b096ecf8f04ecc9 Mon Sep 17 00:00:00 2001 From: Stark Sama Date: Wed, 22 Apr 2026 13:36:35 +0800 Subject: [PATCH 6/7] fix: show swap impact in quote errors --- src/hooks/leverage/velora-quote-errors.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/hooks/leverage/velora-quote-errors.ts b/src/hooks/leverage/velora-quote-errors.ts index bdea9a91..3fa8a39b 100644 --- a/src/hooks/leverage/velora-quote-errors.ts +++ b/src/hooks/leverage/velora-quote-errors.ts @@ -1,9 +1,20 @@ +import { VeloraApiError } from '@/features/swap/api/velora'; + const normalizeMessage = (error: unknown): string => { if (error instanceof Error) return error.message; if (typeof error === 'string') return error; return ''; }; +const extractImpactValue = (error: unknown): string | null => { + if (!(error instanceof VeloraApiError) || !error.details || typeof error.details !== 'object') { + return null; + } + + const details = error.details as Record; + return typeof details.value === 'string' ? details.value : null; +}; + const isPairValidationError = (message: string): boolean => message.includes('validation failed') && (message.includes('srctoken') || message.includes('desttoken')); @@ -16,7 +27,10 @@ export const toUserFacingVeloraQuoteError = ({ error, action }: { error: unknown } if (message.includes('estimated_loss_greater_than_max_impact') || message.includes('max impact')) { - return 'This swap is too expensive right now. Try a smaller amount or a different market.'; + const impactValue = extractImpactValue(error); + return impactValue + ? `This swap would lose too much value right now (~${impactValue} impact). Try a smaller amount or a different market.` + : 'This swap is too expensive right now. Try a smaller amount or a different market.'; } if (message.includes('failed to size velora sell route for target leverage')) { From b01b3697fe2edabc40978f0fced6a095e00b7a4f Mon Sep 17 00:00:00 2001 From: Stark Sama Date: Wed, 22 Apr 2026 13:37:55 +0800 Subject: [PATCH 7/7] fix: clarify Velora quote error wording --- src/hooks/leverage/velora-quote-errors.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/hooks/leverage/velora-quote-errors.ts b/src/hooks/leverage/velora-quote-errors.ts index 3fa8a39b..6bd05f2e 100644 --- a/src/hooks/leverage/velora-quote-errors.ts +++ b/src/hooks/leverage/velora-quote-errors.ts @@ -29,12 +29,12 @@ export const toUserFacingVeloraQuoteError = ({ error, action }: { error: unknown if (message.includes('estimated_loss_greater_than_max_impact') || message.includes('max impact')) { const impactValue = extractImpactValue(error); return impactValue - ? `This swap would lose too much value right now (~${impactValue} impact). Try a smaller amount or a different market.` - : 'This swap is too expensive right now. Try a smaller amount or a different market.'; + ? `Could not find a quote with reasonable impact right now (~${impactValue} impact). Try a smaller amount or a different market.` + : 'Could not find a quote with reasonable impact right now. Try a smaller amount or a different market.'; } if (message.includes('failed to size velora sell route for target leverage')) { - return 'Could not find a swap route that reaches this size. Try a smaller amount or a lower multiplier.'; + return 'Could not find a swap route that reaches this size. Try a smaller amount or lower leverage.'; } if (isPairValidationError(message)) {