From 511e46d3bffb6da839a5726276b9f2730b3a1d4a Mon Sep 17 00:00:00 2001 From: Jared Vu Date: Thu, 20 Nov 2025 08:37:34 -0800 Subject: [PATCH 01/11] fix: Swap UI (#1991) --- src/constants/analytics.ts | 14 +++++++- src/hooks/swap/useSwapQuote.ts | 4 ++- src/hooks/useUpdateSwaps.tsx | 30 +++++++++++++++- src/icons/card-holder.svg | 2 +- src/pages/token/Swap.tsx | 66 +++++++++++++++++----------------- 5 files changed, 79 insertions(+), 37 deletions(-) diff --git a/src/constants/analytics.ts b/src/constants/analytics.ts index 8d65277a23..dca7caaa4a 100644 --- a/src/constants/analytics.ts +++ b/src/constants/analytics.ts @@ -2,7 +2,7 @@ import { OrderSide, TradeFormType } from '@/bonsai/forms/trade/types'; import { PlaceOrderPayload } from '@/bonsai/forms/triggers/types'; import { ApiStatus, SubaccountFill } from '@/bonsai/types/summaryTypes'; import { type SupportedLocale } from '@dydxprotocol/v4-localization'; -import { RouteResponse, UserAddress } from '@skip-go/client'; +import { Route, RouteResponse, UserAddress } from '@skip-go/client'; import { RecordOf, TagsOf, UnionOf, ofType, unionize } from 'unionize'; import { type CustomFlags, type StatsigFlags } from '@/constants/statsig'; @@ -388,6 +388,18 @@ export const AnalyticsEvents = unionize( validatorAddress?: string; }>(), + // Swapping + SwapInitiated: ofType<{ id: string } & Route>(), + SwapError: ofType< + { + id: string; + step: 'withdraw-subaccount' | 'execute-swap'; + error: string; + } & Route + >(), + SwapSubmitted: ofType<{ id: string; txHash: string; chainId: string } & Route>(), + SwapFinalized: ofType<{ id: string; txHash: string; chainId: string } & Route>(), + // Sharing SharePnlShared: ofType<{ asset: string; diff --git a/src/hooks/swap/useSwapQuote.ts b/src/hooks/swap/useSwapQuote.ts index fa121c87d5..22143c5315 100644 --- a/src/hooks/swap/useSwapQuote.ts +++ b/src/hooks/swap/useSwapQuote.ts @@ -61,12 +61,14 @@ export function useSwapQuote( const { skipClient } = useSkipClient(); const tokenConfig = useTokenConfigs(); const selectedDydxChainId = useAppSelector(getSelectedDydxChainId); + const rawAmount = amount && parseFloat(amount); + const isAmountValid = rawAmount && rawAmount > 0; return useQuery({ queryKey: ['swap-quote', input, amount, mode, selectedDydxChainId], queryFn: () => getSkipSwapRoute(skipClient, input, tokenConfig, selectedDydxChainId, amount, mode), - enabled: Boolean(amount), + enabled: Boolean(isAmountValid), staleTime: 15 * timeUnits.second, // Don't auto-retry because some errors are legitimate retry: 0, diff --git a/src/hooks/useUpdateSwaps.tsx b/src/hooks/useUpdateSwaps.tsx index 39d4f459cb..8d7ca01e3e 100644 --- a/src/hooks/useUpdateSwaps.tsx +++ b/src/hooks/useUpdateSwaps.tsx @@ -5,6 +5,7 @@ import { BonsaiCore } from '@/bonsai/ontology'; import { formatUnits, parseUnits } from 'viem'; import { AMOUNT_RESERVED_FOR_GAS_USDC } from '@/constants/account'; +import { AnalyticsEvents } from '@/constants/analytics'; import { DYDX_CHAIN_DYDX_DENOM, USDC_DECIMALS } from '@/constants/tokens'; import { useSkipClient } from '@/hooks/transfers/skipClient'; @@ -18,6 +19,8 @@ import { useAppDispatch, useAppSelector } from '@/state/appTypes'; import { getPendingSwaps } from '@/state/swapSelectors'; import { Swap, updateSwap } from '@/state/swaps'; +import { track } from '@/lib/analytics/analytics'; + import { useSubaccount } from './useSubaccount'; const SWAP_SLIPPAGE_PERCENT = '0.50'; // 0.50% (50 bps) @@ -82,7 +85,10 @@ export const useUpdateSwaps = () => { route, userAddresses, slippageTolerancePercent: SWAP_SLIPPAGE_PERCENT, - onTransactionCompleted: async ({ txHash, status }) => { + onTransactionBroadcast: async ({ txHash, chainId }) => { + track(AnalyticsEvents.SwapSubmitted({ id: swap.id, txHash, ...route, chainId })); + }, + onTransactionCompleted: async ({ chainId, txHash, status }) => { const errorStates = ['STATE_COMPLETED_ERROR', 'STATE_ABANDONED', 'STATE_PENDING_ERROR']; if (status?.state && errorStates.includes(status.state)) { logBonsaiError('useUpdateSwaps', 'Error executing swap', { @@ -97,6 +103,8 @@ export const useUpdateSwaps = () => { txHash, swapId: swap.id, }); + + track(AnalyticsEvents.SwapFinalized({ id: swap.id, txHash, chainId, ...route })); dispatch(updateSwap({ swap: { id: swap.id, txHash, status: 'success' } })); } }, @@ -123,6 +131,16 @@ export const useUpdateSwaps = () => { logBonsaiError('useUpdateSwaps', 'Error withdrawing from subaccount', { error, }); + + track( + AnalyticsEvents.SwapError({ + id: swap.id, + step: 'withdraw-subaccount', + error: error.message, + ...route, + }) + ); + dispatch(updateSwap({ swap: { ...swap, status: 'error' } })); }) .then(() => { @@ -137,6 +155,16 @@ export const useUpdateSwaps = () => { error, swapId: swap.id, }); + + track( + AnalyticsEvents.SwapError({ + id: swap.id, + step: 'execute-swap', + error: error.message, + ...route, + }) + ); + dispatch(updateSwap({ swap: { ...swap, status: 'error' } })); }) .then(() => { diff --git a/src/icons/card-holder.svg b/src/icons/card-holder.svg index ebba258eb3..c3002ed3a7 100644 --- a/src/icons/card-holder.svg +++ b/src/icons/card-holder.svg @@ -1,3 +1,3 @@ - + diff --git a/src/pages/token/Swap.tsx b/src/pages/token/Swap.tsx index 53b52ac07f..af127e118b 100644 --- a/src/pages/token/Swap.tsx +++ b/src/pages/token/Swap.tsx @@ -1,17 +1,16 @@ import { EventHandler, useMemo, useState } from 'react'; import { BonsaiCore } from '@/bonsai/ontology'; -import { ArrowDownIcon } from '@radix-ui/react-icons'; -import { capitalize } from 'lodash'; import { SyntheticInputEvent } from 'react-number-format/types/types'; import styled from 'styled-components'; import tw from 'twin.macro'; import { formatUnits, parseUnits } from 'viem'; import { OnboardingState } from '@/constants/account'; +import { AnalyticsEvents } from '@/constants/analytics'; import { ButtonAction, ButtonShape, ButtonSize, ButtonStyle } from '@/constants/buttons'; import { STRING_KEYS } from '@/constants/localization'; -import { DYDX_CHAIN_DYDX_DENOM, DYDX_DECIMALS, USDC_DECIMALS } from '@/constants/tokens'; +import { DYDX_DECIMALS, USDC_DECIMALS } from '@/constants/tokens'; import { useSwapQuote } from '@/hooks/swap/useSwapQuote'; import { useDebounce } from '@/hooks/useDebounce'; @@ -38,6 +37,7 @@ import { useAppDispatch, useAppSelector } from '@/state/appTypes'; import { selectHasPendingSwaps } from '@/state/swapSelectors'; import { addSwap } from '@/state/swaps'; +import { track } from '@/lib/analytics/analytics'; import { escapeRegExp, numericValueRegex } from '@/lib/inputUtils'; import { BIG_NUMBERS } from '@/lib/numbers'; @@ -47,7 +47,7 @@ function otherToken(currToken: 'usdc' | 'dydx') { } function getTokenLabel(token: 'usdc' | 'dydx') { - return token === 'usdc' ? 'USDC' : 'dYdX'; + return token === 'usdc' ? 'USDC' : 'DYDX'; } const SWAP_SLIPPAGE_PERCENT = '0.50'; // 0.50% (50 bps) @@ -116,6 +116,7 @@ export const Swap = () => { const debouncedAmount = useDebounce(amount); + // Swap Quote const { data: quote, isLoading, @@ -123,24 +124,16 @@ export const Swap = () => { error, } = useSwapQuote(inputToken, debouncedAmount, mode); + // Exchange Rate Quote + const { data: priceQuote } = useSwapQuote('dydx', '1', 'exact-in'); + const hasSufficientBalance = useMemo(() => { if (!quote || !amount) return true; - - const inputAmountBigInt = parseUnits( - quote.amountIn, - quote.sourceAssetDenom === DYDX_CHAIN_DYDX_DENOM ? DYDX_DECIMALS : USDC_DECIMALS - ); - const inputBalanceBigInt = - quote.sourceAssetDenom === DYDX_CHAIN_DYDX_DENOM - ? tokenBalances.dydx?.rawBalanceBigInt - : tokenBalances.usdc?.rawBalanceBigInt; - + const inputAmountBigInt = BigInt(quote.amountIn); + const inputBalanceBigInt = tokenBalances.inputBalance.rawBalanceBigInt; if (!inputBalanceBigInt) return true; - - return inputBalanceBigInt <= inputAmountBigInt; - }, [quote, amount, tokenBalances.dydx?.rawBalanceBigInt, tokenBalances.usdc?.rawBalanceBigInt]); - - const { data: priceQuote } = useSwapQuote('dydx', '1', 'exact-in'); + return inputBalanceBigInt >= inputAmountBigInt; + }, [quote, amount, tokenBalances.inputBalance.rawBalanceBigInt]); const usdcPerDydx = useMemo(() => { if (!priceQuote) return undefined; @@ -150,12 +143,10 @@ export const Swap = () => { const quotedAmount = useMemo(() => { if (!quote || !amount) return ''; - const quotedToken = mode === 'exact-in' ? otherToken(inputToken) : inputToken; const quotedTokenDecimals = quotedToken === 'dydx' ? DYDX_DECIMALS : USDC_DECIMALS; const quotedTokenAmount = mode === 'exact-in' ? quote.amountOut : quote.amountIn; const formattedQuotedTokenAmount = formatUnits(BigInt(quotedTokenAmount), quotedTokenDecimals); - return Number(formattedQuotedTokenAmount).toFixed(2); }, [quote, inputToken, mode, amount]); @@ -181,6 +172,7 @@ export const Swap = () => { return; } const swapId = `swap-${crypto.randomUUID()}`; + track(AnalyticsEvents.SwapInitiated({ id: swapId, ...quote })); dispatch(addSwap({ swap: { id: swapId, route: quote, status: 'pending' } })); }; @@ -196,7 +188,7 @@ export const Swap = () => { + + + ); +}; + +const $LeverageInputContainer = styled.div` + ${formMixins.inputContainer} + --input-height: 3.5rem; + --input-backgroundColor: none; +`; + +const $InnerInputContainer = styled.div` + ${formMixins.inputContainer} + --input-backgroundColor: var(--color-layer-4); + --input-borderColor: none; + --input-height: 2.25rem; + --input-width: 4rem; + + margin-left: 0.25rem; + + input { + text-align: end; + padding: 0 var(--form-input-paddingX); + } +`; + +const $LeverageSlider = styled(Slider)` + height: 1.375rem; + --slider-track-background: linear-gradient( + 90deg, + var(--color-layer-7) 0%, + var(--color-text-2) 100% + ); +`; diff --git a/src/views/forms/AdjustIsolatedMarginForm.tsx b/src/views/forms/AdjustIsolatedMarginForm.tsx index a2047e52dd..00d30041ef 100644 --- a/src/views/forms/AdjustIsolatedMarginForm.tsx +++ b/src/views/forms/AdjustIsolatedMarginForm.tsx @@ -425,15 +425,17 @@ const $ToggleGroup = styled(ToggleGroup)` function useForm() { const rawParentSubaccountData = useAppSelector(BonsaiRaw.parentSubaccountBase); const rawRelevantMarkets = useAppSelector(BonsaiRaw.parentSubaccountRelevantMarkets); + const selectedMarketLeverages = useAppSelector(BonsaiRaw.selectedMarketLeverages); const canViewAccount = useAppSelector(calculateCanViewAccount); const inputs = useMemo( () => ({ rawParentSubaccountData, rawRelevantMarkets, + selectedMarketLeverages: selectedMarketLeverages ?? {}, canViewAccount, }), - [canViewAccount, rawParentSubaccountData, rawRelevantMarkets] + [canViewAccount, rawParentSubaccountData, rawRelevantMarkets, selectedMarketLeverages] ); return useFormValues(BonsaiForms.AdjustIsolatedMarginFormFns, inputs); diff --git a/src/views/forms/ClosePositionForm.tsx b/src/views/forms/ClosePositionForm.tsx index 19b7b7980f..7a9fff0693 100644 --- a/src/views/forms/ClosePositionForm.tsx +++ b/src/views/forms/ClosePositionForm.tsx @@ -47,7 +47,7 @@ import { orEmptyObj } from '@/lib/typeUtils'; import { CanvasOrderbook } from '../CanvasOrderbook/CanvasOrderbook'; import { TradeFormMessages } from '../TradeFormMessages/TradeFormMessages'; -import { AmountCloseInput } from './TradeForm/AmountCloseInput'; +import { AllocationSlider } from './TradeForm/AllocationSlider'; import { PlaceOrderButtonAndReceipt } from './TradeForm/PlaceOrderButtonAndReceipt'; type ElementProps = { @@ -203,21 +203,19 @@ export const ClosePositionForm = ({ tw="w-full" /> - (positionSize > 0 ? tSize / positionSize : 0) - ) - ) - ) - ?.times(100) - .toFixed(0, BigNumber.ROUND_FLOOR)} - setAmountCloseInput={(value: string | undefined) => { + { dispatch( closePositionFormActions.setSizeAvailablePercent( mapIfPresent(value, (v) => MaybeBigNumber(v)?.div(100).toFixed(2)) ?? '' diff --git a/src/views/forms/TradeForm.tsx b/src/views/forms/TradeForm.tsx index 776cd3fcf7..9e25bdc277 100644 --- a/src/views/forms/TradeForm.tsx +++ b/src/views/forms/TradeForm.tsx @@ -115,7 +115,6 @@ export const TradeForm = ({ [ rawInput.triggerPrice, rawInput.limitPrice, - rawInput.targetLeverage, rawInput.reduceOnly, rawInput.goodTil, rawInput.execution, diff --git a/src/views/forms/TradeForm/AdvancedTradeOptions.tsx b/src/views/forms/TradeForm/AdvancedTradeOptions.tsx index 928529e3db..e5896cc4b9 100644 --- a/src/views/forms/TradeForm/AdvancedTradeOptions.tsx +++ b/src/views/forms/TradeForm/AdvancedTradeOptions.tsx @@ -1,9 +1,10 @@ import { useEffect } from 'react'; -import { ExecutionType, TimeInForce, TimeUnit } from '@/bonsai/forms/trade/types'; +import { ExecutionType, TimeInForce, TimeUnit, TradeFormType } from '@/bonsai/forms/trade/types'; import { BonsaiHelpers } from '@/bonsai/ontology'; import { type NumberFormatValues } from 'react-number-format'; import styled from 'styled-components'; +import tw from 'twin.macro'; import { ComplianceStates } from '@/constants/compliance'; import { STRING_KEYS, StringKey } from '@/constants/localization'; @@ -46,8 +47,16 @@ export const AdvancedTradeOptions = () => { useAppSelector(BonsaiHelpers.currentMarket.stableMarketInfo) ); - const { execution, goodTil, postOnly, reduceOnly, timeInForce, stopLossOrder, takeProfitOrder } = - inputTradeData; + const { + execution, + goodTil, + postOnly, + reduceOnly, + timeInForce, + stopLossOrder, + takeProfitOrder, + type, + } = inputTradeData; const { stopLossOrder: stopLossSummary, takeProfitOrder: takeProfitSummary } = orEmptyObj( currentTradeFormSummary.triggersSummary ); @@ -90,6 +99,181 @@ export const AdvancedTradeOptions = () => { return undefined; } + const fullContents = ( +
+ {needsTimeRow && ( + <$AdvancedInputsRow> + {hasTimeInForce && timeInForce != null && ( + <$SelectMenu + value={timeInForce} + onValueChange={(selectedTimeInForceOption: string) => { + if (!selectedTimeInForceOption) { + return; + } + dispatch(tradeFormActions.setTimeInForce(selectedTimeInForceOption as TimeInForce)); + }} + label={stringGetter({ key: STRING_KEYS.TIME_IN_FORCE })} + > + {timeInForceOptions.map(({ value, stringKey }) => ( + <$SelectItem + key={value} + value={value} + label={stringGetter({ key: stringKey as StringKey })} + /> + ))} + + )} + {showGoodTil && ( + <$FormInput + id="trade-good-til-time" + type={InputType.Number} + decimals={INTEGER_DECIMALS} + label={stringGetter({ + key: hasTimeInForce ? STRING_KEYS.TIME : STRING_KEYS.GOOD_TIL_TIME, + })} + onChange={({ value }: NumberFormatValues) => { + dispatch( + tradeFormActions.setGoodTilTime({ duration: value, unit: unit ?? TimeUnit.DAY }) + ); + }} + value={duration ?? ''} + slotRight={ + unit != null && ( + <$InnerSelectMenu + value={unit} + onValueChange={(goodTilTimeTimescale: string) => { + if (!goodTilTimeTimescale) { + return; + } + dispatch( + tradeFormActions.setGoodTilTime({ + duration: duration ?? '', + unit: goodTilTimeTimescale as TimeUnit, + }) + ); + }} + > + {Object.values(TimeUnitShort).map((goodTilTimeTimescale: TimeUnitShort) => ( + <$InnerSelectItem + key={goodTilTimeTimescale} + value={goodTilTimeTimescale} + label={stringGetter({ + key: GOOD_TIL_TIME_TIMESCALE_STRINGS[goodTilTimeTimescale], + })} + /> + ))} + + ) + } + /> + )} + + )} + {needsExecution && ( + <> + {executionOptions.length > 0 && execution != null && ( + <$SelectMenu + value={execution} + label={stringGetter({ key: STRING_KEYS.EXECUTION })} + onValueChange={(selectedExecution: string) => { + if (!selectedExecution) { + return; + } + dispatch(tradeFormActions.setExecution(selectedExecution as ExecutionType)); + }} + > + {executionOptions.map(({ value, stringKey }) => ( + <$SelectItem + key={value} + value={value} + label={stringGetter({ key: stringKey as StringKey })} + /> + ))} + + )} + {shouldShowReduceOnly && ( + dispatch(tradeFormActions.setReduceOnly(checked))} + id="reduce-only" + label={ + + + {stringGetter({ key: STRING_KEYS.REDUCE_ONLY })} + + + } + /> + )} + {shouldShowPostOnly && ( + dispatch(tradeFormActions.setPostOnly(checked))} + id="post-only" + label={ + + {stringGetter({ key: STRING_KEYS.POST_ONLY })} + + } + /> + )} + + )} + {needsTriggers && ( +
+ + dispatch(checked ? tradeFormActions.showTriggers() : tradeFormActions.hideTriggers()) + } + id="show-trigger-orders" + label={stringGetter({ key: STRING_KEYS.TAKE_PROFIT_STOP_LOSS })} + /> + {triggerOrdersChecked && ( +
+ + +
+ )} +
+ )} +
+ ); + + if (type === TradeFormType.MARKET) { + return
{fullContents}
; + } return ( <$Collapsible defaultOpen={!isTablet} @@ -97,177 +281,7 @@ export const AdvancedTradeOptions = () => { triggerIconSide="right" fullWidth > -
- {needsTimeRow && ( - <$AdvancedInputsRow> - {hasTimeInForce && timeInForce != null && ( - <$SelectMenu - value={timeInForce} - onValueChange={(selectedTimeInForceOption: string) => { - if (!selectedTimeInForceOption) { - return; - } - dispatch( - tradeFormActions.setTimeInForce(selectedTimeInForceOption as TimeInForce) - ); - }} - label={stringGetter({ key: STRING_KEYS.TIME_IN_FORCE })} - > - {timeInForceOptions.map(({ value, stringKey }) => ( - <$SelectItem - key={value} - value={value} - label={stringGetter({ key: stringKey as StringKey })} - /> - ))} - - )} - {showGoodTil && ( - <$FormInput - id="trade-good-til-time" - type={InputType.Number} - decimals={INTEGER_DECIMALS} - label={stringGetter({ - key: hasTimeInForce ? STRING_KEYS.TIME : STRING_KEYS.GOOD_TIL_TIME, - })} - onChange={({ value }: NumberFormatValues) => { - dispatch( - tradeFormActions.setGoodTilTime({ duration: value, unit: unit ?? TimeUnit.DAY }) - ); - }} - value={duration ?? ''} - slotRight={ - unit != null && ( - <$InnerSelectMenu - value={unit} - onValueChange={(goodTilTimeTimescale: string) => { - if (!goodTilTimeTimescale) { - return; - } - dispatch( - tradeFormActions.setGoodTilTime({ - duration: duration ?? '', - unit: goodTilTimeTimescale as TimeUnit, - }) - ); - }} - > - {Object.values(TimeUnitShort).map((goodTilTimeTimescale: TimeUnitShort) => ( - <$InnerSelectItem - key={goodTilTimeTimescale} - value={goodTilTimeTimescale} - label={stringGetter({ - key: GOOD_TIL_TIME_TIMESCALE_STRINGS[goodTilTimeTimescale], - })} - /> - ))} - - ) - } - /> - )} - - )} - {needsExecution && ( - <> - {executionOptions.length > 0 && execution != null && ( - <$SelectMenu - value={execution} - label={stringGetter({ key: STRING_KEYS.EXECUTION })} - onValueChange={(selectedExecution: string) => { - if (!selectedExecution) { - return; - } - dispatch(tradeFormActions.setExecution(selectedExecution as ExecutionType)); - }} - > - {executionOptions.map(({ value, stringKey }) => ( - <$SelectItem - key={value} - value={value} - label={stringGetter({ key: stringKey as StringKey })} - /> - ))} - - )} - {shouldShowReduceOnly && ( - dispatch(tradeFormActions.setReduceOnly(checked))} - id="reduce-only" - label={ - - {stringGetter({ key: STRING_KEYS.REDUCE_ONLY })} - - } - /> - )} - {shouldShowPostOnly && ( - dispatch(tradeFormActions.setPostOnly(checked))} - id="post-only" - label={ - - {stringGetter({ key: STRING_KEYS.POST_ONLY })} - - } - /> - )} - - )} - {needsTriggers && ( -
- - dispatch( - checked ? tradeFormActions.showTriggers() : tradeFormActions.hideTriggers() - ) - } - id="show-trigger-orders" - label={stringGetter({ key: STRING_KEYS.TAKE_PROFIT_STOP_LOSS })} - /> - {triggerOrdersChecked && ( -
- - -
- )} -
- )} -
+ {fullContents} ); }; @@ -280,8 +294,6 @@ const $Collapsible = styled(Collapsible)` font: var(--font-small-book); outline: none; - - margin: -0.5rem 0; `; const $SelectMenu = styled(SelectMenu)` ${formMixins.inputSelectMenu} diff --git a/src/views/forms/TradeForm/AmountCloseInput.tsx b/src/views/forms/TradeForm/AllocationSlider.tsx similarity index 61% rename from src/views/forms/TradeForm/AmountCloseInput.tsx rename to src/views/forms/TradeForm/AllocationSlider.tsx index 74f60c5047..b5b071e615 100644 --- a/src/views/forms/TradeForm/AmountCloseInput.tsx +++ b/src/views/forms/TradeForm/AllocationSlider.tsx @@ -1,71 +1,64 @@ import styled from 'styled-components'; -import { STRING_KEYS } from '@/constants/localization'; - import { useQuickUpdatingState } from '@/hooks/useQuickUpdatingState'; -import { useStringGetter } from '@/hooks/useStringGetter'; import breakpoints from '@/styles/breakpoints'; import { formMixins } from '@/styles/formMixins'; import { Input, InputType } from '@/components/Input'; import { Slider } from '@/components/Slider'; -import { WithLabel } from '@/components/WithLabel'; import { mapIfPresent } from '@/lib/do'; import { MustBigNumber } from '@/lib/numbers'; -export const AmountCloseInput = ({ - amountClosePercentInput, - setAmountCloseInput, +export const AllocationSlider = ({ + allocationPercentInput, + setAllocationInput, }: { - amountClosePercentInput: string | undefined; - setAmountCloseInput: (val: string | undefined) => void; + allocationPercentInput: string | undefined; + setAllocationInput: (val: string | undefined) => void; }) => { const { - value: amountClose, - setValue: setAmountClose, - commitValue: commitAmountClose, + value: allocation, + setValue: setAllocation, + commitValue: commitAllocation, } = useQuickUpdatingState({ - setValueSlow: setAmountCloseInput, - slowValue: amountClosePercentInput, + setValueSlow: setAllocationInput, + slowValue: allocationPercentInput, debounceMs: 100, }); const onSliderDrag = ([newValue]: number[]) => { const newValueString = mapIfPresent(newValue, (lev) => MustBigNumber(lev).toFixed(0)); - setAmountClose(newValueString ?? ''); + setAllocation(newValueString ?? ''); }; const commitValue = (newValue: string | undefined) => { - commitAmountClose(newValue); + commitAllocation(newValue); }; const onValueCommit = ([newValue]: number[]) => { commitValue(MustBigNumber(newValue).toFixed(0)); }; - const stringGetter = useStringGetter(); return ( <$InputContainer> - <$WithLabel - label={
{stringGetter({ key: STRING_KEYS.AMOUNT_CLOSE })}
} - > - <$AmountCloseSlider - label={stringGetter({ key: STRING_KEYS.AMOUNT_CLOSE })} +
+ <$AllocationSlider + label="Allocation" min={0} max={100} step={0.1} - value={MustBigNumber(amountClose).toNumber()} + value={MustBigNumber(allocation).toNumber()} onSliderDrag={onSliderDrag} onValueCommit={onValueCommit} /> - +
<$InnerInputContainer> { commitValue(formattedValue); @@ -81,23 +74,17 @@ const $InputContainer = styled.div` --input-height: 3.5rem; --input-backgroundColor: none; - padding: var(--form-input-paddingY) var(--form-input-paddingX); - @media ${breakpoints.tablet} { --input-height: 4rem; } `; -const $WithLabel = styled(WithLabel)` - ${formMixins.inputLabel} -`; - const $InnerInputContainer = styled.div` ${formMixins.inputContainer} --input-backgroundColor: var(--color-layer-4); --input-borderColor: none; --input-height: 2.25rem; - --input-width: 5rem; + --input-width: 4rem; margin-left: 0.25rem; @@ -111,7 +98,7 @@ const $InnerInputContainer = styled.div` } `; -const $AmountCloseSlider = styled(Slider)` +const $AllocationSlider = styled(Slider)` height: 1.375rem; --slider-track-background: linear-gradient( 90deg, diff --git a/src/views/forms/TradeForm/MarginAndLeverageButtons.tsx b/src/views/forms/TradeForm/MarginAndLeverageButtons.tsx index e1e4b9adf9..52b70107cd 100644 --- a/src/views/forms/TradeForm/MarginAndLeverageButtons.tsx +++ b/src/views/forms/TradeForm/MarginAndLeverageButtons.tsx @@ -17,12 +17,13 @@ export const MarginAndLeverageButtons = ({ className }: StyleProps) => { }; const $MarginAndLeverageButtons = styled.div` + height: 2.625rem; display: flex; gap: 0.5rem; - abbr, - button, - span { + > abbr, + > button, + > span { ${layoutMixins.flexExpandToSpace} height: 2.25rem; flex-basis: 100%; diff --git a/src/views/forms/TradeForm/MarginModeSelector.tsx b/src/views/forms/TradeForm/MarginModeSelector.tsx index 65c353e44c..69d6322a4a 100644 --- a/src/views/forms/TradeForm/MarginModeSelector.tsx +++ b/src/views/forms/TradeForm/MarginModeSelector.tsx @@ -2,18 +2,23 @@ import { MarginMode } from '@/bonsai/forms/trade/types'; import { BonsaiHelpers } from '@/bonsai/ontology'; import styled from 'styled-components'; +import { ButtonSize } from '@/constants/buttons'; +import { DialogTypes } from '@/constants/dialogs'; import { STRING_KEYS } from '@/constants/localization'; import { useStringGetter } from '@/hooks/useStringGetter'; import breakpoints from '@/styles/breakpoints'; +import { Button } from '@/components/Button'; import { Icon, IconName } from '@/components/Icon'; +import { Output, OutputType, ShowSign } from '@/components/Output'; import { ToggleGroup } from '@/components/ToggleGroup'; import { WithTooltip } from '@/components/WithTooltip'; import { calculateCanAccountTrade } from '@/state/accountCalculators'; import { useAppDispatch, useAppSelector } from '@/state/appTypes'; +import { openDialog } from '@/state/dialogs'; import { tradeFormActions } from '@/state/tradeForm'; import { getTradeFormSummary, getTradeFormValues } from '@/state/tradeFormSelectors'; @@ -48,9 +53,6 @@ export const MarginModeSelector = ({ className }: { className?: string }) => { const selector = ( <$MarginModeSelector tw="flex flex-1 items-center justify-between"> - {stringGetter({ - key: STRING_KEYS.MARGIN_MODE, - })} <$ToggleGroup disabled={!canAccountTrade || !needsMarginMode} withSeparators @@ -59,17 +61,21 @@ export const MarginModeSelector = ({ className }: { className?: string }) => { { value: MarginMode.CROSS, label: showMarginModeUnToggleableTooltip ? ( - stringGetter({ - key: STRING_KEYS.CROSS, - }) + + {stringGetter({ + key: STRING_KEYS.CROSS, + })} + ) : ( - {stringGetter({ - key: STRING_KEYS.CROSS, - })} + + {stringGetter({ + key: STRING_KEYS.CROSS, + })} + ), disabled: !needsMarginMode && marginMode !== MarginMode.CROSS, @@ -77,17 +83,21 @@ export const MarginModeSelector = ({ className }: { className?: string }) => { { value: MarginMode.ISOLATED, label: showMarginModeUnToggleableTooltip ? ( - stringGetter({ - key: STRING_KEYS.ISOLATED, - }) + + {stringGetter({ + key: STRING_KEYS.ISOLATED, + })} + ) : ( - {stringGetter({ - key: STRING_KEYS.ISOLATED, - })} + + {stringGetter({ + key: STRING_KEYS.ISOLATED, + })} + ), disabled: !needsMarginMode && marginMode !== MarginMode.ISOLATED, @@ -99,13 +109,45 @@ export const MarginModeSelector = ({ className }: { className?: string }) => { ); - return showMarginModeUnToggleableTooltip ? ( + const selectorWithTooltip = showMarginModeUnToggleableTooltip ? ( <$WarningTooltip className={className} slotTooltip={warningTooltip}> {selector} ) : ( selector ); + + const currentMarket = useAppSelector(BonsaiHelpers.currentMarket.stableMarketInfo)?.ticker; + const effectiveSelectedLeverage = useAppSelector( + BonsaiHelpers.currentMarket.effectiveSelectedLeverage + ); + + const leverageButton = ( + + ); + return ( +
+ {selectorWithTooltip} + {leverageButton} +
+ ); }; const $MarginModeSelector = styled.div` diff --git a/src/views/forms/TradeForm/PlaceOrderButtonAndReceipt.tsx b/src/views/forms/TradeForm/PlaceOrderButtonAndReceipt.tsx index 763052498a..5756d32838 100644 --- a/src/views/forms/TradeForm/PlaceOrderButtonAndReceipt.tsx +++ b/src/views/forms/TradeForm/PlaceOrderButtonAndReceipt.tsx @@ -1,6 +1,6 @@ import { useState } from 'react'; -import { MarginMode, TradeFormSummary } from '@/bonsai/forms/trade/types'; +import { TradeFormSummary } from '@/bonsai/forms/trade/types'; import { BonsaiHelpers } from '@/bonsai/ontology'; import styled from 'styled-components'; @@ -25,7 +25,7 @@ import { Button } from '@/components/Button'; import { DetailsItem } from '@/components/Details'; import { DiffOutput } from '@/components/DiffOutput'; import { Icon, IconName } from '@/components/Icon'; -import { Output, OutputType, ShowSign } from '@/components/Output'; +import { Output, OutputType } from '@/components/Output'; import { WithSeparators } from '@/components/Separator'; import { ToggleButton } from '@/components/ToggleButton'; import { TradeFeeDiscountTag } from '@/components/TradeFeeDiscountTag'; @@ -37,12 +37,10 @@ import { calculateCanAccountTrade } from '@/state/accountCalculators'; import { getSubaccountId } from '@/state/accountInfoSelectors'; import { useAppDispatch, useAppSelector } from '@/state/appTypes'; import { openDialog } from '@/state/dialogs'; -import { getTradeFormValues } from '@/state/tradeFormSelectors'; import { getDisplayableAssetFromBaseAsset } from '@/lib/assetUtils'; import { isTruthy } from '@/lib/isTruthy'; -import { nullIfZero } from '@/lib/numbers'; -import { calculateCrossPositionMargin, getDoubleValuesHasDiff } from '@/lib/tradeData'; +import { getDoubleValuesHasDiff } from '@/lib/tradeData'; import { orEmptyObj } from '@/lib/typeUtils'; type ConfirmButtonConfig = { @@ -92,19 +90,12 @@ export const PlaceOrderButtonAndReceipt = ({ BonsaiHelpers.currentMarket.marketInfo )?.marketFeeDiscountMultiplier; - const { - liquidationPrice, - leverage, - notional: notionalTotal, - adjustedImf, - marginValueMaintenance, - } = orEmptyObj(summary.accountDetailsBefore?.position); + const { liquidationPrice, marginValueInitialFromSelectedLeverage } = orEmptyObj( + summary.accountDetailsBefore?.position + ); const postOrderPositionData = orEmptyObj(summary.accountDetailsAfter?.position); - const tradeValues = useAppSelector(getTradeFormValues); - const { marginMode } = tradeValues; - const [isReceiptOpen, setIsReceiptOpen] = useState(true); const hasMissingData = subaccountNumber === undefined; @@ -117,43 +108,17 @@ export const PlaceOrderButtonAndReceipt = ({ const areInputsFilled = tradePayload != null; const renderMarginValue = () => { - if (marginMode === MarginMode.CROSS) { - const currentCrossMargin = nullIfZero( - calculateCrossPositionMargin({ - notionalTotal: notionalTotal?.toNumber(), - adjustedImf: adjustedImf?.toNumber(), - }) - ); - - const postOrderCrossMargin = nullIfZero( - calculateCrossPositionMargin({ - notionalTotal: postOrderPositionData.notional?.toNumber(), - adjustedImf: postOrderPositionData.adjustedImf?.toNumber(), - }) - ); - - return ( - - ); - } - return ( - {stringGetter({ key: STRING_KEYS.POSITION_LEVERAGE })} - - ), - value: ( - - ), - }, { key: 'fee', label: ( diff --git a/src/views/forms/TradeForm/TargetLeverageInput.tsx b/src/views/forms/TradeForm/TargetLeverageInput.tsx deleted file mode 100644 index 48582f6f7c..0000000000 --- a/src/views/forms/TradeForm/TargetLeverageInput.tsx +++ /dev/null @@ -1,163 +0,0 @@ -import { useCallback, useMemo } from 'react'; - -import { BonsaiHelpers } from '@/bonsai/ontology'; -import styled from 'styled-components'; - -import { STRING_KEYS } from '@/constants/localization'; -import { LEVERAGE_DECIMALS } from '@/constants/numbers'; - -import { useQuickUpdatingState } from '@/hooks/useQuickUpdatingState'; -import { useStringGetter } from '@/hooks/useStringGetter'; - -import breakpoints from '@/styles/breakpoints'; -import { formMixins } from '@/styles/formMixins'; - -import { Input, InputType } from '@/components/Input'; -import { Slider } from '@/components/Slider'; -import { WithLabel } from '@/components/WithLabel'; -import { WithTooltip } from '@/components/WithTooltip'; - -import { useAppDispatch, useAppSelector } from '@/state/appTypes'; -import { tradeFormActions } from '@/state/tradeForm'; -import { getTradeFormValues } from '@/state/tradeFormSelectors'; - -import { mapIfPresent } from '@/lib/do'; -import { calculateMarketMaxLeverage } from '@/lib/marketsHelpers'; -import { AttemptBigNumber, MaybeBigNumber, MustBigNumber } from '@/lib/numbers'; -import { orEmptyObj } from '@/lib/typeUtils'; - -export const TargetLeverageInput = () => { - const stringGetter = useStringGetter(); - const dispatch = useAppDispatch(); - - const { targetLeverage } = useAppSelector(getTradeFormValues); - const { initialMarginFraction, effectiveInitialMarginFraction } = orEmptyObj( - useAppSelector(BonsaiHelpers.currentMarket.stableMarketInfo) - ); - - const setLeverageSlow = useCallback( - (newLeverageString: string | undefined) => { - dispatch(tradeFormActions.setTargetLeverage(newLeverageString ?? '')); - }, - [dispatch] - ); - - const { - value: leverage, - setValue: setLeverage, - commitValue: commitLeverageState, - } = useQuickUpdatingState({ - setValueSlow: setLeverageSlow, - slowValue: targetLeverage ?? '', - debounceMs: 100, - }); - - const maxLeverage = useMemo(() => { - return calculateMarketMaxLeverage({ - initialMarginFraction: MaybeBigNumber(initialMarginFraction)?.toNumber(), - effectiveInitialMarginFraction, - }); - }, [initialMarginFraction, effectiveInitialMarginFraction]); - - const onSliderDrag = ([newLeverage]: number[]) => { - const newLeverageString = mapIfPresent(newLeverage, (lev) => - MustBigNumber(lev).toFixed(LEVERAGE_DECIMALS) - ); - setLeverage(newLeverageString); - }; - - const commitLeverage = (newLeverage: string) => { - commitLeverageState(newLeverage); - }; - - const onValueCommit = ([newLeverage]: number[]) => { - const newLeverageString = mapIfPresent(newLeverage, (lev) => - MustBigNumber(lev).toFixed(LEVERAGE_DECIMALS) - ); - commitLeverage(newLeverageString ?? ''); - }; - - return ( - <$InputContainer> - <$WithLabel - label={ -
- - {stringGetter({ key: STRING_KEYS.TARGET_LEVERAGE })} - -
- } - > - <$LeverageSlider - label="TargetLeverage" - min={1} - max={maxLeverage} - value={MustBigNumber(leverage).abs().toNumber()} - onSliderDrag={onSliderDrag} - onValueCommit={onValueCommit} - /> - - <$InnerInputContainer> - { - commitLeverage(formattedValue); - }} - /> - - - ); -}; - -const $InputContainer = styled.div` - ${formMixins.inputContainer} - --input-height: 3.5rem; - --input-backgroundColor: none; - - padding: var(--form-input-paddingY) var(--form-input-paddingX); - - @media ${breakpoints.tablet} { - --input-height: 4rem; - } -`; - -const $WithLabel = styled(WithLabel)` - ${formMixins.inputLabel} -`; - -const $InnerInputContainer = styled.div` - ${formMixins.inputContainer} - --input-backgroundColor: var(--color-layer-4); - --input-borderColor: none; - --input-height: 2.25rem; - --input-width: 5rem; - - margin-left: 0.25rem; - - input { - text-align: end; - padding: 0 var(--form-input-paddingX); - } - - @media ${breakpoints.tablet} { - --input-height: 2.5rem; - } -`; - -const $LeverageSlider = styled(Slider)` - height: 1.375rem; - --slider-track-background: linear-gradient( - 90deg, - var(--color-layer-7) 0%, - var(--color-text-2) 100% - ); -`; diff --git a/src/views/forms/TradeForm/TradeSizeInputs.tsx b/src/views/forms/TradeForm/TradeSizeInputs.tsx index c4e8e72fd8..76a9013e08 100644 --- a/src/views/forms/TradeForm/TradeSizeInputs.tsx +++ b/src/views/forms/TradeForm/TradeSizeInputs.tsx @@ -2,6 +2,7 @@ import { useCallback, useMemo } from 'react'; import { OrderSizeInputs } from '@/bonsai/forms/trade/types'; import { BonsaiHelpers } from '@/bonsai/ontology'; +import { BigNumber } from 'bignumber.js'; import { debounce } from 'lodash'; import styled from 'styled-components'; import tw from 'twin.macro'; @@ -37,12 +38,10 @@ import { getTradeFormSummary, getTradeFormValues } from '@/state/tradeFormSelect import { getDisplayableAssetFromBaseAsset } from '@/lib/assetUtils'; import { mapIfPresent } from '@/lib/do'; -import { AttemptBigNumber, MaybeBigNumber, MustBigNumber } from '@/lib/numbers'; +import { AttemptBigNumber, MaybeBigNumber } from '@/lib/numbers'; import { orEmptyObj } from '@/lib/typeUtils'; -import { AmountCloseInput } from './AmountCloseInput'; -import { MarketLeverageInput } from './MarketLeverageInput'; -import { TargetLeverageInput } from './TargetLeverageInput'; +import { AllocationSlider } from './AllocationSlider'; export const TradeSizeInputs = () => { const dispatch = useAppDispatch(); @@ -59,7 +58,7 @@ export const TradeSizeInputs = () => { const effectiveSizes = orEmptyObj(tradeSummary.tradeInfo.inputSummary.size); - const { showLeverage, showTargetLeverage, showAmountClose } = tradeSummary.options; + const { showAllocationSlider } = tradeSummary.options; const decimals = stepSizeDecimals ?? TOKEN_DECIMALS; @@ -187,47 +186,27 @@ export const TradeSizeInputs = () => { } slotRight={inputToggleButton()} type={inputConfig.type} - value={inputConfig.value ?? ''} + value={inputConfig.value} /> ); return ( -
+
{sizeInput} - {showLeverage && ( - { - dispatch(tradeFormActions.setSizeLeverageSigned(value)); - }} - /> - )} - {showTargetLeverage && } - {showAmountClose && ( - (positionSize > 0 ? tSize / positionSize : 0) - ) - ) - ) - ?.times(100) - .toFixed(0)} - setAmountCloseInput={(value: string | undefined) => { + setAllocationInput={(value: string | undefined) => { dispatch( tradeFormActions.setSizeAvailablePercent( mapIfPresent(value, (v) => MaybeBigNumber(v)?.div(100).toFixed(2)) ?? '' diff --git a/src/views/tables/PositionsTable.tsx b/src/views/tables/PositionsTable.tsx index 1896c1f359..ed238c73b4 100644 --- a/src/views/tables/PositionsTable.tsx +++ b/src/views/tables/PositionsTable.tsx @@ -48,6 +48,7 @@ import { } from '../../lib/enumToStringKeyHelpers'; import { CloseAllPositionsButton } from './PositionsTable/CloseAllPositionsButton'; import { PositionsActionsCell } from './PositionsTable/PositionsActionsCell'; +import { PositionsLeverageCell } from './PositionsTable/PositionsLeverageCell'; import { PositionsMarginCell } from './PositionsTable/PositionsMarginCell'; import { PositionsTriggersCell } from './PositionsTable/PositionsTriggersCell'; @@ -107,7 +108,7 @@ const getPositionsTableColumnDef = ({ columnKey: 'details', getCellValue: (row) => row.uniqueId, label: stringGetter({ key: STRING_KEYS.DETAILS }), - renderCell: ({ marketSummary, leverage, signedSize, side }) => ( + renderCell: ({ marketSummary, effectiveSelectedLeverage, signedSize, side }) => ( @ <$HighlightOutput type={OutputType.Multiple} - value={leverage} + value={effectiveSelectedLeverage} showSign={ShowSign.None} />
@@ -226,13 +227,15 @@ const getPositionsTableColumnDef = ({ }, [PositionsTableColumnKey.Leverage]: { columnKey: 'leverage', - getCellValue: (row) => row.leverage?.toNumber(), + getCellValue: (row) => row.effectiveSelectedLeverage.toNumber(), label: stringGetter({ key: STRING_KEYS.LEVERAGE }), hideOnBreakpoint: MediaQueryKeys.isMobile, - renderCell: ({ leverage }) => ( - - - + isActionable: true, + renderCell: ({ effectiveSelectedLeverage, market }) => ( + ), }, [PositionsTableColumnKey.Type]: { @@ -282,7 +285,7 @@ const getPositionsTableColumnDef = ({ }, [PositionsTableColumnKey.Margin]: { columnKey: 'margin', - getCellValue: (row) => row.marginValueInitial.toNumber(), + getCellValue: (row) => row.marginValueInitialFromSelectedLeverage.toNumber(), label: stringGetter({ key: STRING_KEYS.MARGIN }), hideOnBreakpoint: MediaQueryKeys.isMobile, isActionable: true, @@ -403,7 +406,7 @@ const getPositionsTableColumnDef = ({ market, marketSummary, assetId, - leverage, + effectiveSelectedLeverage, side, entryPrice, updatedUnrealizedPnl: unrealizedPnl, @@ -412,7 +415,7 @@ const getPositionsTableColumnDef = ({ marketId={market} assetId={assetId} side={side} - leverage={leverage} + leverage={effectiveSelectedLeverage} oraclePrice={MaybeBigNumber(marketSummary?.oraclePrice)} entryPrice={entryPrice} unrealizedPnl={unrealizedPnl} diff --git a/src/views/tables/PositionsTable/PositionsLeverageCell.tsx b/src/views/tables/PositionsTable/PositionsLeverageCell.tsx new file mode 100644 index 0000000000..44475ddc6e --- /dev/null +++ b/src/views/tables/PositionsTable/PositionsLeverageCell.tsx @@ -0,0 +1,59 @@ +import BigNumber from 'bignumber.js'; +import styled from 'styled-components'; + +import { ButtonShape, ButtonSize } from '@/constants/buttons'; +import { DialogTypes } from '@/constants/dialogs'; +import { STRING_KEYS } from '@/constants/localization'; + +import { useStringGetter } from '@/hooks/useStringGetter'; + +import { IconName } from '@/components/Icon'; +import { IconButton } from '@/components/IconButton'; +import { Output, OutputType, ShowSign } from '@/components/Output'; +import { TableCell } from '@/components/Table/TableCell'; +import { WithTooltip } from '@/components/WithTooltip'; + +import { useAppDispatch } from '@/state/appTypes'; +import { openDialog } from '@/state/dialogs'; + +type ElementProps = { + marketId: string; + effectiveSelectedLeverage: BigNumber; +}; + +export const PositionsLeverageCell = ({ marketId, effectiveSelectedLeverage }: ElementProps) => { + const stringGetter = useStringGetter(); + const dispatch = useAppDispatch(); + + return ( + + <$EditButton + key="edit-leverage" + tw="mt-0.125" + iconName={IconName.Pencil} + shape={ButtonShape.Square} + size={ButtonSize.XSmall} + onClick={() => dispatch(openDialog(DialogTypes.SetMarketLeverage({ marketId })))} + /> + + } + > + + + ); +}; + +const $EditButton = styled(IconButton)` + --button-textColor: var(--color-text-0); + --button-hover-textColor: var(--color-text-1); + --button-backgroundColor: transparent; + --button-border: none; + --button-width: min-content; +`; diff --git a/src/views/tables/PositionsTable/PositionsMarginCell.tsx b/src/views/tables/PositionsTable/PositionsMarginCell.tsx index aa944cf06a..0a8e0a35ac 100644 --- a/src/views/tables/PositionsTable/PositionsMarginCell.tsx +++ b/src/views/tables/PositionsTable/PositionsMarginCell.tsx @@ -35,7 +35,6 @@ export const PositionsMarginCell = ({ position }: PositionsMarginCellProps) => { shape={ButtonShape.Square} size={ButtonSize.XSmall} onClick={() => - // todo this handoff should be using uniqueid dispatch( openDialog(DialogTypes.AdjustIsolatedMargin({ positionId: position.uniqueId })) ) @@ -45,7 +44,27 @@ export const PositionsMarginCell = ({ position }: PositionsMarginCellProps) => { ) } > - + + {position.marginMode === 'ISOLATED' && + position.effectiveSelectedLeverage + .minus(position.leverage ?? 0) + .abs() + .gt(1) && ( +
+ ( + + ) +
+ )} ); }; From 3b840ba94f109413c40a1b4ba3688f2ba3536140 Mon Sep 17 00:00:00 2001 From: Sam-dYdX Date: Mon, 1 Dec 2025 14:27:39 -0500 Subject: [PATCH 10/11] fix: Hide the swap panel if the feature flag is not enabled (#2000) --- src/constants/statsig.ts | 1 + src/pages/token/Swap.tsx | 13 +++++++++++- src/pages/token/SwapAndStakingPanel.tsx | 27 ++++++++++++++++--------- 3 files changed, 31 insertions(+), 10 deletions(-) diff --git a/src/constants/statsig.ts b/src/constants/statsig.ts index 03405562b7..36f90d3143 100644 --- a/src/constants/statsig.ts +++ b/src/constants/statsig.ts @@ -15,6 +15,7 @@ export enum StatsigFlags { ffWithdrawRewrite = 'ff_withdraw_rewrite', ffSeptember2025Rewards = 'ff_rewards_sep_2025', ffTurnkeyWeb = 'ff_turnkey_web', + ffSwapEnabled = 'ff_swap_ui_web', abPopupDeposit = 'ab_popup_deposit', } diff --git a/src/pages/token/Swap.tsx b/src/pages/token/Swap.tsx index c1945479c7..7207263a22 100644 --- a/src/pages/token/Swap.tsx +++ b/src/pages/token/Swap.tsx @@ -145,10 +145,21 @@ export const Swap = () => { }, [quote, amount, mode, tokenBalances]); const usdcPerDydx = useMemo(() => { + if (quote) { + const usdcAmount = formatUnits( + BigInt(inputToken === 'usdc' ? quote.amountIn : quote.amountOut), + USDC_DECIMALS + ); + const dydxAmount = formatUnits( + BigInt(inputToken === 'dydx' ? quote.amountIn : quote.amountOut), + DYDX_DECIMALS + ); + return Number(usdcAmount) / Number(dydxAmount); + } if (!priceQuote) return undefined; return Number(formatUnits(BigInt(priceQuote.amountOut), USDC_DECIMALS)); - }, [priceQuote]); + }, [priceQuote, quote, inputToken]); const quotedAmount = useMemo(() => { if (!quote || !amount) return ''; diff --git a/src/pages/token/SwapAndStakingPanel.tsx b/src/pages/token/SwapAndStakingPanel.tsx index ace5ff4ba4..859b0880d0 100644 --- a/src/pages/token/SwapAndStakingPanel.tsx +++ b/src/pages/token/SwapAndStakingPanel.tsx @@ -3,7 +3,10 @@ import { useState } from 'react'; import styled from 'styled-components'; import { STRING_KEYS } from '@/constants/localization'; +import { isDev } from '@/constants/networks'; +import { StatsigFlags } from '@/constants/statsig'; +import { useStatsigGateValue } from '@/hooks/useStatsig'; import { useStringGetter } from '@/hooks/useStringGetter'; import { Panel } from '@/components/Panel'; @@ -13,20 +16,26 @@ import { Swap } from './Swap'; export const SwapAndStakingPanel = ({ className }: { className?: string }) => { const stringGetter = useStringGetter(); + const isSwapEnabledBase = useStatsigGateValue(StatsigFlags.ffSwapEnabled); + const isSwapEnabled = isDev || isSwapEnabledBase; - const [selectedTab, setSelectedTab] = useState<'swap' | 'stake'>('swap'); + const [selectedTab, setSelectedTab] = useState<'swap' | 'stake'>( + isSwapEnabled ? 'swap' : 'stake' + ); return ( - <$HeaderButton - onClick={() => setSelectedTab('swap')} - $isSelected={selectedTab === 'swap'} - type="button" - > - {stringGetter({ key: STRING_KEYS.SWAP })} - + {isSwapEnabled && ( + <$HeaderButton + onClick={() => setSelectedTab('swap')} + $isSelected={selectedTab === 'swap'} + type="button" + > + {stringGetter({ key: STRING_KEYS.SWAP })} + + )} <$HeaderButton onClick={() => setSelectedTab('stake')} $isSelected={selectedTab === 'stake'} @@ -37,7 +46,7 @@ export const SwapAndStakingPanel = ({ className }: { className?: string }) => { } > - {selectedTab === 'swap' && } + {isSwapEnabled && selectedTab === 'swap' && } {selectedTab === 'stake' && } ); From 01eb47d824dd2cac8340468d88a8c63416ee8ee7 Mon Sep 17 00:00:00 2001 From: Luka Buzaladze Date: Tue, 2 Dec 2025 09:48:48 -0500 Subject: [PATCH 11/11] feat: switch pnl trading comp page to liquidation rewards page (#2001) Co-authored-by: qardpeet --- package.json | 2 +- pnpm-lock.yaml | 8 +++---- src/hooks/rewards/util.ts | 8 +++---- .../token/CompetitionIncentivesPanel.tsx | 17 +++++--------- .../token/CompetitionLeaderboardPanel.tsx | 22 ------------------- 5 files changed, 15 insertions(+), 42 deletions(-) diff --git a/package.json b/package.json index 8ebf3f3a15..2fb29ddcf0 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "@cosmjs/tendermint-rpc": "^0.32.1", "@datadog/browser-logs": "^5.23.3", "@dydxprotocol/v4-client-js": "3.2.0", - "@dydxprotocol/v4-localization": "1.1.360", + "@dydxprotocol/v4-localization": "1.1.361", "@dydxprotocol/v4-proto": "^7.0.0-dev.0", "@emotion/is-prop-valid": "^1.3.0", "@hugocxl/react-to-image": "^0.0.9", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fe1df373b9..8132781cb3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -33,8 +33,8 @@ dependencies: specifier: 3.2.0 version: 3.2.0 '@dydxprotocol/v4-localization': - specifier: 1.1.360 - version: 1.1.360 + specifier: 1.1.361 + version: 1.1.361 '@dydxprotocol/v4-proto': specifier: ^7.0.0-dev.0 version: 7.0.5 @@ -1690,8 +1690,8 @@ packages: - utf-8-validate dev: false - /@dydxprotocol/v4-localization@1.1.360: - resolution: {integrity: sha512-7h5oap3VgSG3Z30UFcT5KZROPBNXqlWpIlJfpwTztkrXpbx+ToXomZSAX3MBOX2iPToDPlpoCbQj2lrOWiTtmw==} + /@dydxprotocol/v4-localization@1.1.361: + resolution: {integrity: sha512-Tq7KBxZ/H6RZgGwM3VQw7euQ7hgVR4FTymPYbU5AYV5pRXdLee5IkYKqkbc/SJBUsDuMaMNQ8WEARdeEaam5HA==} dev: false /@dydxprotocol/v4-proto@7.0.5: diff --git a/src/hooks/rewards/util.ts b/src/hooks/rewards/util.ts index a38fa79405..1a68e24dfa 100644 --- a/src/hooks/rewards/util.ts +++ b/src/hooks/rewards/util.ts @@ -29,8 +29,8 @@ export const CURRENT_SURGE_REWARDS_DETAILS = { endTime: '2025-12-31T23:59:59.000Z', // end of month }; -export const NOV_2025_COMPETITION_DETAILS = { - rewardAmount: '$250k', - rewardAmountUsd: 250_000, - endTime: '2025-11-30T23:59:59.000Z', // end of month +export const DEC_2025_COMPETITION_DETAILS = { + rewardAmount: '$1M', + rewardAmountUsd: 1_000_000, + endTime: '2025-12-31T23:59:59.000Z', // end of month }; diff --git a/src/pages/token/CompetitionIncentivesPanel.tsx b/src/pages/token/CompetitionIncentivesPanel.tsx index a959f1fdfb..2343b68056 100644 --- a/src/pages/token/CompetitionIncentivesPanel.tsx +++ b/src/pages/token/CompetitionIncentivesPanel.tsx @@ -9,13 +9,12 @@ import { isDev } from '@/constants/networks'; import { StatsigFlags } from '@/constants/statsig'; import { useChaosLabsPnlDistribution } from '@/hooks/rewards/hooks'; -import { NOV_2025_COMPETITION_DETAILS } from '@/hooks/rewards/util'; +import { DEC_2025_COMPETITION_DETAILS } from '@/hooks/rewards/util'; import { useAccounts } from '@/hooks/useAccounts'; import { useNow } from '@/hooks/useNow'; import { useStatsigGateValue } from '@/hooks/useStatsig'; import { useStringGetter } from '@/hooks/useStringGetter'; -import { ChaosLabsIcon } from '@/icons/chaos-labs'; import { layoutMixins } from '@/styles/layoutMixins'; import { Icon, IconName } from '@/components/Icon'; @@ -57,9 +56,9 @@ const September2025RewardsPanel = () => {
{stringGetter({ - key: STRING_KEYS.COMPETITION_HEADLINE_NOV_2025, + key: STRING_KEYS.COMPETITION_HEADLINE_DEC_2025, params: { - REWARD_AMOUNT: NOV_2025_COMPETITION_DETAILS.rewardAmount, + REWARD_AMOUNT: DEC_2025_COMPETITION_DETAILS.rewardAmount, }, })} @@ -71,12 +70,12 @@ const September2025RewardsPanel = () => { {stringGetter({ - key: STRING_KEYS.COMPETITION_BODY_NOV_2025, + key: STRING_KEYS.COMPETITION_BODY_DEC_2025, params: { - REWARD_AMOUNT: NOV_2025_COMPETITION_DETAILS.rewardAmount, + REWARD_AMOUNT: DEC_2025_COMPETITION_DETAILS.rewardAmount, }, })}{' '} - + {stringGetter({ key: STRING_KEYS.LEARN_MORE })} @@ -141,10 +140,6 @@ const Sept2025RewardsPanel = () => {
reward-stars
- -
- {stringGetter({ key: STRING_KEYS.POWERED_BY_ALL_CAPS })} -
); }; diff --git a/src/pages/token/CompetitionLeaderboardPanel.tsx b/src/pages/token/CompetitionLeaderboardPanel.tsx index 6bde7e3db8..bdc6384d4e 100644 --- a/src/pages/token/CompetitionLeaderboardPanel.tsx +++ b/src/pages/token/CompetitionLeaderboardPanel.tsx @@ -25,7 +25,6 @@ export enum RewardsLeaderboardTableColumns { Rank = 'Rank', Trader = 'Trader', PNL = 'PNL', - Rewards = 'Rewards', } export const CompetitionLeaderboardPanel = () => { @@ -81,10 +80,6 @@ export const CompetitionLeaderboardPanel = () => { key: 'pnl', displayLabel: stringGetter({ key: STRING_KEYS.PNL }), }, - { - key: 'dollarReward', - displayLabel: stringGetter({ key: STRING_KEYS.REWARDS }), - }, ], }); }; @@ -276,23 +271,6 @@ const getRewardsLeaderboardTableColumnDef = ({ /> ), }, - [RewardsLeaderboardTableColumns.Rewards]: { - columnKey: RewardsLeaderboardTableColumns.Rewards, - getCellValue: (row) => row.dollarReward, - label: ( -
- {stringGetter({ key: STRING_KEYS.PRIZE })} -
- ), - renderCell: ({ dollarReward, account }) => ( - - ), - }, } satisfies Record> )[key], });