Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 46 additions & 33 deletions src/app/trade/components/leverage-widget/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,34 +36,35 @@ import type { SymbolsByChain } from '@indexcoop/tokenlists'

const hiddenLeverageWarnings = [WarningType.flashbots]

// Tokens temporarily disabled for trading, per chain. Symbols are typed
// against the tokenlist so a typo or wrong-chain entry is a compile error.
// Set `mintReason` and/or `redeemReason` to disable the corresponding
// direction; omit a field to leave that direction enabled.
// Tokens flagged for paused markets, per chain. Symbols are typed against
// the tokenlist so a typo or wrong-chain entry is a compile error.
// - `mintReason` blocks the trade button when minting and renders the reason.
// - `redeemWarning` shows an informational banner when redeeming but leaves
// the trade button active (users can still attempt to exit).
type DisableEntry<
C extends typeof mainnet.id | typeof arbitrum.id | typeof base.id,
> = {
symbols: readonly SymbolsByChain<C>[]
mintReason?: string
redeemReason?: string
redeemWarning?: string
}

const ETH_MINT_PAUSED_REASON =
'Redemptions for ETH2x and ETH3x on Ethereum and Arbitrum are temporarily paused. Minting is disabled to prevent users from being trapped in positions they cannot exit.'

const ETH_REDEEM_PAUSED_REASON =
'Redemptions for ETH2x and ETH3x on Ethereum and Arbitrum are temporarily paused due to ongoing issues with Aave. Please check back later.'
const ETH_REDEEM_WARNING =
'Redemptions for ETH2x and ETH3x on Ethereum and Arbitrum are affected by ongoing issues with Aave and may fail. Please try again later if your transaction does not go through.'

const TEMPORARILY_DISABLED_TOKENS_BY_CHAIN = {
[mainnet.id]: {
symbols: ['ETH2X', 'ETH3x'],
mintReason: ETH_MINT_PAUSED_REASON,
redeemReason: ETH_REDEEM_PAUSED_REASON,
redeemWarning: ETH_REDEEM_WARNING,
} satisfies DisableEntry<typeof mainnet.id>,
[arbitrum.id]: {
symbols: ['ETH2X', 'ETH3X'],
mintReason: ETH_MINT_PAUSED_REASON,
redeemReason: ETH_REDEEM_PAUSED_REASON,
redeemWarning: ETH_REDEEM_WARNING,
} satisfies DisableEntry<typeof arbitrum.id>,
[base.id]: {
symbols: ['uSOL2x', 'uSOL3x', 'uSUI2x', 'uSUI3x'],
Expand Down Expand Up @@ -149,7 +150,7 @@ export function LeverageWidget() {
[tradeState],
)

const tradeDisabledReason = useMemo(() => {
const pausedEntry = useMemo(() => {
const leverageToken = isMinting ? outputToken : inputToken
if (!leverageToken.chainId) return null
const entry = TEMPORARILY_DISABLED_TOKENS_BY_CHAIN[
Expand All @@ -158,14 +159,18 @@ export function LeverageWidget() {
| {
symbols: readonly string[]
mintReason?: string
redeemReason?: string
redeemWarning?: string
}
| undefined
if (!entry) return null
if (!entry.symbols.includes(leverageToken.symbol)) return null
return (isMinting ? entry.mintReason : entry.redeemReason) ?? null
return entry.symbols.includes(leverageToken.symbol) ? entry : null
}, [isMinting, inputToken, outputToken])

const mintDisabledReason = isMinting
? (pausedEntry?.mintReason ?? null)
: null
const redeemWarning = !isMinting ? (pausedEntry?.redeemWarning ?? null) : null

return (
<div
className='flex w-full flex-col gap-3 rounded-lg border border-white/15 bg-zinc-900 px-4 pb-5 pt-4 sm:gap-4'
Expand Down Expand Up @@ -229,33 +234,41 @@ export function LeverageWidget() {
</div>
)}
<Summary />
{tradeDisabledReason ? (
{mintDisabledReason ? (
<div className='flex flex-col items-center justify-center gap-2 rounded-lg border border-amber-500/30 bg-amber-500/10 px-4 py-3 text-center'>
<div className='flex items-center gap-2 text-amber-400'>
<ExclamationCircleIcon className='size-5' />
<span className='font-semibold'>Temporarily Unavailable</span>
</div>
<p className='text-sm text-zinc-400'>{tradeDisabledReason}</p>
<p className='text-sm text-zinc-400'>{mintDisabledReason}</p>
</div>
) : (
<SmartTradeButton
contract={contract ?? ''}
hasFetchingError={hasFetchingError}
hasInsufficientFunds={hasInsufficientFunds}
hiddenWarnings={hiddenLeverageWarnings}
inputTokenAmount={inputTokenAmount}
inputToken={inputToken}
inputValue={inputValue}
isFetchingQuote={isFetchingQuote}
isSupportedNetwork={isSupportedNetwork}
queryNetwork={queryParams.queryNetwork}
outputToken={outputToken}
buttonLabelOverrides={{
[TradeButtonState.default]: 'Review Transaction',
}}
onOpenTransactionReview={() => sendTradeEvent({ type: 'REVIEW' })}
onRefetchQuote={refetchQuote}
/>
<>
{redeemWarning && (
<div className='flex items-start gap-2 rounded-lg border border-amber-500/30 bg-amber-500/10 px-3 py-2 text-sm text-zinc-300'>
<ExclamationCircleIcon className='mt-0.5 size-4 shrink-0 text-amber-400' />
<p>{redeemWarning}</p>
</div>
)}
<SmartTradeButton
contract={contract ?? ''}
hasFetchingError={hasFetchingError}
hasInsufficientFunds={hasInsufficientFunds}
hiddenWarnings={hiddenLeverageWarnings}
inputTokenAmount={inputTokenAmount}
inputToken={inputToken}
inputValue={inputValue}
isFetchingQuote={isFetchingQuote}
isSupportedNetwork={isSupportedNetwork}
queryNetwork={queryParams.queryNetwork}
outputToken={outputToken}
buttonLabelOverrides={{
[TradeButtonState.default]: 'Review Transaction',
}}
onOpenTransactionReview={() => sendTradeEvent({ type: 'REVIEW' })}
onRefetchQuote={refetchQuote}
/>
</>
)}
<SelectTokenModal
isDarkMode={true}
Expand Down
13 changes: 9 additions & 4 deletions src/lib/hooks/use-best-quote/utils/flashmint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,9 @@ export async function getFlashMintQuote(
slippage,
}

const { data: result, status } = await getQuote(quoteRequest)
const { data: result, status, error } = await getQuote(quoteRequest)

if (status === 200) {
if (status === 200 && result) {
const quoteFM = result as GetApiV2QuoteQuery['Response']

const {
Expand Down Expand Up @@ -133,8 +133,13 @@ export async function getFlashMintQuote(
warning: isHighPriceImpact ? 'Price impact is high.' : undefined,
tx: transaction,
}
} else {
return result as unknown as GetApiV2QuoteQuery['Errors']
}

if (isQuoteError(result)) return result

return {
type: 'QuoteNotFound',
message: error ?? `Quote request failed (status ${status})`,
}
} catch (e) {
console.warn('Error fetching FlashMintQuote', e)
Expand Down
52 changes: 34 additions & 18 deletions src/lib/hooks/use-quote-result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ export function useQuoteResult(request: QuoteRequest) {

const {
data: flashmintQuote,
error: flashmintQuoteError,
isFetching: isFetchingFlashMintQuote,
refetch: refetchQuote,
} = useQuery({
Expand Down Expand Up @@ -113,42 +114,57 @@ export function useQuoteResult(request: QuoteRequest) {
})

useEffect(() => {
if (flashmintQuote === undefined || isFetchingFlashMintQuote) return
if (isFetchingFlashMintQuote) return

if (flashmintQuoteError) {
logEvent('Quote Failed', { message: flashmintQuoteError.message })
sendTradeEvent({
type: 'QUOTE_NOT_FOUND',
reason: flashmintQuoteError.message || 'Unknown Reason',
})
return
}

if (flashmintQuote === undefined) return

if (isQuoteError(flashmintQuote)) {
logEvent('Quote Failed', flashmintQuote)

sendTradeEvent({
type: 'QUOTE_NOT_FOUND',
reason:
get(quoteErrorCode, `${flashmintQuote.type}`) ?? 'Unknown Reason',
get(quoteErrorCode, `${flashmintQuote.type}`) ??
(flashmintQuote.message || 'Unknown Reason'),
})

return
}

if (flashmintQuote) {
logEvent('Quote Received', formatQuoteAnalytics(flashmintQuote))
}

const quoteResult = {
type: flashmintQuote?.type ?? QuoteType.flashmint,
isAvailable: true,
quote: flashmintQuote,
error: null,
}

if (quoteResult.quote) {
if (!flashmintQuote) {
sendTradeEvent({
type: 'QUOTE',
inputValue,
quoteResult,
quoteType: flashmintQuote?.type ?? QuoteType.flashmint,
type: 'QUOTE_NOT_FOUND',
reason: 'Quote Not Found',
})
return
}

logEvent('Quote Received', formatQuoteAnalytics(flashmintQuote))

sendTradeEvent({
type: 'QUOTE',
inputValue,
quoteResult: {
type: flashmintQuote.type ?? QuoteType.flashmint,
isAvailable: true,
quote: flashmintQuote,
error: null,
},
quoteType: flashmintQuote.type ?? QuoteType.flashmint,
})
}, [
chainId,
flashmintQuote,
flashmintQuoteError,
inputValue,
isFetchingFlashMintQuote,
logEvent,
Expand Down
Loading