From 1d5dcb1452ad486618b977d37ccb073483dce5b2 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Fri, 10 May 2024 07:18:10 +1000 Subject: [PATCH 001/192] feat: move Swap/Limit and Buy/Sell controls to radio group controls --- .../RadioButtonGroupInput.tsx | 2 +- src/components/cards/LimitOrderCard.scss | 21 --- src/components/cards/LimitOrderCard.tsx | 128 +++++++----------- 3 files changed, 48 insertions(+), 103 deletions(-) diff --git a/src/components/RadioButtonGroupInput/RadioButtonGroupInput.tsx b/src/components/RadioButtonGroupInput/RadioButtonGroupInput.tsx index ba8abb11f..87c474fb9 100644 --- a/src/components/RadioButtonGroupInput/RadioButtonGroupInput.tsx +++ b/src/components/RadioButtonGroupInput/RadioButtonGroupInput.tsx @@ -72,7 +72,7 @@ function useSelectedButtonBackgroundMove( interface Props { className?: string; buttonClassName?: string; - values: { [value in T]: ReactNode } | Map | T[]; + values: { [value in T]: ReactNode } | Map | readonly T[]; value: T; onChange: (value: T) => void; } diff --git a/src/components/cards/LimitOrderCard.scss b/src/components/cards/LimitOrderCard.scss index 1213e564d..5f348e60b 100644 --- a/src/components/cards/LimitOrderCard.scss +++ b/src/components/cards/LimitOrderCard.scss @@ -1,27 +1,6 @@ @use '../../styles/mixins-vars/paddings.scss' as paddings; .limitorder-card { - .limitorder-type { - // style limit order type selection like pills - .tabs__nav { - margin-left: 0 !important; - margin-right: 0 !important; - border: 1px solid var(--page-card-border); - border-radius: paddings.$p-3; - overflow: hidden; - .tabs__nav-button { - flex: 1; - border-radius: 0; - padding: paddings.$p-3 0 !important; - font-size: 0.85em; - &.active { - color: hsla(165, 83%, 57%, 1); - font-weight: bold; - } - } - } - } - // override default select input styles .select-input { .select-input-selection { diff --git a/src/components/cards/LimitOrderCard.tsx b/src/components/cards/LimitOrderCard.tsx index b06a61431..680ee5399 100644 --- a/src/components/cards/LimitOrderCard.tsx +++ b/src/components/cards/LimitOrderCard.tsx @@ -3,7 +3,6 @@ import BigNumber from 'bignumber.js'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faSpinner } from '@fortawesome/free-solid-svg-icons'; import { - createContext, useCallback, useContext, useEffect, @@ -16,9 +15,6 @@ import type { MsgPlaceLimitOrder, } from '@duality-labs/neutronjs/types/codegen/neutron/dex/tx'; -import TabsCard from './TabsCard'; -import Tabs from '../Tabs'; - import { Token, getBaseDenomAmount, @@ -47,10 +43,10 @@ import { useRealtimePrice } from '../stats/hooks'; import RangeListSliderInput from '../inputs/RangeInput/RangeListSliderInput'; import { - LimitOrderContextProvider, LimitOrderFormContext, LimitOrderFormSetContext, } from './LimitOrderContext'; +import RadioButtonGroupInput from '../RadioButtonGroupInput/RadioButtonGroupInput'; import SelectInput from '../inputs/SelectInput'; import { timeUnits } from '../../lib/utils/time'; import { @@ -94,13 +90,6 @@ function formatNumericAmount(defaultValue = '') { }; } -const TabContext = createContext< - [ - tabIndex?: number, - setTabIndex?: React.Dispatch> - ] ->([]); - export default function LimitOrderCard({ tokenA, tokenB, @@ -109,82 +98,54 @@ export default function LimitOrderCard({ tokenB?: Token; }) { return ( - - { - return [ - { - nav: 'Buy', - Tab: () => , - }, - { - nav: 'Sell', - Tab: () => , - }, - ]; - }, [tokenA, tokenB])} - /> - +
+

Place Order

+ +
); } -function LimitOrderNav({ - tokenA, - tokenB, - sell = false, -}: { - tokenA?: Token; - tokenB?: Token; - sell?: boolean; -}) { - const [tabIndex, setTabIndex] = useContext(TabContext); - const tabs = useMemo(() => { - const props = { tokenA, tokenB, sell }; - return [ - { - nav: 'Limit', - Tab: () => , - }, - { - nav: 'Market', - Tab: () => , - }, - ]; - }, [tokenA, tokenB, sell]); +const modeTabs = ['Buy', 'Sell'] as const; +type ModeTab = typeof modeTabs[number]; - return ( -
- - (modeTabs[0]); + const [priceTab, setPriceTab] = useState(priceTabs[0]); + + const cardModeNav = ( +
+
+ + className="order-type-input mb-4" + values={priceTabs} + value={priceTab} + onChange={setPriceTab} + /> +
+
+ + className="order-type-input mb-4" + values={modeTabs} + value={modeTab} + onChange={setModeTab} /> - +
); -} - -const userBankBalanceRangePercentages = [0, 0.1, 0.25, 0.5, 0.75, 1]; -function LimitOrder({ - tokenA, - tokenB, - sell: sellMode = false, - showLimitPrice = false, -}: { - tokenA?: Token; - tokenB?: Token; - sell?: boolean; - showLimitPrice?: boolean; -}) { - const buyMode = !sellMode; + const showLimitPrice = priceTab === 'Limit'; + const buyMode = modeTab === 'Buy'; const [denomA, denomB] = [getTokenId(tokenA), getTokenId(tokenB)]; const formState = useContext(LimitOrderFormContext); @@ -853,7 +814,12 @@ function LimitOrder({ /> ); - return
{fieldset}
; + return ( + <> + {cardModeNav} +
{fieldset}
+ + ); } function NumericInputRow({ From cc01915358a54eced63079a6686b1fb1eeb886f6 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Fri, 10 May 2024 10:55:35 +1000 Subject: [PATCH 002/192] feat: remove "HALF" wallet amount shortcut button from token input --- src/components/TokenInputGroup/TokenInputGroup.tsx | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/components/TokenInputGroup/TokenInputGroup.tsx b/src/components/TokenInputGroup/TokenInputGroup.tsx index 3125bb145..eb3f4d1f6 100644 --- a/src/components/TokenInputGroup/TokenInputGroup.tsx +++ b/src/components/TokenInputGroup/TokenInputGroup.tsx @@ -105,18 +105,6 @@ export default function TokenInputGroup({ > MAX - )} Date: Fri, 10 May 2024 10:56:39 +1000 Subject: [PATCH 003/192] feat: allow custom header text on token input --- .../TokenInputGroup/TokenInputGroup.tsx | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/components/TokenInputGroup/TokenInputGroup.tsx b/src/components/TokenInputGroup/TokenInputGroup.tsx index eb3f4d1f6..fa03e0f9b 100644 --- a/src/components/TokenInputGroup/TokenInputGroup.tsx +++ b/src/components/TokenInputGroup/TokenInputGroup.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useMemo } from 'react'; +import React, { ReactNode, useCallback, useMemo } from 'react'; import BigNumber from 'bignumber.js'; import TokenPicker from '../TokenPicker'; @@ -29,6 +29,7 @@ interface InputGroupProps { disabled?: boolean; disabledInput?: boolean; disabledToken?: boolean; + header?: ReactNode; maxValue?: number; } @@ -46,6 +47,7 @@ export default function TokenInputGroup({ token, denom = token?.base, defaultAssetMode, + header, disabled = false, disabledInput = disabled, disabledToken = disabled, @@ -83,14 +85,15 @@ export default function TokenInputGroup({ .filter(Boolean) .join(' ')} > - {maxValue && ( -
- Available{' '} - {formatAmount(maxValue, { - useGrouping: true, - })} -
- )} + {header || + (maxValue && ( +
+ Available{' '} + {formatAmount(maxValue, { + useGrouping: true, + })} +
+ ))} {!disabledInput && token && maxValue && Number(maxValue) > 0 && (
); } @@ -117,12 +122,18 @@ type ModeTab = typeof modeTabs[number]; const priceTabs = ['Swap', 'Limit'] as const; type PriceTab = typeof priceTabs[number]; -const userBankBalanceRangePercentages = [0, 0.1, 0.25, 0.5, 0.75, 1]; - -function LimitOrder({ tokenA, tokenB }: { tokenA?: Token; tokenB?: Token }) { +function LimitOrder({ tokenA, tokenB }: { tokenA: Token; tokenB: Token }) { const [modeTab, setModeTab] = useState(modeTabs[0]); const [priceTab, setPriceTab] = useState(priceTabs[0]); + const formState = useContext(LimitOrderFormContext); + const formSetState = useContext(LimitOrderFormSetContext); + const switchModeTab = useCallback(() => { + // change tab and defined amount + setModeTab((mode) => (mode === 'Buy' ? 'Sell' : 'Buy')); + formSetState.setAmountInOut?.(([a, b]) => [b, a]); + }, [formSetState]); + const cardModeNav = (
@@ -138,7 +149,7 @@ function LimitOrder({ tokenA, tokenB }: { tokenA?: Token; tokenB?: Token }) { className="order-type-input mb-4" values={modeTabs} value={modeTab} - onChange={setModeTab} + onChange={switchModeTab} />
@@ -148,121 +159,44 @@ function LimitOrder({ tokenA, tokenB }: { tokenA?: Token; tokenB?: Token }) { const buyMode = modeTab === 'Buy'; const [denomA, denomB] = [getTokenId(tokenA), getTokenId(tokenB)]; - const formState = useContext(LimitOrderFormContext); - const formSetState = useContext(LimitOrderFormSetContext); - const tokenIn = !buyMode ? tokenB : tokenA; const tokenOut = buyMode ? tokenB : tokenA; - const [denomIn, denomOut] = [getTokenId(tokenIn), getTokenId(tokenOut)]; + const denomIn = getTokenId(tokenIn); const { data: userBalanceTokenIn, isLoading: isLoadingUserBalanceTokenIn } = useBankBalanceBaseAmount(denomIn); - const { data: userBalanceTokenOut } = useBankBalanceBaseAmount(denomOut); const { data: userBalanceTokenInDisplayAmount } = useBankBalanceDisplayAmount(denomIn); const { meta } = useTokenPairMapLiquidity([denomA, denomB]); + const setAmountIn = useCallback( + (v: string) => formSetState?.setAmountInOut?.([v, '']), + [formSetState] + ); + const setAmountOut = useCallback( + (v: string) => formSetState?.setAmountInOut?.(['', v]), + [formSetState] + ); + const [{ isValidating: isValidatingSwap, error }, swapRequest] = useSwap( [denomA, denomB].filter((denom): denom is string => !!denom) ); const { address, connectWallet } = useWeb3(); - const [tokenInBalanceFraction, setTokenInBalanceFraction] = - useState(); - - const buyAmountSimulatedMsgPlaceLimitOrder = useMemo< - MsgPlaceLimitOrder | undefined - >(() => { - if (address && userBalanceTokenIn && tokenInBalanceFraction !== undefined) { - const [denomIn, denomOut] = [getTokenId(tokenIn), getTokenId(tokenOut)]; - if ( - denomIn && - denomOut && - userBalanceTokenIn && - Number(userBalanceTokenIn) > 0 && - tokenInBalanceFraction > 0 - ) { - return { - amount_in: new BigNumber(userBalanceTokenIn) - .multipliedBy(tokenInBalanceFraction) - .toFixed(0), - token_in: denomIn, - token_out: denomOut, - creator: address, - receiver: address, - order_type: orderTypeEnum.IMMEDIATE_OR_CANCEL, - // don't use a limit to find target amount in - tick_index_in_to_out: Long.fromNumber(priceMaxIndex), - }; - } - } - }, [address, tokenIn, tokenInBalanceFraction, tokenOut, userBalanceTokenIn]); - const { - data: buyAmountSimulationResult, - isValidating: isValidatingBuyAmountSimulationResult, - } = useSimulatedLimitOrderResult(buyAmountSimulatedMsgPlaceLimitOrder, { - // invalidate the request cache when the liquidity height changes - keepPreviousData: - Number(buyAmountSimulatedMsgPlaceLimitOrder?.amount_in) > 0, - memo: `height of ${meta?.height}`, - }); - - const { - amountInDisplayAmount, - amountInBaseAmount, - amountOutDisplayAmount, - amountOutBaseAmount, - } = useMemo(() => { + const { amountInBaseAmount, amountOutBaseAmount } = useMemo(() => { if (tokenIn && tokenOut) { - // get amount in from input in sell mode or the slider in buy mode - const amountInBaseAmount = !buyMode - ? // get amount in from sell mode (in base amount to round input correctly) - getBaseDenomAmount(tokenIn, formState.amount || 0) - : // in buy mode get the input slider value only if defined - (tokenInBalanceFraction !== undefined && - new BigNumber(userBalanceTokenIn || 0) - .multipliedBy(tokenInBalanceFraction || 0) - .toFixed(0)) || - undefined; - // get amount out from buy mode - const amountOutBaseAmount = - (buyMode || undefined) && - (buyAmountSimulationResult || isValidatingBuyAmountSimulationResult - ? // if we have a buy simulation result then show it or loading state - buyAmountSimulationResult?.response?.taker_coin_out.amount ?? '' - : // else use value directly (in base amount to round input correctly) - getBaseDenomAmount(tokenOut, formState.amount || 0)); - // return converted values for convenience return { - amountInBaseAmount, - amountInDisplayAmount: !buyMode - ? formState.amount - : amountInBaseAmount && - getDisplayDenomAmount(tokenIn, amountInBaseAmount), - amountOutBaseAmount, - amountOutDisplayAmount: - buyAmountSimulationResult || isValidatingBuyAmountSimulationResult - ? amountOutBaseAmount && - getDisplayDenomAmount(tokenOut, amountOutBaseAmount, { - // output a little more rounded than usual for form inputs - fractionalDigits: 3, - significantDigits: 5, - }) - : formState.amount, + amountInBaseAmount: formState.amountIn + ? getBaseDenomAmount(tokenIn, formState.amountIn || 0) + : undefined, + amountOutBaseAmount: formState.amountOut + ? getBaseDenomAmount(tokenOut, formState.amountOut || 0) + : undefined, }; } return {}; - }, [ - buyAmountSimulationResult, - buyMode, - formState.amount, - isValidatingBuyAmountSimulationResult, - tokenInBalanceFraction, - tokenIn, - tokenOut, - userBalanceTokenIn, - ]); + }, [tokenIn, tokenOut, formState.amountIn, formState.amountOut]); const [, currentPrice] = useRealtimePrice(tokenIn, tokenOut); @@ -282,10 +216,7 @@ function LimitOrder({ tokenA, tokenB }: { tokenA?: Token; tokenB?: Token }) { // find amounts in/out for the order // in buy mode: buy the amount out with the user's available balance - const amountIn = - amountInBaseAmount || - // use bank balance in buy mode if amount was not defined by slider - (buyMode ? userBalanceTokenIn : undefined); + const amountIn = amountInBaseAmount || userBalanceTokenIn; // use amount out to set the order limit only if the amount in is not set const maxAmountOut = amountOutBaseAmount; @@ -346,7 +277,7 @@ function LimitOrder({ tokenA, tokenB }: { tokenA?: Token; tokenB?: Token }) { } // set an estimated max in instead for non-immediate orders else { - // note: this is a bit or an awkward estimation that goes from the + // note: this is a bit of an awkward estimation that goes from the // user's set limit to the current price const estimatedTokenIn = Number(maxAmountOut) * @@ -392,9 +323,7 @@ function LimitOrder({ tokenA, tokenB }: { tokenA?: Token; tokenB?: Token }) { // if the limit order payload hasn't changed then keep the previous data keepPreviousData: // don't keep when input goes to 0 - (Number(simulatedMsgPlaceLimitOrder?.amount_in) > 0 && - // if in buy amount calculation mode let it pass - buyAmountSimulationResult?.response !== undefined) || + Number(simulatedMsgPlaceLimitOrder?.amount_in) > 0 || // don't change if input exists and hasn't changed (simulatedMsgPlaceLimitOrder !== undefined && lastSimulatedMsgPlaceLimitOrder.current === @@ -554,7 +483,7 @@ function LimitOrder({ tokenA, tokenB }: { tokenA?: Token; tokenB?: Token }) { } }, [nativeChain, setChainFeeToken]); - const [lastKnownPrice, setLastKnownPrice] = useState(0); + const [, setLastKnownPrice] = useState(0); useEffect(() => { if (simulationResult?.response) { const price = new BigNumber(simulationResult.response.coin_in.amount).div( @@ -592,76 +521,8 @@ function LimitOrder({ tokenA, tokenB }: { tokenA?: Token; tokenB?: Token }) { // disable fieldset with no address because the estimation requires a signed client const fieldset = (
-
- { - formSetState.setAmount?.(value); - setTokenInBalanceFraction(undefined); - }} - suffix={tokenB?.symbol} - format={formatNumericAmount('')} - /> -
- { - const numericValue = Math.max(0, Math.min(1, Number(value) || 0)); - if (buyMode) { - formSetState.setAmount?.(''); - setTokenInBalanceFraction(numericValue); - } else { - setTokenInBalanceFraction(undefined); - const display = new BigNumber( - userBalanceTokenInDisplayAmount || 0 - ); - const newValue = - numericValue < 1 - ? // round calculated values - formatAmount(display.multipliedBy(numericValue).toNumber()) - : // or pass full value (while truncating fractional zeros) - display.toFixed(); - if (newValue) { - formSetState.setAmount?.(newValue || ''); - } - } - }, - [buyMode, formSetState, userBalanceTokenInDisplayAmount] - )} - /> {showLimitPrice && ( -
+
='}`} value={formState.limitPrice ?? ''} @@ -672,6 +533,44 @@ function LimitOrder({ tokenA, tokenB }: { tokenA?: Token; tokenB?: Token }) { />
)} +
+ +
+
+ +
+
+ +
Order type{' '} @@ -767,7 +666,9 @@ function LimitOrder({ tokenA, tokenB }: { tokenA?: Token; tokenB?: Token }) {
{warning}
- ) : Number(formState.amount) && simulationResult && !coinIn ? ( + ) : simulationResult && + ((Number(formState.amountIn) && !coinOut) || + (Number(formState.amountOut) && !coinIn)) ? ( // show a warning if an amount in, but no amount out
diff --git a/src/components/cards/LimitOrderContext.tsx b/src/components/cards/LimitOrderContext.tsx index 79759de3d..6ef7a8ebf 100644 --- a/src/components/cards/LimitOrderContext.tsx +++ b/src/components/cards/LimitOrderContext.tsx @@ -12,7 +12,8 @@ import { } from '../../lib/web3/utils/limitOrders'; interface FormState { - amount: string; + amountIn: string; + amountOut: string; limitPrice: string; timeAmount: string; timePeriod: TimePeriod; @@ -20,7 +21,7 @@ interface FormState { slippage: string; } interface FormSetState { - setAmount: Dispatch>; + setAmountInOut: Dispatch>; setLimitPrice: Dispatch>; setTimeAmount: Dispatch>; setTimePeriod: Dispatch>; @@ -40,7 +41,7 @@ export function LimitOrderContextProvider({ defaultExecutionType: AllowedLimitOrderTypeKey; children: ReactNode; }) { - const [amount, setAmount] = useState(''); + const [amountInOut, setAmountInOut] = useState<[string, string]>(['', '']); const [limitPrice, setLimitPrice] = useState(''); const [timeAmount, setTimeAmount] = useState('28'); const [timePeriod, setTimePeriod] = useState('days'); @@ -48,19 +49,21 @@ export function LimitOrderContextProvider({ const [slippage, setSlippage] = useState(''); const state = useMemo(() => { + const [amountIn, amountOut] = amountInOut; return { - amount, + amountIn, + amountOut, limitPrice, timeAmount, timePeriod, execution, slippage, }; - }, [amount, limitPrice, timeAmount, timePeriod, execution, slippage]); + }, [amountInOut, limitPrice, timeAmount, timePeriod, execution, slippage]); const setState = useMemo(() => { return { - setAmount, + setAmountInOut, setLimitPrice, setTimeAmount, setTimePeriod, @@ -68,7 +71,7 @@ export function LimitOrderContextProvider({ setSlippage, }; }, [ - setAmount, + setAmountInOut, setLimitPrice, setTimeAmount, setTimePeriod, From 1e6300cc4ac042bbca1cfce34a14f68334701779 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Fri, 10 May 2024 10:57:30 +1000 Subject: [PATCH 006/192] feat: allow maxValue=0 to remove "MAX" shortcut button on token input --- src/components/TokenInputGroup/TokenInputGroup.tsx | 4 ++-- src/components/cards/LimitOrderCard.tsx | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/TokenInputGroup/TokenInputGroup.tsx b/src/components/TokenInputGroup/TokenInputGroup.tsx index 7ac70ca72..0c98d63fb 100644 --- a/src/components/TokenInputGroup/TokenInputGroup.tsx +++ b/src/components/TokenInputGroup/TokenInputGroup.tsx @@ -74,7 +74,7 @@ export default function TokenInputGroup({ }, [value, price]); const { data: balance } = useBankBalanceDisplayAmount(denom); - const maxValue = givenMaxValue || balance; + const maxValue = givenMaxValue ?? balance; return (
))} - {!disabledInput && token && maxValue && Number(maxValue) > 0 && ( + {!disabledInput && token && !!maxValue && Number(maxValue) > 0 && (
From 56b932418ebfadcb3f18e61a869d73c355b28921 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Fri, 10 May 2024 11:58:26 +1000 Subject: [PATCH 007/192] feat: improve estimated trade price --- src/components/cards/LimitOrderCard.tsx | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/src/components/cards/LimitOrderCard.tsx b/src/components/cards/LimitOrderCard.tsx index 48052cb1e..92eaf78cb 100644 --- a/src/components/cards/LimitOrderCard.tsx +++ b/src/components/cards/LimitOrderCard.tsx @@ -128,10 +128,15 @@ function LimitOrder({ tokenA, tokenB }: { tokenA: Token; tokenB: Token }) { const formState = useContext(LimitOrderFormContext); const formSetState = useContext(LimitOrderFormSetContext); + const [lastKnownPrice, setLastKnownPrice] = useState(0); const switchModeTab = useCallback(() => { - // change tab and defined amount + // change tab setModeTab((mode) => (mode === 'Buy' ? 'Sell' : 'Buy')); + // set new amount and estimated exchange rate formSetState.setAmountInOut?.(([a, b]) => [b, a]); + setLastKnownPrice((lastKnownPrice) => + lastKnownPrice ? 1 / lastKnownPrice : 0 + ); }, [formSetState]); const cardModeNav = ( @@ -483,7 +488,6 @@ function LimitOrder({ tokenA, tokenB }: { tokenA: Token; tokenB: Token }) { } }, [nativeChain, setChainFeeToken]); - const [, setLastKnownPrice] = useState(0); useEffect(() => { if (simulationResult?.response) { const price = new BigNumber(simulationResult.response.coin_in.amount).div( @@ -513,10 +517,21 @@ function LimitOrder({ tokenA, tokenB }: { tokenA: Token; tokenB: Token }) { ).toNumber() : // use the market price or NaN currentPrice ?? 0) || 0; - const coinIn = Number(simulationResult?.response?.coin_in.amount || 0); + const coinIn = + isValidatingSimulation && lastKnownPrice + ? amountOutBaseAmount && lastKnownPrice > 0 + ? new BigNumber(amountOutBaseAmount) + .multipliedBy(lastKnownPrice) + .toNumber() + : 0 + : Number(simulationResult?.response?.coin_in.amount || 0); const coinOut = - Number(simulationResult?.response?.taker_coin_out.amount || 0) + - tranchedAmountOut; + isValidatingSimulation && lastKnownPrice + ? amountInBaseAmount && lastKnownPrice > 0 + ? new BigNumber(amountInBaseAmount).dividedBy(lastKnownPrice).toNumber() + : 0 + : Number(simulationResult?.response?.taker_coin_out.amount || 0) + + tranchedAmountOut; // disable fieldset with no address because the estimation requires a signed client const fieldset = ( From 13e732f779dc7c3a7d3368b24aceb429bea0f3b6 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Fri, 10 May 2024 19:37:53 +1000 Subject: [PATCH 008/192] feat: improve estimated (validating) trade price part 2 --- src/components/cards/LimitOrderCard.tsx | 72 ++++++++++++++++++++----- 1 file changed, 58 insertions(+), 14 deletions(-) diff --git a/src/components/cards/LimitOrderCard.tsx b/src/components/cards/LimitOrderCard.tsx index 92eaf78cb..15864fe4c 100644 --- a/src/components/cards/LimitOrderCard.tsx +++ b/src/components/cards/LimitOrderCard.tsx @@ -128,15 +128,11 @@ function LimitOrder({ tokenA, tokenB }: { tokenA: Token; tokenB: Token }) { const formState = useContext(LimitOrderFormContext); const formSetState = useContext(LimitOrderFormSetContext); - const [lastKnownPrice, setLastKnownPrice] = useState(0); const switchModeTab = useCallback(() => { // change tab setModeTab((mode) => (mode === 'Buy' ? 'Sell' : 'Buy')); // set new amount and estimated exchange rate formSetState.setAmountInOut?.(([a, b]) => [b, a]); - setLastKnownPrice((lastKnownPrice) => - lastKnownPrice ? 1 / lastKnownPrice : 0 - ); }, [formSetState]); const cardModeNav = ( @@ -203,7 +199,48 @@ function LimitOrder({ tokenA, tokenB }: { tokenA: Token; tokenB: Token }) { return {}; }, [tokenIn, tokenOut, formState.amountIn, formState.amountOut]); - const [, currentPrice] = useRealtimePrice(tokenIn, tokenOut); + const [, currentPriceAtoB] = useRealtimePrice(tokenA, tokenB); + const currentPriceInToOut = currentPriceAtoB + ? tokenA === tokenIn + ? currentPriceAtoB + : 1 / currentPriceAtoB + : undefined; + + // detect when the user has asked for a limit "outside liquidity bounds" + // in these cases the simulation won't help because it can only compute the + // immediate result of the msg tx: the future amountOut must be estimated + const outsideBounds = + // ensure bounds can be calculated + currentPriceAtoB && + formState.limitPrice && + formState.execution && + // check if the user is using a limit order + // ie. the user may have requested a price outside liquidity bounds + priceTab === 'Limit' && + // check if the user is using a non-immediate order type + // ie. the order may not be filled immediately in the simulation + nonImmediateOrderTypes.includes( + orderTypeEnum[formState.execution] as number + ) && + // check if the user requested a price outside liquidity bounds + // ie. the order may not be filled immediately in the simulation + (buyMode + ? new BigNumber(formState.limitPrice).lt(currentPriceAtoB) + : new BigNumber(formState.limitPrice).gt(currentPriceAtoB)); + + // the best estimated price will depend on whether the limit is out of bounds + const [lastKnownPrice, setLastKnownPrice] = useState(0); + const estimatedPriceInToOut = + outsideBounds && formState.limitPrice + ? // calculate limitPriceInToOut value + tokenA === tokenIn + ? new BigNumber(formState.limitPrice).toNumber() + : new BigNumber(1).dividedBy(formState.limitPrice).toNumber() + : // or use last known price (for more accuracy) + // falling back to current price (for less accuracy) + Number.isFinite(lastKnownPrice) + ? lastKnownPrice + : currentPriceInToOut || 0; const simulatedMsgPlaceLimitOrder: MsgPlaceLimitOrder | undefined = useMemo(() => { @@ -265,13 +302,13 @@ function LimitOrder({ tokenA, tokenB }: { tokenA: Token; tokenB: Token }) { order_type: orderTypeEnum[execution] as LimitOrderType, // if no limit assume market value tick_index_in_to_out: - limitTickIndexInToOut !== undefined + limitTickIndexInToOut !== undefined && priceTab === 'Limit' ? Long.fromNumber(limitTickIndexInToOut.toNumber()) : Long.fromNumber(priceMaxIndex), }; // optional params // only add maxOut for buy orders - if (tokenOut && maxAmountOut && currentPrice) { + if (tokenOut && maxAmountOut && estimatedPriceInToOut) { // set max out on taker orders if ( immediateOrderTypes.includes(orderTypeEnum[execution] as number) @@ -290,7 +327,7 @@ function LimitOrder({ tokenA, tokenB }: { tokenA: Token; tokenB: Token }) { tickIndexToPrice( limitTickIndexInToOut ?? new BigNumber(priceMaxIndex) ), - currentPrice + estimatedPriceInToOut ).toNumber(); msgPlaceLimitOrder.amount_in = BigNumber.min( msgPlaceLimitOrder.amount_in, @@ -312,11 +349,12 @@ function LimitOrder({ tokenA, tokenB }: { tokenA: Token; tokenB: Token }) { amountInBaseAmount, amountOutBaseAmount, buyMode, - currentPrice, + estimatedPriceInToOut, formState.execution, formState.limitPrice, formState.timeAmount, formState.timePeriod, + priceTab, tokenIn, tokenOut, userBalanceTokenIn, @@ -516,19 +554,21 @@ function LimitOrder({ tokenA, tokenB }: { tokenA: Token; tokenB: Token }) { ) ).toNumber() : // use the market price or NaN - currentPrice ?? 0) || 0; + currentPriceInToOut ?? 0) || 0; const coinIn = isValidatingSimulation && lastKnownPrice ? amountOutBaseAmount && lastKnownPrice > 0 ? new BigNumber(amountOutBaseAmount) - .multipliedBy(lastKnownPrice) + .multipliedBy(estimatedPriceInToOut) .toNumber() : 0 : Number(simulationResult?.response?.coin_in.amount || 0); const coinOut = isValidatingSimulation && lastKnownPrice ? amountInBaseAmount && lastKnownPrice > 0 - ? new BigNumber(amountInBaseAmount).dividedBy(lastKnownPrice).toNumber() + ? new BigNumber(amountInBaseAmount) + .dividedBy(estimatedPriceInToOut || 1) + .toNumber() : 0 : Number(simulationResult?.response?.taker_coin_out.amount || 0) + tranchedAmountOut; @@ -652,8 +692,12 @@ function LimitOrder({ tokenA, tokenB }: { tokenA: Token; tokenB: Token }) { formatMaximumSignificantDecimals( simulationResult?.response ? !buyMode - ? coinOut / coinIn || '-' - : coinIn / coinOut || '-' + ? (isValidatingSimulation + ? 1 / estimatedPriceInToOut + : coinOut / coinIn) || '-' + : (isValidatingSimulation + ? estimatedPriceInToOut + : coinIn / coinOut) || '-' : '-' ) )} From b0ac12d15efecd4d57aa4bc41efb833213b25cea Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Fri, 10 May 2024 19:53:48 +1000 Subject: [PATCH 009/192] feat: enforce order type change with price tab type change --- src/components/cards/LimitOrderCard.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/components/cards/LimitOrderCard.tsx b/src/components/cards/LimitOrderCard.tsx index 15864fe4c..93afb9f57 100644 --- a/src/components/cards/LimitOrderCard.tsx +++ b/src/components/cards/LimitOrderCard.tsx @@ -128,6 +128,7 @@ function LimitOrder({ tokenA, tokenB }: { tokenA: Token; tokenB: Token }) { const formState = useContext(LimitOrderFormContext); const formSetState = useContext(LimitOrderFormSetContext); + const switchModeTab = useCallback(() => { // change tab setModeTab((mode) => (mode === 'Buy' ? 'Sell' : 'Buy')); @@ -135,6 +136,15 @@ function LimitOrder({ tokenA, tokenB }: { tokenA: Token; tokenB: Token }) { formSetState.setAmountInOut?.(([a, b]) => [b, a]); }, [formSetState]); + const switchPriceTab = useCallback(() => { + // change tab + const newPriceTab = priceTab === 'Limit' ? 'Swap' : 'Limit'; + setPriceTab(newPriceTab); + // enforce changed execution type + const newExec = newPriceTab === 'Limit' ? 'GOOD_TIL_TIME' : 'FILL_OR_KILL'; + formSetState.setExecution?.(newExec); + }, [formSetState, priceTab]); + const cardModeNav = (
@@ -142,7 +152,7 @@ function LimitOrder({ tokenA, tokenB }: { tokenA: Token; tokenB: Token }) { className="order-type-input mb-4" values={priceTabs} value={priceTab} - onChange={setPriceTab} + onChange={switchPriceTab} />
From eea0679b05c0d3833d011c8fe45045467a13a06a Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Fri, 10 May 2024 22:36:36 +1000 Subject: [PATCH 010/192] feat: refine Expiration control setting in Limit Order card --- .../RadioButtonGroupInput.tsx | 7 +- src/components/cards/LimitOrderCard.scss | 8 + src/components/cards/LimitOrderCard.tsx | 167 ++++++++++++++---- src/components/cards/LimitOrderContext.tsx | 4 +- src/lib/web3/utils/limitOrders.ts | 4 + 5 files changed, 150 insertions(+), 40 deletions(-) diff --git a/src/components/RadioButtonGroupInput/RadioButtonGroupInput.tsx b/src/components/RadioButtonGroupInput/RadioButtonGroupInput.tsx index 87c474fb9..106278561 100644 --- a/src/components/RadioButtonGroupInput/RadioButtonGroupInput.tsx +++ b/src/components/RadioButtonGroupInput/RadioButtonGroupInput.tsx @@ -72,7 +72,10 @@ function useSelectedButtonBackgroundMove( interface Props { className?: string; buttonClassName?: string; - values: { [value in T]: ReactNode } | Map | readonly T[]; + values: + | { readonly [value in T]: ReactNode } + | Map + | readonly T[]; value: T; onChange: (value: T) => void; } @@ -88,7 +91,7 @@ export default function RadioButtonGroupInput({ useSelectedButtonBackgroundMove(value); const entries = useMemo(() => { return Array.isArray(values) - ? values.map<[T, string]>((value) => [value, `${value}`]) + ? values.filter(Boolean).map<[T, string]>((value) => [value, `${value}`]) : values instanceof Map ? Array.from(values.entries()) : (Object.entries(values).map(([value, description]) => [ diff --git a/src/components/cards/LimitOrderCard.scss b/src/components/cards/LimitOrderCard.scss index b8a925697..6c4d8770f 100644 --- a/src/components/cards/LimitOrderCard.scss +++ b/src/components/cards/LimitOrderCard.scss @@ -1,4 +1,5 @@ @use '../../styles/mixins-vars/paddings.scss' as paddings; +@use '../../styles/font/size.scss' as font-size; .limitorder-card { // override default select input styles @@ -42,6 +43,13 @@ } } + .radio-button-group-switch { + &.text-s button { + padding: paddings.$p-2 paddings.$p-sm; + font-size: font-size.$text-s; + } + } + .numeric-value-input { color: hsl(218deg, 11%, 65%); background-color: hsla(216, 20%, 25%, 1); diff --git a/src/components/cards/LimitOrderCard.tsx b/src/components/cards/LimitOrderCard.tsx index 93afb9f57..0f129a53c 100644 --- a/src/components/cards/LimitOrderCard.tsx +++ b/src/components/cards/LimitOrderCard.tsx @@ -56,8 +56,6 @@ import { timePeriodLabels, TimePeriod, AllowedLimitOrderTypeKey, - inputOrderTypes, - orderTypeTextMap, nonImmediateOrderTypes, immediateOrderTypes, } from '../../lib/web3/utils/limitOrders'; @@ -108,7 +106,7 @@ export default function LimitOrderCard({ >

Place Order

{tokenA && tokenB && ( - + )} @@ -122,6 +120,53 @@ type ModeTab = typeof modeTabs[number]; const priceTabs = ['Swap', 'Limit'] as const; type PriceTab = typeof priceTabs[number]; +const expirationOptions = { + '1 days': '1 day', + '1 weeks': '1 week', + '1 months': '1 month', + '1 years': '1 year', + custom: 'Custom', +} as const; +type ExpirationOptions = keyof typeof expirationOptions; + +function getShortcutExpirationTime( + expiration: ExpirationOptions +): [number, TimePeriod] | undefined { + const timeString = expiration.split(' '); + if (timeString.length >= 2) { + const [timeAmount, timePeriod] = timeString; + return [Number(timeAmount), timePeriod as TimePeriod]; + } +} +function getCustomExpirationTimeMs(timeAmount: number, timePeriod: TimePeriod) { + return timeAmount && timePeriod + ? timePeriod === 'years' || timePeriod === 'months' + ? (() => { + const date = new Date(); + if (timePeriod === 'years') { + date.setFullYear(date.getFullYear() + 1); + } + if (timePeriod === 'months') { + date.setMonth(date.getMonth() + 1); + } + return date; + })().getTime() + : new Date(Date.now() + timeAmount * timeUnits[timePeriod]).getTime() + : NaN; +} + +function getExpirationTimeMs( + expiration: ExpirationOptions, + formState: { timeAmount?: string; timePeriod?: TimePeriod } +) { + const [timeAmount, timePeriod]: [number, TimePeriod] = + getShortcutExpirationTime(expiration) || [ + Number(formState.timeAmount ?? 1), + formState.timePeriod || 'days', + ]; + return getCustomExpirationTimeMs(timeAmount, timePeriod); +} + function LimitOrder({ tokenA, tokenB }: { tokenA: Token; tokenB: Token }) { const [modeTab, setModeTab] = useState(modeTabs[0]); const [priceTab, setPriceTab] = useState(priceTabs[0]); @@ -129,6 +174,9 @@ function LimitOrder({ tokenA, tokenB }: { tokenA: Token; tokenB: Token }) { const formState = useContext(LimitOrderFormContext); const formSetState = useContext(LimitOrderFormSetContext); + const [hasExpiry, setHasExpiry] = useState(false); + const [expiration, setExpiration] = useState('1 days'); + const switchModeTab = useCallback(() => { // change tab setModeTab((mode) => (mode === 'Buy' ? 'Sell' : 'Buy')); @@ -257,14 +305,12 @@ function LimitOrder({ tokenA, tokenB }: { tokenA: Token; tokenB: Token }) { const [denomIn, denomOut] = [getTokenId(tokenIn), getTokenId(tokenOut)]; const execution = formState.execution; - const timePeriod = formState.timePeriod; - const timeAmount = Number(formState.timeAmount ?? NaN); const limitPrice = Number(formState.limitPrice || NaN); // do not allow 0 // calculate the expiration time in JS epoch (milliseconds) - const expirationTimeMs = - timeAmount && timePeriod - ? new Date(Date.now() + timeAmount * timeUnits[timePeriod]).getTime() - : NaN; + const expirationTimeMs = getExpirationTimeMs(expiration, { + timeAmount: formState.timeAmount, + timePeriod: formState.timePeriod, + }); // find amounts in/out for the order // in buy mode: buy the amount out with the user's available balance @@ -276,7 +322,6 @@ function LimitOrder({ tokenA, tokenB }: { tokenA: Token; tokenB: Token }) { if ( execution && (execution === 'GOOD_TIL_TIME' ? !isNaN(expirationTimeMs) : true) && - (execution === 'GOOD_TIL_TIME' ? timePeriod !== undefined : true) && address && denomIn && denomOut && @@ -346,11 +391,19 @@ function LimitOrder({ tokenA, tokenB }: { tokenA: Token; tokenB: Token }) { } } // only add expiration time to timed limit orders - if (execution === 'GOOD_TIL_TIME' && !isNaN(expirationTimeMs)) { - msgPlaceLimitOrder.expiration_time = { - seconds: Long.fromNumber(Math.round(expirationTimeMs / 1000)), - nanos: 0, - }; + if (formState.execution === 'GOOD_TIL_TIME') { + const expirationTimeMs = getExpirationTimeMs(expiration, { + timeAmount: formState.timeAmount, + timePeriod: formState.timePeriod, + }); + if (hasExpiry && !isNaN(expirationTimeMs)) { + msgPlaceLimitOrder.expiration_time = { + seconds: Long.fromNumber(Math.round(expirationTimeMs / 1000)), + nanos: 0, + }; + } else { + msgPlaceLimitOrder.order_type = orderTypeEnum['GOOD_TIL_CANCELLED']; + } } return msgPlaceLimitOrder; } @@ -360,10 +413,12 @@ function LimitOrder({ tokenA, tokenB }: { tokenA: Token; tokenB: Token }) { amountOutBaseAmount, buyMode, estimatedPriceInToOut, + expiration, formState.execution, formState.limitPrice, formState.timeAmount, formState.timePeriod, + hasExpiry, priceTab, tokenIn, tokenOut, @@ -441,6 +496,23 @@ function LimitOrder({ tokenA, tokenB }: { tokenA: Token; tokenB: Token }) { ...simulatedMsgPlaceLimitOrder, tick_index_in_to_out: Long.fromNumber(tickIndexLimitInToOut), }; + + // only add expiration time to timed limit orders + if (formState.execution === 'GOOD_TIL_TIME') { + const expirationTimeMs = getExpirationTimeMs(expiration, { + timeAmount: formState.timeAmount, + timePeriod: formState.timePeriod, + }); + if (hasExpiry && !isNaN(expirationTimeMs)) { + msgPlaceLimitOrder.expiration_time = { + seconds: Long.fromNumber(Math.round(expirationTimeMs / 1000)), + nanos: 0, + }; + } else { + msgPlaceLimitOrder.order_type = orderTypeEnum['GOOD_TIL_CANCELLED']; + } + } + const gasEstimate = simulationResult?.gasInfo?.gasUsed.toNumber(); swapRequest(msgPlaceLimitOrder, (gasEstimate || 0) * 1.5); } @@ -448,12 +520,17 @@ function LimitOrder({ tokenA, tokenB }: { tokenA: Token; tokenB: Token }) { [ formState.limitPrice, formState.slippage, + formState.timeAmount, + formState.timePeriod, + formState.execution, tokenIn, tokenOut, buyMode, simulatedMsgPlaceLimitOrder, simulationResult?.result?.events, simulationResult?.gasInfo?.gasUsed, + expiration, + hasExpiry, swapRequest, ] ); @@ -638,32 +715,50 @@ function LimitOrder({ tokenA, tokenB }: { tokenA: Token; tokenB: Token }) { >
-
- Order type{' '} - - ? - -
- - className="flex col m-0 p-0" - list={inputOrderTypes} - getLabel={(key = defaultExecutionType) => orderTypeTextMap[key]} - value={formState.execution} - onChange={formSetState.setExecution} - floating - /> -
+
+
+ +
+
+ {hasExpiry ? ( + + className="order-type-input text-s" + values={expirationOptions} + value={expiration} + onChange={setExpiration} + /> + ) : ( + 'No expiry' + )} +
+
+ + +
{ + setExpiration('custom'); + formSetState.setTimeAmount?.(v); + }} /> className="flex col m-0 p-0" diff --git a/src/components/cards/LimitOrderContext.tsx b/src/components/cards/LimitOrderContext.tsx index 6ef7a8ebf..cb5178830 100644 --- a/src/components/cards/LimitOrderContext.tsx +++ b/src/components/cards/LimitOrderContext.tsx @@ -43,8 +43,8 @@ export function LimitOrderContextProvider({ }) { const [amountInOut, setAmountInOut] = useState<[string, string]>(['', '']); const [limitPrice, setLimitPrice] = useState(''); - const [timeAmount, setTimeAmount] = useState('28'); - const [timePeriod, setTimePeriod] = useState('days'); + const [timeAmount, setTimeAmount] = useState('1'); + const [timePeriod, setTimePeriod] = useState('hours'); const [execution, setExecution] = useState(defaultExecutionType); const [slippage, setSlippage] = useState(''); diff --git a/src/lib/web3/utils/limitOrders.ts b/src/lib/web3/utils/limitOrders.ts index edbde43bc..5f86be5bd 100644 --- a/src/lib/web3/utils/limitOrders.ts +++ b/src/lib/web3/utils/limitOrders.ts @@ -57,6 +57,8 @@ export const timePeriods = [ 'hours', 'days', 'weeks', + 'months', + 'years', ] as const; export type TimePeriod = typeof timePeriods[number]; @@ -68,4 +70,6 @@ export const timePeriodLabels: { hours: 'Hours', days: 'Days', weeks: 'Weeks', + months: 'Months', + years: 'Years', }; From f9f96f675c1f6bc4d2721ec6972462c0a9828ae4 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Fri, 10 May 2024 22:48:46 +1000 Subject: [PATCH 011/192] feat: reduce recomputation on expiration time changes --- src/components/cards/LimitOrderCard.tsx | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/components/cards/LimitOrderCard.tsx b/src/components/cards/LimitOrderCard.tsx index 0f129a53c..41be775ed 100644 --- a/src/components/cards/LimitOrderCard.tsx +++ b/src/components/cards/LimitOrderCard.tsx @@ -306,11 +306,6 @@ function LimitOrder({ tokenA, tokenB }: { tokenA: Token; tokenB: Token }) { const execution = formState.execution; const limitPrice = Number(formState.limitPrice || NaN); // do not allow 0 - // calculate the expiration time in JS epoch (milliseconds) - const expirationTimeMs = getExpirationTimeMs(expiration, { - timeAmount: formState.timeAmount, - timePeriod: formState.timePeriod, - }); // find amounts in/out for the order // in buy mode: buy the amount out with the user's available balance @@ -321,7 +316,6 @@ function LimitOrder({ tokenA, tokenB }: { tokenA: Token; tokenB: Token }) { // check format of request if ( execution && - (execution === 'GOOD_TIL_TIME' ? !isNaN(expirationTimeMs) : true) && address && denomIn && denomOut && @@ -393,8 +387,8 @@ function LimitOrder({ tokenA, tokenB }: { tokenA: Token; tokenB: Token }) { // only add expiration time to timed limit orders if (formState.execution === 'GOOD_TIL_TIME') { const expirationTimeMs = getExpirationTimeMs(expiration, { - timeAmount: formState.timeAmount, - timePeriod: formState.timePeriod, + timeAmount: '1', + timePeriod: 'days', }); if (hasExpiry && !isNaN(expirationTimeMs)) { msgPlaceLimitOrder.expiration_time = { @@ -416,8 +410,6 @@ function LimitOrder({ tokenA, tokenB }: { tokenA: Token; tokenB: Token }) { expiration, formState.execution, formState.limitPrice, - formState.timeAmount, - formState.timePeriod, hasExpiry, priceTab, tokenIn, From d31a9476a30a345cf3a14b4363dda0b54772bb31 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Sat, 11 May 2024 00:09:45 +1000 Subject: [PATCH 012/192] feat: put Swap/Limit Buy/Sell buttons in card header --- src/components/cards/LimitOrderCard.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/cards/LimitOrderCard.tsx b/src/components/cards/LimitOrderCard.tsx index 41be775ed..8f8a9b4ae 100644 --- a/src/components/cards/LimitOrderCard.tsx +++ b/src/components/cards/LimitOrderCard.tsx @@ -104,7 +104,6 @@ export default function LimitOrderCard({ minWidth: '24.5em', }} > -

Place Order

{tokenA && tokenB && ( @@ -195,17 +194,18 @@ function LimitOrder({ tokenA, tokenB }: { tokenA: Token; tokenB: Token }) { const cardModeNav = (
-
+

Order

+
- className="order-type-input mb-4" + className="order-type-input my-4" values={priceTabs} value={priceTab} onChange={switchPriceTab} />
-
+
- className="order-type-input mb-4" + className="order-type-input my-4" values={modeTabs} value={modeTab} onChange={switchModeTab} From 3b5529d188aab121db50d59a685029dd15a35404 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Sat, 11 May 2024 00:12:00 +1000 Subject: [PATCH 013/192] Revert "feat: put Swap/Limit Buy/Sell buttons in card header" This reverts commit d31a9476a30a345cf3a14b4363dda0b54772bb31. --- src/components/cards/LimitOrderCard.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/cards/LimitOrderCard.tsx b/src/components/cards/LimitOrderCard.tsx index 8f8a9b4ae..41be775ed 100644 --- a/src/components/cards/LimitOrderCard.tsx +++ b/src/components/cards/LimitOrderCard.tsx @@ -104,6 +104,7 @@ export default function LimitOrderCard({ minWidth: '24.5em', }} > +

Place Order

{tokenA && tokenB && ( @@ -194,18 +195,17 @@ function LimitOrder({ tokenA, tokenB }: { tokenA: Token; tokenB: Token }) { const cardModeNav = (
-

Order

-
+
- className="order-type-input my-4" + className="order-type-input mb-4" values={priceTabs} value={priceTab} onChange={switchPriceTab} />
-
+
- className="order-type-input my-4" + className="order-type-input mb-4" values={modeTabs} value={modeTab} onChange={switchModeTab} From 56e96a11ceb8a0dcca8d49c2e6010757f7a7df5f Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Sat, 11 May 2024 00:31:29 +1000 Subject: [PATCH 014/192] feat: focus on input control when click input group --- src/components/TokenInputGroup/TokenInputGroup.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/TokenInputGroup/TokenInputGroup.tsx b/src/components/TokenInputGroup/TokenInputGroup.tsx index 0c98d63fb..ca043bde2 100644 --- a/src/components/TokenInputGroup/TokenInputGroup.tsx +++ b/src/components/TokenInputGroup/TokenInputGroup.tsx @@ -1,4 +1,4 @@ -import React, { ReactNode, useCallback, useMemo } from 'react'; +import React, { ReactNode, useCallback, useMemo, useRef } from 'react'; import BigNumber from 'bignumber.js'; import TokenPicker from '../TokenPicker'; @@ -75,6 +75,7 @@ export default function TokenInputGroup({ const { data: balance } = useBankBalanceDisplayAmount(denom); const maxValue = givenMaxValue ?? balance; + const inputRef = useRef(null); return (
inputRef.current?.focus()} + onKeyDown={() => inputRef.current?.focus()} + role="button" + tabIndex={0} > {header || (maxValue && ( @@ -120,6 +125,7 @@ export default function TokenInputGroup({ /> Date: Sat, 11 May 2024 01:04:18 +1000 Subject: [PATCH 015/192] feat: remove less useful simulation result information --- src/components/cards/LimitOrderCard.tsx | 36 ++----------------------- 1 file changed, 2 insertions(+), 34 deletions(-) diff --git a/src/components/cards/LimitOrderCard.tsx b/src/components/cards/LimitOrderCard.tsx index 41be775ed..cb4e38f4b 100644 --- a/src/components/cards/LimitOrderCard.tsx +++ b/src/components/cards/LimitOrderCard.tsx @@ -679,6 +679,7 @@ function LimitOrder({ tokenA, tokenB }: { tokenA: Token; tokenB: Token }) { maximumSignificantDigits: 5, }) } + disabledInput={isLoadingUserBalanceTokenIn} >
@@ -785,6 +786,7 @@ function LimitOrder({ tokenA, tokenB }: { tokenA: Token; tokenB: Token }) {
-
- -
{warning ? ( // show a warning if an amount has been entered, but the form fails validation
@@ -853,23 +838,6 @@ function LimitOrder({ tokenA, tokenB }: { tokenA: Token; tokenB: Token }) { {!address ? 'Connect Wallet' : buyMode ? 'Buy' : 'Sell'}
-
); return ( From d09be3241ee47966c1277f8da14fb8a13a271ca9 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Sat, 11 May 2024 01:14:45 +1000 Subject: [PATCH 016/192] fix: initial state then type amountOut fix, price was 0 --- src/components/cards/LimitOrderCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/cards/LimitOrderCard.tsx b/src/components/cards/LimitOrderCard.tsx index cb4e38f4b..afc3cc97c 100644 --- a/src/components/cards/LimitOrderCard.tsx +++ b/src/components/cards/LimitOrderCard.tsx @@ -296,7 +296,7 @@ function LimitOrder({ tokenA, tokenB }: { tokenA: Token; tokenB: Token }) { : new BigNumber(1).dividedBy(formState.limitPrice).toNumber() : // or use last known price (for more accuracy) // falling back to current price (for less accuracy) - Number.isFinite(lastKnownPrice) + lastKnownPrice && Number.isFinite(lastKnownPrice) ? lastKnownPrice : currentPriceInToOut || 0; From eb661dda593346e1f1eec1d8a9172b0e24f8b82c Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Sat, 11 May 2024 01:18:37 +1000 Subject: [PATCH 017/192] feat: make easier to focus on text inputs from anywhere in group --- src/components/cards/LimitOrderCard.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/components/cards/LimitOrderCard.tsx b/src/components/cards/LimitOrderCard.tsx index afc3cc97c..6fa5376f9 100644 --- a/src/components/cards/LimitOrderCard.tsx +++ b/src/components/cards/LimitOrderCard.tsx @@ -903,11 +903,16 @@ function NumericInputRow({ className={['numeric-value-input flex row py-3 px-4', className] .filter(Boolean) .join(' ')} + onClick={() => inputRef.current?.focus()} + onKeyDown={() => inputRef.current?.focus()} + role="button" + tabIndex={0} > {prefix && (
{prefix}
)} Date: Sat, 11 May 2024 01:53:15 +1000 Subject: [PATCH 018/192] feat: add basic styling of limit price input --- src/components/cards/LimitOrderCard.scss | 21 +++++++++++ src/components/cards/LimitOrderCard.tsx | 46 ++++++++++++++++++++---- 2 files changed, 60 insertions(+), 7 deletions(-) diff --git a/src/components/cards/LimitOrderCard.scss b/src/components/cards/LimitOrderCard.scss index 6c4d8770f..8cd7bad9b 100644 --- a/src/components/cards/LimitOrderCard.scss +++ b/src/components/cards/LimitOrderCard.scss @@ -29,6 +29,26 @@ } } + .limit-price { + & { + padding: paddings.$p-4; + border-radius: 1rem; + border: 1px solid transparent; + background-color: var(--default); + } + .numeric-value-input { + padding: paddings.$p-0; + border: 1px solid transparent; + background-color: var(--default); + color: white; + & input { + align-self: center; + color: white; + font-size: 1.5em; + } + } + } + .direction-button-row { & { z-index: 1; @@ -55,6 +75,7 @@ background-color: hsla(216, 20%, 25%, 1); border: 1px solid var(--page-card-border); border-radius: paddings.$p-3; + padding: paddings.$p-3 paddings.$p-4; input { font-size: 1rem; font-weight: 500; diff --git a/src/components/cards/LimitOrderCard.tsx b/src/components/cards/LimitOrderCard.tsx index 6fa5376f9..9c7221748 100644 --- a/src/components/cards/LimitOrderCard.tsx +++ b/src/components/cards/LimitOrderCard.tsx @@ -3,6 +3,7 @@ import BigNumber from 'bignumber.js'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faArrowDown, faSpinner } from '@fortawesome/free-solid-svg-icons'; import { + ReactNode, useCallback, useContext, useEffect, @@ -69,6 +70,9 @@ import { } from '../../lib/web3/utils/ticks'; import { guessInvertedOrder } from '../../lib/web3/utils/pairs'; +import AssetIcon from '../assets/AssetIcon'; +import AssetSymbol from '../assets/AssetName'; + import Drawer from '../Drawer'; const { REACT_APP__MAX_TICK_INDEXES = '' } = import.meta.env; @@ -656,13 +660,41 @@ function LimitOrder({ tokenA, tokenB }: { tokenA: Token; tokenB: Token }) { const fieldset = (
{showLimitPrice && ( -
+
+
+ When 1 +
+ +
+
+ +
+ is worth +
='}`} value={formState.limitPrice ?? ''} - placeholder="market" + placeholder={formatAmount(currentPriceAtoB || 0, { + maximumSignificantDigits: 7, + })} onChange={formSetState.setLimitPrice} - suffix={tokenA && tokenB && `${tokenA.symbol} per ${tokenB.symbol}`} + suffix={ + tokenA && ( +
+
+ +
+
+ +
+
+ ) + } format={formatNumericAmount('')} />
@@ -862,12 +894,12 @@ function NumericInputRow({ readOnly = false, }: { className?: string; - prefix?: string; + prefix?: ReactNode; value: string; placeholder?: string; onInput?: (value: string) => void; onChange?: (value: string) => void; - suffix?: string; + suffix?: ReactNode; min?: number; max?: number; format?: (value: number) => string; @@ -900,7 +932,7 @@ function NumericInputRow({ return (
inputRef.current?.focus()} From a8265e5ae23bdc756eb266608fbc5f52ef5fb101 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Sat, 11 May 2024 02:04:31 +1000 Subject: [PATCH 019/192] feat: refine design of Expiry control to single click to custom --- src/components/cards/LimitOrderCard.tsx | 40 ++++++++----------------- 1 file changed, 12 insertions(+), 28 deletions(-) diff --git a/src/components/cards/LimitOrderCard.tsx b/src/components/cards/LimitOrderCard.tsx index 9c7221748..dd99118fc 100644 --- a/src/components/cards/LimitOrderCard.tsx +++ b/src/components/cards/LimitOrderCard.tsx @@ -105,7 +105,7 @@ export default function LimitOrderCard({ className="page-card flex limitorder-card py-0 px-md" style={{ // fix width to a minimum for most pairs to be displayed comfortably - minWidth: '24.5em', + minWidth: '25em', }} >

Place Order

@@ -125,6 +125,7 @@ const priceTabs = ['Swap', 'Limit'] as const; type PriceTab = typeof priceTabs[number]; const expirationOptions = { + none: 'None', '1 days': '1 day', '1 weeks': '1 week', '1 months': '1 month', @@ -178,8 +179,8 @@ function LimitOrder({ tokenA, tokenB }: { tokenA: Token; tokenB: Token }) { const formState = useContext(LimitOrderFormContext); const formSetState = useContext(LimitOrderFormSetContext); - const [hasExpiry, setHasExpiry] = useState(false); - const [expiration, setExpiration] = useState('1 days'); + const [expiration, setExpiration] = useState('none'); + const hasExpiry = expiration !== 'none'; const switchModeTab = useCallback(() => { // change tab @@ -742,37 +743,20 @@ function LimitOrder({ tokenA, tokenB }: { tokenA: Token; tokenB: Token }) {
-
- -
+
Expiry
- {hasExpiry ? ( - - className="order-type-input text-s" - values={expirationOptions} - value={expiration} - onChange={setExpiration} - /> - ) : ( - 'No expiry' - )} + + className="order-type-input text-s" + values={expirationOptions} + value={expiration} + onChange={setExpiration} + />
From da8c7b300fe684387d9ca34b032220675da055e8 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Sat, 11 May 2024 03:36:25 +1000 Subject: [PATCH 020/192] feat: enable RadioButtonGroup to skip buttons of no description --- .../RadioButtonGroupInput/RadioButtonGroupInput.tsx | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/components/RadioButtonGroupInput/RadioButtonGroupInput.tsx b/src/components/RadioButtonGroupInput/RadioButtonGroupInput.tsx index 106278561..eec055ecb 100644 --- a/src/components/RadioButtonGroupInput/RadioButtonGroupInput.tsx +++ b/src/components/RadioButtonGroupInput/RadioButtonGroupInput.tsx @@ -43,11 +43,17 @@ function useSelectedButtonBackgroundMove( if (movingButton && targetButton) { movingButton.style.width = `${targetButton.offsetWidth}px`; movingButton.style.left = `${targetButton.offsetLeft}px`; - if (newValue !== undefined) { + if (newValue !== undefined && movingButton.style.opacity !== '0') { movingButton.classList.add('transition-ready'); } else { movingButton?.classList.remove('transition-ready'); } + movingButton.style.opacity = '1'; + } + // "remove" button if the target was not found + else if (movingButton) { + movingButton?.classList.remove('transition-ready'); + movingButton.style.opacity = '0'; } }, [value, refsByValue, movingButton] @@ -105,6 +111,8 @@ export default function RadioButtonGroupInput({ const includedIndexes = useMemo(() => { return ( entries + // do not display falsy description buttons + .filter(([_key, value]) => !!value) .map((_, index, entries) => { // cumulate weightings let result = 0; @@ -159,6 +167,9 @@ export default function RadioButtonGroupInput({ const currentIndex = includedIndexes.includes(index); const nextIndex = includedIndexes.includes(index + 1); + // skip those with no description + if (!description) return []; + // include button if required or if excluding it // will not reduce the number of shown buttons if (currentIndex || (previousIndex && nextIndex)) { From e4712bbae6f9e53bead339f094ecee4c2761e5c2 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Sat, 11 May 2024 04:02:10 +1000 Subject: [PATCH 021/192] feat: add LimitPrice input, connected to all other input fields --- src/components/cards/LimitOrderCard.scss | 11 ++ src/components/cards/LimitOrderCard.tsx | 138 ++++++++++++++++++----- 2 files changed, 118 insertions(+), 31 deletions(-) diff --git a/src/components/cards/LimitOrderCard.scss b/src/components/cards/LimitOrderCard.scss index 8cd7bad9b..255f508b3 100644 --- a/src/components/cards/LimitOrderCard.scss +++ b/src/components/cards/LimitOrderCard.scss @@ -47,6 +47,17 @@ font-size: 1.5em; } } + .radio-button-group-switch { + & { + flex: 0 0 auto; + } + .moving-background { + background-color: var(--token-search-bg); + } + } + } + .radio-button-group-switch { + flex: 0 0 auto; } .direction-button-row { diff --git a/src/components/cards/LimitOrderCard.tsx b/src/components/cards/LimitOrderCard.tsx index dd99118fc..894978240 100644 --- a/src/components/cards/LimitOrderCard.tsx +++ b/src/components/cards/LimitOrderCard.tsx @@ -134,6 +134,24 @@ const expirationOptions = { } as const; type ExpirationOptions = keyof typeof expirationOptions; +const buyLimitPriceOptions = { + 0: 'Market', + 1: '-1%', + 5: '-5%', + 10: '-10%', + [-1]: '', +} as const; +const sellLimitPriceOptions = { + 0: 'Market', + 1: '+1%', + 5: '+5%', + 10: '+10%', + [-1]: '', +} as const; +type LimitPriceOptions = + | keyof typeof buyLimitPriceOptions + | keyof typeof sellLimitPriceOptions; + function getShortcutExpirationTime( expiration: ExpirationOptions ): [number, TimePeriod] | undefined { @@ -182,21 +200,29 @@ function LimitOrder({ tokenA, tokenB }: { tokenA: Token; tokenB: Token }) { const [expiration, setExpiration] = useState('none'); const hasExpiry = expiration !== 'none'; - const switchModeTab = useCallback(() => { - // change tab - setModeTab((mode) => (mode === 'Buy' ? 'Sell' : 'Buy')); - // set new amount and estimated exchange rate - formSetState.setAmountInOut?.(([a, b]) => [b, a]); - }, [formSetState]); - - const switchPriceTab = useCallback(() => { - // change tab - const newPriceTab = priceTab === 'Limit' ? 'Swap' : 'Limit'; - setPriceTab(newPriceTab); - // enforce changed execution type - const newExec = newPriceTab === 'Limit' ? 'GOOD_TIL_TIME' : 'FILL_OR_KILL'; - formSetState.setExecution?.(newExec); - }, [formSetState, priceTab]); + const switchModeTab = useCallback( + (newModeTab: ModeTab) => { + // change tab + setModeTab(newModeTab); + // set new amount and estimated exchange rate + if (modeTab !== newModeTab) { + formSetState.setAmountInOut?.(([a, b]) => [b, a]); + } + }, + [formSetState, modeTab] + ); + + const switchPriceTab = useCallback( + (newPriceTab: PriceTab) => { + // change tab + setPriceTab(newPriceTab); + // enforce changed execution type + const newExec = + newPriceTab === 'Limit' ? 'GOOD_TIL_TIME' : 'FILL_OR_KILL'; + formSetState.setExecution?.(newExec); + }, + [formSetState] + ); const cardModeNav = (
@@ -219,7 +245,6 @@ function LimitOrder({ tokenA, tokenB }: { tokenA: Token; tokenB: Token }) {
); - const showLimitPrice = priceTab === 'Limit'; const buyMode = modeTab === 'Buy'; const [denomA, denomB] = [getTokenId(tokenA), getTokenId(tokenB)]; @@ -269,6 +294,34 @@ function LimitOrder({ tokenA, tokenB }: { tokenA: Token; tokenB: Token }) { : 1 / currentPriceAtoB : undefined; + const [limitOption, setLimitOption] = useState(0); + + const switchLimitOption = useCallback( + (limitOption: LimitPriceOptions) => { + // set value + setLimitOption(limitOption); + // remove custom limit price if setting one here + if (Number(limitOption) >= 0) { + formSetState.setLimitPrice?.(''); + } + // set mode to limit order if limit is greater than 0 + switchPriceTab(Number(limitOption) === 0 ? 'Swap' : 'Limit'); + }, + [formSetState, switchPriceTab] + ); + + // combine offset and current price to get dynamic limit price + const offsetLimitPrice = useMemo(() => { + if (currentPriceAtoB && limitOption >= 0) { + return formatAmount( + new BigNumber(currentPriceAtoB) + .multipliedBy(1 + (buyMode ? -limitOption : limitOption) / 100) + .toFixed(), + { maximumSignificantDigits: 6 } + ); + } + }, [buyMode, currentPriceAtoB, limitOption]); + // detect when the user has asked for a limit "outside liquidity bounds" // in these cases the simulation won't help because it can only compute the // immediate result of the msg tx: the future amountOut must be estimated @@ -310,7 +363,11 @@ function LimitOrder({ tokenA, tokenB }: { tokenA: Token; tokenB: Token }) { const [denomIn, denomOut] = [getTokenId(tokenIn), getTokenId(tokenOut)]; const execution = formState.execution; - const limitPrice = Number(formState.limitPrice || NaN); // do not allow 0 + // find limit price from custom limit price or limit price offset + // calculation, and do not pass 0: allow NaN to raise errors + const limitPrice = Number( + formState.limitPrice || (limitOption > 0 ? offsetLimitPrice : NaN) + ); // find amounts in/out for the order // in buy mode: buy the amount out with the user's available balance @@ -416,6 +473,8 @@ function LimitOrder({ tokenA, tokenB }: { tokenA: Token; tokenB: Token }) { formState.execution, formState.limitPrice, hasExpiry, + limitOption, + offsetLimitPrice, priceTab, tokenIn, tokenOut, @@ -477,12 +536,14 @@ function LimitOrder({ tokenA, tokenB }: { tokenA: Token; tokenB: Token }) { : Math.floor(Number(lastPrice.TickIndex) * toleranceFactor); }; - const tickIndexLimitInToOut = formState.limitPrice - ? displayPriceToTickIndex( - new BigNumber(formState.limitPrice), - tokenIn, - tokenOut - ) + // find limit price from custom limit price or limit price offset + // calculation, and do not pass 0: allow NaN to raise errors + const limitPrice = Number( + formState.limitPrice || (limitOption > 0 ? offsetLimitPrice : NaN) + ); + + const tickIndexLimitInToOut = limitPrice + ? displayPriceToTickIndex(new BigNumber(limitPrice), tokenIn, tokenOut) ?.multipliedBy(buyMode ? 1 : -1) ?.toNumber() : getMarketLimitIndex(); @@ -517,18 +578,20 @@ function LimitOrder({ tokenA, tokenB }: { tokenA: Token; tokenB: Token }) { [ formState.limitPrice, formState.slippage, + formState.execution, formState.timeAmount, formState.timePeriod, - formState.execution, + limitOption, + offsetLimitPrice, tokenIn, tokenOut, buyMode, simulatedMsgPlaceLimitOrder, simulationResult?.result?.events, simulationResult?.gasInfo?.gasUsed, + swapRequest, expiration, hasExpiry, - swapRequest, ] ); @@ -660,8 +723,8 @@ function LimitOrder({ tokenA, tokenB }: { tokenA: Token; tokenB: Token }) { // disable fieldset with no address because the estimation requires a signed client const fieldset = (
- {showLimitPrice && ( -
+
+
is worth
{ + // set price + formSetState.setLimitPrice?.(price); + // and remove generated market offset price + switchLimitOption(-1); + }} suffix={ tokenA && (
+
+ + className="mt-3 order-type-input text-s" + values={buyMode ? buyLimitPriceOptions : sellLimitPriceOptions} + value={limitOption} + onChange={switchLimitOption} + /> +
- )} +
switchModeTab(modeTab === 'Buy' ? 'Sell' : 'Buy')} className="icon-button direction-button" > From 49fa8cb430c64ca6aa9edbfdf558dab8a2cf7ba6 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Sat, 11 May 2024 00:09:45 +1000 Subject: [PATCH 022/192] feat: remove Swap/Limit buttons to make card header more compact --- src/components/cards/LimitOrderCard.tsx | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/components/cards/LimitOrderCard.tsx b/src/components/cards/LimitOrderCard.tsx index 894978240..4cb8d8e1f 100644 --- a/src/components/cards/LimitOrderCard.tsx +++ b/src/components/cards/LimitOrderCard.tsx @@ -108,7 +108,6 @@ export default function LimitOrderCard({ minWidth: '25em', }} > -

Place Order

{tokenA && tokenB && ( @@ -226,17 +225,10 @@ function LimitOrder({ tokenA, tokenB }: { tokenA: Token; tokenB: Token }) { const cardModeNav = (
-
- - className="order-type-input mb-4" - values={priceTabs} - value={priceTab} - onChange={switchPriceTab} - /> -
+

Place Order

- className="order-type-input mb-4" + className="order-type-input my-4" values={modeTabs} value={modeTab} onChange={switchModeTab} From ba40847a90d82121cee6f6b05cb814d238ee7f50 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Sat, 11 May 2024 09:07:40 +1000 Subject: [PATCH 023/192] Revert "feat: remove Swap/Limit buttons to make card header more compact" This reverts commit 49fa8cb430c64ca6aa9edbfdf558dab8a2cf7ba6. --- src/components/cards/LimitOrderCard.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/components/cards/LimitOrderCard.tsx b/src/components/cards/LimitOrderCard.tsx index 4cb8d8e1f..894978240 100644 --- a/src/components/cards/LimitOrderCard.tsx +++ b/src/components/cards/LimitOrderCard.tsx @@ -108,6 +108,7 @@ export default function LimitOrderCard({ minWidth: '25em', }} > +

Place Order

{tokenA && tokenB && ( @@ -225,10 +226,17 @@ function LimitOrder({ tokenA, tokenB }: { tokenA: Token; tokenB: Token }) { const cardModeNav = (
-

Place Order

+
+ + className="order-type-input mb-4" + values={priceTabs} + value={priceTab} + onChange={switchPriceTab} + /> +
- className="order-type-input my-4" + className="order-type-input mb-4" values={modeTabs} value={modeTab} onChange={switchModeTab} From 1abd58cb100c122a9b7c9a667d05b88bbf9fd3f0 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Sat, 11 May 2024 09:27:30 +1000 Subject: [PATCH 024/192] feat: control LimitPrice visibility with Swap tab only --- src/components/cards/LimitOrderCard.tsx | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/components/cards/LimitOrderCard.tsx b/src/components/cards/LimitOrderCard.tsx index 894978240..a8273a142 100644 --- a/src/components/cards/LimitOrderCard.tsx +++ b/src/components/cards/LimitOrderCard.tsx @@ -304,10 +304,8 @@ function LimitOrder({ tokenA, tokenB }: { tokenA: Token; tokenB: Token }) { if (Number(limitOption) >= 0) { formSetState.setLimitPrice?.(''); } - // set mode to limit order if limit is greater than 0 - switchPriceTab(Number(limitOption) === 0 ? 'Swap' : 'Limit'); }, - [formSetState, switchPriceTab] + [formSetState] ); // combine offset and current price to get dynamic limit price @@ -723,8 +721,8 @@ function LimitOrder({ tokenA, tokenB }: { tokenA: Token; tokenB: Token }) { // disable fieldset with no address because the estimation requires a signed client const fieldset = (
-
-
+ +
-
+
 
+
Date: Sat, 11 May 2024 00:09:45 +1000 Subject: [PATCH 025/192] feat: put Swap/Limit Buy/Sell buttons in card header --- src/components/cards/LimitOrderCard.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/cards/LimitOrderCard.tsx b/src/components/cards/LimitOrderCard.tsx index a8273a142..657e90e11 100644 --- a/src/components/cards/LimitOrderCard.tsx +++ b/src/components/cards/LimitOrderCard.tsx @@ -108,7 +108,6 @@ export default function LimitOrderCard({ minWidth: '25em', }} > -

Place Order

{tokenA && tokenB && ( @@ -226,17 +225,18 @@ function LimitOrder({ tokenA, tokenB }: { tokenA: Token; tokenB: Token }) { const cardModeNav = (
-
+

Order

+
- className="order-type-input mb-4" + className="order-type-input my-4" values={priceTabs} value={priceTab} onChange={switchPriceTab} />
-
+
- className="order-type-input mb-4" + className="order-type-input my-4" values={modeTabs} value={modeTab} onChange={switchModeTab} From 0873663f66cef7dda60946be4f890ade5451683c Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Sat, 11 May 2024 09:34:51 +1000 Subject: [PATCH 026/192] feat: stop LimitOrder card from scrolling --- src/components/cards/LimitOrderCard.scss | 4 ++++ src/components/cards/LimitOrderCard.tsx | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/cards/LimitOrderCard.scss b/src/components/cards/LimitOrderCard.scss index 255f508b3..302ffaf60 100644 --- a/src/components/cards/LimitOrderCard.scss +++ b/src/components/cards/LimitOrderCard.scss @@ -2,6 +2,10 @@ @use '../../styles/font/size.scss' as font-size; .limitorder-card { + & { + overflow: visible; + } + // override default select input styles .select-input { .select-input-selection { diff --git a/src/components/cards/LimitOrderCard.tsx b/src/components/cards/LimitOrderCard.tsx index 657e90e11..4f09c2d64 100644 --- a/src/components/cards/LimitOrderCard.tsx +++ b/src/components/cards/LimitOrderCard.tsx @@ -917,7 +917,7 @@ function LimitOrder({ tokenA, tokenB }: { tokenA: Token; tokenB: Token }) { ) : null}
- - )} - - { - return { - // set width as minimum amount available - minWidth: '100%', - width: 0, - }; - }, [])} - /> - {secondaryValue} +
+ {!disabledInput && token && !!maxValue && Number(maxValue) > 0 && ( +
+ +
+ )} +
+
+
+ +
+
+ { + return { + // set width as minimum amount available + minWidth: '100%', + width: 0, + }; + }, [])} + /> + {secondaryValue} +
+
); } From 4fd2e5dd006a44034660453e26e7e5262163c961 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Tue, 14 May 2024 17:11:50 +1000 Subject: [PATCH 028/192] refactor: don't redefine border width and style twice --- src/components/TokenInputGroup/TokenInputGroup.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/TokenInputGroup/TokenInputGroup.scss b/src/components/TokenInputGroup/TokenInputGroup.scss index 1912022bb..15049b9d4 100644 --- a/src/components/TokenInputGroup/TokenInputGroup.scss +++ b/src/components/TokenInputGroup/TokenInputGroup.scss @@ -19,7 +19,7 @@ background-color: hsla(0, 73%, 97%, 1); &, .token-group-balance button { - border: 1px solid var(--error); + border-color: var(--error); } .token-group-title, .token-picker-toggle, From 6ef30e3d2c28be7b5493285dc73ab3ffa45b12df Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Tue, 14 May 2024 17:12:35 +1000 Subject: [PATCH 029/192] fix: align header text and header button text using extra margins --- src/components/TokenInputGroup/TokenInputGroup.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/TokenInputGroup/TokenInputGroup.tsx b/src/components/TokenInputGroup/TokenInputGroup.tsx index 3540ea4c1..d6a77106c 100644 --- a/src/components/TokenInputGroup/TokenInputGroup.tsx +++ b/src/components/TokenInputGroup/TokenInputGroup.tsx @@ -91,7 +91,7 @@ export default function TokenInputGroup({ tabIndex={0} >
-
+
{header || (maxValue && From 0c8a5561606376ecf43514f0e54ced03d779dacf Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Tue, 14 May 2024 17:13:13 +1000 Subject: [PATCH 030/192] feat: allow extra subheader fields to TokenInputGroup --- src/components/TokenInputGroup/TokenInputGroup.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/components/TokenInputGroup/TokenInputGroup.tsx b/src/components/TokenInputGroup/TokenInputGroup.tsx index d6a77106c..cd45a9d40 100644 --- a/src/components/TokenInputGroup/TokenInputGroup.tsx +++ b/src/components/TokenInputGroup/TokenInputGroup.tsx @@ -30,6 +30,7 @@ interface InputGroupProps { disabledInput?: boolean; disabledToken?: boolean; header?: ReactNode; + subHeader?: ReactNode; maxValue?: number; } @@ -48,6 +49,7 @@ export default function TokenInputGroup({ denom = token?.base, defaultAssetMode, header, + subHeader, disabled = false, disabledInput = disabled, disabledToken = disabled, @@ -100,6 +102,9 @@ export default function TokenInputGroup({ })}`) || <> }
+ {subHeader && ( +
{subHeader}
+ )} {!disabledInput && token && !!maxValue && Number(maxValue) > 0 && (
@@ -806,12 +823,7 @@ function LimitOrder({ tokenA, tokenB }: { tokenA: Token; tokenB: Token }) { header="You receive" onValueChanged={setAmountOut} token={tokenOut} - value={ - formState?.amountOut || - formatAmount(getDisplayDenomAmount(tokenOut, coinOut) || 0, { - maximumSignificantDigits: 5, - }) - } + value={valueOut} maxValue={0} >
From f9b01ea95e593cdf28efcbd6a79155f4ee17ffc5 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Tue, 14 May 2024 17:16:31 +1000 Subject: [PATCH 032/192] feat: display zero balance to explain why there is no trade preview --- src/components/cards/LimitOrderCard.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/components/cards/LimitOrderCard.tsx b/src/components/cards/LimitOrderCard.tsx index 7309064be..9c11c8fd5 100644 --- a/src/components/cards/LimitOrderCard.tsx +++ b/src/components/cards/LimitOrderCard.tsx @@ -800,6 +800,13 @@ function LimitOrder({ tokenA, tokenB }: { tokenA: Token; tokenB: Token }) { Date: Tue, 14 May 2024 17:19:13 +1000 Subject: [PATCH 033/192] fix: prevent estimated trades from going above user's input balance: - this can be a jarring experience because the estimated trade exists temporarily and will be corrected with "simulate" results - when this value goes outside the user's balance it triggers displayed error states, so an estimate outside the bounds usually appears as a temporary flash of an error state before the simulated response is seen --- src/components/cards/LimitOrderCard.tsx | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/components/cards/LimitOrderCard.tsx b/src/components/cards/LimitOrderCard.tsx index 9c11c8fd5..4bb24f1b0 100644 --- a/src/components/cards/LimitOrderCard.tsx +++ b/src/components/cards/LimitOrderCard.tsx @@ -703,15 +703,25 @@ function LimitOrder({ tokenA, tokenB }: { tokenA: Token; tokenB: Token }) { const coinIn = isValidatingSimulation && lastKnownPrice ? amountOutBaseAmount && lastKnownPrice > 0 - ? new BigNumber(amountOutBaseAmount) - .multipliedBy(estimatedPriceInToOut) - .toNumber() + ? // don't estimate higher than the user's balance because the simulate + // function won't accept an input higher than the user's balance + BigNumber.min( + userBalanceTokenInDisplayAmount || 0, + new BigNumber(amountOutBaseAmount).multipliedBy( + estimatedPriceInToOut + ) + ).toNumber() : 0 : Number(simulationResult?.response?.coin_in.amount || 0); const coinOut = isValidatingSimulation && lastKnownPrice ? amountInBaseAmount && lastKnownPrice > 0 - ? new BigNumber(amountInBaseAmount) + ? // don't estimate higher than the user's balance because the simulate + // function won't accept an input higher than the user's balance + BigNumber.min( + userBalanceTokenInDisplayAmount || 0, + new BigNumber(amountInBaseAmount) + ) .dividedBy(estimatedPriceInToOut || 1) .toNumber() : 0 From 676ec6beb8ba8028f4f686641ec431f8230501f1 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Tue, 14 May 2024 20:14:04 +1000 Subject: [PATCH 034/192] fix: make order confirmation button state based on simulation result - the button should be enabled if the simulation result is of the requested user input (even if the result is refreshing due to liquidity state changes) - the button should be disabled if the simulation result is not of the requested user input, because the estimated result may be very inaccurate (though hopefully it is not) --- src/components/cards/LimitOrderCard.tsx | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/src/components/cards/LimitOrderCard.tsx b/src/components/cards/LimitOrderCard.tsx index 4bb24f1b0..12f9a803b 100644 --- a/src/components/cards/LimitOrderCard.tsx +++ b/src/components/cards/LimitOrderCard.tsx @@ -479,7 +479,17 @@ function LimitOrder({ tokenA, tokenB }: { tokenA: Token; tokenB: Token }) { userBalanceTokenIn, ]); - const lastSimulatedMsgPlaceLimitOrder = useRef(); + // keep track of whether the simulation result was calculated on the given + // user inputs (but the dex price may be different) by tracking last msg + const [lastSimulatedMsgPlaceLimitOrder, setLastSimulatedMsgPlaceLimitOrder] = + useState(); + const simulationResultMatchesInput = useMemo(() => { + return ( + simulatedMsgPlaceLimitOrder !== undefined && + lastSimulatedMsgPlaceLimitOrder === simulatedMsgPlaceLimitOrder + ); + }, [lastSimulatedMsgPlaceLimitOrder, simulatedMsgPlaceLimitOrder]); + const { data: simulationResult, isValidating: isValidatingSimulation } = useSimulatedLimitOrderResult(simulatedMsgPlaceLimitOrder, { // if the limit order payload hasn't changed then keep the previous data @@ -487,16 +497,17 @@ function LimitOrder({ tokenA, tokenB }: { tokenA: Token; tokenB: Token }) { // don't keep when input goes to 0 Number(simulatedMsgPlaceLimitOrder?.amount_in) > 0 || // don't change if input exists and hasn't changed - (simulatedMsgPlaceLimitOrder !== undefined && - lastSimulatedMsgPlaceLimitOrder.current === - simulatedMsgPlaceLimitOrder), + simulationResultMatchesInput, // invalidate the request cache when the liquidity height changes memo: `height of ${meta?.height}`, }); // update last simulatedMsgPlaceLimitOrder for comparison on the next render useEffect(() => { - lastSimulatedMsgPlaceLimitOrder.current = simulatedMsgPlaceLimitOrder; - }, [simulatedMsgPlaceLimitOrder]); + // set only after simulation result has been found + if (!isValidatingSimulation) { + setLastSimulatedMsgPlaceLimitOrder(simulatedMsgPlaceLimitOrder); + } + }, [isValidatingSimulation, simulatedMsgPlaceLimitOrder]); const onFormSubmit = useCallback( function (event?: React.FormEvent) { @@ -951,7 +962,7 @@ function LimitOrder({ tokenA, tokenB }: { tokenA: Token; tokenB: Token }) { disabled={ isValidatingSwap || !simulationResult || - !coinOut || + !simulationResultMatchesInput || (!Number(amountInBaseAmount) && !Number(amountOutBaseAmount)) } > From 19b4e07117ca3181702d5445d5531d73b9571d3e Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Tue, 14 May 2024 20:44:01 +1000 Subject: [PATCH 035/192] feat: add inactive state to token input to indicate validating results --- .../TokenInputGroup/TokenInputGroup.scss | 1 - .../TokenInputGroup/TokenInputGroup.tsx | 16 ++++++++++++++-- src/components/cards/LimitOrderCard.tsx | 2 ++ src/styles/globals.scss | 1 + 4 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/components/TokenInputGroup/TokenInputGroup.scss b/src/components/TokenInputGroup/TokenInputGroup.scss index 15049b9d4..e8bc3b7f1 100644 --- a/src/components/TokenInputGroup/TokenInputGroup.scss +++ b/src/components/TokenInputGroup/TokenInputGroup.scss @@ -59,7 +59,6 @@ background-color: transparent; border: 0px none transparent; font-size: font-size.$input; - color: var(--default-alt); outline: none; } diff --git a/src/components/TokenInputGroup/TokenInputGroup.tsx b/src/components/TokenInputGroup/TokenInputGroup.tsx index cd45a9d40..1072410c0 100644 --- a/src/components/TokenInputGroup/TokenInputGroup.tsx +++ b/src/components/TokenInputGroup/TokenInputGroup.tsx @@ -29,6 +29,7 @@ interface InputGroupProps { disabled?: boolean; disabledInput?: boolean; disabledToken?: boolean; + inactive?: boolean; header?: ReactNode; subHeader?: ReactNode; maxValue?: number; @@ -53,6 +54,7 @@ export default function TokenInputGroup({ disabled = false, disabledInput = disabled, disabledToken = disabled, + inactive = false, maxValue: givenMaxValue, }: InputGroupProps) { const onPickerChange = useCallback( @@ -137,7 +139,11 @@ export default function TokenInputGroup({ - {secondaryValue} + + {secondaryValue} +
diff --git a/src/components/cards/LimitOrderCard.tsx b/src/components/cards/LimitOrderCard.tsx index 12f9a803b..b511a8013 100644 --- a/src/components/cards/LimitOrderCard.tsx +++ b/src/components/cards/LimitOrderCard.tsx @@ -831,6 +831,7 @@ function LimitOrder({ tokenA, tokenB }: { tokenA: Token; tokenB: Token }) { onValueChanged={setAmountIn} token={tokenIn} variant={hasInsufficientFunds && 'error'} + inactive={!formState?.amountIn && isValidatingSimulation} value={valueIn} disabledInput={isLoadingUserBalanceTokenIn} >
@@ -851,6 +852,7 @@ function LimitOrder({ tokenA, tokenB }: { tokenA: Token; tokenB: Token }) { header="You receive" onValueChanged={setAmountOut} token={tokenOut} + inactive={!formState?.amountOut && isValidatingSimulation} value={valueOut} maxValue={0} > diff --git a/src/styles/globals.scss b/src/styles/globals.scss index b38ae2f39..3dadba911 100644 --- a/src/styles/globals.scss +++ b/src/styles/globals.scss @@ -258,6 +258,7 @@ fieldset:disabled *:not(:has(> *)) { input { background-color: transparent; border: none; + color: var(--default-alt); } hr { From d3ff6040b2feb6d5b2a0aad357b7d15d9dcd1c26 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Wed, 15 May 2024 07:30:43 +1000 Subject: [PATCH 036/192] feat: improve unconnected wallet state --- src/components/cards/LimitOrderCard.scss | 4 --- src/components/cards/LimitOrderCard.tsx | 45 ++++++++++++++---------- 2 files changed, 27 insertions(+), 22 deletions(-) diff --git a/src/components/cards/LimitOrderCard.scss b/src/components/cards/LimitOrderCard.scss index 302ffaf60..abdb8dc8c 100644 --- a/src/components/cards/LimitOrderCard.scss +++ b/src/components/cards/LimitOrderCard.scss @@ -112,8 +112,4 @@ // fix vertical alignment align-items: baseline; } - - .limit-order__confirm-button { - font-size: 1.25rem; - } } diff --git a/src/components/cards/LimitOrderCard.tsx b/src/components/cards/LimitOrderCard.tsx index b511a8013..30870169c 100644 --- a/src/components/cards/LimitOrderCard.tsx +++ b/src/components/cards/LimitOrderCard.tsx @@ -762,7 +762,7 @@ function LimitOrder({ tokenA, tokenB }: { tokenA: Token; tokenB: Token }) { // disable fieldset with no address because the estimation requires a signed client const fieldset = ( -
+
) : null}
- + {address ? ( + + ) : ( + + )}
); From b54182379a4b5515d3e56d6a63ace949d0d5ae9d Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Wed, 15 May 2024 07:32:28 +1000 Subject: [PATCH 037/192] fix: improve isLoading state: - isPending stays true when react-query request is not enabled - leaving an unconnected wallet in isPending but !isLoading state --- src/components/cards/LimitOrderCard.tsx | 1 + src/lib/web3/hooks/useSWR.ts | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/components/cards/LimitOrderCard.tsx b/src/components/cards/LimitOrderCard.tsx index 30870169c..59e5410fd 100644 --- a/src/components/cards/LimitOrderCard.tsx +++ b/src/components/cards/LimitOrderCard.tsx @@ -856,6 +856,7 @@ function LimitOrder({ tokenA, tokenB }: { tokenA: Token; tokenB: Token }) { inactive={!formState?.amountOut && isValidatingSimulation} value={valueOut} maxValue={0} + disabledInput={isLoadingUserBalanceTokenIn} >
diff --git a/src/lib/web3/hooks/useSWR.ts b/src/lib/web3/hooks/useSWR.ts index ac5a94190..8ce66027b 100644 --- a/src/lib/web3/hooks/useSWR.ts +++ b/src/lib/web3/hooks/useSWR.ts @@ -39,7 +39,7 @@ export function useSwrResponse( type QueryResultCommon = Pick< UseQueryResult, - 'isPending' | 'isFetching' | 'error' + 'isLoading' | 'isFetching' | 'error' > & { refetch: (opts?: RefetchOptions) => unknown }; export function useSwrResponseFromReactQuery( @@ -49,7 +49,7 @@ export function useSwrResponseFromReactQuery( ): SWRCommon { const swr1 = useMemo( () => ({ - isLoading: queryResult1.isPending, + isLoading: queryResult1.isLoading, isValidating: queryResult1.isFetching, error: queryResult1.error || undefined, refetch: queryResult1.refetch, @@ -58,7 +58,7 @@ export function useSwrResponseFromReactQuery( ); const swr2 = useMemo( () => ({ - isLoading: !!queryResult2?.isPending, + isLoading: !!queryResult2?.isLoading, isValidating: !!queryResult2?.isFetching, error: queryResult2?.error || undefined, refetch: queryResult2?.refetch, @@ -83,7 +83,7 @@ export function useCombineResults(): ( // return memoized data and combined result state return { data: memoizedData.current, - isLoading: results.every((result) => result.isPending), + isLoading: results.every((result) => result.isLoading), isValidating: results.some((result) => result.isFetching), error: results.find((result) => result.error)?.error ?? undefined, refetch: results.find((result) => result.refetch)?.refetch ?? undefined, From abcb1892fe9eb66fd77a469a35f15857be19d802 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Wed, 15 May 2024 07:51:48 +1000 Subject: [PATCH 038/192] fix: estimation limit should be in base denom not display denom --- src/components/cards/LimitOrderCard.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/cards/LimitOrderCard.tsx b/src/components/cards/LimitOrderCard.tsx index 59e5410fd..64e5bbb03 100644 --- a/src/components/cards/LimitOrderCard.tsx +++ b/src/components/cards/LimitOrderCard.tsx @@ -717,7 +717,7 @@ function LimitOrder({ tokenA, tokenB }: { tokenA: Token; tokenB: Token }) { ? // don't estimate higher than the user's balance because the simulate // function won't accept an input higher than the user's balance BigNumber.min( - userBalanceTokenInDisplayAmount || 0, + userBalanceTokenIn || 0, new BigNumber(amountOutBaseAmount).multipliedBy( estimatedPriceInToOut ) @@ -730,7 +730,7 @@ function LimitOrder({ tokenA, tokenB }: { tokenA: Token; tokenB: Token }) { ? // don't estimate higher than the user's balance because the simulate // function won't accept an input higher than the user's balance BigNumber.min( - userBalanceTokenInDisplayAmount || 0, + userBalanceTokenIn || 0, new BigNumber(amountInBaseAmount) ) .dividedBy(estimatedPriceInToOut || 1) From 5ed96068e2402b5acf3f2de524eebdb5665fe340 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Wed, 15 May 2024 10:12:24 +1000 Subject: [PATCH 039/192] feat: make chart a dynamic height --- src/components/cards/LimitOrderCard.tsx | 4 ++-- src/pages/Orderbook/Orderbook.scss | 5 ----- src/pages/Orderbook/Orderbook.tsx | 8 +++----- 3 files changed, 5 insertions(+), 12 deletions(-) diff --git a/src/components/cards/LimitOrderCard.tsx b/src/components/cards/LimitOrderCard.tsx index 64e5bbb03..ae025ba32 100644 --- a/src/components/cards/LimitOrderCard.tsx +++ b/src/components/cards/LimitOrderCard.tsx @@ -962,7 +962,7 @@ function LimitOrder({ tokenA, tokenB }: { tokenA: Token; tokenB: Token }) {
{address ? (
-
- {tokenA && tokenB && ( - - )} -
+ {tokenA && tokenB && ( + + )}
Date: Wed, 15 May 2024 10:13:17 +1000 Subject: [PATCH 040/192] feat: make chart row wider if possible --- src/pages/Orderbook/Orderbook.tsx | 2 +- src/styles/globals.scss | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/pages/Orderbook/Orderbook.tsx b/src/pages/Orderbook/Orderbook.tsx index 736761667..f2800e1e0 100644 --- a/src/pages/Orderbook/Orderbook.tsx +++ b/src/pages/Orderbook/Orderbook.tsx @@ -33,7 +33,7 @@ function Orderbook() { const { data: tokenB } = useToken(denomB); return ( -
+
diff --git a/src/styles/globals.scss b/src/styles/globals.scss index 3dadba911..708b3e258 100644 --- a/src/styles/globals.scss +++ b/src/styles/globals.scss @@ -77,6 +77,12 @@ a { width: 100vw; max-width: calc(1440px + 10vw); } +@media (min-width: 1440px) { + .container .decontainer { + margin: 0 calc(1440px / 2 - 100vw / 2); + padding: 0 5vw; + } +} .row, .col { From c709438c0bfb31c1bf9317020b9f5389bfbecef3 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Wed, 15 May 2024 10:21:23 +1000 Subject: [PATCH 041/192] feat: create space for a chart-depth connection element --- src/pages/Orderbook/Orderbook.scss | 10 +++++ src/pages/Orderbook/Orderbook.tsx | 70 +++++++++++++++++------------- 2 files changed, 49 insertions(+), 31 deletions(-) diff --git a/src/pages/Orderbook/Orderbook.scss b/src/pages/Orderbook/Orderbook.scss index 88fd5e089..f912678e1 100644 --- a/src/pages/Orderbook/Orderbook.scss +++ b/src/pages/Orderbook/Orderbook.scss @@ -5,4 +5,14 @@ .page-card { @include paddings.padding('y', 'md'); } + + .orderbook-chart-book { + background-color: #151924; + border-radius: 1rem; + overflow: hidden; + + .chart-depth-connector { + min-width: paddings.$p-3; + } + } } diff --git a/src/pages/Orderbook/Orderbook.tsx b/src/pages/Orderbook/Orderbook.tsx index f2800e1e0..d65133b22 100644 --- a/src/pages/Orderbook/Orderbook.tsx +++ b/src/pages/Orderbook/Orderbook.tsx @@ -39,37 +39,45 @@ function Orderbook() {
- {tokenA && tokenB && ( - - )} -
-
- { - return [ - { - nav: 'Orderbook', - Tab: () => - tokenA && tokenB ? ( - - ) : null, - }, - { - nav: 'Trades', - Tab: () => - tokenA && tokenB ? ( - - ) : null, - }, - ]; - }, [tokenA, tokenB])} - /> +
+
+ {tokenA && tokenB && ( + + )} +
+
+
+ { + return [ + { + nav: 'Orderbook', + Tab: () => + tokenA && tokenB ? ( + + ) : null, + }, + { + nav: 'Trades', + Tab: () => + tokenA && tokenB ? ( + + ) : null, + }, + ]; + }, [tokenA, tokenB])} + /> +
+
From 3d03a36bf08b5f25b30fb40bf107eac7cce5b298 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Wed, 15 May 2024 11:44:15 +1000 Subject: [PATCH 042/192] feat: use custom colors on TradingView chart --- public/tradingview.css | 4 ++++ src/pages/Orderbook/Orderbook.scss | 2 +- src/pages/Orderbook/OrderbookChart.tsx | 5 +++++ 3 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 public/tradingview.css diff --git a/public/tradingview.css b/public/tradingview.css new file mode 100644 index 000000000..dca4ba146 --- /dev/null +++ b/public/tradingview.css @@ -0,0 +1,4 @@ +:root { + --tv-color-platform-background: var(--page-card, hsl(212deg, 28%, 17%)); + --tv-color-pane-background: var(--default, hsl(219deg, 40%, 11%)); +} diff --git a/src/pages/Orderbook/Orderbook.scss b/src/pages/Orderbook/Orderbook.scss index f912678e1..8cfc84751 100644 --- a/src/pages/Orderbook/Orderbook.scss +++ b/src/pages/Orderbook/Orderbook.scss @@ -7,7 +7,7 @@ } .orderbook-chart-book { - background-color: #151924; + background-color: var(--default); border-radius: 1rem; overflow: hidden; diff --git a/src/pages/Orderbook/OrderbookChart.tsx b/src/pages/Orderbook/OrderbookChart.tsx index 22d7ffdaf..b98c60cba 100644 --- a/src/pages/Orderbook/OrderbookChart.tsx +++ b/src/pages/Orderbook/OrderbookChart.tsx @@ -29,6 +29,11 @@ import { days, hours, minutes, seconds } from '../../lib/utils/time'; const { REACT_APP__INDEXER_API = '', PROD } = import.meta.env; const defaultWidgetOptions: Partial = { + custom_css_url: '/tradingview.css', + settings_overrides: { + 'paneProperties.backgroundType': 'solid', + 'paneProperties.background': '#101828', // var(--default) + }, debug: !PROD, autosize: true, container: '', From a44dbe929269b81662d146bd6b5a5edef273fc81 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Wed, 15 May 2024 11:52:10 +1000 Subject: [PATCH 043/192] feat: decouple chart from depth --- src/pages/Orderbook/Orderbook.scss | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pages/Orderbook/Orderbook.scss b/src/pages/Orderbook/Orderbook.scss index 8cfc84751..e96dcb9a6 100644 --- a/src/pages/Orderbook/Orderbook.scss +++ b/src/pages/Orderbook/Orderbook.scss @@ -7,9 +7,9 @@ } .orderbook-chart-book { - background-color: var(--default); - border-radius: 1rem; - overflow: hidden; + iframe { + border-radius: 1rem; + } .chart-depth-connector { min-width: paddings.$p-3; From f6fd635f9d4e53be59caff13229fc9a8647e3ed4 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Wed, 15 May 2024 12:44:45 +1000 Subject: [PATCH 044/192] feat: add more chart resolutions, default to 5 minute wicks --- src/pages/Orderbook/OrderbookChart.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/pages/Orderbook/OrderbookChart.tsx b/src/pages/Orderbook/OrderbookChart.tsx index b98c60cba..970428a12 100644 --- a/src/pages/Orderbook/OrderbookChart.tsx +++ b/src/pages/Orderbook/OrderbookChart.tsx @@ -119,6 +119,8 @@ export default function OrderBookChart({ const supportedResolutions: ResolutionString[] = [ '1S', // second '1', // minute + '5', // 5 minutes + '10', // 10 minutes '60', // hour '1D', // day ] as ResolutionString[]; @@ -128,6 +130,8 @@ export default function OrderBookChart({ } = { '1S': 'second', // second '1': 'minute', // minute + '5': 'minute', // minute + '10': 'minute', // minute '60': 'hour', // hour '1D': 'day', // day }; @@ -137,6 +141,8 @@ export default function OrderBookChart({ } = { '1S': 1 * seconds, '1': 1 * minutes, + '5': 5 * minutes, + '10': 10 * minutes, '60': 1 * hours, '1D': 1 * days, }; @@ -450,7 +456,7 @@ export default function OrderBookChart({ locale: 'en', symbol: tokenPairID, // start with minute resolution - interval: '1' as ResolutionString, + interval: '5' as ResolutionString, datafeed, }; From 13a088606432fd399a160ca45fa0def02a10135c Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Thu, 16 May 2024 02:54:58 +1000 Subject: [PATCH 045/192] feat: hide the pools tab (but keep the page available) - the page is still accessible through other navigation elements --- src/components/Header/routes.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/Header/routes.ts b/src/components/Header/routes.ts index 35a2e0e9c..f3991b49f 100644 --- a/src/components/Header/routes.ts +++ b/src/components/Header/routes.ts @@ -2,7 +2,6 @@ const { REACT_APP__DEFAULT_PAIR = '' } = import.meta.env; export const pageLinkMap = { [['/swap', REACT_APP__DEFAULT_PAIR].join('/')]: 'Swap', - '/pools': 'Pools', [['/orderbook', REACT_APP__DEFAULT_PAIR].join('/')]: 'Orderbook', '/portfolio': 'Portfolio', '/bridge': 'Bridge', From c1d5a48d374c1231db862fba7e7b172851992c88 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Thu, 16 May 2024 17:28:34 +1000 Subject: [PATCH 046/192] Revert "feat: add more chart resolutions, default to 5 minute wicks" This reverts commit f6fd635f9d4e53be59caff13229fc9a8647e3ed4. --- src/pages/Orderbook/OrderbookChart.tsx | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/pages/Orderbook/OrderbookChart.tsx b/src/pages/Orderbook/OrderbookChart.tsx index 970428a12..b98c60cba 100644 --- a/src/pages/Orderbook/OrderbookChart.tsx +++ b/src/pages/Orderbook/OrderbookChart.tsx @@ -119,8 +119,6 @@ export default function OrderBookChart({ const supportedResolutions: ResolutionString[] = [ '1S', // second '1', // minute - '5', // 5 minutes - '10', // 10 minutes '60', // hour '1D', // day ] as ResolutionString[]; @@ -130,8 +128,6 @@ export default function OrderBookChart({ } = { '1S': 'second', // second '1': 'minute', // minute - '5': 'minute', // minute - '10': 'minute', // minute '60': 'hour', // hour '1D': 'day', // day }; @@ -141,8 +137,6 @@ export default function OrderBookChart({ } = { '1S': 1 * seconds, '1': 1 * minutes, - '5': 5 * minutes, - '10': 10 * minutes, '60': 1 * hours, '1D': 1 * days, }; @@ -456,7 +450,7 @@ export default function OrderBookChart({ locale: 'en', symbol: tokenPairID, // start with minute resolution - interval: '5' as ResolutionString, + interval: '1' as ResolutionString, datafeed, }; From a2166584de6ad2cbc3fee4f49e11bc0c6614ed8e Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Thu, 16 May 2024 17:36:23 +1000 Subject: [PATCH 047/192] feat: switch "Order" heading to "Trade" --- src/components/cards/LimitOrderCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/cards/LimitOrderCard.tsx b/src/components/cards/LimitOrderCard.tsx index ae025ba32..b6fccf35d 100644 --- a/src/components/cards/LimitOrderCard.tsx +++ b/src/components/cards/LimitOrderCard.tsx @@ -225,7 +225,7 @@ function LimitOrder({ tokenA, tokenB }: { tokenA: Token; tokenB: Token }) { const cardModeNav = (
-

Order

+

Trade

className="order-type-input my-4" From 3fc1f7915460f0b8cf4eed5fdad6b3b4566effce Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Thu, 16 May 2024 18:12:30 +1000 Subject: [PATCH 048/192] refactor: use NumberInput for limit order input: - remove previous "formatNumericAmount" that was only needed to add a non-numeric placeholder value in commit: https://github.com/duality-labs/duality-web-app/commit/52de5fc8808185edf583735347f438136bd4f999 --- src/components/cards/LimitOrderCard.tsx | 63 +++++++++---------------- 1 file changed, 23 insertions(+), 40 deletions(-) diff --git a/src/components/cards/LimitOrderCard.tsx b/src/components/cards/LimitOrderCard.tsx index b6fccf35d..9b973d87d 100644 --- a/src/components/cards/LimitOrderCard.tsx +++ b/src/components/cards/LimitOrderCard.tsx @@ -43,6 +43,7 @@ import { useTokenPairMapLiquidity } from '../../lib/web3/hooks/useTickLiquidity' import { useRealtimePrice } from '../stats/hooks'; import TokenInputGroup from '../TokenInputGroup'; +import NumberInput from '../inputs/NumberInput'; import { LimitOrderContextProvider, LimitOrderFormContext, @@ -81,18 +82,6 @@ const [, priceMaxIndex = Number.MAX_SAFE_INTEGER] = const defaultExecutionType: AllowedLimitOrderTypeKey = 'FILL_OR_KILL'; -function formatNumericAmount(defaultValue = '') { - return (amount: number | string) => { - return amount - ? formatAmount( - amount, - { useGrouping: false }, - { reformatSmallValues: false } - ) - : defaultValue; - }; -} - export default function LimitOrderCard({ tokenA, tokenB, @@ -778,34 +767,28 @@ function LimitOrder({ tokenA, tokenB }: { tokenA: Token; tokenB: Token }) {
is worth
- { - // set price - formSetState.setLimitPrice?.(price); - // and remove generated market offset price - switchLimitOption(-1); - }} - suffix={ - tokenA && ( -
-
- -
-
- -
-
- ) - } - format={formatNumericAmount('')} - /> +
+ { + // set price + formSetState.setLimitPrice?.(price); + // and remove generated market offset price + switchLimitOption(-1); + }} + /> +
+
+ +
+
+ +
+
+
className="mt-3 order-type-input text-s" From 954985d1e80924ecc2f3b4f23ad57ebeef8989c1 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Thu, 16 May 2024 18:51:36 +1000 Subject: [PATCH 049/192] feat: add error style for "0" limit price --- src/components/cards/LimitOrderCard.scss | 15 +++++++++++++++ src/components/cards/LimitOrderCard.tsx | 12 +++++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/components/cards/LimitOrderCard.scss b/src/components/cards/LimitOrderCard.scss index abdb8dc8c..3c4c15bee 100644 --- a/src/components/cards/LimitOrderCard.scss +++ b/src/components/cards/LimitOrderCard.scss @@ -59,6 +59,21 @@ background-color: var(--token-search-bg); } } + &.limit-price--error { + &, + .radio-button-group-switch { + color: var(--error); + background-color: hsla(0, 73%, 97%, 1); + border-color: var(--error); + } + .numeric-value-input, + button, + input, + input:disabled { + background-color: hsla(0, 73%, 97%, 1); + color: var(--error); + } + } } .radio-button-group-switch { flex: 0 0 auto; diff --git a/src/components/cards/LimitOrderCard.tsx b/src/components/cards/LimitOrderCard.tsx index 9b973d87d..e341f5416 100644 --- a/src/components/cards/LimitOrderCard.tsx +++ b/src/components/cards/LimitOrderCard.tsx @@ -753,7 +753,17 @@ function LimitOrder({ tokenA, tokenB }: { tokenA: Token; tokenB: Token }) { const fieldset = (
-
+
Date: Thu, 16 May 2024 18:57:01 +1000 Subject: [PATCH 050/192] feat: abstract out LimitPriceInput component --- src/components/cards/LimitOrderCard.tsx | 132 ++++++++++++++---------- 1 file changed, 77 insertions(+), 55 deletions(-) diff --git a/src/components/cards/LimitOrderCard.tsx b/src/components/cards/LimitOrderCard.tsx index e341f5416..1bb5ad170 100644 --- a/src/components/cards/LimitOrderCard.tsx +++ b/src/components/cards/LimitOrderCard.tsx @@ -753,61 +753,20 @@ function LimitOrder({ tokenA, tokenB }: { tokenA: Token; tokenB: Token }) { const fieldset = (
-
-
- When 1 -
- -
-
- -
- is worth -
-
- { - // set price - formSetState.setLimitPrice?.(price); - // and remove generated market offset price - switchLimitOption(-1); - }} - /> -
-
- -
-
- -
-
-
-
- - className="mt-3 order-type-input text-s" - values={buyMode ? buyLimitPriceOptions : sellLimitPriceOptions} - value={limitOption} - onChange={switchLimitOption} - /> -
-
+ { + // set price + formSetState.setLimitPrice?.(price); + // and remove generated market offset price + switchLimitOption(-1); + }} + limitOptions={buyMode ? buyLimitPriceOptions : sellLimitPriceOptions} + limitOption={limitOption} + onLimitOptionChange={switchLimitOption} + />
 
@@ -1096,3 +1055,66 @@ function NumericValueRow({
); } + +function LimitPriceInput({ + tokenA, + tokenB, + limitPrice, + onLimitPriceChange, + limitOptions, + limitOption, + onLimitOptionChange, +}: { + tokenA: Token; + tokenB: Token; + limitPrice: string; + onLimitPriceChange: (limitPrice: string) => void; + limitOptions: Record; + limitOption: LimitPriceOptions; + onLimitOptionChange: (limitOption: LimitPriceOptions) => void; +}) { + return ( +
+
+ When 1 +
+ +
+
+ +
+ is worth +
+
+ +
+
+ +
+
+ +
+
+
+
+ + className="mt-3 order-type-input text-s" + values={limitOptions} + value={limitOption} + onChange={onLimitOptionChange} + /> +
+
+ ); +} From d2524dd1f1c64bcdd4493e383ba93d75f8ae592d Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Thu, 16 May 2024 19:37:33 +1000 Subject: [PATCH 051/192] fix: tabbing in TokenInputGroup where Tokens cannot be changed --- src/components/TokenInputGroup/TokenInputGroup.tsx | 4 ++-- src/components/TokenPicker/SelectionModal.tsx | 4 ++-- src/components/TokenPicker/TokenPairPicker.tsx | 2 +- src/components/TokenPicker/TokenPicker.tsx | 3 ++- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/components/TokenInputGroup/TokenInputGroup.tsx b/src/components/TokenInputGroup/TokenInputGroup.tsx index 1072410c0..069bba264 100644 --- a/src/components/TokenInputGroup/TokenInputGroup.tsx +++ b/src/components/TokenInputGroup/TokenInputGroup.tsx @@ -90,9 +90,9 @@ export default function TokenInputGroup({ .filter(Boolean) .join(' ')} onClick={() => inputRef.current?.focus()} - onKeyDown={() => inputRef.current?.focus()} + onKeyDown={() => undefined} role="button" - tabIndex={0} + tabIndex={-1} >
diff --git a/src/components/TokenPicker/SelectionModal.tsx b/src/components/TokenPicker/SelectionModal.tsx index d99b1327f..b311371c3 100644 --- a/src/components/TokenPicker/SelectionModal.tsx +++ b/src/components/TokenPicker/SelectionModal.tsx @@ -58,7 +58,7 @@ export interface SelectionModalChild { className?: string; disabled?: boolean; value: T | undefined; - open: () => void; + open?: () => void; } export type SelectionModal = Omit< SelectionModalDialog, @@ -98,7 +98,7 @@ export default function SelectionModal({ .join(' ')} disabled={disabled} value={value} - open={open} + open={onChange && open} /> {isOpen && onChange && ( diff --git a/src/components/TokenPicker/TokenPairPicker.tsx b/src/components/TokenPicker/TokenPairPicker.tsx index 0019ee8de..2e8185a3b 100644 --- a/src/components/TokenPicker/TokenPairPicker.tsx +++ b/src/components/TokenPicker/TokenPairPicker.tsx @@ -44,7 +44,7 @@ function OpenTokenPairPickerButton({ className?: string; value: TokenPair | undefined; disabled?: boolean; - open: () => void; + open?: () => void; }) { const [tokenA, tokenB] = value || []; return ( diff --git a/src/components/TokenPicker/TokenPicker.tsx b/src/components/TokenPicker/TokenPicker.tsx index faef917b5..72bc25b78 100644 --- a/src/components/TokenPicker/TokenPicker.tsx +++ b/src/components/TokenPicker/TokenPicker.tsx @@ -50,7 +50,7 @@ function OpenTokenPickerButton({ className?: string; value: Token | undefined; disabled?: boolean; - open: () => void; + open?: () => void; showChain?: boolean; }) { return ( @@ -59,6 +59,7 @@ function OpenTokenPickerButton({ className={[className, 'my-1'].filter(Boolean).join(' ')} onClick={open} disabled={disabled} + tabIndex={open ? 0 : -1} > {value?.logo_URIs ? ( Date: Thu, 16 May 2024 19:41:18 +1000 Subject: [PATCH 052/192] feat: allow input focus on LimitPrice group click --- src/components/cards/LimitOrderCard.tsx | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/components/cards/LimitOrderCard.tsx b/src/components/cards/LimitOrderCard.tsx index 1bb5ad170..9b40718db 100644 --- a/src/components/cards/LimitOrderCard.tsx +++ b/src/components/cards/LimitOrderCard.tsx @@ -1073,6 +1073,14 @@ function LimitPriceInput({ limitOption: LimitPriceOptions; onLimitOptionChange: (limitOption: LimitPriceOptions) => void; }) { + const inputRef = useRef(null); + const handleClick = useCallback((event: { target: EventTarget }) => { + // only focus input if click comes from an unfocusable element + const target = event.target as HTMLElement | null; + if (target && target.tabIndex < 0) { + inputRef.current?.focus(); + } + }, []); return (
is worth
- +
From b5f1ca41a27ea318d6cdce7c92dd858d93c6414f Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Thu, 16 May 2024 19:41:50 +1000 Subject: [PATCH 053/192] fix: allow unrounded numbers in Custom time amount input --- src/components/cards/LimitOrderCard.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/cards/LimitOrderCard.tsx b/src/components/cards/LimitOrderCard.tsx index 9b40718db..c1ff0dd75 100644 --- a/src/components/cards/LimitOrderCard.tsx +++ b/src/components/cards/LimitOrderCard.tsx @@ -834,6 +834,7 @@ function LimitOrder({ tokenA, tokenB }: { tokenA: Token; tokenB: Token }) { { setExpiration('custom'); From 7998a0bddcbe217e5eb3598b7538c328c6e8c368 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Thu, 16 May 2024 20:02:39 +1000 Subject: [PATCH 054/192] feat: allow "Custom" PriceLimit shortcut button: - to make it obvious that the user can select a custom limit price --- src/components/cards/LimitOrderCard.tsx | 49 ++++++++++++++++--------- 1 file changed, 32 insertions(+), 17 deletions(-) diff --git a/src/components/cards/LimitOrderCard.tsx b/src/components/cards/LimitOrderCard.tsx index c1ff0dd75..f638519e8 100644 --- a/src/components/cards/LimitOrderCard.tsx +++ b/src/components/cards/LimitOrderCard.tsx @@ -127,14 +127,14 @@ const buyLimitPriceOptions = { 1: '-1%', 5: '-5%', 10: '-10%', - [-1]: '', + [-1]: 'Custom', } as const; const sellLimitPriceOptions = { 0: 'Market', 1: '+1%', 5: '+5%', 10: '+10%', - [-1]: '', + [-1]: 'Custom', } as const; type LimitPriceOptions = | keyof typeof buyLimitPriceOptions @@ -285,18 +285,6 @@ function LimitOrder({ tokenA, tokenB }: { tokenA: Token; tokenB: Token }) { const [limitOption, setLimitOption] = useState(0); - const switchLimitOption = useCallback( - (limitOption: LimitPriceOptions) => { - // set value - setLimitOption(limitOption); - // remove custom limit price if setting one here - if (Number(limitOption) >= 0) { - formSetState.setLimitPrice?.(''); - } - }, - [formSetState] - ); - // combine offset and current price to get dynamic limit price const offsetLimitPrice = useMemo(() => { if (currentPriceAtoB && limitOption >= 0) { @@ -309,6 +297,20 @@ function LimitOrder({ tokenA, tokenB }: { tokenA: Token; tokenB: Token }) { } }, [buyMode, currentPriceAtoB, limitOption]); + const switchLimitOption = useCallback( + (limitOption: LimitPriceOptions) => { + // set value + setLimitOption(limitOption); + // remove custom limit price if setting one here + if (Number(limitOption) >= 0) { + formSetState.setLimitPrice?.(''); + } else { + formSetState.setLimitPrice?.(offsetLimitPrice || ''); + } + }, + [formSetState, offsetLimitPrice] + ); + // detect when the user has asked for a limit "outside liquidity bounds" // in these cases the simulation won't help because it can only compute the // immediate result of the msg tx: the future amountOut must be estimated @@ -758,10 +760,12 @@ function LimitOrder({ tokenA, tokenB }: { tokenA: Token; tokenB: Token }) { tokenB={tokenB} limitPrice={formState.limitPrice || offsetLimitPrice || ''} onLimitPriceChange={(price) => { + // and remove generated market offset price + if (limitOption >= 0) { + switchLimitOption(-1); + } // set price formSetState.setLimitPrice?.(price); - // and remove generated market offset price - switchLimitOption(-1); }} limitOptions={buyMode ? buyLimitPriceOptions : sellLimitPriceOptions} limitOption={limitOption} @@ -1082,6 +1086,17 @@ function LimitPriceInput({ inputRef.current?.focus(); } }, []); + const handleLimitOptionChange = useCallback( + (limitOption: LimitPriceOptions) => { + // action callback + onLimitOptionChange(limitOption); + // focus input when "custom" is selected + if (limitOption < 0) { + inputRef.current?.focus(); + } + }, + [onLimitOptionChange] + ); return (
From 5934992c2afa7f1619cd638a5f4142dbd8557195 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Thu, 16 May 2024 20:07:17 +1000 Subject: [PATCH 055/192] feat: select all limit price input on "Custom" price limit shortcut --- src/components/cards/LimitOrderCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/cards/LimitOrderCard.tsx b/src/components/cards/LimitOrderCard.tsx index f638519e8..54be2d8f6 100644 --- a/src/components/cards/LimitOrderCard.tsx +++ b/src/components/cards/LimitOrderCard.tsx @@ -1092,7 +1092,7 @@ function LimitPriceInput({ onLimitOptionChange(limitOption); // focus input when "custom" is selected if (limitOption < 0) { - inputRef.current?.focus(); + inputRef.current?.select(); } }, [onLimitOptionChange] From 4f0f5a6300090ef8f9e22dcefc93d36afe4942cb Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Thu, 16 May 2024 20:45:21 +1000 Subject: [PATCH 056/192] feat: refine expiration time shortcuts --- src/components/cards/LimitOrderCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/cards/LimitOrderCard.tsx b/src/components/cards/LimitOrderCard.tsx index 54be2d8f6..63da18da8 100644 --- a/src/components/cards/LimitOrderCard.tsx +++ b/src/components/cards/LimitOrderCard.tsx @@ -114,10 +114,10 @@ type PriceTab = typeof priceTabs[number]; const expirationOptions = { none: 'None', + '1 hours': '1 hour', '1 days': '1 day', '1 weeks': '1 week', '1 months': '1 month', - '1 years': '1 year', custom: 'Custom', } as const; type ExpirationOptions = keyof typeof expirationOptions; From b11da0dab5856b041f94453c1a9da51104c949d7 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Thu, 16 May 2024 20:45:51 +1000 Subject: [PATCH 057/192] feat: allow expiration time shortcut to prefill custom expiration time --- src/components/cards/LimitOrderCard.tsx | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/components/cards/LimitOrderCard.tsx b/src/components/cards/LimitOrderCard.tsx index 63da18da8..9baa2e572 100644 --- a/src/components/cards/LimitOrderCard.tsx +++ b/src/components/cards/LimitOrderCard.tsx @@ -186,6 +186,19 @@ function LimitOrder({ tokenA, tokenB }: { tokenA: Token; tokenB: Token }) { const formSetState = useContext(LimitOrderFormSetContext); const [expiration, setExpiration] = useState('none'); + // allow an expiration change to change the custom time amount and period + const switchExpiration = useCallback( + (expiration: ExpirationOptions) => { + setExpiration(expiration); + const timeParts = expiration.split(' ') as [string, TimePeriod]; + if (timeParts.length > 1) { + const [timeAmount, timePeriod] = timeParts; + formSetState.setTimeAmount?.(timeAmount); + formSetState.setTimePeriod?.(timePeriod); + } + }, + [formSetState] + ); const hasExpiry = expiration !== 'none'; const switchModeTab = useCallback( @@ -824,7 +837,7 @@ function LimitOrder({ tokenA, tokenB }: { tokenA: Token; tokenB: Token }) { className="order-type-input text-s" values={expirationOptions} value={expiration} - onChange={setExpiration} + onChange={switchExpiration} />
From 75120295b521b43293413d0271ab3bab02de7548 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Thu, 16 May 2024 20:49:20 +1000 Subject: [PATCH 058/192] fix: change form values before animation makes the change visible --- src/components/cards/LimitOrderCard.tsx | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/components/cards/LimitOrderCard.tsx b/src/components/cards/LimitOrderCard.tsx index 9baa2e572..256aa7079 100644 --- a/src/components/cards/LimitOrderCard.tsx +++ b/src/components/cards/LimitOrderCard.tsx @@ -188,16 +188,19 @@ function LimitOrder({ tokenA, tokenB }: { tokenA: Token; tokenB: Token }) { const [expiration, setExpiration] = useState('none'); // allow an expiration change to change the custom time amount and period const switchExpiration = useCallback( - (expiration: ExpirationOptions) => { - setExpiration(expiration); - const timeParts = expiration.split(' ') as [string, TimePeriod]; - if (timeParts.length > 1) { - const [timeAmount, timePeriod] = timeParts; - formSetState.setTimeAmount?.(timeAmount); - formSetState.setTimePeriod?.(timePeriod); + (newExpiration: ExpirationOptions) => { + // when selecting custom time: prefill the form with the previous shortcut + if (newExpiration === 'custom') { + const timeParts = expiration.split(' ') as [string, TimePeriod]; + if (timeParts.length > 1) { + const [timeAmount, timePeriod] = timeParts; + formSetState.setTimeAmount?.(timeAmount); + formSetState.setTimePeriod?.(timePeriod); + } } + setExpiration(newExpiration); }, - [formSetState] + [formSetState, expiration] ); const hasExpiry = expiration !== 'none'; From a309054d56a33a0fd8b6248fd48a2bac8b0f8a7e Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Thu, 16 May 2024 20:53:32 +1000 Subject: [PATCH 059/192] feat: refine wording of "My Orders" table --- src/pages/Orderbook/OrderbookFooter.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/Orderbook/OrderbookFooter.tsx b/src/pages/Orderbook/OrderbookFooter.tsx index 8d2297e3f..970968d19 100644 --- a/src/pages/Orderbook/OrderbookFooter.tsx +++ b/src/pages/Orderbook/OrderbookFooter.tsx @@ -64,7 +64,7 @@ export default function OrderbookFooter({ const [filter, setFilter] = useState('recent'); return ( - title="Orders" + title="My Orders" className="pb-5 flex" scrolling={false} switchValues={switchValues} From e031c6a671761e45d2cae66242b2a3fd1010f6b4 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Thu, 16 May 2024 21:24:13 +1000 Subject: [PATCH 060/192] feat: make focusability of limit order inputs more visible: - add border color highlight when highlighted or focused --- src/components/cards/LimitOrderCard.scss | 12 ++++++++++-- src/components/cards/LimitOrderCard.tsx | 12 ++++++++---- src/styles/components/cards.scss | 17 ++++++++++++++++- 3 files changed, 34 insertions(+), 7 deletions(-) diff --git a/src/components/cards/LimitOrderCard.scss b/src/components/cards/LimitOrderCard.scss index 3c4c15bee..df0ba9745 100644 --- a/src/components/cards/LimitOrderCard.scss +++ b/src/components/cards/LimitOrderCard.scss @@ -102,8 +102,8 @@ .numeric-value-input { color: hsl(218deg, 11%, 65%); - background-color: hsla(216, 20%, 25%, 1); - border: 1px solid var(--page-card-border); + background-color: var(--default); + border: 1px solid var(--page-card); border-radius: paddings.$p-3; padding: paddings.$p-3 paddings.$p-4; input { @@ -127,4 +127,12 @@ // fix vertical alignment align-items: baseline; } + + .select-input { + .select-input-selection, + .select-input-group, + .select-input-group label { + background-color: var(--default); + } + } } diff --git a/src/components/cards/LimitOrderCard.tsx b/src/components/cards/LimitOrderCard.tsx index 256aa7079..41eab8847 100644 --- a/src/components/cards/LimitOrderCard.tsx +++ b/src/components/cards/LimitOrderCard.tsx @@ -772,6 +772,7 @@ function LimitOrder({ tokenA, tokenB }: { tokenA: Token; tokenB: Token }) {
- className="flex col m-0 p-0" + className="focusable flex col m-0 p-0" list={timePeriods.slice()} getLabel={(key = 'days') => timePeriodLabels[key]} value={formState.timePeriod} @@ -1078,6 +1079,7 @@ function NumericValueRow({ } function LimitPriceInput({ + className, tokenA, tokenB, limitPrice, @@ -1086,6 +1088,7 @@ function LimitPriceInput({ limitOption, onLimitOptionChange, }: { + className?: string; tokenA: Token; tokenB: Token; limitPrice: string; @@ -1116,6 +1119,7 @@ function LimitPriceInput({ return (
.select-input-selection { + & { + border-color: var(--page-card); + } + &:hover, + &:focus-within { + border-color: var(--page-card-border); + } + input { + outline: none; + } + } + .page-card { margin-top: 0; margin-bottom: 0; From a6160193faa73bad0bbe6c01336c26f381ea9359 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Thu, 16 May 2024 22:42:42 +1000 Subject: [PATCH 061/192] Revert "Revert "feat: add more chart resolutions, default to 5 minute wicks"" This reverts commit c1d5a48d374c1231db862fba7e7b172851992c88. --- src/pages/Orderbook/OrderbookChart.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/pages/Orderbook/OrderbookChart.tsx b/src/pages/Orderbook/OrderbookChart.tsx index b98c60cba..970428a12 100644 --- a/src/pages/Orderbook/OrderbookChart.tsx +++ b/src/pages/Orderbook/OrderbookChart.tsx @@ -119,6 +119,8 @@ export default function OrderBookChart({ const supportedResolutions: ResolutionString[] = [ '1S', // second '1', // minute + '5', // 5 minutes + '10', // 10 minutes '60', // hour '1D', // day ] as ResolutionString[]; @@ -128,6 +130,8 @@ export default function OrderBookChart({ } = { '1S': 'second', // second '1': 'minute', // minute + '5': 'minute', // minute + '10': 'minute', // minute '60': 'hour', // hour '1D': 'day', // day }; @@ -137,6 +141,8 @@ export default function OrderBookChart({ } = { '1S': 1 * seconds, '1': 1 * minutes, + '5': 5 * minutes, + '10': 10 * minutes, '60': 1 * hours, '1D': 1 * days, }; @@ -450,7 +456,7 @@ export default function OrderBookChart({ locale: 'en', symbol: tokenPairID, // start with minute resolution - interval: '1' as ResolutionString, + interval: '5' as ResolutionString, datafeed, }; From 73daf77498c3bb09d0ac5be707c2ca32d54bd5ac Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Fri, 17 May 2024 10:14:05 +1000 Subject: [PATCH 062/192] feat: add normal page-card background to OrderbookChart card --- src/pages/Orderbook/Orderbook.scss | 4 ++++ src/pages/Orderbook/Orderbook.tsx | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/pages/Orderbook/Orderbook.scss b/src/pages/Orderbook/Orderbook.scss index e96dcb9a6..4b3815071 100644 --- a/src/pages/Orderbook/Orderbook.scss +++ b/src/pages/Orderbook/Orderbook.scss @@ -7,6 +7,10 @@ } .orderbook-chart-book { + .page-card:has(iframe) { + border-width: 0; + } + iframe { border-radius: 1rem; } diff --git a/src/pages/Orderbook/Orderbook.tsx b/src/pages/Orderbook/Orderbook.tsx index d65133b22..aac4dfdaf 100644 --- a/src/pages/Orderbook/Orderbook.tsx +++ b/src/pages/Orderbook/Orderbook.tsx @@ -40,7 +40,7 @@ function Orderbook() {
-
+
{tokenA && tokenB && ( )} From a8a9b4127dfcb491a25ccb01fe9b0dc33c71d92a Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Fri, 17 May 2024 10:17:55 +1000 Subject: [PATCH 063/192] feat: visually connect the chart and depth cards with background color --- src/pages/Orderbook/Orderbook.scss | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/pages/Orderbook/Orderbook.scss b/src/pages/Orderbook/Orderbook.scss index 4b3815071..670e03891 100644 --- a/src/pages/Orderbook/Orderbook.scss +++ b/src/pages/Orderbook/Orderbook.scss @@ -7,12 +7,15 @@ } .orderbook-chart-book { - .page-card:has(iframe) { - border-width: 0; + & { + background-color: var(--default); + border-radius: 1rem; } - iframe { - border-radius: 1rem; + .page-card:has(iframe) { + border-width: 0; + border-top-right-radius: 0; + border-bottom-right-radius: 0; } .chart-depth-connector { From d8083306bc1be4f2addf5ea5bf89da98c713262b Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Fri, 17 May 2024 10:47:30 +1000 Subject: [PATCH 064/192] feat: add order depth price indication tracking --- src/pages/Orderbook/Orderbook.tsx | 13 ++++++++++--- src/pages/Orderbook/OrderbookList.tsx | 28 +++++++++++++++++++++++++-- 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/src/pages/Orderbook/Orderbook.tsx b/src/pages/Orderbook/Orderbook.tsx index aac4dfdaf..f52d497e2 100644 --- a/src/pages/Orderbook/Orderbook.tsx +++ b/src/pages/Orderbook/Orderbook.tsx @@ -1,4 +1,4 @@ -import { useMemo } from 'react'; +import { useMemo, useState } from 'react'; import { useMatch } from 'react-router-dom'; import { useDenomFromPathParam } from '../../lib/web3/hooks/useTokens'; @@ -32,6 +32,8 @@ function Orderbook() { const { data: tokenA } = useToken(denomA); const { data: tokenB } = useToken(denomB); + const [depthPriceIndication, setDepthPriceIndication] = useState(); + return (
@@ -60,7 +62,12 @@ function Orderbook() { nav: 'Orderbook', Tab: () => tokenA && tokenB ? ( - + ) : null, }, { @@ -74,7 +81,7 @@ function Orderbook() { ) : null, }, ]; - }, [tokenA, tokenB])} + }, [depthPriceIndication, tokenA, tokenB])} />
diff --git a/src/pages/Orderbook/OrderbookList.tsx b/src/pages/Orderbook/OrderbookList.tsx index ea23b577a..1836c1303 100644 --- a/src/pages/Orderbook/OrderbookList.tsx +++ b/src/pages/Orderbook/OrderbookList.tsx @@ -22,9 +22,12 @@ const spacingTicks = Array.from({ length: shownTickRows }).map(() => undefined); export default function OrderBookList({ tokenA, tokenB, + setPriceIndication, }: { tokenA: Token; tokenB: Token; + priceIndication?: number | undefined; + setPriceIndication?: React.Dispatch>; }) { const [tokenIdA, tokenIdB] = [getTokenId(tokenA), getTokenId(tokenB)]; const [tokenId0, tokenId1] = useOrderedTokenPair([tokenIdA, tokenIdB]) || []; @@ -37,6 +40,13 @@ export default function OrderBookList({ tokenId1 === tokenIdA && tokenId0 === tokenIdB, ]; + const onHighlightPrice = useCallback( + (tick: TickInfo | undefined) => { + setPriceIndication?.(tick && tick?.price1To0.toNumber()); + }, + [setPriceIndication] + ); + const [, currentPrice] = useRealtimePrice(tokenA, tokenB); const resolutionPercent = 0.01; // size of price steps @@ -225,12 +235,16 @@ export default function OrderBookList({ token={tokenA} reserveKey={forward ? 'reserve0' : 'reserve1'} priceDecimalPlaces={priceDecimalPlaces} + onHighlight={onHighlightPrice} /> ); })} - + setPriceIndication?.(currentPrice || undefined)} + onMouseLeave={() => setPriceIndication?.(undefined)} + > ); })} @@ -274,6 +289,7 @@ function OrderbookListRow({ reserveKey, priceDecimalPlaces = 6, amountDecimalPlaces = 2, + onHighlight, }: { tick: TickInfo | undefined; previousTicks: TickInfo[]; @@ -281,8 +297,12 @@ function OrderbookListRow({ reserveKey: 'reserve0' | 'reserve1'; priceDecimalPlaces?: number; amountDecimalPlaces?: number; + onHighlight?: (tick: TickInfo | undefined) => void; }) { const { data: price } = useSimplePrice(token); + const onHover = useCallback(() => onHighlight?.(tick), [onHighlight, tick]); + const onHoverOut = useCallback(() => onHighlight?.(undefined), [onHighlight]); + // add empty row if (!tick) { return ( @@ -301,7 +321,11 @@ function OrderbookListRow({ const value = getTokenValue(token, tick[reserveKey], price); return ( - + {formatAmount(tick.price1To0.toNumber(), { minimumFractionDigits: priceDecimalPlaces, From 7409a577453f163bd3ba2b124e0a75f576cb57b4 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Fri, 17 May 2024 11:36:39 +1000 Subject: [PATCH 065/192] fix: higher prices should be at the top of the depth table --- src/pages/Orderbook/OrderbookList.tsx | 30 ++++++++++++++++++--------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/src/pages/Orderbook/OrderbookList.tsx b/src/pages/Orderbook/OrderbookList.tsx index 1836c1303..0c2ca6850 100644 --- a/src/pages/Orderbook/OrderbookList.tsx +++ b/src/pages/Orderbook/OrderbookList.tsx @@ -203,6 +203,12 @@ export default function OrderBookList({ } }, [currentPrice, filteredTokenATicks]); + const [topTicks, bottomTicks] = useMemo(() => { + return forward + ? [filteredTokenBTicks.reverse(), filteredTokenATicks.reverse()] + : [filteredTokenATicks, filteredTokenBTicks]; + }, [filteredTokenATicks, filteredTokenBTicks, forward]); + const priceDecimalPlaces = currentPrice !== undefined && currentPrice !== null ? getDecimalPlaces( @@ -225,15 +231,17 @@ export default function OrderBookList({ Amount - - {filteredTokenATicks.map((tick, index) => { + + {topTicks.map((tick, index) => { return ( @@ -262,15 +270,17 @@ export default function OrderBookList({ - - {filteredTokenBTicks.map((tick, index) => { + + {bottomTicks.map((tick, index) => { return ( From 5144c808eaed97a18a047e159f2650948dd4cfb5 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Fri, 17 May 2024 11:43:38 +1000 Subject: [PATCH 066/192] feat: show priceIndication on depth table --- src/pages/Orderbook/OrderbookList.scss | 4 ++++ src/pages/Orderbook/OrderbookList.tsx | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/src/pages/Orderbook/OrderbookList.scss b/src/pages/Orderbook/OrderbookList.scss index ccc494a34..c8ed36df5 100644 --- a/src/pages/Orderbook/OrderbookList.scss +++ b/src/pages/Orderbook/OrderbookList.scss @@ -17,6 +17,10 @@ text-align: left; } } + .active td { + background-color: var(--page-card-border); + } + // add cell spacing & { th { diff --git a/src/pages/Orderbook/OrderbookList.tsx b/src/pages/Orderbook/OrderbookList.tsx index 0c2ca6850..eb62e4ab7 100644 --- a/src/pages/Orderbook/OrderbookList.tsx +++ b/src/pages/Orderbook/OrderbookList.tsx @@ -22,6 +22,7 @@ const spacingTicks = Array.from({ length: shownTickRows }).map(() => undefined); export default function OrderBookList({ tokenA, tokenB, + priceIndication, setPriceIndication, }: { tokenA: Token; @@ -233,6 +234,7 @@ export default function OrderBookList({ {topTicks.map((tick, index) => { + const price = tick && tick?.price1To0.toNumber(); return ( ); @@ -272,6 +275,7 @@ export default function OrderBookList({ {bottomTicks.map((tick, index) => { + const price = tick && tick?.price1To0.toNumber(); return ( = priceIndication} onHighlight={onHighlightPrice} /> ); @@ -299,6 +304,7 @@ function OrderbookListRow({ reserveKey, priceDecimalPlaces = 6, amountDecimalPlaces = 2, + active, onHighlight, }: { tick: TickInfo | undefined; @@ -307,6 +313,7 @@ function OrderbookListRow({ reserveKey: 'reserve0' | 'reserve1'; priceDecimalPlaces?: number; amountDecimalPlaces?: number; + active?: boolean | 0; onHighlight?: (tick: TickInfo | undefined) => void; }) { const { data: price } = useSimplePrice(token); @@ -333,6 +340,7 @@ function OrderbookListRow({ return ( From 85eeecc83f8e00a734a20cd7366b83a27ecc65f5 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Fri, 17 May 2024 19:11:44 +1000 Subject: [PATCH 067/192] feat: add depth price indication line on depth table hover --- src/pages/Orderbook/OrderbookList.scss | 12 +++++++++++- src/pages/Orderbook/OrderbookList.tsx | 22 ++++++++++++++++++---- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/src/pages/Orderbook/OrderbookList.scss b/src/pages/Orderbook/OrderbookList.scss index c8ed36df5..a287c8d30 100644 --- a/src/pages/Orderbook/OrderbookList.scss +++ b/src/pages/Orderbook/OrderbookList.scss @@ -6,12 +6,16 @@ font-size: 0.75em; color: #f9fafb; border-spacing: 0; - border-collapse: collapse; th { border: 1px solid var(--page-card-border); border-left: none; border-right: none; } + td { + border: 1px solid transparent; + border-left: none; + border-right: none; + } .orderbook-list__table__tick-center { td { text-align: left; @@ -20,6 +24,12 @@ .active td { background-color: var(--page-card-border); } + .orderbook-list__table__ticks-top .hovered td { + border-top-color: var(--text-default); + } + .orderbook-list__table__ticks-bottom .hovered td { + border-bottom-color: var(--text-default); + } // add cell spacing & { diff --git a/src/pages/Orderbook/OrderbookList.tsx b/src/pages/Orderbook/OrderbookList.tsx index eb62e4ab7..a6c7ecedd 100644 --- a/src/pages/Orderbook/OrderbookList.tsx +++ b/src/pages/Orderbook/OrderbookList.tsx @@ -317,8 +317,20 @@ function OrderbookListRow({ onHighlight?: (tick: TickInfo | undefined) => void; }) { const { data: price } = useSimplePrice(token); - const onHover = useCallback(() => onHighlight?.(tick), [onHighlight, tick]); - const onHoverOut = useCallback(() => onHighlight?.(undefined), [onHighlight]); + + // keep track of hover state and use as a price indication in parents + const [hovered, setHovered] = useState(false); + const onHover = useCallback( + (tick: TickInfo | undefined) => { + // set hovered state + setHovered(!!tick); + // set parent state + onHighlight?.(tick); + }, + [onHighlight] + ); + const onHoverIn = useCallback(() => onHover(tick), [onHover, tick]); + const onHoverOut = useCallback(() => onHover(undefined), [onHover]); // add empty row if (!tick) { @@ -340,8 +352,10 @@ function OrderbookListRow({ return ( From 2a9625c1ea0275e73f9857df3290035c536ac396 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Fri, 17 May 2024 19:16:18 +1000 Subject: [PATCH 068/192] fix: Tab components were causing abrupt re-renders when not needed --- src/components/Tabs/Tabs.tsx | 8 +++----- src/components/cards/TabsCard.tsx | 6 ++---- src/pages/Orderbook/Orderbook.tsx | 9 +++++---- src/pages/Pool/PoolOverview.tsx | 8 ++++---- 4 files changed, 14 insertions(+), 17 deletions(-) diff --git a/src/components/Tabs/Tabs.tsx b/src/components/Tabs/Tabs.tsx index 1d0b7da86..6dfd8880f 100644 --- a/src/components/Tabs/Tabs.tsx +++ b/src/components/Tabs/Tabs.tsx @@ -4,7 +4,7 @@ import './Tabs.scss'; export interface Tab { nav: ReactNode; - Tab: React.FunctionComponent; + tab?: ReactNode; } export default function Tabs({ tabs, @@ -23,7 +23,7 @@ export default function Tabs({ givenTabIndex !== undefined ? [givenTabIndex, givenSetTabIndex] : [defaultTabIndex, defaultSetTabIndex]; - const { Tab } = tabs[tabIndex]; + const { tab } = tabs[tabIndex]; return (
@@ -47,9 +47,7 @@ export default function Tabs({ })}
-
- -
+
{tab}
); diff --git a/src/components/cards/TabsCard.tsx b/src/components/cards/TabsCard.tsx index cc11e60db..2bba0062e 100644 --- a/src/components/cards/TabsCard.tsx +++ b/src/components/cards/TabsCard.tsx @@ -12,7 +12,7 @@ export default function TabsCard({ tabs: Array; }) { const [tabIndex, setTabIndex] = useState(0); - const { Tab } = tabs[tabIndex]; + const { tab } = tabs[tabIndex]; return (
-
- -
+
{tab}
); diff --git a/src/pages/Orderbook/Orderbook.tsx b/src/pages/Orderbook/Orderbook.tsx index f52d497e2..b7f421ece 100644 --- a/src/pages/Orderbook/Orderbook.tsx +++ b/src/pages/Orderbook/Orderbook.tsx @@ -5,14 +5,15 @@ import { useDenomFromPathParam } from '../../lib/web3/hooks/useTokens'; import { useToken } from '../../lib/web3/hooks/useDenomClients'; import TabsCard from '../../components/cards/TabsCard'; +import { Tab } from '../../components/Tabs/Tabs'; import OrderbookHeader from './OrderbookHeader'; import OrderbookFooter from './OrderbookFooter'; import OrderBookChart from './OrderbookChart'; import OrderBookList from './OrderbookList'; +import OrderBookTradesList from './OrderbookTradesList'; import LimitOrderCard from '../../components/cards/LimitOrderCard'; import './Orderbook.scss'; -import OrderBookTradesList from './OrderbookTradesList'; export default function OrderbookPage() { return ( @@ -56,11 +57,11 @@ function Orderbook() { // sized to the word "Orderbook" with padding minWidth: '15em', }} - tabs={useMemo(() => { + tabs={useMemo(() => { return [ { nav: 'Orderbook', - Tab: () => + tab: tokenA && tokenB ? ( + tab: tokenA && tokenB ? ( , + tab: , }, { nav: 'Swaps', - Tab: () => , + tab: , }, { nav: 'Adds', - Tab: () => , + tab: , }, { nav: 'Removes', - Tab: () => , + tab: , }, ]; // intermediary component to avoid repeating tokenA={tokenA} tokenB={tokenB} From 24ee5e8328b75a362766092397d23045e0c9665c Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Fri, 17 May 2024 20:35:57 +1000 Subject: [PATCH 069/192] fix: ensure chart style overrides are applied properly --- src/pages/Orderbook/OrderbookChart.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/pages/Orderbook/OrderbookChart.tsx b/src/pages/Orderbook/OrderbookChart.tsx index 970428a12..2684f5b63 100644 --- a/src/pages/Orderbook/OrderbookChart.tsx +++ b/src/pages/Orderbook/OrderbookChart.tsx @@ -462,6 +462,12 @@ export default function OrderBookChart({ // create chart widget const tvWidget = new widget(widgetOptions); + tvWidget.onChartReady(() => { + // re-apply overrides (somtimes not applied properly) + if (defaultWidgetOptions.settings_overrides) { + tvWidget.applyOverrides(defaultWidgetOptions.settings_overrides); + } + }); // return method to cleanup widget and data subscribers return () => { From 6952382749a5895d80bff522f98d266e3581c5e5 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Fri, 17 May 2024 20:34:57 +1000 Subject: [PATCH 070/192] feat: show Orderbook depth price indication on Orderbook chart --- src/pages/Orderbook/Orderbook.tsx | 7 ++++++- src/pages/Orderbook/OrderbookChart.tsx | 22 ++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/pages/Orderbook/Orderbook.tsx b/src/pages/Orderbook/Orderbook.tsx index b7f421ece..c93d5b6c9 100644 --- a/src/pages/Orderbook/Orderbook.tsx +++ b/src/pages/Orderbook/Orderbook.tsx @@ -45,7 +45,12 @@ function Orderbook() {
{tokenA && tokenB && ( - + )}
diff --git a/src/pages/Orderbook/OrderbookChart.tsx b/src/pages/Orderbook/OrderbookChart.tsx index 2684f5b63..5f750ab39 100644 --- a/src/pages/Orderbook/OrderbookChart.tsx +++ b/src/pages/Orderbook/OrderbookChart.tsx @@ -12,6 +12,7 @@ import { SearchSymbolResultItem, Bar, Timezone, + IChartingLibraryWidget, } from 'charting_library'; import { Token, getTokenId } from '../../lib/web3/utils/tokens'; @@ -74,9 +75,12 @@ type TimeSeriesResolution = 'second' | 'minute' | 'hour' | 'day' | 'month'; export default function OrderBookChart({ tokenA, tokenB, + priceIndication, }: { tokenA: Token; tokenB: Token; + priceIndication?: number | undefined; + setPriceIndication?: React.Dispatch>; }) { const tokenIdA = getTokenId(tokenA); const tokenIdB = getTokenId(tokenB); @@ -86,6 +90,9 @@ export default function OrderBookChart({ // find chart container to fit const chartRef = useRef(null); + // store chart widget when ready + const chartWidget = useRef(); + const { data: tokenPairReserves } = useTokenPairs(); const { data: tokenByDenom } = useTokenByDenom( tokenPairReserves?.flatMap(([denom0, denom1]) => [denom0, denom1]) @@ -467,11 +474,14 @@ export default function OrderBookChart({ if (defaultWidgetOptions.settings_overrides) { tvWidget.applyOverrides(defaultWidgetOptions.settings_overrides); } + // store current widget + chartWidget.current = tvWidget; }); // return method to cleanup widget and data subscribers return () => { // remove widget + chartWidget.current = undefined; tvWidget.remove(); // unsubscribe all requests (both getBars and subscribeBars) streams.forEach((stream) => { @@ -481,6 +491,18 @@ export default function OrderBookChart({ } }, [navigate, tokenIdA, tokenIdB, tokenPairID, tokenPairs]); + useEffect(() => { + const chart = chartWidget.current?.activeChart(); + if (chart && priceIndication) { + const line = chart + .createOrderLine() + .setPrice(priceIndication) + .setText('') + .setQuantity(''); + return () => line.remove(); + } + }, [priceIndication]); + return
; } From 129ae48739618cc34e37432a4d56b2f5368be131 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Sat, 18 May 2024 06:58:43 +1000 Subject: [PATCH 071/192] fix: switch to useState for chart tracking: to be able to react to --- src/pages/Orderbook/OrderbookChart.tsx | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/pages/Orderbook/OrderbookChart.tsx b/src/pages/Orderbook/OrderbookChart.tsx index 5f750ab39..43476ea9d 100644 --- a/src/pages/Orderbook/OrderbookChart.tsx +++ b/src/pages/Orderbook/OrderbookChart.tsx @@ -1,5 +1,5 @@ import BigNumber from 'bignumber.js'; -import { useEffect, useMemo, useRef } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { useDeepCompareMemoize } from 'use-deep-compare-effect'; import { @@ -12,7 +12,7 @@ import { SearchSymbolResultItem, Bar, Timezone, - IChartingLibraryWidget, + IChartWidgetApi, } from 'charting_library'; import { Token, getTokenId } from '../../lib/web3/utils/tokens'; @@ -91,7 +91,7 @@ export default function OrderBookChart({ const chartRef = useRef(null); // store chart widget when ready - const chartWidget = useRef(); + const [chart, setChart] = useState(); const { data: tokenPairReserves } = useTokenPairs(); const { data: tokenByDenom } = useTokenByDenom( @@ -475,13 +475,13 @@ export default function OrderBookChart({ tvWidget.applyOverrides(defaultWidgetOptions.settings_overrides); } // store current widget - chartWidget.current = tvWidget; + setChart(tvWidget.activeChart()); }); // return method to cleanup widget and data subscribers return () => { // remove widget - chartWidget.current = undefined; + setChart(undefined); tvWidget.remove(); // unsubscribe all requests (both getBars and subscribeBars) streams.forEach((stream) => { @@ -492,7 +492,6 @@ export default function OrderBookChart({ }, [navigate, tokenIdA, tokenIdB, tokenPairID, tokenPairs]); useEffect(() => { - const chart = chartWidget.current?.activeChart(); if (chart && priceIndication) { const line = chart .createOrderLine() @@ -501,7 +500,7 @@ export default function OrderBookChart({ .setQuantity(''); return () => line.remove(); } - }, [priceIndication]); + }, [chart, priceIndication]); return
; } From a233b000d4ef2186db289138ddac222219f22586 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Sat, 18 May 2024 15:47:01 +1000 Subject: [PATCH 072/192] feat: connect chart and depth components by price --- src/pages/Orderbook/Orderbook.scss | 4 -- src/pages/Orderbook/Orderbook.tsx | 72 ++++++++++++++++++- src/pages/Orderbook/OrderbookChart.tsx | 54 +++++++++++++- .../Orderbook/OrderbookChartConnector.scss | 9 +++ .../Orderbook/OrderbookChartConnector.tsx | 71 ++++++++++++++++++ src/pages/Orderbook/OrderbookList.tsx | 62 ++++++++++++---- 6 files changed, 253 insertions(+), 19 deletions(-) create mode 100644 src/pages/Orderbook/OrderbookChartConnector.scss create mode 100644 src/pages/Orderbook/OrderbookChartConnector.tsx diff --git a/src/pages/Orderbook/Orderbook.scss b/src/pages/Orderbook/Orderbook.scss index 670e03891..aba7f6828 100644 --- a/src/pages/Orderbook/Orderbook.scss +++ b/src/pages/Orderbook/Orderbook.scss @@ -17,9 +17,5 @@ border-top-right-radius: 0; border-bottom-right-radius: 0; } - - .chart-depth-connector { - min-width: paddings.$p-3; - } } } diff --git a/src/pages/Orderbook/Orderbook.tsx b/src/pages/Orderbook/Orderbook.tsx index c93d5b6c9..f3f61683b 100644 --- a/src/pages/Orderbook/Orderbook.tsx +++ b/src/pages/Orderbook/Orderbook.tsx @@ -8,7 +8,8 @@ import TabsCard from '../../components/cards/TabsCard'; import { Tab } from '../../components/Tabs/Tabs'; import OrderbookHeader from './OrderbookHeader'; import OrderbookFooter from './OrderbookFooter'; -import OrderBookChart from './OrderbookChart'; +import OrderBookChart, { ChartPriceAxisInfo } from './OrderbookChart'; +import OrderbookChartConnector from './OrderbookChartConnector'; import OrderBookList from './OrderbookList'; import OrderBookTradesList from './OrderbookTradesList'; import LimitOrderCard from '../../components/cards/LimitOrderCard'; @@ -33,7 +34,9 @@ function Orderbook() { const { data: tokenA } = useToken(denomA); const { data: tokenB } = useToken(denomB); + const [chartPriceAxis, setChartPriceAxis] = useState(); const [depthPriceIndication, setDepthPriceIndication] = useState(); + const [depthPriceOffset, setDepthPriceOffset] = useState(); return (
@@ -50,10 +53,32 @@ function Orderbook() { tokenB={tokenA} priceIndication={depthPriceIndication} setPriceIndication={setDepthPriceIndication} + setPriceAxis={setChartPriceAxis} /> )}
-
+
+ { + if ( + chartPriceAxis && + depthPriceIndication && + depthPriceOffset + ) { + return [ + [ + getChartPricePointOffset( + chartPriceAxis, + depthPriceIndication + ), + depthPriceOffset, + ], + ]; + } + return []; + }, [chartPriceAxis, depthPriceIndication, depthPriceOffset])} + /> +
) : null, }, @@ -102,3 +128,45 @@ function Orderbook() {
); } + +function getPointOffsetPercent( + value: number, + max: number, + min: number = 0, + mode: ChartPriceAxisInfo['mode'] = 0 +): number { + switch (mode) { + // handle linear math + case 0: { + return (value - min) / (max - min); + } + } + // todo: handle other math + return NaN; +} +function getPointOffset( + percent: number, + extent: number, + offset: number = 0 +): number { + return percent * extent + offset; +} + +const chartPriceAxisOffset = { + top: 42, +}; +function getChartPricePointOffset( + chartPriceAxis: ChartPriceAxisInfo, + price: number +) { + return getPointOffset( + getPointOffsetPercent( + price, + chartPriceAxis.from, + chartPriceAxis.to, + chartPriceAxis.mode + ), + chartPriceAxis.height, + chartPriceAxisOffset.top + ); +} diff --git a/src/pages/Orderbook/OrderbookChart.tsx b/src/pages/Orderbook/OrderbookChart.tsx index 43476ea9d..18be1211d 100644 --- a/src/pages/Orderbook/OrderbookChart.tsx +++ b/src/pages/Orderbook/OrderbookChart.tsx @@ -12,6 +12,8 @@ import { SearchSymbolResultItem, Bar, Timezone, + VisiblePriceRange, + PriceScaleMode, IChartWidgetApi, } from 'charting_library'; @@ -40,7 +42,14 @@ const defaultWidgetOptions: Partial = { container: '', locale: 'en', timezone: Intl.DateTimeFormat().resolvedOptions().timeZone as Timezone, - disabled_features: ['header_symbol_search', 'header_compare'], + disabled_features: [ + // disable searching for symbols inside chart: token pair search is on page + 'header_symbol_search', + // disable comparing multiple symbols: can get complicated + 'header_compare', + // disable mouse-down panning behavior: ensures last tick is current time + 'chart_scroll', + ], enabled_features: [], charts_storage_url: 'https://saveload.tradingview.com', charts_storage_api_version: '1.1', @@ -72,15 +81,23 @@ interface BlockRangeRequestQuery extends RequestQuery { type TimeSeriesResolution = 'second' | 'minute' | 'hour' | 'day' | 'month'; +export interface ChartPriceAxisInfo extends VisiblePriceRange { + mode: PriceScaleMode; + height: number; +} export default function OrderBookChart({ tokenA, tokenB, priceIndication, + setPriceAxis, }: { tokenA: Token; tokenB: Token; priceIndication?: number | undefined; setPriceIndication?: React.Dispatch>; + setPriceAxis?: React.Dispatch< + React.SetStateAction + >; }) { const tokenIdA = getTokenId(tokenA); const tokenIdB = getTokenId(tokenB); @@ -502,6 +519,41 @@ export default function OrderBookChart({ } }, [chart, priceIndication]); + useEffect(() => { + if (chart) { + const checkChartAxis = () => { + setPriceAxis?.((chartPriceAxis) => { + const pane = chart?.getPanes()[0]; + const rightPriceScale = pane?.getRightPriceScales()[0]; + const visiblePriceRange = rightPriceScale?.getVisiblePriceRange(); + const newChartPriceAxis: ChartPriceAxisInfo | undefined = + rightPriceScale && visiblePriceRange + ? { + from: visiblePriceRange.from, + to: visiblePriceRange.to, + mode: rightPriceScale.getMode(), + height: pane.getHeight(), + } + : undefined; + if ( + (chartPriceAxis && !newChartPriceAxis) || + (!chartPriceAxis && newChartPriceAxis) || + chartPriceAxis?.from !== newChartPriceAxis?.from || + chartPriceAxis?.to !== newChartPriceAxis?.to || + chartPriceAxis?.mode !== newChartPriceAxis?.mode || + chartPriceAxis?.height !== newChartPriceAxis?.height + ) { + return newChartPriceAxis; + } + return chartPriceAxis; + }); + timeout = setTimeout(checkChartAxis, 100); + }; + let timeout = setTimeout(checkChartAxis, 0); + return () => clearTimeout(timeout); + } + }, [chart, setPriceAxis]); + return
; } diff --git a/src/pages/Orderbook/OrderbookChartConnector.scss b/src/pages/Orderbook/OrderbookChartConnector.scss new file mode 100644 index 000000000..30225a590 --- /dev/null +++ b/src/pages/Orderbook/OrderbookChartConnector.scss @@ -0,0 +1,9 @@ +@use '../../styles/mixins-vars/margins.scss' as margins; +@use '../../styles/mixins-vars/paddings.scss' as paddings; + +.orderbook-page { + .chart-depth-connector { + min-width: paddings.$p-3; + max-width: 50px; + } +} diff --git a/src/pages/Orderbook/OrderbookChartConnector.tsx b/src/pages/Orderbook/OrderbookChartConnector.tsx new file mode 100644 index 000000000..0f369bbb0 --- /dev/null +++ b/src/pages/Orderbook/OrderbookChartConnector.tsx @@ -0,0 +1,71 @@ +import { useCallback, useState } from 'react'; +import useResizeObserver from '@react-hook/resize-observer'; + +import './OrderbookChartConnector.scss'; + +type ConnectionPoint = [number, number]; + +export default function OrderbookChartConnector({ + connectionPoints, +}: { + connectionPoints?: ConnectionPoint[]; +}) { + // define what to draw + const draw = useCallback( + (canvas: HTMLCanvasElement | null) => { + const ctx = canvas?.getContext('2d'); + if (canvas && ctx) { + // reset canvas + canvas.width = canvas.offsetWidth; + canvas.height = canvas.offsetHeight; + const width = canvas.width; + const height = canvas.height; + ctx?.clearRect(0, 0, width, height); + // draw elements + if (connectionPoints?.length) { + drawConnectionPoints(ctx, connectionPoints); + } + } + + // define drawing functions + function drawConnectionPoints( + ctx: CanvasRenderingContext2D, + connectionPoints: ConnectionPoint[], + width: number = ctx.canvas.width + ) { + ctx.lineWidth = 1; + ctx.lineJoin = 'round'; + ctx.strokeStyle = 'white'; + ctx.beginPath(); + connectionPoints.forEach(([y1, y2]) => { + ctx.moveTo(sharpPoint(0), sharpPoint(y1)); + ctx.lineTo(sharpPoint(0.7 * width), sharpPoint(y1)); + ctx.lineTo(sharpPoint(0.9 * width), sharpPoint(y2)); + ctx.lineTo(sharpPoint(width), sharpPoint(y2)); + }); + ctx.stroke(); + } + + // use points centered at half-pixels for sharp lines + function sharpPoint(value: number): number { + return Math.round(value) + 0.5; + } + }, + [connectionPoints] + ); + + // store ref but also draw on canvas when first found + const [canvas, setCanvas] = useState(null); + const getCanvasRef = useCallback( + (canvas: HTMLCanvasElement | null) => { + setCanvas(canvas); + draw(canvas); + }, + [draw] + ); + + // redraw canvas when the screen size changes + useResizeObserver(canvas, () => draw(canvas)); + + return ; +} diff --git a/src/pages/Orderbook/OrderbookList.tsx b/src/pages/Orderbook/OrderbookList.tsx index a6c7ecedd..000cf6071 100644 --- a/src/pages/Orderbook/OrderbookList.tsx +++ b/src/pages/Orderbook/OrderbookList.tsx @@ -16,7 +16,7 @@ import { TickInfo, priceToTickIndex } from '../../lib/web3/utils/ticks'; import './OrderbookList.scss'; // ensure that a certain amount of liquidity rows are shown in the card -const shownTickRows = 10; +const shownTickRows = 8; const spacingTicks = Array.from({ length: shownTickRows }).map(() => undefined); export default function OrderBookList({ @@ -24,11 +24,13 @@ export default function OrderBookList({ tokenB, priceIndication, setPriceIndication, + setPriceOffset, }: { tokenA: Token; tokenB: Token; priceIndication?: number | undefined; setPriceIndication?: React.Dispatch>; + setPriceOffset?: React.Dispatch>; }) { const [tokenIdA, tokenIdB] = [getTokenId(tokenA), getTokenId(tokenB)]; const [tokenId0, tokenId1] = useOrderedTokenPair([tokenIdA, tokenIdB]) || []; @@ -42,10 +44,34 @@ export default function OrderBookList({ ]; const onHighlightPrice = useCallback( - (tick: TickInfo | undefined) => { - setPriceIndication?.(tick && tick?.price1To0.toNumber()); - }, - [setPriceIndication] + (includeElementHeight: boolean) => + ( + e: React.MouseEvent, + tick: TickInfo | undefined + ) => { + setPriceIndication?.(tick && tick?.price1To0.toNumber()); + const row = e.target as HTMLTableRowElement | null; + const addOffset = (el: HTMLElement): number => { + if (el.tagName === 'TBODY') return 0; + return ( + el.offsetTop + + (el.tagName !== 'TABLE' && el.offsetParent + ? addOffset(el.offsetParent as HTMLElement) + : 0) + ); + }; + setPriceOffset?.( + row && tick + ? addOffset(row) + (includeElementHeight ? row.offsetHeight : 1) + : undefined + ); + }, + [setPriceIndication, setPriceOffset] + ); + + const [onHighlightHighPrice, onHighlightLowPrice] = useMemo( + () => [onHighlightPrice(false), onHighlightPrice(true)], + [onHighlightPrice] ); const [, currentPrice] = useRealtimePrice(tokenA, tokenB); @@ -246,7 +272,7 @@ export default function OrderBookList({ reserveKey="reserve1" priceDecimalPlaces={priceDecimalPlaces} active={price && priceIndication && price <= priceIndication} - onHighlight={onHighlightPrice} + onHighlight={onHighlightHighPrice} /> ); })} @@ -287,7 +313,7 @@ export default function OrderBookList({ reserveKey="reserve0" priceDecimalPlaces={priceDecimalPlaces} active={price && priceIndication && price >= priceIndication} - onHighlight={onHighlightPrice} + onHighlight={onHighlightLowPrice} /> ); })} @@ -314,23 +340,35 @@ function OrderbookListRow({ priceDecimalPlaces?: number; amountDecimalPlaces?: number; active?: boolean | 0; - onHighlight?: (tick: TickInfo | undefined) => void; + onHighlight?: ( + e: React.MouseEvent, + tick: TickInfo | undefined + ) => void; }) { const { data: price } = useSimplePrice(token); // keep track of hover state and use as a price indication in parents const [hovered, setHovered] = useState(false); const onHover = useCallback( - (tick: TickInfo | undefined) => { + ( + e: React.MouseEvent, + tick: TickInfo | undefined + ) => { // set hovered state setHovered(!!tick); // set parent state - onHighlight?.(tick); + onHighlight?.(e, tick); }, [onHighlight] ); - const onHoverIn = useCallback(() => onHover(tick), [onHover, tick]); - const onHoverOut = useCallback(() => onHover(undefined), [onHover]); + const onHoverIn = useCallback>( + (e) => onHover(e, tick), + [onHover, tick] + ); + const onHoverOut = useCallback>( + (e) => onHover(e, undefined), + [onHover] + ); // add empty row if (!tick) { From fdea84a25f41600c21f0bed1cc0522c6039e7511 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Sat, 18 May 2024 15:47:38 +1000 Subject: [PATCH 073/192] fix: remove non-zero depth rows --- src/pages/Orderbook/OrderbookList.tsx | 40 +++++++++++++++------------ 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/src/pages/Orderbook/OrderbookList.tsx b/src/pages/Orderbook/OrderbookList.tsx index 000cf6071..46af7b9fe 100644 --- a/src/pages/Orderbook/OrderbookList.tsx +++ b/src/pages/Orderbook/OrderbookList.tsx @@ -89,21 +89,23 @@ export default function OrderBookList({ const ticks = forward ? tokenATicks : tokenBTicks; const precision = 1 - resolutionOrderOfMagnitude; - const tickBucketLimits = Array.from({ length: shownTickRows }).flatMap( - (_, index) => - currentPrice && step - ? descendingOrder - ? Number( - new BigNumber(currentPrice) - .minus(index * step) - .toPrecision(precision, BigNumber.ROUND_FLOOR) - ) - : Number( - new BigNumber(currentPrice) - .plus(index * step) - .toPrecision(precision, BigNumber.ROUND_CEIL) - ) - : [] + // todo: fix bucket collect to stop when enough buckets are filled + const tickBucketLimits = Array.from({ + length: shownTickRows * 100, + }).flatMap((_, index) => + currentPrice && step + ? descendingOrder + ? Number( + new BigNumber(currentPrice) + .minus(index * step) + .toPrecision(precision, BigNumber.ROUND_FLOOR) + ) + : Number( + new BigNumber(currentPrice) + .plus(index * step) + .toPrecision(precision, BigNumber.ROUND_CEIL) + ) + : [] ); const tickBucketsLimit = descendingOrder ? Math.min(...tickBucketLimits) @@ -144,7 +146,7 @@ export default function OrderBookList({ const groupedTicks = Object.fromEntries(groupedTickEntries); // create TickInfo replacements for bucketed data - const fakeTicks = tickBucketLimits.map((key): TickInfo => { + const syntheticTicks = tickBucketLimits.map((key): TickInfo => { return { token0: forward ? tokenA : tokenB, token1: forward ? tokenB : tokenA, @@ -156,7 +158,11 @@ export default function OrderBookList({ }; }); - return [...fakeTicks, ...spacingTicks].slice(0, shownTickRows); + const nonZeroTicks = syntheticTicks.filter( + (tick) => !tick.reserve0.isZero() || !tick.reserve1.isZero() + ); + + return [...nonZeroTicks, ...spacingTicks].slice(0, shownTickRows); }, [currentPrice, tokenATicks, tokenBTicks, tokenA, tokenB] ); From aaa1e7aad70d5b41b8684d773e83cadefac90dc2 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Sat, 18 May 2024 16:28:55 +1000 Subject: [PATCH 074/192] feat: add pixel to overlap depth table border --- src/pages/Orderbook/OrderbookChartConnector.scss | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pages/Orderbook/OrderbookChartConnector.scss b/src/pages/Orderbook/OrderbookChartConnector.scss index 30225a590..3ffd0f5c1 100644 --- a/src/pages/Orderbook/OrderbookChartConnector.scss +++ b/src/pages/Orderbook/OrderbookChartConnector.scss @@ -4,6 +4,8 @@ .orderbook-page { .chart-depth-connector { min-width: paddings.$p-3; - max-width: 50px; + max-width: 51px; + margin-right: -1px; + z-index: 2; } } From b4566d3cd54665bb061663d60dcaf659ef524756 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Sat, 18 May 2024 16:29:22 +1000 Subject: [PATCH 075/192] feat: use a bezier curve to join lines --- src/pages/Orderbook/OrderbookChartConnector.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/pages/Orderbook/OrderbookChartConnector.tsx b/src/pages/Orderbook/OrderbookChartConnector.tsx index 0f369bbb0..08a111523 100644 --- a/src/pages/Orderbook/OrderbookChartConnector.tsx +++ b/src/pages/Orderbook/OrderbookChartConnector.tsx @@ -39,8 +39,12 @@ export default function OrderbookChartConnector({ ctx.beginPath(); connectionPoints.forEach(([y1, y2]) => { ctx.moveTo(sharpPoint(0), sharpPoint(y1)); - ctx.lineTo(sharpPoint(0.7 * width), sharpPoint(y1)); - ctx.lineTo(sharpPoint(0.9 * width), sharpPoint(y2)); + ctx.lineTo(sharpPoint(0.65 * width), sharpPoint(y1)); + ctx.bezierCurveTo( + ...[sharpPoint(0.9 * width), sharpPoint(y1)], + ...[sharpPoint(0.8 * width), sharpPoint(y2)], + ...[sharpPoint(width - 1), sharpPoint(y2)] + ); ctx.lineTo(sharpPoint(width), sharpPoint(y2)); }); ctx.stroke(); From 936019ede5118b461f857ee72ebe1c5b9af5fd10 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Sat, 18 May 2024 16:55:45 +1000 Subject: [PATCH 076/192] feat: ease up on connection line sharpness --- src/pages/Orderbook/OrderbookChartConnector.scss | 2 +- src/pages/Orderbook/OrderbookChartConnector.tsx | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pages/Orderbook/OrderbookChartConnector.scss b/src/pages/Orderbook/OrderbookChartConnector.scss index 3ffd0f5c1..323f1d16b 100644 --- a/src/pages/Orderbook/OrderbookChartConnector.scss +++ b/src/pages/Orderbook/OrderbookChartConnector.scss @@ -4,7 +4,7 @@ .orderbook-page { .chart-depth-connector { min-width: paddings.$p-3; - max-width: 51px; + max-width: 61px; margin-right: -1px; z-index: 2; } diff --git a/src/pages/Orderbook/OrderbookChartConnector.tsx b/src/pages/Orderbook/OrderbookChartConnector.tsx index 08a111523..773aa9c59 100644 --- a/src/pages/Orderbook/OrderbookChartConnector.tsx +++ b/src/pages/Orderbook/OrderbookChartConnector.tsx @@ -39,10 +39,10 @@ export default function OrderbookChartConnector({ ctx.beginPath(); connectionPoints.forEach(([y1, y2]) => { ctx.moveTo(sharpPoint(0), sharpPoint(y1)); - ctx.lineTo(sharpPoint(0.65 * width), sharpPoint(y1)); + ctx.lineTo(sharpPoint(0.4 * width), sharpPoint(y1)); ctx.bezierCurveTo( - ...[sharpPoint(0.9 * width), sharpPoint(y1)], - ...[sharpPoint(0.8 * width), sharpPoint(y2)], + ...[sharpPoint(0.75 * width), sharpPoint(y1)], + ...[sharpPoint(0.65 * width), sharpPoint(y2)], ...[sharpPoint(width - 1), sharpPoint(y2)] ); ctx.lineTo(sharpPoint(width), sharpPoint(y2)); From 6ba4283e4a16e67161ff8c6fc74b7294be1c340f Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Sat, 18 May 2024 16:56:17 +1000 Subject: [PATCH 077/192] feat: add connection area drawings --- .../Orderbook/OrderbookChartConnector.tsx | 41 ++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/src/pages/Orderbook/OrderbookChartConnector.tsx b/src/pages/Orderbook/OrderbookChartConnector.tsx index 773aa9c59..f388bd556 100644 --- a/src/pages/Orderbook/OrderbookChartConnector.tsx +++ b/src/pages/Orderbook/OrderbookChartConnector.tsx @@ -4,11 +4,14 @@ import useResizeObserver from '@react-hook/resize-observer'; import './OrderbookChartConnector.scss'; type ConnectionPoint = [number, number]; +type ConnectionArea = [ConnectionPoint, ConnectionPoint, weight: number]; export default function OrderbookChartConnector({ connectionPoints, + connectionAreas, }: { connectionPoints?: ConnectionPoint[]; + connectionAreas?: ConnectionArea[]; }) { // define what to draw const draw = useCallback( @@ -25,6 +28,9 @@ export default function OrderbookChartConnector({ if (connectionPoints?.length) { drawConnectionPoints(ctx, connectionPoints); } + connectionAreas?.forEach((connectionArea) => { + drawConnectionArea(ctx, connectionArea); + }); } // define drawing functions @@ -50,12 +56,45 @@ export default function OrderbookChartConnector({ ctx.stroke(); } + // define drawing functions + function drawConnectionArea( + ctx: CanvasRenderingContext2D, + connectionArea: ConnectionArea, + width: number = ctx.canvas.width + ) { + const [[y1, y2], [y3, y4], weight] = connectionArea; + ctx.lineJoin = 'round'; + ctx.fillStyle = 'white'; + ctx.filter = `opacity(${weight})`; + ctx.beginPath(); + // draw top line + ctx.moveTo(sharpPoint(0), sharpPoint(y1)); + ctx.lineTo(sharpPoint(0.4 * width), sharpPoint(y1)); + ctx.bezierCurveTo( + ...[sharpPoint(0.75 * width), sharpPoint(y1)], + ...[sharpPoint(0.65 * width), sharpPoint(y2)], + ...[sharpPoint(width - 1), sharpPoint(y2)] + ); + ctx.lineTo(sharpPoint(width), sharpPoint(y2)); + // draw bottom line + ctx.lineTo(sharpPoint(width), sharpPoint(y4)); + ctx.lineTo(sharpPoint(width - 1), sharpPoint(y4)); + ctx.bezierCurveTo( + ...[sharpPoint(0.65 * width), sharpPoint(y4)], + ...[sharpPoint(0.75 * width), sharpPoint(y3)], + ...[sharpPoint(0.4 * width), sharpPoint(y3)] + ); + ctx.lineTo(sharpPoint(0), sharpPoint(y3)); + // fill + ctx.fill(); + } + // use points centered at half-pixels for sharp lines function sharpPoint(value: number): number { return Math.round(value) + 0.5; } }, - [connectionPoints] + [connectionAreas, connectionPoints] ); // store ref but also draw on canvas when first found From e26e2479e78f2346c1ece5a8f2bf7f63133721b9 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Sun, 19 May 2024 12:11:07 +1000 Subject: [PATCH 078/192] feat: combine price indication and offset together --- src/pages/Orderbook/Orderbook.tsx | 8 ++++---- src/pages/Orderbook/OrderbookList.tsx | 19 +++++++++++-------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/src/pages/Orderbook/Orderbook.tsx b/src/pages/Orderbook/Orderbook.tsx index f3f61683b..ba928ef7e 100644 --- a/src/pages/Orderbook/Orderbook.tsx +++ b/src/pages/Orderbook/Orderbook.tsx @@ -26,6 +26,8 @@ export default function OrderbookPage() { ); } +export type PriceOffset = [price?: number, offset?: number]; + function Orderbook() { // change tokens to match pathname const match = useMatch('/orderbook/:tokenA/:tokenB'); @@ -35,8 +37,8 @@ function Orderbook() { const { data: tokenB } = useToken(denomB); const [chartPriceAxis, setChartPriceAxis] = useState(); - const [depthPriceIndication, setDepthPriceIndication] = useState(); - const [depthPriceOffset, setDepthPriceOffset] = useState(); + const [[depthPriceIndication, depthPriceOffset] = [], setDepthPriceOffset] = + useState(); return (
@@ -52,7 +54,6 @@ function Orderbook() { tokenA={tokenB} tokenB={tokenA} priceIndication={depthPriceIndication} - setPriceIndication={setDepthPriceIndication} setPriceAxis={setChartPriceAxis} /> )} @@ -97,7 +98,6 @@ function Orderbook() { tokenA={tokenA} tokenB={tokenB} priceIndication={depthPriceIndication} - setPriceIndication={setDepthPriceIndication} setPriceOffset={setDepthPriceOffset} /> ) : null, diff --git a/src/pages/Orderbook/OrderbookList.tsx b/src/pages/Orderbook/OrderbookList.tsx index 46af7b9fe..cd71399ea 100644 --- a/src/pages/Orderbook/OrderbookList.tsx +++ b/src/pages/Orderbook/OrderbookList.tsx @@ -12,6 +12,7 @@ import { useOrderedTokenPair } from '../../lib/web3/hooks/useTokenPairs'; import { useSimplePrice } from '../../lib/tokenPrices'; import { Token, getTokenId, getTokenValue } from '../../lib/web3/utils/tokens'; import { TickInfo, priceToTickIndex } from '../../lib/web3/utils/ticks'; +import type { PriceOffset } from './Orderbook'; import './OrderbookList.scss'; @@ -23,14 +24,14 @@ export default function OrderBookList({ tokenA, tokenB, priceIndication, - setPriceIndication, setPriceOffset, }: { tokenA: Token; tokenB: Token; priceIndication?: number | undefined; - setPriceIndication?: React.Dispatch>; - setPriceOffset?: React.Dispatch>; + setPriceOffset?: React.Dispatch< + React.SetStateAction + >; }) { const [tokenIdA, tokenIdB] = [getTokenId(tokenA), getTokenId(tokenB)]; const [tokenId0, tokenId1] = useOrderedTokenPair([tokenIdA, tokenIdB]) || []; @@ -49,7 +50,6 @@ export default function OrderBookList({ e: React.MouseEvent, tick: TickInfo | undefined ) => { - setPriceIndication?.(tick && tick?.price1To0.toNumber()); const row = e.target as HTMLTableRowElement | null; const addOffset = (el: HTMLElement): number => { if (el.tagName === 'TBODY') return 0; @@ -62,11 +62,14 @@ export default function OrderBookList({ }; setPriceOffset?.( row && tick - ? addOffset(row) + (includeElementHeight ? row.offsetHeight : 1) + ? [ + tick.price1To0.toNumber(), + addOffset(row) + (includeElementHeight ? row.offsetHeight : 1), + ] : undefined ); }, - [setPriceIndication, setPriceOffset] + [setPriceOffset] ); const [onHighlightHighPrice, onHighlightLowPrice] = useMemo( @@ -285,8 +288,8 @@ export default function OrderBookList({ setPriceIndication?.(currentPrice || undefined)} - onMouseLeave={() => setPriceIndication?.(undefined)} + onMouseEnter={() => setPriceOffset?.([currentPrice || undefined])} + onMouseLeave={() => setPriceOffset?.([])} > Date: Sun, 19 May 2024 13:21:16 +1000 Subject: [PATCH 079/192] feat: improve indexer to add each new update in onAccumulated metadata --- src/lib/web3/hooks/useIndexer.ts | 77 ++++++++++++++++---------- src/lib/web3/hooks/useTickLiquidity.ts | 5 +- 2 files changed, 53 insertions(+), 29 deletions(-) diff --git a/src/lib/web3/hooks/useIndexer.ts b/src/lib/web3/hooks/useIndexer.ts index 92ac44708..f9c77c077 100644 --- a/src/lib/web3/hooks/useIndexer.ts +++ b/src/lib/web3/hooks/useIndexer.ts @@ -309,7 +309,7 @@ interface StreamSingleDataSetCallbacks< // onCompleted indicates when the data stream is finished onCompleted?: (dataSet: DataSet, height: number) => void; // onAccumulated returns accumulated DataSet so far as a Map - onAccumulated?: (dataSet: DataSet, height: number) => void; + onAccumulated?: (dataSet: DataSet, height: number, update: DataRow[]) => void; } export class IndexerStreamAccumulateSingleDataSet< DataRow extends BaseDataRow, @@ -327,15 +327,15 @@ export class IndexerStreamAccumulateSingleDataSet< this.stream = new IndexerStream( relativeURL, { - onUpdate: (dataUpdates: DataRow[], dataHeight: number) => { - callbacks.onUpdate?.(dataUpdates, dataHeight); + onUpdate: (update: DataRow[], dataHeight: number) => { + callbacks.onUpdate?.(update, dataHeight); // update accumulated dataSet this.dataHeight = dataHeight; - this.dataSet = this.accumulateDataSet(dataUpdates, { + this.dataSet = this.accumulateDataSet(update, { mapEntryRemovalValue: opts?.mapEntryRemovalValue, }); // send updated dataSet to listener - callbacks.onAccumulated?.(this.dataSet, this.dataHeight); + callbacks.onAccumulated?.(this.dataSet, this.dataHeight, update); }, onError: callbacks.onError, onCompleted: () => @@ -375,7 +375,11 @@ interface StreamDualDataSetCallbacks< // onCompleted indicates when the data stream is finished onCompleted?: (dataSet: DataSet[], height: number) => void; // onAccumulated returns accumulated DataSet so far as a Map - onAccumulated?: (dataSet: DataSet[], height: number) => void; + onAccumulated?: ( + dataSet: DataSet[], + height: number, + update: DataRow[][] + ) => void; } export class IndexerStreamAccumulateDualDataSet< DataRow extends BaseDataRow, @@ -394,15 +398,15 @@ export class IndexerStreamAccumulateDualDataSet< this.stream = new IndexerStream( relativeURL, { - onUpdate: (dataUpdates: DataRow[][], dataHeight: number) => { - callbacks.onUpdate?.(dataUpdates, dataHeight); + onUpdate: (update: DataRow[][], dataHeight: number) => { + callbacks.onUpdate?.(update, dataHeight); // update accumulated dataSet this.dataHeight = dataHeight; - this.dataSets = this.accumulateDataSet(dataUpdates, { + this.dataSets = this.accumulateDataSet(update, { mapEntryRemovalValue: opts?.mapEntryRemovalValue, }); // send updated dataSet to listener - callbacks.onAccumulated?.(this.dataSets, this.dataHeight); + callbacks.onAccumulated?.(this.dataSets, this.dataHeight, update); }, onError: callbacks.onError, onCompleted: () => @@ -438,12 +442,16 @@ export class IndexerStreamAccumulateDualDataSet< } } -interface StreamMetadata { +interface StreamMetadata { height: number; + update: DataRowOrDataRows[]; } -export interface StaleWhileRevalidateStreamState { +export interface StaleWhileRevalidateStreamState< + DataRowOrDataRows, + DataSetOrDataSets +> { data?: DataSetOrDataSets; - meta?: StreamMetadata; + meta?: StreamMetadata; error?: Error; } // add higher-level hook to stream real-time DataSet or DataSets of Indexer URL @@ -454,7 +462,7 @@ function useIndexerStream< url: URL | string | undefined, IndexerClass: typeof IndexerStreamAccumulateSingleDataSet, opts?: AccumulatorOptions -): StaleWhileRevalidateStreamState; +): StaleWhileRevalidateStreamState; function useIndexerStream< DataRow extends BaseDataRow, DataSet = BaseDataSet @@ -462,7 +470,7 @@ function useIndexerStream< url: URL | string | undefined, IndexerClass: typeof IndexerStreamAccumulateDualDataSet, opts?: AccumulatorOptions -): StaleWhileRevalidateStreamState; +): StaleWhileRevalidateStreamState; function useIndexerStream< DataRow extends BaseDataRow, DataSet = BaseDataSet @@ -472,13 +480,13 @@ function useIndexerStream< | typeof IndexerStreamAccumulateSingleDataSet | typeof IndexerStreamAccumulateDualDataSet, opts?: AccumulatorOptions -): StaleWhileRevalidateStreamState { +): StaleWhileRevalidateStreamState { // define subscription callback which may or may not be used in this component // it is passed to useSWRSubscription to handle if the subscription should be // created/held/destroyed as multiple components may listen for the same data const subscribe: SWRSubscription< string[], - [DataSet | DataSet[], StreamMetadata], + [DataSet | DataSet[], StreamMetadata], Error > = ([url], { next }) => { // if URL is undefined or an empty string, do not stream it @@ -489,9 +497,15 @@ function useIndexerStream< url, { // note: dataSet may be empty on return of an initial "no rows" payload - onAccumulated: (dataSet, height) => { - // note: the TypeScript here is a bit hacky but this should be ok - next(null, [dataSet as unknown as DataSet | DataSet[], { height }]); + onAccumulated: (dataSet, height, update) => { + next(null, [ + // note: the TypeScript here is a bit hacky but this should be ok + dataSet as unknown as DataSet | DataSet[], + { + height, + update: update as unknown as (DataRow | DataRow[])[], + }, + ]); }, onError: (error) => next(error), }, @@ -506,7 +520,9 @@ function useIndexerStream< }; // return cached subscription data - const streamState = useSWRSubscription<[DataSet | DataSet[], StreamMetadata]>( + const streamState = useSWRSubscription< + [DataSet | DataSet[], StreamMetadata] + >( // create key from URL and options [`${url}`, IndexerClass, ...Object.entries(opts || {}).flat()], // the subscription object will differ for any given options @@ -514,7 +530,9 @@ function useIndexerStream< ); // extract out meta data into an additional property - return useMemo>( + return useMemo< + StaleWhileRevalidateStreamState + >( () => ({ ...streamState, data: streamState.data?.[0], @@ -545,7 +563,7 @@ export function useIndexerStreamOfDualDataSet< url, IndexerStreamAccumulateDualDataSet, opts - ) as StaleWhileRevalidateStreamState<[DataSet, DataSet]>; + ) as StaleWhileRevalidateStreamState<[DataRow, DataRow], [DataSet, DataSet]>; } function useIndexerStreamLastUpdate< @@ -557,7 +575,10 @@ function useIndexerStreamLastUpdate< | typeof IndexerStreamAccumulateSingleDataSet | typeof IndexerStreamAccumulateDualDataSet, opts?: AccumulatorOptions -): StaleWhileRevalidateStreamState { +): StaleWhileRevalidateStreamState< + DataRow | [DataRow, DataRow], + DataSet | [DataSet, DataSet] +> { // return cached subscription data const hook = IndexerClass === IndexerStreamAccumulateDualDataSet @@ -576,7 +597,7 @@ export function useIndexerStreamLastUpdateOfSingleDataSet< >( url: URL | string | undefined, opts?: AccumulatorOptions -): StaleWhileRevalidateStreamState { +): StaleWhileRevalidateStreamState { // return cached subscription data return useIndexerStreamLastUpdate( url, @@ -585,7 +606,7 @@ export function useIndexerStreamLastUpdateOfSingleDataSet< accumulateUpdates: accumulateLastUpdateOnly, ...opts, } - ) as StaleWhileRevalidateStreamState; + ) as StaleWhileRevalidateStreamState; } export function useIndexerStreamLastUpdateOfDualDataSet< DataRow extends BaseDataRow, @@ -593,7 +614,7 @@ export function useIndexerStreamLastUpdateOfDualDataSet< >( url: URL | string | undefined, opts?: AccumulatorOptions -): StaleWhileRevalidateStreamState<[DataSet, DataSet]> { +): StaleWhileRevalidateStreamState<[DataRow, DataRow], [DataSet, DataSet]> { // return cached subscription data return useIndexerStreamLastUpdate( url, @@ -602,7 +623,7 @@ export function useIndexerStreamLastUpdateOfDualDataSet< accumulateUpdates: accumulateLastUpdateOnly, ...opts, } - ) as StaleWhileRevalidateStreamState<[DataSet, DataSet]>; + ) as StaleWhileRevalidateStreamState<[DataRow, DataRow], [DataSet, DataSet]>; } // add higher-level functions to fetch multiple pages of data as "one request" diff --git a/src/lib/web3/hooks/useTickLiquidity.ts b/src/lib/web3/hooks/useTickLiquidity.ts index 14ad22dd3..f60927688 100644 --- a/src/lib/web3/hooks/useTickLiquidity.ts +++ b/src/lib/web3/hooks/useTickLiquidity.ts @@ -19,7 +19,10 @@ type ReserveDataSet = Map; export function useTokenPairMapLiquidity([tokenIdA, tokenIdB]: [ TokenID?, TokenID? -]): StaleWhileRevalidateStreamState<[ReserveDataSet, ReserveDataSet]> { +]): StaleWhileRevalidateStreamState< + [ReserveDataRow, ReserveDataRow], + [ReserveDataSet, ReserveDataSet] +> { const encodedA = tokenIdA && encodeURIComponent(tokenIdA); const encodedB = tokenIdB && encodeURIComponent(tokenIdB); // stream data from indexer From 05b264c14840f48fb79cb67d98f129e86c905335 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Sun, 19 May 2024 16:31:25 +1000 Subject: [PATCH 080/192] feat: add useBuckets hook for bucketing to a specific price resolution --- src/lib/web3/hooks/useTickLiquidity.ts | 2 +- src/pages/Orderbook/useBuckets.ts | 144 +++++++++++++++++++++++++ 2 files changed, 145 insertions(+), 1 deletion(-) create mode 100644 src/pages/Orderbook/useBuckets.ts diff --git a/src/lib/web3/hooks/useTickLiquidity.ts b/src/lib/web3/hooks/useTickLiquidity.ts index f60927688..e0df9638e 100644 --- a/src/lib/web3/hooks/useTickLiquidity.ts +++ b/src/lib/web3/hooks/useTickLiquidity.ts @@ -12,7 +12,7 @@ import { import { Token, TokenID } from '../utils/tokens'; import { useToken } from './useDenomClients'; -type ReserveDataRow = [tickIndex: number, reserves: number]; +export type ReserveDataRow = [tickIndex: number, reserves: number]; type ReserveDataSet = Map; // add convenience method to fetch liquidity maps of a pair diff --git a/src/pages/Orderbook/useBuckets.ts b/src/pages/Orderbook/useBuckets.ts new file mode 100644 index 000000000..7889662f0 --- /dev/null +++ b/src/pages/Orderbook/useBuckets.ts @@ -0,0 +1,144 @@ +import BigNumber from 'bignumber.js'; +import { useMemo } from 'react'; + +import { useRealtimePrice } from '../../components/stats/hooks'; +import { + ReserveDataRow, + useTokenPairMapLiquidity, +} from '../../lib/web3/hooks/useTickLiquidity'; + +import { tickIndexToDisplayPrice } from '../../lib/web3/utils/ticks'; +import { + Token, + getDisplayDenomAmount, + getTokenId, +} from '../../lib/web3/utils/tokens'; +import { getOrderOfMagnitude } from '../../lib/utils/number'; + +type Bucket = [lowerBound: number, upperBound: number, displayReserves: number]; + +export default function useBucketsByPriceResolution( + tokenA?: Token, + tokenB?: Token, + bucketResolution: number = 1 +) { + const [tokenIdA, tokenIdB] = [getTokenId(tokenA), getTokenId(tokenB)]; + const [, currentPrice] = useRealtimePrice(tokenA, tokenB); + const { data: [tokenAReserves, tokenBReserves] = [] } = + useTokenPairMapLiquidity([tokenIdA, tokenIdB]); + + // return bucketReserves + return useMemo((): [bucketsA: Bucket[], bucketsB: Bucket[]] | undefined => { + const resolutionMagnitude = getOrderOfMagnitude(bucketResolution); + const zero = new BigNumber(0); + + // return no buckets if the current price is not yet defined + if (!tokenA || !tokenB || !currentPrice) return undefined; + const reduceReservesToBuckets = (inverseDirection: boolean) => { + const getDisplayPrice = !inverseDirection + ? // get token A prices + (tickIndex: number): BigNumber => + tickIndexToDisplayPrice(new BigNumber(tickIndex), tokenA, tokenB) || + zero + : // get token B prices + (tickIndex: number): BigNumber => + tickIndexToDisplayPrice( + new BigNumber(tickIndex).negated(), + tokenA, + tokenB + ) || zero; + // define reducer to accumulate buckets + return ( + acc: Bucket[], + [tickIndex, reserves]: ReserveDataRow + ): Bucket[] => { + const lastValue = acc.at(-1); + const outerBound = lastValue?.[inverseDirection ? 1 : 0]; + const displayPrice = getDisplayPrice(tickIndex); + // does value belong in the last bucket? + if ( + lastValue && + outerBound && + (inverseDirection + ? displayPrice.isLessThanOrEqualTo(outerBound) + : displayPrice.isGreaterThanOrEqualTo(outerBound)) + ) { + lastValue[2] += reserves; + return acc; + } + // does value belong in a new bucket? + else if ( + lastValue || + (inverseDirection + ? displayPrice.isGreaterThanOrEqualTo(currentPrice) + : displayPrice.isLessThanOrEqualTo(currentPrice)) + ) { + // find the bucket limits from current value and bucket resolution + const priceMagnitude = getOrderOfMagnitude(displayPrice.toNumber()); + const precision = priceMagnitude - resolutionMagnitude + 1; + // get outer bound from the precision (or fallback to lowest value) + const outerBound = + precision > 0 + ? inverseDirection + ? Number( + displayPrice.toPrecision(precision, BigNumber.ROUND_UP) + ) + : Number( + displayPrice.toPrecision(precision, BigNumber.ROUND_DOWN) + ) + : bucketResolution; + // get inner bound based on outer bound, but limit to current price + const innerBound = inverseDirection + ? Math.max( + new BigNumber(outerBound).minus(bucketResolution).toNumber(), + currentPrice + ) + : Math.min( + new BigNumber(outerBound).plus(bucketResolution).toNumber(), + currentPrice + ); + // add the new bucket + if (inverseDirection) { + acc.push([innerBound, outerBound, reserves]); + } else { + acc.push([outerBound, innerBound, reserves]); + } + } + return acc; + }; + }; + // use the correct reducer to reduce reserves into buckets + // in order from closest-to-current-price to furthest-from-current-price + return [ + Array.from((tokenAReserves || []).entries()) + // ensure order from nearly-ordered map + .sort(([a], [b]) => b - a) + // group reserves into buckets + .reduce>(reduceReservesToBuckets(false), []) + // translate reserves into displayReserves + .map(([lowerBound, upperBound, reserves]) => [ + lowerBound, + upperBound, + Number(getDisplayDenomAmount(tokenA, reserves)), + ]), + Array.from((tokenBReserves || []).entries()) + // ensure order from nearly-ordered map + .sort(([a], [b]) => b - a) + // group reserves into buckets + .reduce>(reduceReservesToBuckets(true), []) + // translate reserves into displayReserves + .map(([lowerBound, upperBound, reserves]) => [ + lowerBound, + upperBound, + Number(getDisplayDenomAmount(tokenB, reserves)), + ]), + ]; + }, [ + bucketResolution, + currentPrice, + tokenAReserves, + tokenBReserves, + tokenA, + tokenB, + ]); +} From eb13c1f6d3df143f3e392b2beeef1a0791486313 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Mon, 20 May 2024 12:14:44 +1000 Subject: [PATCH 081/192] fix: remove unused bucket transition style calculations --- src/pages/Orderbook/OrderbookList.tsx | 61 +-------------------------- 1 file changed, 1 insertion(+), 60 deletions(-) diff --git a/src/pages/Orderbook/OrderbookList.tsx b/src/pages/Orderbook/OrderbookList.tsx index cd71399ea..db9a30bd7 100644 --- a/src/pages/Orderbook/OrderbookList.tsx +++ b/src/pages/Orderbook/OrderbookList.tsx @@ -178,50 +178,6 @@ export default function OrderBookList({ return getTickBuckets(!forward); }, [forward, getTickBuckets]); - const [previousTokenATicks, setPrevTokenATicks] = useState>( - [] - ); - const lastTokenATicks = useRef>(); - useEffect(() => { - if ( - !lastTokenATicks.current || - JSON.stringify(filteredTokenATicks) !== - JSON.stringify(lastTokenATicks.current) - ) { - // set old data - if (lastTokenATicks.current) { - // and remove undefined ticks from list - setPrevTokenATicks( - lastTokenATicks.current.filter((tick): tick is TickInfo => !!tick) - ); - } - // set new data - lastTokenATicks.current = filteredTokenATicks; - } - }, [filteredTokenATicks]); - - const [previousTokenBTicks, setPrevTokenBTicks] = useState>( - [] - ); - const lastTokenBTicks = useRef>(); - useEffect(() => { - if ( - !lastTokenBTicks.current || - JSON.stringify(filteredTokenBTicks) !== - JSON.stringify(lastTokenBTicks.current) - ) { - // set old data - if (lastTokenBTicks.current) { - // and remove undefined ticks from list - setPrevTokenBTicks( - lastTokenBTicks.current.filter((tick): tick is TickInfo => !!tick) - ); - } - // set new data - lastTokenBTicks.current = filteredTokenBTicks; - } - }, [filteredTokenBTicks]); - const [previousPrice, setPreviousPrice] = useState(); const lastPrice = useRef(currentPrice || undefined); useEffect(() => { @@ -274,9 +230,6 @@ export default function OrderBookList({ { - return prev.tickIndex1To0 === tick.tickIndex1To0; - }); - const diff = previousTokenATick - ? tick[reserveKey].minus(previousTokenATick[reserveKey]) - : new BigNumber(0); - const value = getTokenValue(token, tick[reserveKey], price); return ( - + {formatAmount(tick.price1To0.toNumber(), { minimumFractionDigits: priceDecimalPlaces, maximumFractionDigits: priceDecimalPlaces, From 108574f3e75d841b903eb8b8e468dba1f271a89b Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Mon, 20 May 2024 12:33:10 +1000 Subject: [PATCH 082/192] fix: chart-connector height should grow and shrink depending on its container --- .../Orderbook/OrderbookChartConnector.scss | 16 ++++++++++++-- .../Orderbook/OrderbookChartConnector.tsx | 21 ++++++++++++------- 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/src/pages/Orderbook/OrderbookChartConnector.scss b/src/pages/Orderbook/OrderbookChartConnector.scss index 323f1d16b..d87f0ea65 100644 --- a/src/pages/Orderbook/OrderbookChartConnector.scss +++ b/src/pages/Orderbook/OrderbookChartConnector.scss @@ -3,9 +3,21 @@ .orderbook-page { .chart-depth-connector { - min-width: paddings.$p-3; - max-width: 61px; + width: 61px; margin-right: -1px; z-index: 2; + + .connector-container { + position: relative; + overflow: hidden; + flex: 1 1 0; + canvas { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + } + } } } diff --git a/src/pages/Orderbook/OrderbookChartConnector.tsx b/src/pages/Orderbook/OrderbookChartConnector.tsx index f388bd556..45b2b2ddd 100644 --- a/src/pages/Orderbook/OrderbookChartConnector.tsx +++ b/src/pages/Orderbook/OrderbookChartConnector.tsx @@ -1,4 +1,4 @@ -import { useCallback, useState } from 'react'; +import { useCallback, useRef, useState } from 'react'; import useResizeObserver from '@react-hook/resize-observer'; import './OrderbookChartConnector.scss'; @@ -15,12 +15,12 @@ export default function OrderbookChartConnector({ }) { // define what to draw const draw = useCallback( - (canvas: HTMLCanvasElement | null) => { + (canvas: HTMLCanvasElement | null, container: HTMLDivElement | null) => { const ctx = canvas?.getContext('2d'); - if (canvas && ctx) { + if (container && canvas && ctx) { // reset canvas - canvas.width = canvas.offsetWidth; - canvas.height = canvas.offsetHeight; + canvas.width = container.offsetWidth; + canvas.height = container.offsetHeight; const width = canvas.width; const height = canvas.height; ctx?.clearRect(0, 0, width, height); @@ -102,13 +102,18 @@ export default function OrderbookChartConnector({ const getCanvasRef = useCallback( (canvas: HTMLCanvasElement | null) => { setCanvas(canvas); - draw(canvas); + draw(canvas, containerRef.current); }, [draw] ); // redraw canvas when the screen size changes - useResizeObserver(canvas, () => draw(canvas)); + const containerRef = useRef(null); + useResizeObserver(containerRef, () => draw(canvas, containerRef.current)); - return ; + return ( +
+ +
+ ); } From 2a07cef129b4e90c3c629dbbeab1c768f50731cd Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Mon, 20 May 2024 17:40:32 +1000 Subject: [PATCH 083/192] fix: allow dynamic row count in Orderbook depth table --- src/pages/Orderbook/OrderbookList.scss | 14 +++- src/pages/Orderbook/OrderbookList.tsx | 75 +++++++++++++++++++-- src/pages/Orderbook/OrderbookTradesList.tsx | 5 ++ 3 files changed, 85 insertions(+), 9 deletions(-) diff --git a/src/pages/Orderbook/OrderbookList.scss b/src/pages/Orderbook/OrderbookList.scss index a287c8d30..02638605a 100644 --- a/src/pages/Orderbook/OrderbookList.scss +++ b/src/pages/Orderbook/OrderbookList.scss @@ -1,11 +1,20 @@ @use '../../styles/mixins-vars/paddings.scss' as paddings; .orderbook-list { + position: relative; + .orderbook-list__table { width: 100%; font-size: 0.75em; color: #f9fafb; border-spacing: 0; + + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + th { border: 1px solid var(--page-card-border); border-left: none; @@ -45,8 +54,9 @@ padding-left: 0; } // add more spacing on first row to align the spacing for whole table - tbody:first-of-type tr:first-child td { - padding-top: paddings.$p-3; + .table-body-spacer { + padding: 0; + padding-top: paddings.$p-2; } } } diff --git a/src/pages/Orderbook/OrderbookList.tsx b/src/pages/Orderbook/OrderbookList.tsx index db9a30bd7..5e7a71825 100644 --- a/src/pages/Orderbook/OrderbookList.tsx +++ b/src/pages/Orderbook/OrderbookList.tsx @@ -1,5 +1,13 @@ -import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { + useCallback, + useEffect, + useLayoutEffect, + useMemo, + useRef, + useState, +} from 'react'; import BigNumber from 'bignumber.js'; +import useResizeObserver from '@react-hook/resize-observer'; import { useRealtimePrice } from '../../components/stats/hooks'; import { @@ -16,10 +24,6 @@ import type { PriceOffset } from './Orderbook'; import './OrderbookList.scss'; -// ensure that a certain amount of liquidity rows are shown in the card -const shownTickRows = 8; -const spacingTicks = Array.from({ length: shownTickRows }).map(() => undefined); - export default function OrderBookList({ tokenA, tokenB, @@ -80,6 +84,13 @@ export default function OrderBookList({ const [, currentPrice] = useRealtimePrice(tokenA, tokenB); const resolutionPercent = 0.01; // size of price steps + // ensure that a certain amount of liquidity rows are shown in the card + const [shownTickRows, setShownTickRows] = useState(8); + const spacingTicks = useMemo( + () => Array.from({ length: shownTickRows }).map(() => undefined), + [shownTickRows] + ); + const getTickBuckets = useCallback( (forward: boolean, descendingOrder = forward) => { const resolutionOrderOfMagnitude = getOrderOfMagnitude(resolutionPercent); @@ -167,7 +178,15 @@ export default function OrderBookList({ return [...nonZeroTicks, ...spacingTicks].slice(0, shownTickRows); }, - [currentPrice, tokenATicks, tokenBTicks, tokenA, tokenB] + [ + currentPrice, + tokenATicks, + tokenBTicks, + shownTickRows, + spacingTicks, + tokenA, + tokenB, + ] ); // get tokenA as the top ascending from current price list @@ -209,8 +228,45 @@ export default function OrderBookList({ ) : undefined; + // execute on every layout update + const tableContainerRef = useRef(null); + const calculateRows = useCallback(() => { + const table = tableContainerRef.current; + const sections = table && { + header: table.querySelector('thead'), + spacer: table.querySelectorAll('tbody').item(0), + top: table.querySelectorAll('tbody').item(1), + center: table.querySelectorAll('tbody').item(2), + bottom: table.querySelectorAll('tbody').item(3), + }; + if ( + sections && + sections.header && + sections.top && + sections.center && + sections.bottom + ) { + // measure the table height + const tableHeight = table.offsetHeight; + // measure the table rows + const tableHeightForRows = + tableHeight - + sections.spacer.offsetHeight - + sections.header.offsetHeight - + sections.center.offsetHeight; + const tableRowHeight = + (sections.top.offsetHeight + sections.bottom.offsetHeight) / + (2 * shownTickRows); + // determine how many rows we could add to the table + const maxRowCount = Math.floor(tableHeightForRows / tableRowHeight / 2); + setShownTickRows(maxRowCount); + } + }, [shownTickRows]); + useResizeObserver(tableContainerRef, calculateRows); + useLayoutEffect(calculateRows); + return ( -
+
{/* minimize the first column width */} @@ -223,6 +279,11 @@ export default function OrderBookList({ + + + + + {topTicks.map((tick, index) => { const price = tick && tick?.price1To0.toNumber(); diff --git a/src/pages/Orderbook/OrderbookTradesList.tsx b/src/pages/Orderbook/OrderbookTradesList.tsx index f661d8e83..42a700af2 100644 --- a/src/pages/Orderbook/OrderbookTradesList.tsx +++ b/src/pages/Orderbook/OrderbookTradesList.tsx @@ -86,6 +86,11 @@ export default function OrderBookTradesList({ + + + + + {tradeList.map((tx, index, txs) => { if (index < tradeListSize) { From 4d4742aaa98821034d9c11a31e8fcad18882041e Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Mon, 20 May 2024 17:48:46 +1000 Subject: [PATCH 084/192] fix: align the set price line from the Orderbook depth with connector --- src/pages/Orderbook/OrderbookList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/Orderbook/OrderbookList.tsx b/src/pages/Orderbook/OrderbookList.tsx index 5e7a71825..fcbc72e41 100644 --- a/src/pages/Orderbook/OrderbookList.tsx +++ b/src/pages/Orderbook/OrderbookList.tsx @@ -59,7 +59,7 @@ export default function OrderBookList({ if (el.tagName === 'TBODY') return 0; return ( el.offsetTop + - (el.tagName !== 'TABLE' && el.offsetParent + (['TABLE', 'TD'].includes(el.tagName) && el.offsetParent ? addOffset(el.offsetParent as HTMLElement) : 0) ); From 05a298dd5c6b2e0192020f60aba5e62e3dcdbe12 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Mon, 20 May 2024 17:52:05 +1000 Subject: [PATCH 085/192] refactor: make column widths easier to understand --- src/pages/Orderbook/OrderbookList.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pages/Orderbook/OrderbookList.tsx b/src/pages/Orderbook/OrderbookList.tsx index fcbc72e41..9da350019 100644 --- a/src/pages/Orderbook/OrderbookList.tsx +++ b/src/pages/Orderbook/OrderbookList.tsx @@ -270,8 +270,9 @@ export default function OrderBookList({
Amount
Time
{/* minimize the first column width */} - - + + + From d560f86520e7f4f13a2283c1ef904ea4504e49db Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Mon, 20 May 2024 18:37:23 +1000 Subject: [PATCH 086/192] feat: use buckets hook for Orderbook depth table --- src/pages/Orderbook/Orderbook.tsx | 7 +- src/pages/Orderbook/OrderbookList.tsx | 247 +++++++------------------- src/pages/Orderbook/useBuckets.ts | 11 +- 3 files changed, 80 insertions(+), 185 deletions(-) diff --git a/src/pages/Orderbook/Orderbook.tsx b/src/pages/Orderbook/Orderbook.tsx index ba928ef7e..593c40ad4 100644 --- a/src/pages/Orderbook/Orderbook.tsx +++ b/src/pages/Orderbook/Orderbook.tsx @@ -3,6 +3,7 @@ import { useMatch } from 'react-router-dom'; import { useDenomFromPathParam } from '../../lib/web3/hooks/useTokens'; import { useToken } from '../../lib/web3/hooks/useDenomClients'; +import useBuckets from './useBuckets'; import TabsCard from '../../components/cards/TabsCard'; import { Tab } from '../../components/Tabs/Tabs'; @@ -27,6 +28,7 @@ export default function OrderbookPage() { } export type PriceOffset = [price?: number, offset?: number]; +const bucketResolution = 0.001; // size of price set for buckets function Orderbook() { // change tokens to match pathname @@ -39,6 +41,7 @@ function Orderbook() { const [chartPriceAxis, setChartPriceAxis] = useState(); const [[depthPriceIndication, depthPriceOffset] = [], setDepthPriceOffset] = useState(); + const buckets = useBuckets(tokenA, tokenB, bucketResolution); return (
@@ -97,6 +100,8 @@ function Orderbook() { @@ -113,7 +118,7 @@ function Orderbook() { ) : null, }, ]; - }, [depthPriceIndication, tokenA, tokenB])} + }, [buckets, depthPriceIndication, tokenA, tokenB])} />
diff --git a/src/pages/Orderbook/OrderbookList.tsx b/src/pages/Orderbook/OrderbookList.tsx index 9da350019..5ebde9dd4 100644 --- a/src/pages/Orderbook/OrderbookList.tsx +++ b/src/pages/Orderbook/OrderbookList.tsx @@ -6,53 +6,40 @@ import { useRef, useState, } from 'react'; -import BigNumber from 'bignumber.js'; import useResizeObserver from '@react-hook/resize-observer'; import { useRealtimePrice } from '../../components/stats/hooks'; -import { - formatAmount, - getDecimalPlaces, - getOrderOfMagnitude, -} from '../../lib/utils/number'; -import { useTokenPairTickLiquidity } from '../../lib/web3/hooks/useTickLiquidity'; -import { useOrderedTokenPair } from '../../lib/web3/hooks/useTokenPairs'; +import { formatAmount } from '../../lib/utils/number'; import { useSimplePrice } from '../../lib/tokenPrices'; -import { Token, getTokenId, getTokenValue } from '../../lib/web3/utils/tokens'; -import { TickInfo, priceToTickIndex } from '../../lib/web3/utils/ticks'; +import { Token } from '../../lib/web3/utils/tokens'; + import type { PriceOffset } from './Orderbook'; +import type { Bucket, Buckets } from './useBuckets'; import './OrderbookList.scss'; export default function OrderBookList({ tokenA, tokenB, + buckets, + bucketResolution, priceIndication, setPriceOffset, }: { tokenA: Token; tokenB: Token; priceIndication?: number | undefined; + buckets?: Buckets; + bucketResolution: number; setPriceOffset?: React.Dispatch< React.SetStateAction >; }) { - const [tokenIdA, tokenIdB] = [getTokenId(tokenA), getTokenId(tokenB)]; - const [tokenId0, tokenId1] = useOrderedTokenPair([tokenIdA, tokenIdB]) || []; - const { - data: [tokenATicks = [], tokenBTicks = []], - } = useTokenPairTickLiquidity([tokenIdA, tokenIdB]); - - const [forward, reverse] = [ - tokenId0 === tokenIdA && tokenId1 === tokenIdB, - tokenId1 === tokenIdA && tokenId0 === tokenIdB, - ]; - const onHighlightPrice = useCallback( (includeElementHeight: boolean) => ( e: React.MouseEvent, - tick: TickInfo | undefined + bucketPrice: number | undefined ) => { const row = e.target as HTMLTableRowElement | null; const addOffset = (el: HTMLElement): number => { @@ -65,9 +52,9 @@ export default function OrderBookList({ ); }; setPriceOffset?.( - row && tick + row && bucketPrice ? [ - tick.price1To0.toNumber(), + bucketPrice, addOffset(row) + (includeElementHeight ? row.offsetHeight : 1), ] : undefined @@ -82,7 +69,6 @@ export default function OrderBookList({ ); const [, currentPrice] = useRealtimePrice(tokenA, tokenB); - const resolutionPercent = 0.01; // size of price steps // ensure that a certain amount of liquidity rows are shown in the card const [shownTickRows, setShownTickRows] = useState(8); @@ -91,112 +77,6 @@ export default function OrderBookList({ [shownTickRows] ); - const getTickBuckets = useCallback( - (forward: boolean, descendingOrder = forward) => { - const resolutionOrderOfMagnitude = getOrderOfMagnitude(resolutionPercent); - const step = - currentPrice && - Math.pow( - 10, - getOrderOfMagnitude(currentPrice) + resolutionOrderOfMagnitude - ); - - const ticks = forward ? tokenATicks : tokenBTicks; - const precision = 1 - resolutionOrderOfMagnitude; - // todo: fix bucket collect to stop when enough buckets are filled - const tickBucketLimits = Array.from({ - length: shownTickRows * 100, - }).flatMap((_, index) => - currentPrice && step - ? descendingOrder - ? Number( - new BigNumber(currentPrice) - .minus(index * step) - .toPrecision(precision, BigNumber.ROUND_FLOOR) - ) - : Number( - new BigNumber(currentPrice) - .plus(index * step) - .toPrecision(precision, BigNumber.ROUND_CEIL) - ) - : [] - ); - const tickBucketsLimit = descendingOrder - ? Math.min(...tickBucketLimits) - : Math.max(...tickBucketLimits); - - const groupedTickEntries = ticks.reduce< - Array<[roundedPrice: number, reserves: number]> - >( - (acc, tick) => { - // add if price is within bounds - if ( - step && - currentPrice && - // select tick prices within the outer edge of price buckets - (descendingOrder - ? tick.price1To0.isGreaterThanOrEqualTo(tickBucketsLimit) - : tick.price1To0.isLessThanOrEqualTo(tickBucketsLimit)) && - // select tick prices within the inner edge of price buckets - (descendingOrder - ? tick.price1To0.isLessThanOrEqualTo(currentPrice) - : tick.price1To0.isGreaterThanOrEqualTo(currentPrice)) - ) { - const foundEntry = acc.find(([limit]) => { - return descendingOrder - ? tick.price1To0.isGreaterThanOrEqualTo(limit) - : tick.price1To0.isLessThanOrEqualTo(limit); - }); - if (foundEntry !== undefined) { - foundEntry[1] += ( - forward ? tick.reserve0 : tick.reserve1 - ).toNumber(); - } - } - return acc; - }, - tickBucketLimits.map((limit) => [limit, 0]) - ); - const groupedTicks = Object.fromEntries(groupedTickEntries); - - // create TickInfo replacements for bucketed data - const syntheticTicks = tickBucketLimits.map((key): TickInfo => { - return { - token0: forward ? tokenA : tokenB, - token1: forward ? tokenB : tokenA, - fee: new BigNumber(0), - price1To0: new BigNumber(key), - tickIndex1To0: priceToTickIndex(new BigNumber(key)), - reserve0: new BigNumber(forward ? groupedTicks[key] || 0 : 0), - reserve1: new BigNumber(forward ? 0 : groupedTicks[key] || 0), - }; - }); - - const nonZeroTicks = syntheticTicks.filter( - (tick) => !tick.reserve0.isZero() || !tick.reserve1.isZero() - ); - - return [...nonZeroTicks, ...spacingTicks].slice(0, shownTickRows); - }, - [ - currentPrice, - tokenATicks, - tokenBTicks, - shownTickRows, - spacingTicks, - tokenA, - tokenB, - ] - ); - - // get tokenA as the top ascending from current price list - const filteredTokenATicks = useMemo>(() => { - return getTickBuckets(forward).reverse(); - }, [forward, getTickBuckets]); - const filteredTokenBTicks = useMemo>(() => { - return getTickBuckets(!forward); - }, [forward, getTickBuckets]); - const [previousPrice, setPreviousPrice] = useState(); const lastPrice = useRef(currentPrice || undefined); useEffect(() => { @@ -212,21 +92,21 @@ export default function OrderBookList({ // set new data lastPrice.current = currentPrice; } - }, [currentPrice, filteredTokenATicks]); + }, [currentPrice, buckets]); - const [topTicks, bottomTicks] = useMemo(() => { - return forward - ? [filteredTokenBTicks.reverse(), filteredTokenATicks.reverse()] - : [filteredTokenATicks, filteredTokenBTicks]; - }, [filteredTokenATicks, filteredTokenBTicks, forward]); + const [bucketsA, bucketsB] = buckets || []; + const [topBuckets, bottomBuckets] = useMemo(() => { + return [ + [...(bucketsB ?? []), ...spacingTicks].slice(0, shownTickRows).reverse(), + [...(bucketsA ?? []), ...spacingTicks].slice(0, shownTickRows), + ]; + }, [bucketsA, bucketsB, shownTickRows, spacingTicks]); - const priceDecimalPlaces = - currentPrice !== undefined && currentPrice !== null - ? getDecimalPlaces( - currentPrice, - 1 - getOrderOfMagnitude(resolutionPercent) - ) - : undefined; + // find how numbers should be displayed + const priceDecimalPlaces = useMemo(() => { + const decimals = bucketResolution.toFixed(18).split('.')[1] || ''; + return decimals.replace(/0+$/, '').length; + }, [bucketResolution]); // execute on every layout update const tableContainerRef = useRef(null); @@ -256,12 +136,14 @@ export default function OrderBookList({ sections.center.offsetHeight; const tableRowHeight = (sections.top.offsetHeight + sections.bottom.offsetHeight) / - (2 * shownTickRows); + (sections.top.childElementCount + sections.bottom.childElementCount); // determine how many rows we could add to the table - const maxRowCount = Math.floor(tableHeightForRows / tableRowHeight / 2); + const maxRowCount = tableRowHeight + ? Math.floor(tableHeightForRows / tableRowHeight / 2) + : 1; setShownTickRows(maxRowCount); } - }, [shownTickRows]); + }, []); useResizeObserver(tableContainerRef, calculateRows); useLayoutEffect(calculateRows); @@ -286,18 +168,20 @@ export default function OrderBookList({
- {topTicks.map((tick, index) => { - const price = tick && tick?.price1To0.toNumber(); - return ( + {topBuckets?.map((bucket, index) => { + const price = bucket?.[1]; + return price && bucket ? ( + ) : ( + ); })} @@ -324,18 +208,20 @@ export default function OrderBookList({ - {bottomTicks.map((tick, index) => { - const price = tick && tick?.price1To0.toNumber(); - return ( + {bottomBuckets?.map((bucket, index) => { + const price = bucket?.[0]; + return price && bucket ? ( = priceIndication} onHighlight={onHighlightLowPrice} /> + ) : ( + ); })} @@ -344,24 +230,32 @@ export default function OrderBookList({ ); } +function EmptyRow() { + return ( + + + + ); +} + function OrderbookListRow({ - tick, + price: bucketPrice, + bucket: [_lowerBound, _upperBound, reserves], token, - reserveKey, priceDecimalPlaces = 6, amountDecimalPlaces = 2, active, onHighlight, }: { - tick: TickInfo | undefined; + price: number; + bucket: Bucket; token: Token; - reserveKey: 'reserve0' | 'reserve1'; priceDecimalPlaces?: number; amountDecimalPlaces?: number; active?: boolean | 0; onHighlight?: ( e: React.MouseEvent, - tick: TickInfo | undefined + bucketPrice: number | undefined ) => void; }) { const { data: price } = useSimplePrice(token); @@ -371,37 +265,28 @@ function OrderbookListRow({ const onHover = useCallback( ( e: React.MouseEvent, - tick: TickInfo | undefined + bucketPrice: number | undefined ) => { // set hovered state - setHovered(!!tick); + setHovered(!!bucketPrice); // set parent state - onHighlight?.(e, tick); + onHighlight?.(e, bucketPrice); }, [onHighlight] ); const onHoverIn = useCallback>( - (e) => onHover(e, tick), - [onHover, tick] + (e) => onHover(e, bucketPrice), + [onHover, bucketPrice] ); const onHoverOut = useCallback>( (e) => onHover(e, undefined), [onHover] ); - // add empty row - if (!tick) { - return ( - - - - ); - } - // add tick row - const value = getTokenValue(token, tick[reserveKey], price); + const value = reserves * (price || 0); return ( - {formatAmount(tick.price1To0.toNumber(), { + {formatAmount(bucketPrice, { minimumFractionDigits: priceDecimalPlaces, maximumFractionDigits: priceDecimalPlaces, })} diff --git a/src/pages/Orderbook/useBuckets.ts b/src/pages/Orderbook/useBuckets.ts index 7889662f0..a9737652e 100644 --- a/src/pages/Orderbook/useBuckets.ts +++ b/src/pages/Orderbook/useBuckets.ts @@ -15,20 +15,25 @@ import { } from '../../lib/web3/utils/tokens'; import { getOrderOfMagnitude } from '../../lib/utils/number'; -type Bucket = [lowerBound: number, upperBound: number, displayReserves: number]; +export type Bucket = [ + lowerBound: number, + upperBound: number, + displayReserves: number +]; +export type Buckets = [bucketsA: Bucket[], bucketsB: Bucket[]]; export default function useBucketsByPriceResolution( tokenA?: Token, tokenB?: Token, bucketResolution: number = 1 -) { +): Buckets | undefined { const [tokenIdA, tokenIdB] = [getTokenId(tokenA), getTokenId(tokenB)]; const [, currentPrice] = useRealtimePrice(tokenA, tokenB); const { data: [tokenAReserves, tokenBReserves] = [] } = useTokenPairMapLiquidity([tokenIdA, tokenIdB]); // return bucketReserves - return useMemo((): [bucketsA: Bucket[], bucketsB: Bucket[]] | undefined => { + return useMemo((): Buckets | undefined => { const resolutionMagnitude = getOrderOfMagnitude(bucketResolution); const zero = new BigNumber(0); From 32ceff0d717f1b736acd9e73a1e616f2b40a91f2 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Tue, 21 May 2024 03:50:33 +1000 Subject: [PATCH 087/192] refactor: simplify PriceOffset type --- src/pages/Orderbook/Orderbook.tsx | 2 +- src/pages/Orderbook/OrderbookList.tsx | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/pages/Orderbook/Orderbook.tsx b/src/pages/Orderbook/Orderbook.tsx index 593c40ad4..205de4e2e 100644 --- a/src/pages/Orderbook/Orderbook.tsx +++ b/src/pages/Orderbook/Orderbook.tsx @@ -27,7 +27,7 @@ export default function OrderbookPage() { ); } -export type PriceOffset = [price?: number, offset?: number]; +export type PriceOffset = [price: number, offset?: number]; const bucketResolution = 0.001; // size of price set for buckets function Orderbook() { diff --git a/src/pages/Orderbook/OrderbookList.tsx b/src/pages/Orderbook/OrderbookList.tsx index 5ebde9dd4..cd51ebb32 100644 --- a/src/pages/Orderbook/OrderbookList.tsx +++ b/src/pages/Orderbook/OrderbookList.tsx @@ -187,8 +187,10 @@ export default function OrderBookList({ setPriceOffset?.([currentPrice || undefined])} - onMouseLeave={() => setPriceOffset?.([])} + onMouseEnter={() => + setPriceOffset?.(currentPrice ? [currentPrice] : undefined) + } + onMouseLeave={() => setPriceOffset?.(undefined)} > Date: Tue, 21 May 2024 06:00:39 +1000 Subject: [PATCH 088/192] feat: expand ConnectionArea type --- src/pages/Orderbook/OrderbookChartConnector.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/pages/Orderbook/OrderbookChartConnector.tsx b/src/pages/Orderbook/OrderbookChartConnector.tsx index 45b2b2ddd..c2f7c66f3 100644 --- a/src/pages/Orderbook/OrderbookChartConnector.tsx +++ b/src/pages/Orderbook/OrderbookChartConnector.tsx @@ -4,7 +4,12 @@ import useResizeObserver from '@react-hook/resize-observer'; import './OrderbookChartConnector.scss'; type ConnectionPoint = [number, number]; -type ConnectionArea = [ConnectionPoint, ConnectionPoint, weight: number]; +type ConnectionArea = [ + ConnectionPoint, + ConnectionPoint, + weight: number, + fillColor?: string | CanvasGradient | CanvasPattern +]; export default function OrderbookChartConnector({ connectionPoints, @@ -62,9 +67,9 @@ export default function OrderbookChartConnector({ connectionArea: ConnectionArea, width: number = ctx.canvas.width ) { - const [[y1, y2], [y3, y4], weight] = connectionArea; + const [[y1, y2], [y3, y4], weight, fill = 'white'] = connectionArea; ctx.lineJoin = 'round'; - ctx.fillStyle = 'white'; + ctx.fillStyle = fill; ctx.filter = `opacity(${weight})`; ctx.beginPath(); // draw top line From 2809187cd6c8c6cff9080c074599910325c4d219 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Tue, 21 May 2024 06:06:34 +1000 Subject: [PATCH 089/192] fix: table row keys were inefficient or not present --- src/pages/Orderbook/OrderbookList.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pages/Orderbook/OrderbookList.tsx b/src/pages/Orderbook/OrderbookList.tsx index cd51ebb32..cde9e5e80 100644 --- a/src/pages/Orderbook/OrderbookList.tsx +++ b/src/pages/Orderbook/OrderbookList.tsx @@ -172,7 +172,7 @@ export default function OrderBookList({ const price = bucket?.[1]; return price && bucket ? ( ) : ( - + ); })} @@ -214,7 +214,7 @@ export default function OrderBookList({ const price = bucket?.[0]; return price && bucket ? ( ) : ( - + ); })} From ed3741e5cd5bfb57044cfdff2120555c9fa9c865 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Tue, 21 May 2024 06:12:34 +1000 Subject: [PATCH 090/192] feat: add tracking for bucket price position offsets of Depth table --- src/pages/Orderbook/Orderbook.tsx | 1 + src/pages/Orderbook/OrderbookList.tsx | 97 +++++++++++++++++++++++---- 2 files changed, 84 insertions(+), 14 deletions(-) diff --git a/src/pages/Orderbook/Orderbook.tsx b/src/pages/Orderbook/Orderbook.tsx index 205de4e2e..f322944a7 100644 --- a/src/pages/Orderbook/Orderbook.tsx +++ b/src/pages/Orderbook/Orderbook.tsx @@ -28,6 +28,7 @@ export default function OrderbookPage() { } export type PriceOffset = [price: number, offset?: number]; +export type BucketOffset = [inner: PriceOffset, outer: PriceOffset]; const bucketResolution = 0.001; // size of price set for buckets function Orderbook() { diff --git a/src/pages/Orderbook/OrderbookList.tsx b/src/pages/Orderbook/OrderbookList.tsx index cde9e5e80..a1f29dbaa 100644 --- a/src/pages/Orderbook/OrderbookList.tsx +++ b/src/pages/Orderbook/OrderbookList.tsx @@ -13,7 +13,7 @@ import { formatAmount } from '../../lib/utils/number'; import { useSimplePrice } from '../../lib/tokenPrices'; import { Token } from '../../lib/web3/utils/tokens'; -import type { PriceOffset } from './Orderbook'; +import type { PriceOffset, BucketOffset } from './Orderbook'; import type { Bucket, Buckets } from './useBuckets'; import './OrderbookList.scss'; @@ -25,6 +25,7 @@ export default function OrderBookList({ bucketResolution, priceIndication, setPriceOffset, + setBucketOffsets, }: { tokenA: Token; tokenB: Token; @@ -34,6 +35,7 @@ export default function OrderBookList({ setPriceOffset?: React.Dispatch< React.SetStateAction >; + setBucketOffsets?: React.Dispatch>; }) { const onHighlightPrice = useCallback( (includeElementHeight: boolean) => @@ -108,24 +110,23 @@ export default function OrderBookList({ return decimals.replace(/0+$/, '').length; }, [bucketResolution]); - // execute on every layout update const tableContainerRef = useRef(null); - const calculateRows = useCallback(() => { - const table = tableContainerRef.current; - const sections = table && { - header: table.querySelector('thead'), + const getTableSections = useCallback((table: HTMLDivElement) => { + const sections = { + header: table.querySelectorAll('thead').item(0), spacer: table.querySelectorAll('tbody').item(0), top: table.querySelectorAll('tbody').item(1), center: table.querySelectorAll('tbody').item(2), bottom: table.querySelectorAll('tbody').item(3), }; - if ( - sections && - sections.header && - sections.top && - sections.center && - sections.bottom - ) { + if (sections.header && sections.top && sections.center && sections.bottom) { + return sections; + } + }, []); + const calculateRows = useCallback(() => { + const table = tableContainerRef.current; + const sections = table && getTableSections(table); + if (table && sections) { // measure the table height const tableHeight = table.offsetHeight; // measure the table rows @@ -143,10 +144,78 @@ export default function OrderBookList({ : 1; setShownTickRows(maxRowCount); } - }, []); + }, [getTableSections]); + + // execute on every layout update useResizeObserver(tableContainerRef, calculateRows); useLayoutEffect(calculateRows); + // calculate measurements to send back to parent + const updateBucketOffsets = useCallback(() => { + const table = tableContainerRef.current; + const sections = table && getTableSections(table); + const tableRowHeight = + sections && + (sections.top.offsetHeight + sections.bottom.offsetHeight) / + (sections.top.childElementCount + sections.bottom.childElementCount || + 1); + if (tableRowHeight && tableRowHeight > 0 && currentPrice && buckets) { + // set bucket offsets + // measure center bucket + const addOffset = (el: HTMLElement): number => { + // if (el.tagName === 'TBODY') return 0; + return ( + el.offsetTop + + (['TABLE', 'TBODY'].includes(el.tagName) && el.offsetParent + ? addOffset(el.offsetParent as HTMLElement) + : 0) + ); + }; + const centerOffset = addOffset(sections.center); + const topCenterOffset = centerOffset; + const bottomCenterOffset = centerOffset + sections.center.offsetHeight; + + // calculate offsets for all buckets + const [bucketsA = [], bucketsB = []] = buckets || []; + setBucketOffsets?.([ + [ + // add current price bucket + [ + [currentPrice, topCenterOffset], + [currentPrice, bottomCenterOffset], + ], + // add token A bucket offsets + ...bucketsA.map( + ([lowerBound, upperBound], index): [PriceOffset, PriceOffset] => [ + [upperBound, bottomCenterOffset + index * tableRowHeight], + [lowerBound, bottomCenterOffset + (index + 1) * tableRowHeight], + ] + ), + ], + [ + // add current price bucket + [ + [currentPrice, bottomCenterOffset], + [currentPrice, topCenterOffset], + ], + // add token B bucket offsets + ...bucketsB.map( + ([lowerBound, upperBound], index): [PriceOffset, PriceOffset] => [ + [lowerBound, topCenterOffset - index * tableRowHeight], + [upperBound, topCenterOffset - (index + 1) * tableRowHeight], + ] + ), + ], + ]); + } else { + // unset bucket offsets + setBucketOffsets?.((offsets) => (offsets.length ? [] : offsets)); + } + }, [buckets, currentPrice, getTableSections, setBucketOffsets]); + // execute on every relevent update + useResizeObserver(tableContainerRef, updateBucketOffsets); + useEffect(updateBucketOffsets, [updateBucketOffsets]); + return (
 
 
From 46a5c91d935bd188b2094e451e9349d2dcd660e1 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Tue, 21 May 2024 06:52:53 +1000 Subject: [PATCH 091/192] fix: make prices explicit: price was being confused for displayPrice --- src/components/cards/LimitOrderCard.tsx | 4 +- src/components/cards/PriceCard.tsx | 4 +- src/components/stats/hooks.ts | 55 ++++++++++++++++++++----- src/pages/Orderbook/OrderbookHeader.tsx | 12 +++--- src/pages/Orderbook/OrderbookList.tsx | 4 +- src/pages/Orderbook/useBuckets.ts | 4 +- 6 files changed, 58 insertions(+), 25 deletions(-) diff --git a/src/components/cards/LimitOrderCard.tsx b/src/components/cards/LimitOrderCard.tsx index 41eab8847..56d1094b9 100644 --- a/src/components/cards/LimitOrderCard.tsx +++ b/src/components/cards/LimitOrderCard.tsx @@ -40,7 +40,7 @@ import { import { useChainFeeToken } from '../../lib/web3/hooks/useTokens'; import { useNativeChain } from '../../lib/web3/hooks/useChains'; import { useTokenPairMapLiquidity } from '../../lib/web3/hooks/useTickLiquidity'; -import { useRealtimePrice } from '../stats/hooks'; +import { useRealtimeDisplayPrice } from '../stats/hooks'; import TokenInputGroup from '../TokenInputGroup'; import NumberInput from '../inputs/NumberInput'; @@ -292,7 +292,7 @@ function LimitOrder({ tokenA, tokenB }: { tokenA: Token; tokenB: Token }) { return {}; }, [tokenIn, tokenOut, formState.amountIn, formState.amountOut]); - const [, currentPriceAtoB] = useRealtimePrice(tokenA, tokenB); + const [, currentPriceAtoB] = useRealtimeDisplayPrice(tokenA, tokenB); const currentPriceInToOut = currentPriceAtoB ? tokenA === tokenIn ? currentPriceAtoB diff --git a/src/components/cards/PriceCard.tsx b/src/components/cards/PriceCard.tsx index 92b04decc..43900e29e 100644 --- a/src/components/cards/PriceCard.tsx +++ b/src/components/cards/PriceCard.tsx @@ -6,7 +6,7 @@ import AssetSymbol from '../assets/AssetName'; import { Token } from '../../lib/web3/utils/tokens'; import { formatPrice } from '../../lib/utils/number'; import { useSimplePrice } from '../../lib/tokenPrices'; -import { useRealtimePrice } from '../stats/hooks'; +import { useRealtimeDisplayPrice } from '../stats/hooks'; import './PriceCard.scss'; @@ -57,7 +57,7 @@ export function PairPriceCard({ tokenA: Token; tokenB: Token; }) { - const [, currentPriceBtoA] = useRealtimePrice(tokenA, tokenB); + const [, currentPriceBtoA] = useRealtimeDisplayPrice(tokenA, tokenB); return ( { return [ - tickIndexToPrice( - new BigNumber({ open, high, low, close }[attribute]) - ).toNumber(), + tickIndexToDisplayPrice( + new BigNumber({ open, high, low, close }[attribute]), + tokenA, + tokenB + )?.toNumber() || 0, ]; }, - [attribute] + [attribute, tokenA, tokenB] ); const [timeUnix, prices, priceDiffs] = useStatData( tokenA, diff --git a/src/pages/Orderbook/OrderbookHeader.tsx b/src/pages/Orderbook/OrderbookHeader.tsx index 4766d7933..58721b47b 100644 --- a/src/pages/Orderbook/OrderbookHeader.tsx +++ b/src/pages/Orderbook/OrderbookHeader.tsx @@ -9,8 +9,8 @@ import TokenPairPicker from '../../components/TokenPicker/TokenPairPicker'; import { formatCurrency, formatPercentage } from '../../lib/utils/number'; import { - useRealtimePrice, - useStatPrice, + useRealtimeDisplayPrice, + useStatDisplayPrice, useStatVolume, } from '../../components/stats/hooks'; @@ -118,8 +118,8 @@ function OrderbookStatsRow({ } function StatColPrice({ tokenA, tokenB }: { tokenA: Token; tokenB: Token }) { - const [, price, priceDiff] = useStatPrice(tokenA, tokenB); - const [, realtimePrice = price] = useRealtimePrice(tokenA, tokenB); + const [, price, priceDiff] = useStatDisplayPrice(tokenA, tokenB); + const [, realtimePrice = price] = useRealtimeDisplayPrice(tokenA, tokenB); const previousPrice = !isNaN(price ?? NaN) && !isNaN(priceDiff ?? NaN) ? Number(price) - Number(priceDiff) @@ -149,12 +149,12 @@ function StatColPriceHigh({ tokenA: Token; tokenB: Token; }) { - const [, price] = useStatPrice(tokenA, tokenB, 'high'); + const [, price] = useStatDisplayPrice(tokenA, tokenB, 'high'); return ; } function StatColPriceLow({ tokenA, tokenB }: { tokenA: Token; tokenB: Token }) { - const [, price] = useStatPrice(tokenA, tokenB, 'low'); + const [, price] = useStatDisplayPrice(tokenA, tokenB, 'low'); return ; } diff --git a/src/pages/Orderbook/OrderbookList.tsx b/src/pages/Orderbook/OrderbookList.tsx index a1f29dbaa..def5e32a6 100644 --- a/src/pages/Orderbook/OrderbookList.tsx +++ b/src/pages/Orderbook/OrderbookList.tsx @@ -8,7 +8,7 @@ import { } from 'react'; import useResizeObserver from '@react-hook/resize-observer'; -import { useRealtimePrice } from '../../components/stats/hooks'; +import { useRealtimeDisplayPrice } from '../../components/stats/hooks'; import { formatAmount } from '../../lib/utils/number'; import { useSimplePrice } from '../../lib/tokenPrices'; import { Token } from '../../lib/web3/utils/tokens'; @@ -70,7 +70,7 @@ export default function OrderBookList({ [onHighlightPrice] ); - const [, currentPrice] = useRealtimePrice(tokenA, tokenB); + const [, currentPrice] = useRealtimeDisplayPrice(tokenA, tokenB); // ensure that a certain amount of liquidity rows are shown in the card const [shownTickRows, setShownTickRows] = useState(8); diff --git a/src/pages/Orderbook/useBuckets.ts b/src/pages/Orderbook/useBuckets.ts index a9737652e..e7adc5c65 100644 --- a/src/pages/Orderbook/useBuckets.ts +++ b/src/pages/Orderbook/useBuckets.ts @@ -1,7 +1,7 @@ import BigNumber from 'bignumber.js'; import { useMemo } from 'react'; -import { useRealtimePrice } from '../../components/stats/hooks'; +import { useRealtimeDisplayPrice } from '../../components/stats/hooks'; import { ReserveDataRow, useTokenPairMapLiquidity, @@ -28,7 +28,7 @@ export default function useBucketsByPriceResolution( bucketResolution: number = 1 ): Buckets | undefined { const [tokenIdA, tokenIdB] = [getTokenId(tokenA), getTokenId(tokenB)]; - const [, currentPrice] = useRealtimePrice(tokenA, tokenB); + const [, currentPrice] = useRealtimeDisplayPrice(tokenA, tokenB); const { data: [tokenAReserves, tokenBReserves] = [] } = useTokenPairMapLiquidity([tokenIdA, tokenIdB]); From 248bc6b12ab6d4cee07e5ff05b3fa6818357ea40 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Tue, 21 May 2024 08:26:27 +1000 Subject: [PATCH 092/192] refactor: store Buckets in inner/outer bounds rather than lower/upper: - it can help keep the logic more directionless --- src/pages/Orderbook/OrderbookList.tsx | 16 ++++++++-------- src/pages/Orderbook/useBuckets.ts | 20 ++++++++++---------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/pages/Orderbook/OrderbookList.tsx b/src/pages/Orderbook/OrderbookList.tsx index def5e32a6..1051bea62 100644 --- a/src/pages/Orderbook/OrderbookList.tsx +++ b/src/pages/Orderbook/OrderbookList.tsx @@ -186,9 +186,9 @@ export default function OrderBookList({ ], // add token A bucket offsets ...bucketsA.map( - ([lowerBound, upperBound], index): [PriceOffset, PriceOffset] => [ - [upperBound, bottomCenterOffset + index * tableRowHeight], - [lowerBound, bottomCenterOffset + (index + 1) * tableRowHeight], + ([innerBound, outerBound], index): [PriceOffset, PriceOffset] => [ + [innerBound, bottomCenterOffset + index * tableRowHeight], + [outerBound, bottomCenterOffset + (index + 1) * tableRowHeight], ] ), ], @@ -200,9 +200,9 @@ export default function OrderBookList({ ], // add token B bucket offsets ...bucketsB.map( - ([lowerBound, upperBound], index): [PriceOffset, PriceOffset] => [ - [lowerBound, topCenterOffset - index * tableRowHeight], - [upperBound, topCenterOffset - (index + 1) * tableRowHeight], + ([innerBound, outerBound], index): [PriceOffset, PriceOffset] => [ + [innerBound, topCenterOffset - index * tableRowHeight], + [outerBound, topCenterOffset - (index + 1) * tableRowHeight], ] ), ], @@ -280,7 +280,7 @@ export default function OrderBookList({ {bottomBuckets?.map((bucket, index) => { - const price = bucket?.[0]; + const price = bucket?.[1]; return price && bucket ? ( { const lastValue = acc.at(-1); - const outerBound = lastValue?.[inverseDirection ? 1 : 0]; + const outerBound = lastValue?.[1]; const displayPrice = getDisplayPrice(tickIndex); // does value belong in the last bucket? if ( @@ -106,7 +106,7 @@ export default function useBucketsByPriceResolution( if (inverseDirection) { acc.push([innerBound, outerBound, reserves]); } else { - acc.push([outerBound, innerBound, reserves]); + acc.push([innerBound, outerBound, reserves]); } } return acc; @@ -121,9 +121,9 @@ export default function useBucketsByPriceResolution( // group reserves into buckets .reduce>(reduceReservesToBuckets(false), []) // translate reserves into displayReserves - .map(([lowerBound, upperBound, reserves]) => [ - lowerBound, - upperBound, + .map(([innerBound, outerBound, reserves]) => [ + innerBound, + outerBound, Number(getDisplayDenomAmount(tokenA, reserves)), ]), Array.from((tokenBReserves || []).entries()) @@ -132,9 +132,9 @@ export default function useBucketsByPriceResolution( // group reserves into buckets .reduce>(reduceReservesToBuckets(true), []) // translate reserves into displayReserves - .map(([lowerBound, upperBound, reserves]) => [ - lowerBound, - upperBound, + .map(([innerBound, outerBound, reserves]) => [ + innerBound, + outerBound, Number(getDisplayDenomAmount(tokenB, reserves)), ]), ]; From 36b04b19967de069efa8d89fb0d2fccf38bc4932 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Tue, 21 May 2024 09:21:53 +1000 Subject: [PATCH 093/192] feat: draw bucket price connections --- src/pages/Orderbook/Orderbook.tsx | 88 ++++++++++++++++++- .../Orderbook/OrderbookChartConnector.tsx | 2 +- 2 files changed, 88 insertions(+), 2 deletions(-) diff --git a/src/pages/Orderbook/Orderbook.tsx b/src/pages/Orderbook/Orderbook.tsx index f322944a7..0e62fb270 100644 --- a/src/pages/Orderbook/Orderbook.tsx +++ b/src/pages/Orderbook/Orderbook.tsx @@ -3,6 +3,7 @@ import { useMatch } from 'react-router-dom'; import { useDenomFromPathParam } from '../../lib/web3/hooks/useTokens'; import { useToken } from '../../lib/web3/hooks/useDenomClients'; +import { useRealtimeDisplayPrice } from '../../components/stats/hooks'; import useBuckets from './useBuckets'; import TabsCard from '../../components/cards/TabsCard'; @@ -10,7 +11,9 @@ import { Tab } from '../../components/Tabs/Tabs'; import OrderbookHeader from './OrderbookHeader'; import OrderbookFooter from './OrderbookFooter'; import OrderBookChart, { ChartPriceAxisInfo } from './OrderbookChart'; -import OrderbookChartConnector from './OrderbookChartConnector'; +import OrderbookChartConnector, { + ConnectionArea, +} from './OrderbookChartConnector'; import OrderBookList from './OrderbookList'; import OrderBookTradesList from './OrderbookTradesList'; import LimitOrderCard from '../../components/cards/LimitOrderCard'; @@ -30,6 +33,7 @@ export default function OrderbookPage() { export type PriceOffset = [price: number, offset?: number]; export type BucketOffset = [inner: PriceOffset, outer: PriceOffset]; const bucketResolution = 0.001; // size of price set for buckets +const connectionMaxOpacity = 0.75; function Orderbook() { // change tokens to match pathname @@ -44,6 +48,86 @@ function Orderbook() { useState(); const buckets = useBuckets(tokenA, tokenB, bucketResolution); + const [depthBucketOffsets, setDepthBucketOffsets] = useState< + BucketOffset[][] + >([]); + + const [, currentPrice] = useRealtimeDisplayPrice(tokenA, tokenB); + + const depthBucketConnections = useMemo(() => { + if (currentPrice && chartPriceAxis && buckets && depthBucketOffsets) { + const [bucketsA = [], bucketsB = []] = buckets; + const [bucketOffsetsA, bucketOffsetsB] = depthBucketOffsets; + // find the maximum reserve equivalent value for connection opacity + const reservesA = bucketsA.map((bucket) => bucket[2]); + const reservesB = bucketsB.map((bucket) => bucket[2]); + const maxSideReserves = + Math.max( + Math.max(...reservesA), + Math.max(...reservesB) * currentPrice + ) / connectionMaxOpacity; + // draw connections of chart offsets to depth offsets for each bucket + return [ + ...(bucketOffsetsA + ? bucketsA.map( + ([innerBound, outerBound, reserves]) => { + const bucketOffset = bucketOffsetsA.find( + ([[innerPrice], [outerPrice]]) => { + return ( + innerPrice === innerBound && outerPrice === outerBound + ); + } + ); + return [ + [ + // inner chart price offset + getChartPricePointOffset(chartPriceAxis, innerBound), + // inner depth price offset + bucketOffset?.[0][1] || 0, + ], + [ + // outer chart price offset + getChartPricePointOffset(chartPriceAxis, outerBound), + // outer depth price offset + bucketOffset?.[1][1] || 0, + ], + reserves / maxSideReserves, + ]; + } + ) + : []), + ...(bucketOffsetsB + ? bucketsB.map( + ([innerBound, outerBound, reserves]) => { + const bucketOffset = bucketOffsetsB.find( + ([[innerPrice], [outerPrice]]) => { + return ( + innerPrice === innerBound && outerPrice === outerBound + ); + } + ); + return [ + [ + // inner chart price offset + getChartPricePointOffset(chartPriceAxis, innerBound), + // inner depth price offset + bucketOffset?.[0][1] || 0, + ], + [ + // outer chart price offset + getChartPricePointOffset(chartPriceAxis, outerBound), + // outer depth price offset + bucketOffset?.[1][1] || 0, + ], + (reserves * currentPrice) / maxSideReserves, + ]; + } + ) + : []), + ]; + } + }, [buckets, chartPriceAxis, currentPrice, depthBucketOffsets]); + return (
@@ -82,6 +166,7 @@ function Orderbook() { } return []; }, [chartPriceAxis, depthPriceIndication, depthPriceOffset])} + connectionAreas={depthBucketConnections} />
@@ -105,6 +190,7 @@ function Orderbook() { bucketResolution={bucketResolution} priceIndication={depthPriceIndication} setPriceOffset={setDepthPriceOffset} + setBucketOffsets={setDepthBucketOffsets} /> ) : null, }, diff --git a/src/pages/Orderbook/OrderbookChartConnector.tsx b/src/pages/Orderbook/OrderbookChartConnector.tsx index c2f7c66f3..afce35995 100644 --- a/src/pages/Orderbook/OrderbookChartConnector.tsx +++ b/src/pages/Orderbook/OrderbookChartConnector.tsx @@ -4,7 +4,7 @@ import useResizeObserver from '@react-hook/resize-observer'; import './OrderbookChartConnector.scss'; type ConnectionPoint = [number, number]; -type ConnectionArea = [ +export type ConnectionArea = [ ConnectionPoint, ConnectionPoint, weight: number, From 5543643d87638b23d83471c255e7895e16397a9c Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Tue, 21 May 2024 09:22:30 +1000 Subject: [PATCH 094/192] feat: add depthPriceIndication hover state to bucket connections --- src/pages/Orderbook/Orderbook.tsx | 51 ++++++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/src/pages/Orderbook/Orderbook.tsx b/src/pages/Orderbook/Orderbook.tsx index 0e62fb270..bccb9fbce 100644 --- a/src/pages/Orderbook/Orderbook.tsx +++ b/src/pages/Orderbook/Orderbook.tsx @@ -128,6 +128,55 @@ function Orderbook() { } }, [buckets, chartPriceAxis, currentPrice, depthBucketOffsets]); + const depthPriceIndicationBucketConnection = useMemo< + ConnectionArea | undefined + >(() => { + const [bucketOffsetsA, bucketOffsetsB] = depthBucketOffsets; + if (chartPriceAxis && currentPrice && depthPriceIndication) { + const price = depthPriceIndication; + const [innerBucket, outerBucket] = + price <= currentPrice + ? [ + bucketOffsetsA[1], + bucketOffsetsA.find( + ([[innerPriceOffset], [outerPriceOffset]]) => { + return price < innerPriceOffset && price >= outerPriceOffset; + } + ), + ] + : [ + bucketOffsetsB[1], + bucketOffsetsB.find( + ([[innerPriceOffset], [outerPriceOffset]]) => { + return price > innerPriceOffset && price <= outerPriceOffset; + } + ), + ]; + if (innerBucket && outerBucket) { + return [ + [ + getChartPricePointOffset(chartPriceAxis, innerBucket[0][0]), + innerBucket[0][1] || 0, + ], + [ + getChartPricePointOffset(chartPriceAxis, outerBucket[1][0]), + outerBucket[1][1] || 0, + ], + 0.15, + ]; + } + } + }, [chartPriceAxis, currentPrice, depthBucketOffsets, depthPriceIndication]); + + const connectionAreas = useMemo(() => { + return depthPriceIndicationBucketConnection + ? [ + ...(depthBucketConnections ?? []), + depthPriceIndicationBucketConnection, + ] + : depthBucketConnections; + }, [depthBucketConnections, depthPriceIndicationBucketConnection]); + return (
@@ -166,7 +215,7 @@ function Orderbook() { } return []; }, [chartPriceAxis, depthPriceIndication, depthPriceOffset])} - connectionAreas={depthBucketConnections} + connectionAreas={connectionAreas} />
From d4afb38ff85402914484962927cb7b1d120c679c Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Tue, 21 May 2024 11:05:31 +1000 Subject: [PATCH 095/192] feat: draw liquidity and cumulative liquidity buckets in connector --- src/pages/Orderbook/Orderbook.tsx | 84 ++++++++++++++++--- .../Orderbook/OrderbookChartConnector.tsx | 33 +++++++- 2 files changed, 105 insertions(+), 12 deletions(-) diff --git a/src/pages/Orderbook/Orderbook.tsx b/src/pages/Orderbook/Orderbook.tsx index bccb9fbce..b95733a6f 100644 --- a/src/pages/Orderbook/Orderbook.tsx +++ b/src/pages/Orderbook/Orderbook.tsx @@ -12,6 +12,7 @@ import OrderbookHeader from './OrderbookHeader'; import OrderbookFooter from './OrderbookFooter'; import OrderBookChart, { ChartPriceAxisInfo } from './OrderbookChart'; import OrderbookChartConnector, { + ChartArea, ConnectionArea, } from './OrderbookChartConnector'; import OrderBookList from './OrderbookList'; @@ -54,7 +55,7 @@ function Orderbook() { const [, currentPrice] = useRealtimeDisplayPrice(tokenA, tokenB); - const depthBucketConnections = useMemo(() => { + const depthBucketConnections = useMemo(() => { if (currentPrice && chartPriceAxis && buckets && depthBucketOffsets) { const [bucketsA = [], bucketsB = []] = buckets; const [bucketOffsetsA, bucketOffsetsB] = depthBucketOffsets; @@ -68,7 +69,7 @@ function Orderbook() { ) / connectionMaxOpacity; // draw connections of chart offsets to depth offsets for each bucket return [ - ...(bucketOffsetsA + bucketOffsetsA ? bucketsA.map( ([innerBound, outerBound, reserves]) => { const bucketOffset = bucketOffsetsA.find( @@ -95,8 +96,8 @@ function Orderbook() { ]; } ) - : []), - ...(bucketOffsetsB + : [], + bucketOffsetsB ? bucketsB.map( ([innerBound, outerBound, reserves]) => { const bucketOffset = bucketOffsetsB.find( @@ -123,11 +124,70 @@ function Orderbook() { ]; } ) - : []), + : [], ]; } }, [buckets, chartPriceAxis, currentPrice, depthBucketOffsets]); + const chartReserveBuckets = useMemo(() => { + if (depthBucketConnections) { + const [connectionsA = [], connectionsB = []] = depthBucketConnections; + const reservesAreasA = connectionsA.map< + [y1: number, y2: number, reserves: number] + >(([[y1], [y2], reserves]) => { + // either value should be within the chart bounds above zero + return [y1, y2, reserves]; + }); + let cumulativeReservesA = 0; + const cumulativeReservesAreasA = reservesAreasA.map< + [y1: number, y2: number, reserves: number] + >(([y1, y2, reserves], index, reservesAreas) => { + // either value should be within the chart bounds above zero + cumulativeReservesA += reserves; + const nextReserves = reservesAreas[index + 1]; + return [y1, nextReserves ? nextReserves[0] : y2, cumulativeReservesA]; + }); + const reservesAreasB = connectionsB.map< + [y1: number, y2: number, reserves: number] + >(([[y1], [y2], reserves]) => { + // either value should be within the chart bounds above zero + return [y1, y2, reserves]; + }); + let cumulativeReservesB = 0; + const cumulativeReservesAreasB = reservesAreasB.map< + [y1: number, y2: number, reserves: number] + >(([y1, y2, reserves], index, reservesAreas) => { + // either value should be within the chart bounds above zero + cumulativeReservesB += reserves; + const nextReserves = reservesAreas[index + 1]; + return [y1, nextReserves ? nextReserves[0] : y2, cumulativeReservesB]; + }); + const maxCumulativeReserves = + Math.max( + cumulativeReservesAreasA.at(-1)?.[2] || 0, + cumulativeReservesAreasB.at(-1)?.[2] || 0 + ) || 1; + return [ + ...cumulativeReservesAreasA.map(([y1, y2, reserves]) => { + // either value should be within the chart bounds above zero + return [y1, y2, reserves / maxCumulativeReserves, '#FF000022']; + }), + ...cumulativeReservesAreasB.map(([y1, y2, reserves]) => { + // either value should be within the chart bounds above zero + return [y1, y2, reserves / maxCumulativeReserves, '#00FF0022']; + }), + ...reservesAreasA.map(([y1, y2, reserves]) => { + // either value should be within the chart bounds above zero + return [y1, y2, reserves / maxCumulativeReserves, 'red']; + }), + ...reservesAreasB.map(([y1, y2, reserves]) => { + // either value should be within the chart bounds above zero + return [y1, y2, reserves / maxCumulativeReserves, 'green']; + }), + ]; + } + }, [depthBucketConnections]); + const depthPriceIndicationBucketConnection = useMemo< ConnectionArea | undefined >(() => { @@ -169,12 +229,13 @@ function Orderbook() { }, [chartPriceAxis, currentPrice, depthBucketOffsets, depthPriceIndication]); const connectionAreas = useMemo(() => { - return depthPriceIndicationBucketConnection - ? [ - ...(depthBucketConnections ?? []), - depthPriceIndicationBucketConnection, - ] - : depthBucketConnections; + return [ + ...(depthBucketConnections?.[0] ?? []), + ...(depthBucketConnections?.[1] ?? []), + ...(depthPriceIndicationBucketConnection + ? [depthPriceIndicationBucketConnection] + : []), + ]; }, [depthBucketConnections, depthPriceIndicationBucketConnection]); return ( @@ -216,6 +277,7 @@ function Orderbook() { return []; }, [chartPriceAxis, depthPriceIndication, depthPriceOffset])} connectionAreas={connectionAreas} + chartAreas={chartReserveBuckets} />
diff --git a/src/pages/Orderbook/OrderbookChartConnector.tsx b/src/pages/Orderbook/OrderbookChartConnector.tsx index afce35995..c42ca772a 100644 --- a/src/pages/Orderbook/OrderbookChartConnector.tsx +++ b/src/pages/Orderbook/OrderbookChartConnector.tsx @@ -10,13 +10,21 @@ export type ConnectionArea = [ weight: number, fillColor?: string | CanvasGradient | CanvasPattern ]; +export type ChartArea = [ + y1: number, + y2: number, + weight: number, + fillColor?: string | CanvasGradient | CanvasPattern +]; export default function OrderbookChartConnector({ connectionPoints, connectionAreas, + chartAreas, }: { connectionPoints?: ConnectionPoint[]; connectionAreas?: ConnectionArea[]; + chartAreas?: ChartArea[]; }) { // define what to draw const draw = useCallback( @@ -36,6 +44,9 @@ export default function OrderbookChartConnector({ connectionAreas?.forEach((connectionArea) => { drawConnectionArea(ctx, connectionArea); }); + chartAreas?.forEach((chartArea) => { + drawChartArea(ctx, chartArea); + }); } // define drawing functions @@ -94,12 +105,32 @@ export default function OrderbookChartConnector({ ctx.fill(); } + // define drawing functions + function drawChartArea( + ctx: CanvasRenderingContext2D, + chartArea: ChartArea, + width: number = ctx.canvas.width + ) { + const [y1, y2, weight, fill = 'white'] = chartArea; + ctx.fillStyle = fill; + ctx.filter = 'opacity(1)'; + ctx.beginPath(); + // draw top line + ctx.moveTo(sharpPoint(0), sharpPoint(y1)); + ctx.lineTo(sharpPoint(weight * 0.4 * width), sharpPoint(y1)); + // draw bottom line + ctx.lineTo(sharpPoint(weight * 0.4 * width), sharpPoint(y2)); + ctx.lineTo(sharpPoint(0), sharpPoint(y2)); + // fill + ctx.fill(); + } + // use points centered at half-pixels for sharp lines function sharpPoint(value: number): number { return Math.round(value) + 0.5; } }, - [connectionAreas, connectionPoints] + [chartAreas, connectionAreas, connectionPoints] ); // store ref but also draw on canvas when first found From 8caa79b5648e2cea52c3757b8858db039195afb6 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Tue, 21 May 2024 11:09:11 +1000 Subject: [PATCH 096/192] feat: add half-pixel indicator of liquidity --- src/pages/Orderbook/OrderbookChartConnector.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/Orderbook/OrderbookChartConnector.tsx b/src/pages/Orderbook/OrderbookChartConnector.tsx index c42ca772a..eac86b2f1 100644 --- a/src/pages/Orderbook/OrderbookChartConnector.tsx +++ b/src/pages/Orderbook/OrderbookChartConnector.tsx @@ -117,9 +117,9 @@ export default function OrderbookChartConnector({ ctx.beginPath(); // draw top line ctx.moveTo(sharpPoint(0), sharpPoint(y1)); - ctx.lineTo(sharpPoint(weight * 0.4 * width), sharpPoint(y1)); + ctx.lineTo(sharpPoint(weight * 0.4 * width + 0.5), sharpPoint(y1)); // draw bottom line - ctx.lineTo(sharpPoint(weight * 0.4 * width), sharpPoint(y2)); + ctx.lineTo(sharpPoint(weight * 0.4 * width + 0.5), sharpPoint(y2)); ctx.lineTo(sharpPoint(0), sharpPoint(y2)); // fill ctx.fill(); From 9f34089333e7b39d32d281a5646d4138242ea25b Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Tue, 21 May 2024 11:09:44 +1000 Subject: [PATCH 097/192] feat: make conenction areas less distracting --- src/pages/Orderbook/Orderbook.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/Orderbook/Orderbook.tsx b/src/pages/Orderbook/Orderbook.tsx index b95733a6f..2be175724 100644 --- a/src/pages/Orderbook/Orderbook.tsx +++ b/src/pages/Orderbook/Orderbook.tsx @@ -34,7 +34,7 @@ export default function OrderbookPage() { export type PriceOffset = [price: number, offset?: number]; export type BucketOffset = [inner: PriceOffset, outer: PriceOffset]; const bucketResolution = 0.001; // size of price set for buckets -const connectionMaxOpacity = 0.75; +const connectionMaxOpacity = 0.5; function Orderbook() { // change tokens to match pathname @@ -222,7 +222,7 @@ function Orderbook() { getChartPricePointOffset(chartPriceAxis, outerBucket[1][0]), outerBucket[1][1] || 0, ], - 0.15, + 0.1, ]; } } From f34dbee85ef855f38cf93e8b358d1ddf5a508186 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Tue, 21 May 2024 11:25:55 +1000 Subject: [PATCH 098/192] fix: prevent text-wrapping in Orderbook depth current price cell --- src/pages/Orderbook/OrderbookList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/Orderbook/OrderbookList.tsx b/src/pages/Orderbook/OrderbookList.tsx index 1051bea62..d5593940c 100644 --- a/src/pages/Orderbook/OrderbookList.tsx +++ b/src/pages/Orderbook/OrderbookList.tsx @@ -263,7 +263,7 @@ export default function OrderBookList({ > Date: Tue, 21 May 2024 12:24:34 +1000 Subject: [PATCH 099/192] perf: don't require sharp points for fill area drawings --- .../Orderbook/OrderbookChartConnector.tsx | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/pages/Orderbook/OrderbookChartConnector.tsx b/src/pages/Orderbook/OrderbookChartConnector.tsx index eac86b2f1..0478247e0 100644 --- a/src/pages/Orderbook/OrderbookChartConnector.tsx +++ b/src/pages/Orderbook/OrderbookChartConnector.tsx @@ -84,23 +84,23 @@ export default function OrderbookChartConnector({ ctx.filter = `opacity(${weight})`; ctx.beginPath(); // draw top line - ctx.moveTo(sharpPoint(0), sharpPoint(y1)); - ctx.lineTo(sharpPoint(0.4 * width), sharpPoint(y1)); + ctx.moveTo(0, y1); + ctx.lineTo(0.4 * width, y1); ctx.bezierCurveTo( - ...[sharpPoint(0.75 * width), sharpPoint(y1)], - ...[sharpPoint(0.65 * width), sharpPoint(y2)], - ...[sharpPoint(width - 1), sharpPoint(y2)] + ...[0.75 * width, y1], + ...[0.65 * width, y2], + ...[width - 1, y2] ); - ctx.lineTo(sharpPoint(width), sharpPoint(y2)); + ctx.lineTo(width, y2); // draw bottom line - ctx.lineTo(sharpPoint(width), sharpPoint(y4)); - ctx.lineTo(sharpPoint(width - 1), sharpPoint(y4)); + ctx.lineTo(width, y4); + ctx.lineTo(width - 1, y4); ctx.bezierCurveTo( - ...[sharpPoint(0.65 * width), sharpPoint(y4)], - ...[sharpPoint(0.75 * width), sharpPoint(y3)], - ...[sharpPoint(0.4 * width), sharpPoint(y3)] + ...[0.65 * width, y4], + ...[0.75 * width, y3], + ...[0.4 * width, y3] ); - ctx.lineTo(sharpPoint(0), sharpPoint(y3)); + ctx.lineTo(0, y3); // fill ctx.fill(); } @@ -116,11 +116,11 @@ export default function OrderbookChartConnector({ ctx.filter = 'opacity(1)'; ctx.beginPath(); // draw top line - ctx.moveTo(sharpPoint(0), sharpPoint(y1)); - ctx.lineTo(sharpPoint(weight * 0.4 * width + 0.5), sharpPoint(y1)); + ctx.moveTo(0, y1); + ctx.lineTo(weight * 0.4 * width + 0.5, y1); // draw bottom line - ctx.lineTo(sharpPoint(weight * 0.4 * width + 0.5), sharpPoint(y2)); - ctx.lineTo(sharpPoint(0), sharpPoint(y2)); + ctx.lineTo(weight * 0.4 * width + 0.5, y2); + ctx.lineTo(0, y2); // fill ctx.fill(); } From d0c0ebbf592d1155461a06887cd0f37bedca5ac6 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Tue, 21 May 2024 12:25:43 +1000 Subject: [PATCH 100/192] perf: abstract out connection curve point math --- .../Orderbook/OrderbookChartConnector.tsx | 61 +++++++++++-------- 1 file changed, 35 insertions(+), 26 deletions(-) diff --git a/src/pages/Orderbook/OrderbookChartConnector.tsx b/src/pages/Orderbook/OrderbookChartConnector.tsx index 0478247e0..4908af4ef 100644 --- a/src/pages/Orderbook/OrderbookChartConnector.tsx +++ b/src/pages/Orderbook/OrderbookChartConnector.tsx @@ -30,6 +30,18 @@ export default function OrderbookChartConnector({ const draw = useCallback( (canvas: HTMLCanvasElement | null, container: HTMLDivElement | null) => { const ctx = canvas?.getContext('2d'); + + const connectionCurve = ctx + ? [ + 0, + ctx.canvas.width - 30, + ctx.canvas.width - 10, + ctx.canvas.width - 15, + ctx.canvas.width - 1, + ctx.canvas.width, + ] + : []; + if (container && canvas && ctx) { // reset canvas canvas.width = container.offsetWidth; @@ -52,22 +64,21 @@ export default function OrderbookChartConnector({ // define drawing functions function drawConnectionPoints( ctx: CanvasRenderingContext2D, - connectionPoints: ConnectionPoint[], - width: number = ctx.canvas.width + connectionPoints: ConnectionPoint[] ) { ctx.lineWidth = 1; ctx.lineJoin = 'round'; ctx.strokeStyle = 'white'; ctx.beginPath(); connectionPoints.forEach(([y1, y2]) => { - ctx.moveTo(sharpPoint(0), sharpPoint(y1)); - ctx.lineTo(sharpPoint(0.4 * width), sharpPoint(y1)); + ctx.moveTo(sharpPoint(connectionCurve[0]), sharpPoint(y1)); + ctx.lineTo(sharpPoint(connectionCurve[1]), sharpPoint(y1)); ctx.bezierCurveTo( - ...[sharpPoint(0.75 * width), sharpPoint(y1)], - ...[sharpPoint(0.65 * width), sharpPoint(y2)], - ...[sharpPoint(width - 1), sharpPoint(y2)] + ...[sharpPoint(connectionCurve[2]), sharpPoint(y1)], + ...[sharpPoint(connectionCurve[3]), sharpPoint(y2)], + ...[sharpPoint(connectionCurve[4]), sharpPoint(y2)] ); - ctx.lineTo(sharpPoint(width), sharpPoint(y2)); + ctx.lineTo(sharpPoint(connectionCurve[5]), sharpPoint(y2)); }); ctx.stroke(); } @@ -75,8 +86,7 @@ export default function OrderbookChartConnector({ // define drawing functions function drawConnectionArea( ctx: CanvasRenderingContext2D, - connectionArea: ConnectionArea, - width: number = ctx.canvas.width + connectionArea: ConnectionArea ) { const [[y1, y2], [y3, y4], weight, fill = 'white'] = connectionArea; ctx.lineJoin = 'round'; @@ -84,23 +94,23 @@ export default function OrderbookChartConnector({ ctx.filter = `opacity(${weight})`; ctx.beginPath(); // draw top line - ctx.moveTo(0, y1); - ctx.lineTo(0.4 * width, y1); + ctx.moveTo(connectionCurve[0], y1); + ctx.lineTo(connectionCurve[1], y1); ctx.bezierCurveTo( - ...[0.75 * width, y1], - ...[0.65 * width, y2], - ...[width - 1, y2] + ...[connectionCurve[2], y1], + ...[connectionCurve[3], y2], + ...[connectionCurve[4], y2] ); - ctx.lineTo(width, y2); + ctx.lineTo(connectionCurve[5], y2); // draw bottom line - ctx.lineTo(width, y4); - ctx.lineTo(width - 1, y4); + ctx.lineTo(connectionCurve[5], y4); + ctx.lineTo(connectionCurve[4], y4); ctx.bezierCurveTo( - ...[0.65 * width, y4], - ...[0.75 * width, y3], - ...[0.4 * width, y3] + ...[connectionCurve[3], y4], + ...[connectionCurve[2], y3], + ...[connectionCurve[1], y3] ); - ctx.lineTo(0, y3); + ctx.lineTo(connectionCurve[0], y3); // fill ctx.fill(); } @@ -108,8 +118,7 @@ export default function OrderbookChartConnector({ // define drawing functions function drawChartArea( ctx: CanvasRenderingContext2D, - chartArea: ChartArea, - width: number = ctx.canvas.width + chartArea: ChartArea ) { const [y1, y2, weight, fill = 'white'] = chartArea; ctx.fillStyle = fill; @@ -117,9 +126,9 @@ export default function OrderbookChartConnector({ ctx.beginPath(); // draw top line ctx.moveTo(0, y1); - ctx.lineTo(weight * 0.4 * width + 0.5, y1); + ctx.lineTo(weight * connectionCurve[1] + 0.5, y1); // draw bottom line - ctx.lineTo(weight * 0.4 * width + 0.5, y2); + ctx.lineTo(weight * connectionCurve[1] + 0.5, y2); ctx.lineTo(0, y2); // fill ctx.fill(); From b2cf4e4f2a130b9e4819afba768e5237651625fc Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Tue, 21 May 2024 13:48:43 +1000 Subject: [PATCH 101/192] feat: add dynamic decimal places for Orderbook depth amount --- src/pages/Orderbook/OrderbookList.tsx | 28 +++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/src/pages/Orderbook/OrderbookList.tsx b/src/pages/Orderbook/OrderbookList.tsx index d5593940c..e4ad87051 100644 --- a/src/pages/Orderbook/OrderbookList.tsx +++ b/src/pages/Orderbook/OrderbookList.tsx @@ -9,7 +9,7 @@ import { import useResizeObserver from '@react-hook/resize-observer'; import { useRealtimeDisplayPrice } from '../../components/stats/hooks'; -import { formatAmount } from '../../lib/utils/number'; +import { formatAmount, getOrderOfMagnitude } from '../../lib/utils/number'; import { useSimplePrice } from '../../lib/tokenPrices'; import { Token } from '../../lib/web3/utils/tokens'; @@ -109,6 +109,17 @@ export default function OrderBookList({ const decimals = bucketResolution.toFixed(18).split('.')[1] || ''; return decimals.replace(/0+$/, '').length; }, [bucketResolution]); + const [amountDecimalPlacesA, amountDecimalPlacesB] = useMemo(() => { + // ensure minimum displayed amount has at least 2 significant digits + return [ + Math.max(0, 1 - getOrderOfMagnitude(getMinimumReserves(bucketsA))), + Math.max(0, 1 - getOrderOfMagnitude(getMinimumReserves(bucketsB))), + ]; + function getMinimumReserves(buckets: Bucket[] | undefined) { + const bucketReserves = buckets?.slice(0, shownTickRows).map((b) => b[2]); + return bucketReserves ? Math.min(...bucketReserves) : 1; + } + }, [bucketsA, bucketsB, shownTickRows]); const tableContainerRef = useRef(null); const getTableSections = useCallback((table: HTMLDivElement) => { @@ -229,6 +240,7 @@ export default function OrderBookList({
+ @@ -246,6 +258,7 @@ export default function OrderBookList({ bucket={bucket} token={tokenB} priceDecimalPlaces={priceDecimalPlaces} + amountDecimalPlaces={amountDecimalPlacesB} active={price && priceIndication && price <= priceIndication} onHighlight={onHighlightHighPrice} /> @@ -262,7 +275,7 @@ export default function OrderBookList({ onMouseLeave={() => setPriceOffset?.(undefined)} > = priceIndication} onHighlight={onHighlightLowPrice} /> @@ -304,7 +318,7 @@ export default function OrderBookList({ function EmptyRow() { return ( - + ); } @@ -371,11 +385,17 @@ function OrderbookListRow({ })} + ); } From eae46620473a9747cbaeb347736d7ca52407a635 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Tue, 21 May 2024 14:08:30 +1000 Subject: [PATCH 102/192] feat: add directional color to Orderbook depth --- src/pages/Orderbook/OrderbookList.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/pages/Orderbook/OrderbookList.tsx b/src/pages/Orderbook/OrderbookList.tsx index e4ad87051..16734eee5 100644 --- a/src/pages/Orderbook/OrderbookList.tsx +++ b/src/pages/Orderbook/OrderbookList.tsx @@ -259,6 +259,7 @@ export default function OrderBookList({ token={tokenB} priceDecimalPlaces={priceDecimalPlaces} amountDecimalPlaces={amountDecimalPlacesB} + variant="success" active={price && priceIndication && price <= priceIndication} onHighlight={onHighlightHighPrice} /> @@ -302,6 +303,7 @@ export default function OrderBookList({ token={tokenA} priceDecimalPlaces={priceDecimalPlaces} amountDecimalPlaces={amountDecimalPlacesA} + variant="error" active={price && priceIndication && price >= priceIndication} onHighlight={onHighlightLowPrice} /> @@ -329,6 +331,7 @@ function OrderbookListRow({ token, priceDecimalPlaces = 6, amountDecimalPlaces = 2, + variant, active, onHighlight, }: { @@ -337,6 +340,7 @@ function OrderbookListRow({ token: Token; priceDecimalPlaces?: number; amountDecimalPlaces?: number; + variant?: 'success' | 'error'; active?: boolean | 0; onHighlight?: ( e: React.MouseEvent, @@ -378,7 +382,11 @@ function OrderbookListRow({ onMouseEnter={onHoverIn} onMouseLeave={onHoverOut} > - + {formatAmount(bucketPrice, { minimumFractionDigits: priceDecimalPlaces, maximumFractionDigits: priceDecimalPlaces, From cea6f98deda7a63cef5814425d862d8496b15e79 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Tue, 21 May 2024 14:25:19 +1000 Subject: [PATCH 103/192] refactor: move token valuation higher for better efficiency --- src/pages/Orderbook/OrderbookList.tsx | 34 ++++++++++++++------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/src/pages/Orderbook/OrderbookList.tsx b/src/pages/Orderbook/OrderbookList.tsx index 16734eee5..e7407d015 100644 --- a/src/pages/Orderbook/OrderbookList.tsx +++ b/src/pages/Orderbook/OrderbookList.tsx @@ -227,6 +227,11 @@ export default function OrderBookList({ useResizeObserver(tableContainerRef, updateBucketOffsets); useEffect(updateBucketOffsets, [updateBucketOffsets]); + // get price information + const { + data: [priceTokenA, priceTokenB], + } = useSimplePrice([tokenA, tokenB]); + return (
Price AmountValue (USD)
  
- {formatAmount(value ?? '...', { + {formatAmount(reserves ?? '...', { minimumFractionDigits: amountDecimalPlaces, maximumFractionDigits: amountDecimalPlaces, })} + {formatAmount(value ?? '...', { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })} +
@@ -250,13 +255,13 @@ export default function OrderBookList({ {topBuckets?.map((bucket, index) => { - const price = bucket?.[1]; - return price && bucket ? ( + const [, price, reserves] = bucket || []; + return price && reserves ? ( {bottomBuckets?.map((bucket, index) => { - const price = bucket?.[1]; - return price && bucket ? ( + const [, price, reserves] = bucket || []; + return price && reserves ? ( void; }) { - const { data: price } = useSimplePrice(token); - // keep track of hover state and use as a price indication in parents const [hovered, setHovered] = useState(false); const onHover = useCallback( @@ -372,7 +375,6 @@ function OrderbookListRow({ [onHover] ); - const value = reserves * (price || 0); return ( - From bbb204f26162885382a677fec7b6be5114f143b1 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Tue, 21 May 2024 16:35:58 +1000 Subject: [PATCH 110/192] fix: format price values correctly --- src/pages/Orderbook/OrderbookList.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/Orderbook/OrderbookList.tsx b/src/pages/Orderbook/OrderbookList.tsx index 87739388d..63aba63d8 100644 --- a/src/pages/Orderbook/OrderbookList.tsx +++ b/src/pages/Orderbook/OrderbookList.tsx @@ -110,8 +110,8 @@ export default function OrderBookList({ // find how numbers should be displayed const priceDecimalPlaces = useMemo(() => { - const decimals = bucketResolution.toFixed(18).split('.')[1] || ''; - return decimals.replace(/0+$/, '').length; + // use same decimal places as the bucket resolution + return Math.max(0, -Math.round(getOrderOfMagnitude(bucketResolution))); }, [bucketResolution]); const [amountDecimalPlacesA, amountDecimalPlacesB] = useMemo(() => { // ensure minimum displayed amount has at least 2 significant digits From ccfaa597a644368d51c21a7a667b8ac6f5b7ee8b Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Tue, 21 May 2024 19:33:18 +1000 Subject: [PATCH 111/192] fix: allow undefined bucketResolution when liquidity is not yet loaded --- src/pages/Orderbook/Orderbook.tsx | 2 +- src/pages/Orderbook/OrderbookList.tsx | 13 ++++++++----- src/pages/Orderbook/useBuckets.ts | 8 ++++---- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/pages/Orderbook/Orderbook.tsx b/src/pages/Orderbook/Orderbook.tsx index 164dd0c4f..a6fd28e56 100644 --- a/src/pages/Orderbook/Orderbook.tsx +++ b/src/pages/Orderbook/Orderbook.tsx @@ -47,7 +47,7 @@ function Orderbook() { const [chartPriceAxis, setChartPriceAxis] = useState(); const [[depthPriceIndication, depthPriceOffset] = [], setDepthPriceOffset] = useState(); - const [bucketResolution, setBucketResolution] = useState(1); + const [bucketResolution, setBucketResolution] = useState(); const buckets = useBuckets(tokenA, tokenB, bucketResolution); const [depthBucketOffsets, setDepthBucketOffsets] = useState< diff --git a/src/pages/Orderbook/OrderbookList.tsx b/src/pages/Orderbook/OrderbookList.tsx index 63aba63d8..08996604a 100644 --- a/src/pages/Orderbook/OrderbookList.tsx +++ b/src/pages/Orderbook/OrderbookList.tsx @@ -34,8 +34,8 @@ export default function OrderBookList({ tokenB: Token; priceIndication?: number | undefined; buckets?: Buckets; - bucketResolution: number; - setBucketResolution: React.Dispatch>; + bucketResolution: number | undefined; + setBucketResolution: React.Dispatch>; setPriceOffset?: React.Dispatch< React.SetStateAction >; @@ -109,9 +109,12 @@ export default function OrderBookList({ }, [bucketsA, bucketsB, shownTickRows, spacingTicks]); // find how numbers should be displayed - const priceDecimalPlaces = useMemo(() => { + const priceDecimalPlaces = useMemo(() => { // use same decimal places as the bucket resolution - return Math.max(0, -Math.round(getOrderOfMagnitude(bucketResolution))); + return ( + bucketResolution && + Math.max(0, -Math.round(getOrderOfMagnitude(bucketResolution))) + ); }, [bucketResolution]); const [amountDecimalPlacesA, amountDecimalPlacesB] = useMemo(() => { // ensure minimum displayed amount has at least 2 significant digits @@ -401,7 +404,7 @@ export default function OrderBookList({ className="focusable col flex my-0 p-0" list={bucketResolutions} value={bucketResolution} - getLabel={(value) => value || 0} + getLabel={(value) => value} onChange={setBucketResolution} floating /> diff --git a/src/pages/Orderbook/useBuckets.ts b/src/pages/Orderbook/useBuckets.ts index 6de06eeb3..9fe7901b5 100644 --- a/src/pages/Orderbook/useBuckets.ts +++ b/src/pages/Orderbook/useBuckets.ts @@ -25,7 +25,7 @@ export type Buckets = [bucketsA: Bucket[], bucketsB: Bucket[]]; export default function useBucketsByPriceResolution( tokenA?: Token, tokenB?: Token, - bucketResolution: number = 1 + bucketResolution?: number ): Buckets | undefined { const [tokenIdA, tokenIdB] = [getTokenId(tokenA), getTokenId(tokenB)]; const [, currentPrice] = useRealtimeDisplayPrice(tokenA, tokenB); @@ -34,11 +34,11 @@ export default function useBucketsByPriceResolution( // return bucketReserves return useMemo((): Buckets | undefined => { + // return no buckets if the current price is not yet defined + if (!tokenA || !tokenB || !currentPrice || !bucketResolution) return; + // continue const resolutionMagnitude = getOrderOfMagnitude(bucketResolution); const zero = new BigNumber(0); - - // return no buckets if the current price is not yet defined - if (!tokenA || !tokenB || !currentPrice) return undefined; const reduceReservesToBuckets = (inverseDirection: boolean) => { const getDisplayPrice = !inverseDirection ? // get token A prices From 58f7f6b5b94bdf7efe1a45e0becabaedca0871f6 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Tue, 21 May 2024 20:26:16 +1000 Subject: [PATCH 112/192] feat: control connection areas to display only with Depth table tab --- src/components/cards/TabsCard.tsx | 10 +++++++--- src/pages/Orderbook/Orderbook.tsx | 6 +++++- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/components/cards/TabsCard.tsx b/src/components/cards/TabsCard.tsx index 2bba0062e..9e61456fe 100644 --- a/src/components/cards/TabsCard.tsx +++ b/src/components/cards/TabsCard.tsx @@ -6,13 +6,17 @@ import './TabsCard.scss'; export default function TabsCard({ tabs, + tabIndex: givenTabIndex, + setTabIndex: givenSetTabIndex, className, ...props }: JSX.IntrinsicElements['div'] & { tabs: Array; + tabIndex?: number; + setTabIndex?: React.Dispatch>; }) { const [tabIndex, setTabIndex] = useState(0); - const { tab } = tabs[tabIndex]; + const { tab } = tabs[givenTabIndex ?? tabIndex]; return (
setTabIndex(index)} + onClick={() => (givenSetTabIndex ?? setTabIndex)(index)} > {tab.nav} diff --git a/src/pages/Orderbook/Orderbook.tsx b/src/pages/Orderbook/Orderbook.tsx index a6fd28e56..2ee12b8cf 100644 --- a/src/pages/Orderbook/Orderbook.tsx +++ b/src/pages/Orderbook/Orderbook.tsx @@ -242,6 +242,8 @@ function Orderbook() { ]; }, [depthBucketConnections, depthPriceIndicationBucketConnection]); + const [tabIndex, setTabIndex] = useState(0); + return (
@@ -280,7 +282,7 @@ function Orderbook() { } return []; }, [chartPriceAxis, depthPriceIndication, depthPriceOffset])} - connectionAreas={connectionAreas} + connectionAreas={tabIndex === 0 ? connectionAreas : undefined} chartAreas={chartReserveBuckets} />
@@ -292,6 +294,8 @@ function Orderbook() { // sized to the word "Orderbook" with padding minWidth: '15em', }} + tabIndex={tabIndex} + setTabIndex={setTabIndex} tabs={useMemo(() => { return [ { From 284b62b74f2fe76ae86f93f7d77d0a3785202e93 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Tue, 21 May 2024 20:34:39 +1000 Subject: [PATCH 113/192] feat: add more room for depth chart --- src/pages/Orderbook/OrderbookChartConnector.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/Orderbook/OrderbookChartConnector.scss b/src/pages/Orderbook/OrderbookChartConnector.scss index d87f0ea65..1aed10568 100644 --- a/src/pages/Orderbook/OrderbookChartConnector.scss +++ b/src/pages/Orderbook/OrderbookChartConnector.scss @@ -3,7 +3,7 @@ .orderbook-page { .chart-depth-connector { - width: 61px; + width: 90px; margin-right: -1px; z-index: 2; From 2ac05f5b2485fa5f76cbfc2bb712f36d842a73bc Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Wed, 22 May 2024 12:29:49 +1000 Subject: [PATCH 114/192] feat: allow Trades list to display enough rows to scroll --- src/pages/Orderbook/OrderbookTradesList.scss | 11 +++++++++++ src/pages/Orderbook/OrderbookTradesList.tsx | 8 ++++---- 2 files changed, 15 insertions(+), 4 deletions(-) create mode 100644 src/pages/Orderbook/OrderbookTradesList.scss diff --git a/src/pages/Orderbook/OrderbookTradesList.scss b/src/pages/Orderbook/OrderbookTradesList.scss new file mode 100644 index 000000000..9a9d2dffe --- /dev/null +++ b/src/pages/Orderbook/OrderbookTradesList.scss @@ -0,0 +1,11 @@ +@import url('./OrderbookList.scss'); + +.orderbook-trades-list { + overflow: auto; + + th { + position: sticky; + top: 0; + background-color: var(--page-card); + } +} diff --git a/src/pages/Orderbook/OrderbookTradesList.tsx b/src/pages/Orderbook/OrderbookTradesList.tsx index 42a700af2..f83e8cdfd 100644 --- a/src/pages/Orderbook/OrderbookTradesList.tsx +++ b/src/pages/Orderbook/OrderbookTradesList.tsx @@ -15,11 +15,11 @@ import { mapEventAttributes, } from '../../lib/web3/utils/events'; -import './OrderbookList.scss'; +import './OrderbookTradesList.scss'; // ensure that a certain amount of rows are shown in the card -const pageSize = 20; -const tradeListSize = 18; +const pageSize = 30; +const tradeListSize = 30; export default function OrderBookTradesList({ tokenA, @@ -72,7 +72,7 @@ export default function OrderBookTradesList({ : undefined; return ( -
+
- {formatAmount(value ?? '...', { + {formatAmount(reserveValueUSD ?? '...', { minimumFractionDigits: 2, maximumFractionDigits: 2, })} From 9259aed5758f020771c8aae90e0118c135f3b179 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Tue, 21 May 2024 14:55:04 +1000 Subject: [PATCH 104/192] feat: add Orderbook depth colored cell backgrounds --- src/pages/Orderbook/OrderbookList.scss | 15 +++++++++++++++ src/pages/Orderbook/OrderbookList.tsx | 25 ++++++++++++++++++++++++- 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/src/pages/Orderbook/OrderbookList.scss b/src/pages/Orderbook/OrderbookList.scss index 02638605a..68c6363f1 100644 --- a/src/pages/Orderbook/OrderbookList.scss +++ b/src/pages/Orderbook/OrderbookList.scss @@ -59,5 +59,20 @@ padding-top: paddings.$p-2; } } + + .orderbook-list__table__ticks-top .cell-background-gradient { + background: linear-gradient( + 90deg, + rgba(38, 128, 27, 0.1) 0%, + rgba(38, 128, 27, 0.25) 100% + ); + } + .orderbook-list__table__ticks-bottom .cell-background-gradient { + background: linear-gradient( + 90deg, + hsla(0, 84%, 63%, 0.05) 0%, + hsla(0, 84%, 63%, 0.25) 100% + ); + } } } diff --git a/src/pages/Orderbook/OrderbookList.tsx b/src/pages/Orderbook/OrderbookList.tsx index e7407d015..442761abf 100644 --- a/src/pages/Orderbook/OrderbookList.tsx +++ b/src/pages/Orderbook/OrderbookList.tsx @@ -231,6 +231,21 @@ export default function OrderBookList({ const { data: [priceTokenA, priceTokenB], } = useSimplePrice([tokenA, tokenB]); + const maxReservesWeight = useMemo(() => { + const [bucketsA = [], bucketsB = []] = buckets || []; + // find the maximum reserve equivalent value for displaying "USD" value + // note: but we use current Dex price so we don't need to wait for price API + return currentPrice && (bucketsA.length || bucketsB.length) > 0 + ? Math.max( + getMaximumDisplayedReserves(bucketsA), + getMaximumDisplayedReserves(bucketsB) * currentPrice + ) + : Number.POSITIVE_INFINITY; + function getMaximumDisplayedReserves(buckets: Bucket[] | undefined) { + const bucketReserves = buckets?.slice(0, shownTickRows).map((b) => b[2]); + return bucketReserves ? Math.max(...bucketReserves) : 0; + } + }, [buckets, currentPrice, shownTickRows]); return (
@@ -262,6 +277,7 @@ export default function OrderBookList({ price={price} reserves={reserves} reserveValueUSD={priceTokenB && reserves * priceTokenB} + weight={(reserves * (currentPrice ?? 0)) / maxReservesWeight} priceDecimalPlaces={priceDecimalPlaces} amountDecimalPlaces={amountDecimalPlacesB} variant="success" @@ -306,6 +322,7 @@ export default function OrderBookList({ price={price} reserves={reserves} reserveValueUSD={priceTokenA && reserves * priceTokenA} + weight={reserves / maxReservesWeight} priceDecimalPlaces={priceDecimalPlaces} amountDecimalPlaces={amountDecimalPlacesA} variant="error" @@ -334,6 +351,7 @@ function OrderbookListRow({ price: bucketPrice, reserves, reserveValueUSD, + weight, priceDecimalPlaces = 6, amountDecimalPlaces = 2, variant, @@ -343,6 +361,7 @@ function OrderbookListRow({ price: number; reserves: number; reserveValueUSD?: number; + weight?: number; priceDecimalPlaces?: number; amountDecimalPlaces?: number; variant?: 'success' | 'error'; @@ -400,7 +419,11 @@ function OrderbookListRow({ maximumFractionDigits: amountDecimalPlaces, })}
+ +
{formatAmount(reserveValueUSD ?? '...', { minimumFractionDigits: 2, maximumFractionDigits: 2, From 43f367ae3db9a4d5f318fa985e02d71c061e8c1f Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Tue, 21 May 2024 15:11:50 +1000 Subject: [PATCH 105/192] feat: limit area connectors to only visible Orderbook depth rows --- .../Orderbook/OrderbookChartConnector.tsx | 2 ++ src/pages/Orderbook/OrderbookList.tsx | 36 ++++++++++++------- 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/src/pages/Orderbook/OrderbookChartConnector.tsx b/src/pages/Orderbook/OrderbookChartConnector.tsx index 4908af4ef..b857d7c08 100644 --- a/src/pages/Orderbook/OrderbookChartConnector.tsx +++ b/src/pages/Orderbook/OrderbookChartConnector.tsx @@ -89,6 +89,8 @@ export default function OrderbookChartConnector({ connectionArea: ConnectionArea ) { const [[y1, y2], [y3, y4], weight, fill = 'white'] = connectionArea; + // skip areas with no defined points + if (!y1 || !y2 || !y3 || !y4) return; ctx.lineJoin = 'round'; ctx.fillStyle = fill; ctx.filter = `opacity(${weight})`; diff --git a/src/pages/Orderbook/OrderbookList.tsx b/src/pages/Orderbook/OrderbookList.tsx index 442761abf..e347f6231 100644 --- a/src/pages/Orderbook/OrderbookList.tsx +++ b/src/pages/Orderbook/OrderbookList.tsx @@ -196,12 +196,14 @@ export default function OrderBookList({ [currentPrice, bottomCenterOffset], ], // add token A bucket offsets - ...bucketsA.map( - ([innerBound, outerBound], index): [PriceOffset, PriceOffset] => [ - [innerBound, bottomCenterOffset + index * tableRowHeight], - [outerBound, bottomCenterOffset + (index + 1) * tableRowHeight], - ] - ), + ...bucketsA + .slice(0, shownTickRows) + .map( + ([innerBound, outerBound], index): [PriceOffset, PriceOffset] => [ + [innerBound, bottomCenterOffset + index * tableRowHeight], + [outerBound, bottomCenterOffset + (index + 1) * tableRowHeight], + ] + ), ], [ // add current price bucket @@ -210,19 +212,27 @@ export default function OrderBookList({ [currentPrice, topCenterOffset], ], // add token B bucket offsets - ...bucketsB.map( - ([innerBound, outerBound], index): [PriceOffset, PriceOffset] => [ - [innerBound, topCenterOffset - index * tableRowHeight], - [outerBound, topCenterOffset - (index + 1) * tableRowHeight], - ] - ), + ...bucketsB + .slice(0, shownTickRows) + .map( + ([innerBound, outerBound], index): [PriceOffset, PriceOffset] => [ + [innerBound, topCenterOffset - index * tableRowHeight], + [outerBound, topCenterOffset - (index + 1) * tableRowHeight], + ] + ), ], ]); } else { // unset bucket offsets setBucketOffsets?.((offsets) => (offsets.length ? [] : offsets)); } - }, [buckets, currentPrice, getTableSections, setBucketOffsets]); + }, [ + buckets, + currentPrice, + getTableSections, + setBucketOffsets, + shownTickRows, + ]); // execute on every relevent update useResizeObserver(tableContainerRef, updateBucketOffsets); useEffect(updateBucketOffsets, [updateBucketOffsets]); From e3fe5271ef6afb566092392e91b4a31bfc0e8d32 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Tue, 21 May 2024 15:33:19 +1000 Subject: [PATCH 106/192] feat: refine bucket connection opacities --- src/pages/Orderbook/Orderbook.tsx | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/pages/Orderbook/Orderbook.tsx b/src/pages/Orderbook/Orderbook.tsx index 2be175724..01337bdad 100644 --- a/src/pages/Orderbook/Orderbook.tsx +++ b/src/pages/Orderbook/Orderbook.tsx @@ -33,8 +33,8 @@ export default function OrderbookPage() { export type PriceOffset = [price: number, offset?: number]; export type BucketOffset = [inner: PriceOffset, outer: PriceOffset]; -const bucketResolution = 0.001; // size of price set for buckets -const connectionMaxOpacity = 0.5; +const connectionMinOpacity = 0.1; +const connectionMaxOpacity = 0.3; function Orderbook() { // change tokens to match pathname @@ -62,11 +62,10 @@ function Orderbook() { // find the maximum reserve equivalent value for connection opacity const reservesA = bucketsA.map((bucket) => bucket[2]); const reservesB = bucketsB.map((bucket) => bucket[2]); - const maxSideReserves = - Math.max( - Math.max(...reservesA), - Math.max(...reservesB) * currentPrice - ) / connectionMaxOpacity; + const maxSideReserves = Math.max( + Math.max(...reservesA), + Math.max(...reservesB) * currentPrice + ); // draw connections of chart offsets to depth offsets for each bucket return [ bucketOffsetsA @@ -92,7 +91,9 @@ function Orderbook() { // outer depth price offset bucketOffset?.[1][1] || 0, ], - reserves / maxSideReserves, + (reserves / maxSideReserves) * + (connectionMaxOpacity - connectionMinOpacity) + + connectionMinOpacity, ]; } ) @@ -120,7 +121,9 @@ function Orderbook() { // outer depth price offset bucketOffset?.[1][1] || 0, ], - (reserves * currentPrice) / maxSideReserves, + ((reserves * currentPrice) / maxSideReserves) * + (connectionMaxOpacity - connectionMinOpacity) + + connectionMinOpacity, ]; } ) From e434538a8695742aeed99bb2605c6a7f4b7ebfc2 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Tue, 21 May 2024 15:39:43 +1000 Subject: [PATCH 107/192] perf: reduce number of chart liquidity buckets drawn --- src/pages/Orderbook/OrderbookChartConnector.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/pages/Orderbook/OrderbookChartConnector.tsx b/src/pages/Orderbook/OrderbookChartConnector.tsx index b857d7c08..f10f2e47e 100644 --- a/src/pages/Orderbook/OrderbookChartConnector.tsx +++ b/src/pages/Orderbook/OrderbookChartConnector.tsx @@ -57,7 +57,15 @@ export default function OrderbookChartConnector({ drawConnectionArea(ctx, connectionArea); }); chartAreas?.forEach((chartArea) => { - drawChartArea(ctx, chartArea); + // draw chart areas within the container + const [y1, y2] = chartArea; + if ( + // if price is in container area + (y1 >= 0 || y2 >= 0) && + (y1 <= height || y2 <= height) + ) { + drawChartArea(ctx, chartArea); + } }); } From 33d34158d3ce615abcd0aa966dc8108e9b9441b5 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Tue, 21 May 2024 16:02:04 +1000 Subject: [PATCH 108/192] feat: add bucket resolution control --- .../inputs/SelectInput/SelectInput.scss | 1 + .../inputs/SelectInput/SelectInput.tsx | 4 +- src/pages/Orderbook/Orderbook.tsx | 10 ++++- src/pages/Orderbook/OrderbookList.scss | 9 ++++ src/pages/Orderbook/OrderbookList.tsx | 45 ++++++++++++++++++- 5 files changed, 65 insertions(+), 4 deletions(-) diff --git a/src/components/inputs/SelectInput/SelectInput.scss b/src/components/inputs/SelectInput/SelectInput.scss index 81cf7b5ad..548ebbebf 100644 --- a/src/components/inputs/SelectInput/SelectInput.scss +++ b/src/components/inputs/SelectInput/SelectInput.scss @@ -12,6 +12,7 @@ margin-bottom: margins.$m-3; padding: paddings.$p-4; background-color: var(--default); + text-align: left; } .select-input-selection svg { diff --git a/src/components/inputs/SelectInput/SelectInput.tsx b/src/components/inputs/SelectInput/SelectInput.tsx index 9bbd6ff9c..26cecf437 100644 --- a/src/components/inputs/SelectInput/SelectInput.tsx +++ b/src/components/inputs/SelectInput/SelectInput.tsx @@ -128,11 +128,11 @@ export default function SelectInput({ .join(' ')} >
{/* minimize the first column width */} From da2d688583badbc6d26bbbb547b1655f7f10ed8d Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Wed, 22 May 2024 12:43:03 +1000 Subject: [PATCH 115/192] feat: balance red/green brightnesses better, align to text colors --- src/pages/Orderbook/Orderbook.tsx | 8 ++++---- src/pages/Orderbook/OrderbookList.scss | 15 +++++++++++---- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/pages/Orderbook/Orderbook.tsx b/src/pages/Orderbook/Orderbook.tsx index 2ee12b8cf..226c2c381 100644 --- a/src/pages/Orderbook/Orderbook.tsx +++ b/src/pages/Orderbook/Orderbook.tsx @@ -174,19 +174,19 @@ function Orderbook() { return [ ...cumulativeReservesAreasA.map(([y1, y2, reserves]) => { // either value should be within the chart bounds above zero - return [y1, y2, reserves / maxCumulativeReserves, '#FF000022']; + return [y1, y2, reserves / maxCumulativeReserves, '#f0534e42']; }), ...cumulativeReservesAreasB.map(([y1, y2, reserves]) => { // either value should be within the chart bounds above zero - return [y1, y2, reserves / maxCumulativeReserves, '#00FF0022']; + return [y1, y2, reserves / maxCumulativeReserves, '#2683194f']; }), ...reservesAreasA.map(([y1, y2, reserves]) => { // either value should be within the chart bounds above zero - return [y1, y2, reserves / maxCumulativeReserves, 'red']; + return [y1, y2, reserves / maxCumulativeReserves, '#d6363aff']; }), ...reservesAreasB.map(([y1, y2, reserves]) => { // either value should be within the chart bounds above zero - return [y1, y2, reserves / maxCumulativeReserves, 'green']; + return [y1, y2, reserves / maxCumulativeReserves, '#26801bFF']; }), ]; } diff --git a/src/pages/Orderbook/OrderbookList.scss b/src/pages/Orderbook/OrderbookList.scss index 5f183c874..e04803119 100644 --- a/src/pages/Orderbook/OrderbookList.scss +++ b/src/pages/Orderbook/OrderbookList.scss @@ -69,18 +69,25 @@ } } + td.text-success { + color: rgb(38, 128, 27); + } + td.text-error { + color: hsl(1, 66%, 53%); + } + .orderbook-list__table__ticks-top .cell-background-gradient { background: linear-gradient( 90deg, - rgba(38, 128, 27, 0.1) 0%, - rgba(38, 128, 27, 0.25) 100% + rgba(38, 128, 27, 0.08) 0%, + rgba(38, 129, 26, 0.31) 100% ); } .orderbook-list__table__ticks-bottom .cell-background-gradient { background: linear-gradient( 90deg, - hsla(0, 84%, 63%, 0.05) 0%, - hsla(0, 84%, 63%, 0.25) 100% + hsla(0, 84%, 63%, 0.067) 0%, + hsla(1, 84%, 62%, 0.259) 100% ); } } From f330e3cb926c30f82c4bae30a0437c1752c358b1 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Wed, 22 May 2024 13:30:04 +1000 Subject: [PATCH 116/192] fix: flip red/green colors --- src/pages/Orderbook/Orderbook.tsx | 8 ++++---- src/pages/Orderbook/OrderbookList.scss | 8 ++++---- src/pages/Orderbook/OrderbookList.tsx | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/pages/Orderbook/Orderbook.tsx b/src/pages/Orderbook/Orderbook.tsx index 226c2c381..57af55444 100644 --- a/src/pages/Orderbook/Orderbook.tsx +++ b/src/pages/Orderbook/Orderbook.tsx @@ -174,19 +174,19 @@ function Orderbook() { return [ ...cumulativeReservesAreasA.map(([y1, y2, reserves]) => { // either value should be within the chart bounds above zero - return [y1, y2, reserves / maxCumulativeReserves, '#f0534e42']; + return [y1, y2, reserves / maxCumulativeReserves, '#26801b1a']; }), ...cumulativeReservesAreasB.map(([y1, y2, reserves]) => { // either value should be within the chart bounds above zero - return [y1, y2, reserves / maxCumulativeReserves, '#2683194f']; + return [y1, y2, reserves / maxCumulativeReserves, '#f0515115']; }), ...reservesAreasA.map(([y1, y2, reserves]) => { // either value should be within the chart bounds above zero - return [y1, y2, reserves / maxCumulativeReserves, '#d6363aff']; + return [y1, y2, reserves / maxCumulativeReserves, '#26801bFF']; }), ...reservesAreasB.map(([y1, y2, reserves]) => { // either value should be within the chart bounds above zero - return [y1, y2, reserves / maxCumulativeReserves, '#26801bFF']; + return [y1, y2, reserves / maxCumulativeReserves, '#d6363aff']; }), ]; } diff --git a/src/pages/Orderbook/OrderbookList.scss b/src/pages/Orderbook/OrderbookList.scss index e04803119..1ccd7d7d2 100644 --- a/src/pages/Orderbook/OrderbookList.scss +++ b/src/pages/Orderbook/OrderbookList.scss @@ -79,15 +79,15 @@ .orderbook-list__table__ticks-top .cell-background-gradient { background: linear-gradient( 90deg, - rgba(38, 128, 27, 0.08) 0%, - rgba(38, 129, 26, 0.31) 100% + hsla(0, 84%, 63%, 0.067) 0%, + hsla(1, 84%, 62%, 0.259) 100% ); } .orderbook-list__table__ticks-bottom .cell-background-gradient { background: linear-gradient( 90deg, - hsla(0, 84%, 63%, 0.067) 0%, - hsla(1, 84%, 62%, 0.259) 100% + rgba(38, 128, 27, 0.08) 0%, + rgba(38, 129, 26, 0.31) 100% ); } } diff --git a/src/pages/Orderbook/OrderbookList.tsx b/src/pages/Orderbook/OrderbookList.tsx index 08996604a..eb6bd8118 100644 --- a/src/pages/Orderbook/OrderbookList.tsx +++ b/src/pages/Orderbook/OrderbookList.tsx @@ -297,7 +297,7 @@ export default function OrderBookList({ weight={(reserves * (currentPrice ?? 0)) / maxReservesWeight} priceDecimalPlaces={priceDecimalPlaces} amountDecimalPlaces={amountDecimalPlacesB} - variant="success" + variant="error" active={price && priceIndication && price <= priceIndication} onHighlight={onHighlightHighPrice} /> @@ -342,7 +342,7 @@ export default function OrderBookList({ weight={reserves / maxReservesWeight} priceDecimalPlaces={priceDecimalPlaces} amountDecimalPlaces={amountDecimalPlacesA} - variant="error" + variant="success" active={price && priceIndication && price >= priceIndication} onHighlight={onHighlightLowPrice} /> From 1b799a0d7f0546be79a9aef010c72244eccf6c05 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Wed, 22 May 2024 13:39:12 +1000 Subject: [PATCH 117/192] feat: add brightness corrected colors to connection areas --- src/pages/Orderbook/Orderbook.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pages/Orderbook/Orderbook.tsx b/src/pages/Orderbook/Orderbook.tsx index 57af55444..62529c08c 100644 --- a/src/pages/Orderbook/Orderbook.tsx +++ b/src/pages/Orderbook/Orderbook.tsx @@ -95,6 +95,7 @@ function Orderbook() { (reserves / maxSideReserves) * (connectionMaxOpacity - connectionMinOpacity) + connectionMinOpacity, + '#26801bFF', ]; } ) @@ -125,6 +126,7 @@ function Orderbook() { ((reserves * currentPrice) / maxSideReserves) * (connectionMaxOpacity - connectionMinOpacity) + connectionMinOpacity, + '#d6363aff', ]; } ) From 2b936863b5ed08ef58bf7e5aeaaba764996c3163 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Wed, 22 May 2024 18:01:21 +1000 Subject: [PATCH 118/192] feat: ensure depth numbers are drawn over background colors --- src/pages/Orderbook/OrderbookList.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/pages/Orderbook/OrderbookList.tsx b/src/pages/Orderbook/OrderbookList.tsx index eb6bd8118..e91c551a0 100644 --- a/src/pages/Orderbook/OrderbookList.tsx +++ b/src/pages/Orderbook/OrderbookList.tsx @@ -510,10 +510,12 @@ function OrderbookListRow({ className="cell-background-gradient absolute filled" style={{ width: weight && `${Math.round(weight * 100)}%` }} > - {formatAmount(reserveValueUSD ?? '...', { - minimumFractionDigits: 2, - maximumFractionDigits: 2, - })} +
+ {formatAmount(reserveValueUSD ?? '...', { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })} +
); From d52a5eea293d9432d3eaf6fce8375346be351fa2 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Wed, 22 May 2024 13:53:41 +1000 Subject: [PATCH 119/192] fix: draw depth price indication connection line with existing offsets --- src/pages/Orderbook/Orderbook.tsx | 60 ++++++++++++++++++++------- src/pages/Orderbook/OrderbookList.tsx | 57 ++++++------------------- 2 files changed, 58 insertions(+), 59 deletions(-) diff --git a/src/pages/Orderbook/Orderbook.tsx b/src/pages/Orderbook/Orderbook.tsx index 62529c08c..ac579ede1 100644 --- a/src/pages/Orderbook/Orderbook.tsx +++ b/src/pages/Orderbook/Orderbook.tsx @@ -45,8 +45,7 @@ function Orderbook() { const { data: tokenB } = useToken(denomB); const [chartPriceAxis, setChartPriceAxis] = useState(); - const [[depthPriceIndication, depthPriceOffset] = [], setDepthPriceOffset] = - useState(); + const [depthPriceIndication, setDepthPriceIndication] = useState(); const [bucketResolution, setBucketResolution] = useState(); const buckets = useBuckets(tokenA, tokenB, bucketResolution); @@ -270,20 +269,53 @@ function Orderbook() { if ( chartPriceAxis && depthPriceIndication && - depthPriceOffset + depthBucketOffsets ) { - return [ - [ - getChartPricePointOffset( - chartPriceAxis, - depthPriceIndication - ), - depthPriceOffset, - ], - ]; + const [bucketOffsetsA = [], bucketOffsetsB = []] = + depthBucketOffsets; + const offset = + depthPriceIndication === currentPrice + ? // return average of a side's current price bucket + bucketOffsetsA[0] && + ((bucketOffsetsA[0][0][1] || 0) + + (bucketOffsetsA[0][1][1] || 0)) / + 2 + : // return the outer offset of any found bucket + (bucketOffsetsA.find( + ([[innerPrice], [outerPrice]]) => { + return ( + depthPriceIndication < innerPrice && + depthPriceIndication >= outerPrice + ); + } + ) ?? + bucketOffsetsB.find( + ([[innerPrice], [outerPrice]]) => { + return ( + depthPriceIndication > innerPrice && + depthPriceIndication <= outerPrice + ); + } + ))?.[1][1]; + if (offset) { + return [ + [ + getChartPricePointOffset( + chartPriceAxis, + depthPriceIndication + ), + offset, + ], + ]; + } } return []; - }, [chartPriceAxis, depthPriceIndication, depthPriceOffset])} + }, [ + chartPriceAxis, + currentPrice, + depthBucketOffsets, + depthPriceIndication, + ])} connectionAreas={tabIndex === 0 ? connectionAreas : undefined} chartAreas={chartReserveBuckets} /> @@ -311,7 +343,7 @@ function Orderbook() { bucketResolution={bucketResolution} setBucketResolution={setBucketResolution} priceIndication={depthPriceIndication} - setPriceOffset={setDepthPriceOffset} + setPriceIndication={setDepthPriceIndication} setBucketOffsets={setDepthBucketOffsets} /> ) : null, diff --git a/src/pages/Orderbook/OrderbookList.tsx b/src/pages/Orderbook/OrderbookList.tsx index e91c551a0..41a3dfc89 100644 --- a/src/pages/Orderbook/OrderbookList.tsx +++ b/src/pages/Orderbook/OrderbookList.tsx @@ -27,7 +27,7 @@ export default function OrderBookList({ bucketResolution, setBucketResolution, priceIndication, - setPriceOffset, + setPriceIndication, setBucketOffsets, }: { tokenA: Token; @@ -36,42 +36,14 @@ export default function OrderBookList({ buckets?: Buckets; bucketResolution: number | undefined; setBucketResolution: React.Dispatch>; - setPriceOffset?: React.Dispatch< - React.SetStateAction - >; + setPriceIndication?: React.Dispatch>; setBucketOffsets?: React.Dispatch>; }) { const onHighlightPrice = useCallback( - (includeElementHeight: boolean) => - ( - e: React.MouseEvent, - bucketPrice: number | undefined - ) => { - const row = e.target as HTMLTableRowElement | null; - const addOffset = (el: HTMLElement): number => { - if (el.tagName === 'TBODY') return 0; - return ( - el.offsetTop + - (['TABLE', 'TD'].includes(el.tagName) && el.offsetParent - ? addOffset(el.offsetParent as HTMLElement) - : 0) - ); - }; - setPriceOffset?.( - row && bucketPrice - ? [ - bucketPrice, - addOffset(row) + (includeElementHeight ? row.offsetHeight : 1), - ] - : undefined - ); - }, - [setPriceOffset] - ); - - const [onHighlightHighPrice, onHighlightLowPrice] = useMemo( - () => [onHighlightPrice(false), onHighlightPrice(true)], - [onHighlightPrice] + (bucketPrice: number | undefined) => { + setPriceIndication?.(bucketPrice); + }, + [setPriceIndication] ); const [, currentPrice] = useRealtimeDisplayPrice(tokenA, tokenB); @@ -299,7 +271,7 @@ export default function OrderBookList({ amountDecimalPlaces={amountDecimalPlacesB} variant="error" active={price && priceIndication && price <= priceIndication} - onHighlight={onHighlightHighPrice} + onHighlight={onHighlightPrice} /> ) : ( @@ -308,10 +280,8 @@ export default function OrderBookList({
- setPriceOffset?.(currentPrice ? [currentPrice] : undefined) - } - onMouseLeave={() => setPriceOffset?.(undefined)} + onMouseEnter={() => setPriceIndication?.(currentPrice ?? undefined)} + onMouseLeave={() => setPriceIndication?.(undefined)} > = priceIndication} - onHighlight={onHighlightLowPrice} + onHighlight={onHighlightPrice} /> ) : ( @@ -452,10 +422,7 @@ function OrderbookListRow({ amountDecimalPlaces?: number; variant?: 'success' | 'error'; active?: boolean | 0; - onHighlight?: ( - e: React.MouseEvent, - bucketPrice: number | undefined - ) => void; + onHighlight?: (bucketPrice: number | undefined) => void; }) { // keep track of hover state and use as a price indication in parents const [hovered, setHovered] = useState(false); @@ -467,7 +434,7 @@ function OrderbookListRow({ // set hovered state setHovered(!!bucketPrice); // set parent state - onHighlight?.(e, bucketPrice); + onHighlight?.(bucketPrice); }, [onHighlight] ); From 5e381c04049a3ad29b7ce413808d09b6c39bbd7b Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Wed, 22 May 2024 17:27:59 +1000 Subject: [PATCH 120/192] refactor: abstract out depth price indication line creation --- src/pages/Orderbook/Orderbook.tsx | 87 ++++++++----------- .../Orderbook/OrderbookChartConnector.tsx | 2 +- 2 files changed, 37 insertions(+), 52 deletions(-) diff --git a/src/pages/Orderbook/Orderbook.tsx b/src/pages/Orderbook/Orderbook.tsx index ac579ede1..e9a8d12c2 100644 --- a/src/pages/Orderbook/Orderbook.tsx +++ b/src/pages/Orderbook/Orderbook.tsx @@ -14,6 +14,7 @@ import OrderBookChart, { ChartPriceAxisInfo } from './OrderbookChart'; import OrderbookChartConnector, { ChartArea, ConnectionArea, + ConnectionPoint, } from './OrderbookChartConnector'; import OrderBookList from './OrderbookList'; import OrderBookTradesList from './OrderbookTradesList'; @@ -55,6 +56,40 @@ function Orderbook() { const [, currentPrice] = useRealtimeDisplayPrice(tokenA, tokenB); + const depthPriceConnectionPoints = useMemo(() => { + if (chartPriceAxis && depthPriceIndication && depthBucketOffsets) { + const [bucketOffsetsA = [], bucketOffsetsB = []] = depthBucketOffsets; + const offset = + depthPriceIndication === currentPrice + ? // return average of a side's current price bucket + ((bucketOffsetsA[0]?.[0][1] || 0) + + (bucketOffsetsA[0]?.[1][1] || 0)) / + 2 || undefined + : // return the outer offset of any found bucket + (bucketOffsetsA.find(([[innerPrice], [outerPrice]]) => { + return ( + depthPriceIndication < innerPrice && + depthPriceIndication >= outerPrice + ); + }) ?? + bucketOffsetsB.find(([[innerPrice], [outerPrice]]) => { + return ( + depthPriceIndication > innerPrice && + depthPriceIndication <= outerPrice + ); + }))?.[1][1]; + if (offset) { + return [ + [ + getChartPricePointOffset(chartPriceAxis, depthPriceIndication), + offset, + ], + ]; + } + } + return []; + }, [chartPriceAxis, currentPrice, depthBucketOffsets, depthPriceIndication]); + const depthBucketConnections = useMemo(() => { if (currentPrice && chartPriceAxis && buckets && depthBucketOffsets) { const [bucketsA = [], bucketsB = []] = buckets; @@ -265,57 +300,7 @@ function Orderbook() {
{ - if ( - chartPriceAxis && - depthPriceIndication && - depthBucketOffsets - ) { - const [bucketOffsetsA = [], bucketOffsetsB = []] = - depthBucketOffsets; - const offset = - depthPriceIndication === currentPrice - ? // return average of a side's current price bucket - bucketOffsetsA[0] && - ((bucketOffsetsA[0][0][1] || 0) + - (bucketOffsetsA[0][1][1] || 0)) / - 2 - : // return the outer offset of any found bucket - (bucketOffsetsA.find( - ([[innerPrice], [outerPrice]]) => { - return ( - depthPriceIndication < innerPrice && - depthPriceIndication >= outerPrice - ); - } - ) ?? - bucketOffsetsB.find( - ([[innerPrice], [outerPrice]]) => { - return ( - depthPriceIndication > innerPrice && - depthPriceIndication <= outerPrice - ); - } - ))?.[1][1]; - if (offset) { - return [ - [ - getChartPricePointOffset( - chartPriceAxis, - depthPriceIndication - ), - offset, - ], - ]; - } - } - return []; - }, [ - chartPriceAxis, - currentPrice, - depthBucketOffsets, - depthPriceIndication, - ])} + connectionPoints={depthPriceConnectionPoints} connectionAreas={tabIndex === 0 ? connectionAreas : undefined} chartAreas={chartReserveBuckets} /> diff --git a/src/pages/Orderbook/OrderbookChartConnector.tsx b/src/pages/Orderbook/OrderbookChartConnector.tsx index f10f2e47e..e4568c197 100644 --- a/src/pages/Orderbook/OrderbookChartConnector.tsx +++ b/src/pages/Orderbook/OrderbookChartConnector.tsx @@ -3,7 +3,7 @@ import useResizeObserver from '@react-hook/resize-observer'; import './OrderbookChartConnector.scss'; -type ConnectionPoint = [number, number]; +export type ConnectionPoint = [number, number]; export type ConnectionArea = [ ConnectionPoint, ConnectionPoint, From fa9978016acf7ac1161d9cba41c4caaa0466d14c Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Wed, 22 May 2024 17:35:08 +1000 Subject: [PATCH 121/192] refactor: name ConnectionLine better to represent what it looks like: - previously connectionPoint referred to a "y point" of the chart but this was inconsistent with the ConnectionArea type which was named after looking like an area shape --- src/pages/Orderbook/Orderbook.tsx | 6 ++--- .../Orderbook/OrderbookChartConnector.tsx | 22 +++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/pages/Orderbook/Orderbook.tsx b/src/pages/Orderbook/Orderbook.tsx index e9a8d12c2..0efad113d 100644 --- a/src/pages/Orderbook/Orderbook.tsx +++ b/src/pages/Orderbook/Orderbook.tsx @@ -14,7 +14,7 @@ import OrderBookChart, { ChartPriceAxisInfo } from './OrderbookChart'; import OrderbookChartConnector, { ChartArea, ConnectionArea, - ConnectionPoint, + ConnectionLine, } from './OrderbookChartConnector'; import OrderBookList from './OrderbookList'; import OrderBookTradesList from './OrderbookTradesList'; @@ -56,7 +56,7 @@ function Orderbook() { const [, currentPrice] = useRealtimeDisplayPrice(tokenA, tokenB); - const depthPriceConnectionPoints = useMemo(() => { + const depthPriceConnectionLines = useMemo(() => { if (chartPriceAxis && depthPriceIndication && depthBucketOffsets) { const [bucketOffsetsA = [], bucketOffsetsB = []] = depthBucketOffsets; const offset = @@ -300,7 +300,7 @@ function Orderbook() {
diff --git a/src/pages/Orderbook/OrderbookChartConnector.tsx b/src/pages/Orderbook/OrderbookChartConnector.tsx index e4568c197..3a9d38036 100644 --- a/src/pages/Orderbook/OrderbookChartConnector.tsx +++ b/src/pages/Orderbook/OrderbookChartConnector.tsx @@ -3,10 +3,10 @@ import useResizeObserver from '@react-hook/resize-observer'; import './OrderbookChartConnector.scss'; -export type ConnectionPoint = [number, number]; +export type ConnectionLine = [y1: number, y2: number]; export type ConnectionArea = [ - ConnectionPoint, - ConnectionPoint, + ConnectionLine, + ConnectionLine, weight: number, fillColor?: string | CanvasGradient | CanvasPattern ]; @@ -18,11 +18,11 @@ export type ChartArea = [ ]; export default function OrderbookChartConnector({ - connectionPoints, + connectionLines, connectionAreas, chartAreas, }: { - connectionPoints?: ConnectionPoint[]; + connectionLines?: ConnectionLine[]; connectionAreas?: ConnectionArea[]; chartAreas?: ChartArea[]; }) { @@ -50,8 +50,8 @@ export default function OrderbookChartConnector({ const height = canvas.height; ctx?.clearRect(0, 0, width, height); // draw elements - if (connectionPoints?.length) { - drawConnectionPoints(ctx, connectionPoints); + if (connectionLines?.length) { + drawConnectionLines(ctx, connectionLines); } connectionAreas?.forEach((connectionArea) => { drawConnectionArea(ctx, connectionArea); @@ -70,15 +70,15 @@ export default function OrderbookChartConnector({ } // define drawing functions - function drawConnectionPoints( + function drawConnectionLines( ctx: CanvasRenderingContext2D, - connectionPoints: ConnectionPoint[] + connectionLines: ConnectionLine[] ) { ctx.lineWidth = 1; ctx.lineJoin = 'round'; ctx.strokeStyle = 'white'; ctx.beginPath(); - connectionPoints.forEach(([y1, y2]) => { + connectionLines.forEach(([y1, y2]) => { ctx.moveTo(sharpPoint(connectionCurve[0]), sharpPoint(y1)); ctx.lineTo(sharpPoint(connectionCurve[1]), sharpPoint(y1)); ctx.bezierCurveTo( @@ -149,7 +149,7 @@ export default function OrderbookChartConnector({ return Math.round(value) + 0.5; } }, - [chartAreas, connectionAreas, connectionPoints] + [chartAreas, connectionAreas, connectionLines] ); // store ref but also draw on canvas when first found From d92926c2e12bcafa6909df95573ed35354ec82ad Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Wed, 22 May 2024 17:53:29 +1000 Subject: [PATCH 122/192] feat: add active row styling for when current price row is hovered --- src/pages/Orderbook/OrderbookList.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/pages/Orderbook/OrderbookList.tsx b/src/pages/Orderbook/OrderbookList.tsx index 41a3dfc89..cf5804c46 100644 --- a/src/pages/Orderbook/OrderbookList.tsx +++ b/src/pages/Orderbook/OrderbookList.tsx @@ -278,7 +278,14 @@ export default function OrderBookList({ ); })}
- + setPriceIndication?.(currentPrice ?? undefined)} onMouseLeave={() => setPriceIndication?.(undefined)} From 22155210191021f67b2f3d500855d89a65987c89 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Wed, 22 May 2024 17:59:31 +1000 Subject: [PATCH 123/192] feat: remove price indication for current price on chart - it is redundant because the chart has a last price indicator --- src/pages/Orderbook/Orderbook.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/pages/Orderbook/Orderbook.tsx b/src/pages/Orderbook/Orderbook.tsx index 0efad113d..29a3ab249 100644 --- a/src/pages/Orderbook/Orderbook.tsx +++ b/src/pages/Orderbook/Orderbook.tsx @@ -293,7 +293,11 @@ function Orderbook() { )} From bd3fe8e2d1d377b793af42a305cdfc621c37bc3c Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Wed, 22 May 2024 18:05:58 +1000 Subject: [PATCH 124/192] feat: re-enable chart scrolling --- src/pages/Orderbook/OrderbookChart.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/pages/Orderbook/OrderbookChart.tsx b/src/pages/Orderbook/OrderbookChart.tsx index 18be1211d..d3f3c6bc5 100644 --- a/src/pages/Orderbook/OrderbookChart.tsx +++ b/src/pages/Orderbook/OrderbookChart.tsx @@ -47,8 +47,10 @@ const defaultWidgetOptions: Partial = { 'header_symbol_search', // disable comparing multiple symbols: can get complicated 'header_compare', - // disable mouse-down panning behavior: ensures last tick is current time - 'chart_scroll', + // note: the mouse panning feature is called 'chart_scroll' + // disabling this ensures that the right side of chart is current time + // however this isn't necessary to lock to understand the liquidity + // visualization we have added to the right of it ], enabled_features: [], charts_storage_url: 'https://saveload.tradingview.com', From c0e22e912e5684fa96d3fee5a53834884977f290 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Wed, 22 May 2024 18:18:57 +1000 Subject: [PATCH 125/192] feat: allow chart axis updates to run faster when chart is focused --- src/pages/Orderbook/OrderbookChart.tsx | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/pages/Orderbook/OrderbookChart.tsx b/src/pages/Orderbook/OrderbookChart.tsx index d3f3c6bc5..048c49cfd 100644 --- a/src/pages/Orderbook/OrderbookChart.tsx +++ b/src/pages/Orderbook/OrderbookChart.tsx @@ -1,5 +1,5 @@ import BigNumber from 'bignumber.js'; -import { useEffect, useMemo, useRef, useState } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { useDeepCompareMemoize } from 'use-deep-compare-effect'; import { @@ -521,6 +521,10 @@ export default function OrderBookChart({ } }, [chart, priceIndication]); + const [isFocused, setIsFocused] = useState(false); + const setFocused = useCallback(() => setIsFocused(true), []); + const setUnfocused = useCallback(() => setIsFocused(false), []); + useEffect(() => { if (chart) { const checkChartAxis = () => { @@ -549,14 +553,21 @@ export default function OrderBookChart({ } return chartPriceAxis; }); - timeout = setTimeout(checkChartAxis, 100); + timeout = setTimeout(checkChartAxis, isFocused ? 30 : 100); }; let timeout = setTimeout(checkChartAxis, 0); return () => clearTimeout(timeout); } - }, [chart, setPriceAxis]); - - return
; + }, [chart, isFocused, setPriceAxis]); + + return ( +
+ ); } function getBarFromTimeSeriesRow([ From 019eeb02d98b6b6ebb5c6baf46bc2c9b1e2b6ae8 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Wed, 22 May 2024 19:38:28 +1000 Subject: [PATCH 126/192] feat: enable click to select limit price --- src/components/cards/LimitOrderCard.tsx | 33 +++++++++++++++++++++++-- src/pages/Orderbook/Orderbook.tsx | 21 ++++++++++++++-- src/pages/Orderbook/OrderbookList.scss | 5 ++++ src/pages/Orderbook/OrderbookList.tsx | 22 +++++++++++++++++ 4 files changed, 77 insertions(+), 4 deletions(-) diff --git a/src/components/cards/LimitOrderCard.tsx b/src/components/cards/LimitOrderCard.tsx index 56d1094b9..82d785196 100644 --- a/src/components/cards/LimitOrderCard.tsx +++ b/src/components/cards/LimitOrderCard.tsx @@ -85,9 +85,11 @@ const defaultExecutionType: AllowedLimitOrderTypeKey = 'FILL_OR_KILL'; export default function LimitOrderCard({ tokenA, tokenB, + getSetLimitPrice, }: { tokenA?: Token; tokenB?: Token; + getSetLimitPrice?: (setLimitPrice: (limitPrice: string) => void) => void; }) { return (
{tokenA && tokenB && ( - + )}
@@ -178,7 +184,15 @@ function getExpirationTimeMs( return getCustomExpirationTimeMs(timeAmount, timePeriod); } -function LimitOrder({ tokenA, tokenB }: { tokenA: Token; tokenB: Token }) { +function LimitOrder({ + tokenA, + tokenB, + getSetLimitPrice, +}: { + tokenA: Token; + tokenB: Token; + getSetLimitPrice?: (setLimitPrice: (limitPrice: string) => void) => void; +}) { const [modeTab, setModeTab] = useState(modeTabs[0]); const [priceTab, setPriceTab] = useState(priceTabs[0]); @@ -327,6 +341,21 @@ function LimitOrder({ tokenA, tokenB }: { tokenA: Token; tokenB: Token }) { [formSetState, offsetLimitPrice] ); + // pass up a callback for parents to set the limit price here + useEffect(() => { + getSetLimitPrice?.((limitPriceString) => { + const limitPrice = Number(limitPriceString); + if (currentPriceAtoB && limitPrice && !isNaN(limitPrice)) { + setPriceTab('Limit'); + switchLimitOption(-1); + setModeTab(currentPriceAtoB > limitPrice ? 'Buy' : 'Sell'); + formSetState.setLimitPrice?.(limitPriceString); + } else { + switchLimitOption(0); + } + }); + }, [currentPriceAtoB, formSetState, getSetLimitPrice, switchLimitOption]); + // detect when the user has asked for a limit "outside liquidity bounds" // in these cases the simulation won't help because it can only compute the // immediate result of the msg tx: the future amountOut must be estimated diff --git a/src/pages/Orderbook/Orderbook.tsx b/src/pages/Orderbook/Orderbook.tsx index 29a3ab249..a865dfd47 100644 --- a/src/pages/Orderbook/Orderbook.tsx +++ b/src/pages/Orderbook/Orderbook.tsx @@ -1,4 +1,4 @@ -import { useMemo, useState } from 'react'; +import { useCallback, useMemo, useRef, useState } from 'react'; import { useMatch } from 'react-router-dom'; import { useDenomFromPathParam } from '../../lib/web3/hooks/useTokens'; @@ -278,6 +278,17 @@ function Orderbook() { ]; }, [depthBucketConnections, depthPriceIndicationBucketConnection]); + // set limit price using a ref to avoid a useEffect re-rendering issue + const setLimitPriceRef = useRef<(limitPrice: string) => void>(); + const getSetLimitPrice = useCallback< + (setLimitPrice: (limitPrice: string) => void) => void + >((setLimitPrice) => { + setLimitPriceRef.current = setLimitPrice; + }, []); + const setLimitPrice = useCallback<(limitPrice: string) => void>((price) => { + setLimitPriceRef.current?.(price); + }, []); + const [tabIndex, setTabIndex] = useState(0); return ( @@ -334,6 +345,7 @@ function Orderbook() { priceIndication={depthPriceIndication} setPriceIndication={setDepthPriceIndication} setBucketOffsets={setDepthBucketOffsets} + setLimitPrice={setLimitPrice} /> ) : null, }, @@ -352,6 +364,7 @@ function Orderbook() { bucketResolution, buckets, depthPriceIndication, + setLimitPrice, tokenA, tokenB, ])} @@ -360,7 +373,11 @@ function Orderbook() {
- +
diff --git a/src/pages/Orderbook/OrderbookList.scss b/src/pages/Orderbook/OrderbookList.scss index 1ccd7d7d2..a1fd150df 100644 --- a/src/pages/Orderbook/OrderbookList.scss +++ b/src/pages/Orderbook/OrderbookList.scss @@ -48,6 +48,11 @@ .orderbook-list__table__ticks-bottom .hovered td { border-bottom-color: var(--text-default); } + .orderbook-list__table__ticks-top, + .orderbook-list__table__tick-center, + .orderbook-list__table__ticks-bottom { + cursor: pointer; + } // add cell spacing & { diff --git a/src/pages/Orderbook/OrderbookList.tsx b/src/pages/Orderbook/OrderbookList.tsx index cf5804c46..ddca52756 100644 --- a/src/pages/Orderbook/OrderbookList.tsx +++ b/src/pages/Orderbook/OrderbookList.tsx @@ -29,6 +29,7 @@ export default function OrderBookList({ priceIndication, setPriceIndication, setBucketOffsets, + setLimitPrice, }: { tokenA: Token; tokenB: Token; @@ -38,6 +39,7 @@ export default function OrderBookList({ setBucketResolution: React.Dispatch>; setPriceIndication?: React.Dispatch>; setBucketOffsets?: React.Dispatch>; + setLimitPrice?: (limitPrice: string) => void; }) { const onHighlightPrice = useCallback( (bucketPrice: number | undefined) => { @@ -272,6 +274,14 @@ export default function OrderBookList({ variant="error" active={price && priceIndication && price <= priceIndication} onHighlight={onHighlightPrice} + onClick={(bucketPrice) => { + setLimitPrice?.( + formatAmount(bucketPrice, { + minimumFractionDigits: priceDecimalPlaces, + maximumFractionDigits: priceDecimalPlaces, + }) + ); + }} /> ) : ( @@ -289,6 +299,7 @@ export default function OrderBookList({
setPriceIndication?.(currentPrice ?? undefined)} onMouseLeave={() => setPriceIndication?.(undefined)} + onClick={() => setLimitPrice?.('')} > = priceIndication} onHighlight={onHighlightPrice} + onClick={(bucketPrice) => { + setLimitPrice?.( + formatAmount(bucketPrice, { + minimumFractionDigits: priceDecimalPlaces, + maximumFractionDigits: priceDecimalPlaces, + }) + ); + }} /> ) : ( @@ -420,6 +439,7 @@ function OrderbookListRow({ variant, active, onHighlight, + onClick, }: { price: number; reserves: number; @@ -430,6 +450,7 @@ function OrderbookListRow({ variant?: 'success' | 'error'; active?: boolean | 0; onHighlight?: (bucketPrice: number | undefined) => void; + onClick?: (bucketPrice: number) => void; }) { // keep track of hover state and use as a price indication in parents const [hovered, setHovered] = useState(false); @@ -462,6 +483,7 @@ function OrderbookListRow({ .join(' ')} onMouseEnter={onHoverIn} onMouseLeave={onHoverOut} + onClick={() => onClick?.(bucketPrice)} > Date: Wed, 22 May 2024 20:35:59 +1000 Subject: [PATCH 127/192] fix: track "hovered" style using current price indication: - so the state can be controlled by values from other components --- src/pages/Orderbook/OrderbookList.tsx | 32 ++++++++++++++++----------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/src/pages/Orderbook/OrderbookList.tsx b/src/pages/Orderbook/OrderbookList.tsx index ddca52756..73b5c43ec 100644 --- a/src/pages/Orderbook/OrderbookList.tsx +++ b/src/pages/Orderbook/OrderbookList.tsx @@ -261,8 +261,8 @@ export default function OrderBookList({ {topBuckets?.map((bucket, index) => { - const [, price, reserves] = bucket || []; - return price && reserves ? ( + const [minPrice, price, reserves] = bucket || []; + return minPrice && price && reserves ? ( { setLimitPrice?.( @@ -320,8 +325,8 @@ export default function OrderBookList({ {bottomBuckets?.map((bucket, index) => { - const [, price, reserves] = bucket || []; - return price && reserves ? ( + const [maxPrice, price, reserves] = bucket || []; + return maxPrice && price && reserves ? ( = priceIndication} + hovered={ + priceIndication && + maxPrice > priceIndication && + priceIndication >= price + } onHighlight={onHighlightPrice} onClick={(bucketPrice) => { setLimitPrice?.( @@ -438,6 +448,7 @@ function OrderbookListRow({ amountDecimalPlaces = 2, variant, active, + hovered, onHighlight, onClick, }: { @@ -449,29 +460,24 @@ function OrderbookListRow({ amountDecimalPlaces?: number; variant?: 'success' | 'error'; active?: boolean | 0; + hovered?: boolean | 0; onHighlight?: (bucketPrice: number | undefined) => void; onClick?: (bucketPrice: number) => void; }) { // keep track of hover state and use as a price indication in parents - const [hovered, setHovered] = useState(false); const onHover = useCallback( - ( - e: React.MouseEvent, - bucketPrice: number | undefined - ) => { - // set hovered state - setHovered(!!bucketPrice); + (bucketPrice: number | undefined) => { // set parent state onHighlight?.(bucketPrice); }, [onHighlight] ); const onHoverIn = useCallback>( - (e) => onHover(e, bucketPrice), + () => onHover(bucketPrice), [onHover, bucketPrice] ); const onHoverOut = useCallback>( - (e) => onHover(e, undefined), + () => onHover(undefined), [onHover] ); From 4480f94767295f1ae3322bbd8f320e16dbf91132 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Wed, 22 May 2024 21:55:34 +1000 Subject: [PATCH 128/192] feat: allow setting price indication from LimitOrder form --- src/components/cards/LimitOrderCard.tsx | 27 +++++++++++++++ src/pages/Orderbook/Orderbook.tsx | 46 ++++++++++++------------- src/pages/Orderbook/OrderbookList.tsx | 4 +-- 3 files changed, 51 insertions(+), 26 deletions(-) diff --git a/src/components/cards/LimitOrderCard.tsx b/src/components/cards/LimitOrderCard.tsx index 82d785196..72f0324b4 100644 --- a/src/components/cards/LimitOrderCard.tsx +++ b/src/components/cards/LimitOrderCard.tsx @@ -86,10 +86,12 @@ export default function LimitOrderCard({ tokenA, tokenB, getSetLimitPrice, + setPriceIndication, }: { tokenA?: Token; tokenB?: Token; getSetLimitPrice?: (setLimitPrice: (limitPrice: string) => void) => void; + setPriceIndication?: React.Dispatch>; }) { return (
)} @@ -188,10 +191,12 @@ function LimitOrder({ tokenA, tokenB, getSetLimitPrice, + setPriceIndication, }: { tokenA: Token; tokenB: Token; getSetLimitPrice?: (setLimitPrice: (limitPrice: string) => void) => void; + setPriceIndication?: React.Dispatch>; }) { const [modeTab, setModeTab] = useState(modeTabs[0]); const [priceTab, setPriceTab] = useState(priceTabs[0]); @@ -327,6 +332,28 @@ function LimitOrder({ } }, [buyMode, currentPriceAtoB, limitOption]); + // update the price indication from the form + useEffect(() => { + if (setPriceIndication) { + if (priceTab === 'Limit') { + if (currentPriceAtoB && limitOption === 0) { + setPriceIndication(currentPriceAtoB); + } else { + setPriceIndication(Number(formState.limitPrice || offsetLimitPrice)); + } + } else { + setPriceIndication(undefined); + } + } + }, [ + currentPriceAtoB, + formState.limitPrice, + limitOption, + offsetLimitPrice, + priceTab, + setPriceIndication, + ]); + const switchLimitOption = useCallback( (limitOption: LimitPriceOptions) => { // set value diff --git a/src/pages/Orderbook/Orderbook.tsx b/src/pages/Orderbook/Orderbook.tsx index a865dfd47..1f2150033 100644 --- a/src/pages/Orderbook/Orderbook.tsx +++ b/src/pages/Orderbook/Orderbook.tsx @@ -46,6 +46,7 @@ function Orderbook() { const { data: tokenB } = useToken(denomB); const [chartPriceAxis, setChartPriceAxis] = useState(); + const [formPriceIndication, setFormPriceIndication] = useState(); const [depthPriceIndication, setDepthPriceIndication] = useState(); const [bucketResolution, setBucketResolution] = useState(); const buckets = useBuckets(tokenA, tokenB, bucketResolution); @@ -56,11 +57,12 @@ function Orderbook() { const [, currentPrice] = useRealtimeDisplayPrice(tokenA, tokenB); - const depthPriceConnectionLines = useMemo(() => { - if (chartPriceAxis && depthPriceIndication && depthBucketOffsets) { + const priceIndication = depthPriceIndication ?? formPriceIndication; + const priceConnectionLines = useMemo(() => { + if (chartPriceAxis && priceIndication && depthBucketOffsets) { const [bucketOffsetsA = [], bucketOffsetsB = []] = depthBucketOffsets; const offset = - depthPriceIndication === currentPrice + priceIndication === currentPrice ? // return average of a side's current price bucket ((bucketOffsetsA[0]?.[0][1] || 0) + (bucketOffsetsA[0]?.[1][1] || 0)) / @@ -68,27 +70,22 @@ function Orderbook() { : // return the outer offset of any found bucket (bucketOffsetsA.find(([[innerPrice], [outerPrice]]) => { return ( - depthPriceIndication < innerPrice && - depthPriceIndication >= outerPrice + priceIndication < innerPrice && priceIndication >= outerPrice ); }) ?? bucketOffsetsB.find(([[innerPrice], [outerPrice]]) => { return ( - depthPriceIndication > innerPrice && - depthPriceIndication <= outerPrice + priceIndication > innerPrice && priceIndication <= outerPrice ); }))?.[1][1]; if (offset) { return [ - [ - getChartPricePointOffset(chartPriceAxis, depthPriceIndication), - offset, - ], + [getChartPricePointOffset(chartPriceAxis, priceIndication), offset], ]; } } return []; - }, [chartPriceAxis, currentPrice, depthBucketOffsets, depthPriceIndication]); + }, [chartPriceAxis, currentPrice, depthBucketOffsets, priceIndication]); const depthBucketConnections = useMemo(() => { if (currentPrice && chartPriceAxis && buckets && depthBucketOffsets) { @@ -228,12 +225,12 @@ function Orderbook() { } }, [depthBucketConnections]); - const depthPriceIndicationBucketConnection = useMemo< + const priceIndicationBucketConnection = useMemo< ConnectionArea | undefined >(() => { const [bucketOffsetsA, bucketOffsetsB] = depthBucketOffsets; - if (chartPriceAxis && currentPrice && depthPriceIndication) { - const price = depthPriceIndication; + if (chartPriceAxis && currentPrice && priceIndication) { + const price = priceIndication; const [innerBucket, outerBucket] = price <= currentPrice ? [ @@ -266,17 +263,17 @@ function Orderbook() { ]; } } - }, [chartPriceAxis, currentPrice, depthBucketOffsets, depthPriceIndication]); + }, [chartPriceAxis, currentPrice, depthBucketOffsets, priceIndication]); const connectionAreas = useMemo(() => { return [ ...(depthBucketConnections?.[0] ?? []), ...(depthBucketConnections?.[1] ?? []), - ...(depthPriceIndicationBucketConnection - ? [depthPriceIndicationBucketConnection] + ...(priceIndicationBucketConnection + ? [priceIndicationBucketConnection] : []), ]; - }, [depthBucketConnections, depthPriceIndicationBucketConnection]); + }, [depthBucketConnections, priceIndicationBucketConnection]); // set limit price using a ref to avoid a useEffect re-rendering issue const setLimitPriceRef = useRef<(limitPrice: string) => void>(); @@ -305,8 +302,8 @@ function Orderbook() { tokenA={tokenB} tokenB={tokenA} priceIndication={ - depthPriceIndication !== currentPrice - ? depthPriceIndication + priceIndication !== currentPrice + ? priceIndication : undefined } setPriceAxis={setChartPriceAxis} @@ -315,7 +312,7 @@ function Orderbook() {
@@ -342,7 +339,7 @@ function Orderbook() { buckets={buckets} bucketResolution={bucketResolution} setBucketResolution={setBucketResolution} - priceIndication={depthPriceIndication} + priceIndication={priceIndication} setPriceIndication={setDepthPriceIndication} setBucketOffsets={setDepthBucketOffsets} setLimitPrice={setLimitPrice} @@ -363,7 +360,7 @@ function Orderbook() { }, [ bucketResolution, buckets, - depthPriceIndication, + priceIndication, setLimitPrice, tokenA, tokenB, @@ -377,6 +374,7 @@ function Orderbook() { tokenA={tokenA} tokenB={tokenB} getSetLimitPrice={getSetLimitPrice} + setPriceIndication={setFormPriceIndication} />
diff --git a/src/pages/Orderbook/OrderbookList.tsx b/src/pages/Orderbook/OrderbookList.tsx index 73b5c43ec..9e49ace60 100644 --- a/src/pages/Orderbook/OrderbookList.tsx +++ b/src/pages/Orderbook/OrderbookList.tsx @@ -272,7 +272,7 @@ export default function OrderBookList({ priceDecimalPlaces={priceDecimalPlaces} amountDecimalPlaces={amountDecimalPlacesB} variant="error" - active={price && priceIndication && price <= priceIndication} + active={price && priceIndication && minPrice < priceIndication} hovered={ priceIndication && minPrice < priceIndication && @@ -336,7 +336,7 @@ export default function OrderBookList({ priceDecimalPlaces={priceDecimalPlaces} amountDecimalPlaces={amountDecimalPlacesA} variant="success" - active={price && priceIndication && price >= priceIndication} + active={price && priceIndication && maxPrice > priceIndication} hovered={ priceIndication && maxPrice > priceIndication && From 2676f3eaaa9f491695f977e3ff992b18b050a8ea Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Wed, 22 May 2024 21:56:20 +1000 Subject: [PATCH 129/192] fix: RadioButtonGroupInput can send strings when numbers were expected - this is due to numbers changing to strings in the Record type --- .../RadioButtonGroupInput/RadioButtonGroupInput.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/RadioButtonGroupInput/RadioButtonGroupInput.tsx b/src/components/RadioButtonGroupInput/RadioButtonGroupInput.tsx index eec055ecb..9c25415a0 100644 --- a/src/components/RadioButtonGroupInput/RadioButtonGroupInput.tsx +++ b/src/components/RadioButtonGroupInput/RadioButtonGroupInput.tsx @@ -96,15 +96,16 @@ export default function RadioButtonGroupInput({ const [movingAssetRef, createRefForValue] = useSelectedButtonBackgroundMove(value); const entries = useMemo(() => { + const valueIsNumber = typeof value === 'number'; return Array.isArray(values) ? values.filter(Boolean).map<[T, string]>((value) => [value, `${value}`]) : values instanceof Map ? Array.from(values.entries()) : (Object.entries(values).map(([value, description]) => [ - value, + valueIsNumber ? Number(value) : value, description, ]) as [T, string][]); - }, [values]); + }, [value, values]); const selectedIndex = entries.findIndex( ([entryValue]) => entryValue === value ); From c9047eeed3e8835f02250c0e18ac0fca3c8f9708 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Wed, 22 May 2024 22:28:50 +1000 Subject: [PATCH 130/192] feat: highlight top of current price bucket when trading in it --- src/pages/Orderbook/Orderbook.tsx | 33 +++++++++++++++++++------- src/pages/Orderbook/OrderbookList.scss | 6 +++-- src/pages/Orderbook/OrderbookList.tsx | 13 ++++++++++ 3 files changed, 41 insertions(+), 11 deletions(-) diff --git a/src/pages/Orderbook/Orderbook.tsx b/src/pages/Orderbook/Orderbook.tsx index 1f2150033..2ad0649fc 100644 --- a/src/pages/Orderbook/Orderbook.tsx +++ b/src/pages/Orderbook/Orderbook.tsx @@ -59,7 +59,12 @@ function Orderbook() { const priceIndication = depthPriceIndication ?? formPriceIndication; const priceConnectionLines = useMemo(() => { - if (chartPriceAxis && priceIndication && depthBucketOffsets) { + if ( + chartPriceAxis && + priceIndication && + depthBucketOffsets && + currentPrice + ) { const [bucketOffsetsA = [], bucketOffsetsB = []] = depthBucketOffsets; const offset = priceIndication === currentPrice @@ -67,17 +72,27 @@ function Orderbook() { ((bucketOffsetsA[0]?.[0][1] || 0) + (bucketOffsetsA[0]?.[1][1] || 0)) / 2 || undefined - : // return the outer offset of any found bucket - (bucketOffsetsA.find(([[innerPrice], [outerPrice]]) => { - return ( - priceIndication < innerPrice && priceIndication >= outerPrice - ); - }) ?? - bucketOffsetsB.find(([[innerPrice], [outerPrice]]) => { + : // check lower buckets + priceIndication < currentPrice + ? // check the inner edge of buckets + bucketOffsetsA[1] && priceIndication >= bucketOffsetsA[1][0][0] + ? bucketOffsetsA[1][0][1] + : bucketOffsetsA.find(([[innerPrice], [outerPrice]]) => { + return ( + priceIndication < innerPrice && priceIndication >= outerPrice + ); + })?.[1][1] + : // check upper buckets + priceIndication > currentPrice + ? // check the inner edge of buckets + bucketOffsetsB[1] && priceIndication <= bucketOffsetsB[1][0][0] + ? bucketOffsetsB[1][0][1] + : bucketOffsetsB.find(([[innerPrice], [outerPrice]]) => { return ( priceIndication > innerPrice && priceIndication <= outerPrice ); - }))?.[1][1]; + })?.[1][1] + : undefined; if (offset) { return [ [getChartPricePointOffset(chartPriceAxis, priceIndication), offset], diff --git a/src/pages/Orderbook/OrderbookList.scss b/src/pages/Orderbook/OrderbookList.scss index a1fd150df..dfc4c9fe4 100644 --- a/src/pages/Orderbook/OrderbookList.scss +++ b/src/pages/Orderbook/OrderbookList.scss @@ -42,10 +42,12 @@ .active td { background-color: var(--page-card-border); } - .orderbook-list__table__ticks-top .hovered td { + .orderbook-list__table__ticks-top .hovered td, + .orderbook-list__table__tick-center.active-top td { border-top-color: var(--text-default); } - .orderbook-list__table__ticks-bottom .hovered td { + .orderbook-list__table__ticks-bottom .hovered td, + .orderbook-list__table__tick-center.active-bottom td { border-bottom-color: var(--text-default); } .orderbook-list__table__ticks-top, diff --git a/src/pages/Orderbook/OrderbookList.tsx b/src/pages/Orderbook/OrderbookList.tsx index 9e49ace60..71a55d31a 100644 --- a/src/pages/Orderbook/OrderbookList.tsx +++ b/src/pages/Orderbook/OrderbookList.tsx @@ -297,6 +297,19 @@ export default function OrderBookList({ className={[ 'orderbook-list__table__tick-center', currentPrice === priceIndication && 'active', + // if price indication is in the first bucket + currentPrice && + priceIndication && + bucketsB?.[0] && + currentPrice < priceIndication && + priceIndication <= bucketsB[0][0] && + 'active active-top', + currentPrice && + priceIndication && + bucketsA?.[0] && + currentPrice > priceIndication && + priceIndication >= bucketsA[0][0] && + 'active active-bottom', ] .filter(Boolean) .join(' ')} From 5ee2474bf976daf3728115633f4ae86db39b6041 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Wed, 22 May 2024 23:30:18 +1000 Subject: [PATCH 131/192] fix: ensure active buckets are consistently calculated --- src/pages/Orderbook/Orderbook.tsx | 69 +++++++++++++-------------- src/pages/Orderbook/OrderbookList.tsx | 28 +++++++---- 2 files changed, 53 insertions(+), 44 deletions(-) diff --git a/src/pages/Orderbook/Orderbook.tsx b/src/pages/Orderbook/Orderbook.tsx index 2ad0649fc..b0c5e3cbb 100644 --- a/src/pages/Orderbook/Orderbook.tsx +++ b/src/pages/Orderbook/Orderbook.tsx @@ -58,6 +58,19 @@ function Orderbook() { const [, currentPrice] = useRealtimeDisplayPrice(tokenA, tokenB); const priceIndication = depthPriceIndication ?? formPriceIndication; + const activeBucketOffsets = useMemo(() => { + if (currentPrice && priceIndication && currentPrice !== priceIndication) { + const [bucketOffsetsA = [], bucketOffsetsB = []] = depthBucketOffsets; + return priceIndication > currentPrice + ? bucketOffsetsB?.filter( + ([[innerBound]]) => priceIndication > innerBound + ) + : bucketOffsetsA?.filter( + ([[innerBound]]) => priceIndication < innerBound + ); + } + }, [depthBucketOffsets, currentPrice, priceIndication]); + const priceConnectionLines = useMemo(() => { if ( chartPriceAxis && @@ -65,7 +78,7 @@ function Orderbook() { depthBucketOffsets && currentPrice ) { - const [bucketOffsetsA = [], bucketOffsetsB = []] = depthBucketOffsets; + const [bucketOffsetsA = []] = depthBucketOffsets; const offset = priceIndication === currentPrice ? // return average of a side's current price bucket @@ -74,24 +87,10 @@ function Orderbook() { 2 || undefined : // check lower buckets priceIndication < currentPrice - ? // check the inner edge of buckets - bucketOffsetsA[1] && priceIndication >= bucketOffsetsA[1][0][0] - ? bucketOffsetsA[1][0][1] - : bucketOffsetsA.find(([[innerPrice], [outerPrice]]) => { - return ( - priceIndication < innerPrice && priceIndication >= outerPrice - ); - })?.[1][1] + ? activeBucketOffsets?.at(-1)?.[1][1] : // check upper buckets priceIndication > currentPrice - ? // check the inner edge of buckets - bucketOffsetsB[1] && priceIndication <= bucketOffsetsB[1][0][0] - ? bucketOffsetsB[1][0][1] - : bucketOffsetsB.find(([[innerPrice], [outerPrice]]) => { - return ( - priceIndication > innerPrice && priceIndication <= outerPrice - ); - })?.[1][1] + ? activeBucketOffsets?.at(-1)?.[1][1] : undefined; if (offset) { return [ @@ -100,7 +99,13 @@ function Orderbook() { } } return []; - }, [chartPriceAxis, currentPrice, depthBucketOffsets, priceIndication]); + }, [ + activeBucketOffsets, + chartPriceAxis, + currentPrice, + depthBucketOffsets, + priceIndication, + ]); const depthBucketConnections = useMemo(() => { if (currentPrice && chartPriceAxis && buckets && depthBucketOffsets) { @@ -248,22 +253,8 @@ function Orderbook() { const price = priceIndication; const [innerBucket, outerBucket] = price <= currentPrice - ? [ - bucketOffsetsA[1], - bucketOffsetsA.find( - ([[innerPriceOffset], [outerPriceOffset]]) => { - return price < innerPriceOffset && price >= outerPriceOffset; - } - ), - ] - : [ - bucketOffsetsB[1], - bucketOffsetsB.find( - ([[innerPriceOffset], [outerPriceOffset]]) => { - return price > innerPriceOffset && price <= outerPriceOffset; - } - ), - ]; + ? [bucketOffsetsA[1], activeBucketOffsets?.at(-1)] + : [bucketOffsetsB[1], activeBucketOffsets?.at(-1)]; if (innerBucket && outerBucket) { return [ [ @@ -271,14 +262,20 @@ function Orderbook() { innerBucket[0][1] || 0, ], [ - getChartPricePointOffset(chartPriceAxis, outerBucket[1][0]), + getChartPricePointOffset(chartPriceAxis, priceIndication), outerBucket[1][1] || 0, ], 0.1, ]; } } - }, [chartPriceAxis, currentPrice, depthBucketOffsets, priceIndication]); + }, [ + activeBucketOffsets, + chartPriceAxis, + currentPrice, + depthBucketOffsets, + priceIndication, + ]); const connectionAreas = useMemo(() => { return [ diff --git a/src/pages/Orderbook/OrderbookList.tsx b/src/pages/Orderbook/OrderbookList.tsx index 71a55d31a..406bbd7f2 100644 --- a/src/pages/Orderbook/OrderbookList.tsx +++ b/src/pages/Orderbook/OrderbookList.tsx @@ -238,6 +238,14 @@ export default function OrderBookList({ } }, [buckets, currentPrice, shownTickRows]); + const activeBuckets = useMemo(() => { + if (currentPrice && priceIndication && currentPrice !== priceIndication) { + return priceIndication > currentPrice + ? bucketsB?.filter(([innerBound]) => priceIndication > innerBound) + : bucketsA?.filter(([innerBound]) => priceIndication < innerBound); + } + }, [bucketsA, bucketsB, currentPrice, priceIndication]); + const orderbookDepth = (
@@ -262,7 +270,8 @@ export default function OrderBookList({ {topBuckets?.map((bucket, index) => { const [minPrice, price, reserves] = bucket || []; - return minPrice && price && reserves ? ( + const lastBucket = bucketsB?.at(-1); + return minPrice && price && reserves && lastBucket ? ( { @@ -339,7 +349,8 @@ export default function OrderBookList({ {bottomBuckets?.map((bucket, index) => { const [maxPrice, price, reserves] = bucket || []; - return maxPrice && price && reserves ? ( + const lastBucket = bucketsA?.at(-1); + return maxPrice && price && reserves && lastBucket ? ( priceIndication} + active={bucket && activeBuckets?.includes(bucket)} hovered={ + bucket && priceIndication && - maxPrice > priceIndication && - priceIndication >= price + activeBuckets?.at(-1) === bucket && + priceIndication >= lastBucket[1] } onHighlight={onHighlightPrice} onClick={(bucketPrice) => { From 14d987598b0e23d44a149d803859e7555b71b270 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Fri, 24 May 2024 00:48:46 +1000 Subject: [PATCH 132/192] feat: allow toggling the display of the connector depth chart --- src/pages/Orderbook/Orderbook.tsx | 75 +++++++++++++------ .../Orderbook/OrderbookChartConnector.scss | 34 +++++++++ .../Orderbook/OrderbookChartConnector.tsx | 48 ++++++++++++ 3 files changed, 133 insertions(+), 24 deletions(-) diff --git a/src/pages/Orderbook/Orderbook.tsx b/src/pages/Orderbook/Orderbook.tsx index b0c5e3cbb..30e307cf5 100644 --- a/src/pages/Orderbook/Orderbook.tsx +++ b/src/pages/Orderbook/Orderbook.tsx @@ -51,6 +51,9 @@ function Orderbook() { const [bucketResolution, setBucketResolution] = useState(); const buckets = useBuckets(tokenA, tokenB, bucketResolution); + const [showIndividualBuckets, setShowIndividualBuckets] = useState(false); + const [showCumulativeBuckets, setShowCumulativeBuckets] = useState(false); + const [depthBucketOffsets, setDepthBucketOffsets] = useState< BucketOffset[][] >([]); @@ -187,7 +190,10 @@ function Orderbook() { }, [buckets, chartPriceAxis, currentPrice, depthBucketOffsets]); const chartReserveBuckets = useMemo(() => { - if (depthBucketConnections) { + if ( + depthBucketConnections && + (showIndividualBuckets || showCumulativeBuckets) + ) { const [connectionsA = [], connectionsB = []] = depthBucketConnections; const reservesAreasA = connectionsA.map< [y1: number, y2: number, reserves: number] @@ -196,14 +202,20 @@ function Orderbook() { return [y1, y2, reserves]; }); let cumulativeReservesA = 0; - const cumulativeReservesAreasA = reservesAreasA.map< - [y1: number, y2: number, reserves: number] - >(([y1, y2, reserves], index, reservesAreas) => { - // either value should be within the chart bounds above zero - cumulativeReservesA += reserves; - const nextReserves = reservesAreas[index + 1]; - return [y1, nextReserves ? nextReserves[0] : y2, cumulativeReservesA]; - }); + const cumulativeAreasA = showCumulativeBuckets + ? reservesAreasA.map<[y1: number, y2: number, reserves: number]>( + ([y1, y2, reserves], index, reservesAreas) => { + // either value should be within the chart bounds above zero + cumulativeReservesA += reserves; + const nextReserves = reservesAreas[index + 1]; + return [ + y1, + nextReserves ? nextReserves[0] : y2, + cumulativeReservesA, + ]; + } + ) + : []; const reservesAreasB = connectionsB.map< [y1: number, y2: number, reserves: number] >(([[y1], [y2], reserves]) => { @@ -211,25 +223,36 @@ function Orderbook() { return [y1, y2, reserves]; }); let cumulativeReservesB = 0; - const cumulativeReservesAreasB = reservesAreasB.map< - [y1: number, y2: number, reserves: number] - >(([y1, y2, reserves], index, reservesAreas) => { - // either value should be within the chart bounds above zero - cumulativeReservesB += reserves; - const nextReserves = reservesAreas[index + 1]; - return [y1, nextReserves ? nextReserves[0] : y2, cumulativeReservesB]; - }); + const cumulativeAreasB = showCumulativeBuckets + ? reservesAreasB.map<[y1: number, y2: number, reserves: number]>( + ([y1, y2, reserves], index, reservesAreas) => { + // either value should be within the chart bounds above zero + cumulativeReservesB += reserves; + const nextReserves = reservesAreas[index + 1]; + return [ + y1, + nextReserves ? nextReserves[0] : y2, + cumulativeReservesB, + ]; + } + ) + : []; const maxCumulativeReserves = - Math.max( - cumulativeReservesAreasA.at(-1)?.[2] || 0, - cumulativeReservesAreasB.at(-1)?.[2] || 0 - ) || 1; + (showCumulativeBuckets + ? Math.max( + cumulativeAreasA.at(-1)?.[2] || 0, + cumulativeAreasB.at(-1)?.[2] || 0 + ) + : Math.max( + ...reservesAreasA.map((v) => v[2]), + ...reservesAreasB.map((v) => v[2]) + )) || 1; return [ - ...cumulativeReservesAreasA.map(([y1, y2, reserves]) => { + ...cumulativeAreasA.map(([y1, y2, reserves]) => { // either value should be within the chart bounds above zero return [y1, y2, reserves / maxCumulativeReserves, '#26801b1a']; }), - ...cumulativeReservesAreasB.map(([y1, y2, reserves]) => { + ...cumulativeAreasB.map(([y1, y2, reserves]) => { // either value should be within the chart bounds above zero return [y1, y2, reserves / maxCumulativeReserves, '#f0515115']; }), @@ -243,7 +266,7 @@ function Orderbook() { }), ]; } - }, [depthBucketConnections]); + }, [depthBucketConnections, showCumulativeBuckets, showIndividualBuckets]); const priceIndicationBucketConnection = useMemo< ConnectionArea | undefined @@ -327,6 +350,10 @@ function Orderbook() { connectionLines={priceConnectionLines} connectionAreas={tabIndex === 0 ? connectionAreas : undefined} chartAreas={chartReserveBuckets} + showIndividualBuckets={showIndividualBuckets} + showCumulativeBuckets={showCumulativeBuckets} + setShowIndividualBuckets={setShowIndividualBuckets} + setShowCumulativeBuckets={setShowCumulativeBuckets} />
diff --git a/src/pages/Orderbook/OrderbookChartConnector.scss b/src/pages/Orderbook/OrderbookChartConnector.scss index 1aed10568..8f999e2f2 100644 --- a/src/pages/Orderbook/OrderbookChartConnector.scss +++ b/src/pages/Orderbook/OrderbookChartConnector.scss @@ -19,5 +19,39 @@ bottom: 0; } } + + .connection-controls { + position: absolute; + top: 0; + right: 0; + + button { + width: 2em; + background-color: var(--page-card); + + &:hover, + &:focus { + border-color: var(--page-card-border); + } + + .button-box { + height: 0.25em; + width: 1em; + } + .bg-green { + background-color: #26801bff; + } + .bg-red { + background-color: #d6363aff; + } + + .button-box { + opacity: 0.3; + } + &.active .button-box { + opacity: 1; + } + } + } } } diff --git a/src/pages/Orderbook/OrderbookChartConnector.tsx b/src/pages/Orderbook/OrderbookChartConnector.tsx index 3a9d38036..4af90738a 100644 --- a/src/pages/Orderbook/OrderbookChartConnector.tsx +++ b/src/pages/Orderbook/OrderbookChartConnector.tsx @@ -21,11 +21,29 @@ export default function OrderbookChartConnector({ connectionLines, connectionAreas, chartAreas, + showIndividualBuckets, + showCumulativeBuckets, + setShowIndividualBuckets, + setShowCumulativeBuckets, }: { connectionLines?: ConnectionLine[]; connectionAreas?: ConnectionArea[]; chartAreas?: ChartArea[]; + showIndividualBuckets: boolean; + showCumulativeBuckets: boolean; + setShowIndividualBuckets: React.Dispatch>; + setShowCumulativeBuckets: React.Dispatch>; }) { + // improve toggling behaviour to behave like a 3 option state + const toggleShowIndividualBuckets = useCallback(() => { + setShowIndividualBuckets((show) => !show); + setShowCumulativeBuckets(false); + }, [setShowCumulativeBuckets, setShowIndividualBuckets]); + const toggleShowCumulativeBuckets = useCallback(() => { + setShowCumulativeBuckets((show) => !show); + setShowIndividualBuckets(false); + }, [setShowCumulativeBuckets, setShowIndividualBuckets]); + // define what to draw const draw = useCallback( (canvas: HTMLCanvasElement | null, container: HTMLDivElement | null) => { @@ -169,6 +187,36 @@ export default function OrderbookChartConnector({ return (
+
+ + +
); } From e2f1d49a58bb9fce51d37355fdec6609c42e1d2c Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Fri, 24 May 2024 01:36:31 +1000 Subject: [PATCH 133/192] feat: add space for connector visualization only when control is active --- .../Orderbook/OrderbookChartConnector.scss | 4 +++- .../Orderbook/OrderbookChartConnector.tsx | 19 ++++++++++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/pages/Orderbook/OrderbookChartConnector.scss b/src/pages/Orderbook/OrderbookChartConnector.scss index 8f999e2f2..35475b2fa 100644 --- a/src/pages/Orderbook/OrderbookChartConnector.scss +++ b/src/pages/Orderbook/OrderbookChartConnector.scss @@ -3,7 +3,6 @@ .orderbook-page { .chart-depth-connector { - width: 90px; margin-right: -1px; z-index: 2; @@ -11,6 +10,9 @@ position: relative; overflow: hidden; flex: 1 1 0; + + transition: width ease 0.5s; + canvas { position: absolute; top: 0; diff --git a/src/pages/Orderbook/OrderbookChartConnector.tsx b/src/pages/Orderbook/OrderbookChartConnector.tsx index 4af90738a..62a4787ba 100644 --- a/src/pages/Orderbook/OrderbookChartConnector.tsx +++ b/src/pages/Orderbook/OrderbookChartConnector.tsx @@ -185,7 +185,15 @@ export default function OrderbookChartConnector({ useResizeObserver(containerRef, () => draw(canvas, containerRef.current)); return ( -
+
@@ -247,6 +247,21 @@ const styles = { width: 80, }, closed: { - width: 48, + width: 36, + }, + boxWidth1: { + width: '0.2em', + }, + boxWidth2: { + width: '0.45em', + }, + boxWidth3: { + width: '0.6em', + }, + boxWidth4: { + width: '0.6em', + }, + boxWidth5: { + width: '0.8em', }, }; From 02ed15e844fb53635fddb1646acba0efefc2b09c Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Fri, 24 May 2024 09:18:06 +1000 Subject: [PATCH 138/192] feat: make chart connector inidividual buckets width smaller --- src/pages/Orderbook/OrderbookChartConnector.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/Orderbook/OrderbookChartConnector.tsx b/src/pages/Orderbook/OrderbookChartConnector.tsx index 4b11c8c97..716ec0c0c 100644 --- a/src/pages/Orderbook/OrderbookChartConnector.tsx +++ b/src/pages/Orderbook/OrderbookChartConnector.tsx @@ -244,7 +244,7 @@ const styles = { width: 96, }, open: { - width: 80, + width: 72, }, closed: { width: 36, From 0339d8f290968c8f8f6951631f95c2c47ec6c562 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Fri, 24 May 2024 09:43:13 +1000 Subject: [PATCH 139/192] feat: make cumulative depth indicating more subtle --- src/pages/Orderbook/Orderbook.tsx | 2 +- src/pages/Orderbook/OrderbookList.scss | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/Orderbook/Orderbook.tsx b/src/pages/Orderbook/Orderbook.tsx index 2006d57a6..2d2307b8d 100644 --- a/src/pages/Orderbook/Orderbook.tsx +++ b/src/pages/Orderbook/Orderbook.tsx @@ -288,7 +288,7 @@ function Orderbook() { getChartPricePointOffset(chartPriceAxis, priceIndication), outerBucket[1][1] || 0, ], - 0.1, + 0.035, ]; } } diff --git a/src/pages/Orderbook/OrderbookList.scss b/src/pages/Orderbook/OrderbookList.scss index dfc4c9fe4..8ba12ba10 100644 --- a/src/pages/Orderbook/OrderbookList.scss +++ b/src/pages/Orderbook/OrderbookList.scss @@ -40,7 +40,7 @@ } } .active td { - background-color: var(--page-card-border); + background-color: hsla(217, 19%, 27%, 0.275); // var(--page-card-border) } .orderbook-list__table__ticks-top .hovered td, .orderbook-list__table__tick-center.active-top td { From dc3ddcf362fcf7fa9ca26316d2d00d4b1557a5c1 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Fri, 24 May 2024 09:52:33 +1000 Subject: [PATCH 140/192] fix: alignment of connector side was off by border-width --- src/pages/Orderbook/OrderbookList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/Orderbook/OrderbookList.tsx b/src/pages/Orderbook/OrderbookList.tsx index 406bbd7f2..d6c641734 100644 --- a/src/pages/Orderbook/OrderbookList.tsx +++ b/src/pages/Orderbook/OrderbookList.tsx @@ -164,7 +164,7 @@ export default function OrderBookList({ ); }; const centerOffset = addOffset(sections.center); - const topCenterOffset = centerOffset; + const topCenterOffset = centerOffset + 1; const bottomCenterOffset = centerOffset + sections.center.offsetHeight; // calculate offsets for all buckets From 00d41ba87387ce27ccaed3871367e24116871aa2 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Fri, 24 May 2024 09:53:26 +1000 Subject: [PATCH 141/192] perf: only use sharp points for price chart axis connection, dimension --- src/pages/Orderbook/OrderbookChartConnector.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/pages/Orderbook/OrderbookChartConnector.tsx b/src/pages/Orderbook/OrderbookChartConnector.tsx index 716ec0c0c..206c38687 100644 --- a/src/pages/Orderbook/OrderbookChartConnector.tsx +++ b/src/pages/Orderbook/OrderbookChartConnector.tsx @@ -105,14 +105,14 @@ export default function OrderbookChartConnector({ ctx.strokeStyle = 'white'; ctx.beginPath(); connectionLines.forEach(([y1, y2]) => { - ctx.moveTo(sharpPoint(connectionCurve[0]), sharpPoint(y1)); - ctx.lineTo(sharpPoint(connectionCurve[1]), sharpPoint(y1)); + ctx.moveTo(connectionCurve[0], sharpPoint(y1)); + ctx.lineTo(connectionCurve[1], sharpPoint(y1)); ctx.bezierCurveTo( - ...[sharpPoint(connectionCurve[2]), sharpPoint(y1)], - ...[sharpPoint(connectionCurve[3]), sharpPoint(y2)], - ...[sharpPoint(connectionCurve[4]), sharpPoint(y2)] + ...[connectionCurve[2], sharpPoint(y1)], + ...[connectionCurve[3], y2], + ...[connectionCurve[4], y2] ); - ctx.lineTo(sharpPoint(connectionCurve[5]), sharpPoint(y2)); + ctx.lineTo(connectionCurve[5], y2); }); ctx.stroke(); } From 31d5cdfa5430b941b64fd719eee16303d0e7ae93 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Fri, 24 May 2024 10:39:09 +1000 Subject: [PATCH 142/192] feat: hide left toolbar of chart by default --- src/pages/Orderbook/OrderbookChart.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/Orderbook/OrderbookChart.tsx b/src/pages/Orderbook/OrderbookChart.tsx index 048c49cfd..5d71fd774 100644 --- a/src/pages/Orderbook/OrderbookChart.tsx +++ b/src/pages/Orderbook/OrderbookChart.tsx @@ -52,7 +52,7 @@ const defaultWidgetOptions: Partial = { // however this isn't necessary to lock to understand the liquidity // visualization we have added to the right of it ], - enabled_features: [], + enabled_features: ['hide_left_toolbar_by_default'], charts_storage_url: 'https://saveload.tradingview.com', charts_storage_api_version: '1.1', // path to static assets of the charting library From c97eda37bf435cf445587c5aa2949dd3680df16c Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Fri, 24 May 2024 10:57:50 +1000 Subject: [PATCH 143/192] fix: remove price indication line when nothing to connect to --- src/pages/Orderbook/Orderbook.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/Orderbook/Orderbook.tsx b/src/pages/Orderbook/Orderbook.tsx index 2d2307b8d..be099f3c1 100644 --- a/src/pages/Orderbook/Orderbook.tsx +++ b/src/pages/Orderbook/Orderbook.tsx @@ -347,7 +347,7 @@ function Orderbook() {
Date: Fri, 24 May 2024 11:40:49 +1000 Subject: [PATCH 144/192] feat: move connection button controls into the Depth table toolbar --- src/pages/Orderbook/Orderbook.tsx | 8 +- .../Orderbook/OrderbookChartConnector.scss | 36 --------- .../Orderbook/OrderbookChartConnector.tsx | 75 +++---------------- src/pages/Orderbook/OrderbookList.scss | 28 +++++++ src/pages/Orderbook/OrderbookList.tsx | 70 ++++++++++++++++- 5 files changed, 113 insertions(+), 104 deletions(-) diff --git a/src/pages/Orderbook/Orderbook.tsx b/src/pages/Orderbook/Orderbook.tsx index be099f3c1..a67cbccd5 100644 --- a/src/pages/Orderbook/Orderbook.tsx +++ b/src/pages/Orderbook/Orderbook.tsx @@ -352,8 +352,6 @@ function Orderbook() { chartAreas={chartReserveBuckets} showIndividualBuckets={showIndividualBuckets} showCumulativeBuckets={showCumulativeBuckets} - setShowIndividualBuckets={setShowIndividualBuckets} - setShowCumulativeBuckets={setShowCumulativeBuckets} />
@@ -382,6 +380,10 @@ function Orderbook() { setPriceIndication={setDepthPriceIndication} setBucketOffsets={setDepthBucketOffsets} setLimitPrice={setLimitPrice} + showIndividualBuckets={showIndividualBuckets} + showCumulativeBuckets={showCumulativeBuckets} + setShowIndividualBuckets={setShowIndividualBuckets} + setShowCumulativeBuckets={setShowCumulativeBuckets} /> ) : null, }, @@ -401,6 +403,8 @@ function Orderbook() { buckets, priceIndication, setLimitPrice, + showCumulativeBuckets, + showIndividualBuckets, tokenA, tokenB, ])} diff --git a/src/pages/Orderbook/OrderbookChartConnector.scss b/src/pages/Orderbook/OrderbookChartConnector.scss index 479ac11ff..978e9eedf 100644 --- a/src/pages/Orderbook/OrderbookChartConnector.scss +++ b/src/pages/Orderbook/OrderbookChartConnector.scss @@ -21,41 +21,5 @@ bottom: 0; } } - - .connection-controls { - position: absolute; - top: 0; - right: 0; - - button { - width: 2em; - width: 1.5em; - padding: paddings.$p-2 paddings.$p-2; - background-color: var(--page-card); - - &:hover, - &:focus { - border-color: var(--page-card-border); - } - - .button-box { - height: 0.175em; - width: 1em; - } - .bg-green { - background-color: #26801bff; - } - .bg-red { - background-color: #d6363aff; - } - - .button-box { - opacity: 0.4; - } - &.active .button-box { - opacity: 1; - } - } - } } } diff --git a/src/pages/Orderbook/OrderbookChartConnector.tsx b/src/pages/Orderbook/OrderbookChartConnector.tsx index 206c38687..594c5046a 100644 --- a/src/pages/Orderbook/OrderbookChartConnector.tsx +++ b/src/pages/Orderbook/OrderbookChartConnector.tsx @@ -23,27 +23,13 @@ export default function OrderbookChartConnector({ chartAreas, showIndividualBuckets, showCumulativeBuckets, - setShowIndividualBuckets, - setShowCumulativeBuckets, }: { connectionLines?: ConnectionLine[]; connectionAreas?: ConnectionArea[]; chartAreas?: ChartArea[]; showIndividualBuckets: boolean; showCumulativeBuckets: boolean; - setShowIndividualBuckets: React.Dispatch>; - setShowCumulativeBuckets: React.Dispatch>; }) { - // improve toggling behaviour to behave like a 3 option state - const toggleShowIndividualBuckets = useCallback(() => { - setShowIndividualBuckets((show) => !show); - setShowCumulativeBuckets(false); - }, [setShowCumulativeBuckets, setShowIndividualBuckets]); - const toggleShowCumulativeBuckets = useCallback(() => { - setShowCumulativeBuckets((show) => !show); - setShowIndividualBuckets(false); - }, [setShowCumulativeBuckets, setShowIndividualBuckets]); - const maxBucketExtent = showCumulativeBuckets ? 1 : 0.9; // define what to draw @@ -197,44 +183,17 @@ export default function OrderbookChartConnector({ className="connector-container" ref={containerRef} style={ - showCumulativeBuckets - ? styles.max - : showIndividualBuckets - ? styles.open - : styles.closed + connectionLines || connectionLines || chartAreas + ? showCumulativeBuckets + ? styles.max + : showIndividualBuckets + ? styles.open + : styles.closed + : styles.hidden } > -
- - -
+
); } @@ -247,21 +206,9 @@ const styles = { width: 72, }, closed: { - width: 36, - }, - boxWidth1: { - width: '0.2em', - }, - boxWidth2: { - width: '0.45em', - }, - boxWidth3: { - width: '0.6em', - }, - boxWidth4: { - width: '0.6em', + width: 30, }, - boxWidth5: { - width: '0.8em', + hidden: { + width: '0.5rem', }, }; diff --git a/src/pages/Orderbook/OrderbookList.scss b/src/pages/Orderbook/OrderbookList.scss index 8ba12ba10..abf0ad70a 100644 --- a/src/pages/Orderbook/OrderbookList.scss +++ b/src/pages/Orderbook/OrderbookList.scss @@ -99,3 +99,31 @@ } } } + +.connection-controls { + button { + padding: paddings.$p-sm paddings.$p-3; + background-color: var(--page-card); + + &:hover, + &:focus { + border-color: var(--page-card-border); + } + + .button-box { + height: 0.1775em; + } + .bg-green { + background-color: #26801bff; + } + .bg-red { + background-color: #d6363aff; + } + .button-box { + opacity: 0.4; + } + &.active .button-box { + opacity: 1; + } + } +} diff --git a/src/pages/Orderbook/OrderbookList.tsx b/src/pages/Orderbook/OrderbookList.tsx index d6c641734..a5c1ee79f 100644 --- a/src/pages/Orderbook/OrderbookList.tsx +++ b/src/pages/Orderbook/OrderbookList.tsx @@ -30,6 +30,10 @@ export default function OrderBookList({ setPriceIndication, setBucketOffsets, setLimitPrice, + showIndividualBuckets, + showCumulativeBuckets, + setShowIndividualBuckets, + setShowCumulativeBuckets, }: { tokenA: Token; tokenB: Token; @@ -40,7 +44,21 @@ export default function OrderBookList({ setPriceIndication?: React.Dispatch>; setBucketOffsets?: React.Dispatch>; setLimitPrice?: (limitPrice: string) => void; + showIndividualBuckets: boolean; + showCumulativeBuckets: boolean; + setShowIndividualBuckets: React.Dispatch>; + setShowCumulativeBuckets: React.Dispatch>; }) { + // improve toggling behaviour to behave like a 3 option state + const toggleShowIndividualBuckets = useCallback(() => { + setShowIndividualBuckets((show) => !show); + setShowCumulativeBuckets(false); + }, [setShowCumulativeBuckets, setShowIndividualBuckets]); + const toggleShowCumulativeBuckets = useCallback(() => { + setShowCumulativeBuckets((show) => !show); + setShowIndividualBuckets(false); + }, [setShowCumulativeBuckets, setShowIndividualBuckets]); + const onHighlightPrice = useCallback( (bucketPrice: number | undefined) => { setPriceIndication?.(bucketPrice); @@ -417,9 +435,39 @@ export default function OrderBookList({ return ( <> -
+
+
+ + +
Date: Fri, 24 May 2024 13:20:45 +1000 Subject: [PATCH 145/192] refactor: simplify filteredBuckets usage in depth table --- src/pages/Orderbook/OrderbookList.tsx | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/src/pages/Orderbook/OrderbookList.tsx b/src/pages/Orderbook/OrderbookList.tsx index a5c1ee79f..96ec33a03 100644 --- a/src/pages/Orderbook/OrderbookList.tsx +++ b/src/pages/Orderbook/OrderbookList.tsx @@ -68,13 +68,6 @@ export default function OrderBookList({ const [, currentPrice] = useRealtimeDisplayPrice(tokenA, tokenB); - // ensure that a certain amount of liquidity rows are shown in the card - const [shownTickRows, setShownTickRows] = useState(8); - const spacingTicks = useMemo( - () => Array.from({ length: shownTickRows }).map(() => undefined), - [shownTickRows] - ); - const [previousPrice, setPreviousPrice] = useState(); const lastPrice = useRef(currentPrice || undefined); useEffect(() => { @@ -93,12 +86,16 @@ export default function OrderBookList({ }, [currentPrice, buckets]); const [bucketsA, bucketsB] = buckets || []; - const [topBuckets, bottomBuckets] = useMemo(() => { + + // ensure that a certain amount of liquidity rows are shown in the card + const [shownTickRows, setShownTickRows] = useState(8); + const [filteredBucketsB, filteredBucketsA] = useMemo(() => { + const spacingTicks: undefined[] = Array.from({ length: shownTickRows }); return [ - [...(bucketsB ?? []), ...spacingTicks].slice(0, shownTickRows).reverse(), + [...(bucketsB ?? []), ...spacingTicks].slice(0, shownTickRows), [...(bucketsA ?? []), ...spacingTicks].slice(0, shownTickRows), ]; - }, [bucketsA, bucketsB, shownTickRows, spacingTicks]); + }, [bucketsA, bucketsB, shownTickRows]); // find how numbers should be displayed const priceDecimalPlaces = useMemo(() => { @@ -286,7 +283,7 @@ export default function OrderBookList({
- {topBuckets?.map((bucket, index) => { + {filteredBucketsB?.reverse().map((bucket, index) => { const [minPrice, price, reserves] = bucket || []; const lastBucket = bucketsB?.at(-1); return minPrice && price && reserves && lastBucket ? ( @@ -365,7 +362,7 @@ export default function OrderBookList({ - {bottomBuckets?.map((bucket, index) => { + {filteredBucketsA?.map((bucket, index) => { const [maxPrice, price, reserves] = bucket || []; const lastBucket = bucketsA?.at(-1); return maxPrice && price && reserves && lastBucket ? ( From 72b52f8d4c5e8a657c09f6bf1982888adf0a1499 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Fri, 24 May 2024 12:23:39 +1000 Subject: [PATCH 146/192] feat: add show Buy/Sell only buttons to Depth toolbar --- src/pages/Orderbook/OrderbookList.scss | 6 +- src/pages/Orderbook/OrderbookList.tsx | 89 ++++++++++++++++++++++---- 2 files changed, 81 insertions(+), 14 deletions(-) diff --git a/src/pages/Orderbook/OrderbookList.scss b/src/pages/Orderbook/OrderbookList.scss index abf0ad70a..a26b2b02a 100644 --- a/src/pages/Orderbook/OrderbookList.scss +++ b/src/pages/Orderbook/OrderbookList.scss @@ -102,7 +102,7 @@ .connection-controls { button { - padding: paddings.$p-sm paddings.$p-3; + padding: paddings.$p-sm paddings.$p-sm; background-color: var(--page-card); &:hover, @@ -126,4 +126,8 @@ opacity: 1; } } + hr { + margin: paddings.$p-sm; + border-right-width: 0; + } } diff --git a/src/pages/Orderbook/OrderbookList.tsx b/src/pages/Orderbook/OrderbookList.tsx index 96ec33a03..4ee13467d 100644 --- a/src/pages/Orderbook/OrderbookList.tsx +++ b/src/pages/Orderbook/OrderbookList.tsx @@ -85,17 +85,40 @@ export default function OrderBookList({ } }, [currentPrice, buckets]); + const [showBuyOrders, setShowBuyOrders] = useState(true); + const [showSellOrders, setShowSellOrders] = useState(true); + + // improve toggling behaviour to behave like a 3 option state + const toggleShowAllOrders = useCallback(() => { + setShowBuyOrders(true); + setShowSellOrders(true); + }, [setShowSellOrders, setShowBuyOrders]); + const toggleShowBuyOrders = useCallback(() => { + setShowBuyOrders(true); + setShowSellOrders(false); + }, [setShowSellOrders, setShowBuyOrders]); + const toggleShowSellOrders = useCallback(() => { + setShowBuyOrders(false); + setShowSellOrders(true); + }, [setShowSellOrders, setShowBuyOrders]); + const [bucketsA, bucketsB] = buckets || []; // ensure that a certain amount of liquidity rows are shown in the card const [shownTickRows, setShownTickRows] = useState(8); - const [filteredBucketsB, filteredBucketsA] = useMemo(() => { + const [filteredBucketsA, filteredBucketsB] = useMemo(() => { const spacingTicks: undefined[] = Array.from({ length: shownTickRows }); + const paddedBucketsA = [...(bucketsA ?? []), ...spacingTicks]; + const paddedBucketsB = [...(bucketsB ?? []), ...spacingTicks]; return [ - [...(bucketsB ?? []), ...spacingTicks].slice(0, shownTickRows), - [...(bucketsA ?? []), ...spacingTicks].slice(0, shownTickRows), + showBuyOrders + ? paddedBucketsA.slice(0, shownTickRows * (!showSellOrders ? 2 : 1)) + : [], + showSellOrders + ? paddedBucketsB.slice(0, shownTickRows * (!showBuyOrders ? 2 : 1)) + : [], ]; - }, [bucketsA, bucketsB, shownTickRows]); + }, [bucketsA, bucketsB, showBuyOrders, showSellOrders, shownTickRows]); // find how numbers should be displayed const priceDecimalPlaces = useMemo(() => { @@ -166,7 +189,7 @@ export default function OrderBookList({ (sections.top.offsetHeight + sections.bottom.offsetHeight) / (sections.top.childElementCount + sections.bottom.childElementCount || 1); - if (tableRowHeight && tableRowHeight > 0 && currentPrice && buckets) { + if (tableRowHeight && tableRowHeight > 0 && currentPrice) { // set bucket offsets // measure center bucket const addOffset = (el: HTMLElement): number => { @@ -183,7 +206,6 @@ export default function OrderBookList({ const bottomCenterOffset = centerOffset + sections.center.offsetHeight; // calculate offsets for all buckets - const [bucketsA = [], bucketsB = []] = buckets || []; setBucketOffsets?.([ [ // add current price bucket @@ -192,8 +214,8 @@ export default function OrderBookList({ [currentPrice, bottomCenterOffset], ], // add token A bucket offsets - ...bucketsA - .slice(0, shownTickRows) + ...filteredBucketsA + .filter((bucket): bucket is Bucket => !!bucket) .map( ([innerBound, outerBound], index): [PriceOffset, PriceOffset] => [ [innerBound, bottomCenterOffset + index * tableRowHeight], @@ -208,8 +230,9 @@ export default function OrderBookList({ [currentPrice, topCenterOffset], ], // add token B bucket offsets - ...bucketsB - .slice(0, shownTickRows) + ...filteredBucketsB + .filter((bucket): bucket is Bucket => !!bucket) + .reverse() .map( ([innerBound, outerBound], index): [PriceOffset, PriceOffset] => [ [innerBound, topCenterOffset - index * tableRowHeight], @@ -223,11 +246,11 @@ export default function OrderBookList({ setBucketOffsets?.((offsets) => (offsets.length ? [] : offsets)); } }, [ - buckets, currentPrice, + filteredBucketsA, + filteredBucketsB, getTableSections, setBucketOffsets, - shownTickRows, ]); // execute on every relevent update useResizeObserver(tableContainerRef, updateBucketOffsets); @@ -462,6 +485,46 @@ export default function OrderBookList({
+
+ + +
Math.abs(getOrderOfMagnitude(resolution)) ) - ) + 2 + ) + 1.5 }em`, }} > From eb6608e9378c1f8071e7f4461d47dd40532444ce Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Fri, 24 May 2024 13:37:52 +1000 Subject: [PATCH 147/192] feat: balance red and greens more --- src/pages/Orderbook/Orderbook.tsx | 2 +- src/pages/Orderbook/OrderbookList.scss | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pages/Orderbook/Orderbook.tsx b/src/pages/Orderbook/Orderbook.tsx index a67cbccd5..54b039e4e 100644 --- a/src/pages/Orderbook/Orderbook.tsx +++ b/src/pages/Orderbook/Orderbook.tsx @@ -262,7 +262,7 @@ function Orderbook() { }), ...reservesAreasB.map(([y1, y2, reserves]) => { // either value should be within the chart bounds above zero - return [y1, y2, reserves / maxCumulativeReserves, '#c3282a']; + return [y1, y2, reserves / maxCumulativeReserves, '#a92325']; }), ]; } diff --git a/src/pages/Orderbook/OrderbookList.scss b/src/pages/Orderbook/OrderbookList.scss index a26b2b02a..98589137a 100644 --- a/src/pages/Orderbook/OrderbookList.scss +++ b/src/pages/Orderbook/OrderbookList.scss @@ -77,7 +77,7 @@ } td.text-success { - color: rgb(38, 128, 27); + color: rgb(38, 128, 45); } td.text-error { color: hsl(1, 66%, 53%); @@ -114,10 +114,10 @@ height: 0.1775em; } .bg-green { - background-color: #26801bff; + background-color: rgb(38, 128, 27); } .bg-red { - background-color: #d6363aff; + background-color: hsl(1, 66%, 40%); } .button-box { opacity: 0.4; From 00a4e842d14edd98cf205dfd7bf89eaf747d3d9d Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Fri, 24 May 2024 16:44:54 +1000 Subject: [PATCH 148/192] fix: prevent thrown exception when changing between chart timescales --- src/pages/Orderbook/OrderbookChart.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/pages/Orderbook/OrderbookChart.tsx b/src/pages/Orderbook/OrderbookChart.tsx index 5d71fd774..e05fa4ac7 100644 --- a/src/pages/Orderbook/OrderbookChart.tsx +++ b/src/pages/Orderbook/OrderbookChart.tsx @@ -529,9 +529,16 @@ export default function OrderBookChart({ if (chart) { const checkChartAxis = () => { setPriceAxis?.((chartPriceAxis) => { + const visibleTimeScaleRange = chart?.getVisibleRange(); + // skip calculation if timescale is not yet visible + // because otherwise a measurement function will throw an exception + if (!visibleTimeScaleRange.from || !visibleTimeScaleRange.to) { + return undefined; + } const pane = chart?.getPanes()[0]; - const rightPriceScale = pane?.getRightPriceScales()[0]; - const visiblePriceRange = rightPriceScale?.getVisiblePriceRange(); + const rightPriceScale = pane?.getMainSourcePriceScale?.(); + // this function will throw an exception if series is not yet visible + const visiblePriceRange = rightPriceScale?.getVisiblePriceRange?.(); const newChartPriceAxis: ChartPriceAxisInfo | undefined = rightPriceScale && visiblePriceRange ? { From 132061bf6c848563ace41b5ee9f49a8d203fb6cb Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Fri, 24 May 2024 18:05:41 +1000 Subject: [PATCH 149/192] feat: fix possible large width of average price description --- src/components/cards/LimitOrderCard.scss | 8 ++++++++ src/components/cards/LimitOrderCard.tsx | 25 ++++++++++++------------ 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/src/components/cards/LimitOrderCard.scss b/src/components/cards/LimitOrderCard.scss index df0ba9745..c41b14fea 100644 --- a/src/components/cards/LimitOrderCard.scss +++ b/src/components/cards/LimitOrderCard.scss @@ -31,6 +31,10 @@ .numeric-value-row__value { color: white; } + .numeric-value-row__suffix { + width: auto; + max-width: 13em; // sized to max "demoUSDC per demoNTRN" + } } .limit-price { @@ -75,6 +79,10 @@ } } } + + .limit-expiry-selection { + min-width: 24em; // sized so that all options can fit without layout wrapping + } .radio-button-group-switch { flex: 0 0 auto; } diff --git a/src/components/cards/LimitOrderCard.tsx b/src/components/cards/LimitOrderCard.tsx index 72f0324b4..ce2319a1e 100644 --- a/src/components/cards/LimitOrderCard.tsx +++ b/src/components/cards/LimitOrderCard.tsx @@ -94,13 +94,7 @@ export default function LimitOrderCard({ setPriceIndication?: React.Dispatch>; }) { return ( -
+
{tokenA && tokenB && (
-
+
Expiry
@@ -1120,16 +1114,23 @@ function NumericValueRow({ tooltip?: string; }) { return ( -
+
{prefix} {tooltip && {tooltip}}
- {loading && } +
-
{value}
- {suffix &&
{suffix}
} +
{value}
+ {suffix && ( +
{suffix}
+ )}
); } From 1653376e9a376f843e9a6b14de60e4c5b0a28a90 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Fri, 24 May 2024 18:43:54 +1000 Subject: [PATCH 150/192] fix: fix value-suffix spacing layout by combining to one inline-block --- src/components/cards/LimitOrderCard.scss | 4 ++-- src/components/cards/LimitOrderCard.tsx | 26 ++++++++++++++---------- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/components/cards/LimitOrderCard.scss b/src/components/cards/LimitOrderCard.scss index c41b14fea..688f2d42e 100644 --- a/src/components/cards/LimitOrderCard.scss +++ b/src/components/cards/LimitOrderCard.scss @@ -31,9 +31,9 @@ .numeric-value-row__value { color: white; } - .numeric-value-row__suffix { + .numeric-value-row__right { width: auto; - max-width: 13em; // sized to max "demoUSDC per demoNTRN" + max-width: 17em; // sized to max "demoUSDC per demoNTRN" } } diff --git a/src/components/cards/LimitOrderCard.tsx b/src/components/cards/LimitOrderCard.tsx index ce2319a1e..73e0f22a5 100644 --- a/src/components/cards/LimitOrderCard.tsx +++ b/src/components/cards/LimitOrderCard.tsx @@ -1119,18 +1119,22 @@ function NumericValueRow({ {prefix} {tooltip && {tooltip}}
-
- +
+ + + + + {value} + + {suffix && ( + {suffix} + )}
-
{value}
- {suffix && ( -
{suffix}
- )}
); } From f2ee8e69c266a50ecef127afc50d6b9988ba5522 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Fri, 24 May 2024 18:51:34 +1000 Subject: [PATCH 151/192] feat: make Trade card even smaller: sized to header buttons --- src/components/cards/LimitOrderCard.scss | 4 ++-- src/components/cards/LimitOrderCard.tsx | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/components/cards/LimitOrderCard.scss b/src/components/cards/LimitOrderCard.scss index 688f2d42e..5a7252d30 100644 --- a/src/components/cards/LimitOrderCard.scss +++ b/src/components/cards/LimitOrderCard.scss @@ -33,7 +33,7 @@ } .numeric-value-row__right { width: auto; - max-width: 17em; // sized to max "demoUSDC per demoNTRN" + max-width: 12em; // sized to max "demoUSDC per demoNTRN" } } @@ -81,7 +81,7 @@ } .limit-expiry-selection { - min-width: 24em; // sized so that all options can fit without layout wrapping + min-width: 20em; // sized so that all options can fit without layout wrapping } .radio-button-group-switch { flex: 0 0 auto; diff --git a/src/components/cards/LimitOrderCard.tsx b/src/components/cards/LimitOrderCard.tsx index 73e0f22a5..c4bd5e201 100644 --- a/src/components/cards/LimitOrderCard.tsx +++ b/src/components/cards/LimitOrderCard.tsx @@ -120,7 +120,6 @@ const expirationOptions = { '1 hours': '1 hour', '1 days': '1 day', '1 weeks': '1 week', - '1 months': '1 month', custom: 'Custom', } as const; type ExpirationOptions = keyof typeof expirationOptions; @@ -243,7 +242,7 @@ function LimitOrder({ const cardModeNav = (
-

Trade

+

Trade

className="order-type-input my-4" @@ -252,7 +251,7 @@ function LimitOrder({ onChange={switchPriceTab} />
-
+
className="order-type-input my-4" values={modeTabs} From 5fdf6fc0d278376d6504f3c59f06e87351c33ff5 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Fri, 24 May 2024 19:06:29 +1000 Subject: [PATCH 152/192] fix: ensure that warnings and errors don't resize the card width --- src/components/cards/LimitOrderCard.scss | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/components/cards/LimitOrderCard.scss b/src/components/cards/LimitOrderCard.scss index 5a7252d30..2ea7fae13 100644 --- a/src/components/cards/LimitOrderCard.scss +++ b/src/components/cards/LimitOrderCard.scss @@ -6,6 +6,13 @@ overflow: visible; } + .text-warning, + .text-error { + width: 100%; + max-width: 20em; + margin: 0 auto; + } + // override default select input styles .select-input { .select-input-selection { From 1648ee883654a099d37e4a6943e926afdef99508 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Fri, 24 May 2024 19:53:34 +1000 Subject: [PATCH 153/192] feat: add depth reserves % bar indication --- src/pages/Orderbook/OrderbookList.scss | 35 ++++++++++++++++++++++++++ src/pages/Orderbook/OrderbookList.tsx | 32 +++++++++++++++++++++++ 2 files changed, 67 insertions(+) diff --git a/src/pages/Orderbook/OrderbookList.scss b/src/pages/Orderbook/OrderbookList.scss index 98589137a..862745269 100644 --- a/src/pages/Orderbook/OrderbookList.scss +++ b/src/pages/Orderbook/OrderbookList.scss @@ -131,3 +131,38 @@ border-right-width: 0; } } + +.orderbook-buy-sell-bar { + & { + justify-content: space-between; + } + .orderbook-buy-sell-bar_buy-icon, + .orderbook-buy-sell-bar_sell-icon { + width: 2.4em; + text-align: center; + border-radius: 0.25rem; + } + .orderbook-buy-sell-bar_buy-icon { + background-color: rgb(38, 128, 27); + } + .orderbook-buy-sell-bar_sell-icon { + background-color: hsl(1, 66%, 40%); + } + .orderbook-buy-sell-bar_buy-value { + color: hsl(125, 54%, 33%); + } + .orderbook-buy-sell-bar_sell-value { + color: hsl(1, 66%, 53%); + } + .orderbook-buy-sell-bar_buy-bar, + .orderbook-buy-sell-bar_sell-bar { + border-radius: 0.25rem; + opacity: 0.5; + } + .orderbook-buy-sell-bar_buy-bar { + background-color: rgb(38, 128, 27); + } + .orderbook-buy-sell-bar_sell-bar { + background-color: hsl(1, 66%, 40%); + } +} diff --git a/src/pages/Orderbook/OrderbookList.tsx b/src/pages/Orderbook/OrderbookList.tsx index 4ee13467d..e28f99ad4 100644 --- a/src/pages/Orderbook/OrderbookList.tsx +++ b/src/pages/Orderbook/OrderbookList.tsx @@ -453,6 +453,16 @@ export default function OrderBookList({ return getBucketResolutions(currentPrice); }, [currentPrice]); + // add all bucket "display reserves" values together for summary bar + const bucketBuyPercent = useMemo(() => { + const [bucketsA, bucketsB] = buckets || []; + const reservesA = bucketsA?.reduce((acc, [, , v]) => acc + v, 0); + const reservesB = bucketsB?.reduce((acc, [, , v]) => acc + v, 0); + return reservesA && reservesB + ? reservesA / (reservesA + reservesB) + : undefined; + }, [buckets]); + return ( <>
@@ -551,6 +561,28 @@ export default function OrderBookList({
{orderbookDepth} +
+
B
+ {bucketBuyPercent !== undefined && ( + <> +
+ {(100 * bucketBuyPercent).toFixed()}% +
+
+
+
+ {(100 - 100 * bucketBuyPercent).toFixed()}% +
+ + )} +
S
+
); } From a36b8c1dd11320a1973191e945f8aa9f4c1a44dd Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Fri, 24 May 2024 20:54:04 +1000 Subject: [PATCH 154/192] fix: fix accidental mutation of stateful arrays --- src/lib/web3/utils/events.ts | 1 + src/pages/Orderbook/OrderbookChart.tsx | 2 +- src/pages/Orderbook/OrderbookList.tsx | 3 +-- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lib/web3/utils/events.ts b/src/lib/web3/utils/events.ts index 079a4396d..43a8f95e5 100644 --- a/src/lib/web3/utils/events.ts +++ b/src/lib/web3/utils/events.ts @@ -275,6 +275,7 @@ export function getLastPrice( { tokenA, tokenB }: { tokenA: Token; tokenB: Token } ) { const lastTickUpdate = events + .slice() .reverse() .find((event): event is DexTickUpdateEvent => { return ( diff --git a/src/pages/Orderbook/OrderbookChart.tsx b/src/pages/Orderbook/OrderbookChart.tsx index e05fa4ac7..9878c307f 100644 --- a/src/pages/Orderbook/OrderbookChart.tsx +++ b/src/pages/Orderbook/OrderbookChart.tsx @@ -433,7 +433,7 @@ export default function OrderBookChart({ const resolutionInMs = resolutionInMsMap[resolution]; // note: the data needs to be in chronological order // and our API delivers results in reverse-chronological order - const chronologicalUpdates = dataUpdates.reverse(); + const chronologicalUpdates = dataUpdates.slice().reverse(); for (const row of chronologicalUpdates) { const bar = getBarFromTimeSeriesRow(row); // add extra empty bars if there would be a gap in time diff --git a/src/pages/Orderbook/OrderbookList.tsx b/src/pages/Orderbook/OrderbookList.tsx index e28f99ad4..ad545fa88 100644 --- a/src/pages/Orderbook/OrderbookList.tsx +++ b/src/pages/Orderbook/OrderbookList.tsx @@ -232,7 +232,6 @@ export default function OrderBookList({ // add token B bucket offsets ...filteredBucketsB .filter((bucket): bucket is Bucket => !!bucket) - .reverse() .map( ([innerBound, outerBound], index): [PriceOffset, PriceOffset] => [ [innerBound, topCenterOffset - index * tableRowHeight], @@ -306,7 +305,7 @@ export default function OrderBookList({
- {filteredBucketsB?.reverse().map((bucket, index) => { + {[...filteredBucketsB].reverse().map((bucket, index) => { const [minPrice, price, reserves] = bucket || []; const lastBucket = bucketsB?.at(-1); return minPrice && price && reserves && lastBucket ? ( From ff6105ce12fa605cd3000642c4d645a058faea0a Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Sat, 25 May 2024 00:59:53 +1000 Subject: [PATCH 155/192] fix: check chart state before doing some chart functions --- src/pages/Orderbook/OrderbookChart.tsx | 28 +++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/src/pages/Orderbook/OrderbookChart.tsx b/src/pages/Orderbook/OrderbookChart.tsx index 9878c307f..84368ade7 100644 --- a/src/pages/Orderbook/OrderbookChart.tsx +++ b/src/pages/Orderbook/OrderbookChart.tsx @@ -83,6 +83,21 @@ interface BlockRangeRequestQuery extends RequestQuery { type TimeSeriesResolution = 'second' | 'minute' | 'hour' | 'day' | 'month'; +// helper function to ensure that chart is interactable +// note: several chart methods such as chart.getVisiblePriceRange() will throw +// an exception if the chart exists but is not in a ready state +// this method should be able to detect is the chart is not in that state +function isChartReady(chart: IChartWidgetApi | undefined) { + const visibleTimeScaleRange = chart?.getVisibleRange(); + // skip calculation if timescale is not yet visible + // because otherwise a measurement function will throw an exception + return ( + visibleTimeScaleRange && + visibleTimeScaleRange.from > 0 && + visibleTimeScaleRange.to + ); +} + export interface ChartPriceAxisInfo extends VisiblePriceRange { mode: PriceScaleMode; height: number; @@ -511,8 +526,9 @@ export default function OrderBookChart({ }, [navigate, tokenIdA, tokenIdB, tokenPairID, tokenPairs]); useEffect(() => { - if (chart && priceIndication) { + if (priceIndication && chart && isChartReady(chart)) { const line = chart + // this function will throw an exception if chart is not ready yet .createOrderLine() .setPrice(priceIndication) .setText('') @@ -529,14 +545,8 @@ export default function OrderBookChart({ if (chart) { const checkChartAxis = () => { setPriceAxis?.((chartPriceAxis) => { - const visibleTimeScaleRange = chart?.getVisibleRange(); - // skip calculation if timescale is not yet visible - // because otherwise a measurement function will throw an exception - if (!visibleTimeScaleRange.from || !visibleTimeScaleRange.to) { - return undefined; - } - const pane = chart?.getPanes()[0]; - const rightPriceScale = pane?.getMainSourcePriceScale?.(); + const pane = isChartReady(chart) ? chart?.getPanes()[0] : undefined; + const rightPriceScale = pane?.getRightPriceScales?.()?.[0]; // this function will throw an exception if series is not yet visible const visiblePriceRange = rightPriceScale?.getVisiblePriceRange?.(); const newChartPriceAxis: ChartPriceAxisInfo | undefined = From b3230b85a4588bb2156dca4e75e1e8f2bfbc5fbd Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Sat, 25 May 2024 04:55:34 +1000 Subject: [PATCH 156/192] feat: add Depth table transition effects --- src/pages/Orderbook/OrderbookList.scss | 35 +++++++++++++++ src/pages/Orderbook/OrderbookList.tsx | 62 ++++++++++++++++++++++++-- src/pages/Orderbook/useBuckets.ts | 16 ++++--- 3 files changed, 103 insertions(+), 10 deletions(-) diff --git a/src/pages/Orderbook/OrderbookList.scss b/src/pages/Orderbook/OrderbookList.scss index 862745269..59c9139d0 100644 --- a/src/pages/Orderbook/OrderbookList.scss +++ b/src/pages/Orderbook/OrderbookList.scss @@ -83,6 +83,9 @@ color: hsl(1, 66%, 53%); } + .cell-background-gradient { + transition: width ease 0.5s; + } .orderbook-list__table__ticks-top .cell-background-gradient { background: linear-gradient( 90deg, @@ -158,6 +161,7 @@ .orderbook-buy-sell-bar_sell-bar { border-radius: 0.25rem; opacity: 0.5; + transition: width 0.5s ease; } .orderbook-buy-sell-bar_buy-bar { background-color: rgb(38, 128, 27); @@ -166,3 +170,34 @@ background-color: hsl(1, 66%, 40%); } } + +@keyframes appear { + from { + opacity: 0; + } +} +.reserves-new { + animation: appear 0.5s ease; + transition: opacity 0.5s ease; +} + +@keyframes flash { + from { + opacity: 0.5; + } + to { + opacity: 0; + } +} +.reserves-increase .cell-background { + animation: flash 0.5s ease; + transition: opacity 0.5s ease; + background-color: rgb(38, 128, 27); + opacity: 0; +} +.reserves-decrease .cell-background { + animation: flash 0.5s ease; + transition: opacity 0.5s ease; + background-color: hsl(1, 66%, 40%); + opacity: 0; +} diff --git a/src/pages/Orderbook/OrderbookList.tsx b/src/pages/Orderbook/OrderbookList.tsx index ad545fa88..2e4c0c1d9 100644 --- a/src/pages/Orderbook/OrderbookList.tsx +++ b/src/pages/Orderbook/OrderbookList.tsx @@ -102,7 +102,7 @@ export default function OrderBookList({ setShowSellOrders(true); }, [setShowSellOrders, setShowBuyOrders]); - const [bucketsA, bucketsB] = buckets || []; + const [bucketsA, bucketsB, height] = buckets || []; // ensure that a certain amount of liquidity rows are shown in the card const [shownTickRows, setShownTickRows] = useState(8); @@ -120,6 +120,28 @@ export default function OrderBookList({ ]; }, [bucketsA, bucketsB, showBuyOrders, showSellOrders, shownTickRows]); + // keep track of buckets by chain height + const [pastBuckets, setPastBuckets] = useState<(Buckets | undefined)[]>([]); + useEffect(() => { + setPastBuckets((pastBuckets) => { + const previousBuckets = pastBuckets?.at(0); + const previousBucketsHeight = previousBuckets?.[2] || 0; + const currentBuckets: Buckets | undefined = + filteredBucketsA && filteredBucketsB && height + ? [ + filteredBucketsA.filter((bucket): bucket is Bucket => !!bucket), + filteredBucketsB.filter((bucket): bucket is Bucket => !!bucket), + height, + ] + : undefined; + const currentBucketsHeight = currentBuckets?.[2] || 0; + return currentBuckets && currentBucketsHeight > previousBucketsHeight + ? [currentBuckets, previousBuckets] + : pastBuckets; + }); + }, [filteredBucketsA, filteredBucketsB, height]); + const previousBuckets = pastBuckets?.[1]; + // find how numbers should be displayed const priceDecimalPlaces = useMemo(() => { // use same decimal places as the bucket resolution @@ -308,9 +330,22 @@ export default function OrderBookList({ {[...filteredBucketsB].reverse().map((bucket, index) => { const [minPrice, price, reserves] = bucket || []; const lastBucket = bucketsB?.at(-1); + // match previous state by outer bounds + const previousReserves = previousBuckets?.[1].find((previous) => { + return previous[1] === bucket?.[1]; + })?.[2]; + const reservesDiff = + previousReserves && reserves ? reserves - previousReserves : 0; return minPrice && price && reserves && lastBucket ? ( 0 ? 'increase' : 'decrease'}` + : undefined + : 'reserves-new' + } price={price} reserves={reserves} reserveValueUSD={priceTokenB && reserves * priceTokenB} @@ -387,9 +422,22 @@ export default function OrderBookList({ {filteredBucketsA?.map((bucket, index) => { const [maxPrice, price, reserves] = bucket || []; const lastBucket = bucketsA?.at(-1); + // match previous state by outer bounds + const previousReserves = previousBuckets?.[1].find((previous) => { + return previous[1] === bucket?.[1]; + })?.[2]; + const reservesDiff = + previousReserves && reserves ? reserves - previousReserves : 0; return maxPrice && price && reserves && lastBucket ? ( 0 ? 'increase' : 'decrease'}` + : `reserves=${reservesDiff}` + : 'reserves-new' + } price={price} reserves={reserves} reserveValueUSD={priceTokenA && reserves * priceTokenA} @@ -622,6 +670,7 @@ function EmptyRow() { } function OrderbookListRow({ + className, price: bucketPrice, reserves, reserveValueUSD, @@ -634,6 +683,7 @@ function OrderbookListRow({ onHighlight, onClick, }: { + className?: string; price: number; reserves: number; reserveValueUSD?: number; @@ -665,8 +715,11 @@ function OrderbookListRow({ return ( - + + + )}
+ +
{formatAmount(reserves ?? '...', { minimumFractionDigits: amountDecimalPlaces, maximumFractionDigits: amountDecimalPlaces, diff --git a/src/pages/Orderbook/useBuckets.ts b/src/pages/Orderbook/useBuckets.ts index 9fe7901b5..df20d3da5 100644 --- a/src/pages/Orderbook/useBuckets.ts +++ b/src/pages/Orderbook/useBuckets.ts @@ -20,7 +20,7 @@ export type Bucket = [ outerBound: number, displayReserves: number ]; -export type Buckets = [bucketsA: Bucket[], bucketsB: Bucket[]]; +export type Buckets = [bucketsA: Bucket[], bucketsB: Bucket[], height: number]; export default function useBucketsByPriceResolution( tokenA?: Token, @@ -29,13 +29,15 @@ export default function useBucketsByPriceResolution( ): Buckets | undefined { const [tokenIdA, tokenIdB] = [getTokenId(tokenA), getTokenId(tokenB)]; const [, currentPrice] = useRealtimeDisplayPrice(tokenA, tokenB); - const { data: [tokenAReserves, tokenBReserves] = [] } = + const { data: [tokenAReserves, tokenBReserves] = [], meta: { height } = {} } = useTokenPairMapLiquidity([tokenIdA, tokenIdB]); // return bucketReserves return useMemo((): Buckets | undefined => { // return no buckets if the current price is not yet defined - if (!tokenA || !tokenB || !currentPrice || !bucketResolution) return; + if (!tokenA || !tokenB || !currentPrice || !bucketResolution || !height) { + return undefined; + } // continue const resolutionMagnitude = getOrderOfMagnitude(bucketResolution); const zero = new BigNumber(0); @@ -137,13 +139,15 @@ export default function useBucketsByPriceResolution( outerBound, Number(getDisplayDenomAmount(tokenB, reserves)), ]), + height, ]; }, [ - bucketResolution, + tokenA, + tokenB, currentPrice, + bucketResolution, tokenAReserves, tokenBReserves, - tokenA, - tokenB, + height, ]); } From b9bc50f2fe152365926f3c574428db9bde0022df Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Sat, 25 May 2024 01:59:07 +1000 Subject: [PATCH 157/192] feat: add price-slippage indication to Orderbook chart --- src/components/cards/LimitOrderCard.tsx | 38 +++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/components/cards/LimitOrderCard.tsx b/src/components/cards/LimitOrderCard.tsx index c4bd5e201..b4a1e3181 100644 --- a/src/components/cards/LimitOrderCard.tsx +++ b/src/components/cards/LimitOrderCard.tsx @@ -63,10 +63,12 @@ import { } from '../../lib/web3/utils/limitOrders'; import { DexTickUpdateEvent, + DexTickUpdateReservesEvent, mapEventAttributes, } from '../../lib/web3/utils/events'; import { displayPriceToTickIndex, + tickIndexToDisplayPrice, tickIndexToPrice, } from '../../lib/web3/utils/ticks'; import { guessInvertedOrder } from '../../lib/web3/utils/pairs'; @@ -743,10 +745,46 @@ function LimitOrder({ const price = new BigNumber(simulationResult.response.coin_in.amount).div( simulationResult.response.taker_coin_out.amount ); + // track last price state setLastKnownPrice(price.toNumber()); } }, [simulationResult?.response]); + // update slippage indication on chart + useEffect(() => { + if (simulationResult?.result) { + // send slippage indication to rest of Orderbook + if (priceTab === 'Swap' && currentPriceAtoB) { + const events = simulationResult.result?.events + .map(mapEventAttributes) + .filter((event) => event.attributes.TickIndex); + const endTickIndex = events?.at(-1)?.attributes.TickIndex; + const endPrice = + endTickIndex && + tickIndexToDisplayPrice( + new BigNumber(endTickIndex), + tokenA, + tokenB + )?.toNumber(); + setPriceIndication?.( + endPrice + ? buyMode + ? Math.max(currentPriceAtoB, endPrice) + : Math.min(currentPriceAtoB, endPrice) + : undefined + ); + } + } + }, [ + buyMode, + currentPriceAtoB, + priceTab, + setPriceIndication, + simulationResult?.result, + tokenA, + tokenB, + ]); + // find coinIn and coinOut from the simulation results const tranchedReserves = (simulatedMsgPlaceLimitOrder && From 2e1d969d7c90712aaf2d4b974c2cde1ef17b5f38 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Sat, 25 May 2024 18:05:37 +1000 Subject: [PATCH 158/192] fix: opacity admjustments should be applied just before drawing: - the connectionAreas was reused for the chartArea calculations --- src/pages/Orderbook/Orderbook.tsx | 10 ++-------- src/pages/Orderbook/OrderbookChartConnector.tsx | 7 ++++++- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/pages/Orderbook/Orderbook.tsx b/src/pages/Orderbook/Orderbook.tsx index 54b039e4e..584b9ac8a 100644 --- a/src/pages/Orderbook/Orderbook.tsx +++ b/src/pages/Orderbook/Orderbook.tsx @@ -34,8 +34,6 @@ export default function OrderbookPage() { export type PriceOffset = [price: number, offset?: number]; export type BucketOffset = [inner: PriceOffset, outer: PriceOffset]; -const connectionMinOpacity = 0.1; -const connectionMaxOpacity = 0.3; function Orderbook() { // change tokens to match pathname @@ -146,9 +144,7 @@ function Orderbook() { // outer depth price offset bucketOffset?.[1][1] || 0, ], - (reserves / maxSideReserves) * - (connectionMaxOpacity - connectionMinOpacity) + - connectionMinOpacity, + reserves / maxSideReserves, '#26801bFF', ]; } @@ -177,9 +173,7 @@ function Orderbook() { // outer depth price offset bucketOffset?.[1][1] || 0, ], - ((reserves * currentPrice) / maxSideReserves) * - (connectionMaxOpacity - connectionMinOpacity) + - connectionMinOpacity, + (reserves * currentPrice) / maxSideReserves, '#d6363aff', ]; } diff --git a/src/pages/Orderbook/OrderbookChartConnector.tsx b/src/pages/Orderbook/OrderbookChartConnector.tsx index 594c5046a..3da0b3c69 100644 --- a/src/pages/Orderbook/OrderbookChartConnector.tsx +++ b/src/pages/Orderbook/OrderbookChartConnector.tsx @@ -16,6 +16,8 @@ export type ChartArea = [ weight: number, fillColor?: string | CanvasGradient | CanvasPattern ]; +const connectionMinOpacity = 0.1; +const connectionMaxOpacity = 0.3; export default function OrderbookChartConnector({ connectionLines, @@ -109,11 +111,14 @@ export default function OrderbookChartConnector({ connectionArea: ConnectionArea ) { const [[y1, y2], [y3, y4], weight, fill = 'white'] = connectionArea; + const adjustedWeight = + weight * (connectionMaxOpacity - connectionMinOpacity) + + connectionMinOpacity; // skip areas with no defined points if (!y1 || !y2 || !y3 || !y4) return; ctx.lineJoin = 'round'; ctx.fillStyle = fill; - ctx.filter = `opacity(${weight})`; + ctx.filter = `opacity(${adjustedWeight})`; ctx.beginPath(); // draw top line ctx.moveTo(connectionCurve[0], y1); From d948121b133ca6d067cfe15155b768b0bd1ec921 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Fri, 31 May 2024 06:07:51 +1000 Subject: [PATCH 159/192] feat: reduce component logic by asking chart to create own empty bars --- src/pages/Orderbook/OrderbookChart.tsx | 73 +++----------------------- 1 file changed, 6 insertions(+), 67 deletions(-) diff --git a/src/pages/Orderbook/OrderbookChart.tsx b/src/pages/Orderbook/OrderbookChart.tsx index 84368ade7..24bacbb9b 100644 --- a/src/pages/Orderbook/OrderbookChart.tsx +++ b/src/pages/Orderbook/OrderbookChart.tsx @@ -27,7 +27,6 @@ import { IndexerPage, IndexerStreamAccumulateSingleDataSet, } from '../../lib/web3/hooks/useIndexer'; -import { days, hours, minutes, seconds } from '../../lib/utils/time'; const { REACT_APP__INDEXER_API = '', PROD } = import.meta.env; @@ -177,17 +176,6 @@ export default function OrderBookChart({ '1D': 'day', // day }; - const resolutionInMsMap: { - [tradingViewResolution: string]: number; - } = { - '1S': 1 * seconds, - '1': 1 * minutes, - '5': 5 * minutes, - '10': 10 * minutes, - '60': 1 * hours, - '1D': 1 * days, - }; - // keep track of data subscription state here type FetchID = string; type SubscriberUUID = string; @@ -269,6 +257,7 @@ export default function OrderBookChart({ supported_resolutions: supportedResolutions, timezone: 'Etc/UTC', type: 'crypto', + has_empty_bars: true, }; setTimeout(() => { onSymbolResolvedCallback(symbolInfo); @@ -337,46 +326,11 @@ export default function OrderBookChart({ if (height > (knownHeights.get(fetchID) ?? 0)) { knownHeights.set(fetchID, height); } - const resolutionInMs = resolutionInMsMap[resolution]; - const bars: Bar[] = Array.from(data).reduceRight( - (acc, data) => { - const bar = getBarFromTimeSeriesRow(data); - // fill in any gaps with the last close price - const lastBar = acc[acc.length - 1]; - const extraBars: Bar[] = []; - if (lastBar && resolutionInMs) { - let lastTimestamp = lastBar.time; - while (lastTimestamp + resolutionInMs < bar.time) { - lastTimestamp += resolutionInMs; - extraBars.push({ - time: lastTimestamp, - open: lastBar.close, - high: lastBar.close, - low: lastBar.close, - close: lastBar.close, - }); - } - } - return [...acc, ...extraBars, bar]; - }, - [] - ); - // fill in any gaps (between pages) to the earliest known bars - const previousBar = firstBars.get(fetchID); - const currentLastBar = bars.at(-1); - if (previousBar && currentLastBar) { - let lastTimestamp = currentLastBar.time; - while (lastTimestamp + resolutionInMs < previousBar.time) { - lastTimestamp += resolutionInMs; - bars.push({ - time: lastTimestamp, - open: currentLastBar.close, - high: currentLastBar.close, - low: currentLastBar.close, - close: currentLastBar.close, - }); - } - } + // note: the data needs to be in chronological order + // and our API delivers results in reverse-chronological order + const bars: Bar[] = Array.from(data) + .reverse() + .map(getBarFromTimeSeriesRow); // record earliest bar info of this fetch const firstBar = bars.at(0); if ( @@ -445,26 +399,11 @@ export default function OrderBookChart({ new IndexerStreamAccumulateSingleDataSet(url, { onUpdate: (dataUpdates) => { let lastBar = lastBars.get(fetchID); - const resolutionInMs = resolutionInMsMap[resolution]; // note: the data needs to be in chronological order // and our API delivers results in reverse-chronological order const chronologicalUpdates = dataUpdates.slice().reverse(); for (const row of chronologicalUpdates) { const bar = getBarFromTimeSeriesRow(row); - // add extra empty bars if there would be a gap in time - if (lastBar && resolutionInMs) { - let lastTimestamp = lastBar.time; - while (lastTimestamp + resolutionInMs < bar.time) { - lastTimestamp += resolutionInMs; - onRealtimeCallback({ - time: lastTimestamp, - open: lastBar.close, - high: lastBar.close, - low: lastBar.close, - close: lastBar.close, - }); - } - } // add only bars that are new or updates to last timestamp if (!lastBar || bar.time >= lastBar.time) { onRealtimeCallback(bar); From 307f79f97eb5f0b0a5c816f6159a47fd0db0c309 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Fri, 31 May 2024 11:13:40 +1000 Subject: [PATCH 160/192] fix: reduce initial chart renderings on component start --- src/pages/Orderbook/OrderbookChart.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/pages/Orderbook/OrderbookChart.tsx b/src/pages/Orderbook/OrderbookChart.tsx index 24bacbb9b..104de77c9 100644 --- a/src/pages/Orderbook/OrderbookChart.tsx +++ b/src/pages/Orderbook/OrderbookChart.tsx @@ -217,6 +217,12 @@ export default function OrderBookChart({ return url; }; + // skip if token pairs list is not yet ready + // this can cause multiple restarts of the charts until it is ready + if (!tokenPairs.length) { + return; + } + // don't create options unless ID requirements are satisfied if (chartRef.current && tokenPairID && tokenIdA && tokenIdB) { const datafeed: IBasicDataFeed = { From a35b0956f0b31b38dd8b596fbcce04bd6118f002 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Sun, 2 Jun 2024 19:12:42 +1000 Subject: [PATCH 161/192] feat: make order line color white to match orderbook depth table --- src/pages/Orderbook/OrderbookChart.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/Orderbook/OrderbookChart.tsx b/src/pages/Orderbook/OrderbookChart.tsx index 104de77c9..74e55021e 100644 --- a/src/pages/Orderbook/OrderbookChart.tsx +++ b/src/pages/Orderbook/OrderbookChart.tsx @@ -475,6 +475,7 @@ export default function OrderBookChart({ const line = chart // this function will throw an exception if chart is not ready yet .createOrderLine() + .setLineColor('white') .setPrice(priceIndication) .setText('') .setQuantity(''); From 0ad999a3a1153e54513a95a348df27d97fcd9c49 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Sun, 2 Jun 2024 19:31:15 +1000 Subject: [PATCH 162/192] feat: refine price connection area opacity --- src/pages/Orderbook/Orderbook.tsx | 3 ++- src/pages/Orderbook/OrderbookChartConnector.tsx | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/pages/Orderbook/Orderbook.tsx b/src/pages/Orderbook/Orderbook.tsx index 584b9ac8a..4d56ddea2 100644 --- a/src/pages/Orderbook/Orderbook.tsx +++ b/src/pages/Orderbook/Orderbook.tsx @@ -282,7 +282,8 @@ function Orderbook() { getChartPricePointOffset(chartPriceAxis, priceIndication), outerBucket[1][1] || 0, ], - 0.035, + // this opacity will be offset by the "minimum" connection opacity + -0.025, ]; } } diff --git a/src/pages/Orderbook/OrderbookChartConnector.tsx b/src/pages/Orderbook/OrderbookChartConnector.tsx index 3da0b3c69..9476c0492 100644 --- a/src/pages/Orderbook/OrderbookChartConnector.tsx +++ b/src/pages/Orderbook/OrderbookChartConnector.tsx @@ -16,8 +16,8 @@ export type ChartArea = [ weight: number, fillColor?: string | CanvasGradient | CanvasPattern ]; -const connectionMinOpacity = 0.1; -const connectionMaxOpacity = 0.3; +const connectionMinOpacity = 0.05; +const connectionMaxOpacity = 0.4; export default function OrderbookChartConnector({ connectionLines, From 7a78f9e4b7a6f92286c55582ae320b43a87f8357 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Sun, 2 Jun 2024 20:09:10 +1000 Subject: [PATCH 163/192] feat: allow priceIndication to be 0 but disallow price to be 0 --- src/pages/Orderbook/Orderbook.tsx | 10 +++++++--- src/pages/Orderbook/OrderbookList.tsx | 28 ++++++++++++++------------- src/pages/Orderbook/useBuckets.ts | 4 ++-- 3 files changed, 24 insertions(+), 18 deletions(-) diff --git a/src/pages/Orderbook/Orderbook.tsx b/src/pages/Orderbook/Orderbook.tsx index 4d56ddea2..369b68af3 100644 --- a/src/pages/Orderbook/Orderbook.tsx +++ b/src/pages/Orderbook/Orderbook.tsx @@ -60,7 +60,11 @@ function Orderbook() { const priceIndication = depthPriceIndication ?? formPriceIndication; const activeBucketOffsets = useMemo(() => { - if (currentPrice && priceIndication && currentPrice !== priceIndication) { + if ( + currentPrice && + priceIndication !== undefined && + currentPrice !== priceIndication + ) { const [bucketOffsetsA = [], bucketOffsetsB = []] = depthBucketOffsets; return priceIndication > currentPrice ? bucketOffsetsB?.filter( @@ -75,7 +79,7 @@ function Orderbook() { const priceConnectionLines = useMemo(() => { if ( chartPriceAxis && - priceIndication && + priceIndication !== undefined && depthBucketOffsets && currentPrice ) { @@ -266,7 +270,7 @@ function Orderbook() { ConnectionArea | undefined >(() => { const [bucketOffsetsA, bucketOffsetsB] = depthBucketOffsets; - if (chartPriceAxis && currentPrice && priceIndication) { + if (chartPriceAxis && currentPrice && priceIndication !== undefined) { const price = priceIndication; const [innerBucket, outerBucket] = price <= currentPrice diff --git a/src/pages/Orderbook/OrderbookList.tsx b/src/pages/Orderbook/OrderbookList.tsx index 2e4c0c1d9..062711940 100644 --- a/src/pages/Orderbook/OrderbookList.tsx +++ b/src/pages/Orderbook/OrderbookList.tsx @@ -362,12 +362,13 @@ export default function OrderBookList({ } onHighlight={onHighlightPrice} onClick={(bucketPrice) => { - setLimitPrice?.( - formatAmount(bucketPrice, { - minimumFractionDigits: priceDecimalPlaces, - maximumFractionDigits: priceDecimalPlaces, - }) - ); + bucketPrice && + setLimitPrice?.( + formatAmount(bucketPrice, { + minimumFractionDigits: priceDecimalPlaces, + maximumFractionDigits: priceDecimalPlaces, + }) + ); }} /> ) : ( @@ -428,7 +429,7 @@ export default function OrderBookList({ })?.[2]; const reservesDiff = previousReserves && reserves ? reserves - previousReserves : 0; - return maxPrice && price && reserves && lastBucket ? ( + return maxPrice && price !== undefined && reserves && lastBucket ? ( { - setLimitPrice?.( - formatAmount(bucketPrice, { - minimumFractionDigits: priceDecimalPlaces, - maximumFractionDigits: priceDecimalPlaces, - }) - ); + bucketPrice && + setLimitPrice?.( + formatAmount(bucketPrice, { + minimumFractionDigits: priceDecimalPlaces, + maximumFractionDigits: priceDecimalPlaces, + }) + ); }} /> ) : ( diff --git a/src/pages/Orderbook/useBuckets.ts b/src/pages/Orderbook/useBuckets.ts index df20d3da5..042c708d9 100644 --- a/src/pages/Orderbook/useBuckets.ts +++ b/src/pages/Orderbook/useBuckets.ts @@ -65,7 +65,7 @@ export default function useBucketsByPriceResolution( // does value belong in the last bucket? if ( lastValue && - outerBound && + outerBound !== undefined && (inverseDirection ? displayPrice.isLessThanOrEqualTo(outerBound) : displayPrice.isGreaterThanOrEqualTo(outerBound)) @@ -93,7 +93,7 @@ export default function useBucketsByPriceResolution( : Number( displayPrice.toPrecision(precision, BigNumber.ROUND_DOWN) ) - : bucketResolution; + : 0; // get inner bound based on outer bound, but limit to current price const innerBound = inverseDirection ? Math.max( From a89b6cbd241fce91ca10ed9a031e3a1ca9549f12 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Sun, 2 Jun 2024 20:41:34 +1000 Subject: [PATCH 164/192] Revert "feat: hide the pools tab (but keep the page available)" This reverts commit 13a088606432fd399a160ca45fa0def02a10135c. --- src/components/Header/routes.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/Header/routes.ts b/src/components/Header/routes.ts index f3991b49f..35a2e0e9c 100644 --- a/src/components/Header/routes.ts +++ b/src/components/Header/routes.ts @@ -2,6 +2,7 @@ const { REACT_APP__DEFAULT_PAIR = '' } = import.meta.env; export const pageLinkMap = { [['/swap', REACT_APP__DEFAULT_PAIR].join('/')]: 'Swap', + '/pools': 'Pools', [['/orderbook', REACT_APP__DEFAULT_PAIR].join('/')]: 'Orderbook', '/portfolio': 'Portfolio', '/bridge': 'Bridge', From 3fe8cc67786351eb800ce59f3dbd368fac96d3da Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Sun, 2 Jun 2024 22:41:37 +1000 Subject: [PATCH 165/192] fix: fix inconsistent calculation leading to max update depth error --- src/pages/Orderbook/OrderbookList.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/pages/Orderbook/OrderbookList.tsx b/src/pages/Orderbook/OrderbookList.tsx index 062711940..ec6c0b29d 100644 --- a/src/pages/Orderbook/OrderbookList.tsx +++ b/src/pages/Orderbook/OrderbookList.tsx @@ -187,10 +187,15 @@ export default function OrderBookList({ sections.spacer.offsetHeight - sections.header.offsetHeight - sections.center.offsetHeight; - const tableRowHeight = - (sections.top.offsetHeight + sections.bottom.offsetHeight) / - (sections.top.childElementCount + sections.bottom.childElementCount); // determine how many rows we could add to the table + const bucketRows = [ + ...sections.top.children, + ...sections.bottom.children, + ]; + const tableRowHeight = bucketRows.length + ? bucketRows.reduce((acc, v) => acc + v.clientHeight, 0) / + bucketRows.length + : 20; const maxRowCount = tableRowHeight ? Math.floor(tableHeightForRows / tableRowHeight / 2) : 1; From 9792f6c7accbc95dcf41e3ccaee985107c606745 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Mon, 3 Jun 2024 12:44:51 +1000 Subject: [PATCH 166/192] fix: all txs were showing in "My Orders" with unconnected wallets --- src/lib/web3/hooks/useUserLimitOrders.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/lib/web3/hooks/useUserLimitOrders.ts b/src/lib/web3/hooks/useUserLimitOrders.ts index 9dae99a56..ae17b6269 100644 --- a/src/lib/web3/hooks/useUserLimitOrders.ts +++ b/src/lib/web3/hooks/useUserLimitOrders.ts @@ -1,3 +1,4 @@ +import Long from 'long'; import { useMemo } from 'react'; import { useInfiniteQuery } from '@tanstack/react-query'; import type { PageRequest } from '@duality-labs/neutronjs/types/codegen/cosmos/base/query/v1beta1/pagination'; @@ -30,12 +31,16 @@ export function useUserLimitOrderTranches() { pageParam: Uint8Array | undefined; }): Promise => { const client = await restClientPromise; - return client.dex.limitOrderTrancheUserAllByAddress({ - address: address || '', - pagination: { - key: pageKey || [], - } as PageRequest, - }); + return address + ? // query chain for user's transactions + client.dex.limitOrderTrancheUserAllByAddress({ + address, + pagination: { + key: pageKey || [], + } as PageRequest, + }) + : // or return no transactions + { limit_orders: [], pagination: { total: Long.ZERO } }; }, initialPageParam: undefined as Uint8Array | undefined, getNextPageParam: (lastPage): Uint8Array | undefined => { From 6a9c9d6280e1c211398f02374f807a1134ffe496 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Mon, 3 Jun 2024 12:50:39 +1000 Subject: [PATCH 167/192] feat: add better empty My Orders table description --- src/pages/Orderbook/OrderbookFooter.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/Orderbook/OrderbookFooter.tsx b/src/pages/Orderbook/OrderbookFooter.tsx index 970968d19..ec429e205 100644 --- a/src/pages/Orderbook/OrderbookFooter.tsx +++ b/src/pages/Orderbook/OrderbookFooter.tsx @@ -180,6 +180,7 @@ function OrderbookFooterTable({ userLimitOrderMap: userLimitOrderMapOfPair, filterToStatus, }} + rowDescription="Orders" /> ); } From b17415fbef2f0be9771177f64bf4b401a5a33a8e Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Mon, 3 Jun 2024 12:52:19 +1000 Subject: [PATCH 168/192] feat: add empty My Orders table description for unconnected state --- src/components/Table/Table.tsx | 10 +++++++--- src/pages/Orderbook/OrderbookFooter.tsx | 1 + 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx index b0544b8ca..b758a1b05 100644 --- a/src/components/Table/Table.tsx +++ b/src/components/Table/Table.tsx @@ -12,6 +12,7 @@ export default function Table< getRowKey, context, rowDescription = 'Data', + messageOnEmptyData, filtered = false, }: { className?: string; @@ -27,6 +28,7 @@ export default function Table< getRowKey?: (row: DataRow) => string | number; context: Context; rowDescription?: string; + messageOnEmptyData?: ReactNode; filtered?: boolean; }) { if (columns.length !== headings.length) { @@ -85,9 +87,11 @@ export default function Table<
{data ? ( - <> - No {filtered ? 'Matching' : ''} {rowDescription} Found - + messageOnEmptyData || ( + <> + No {filtered ? 'Matching' : ''} {rowDescription} Found + + ) ) : ( <>Loading... )} diff --git a/src/pages/Orderbook/OrderbookFooter.tsx b/src/pages/Orderbook/OrderbookFooter.tsx index ec429e205..f7a0defea 100644 --- a/src/pages/Orderbook/OrderbookFooter.tsx +++ b/src/pages/Orderbook/OrderbookFooter.tsx @@ -181,6 +181,7 @@ function OrderbookFooterTable({ filterToStatus, }} rowDescription="Orders" + messageOnEmptyData={!address && 'Connect wallet to see your orders'} /> ); } From a77346cdf17c5002eafd6e2cb70479077b7e7dac Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Mon, 3 Jun 2024 13:09:46 +1000 Subject: [PATCH 169/192] fix: buy/sell percentage must take into account reserve prices --- src/pages/Orderbook/OrderbookList.tsx | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/pages/Orderbook/OrderbookList.tsx b/src/pages/Orderbook/OrderbookList.tsx index ec6c0b29d..10e580244 100644 --- a/src/pages/Orderbook/OrderbookList.tsx +++ b/src/pages/Orderbook/OrderbookList.tsx @@ -510,12 +510,17 @@ export default function OrderBookList({ // add all bucket "display reserves" values together for summary bar const bucketBuyPercent = useMemo(() => { const [bucketsA, bucketsB] = buckets || []; - const reservesA = bucketsA?.reduce((acc, [, , v]) => acc + v, 0); - const reservesB = bucketsB?.reduce((acc, [, , v]) => acc + v, 0); - return reservesA && reservesB - ? reservesA / (reservesA + reservesB) - : undefined; - }, [buckets]); + if (bucketsA && bucketsB && currentPrice) { + const reservesA = bucketsA.reduce((acc, [, , v]) => acc + v, 0); + const reservesB = bucketsB.reduce((acc, [, , v]) => acc + v, 0); + // calculate value in equivalent tokenA (to avoid USD lookups) + const reservesValueA = reservesA; + const reservesValueB = reservesB * currentPrice; + return reservesValueA + reservesValueB + ? reservesValueA / (reservesValueA + reservesValueB) + : undefined; + } + }, [buckets, currentPrice]); return ( <> From b63c6b329e7490b2a96688e1ea403c111665137a Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Mon, 3 Jun 2024 22:59:49 +1000 Subject: [PATCH 170/192] fix: ensure undefined bucketOffsets are handled correctly --- src/pages/Orderbook/Orderbook.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/Orderbook/Orderbook.tsx b/src/pages/Orderbook/Orderbook.tsx index 369b68af3..5e88b3c26 100644 --- a/src/pages/Orderbook/Orderbook.tsx +++ b/src/pages/Orderbook/Orderbook.tsx @@ -269,7 +269,7 @@ function Orderbook() { const priceIndicationBucketConnection = useMemo< ConnectionArea | undefined >(() => { - const [bucketOffsetsA, bucketOffsetsB] = depthBucketOffsets; + const [bucketOffsetsA = [], bucketOffsetsB = []] = depthBucketOffsets; if (chartPriceAxis && currentPrice && priceIndication !== undefined) { const price = priceIndication; const [innerBucket, outerBucket] = From 6feafcf5b233926c12d73a0f890c3d01a9f2d1e7 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Mon, 3 Jun 2024 23:03:48 +1000 Subject: [PATCH 171/192] refactor: make default undefined bucketOffset more visibly consistent --- src/pages/Orderbook/Orderbook.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pages/Orderbook/Orderbook.tsx b/src/pages/Orderbook/Orderbook.tsx index 5e88b3c26..48496fc7e 100644 --- a/src/pages/Orderbook/Orderbook.tsx +++ b/src/pages/Orderbook/Orderbook.tsx @@ -54,7 +54,7 @@ function Orderbook() { const [depthBucketOffsets, setDepthBucketOffsets] = useState< BucketOffset[][] - >([]); + >([[], []]); const [, currentPrice] = useRealtimeDisplayPrice(tokenA, tokenB); @@ -115,7 +115,7 @@ function Orderbook() { const depthBucketConnections = useMemo(() => { if (currentPrice && chartPriceAxis && buckets && depthBucketOffsets) { const [bucketsA = [], bucketsB = []] = buckets; - const [bucketOffsetsA, bucketOffsetsB] = depthBucketOffsets; + const [bucketOffsetsA = [], bucketOffsetsB = []] = depthBucketOffsets; // find the maximum reserve equivalent value for connection opacity const reservesA = bucketsA.map((bucket) => bucket[2]); const reservesB = bucketsB.map((bucket) => bucket[2]); @@ -125,7 +125,7 @@ function Orderbook() { ); // draw connections of chart offsets to depth offsets for each bucket return [ - bucketOffsetsA + bucketOffsetsA.length > 0 ? bucketsA.map( ([innerBound, outerBound, reserves]) => { const bucketOffset = bucketOffsetsA.find( @@ -154,7 +154,7 @@ function Orderbook() { } ) : [], - bucketOffsetsB + bucketOffsetsB.length > 0 ? bucketsB.map( ([innerBound, outerBound, reserves]) => { const bucketOffset = bucketOffsetsB.find( From f46ec16ed811945015e023fec90cbeade7bcd3d6 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Tue, 4 Jun 2024 02:12:15 +1000 Subject: [PATCH 172/192] fix: incorrect end price could sometimes be calculated --- src/components/cards/LimitOrderCard.tsx | 50 ++++++++++++++++++------- 1 file changed, 36 insertions(+), 14 deletions(-) diff --git a/src/components/cards/LimitOrderCard.tsx b/src/components/cards/LimitOrderCard.tsx index b4a1e3181..97afdc2f0 100644 --- a/src/components/cards/LimitOrderCard.tsx +++ b/src/components/cards/LimitOrderCard.tsx @@ -62,8 +62,10 @@ import { immediateOrderTypes, } from '../../lib/web3/utils/limitOrders'; import { + DexPlaceLimitOrderEvent, DexTickUpdateEvent, DexTickUpdateReservesEvent, + DexTickUpdateTrancheEvent, mapEventAttributes, } from '../../lib/web3/utils/events'; import { @@ -755,24 +757,44 @@ function LimitOrder({ if (simulationResult?.result) { // send slippage indication to rest of Orderbook if (priceTab === 'Swap' && currentPriceAtoB) { - const events = simulationResult.result?.events - .map(mapEventAttributes) - .filter((event) => event.attributes.TickIndex); - const endTickIndex = events?.at(-1)?.attributes.TickIndex; - const endPrice = - endTickIndex && + const events = simulationResult.result?.events.map(mapEventAttributes); + // determine is this swap produced a tranche key (it should not) + const resultTrancheKey = events.find( + (event): event is DexPlaceLimitOrderEvent => + (event as DexPlaceLimitOrderEvent).attributes.action === + 'PlaceLimitOrder' + )?.attributes.TrancheKey; + // get price events + const orderPriceEvents = events + // match reserve updates + .filter( + (event): event is DexTickUpdateReservesEvent => + // match reserves event + !!(event as DexTickUpdateReservesEvent).attributes.TickIndex && + // unmatch result tranch event + // note: this should be removable after the chain update to sdk-50 + (!resultTrancheKey || + (event as DexTickUpdateTrancheEvent).attributes.TrancheKey !== + resultTrancheKey) + ); + // determine the resulting price + const orderPriceEventIndexes = orderPriceEvents?.map( + (event) => + Number(event.attributes.TickIndex) * + (event.attributes.TokenIn === tokenA.base ? -1 : 1) + ); + const endPriceTickIndex = buyMode + ? Math.max(...orderPriceEventIndexes) + : Math.min(...orderPriceEventIndexes); + const endDisplayPrice = + Number.isFinite(endPriceTickIndex) && tickIndexToDisplayPrice( - new BigNumber(endTickIndex), + new BigNumber(endPriceTickIndex), tokenA, tokenB )?.toNumber(); - setPriceIndication?.( - endPrice - ? buyMode - ? Math.max(currentPriceAtoB, endPrice) - : Math.min(currentPriceAtoB, endPrice) - : undefined - ); + // set price if available + setPriceIndication?.(endDisplayPrice || undefined); } } }, [ From daf1de68578a12e248abb8d428fca0127e6426c7 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Tue, 4 Jun 2024 02:13:29 +1000 Subject: [PATCH 173/192] refactor: combine two effects for setting price indiciation into one --- src/components/cards/LimitOrderCard.tsx | 44 +++++++++---------------- 1 file changed, 16 insertions(+), 28 deletions(-) diff --git a/src/components/cards/LimitOrderCard.tsx b/src/components/cards/LimitOrderCard.tsx index 97afdc2f0..1c76455b0 100644 --- a/src/components/cards/LimitOrderCard.tsx +++ b/src/components/cards/LimitOrderCard.tsx @@ -329,28 +329,6 @@ function LimitOrder({ } }, [buyMode, currentPriceAtoB, limitOption]); - // update the price indication from the form - useEffect(() => { - if (setPriceIndication) { - if (priceTab === 'Limit') { - if (currentPriceAtoB && limitOption === 0) { - setPriceIndication(currentPriceAtoB); - } else { - setPriceIndication(Number(formState.limitPrice || offsetLimitPrice)); - } - } else { - setPriceIndication(undefined); - } - } - }, [ - currentPriceAtoB, - formState.limitPrice, - limitOption, - offsetLimitPrice, - priceTab, - setPriceIndication, - ]); - const switchLimitOption = useCallback( (limitOption: LimitPriceOptions) => { // set value @@ -752,11 +730,15 @@ function LimitOrder({ } }, [simulationResult?.response]); - // update slippage indication on chart + // update the price indication from the form useEffect(() => { - if (simulationResult?.result) { - // send slippage indication to rest of Orderbook - if (priceTab === 'Swap' && currentPriceAtoB) { + if (setPriceIndication) { + // set limit price + if (priceTab === 'Limit' && limitOption !== 0) { + setPriceIndication(Number(formState.limitPrice || offsetLimitPrice)); + } + // set market price slippage result: based on trade simulation end price + else if (simulationResult?.result) { const events = simulationResult.result?.events.map(mapEventAttributes); // determine is this swap produced a tranche key (it should not) const resultTrancheKey = events.find( @@ -793,13 +775,19 @@ function LimitOrder({ tokenA, tokenB )?.toNumber(); - // set price if available - setPriceIndication?.(endDisplayPrice || undefined); + setPriceIndication(endDisplayPrice || currentPriceAtoB || undefined); + } + // set market price if available: no simulation result + else { + setPriceIndication(currentPriceAtoB || undefined); } } }, [ buyMode, currentPriceAtoB, + formState.limitPrice, + limitOption, + offsetLimitPrice, priceTab, setPriceIndication, simulationResult?.result, From da2067a4bbcf2244914404702c08b8779a124725 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Tue, 4 Jun 2024 02:48:25 +1000 Subject: [PATCH 174/192] fix: limit market order should set limit at current market price --- src/components/cards/LimitOrderCard.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/components/cards/LimitOrderCard.tsx b/src/components/cards/LimitOrderCard.tsx index 1c76455b0..fd818cb7b 100644 --- a/src/components/cards/LimitOrderCard.tsx +++ b/src/components/cards/LimitOrderCard.tsx @@ -400,9 +400,9 @@ function LimitOrder({ const execution = formState.execution; // find limit price from custom limit price or limit price offset - // calculation, and do not pass 0: allow NaN to raise errors + // calculation, and do not pass 0: allow NaN to act as market price const limitPrice = Number( - formState.limitPrice || (limitOption > 0 ? offsetLimitPrice : NaN) + formState.limitPrice || (priceTab === 'Limit' ? offsetLimitPrice : NaN) ); // find amounts in/out for the order @@ -509,7 +509,6 @@ function LimitOrder({ formState.execution, formState.limitPrice, hasExpiry, - limitOption, offsetLimitPrice, priceTab, tokenIn, @@ -738,7 +737,7 @@ function LimitOrder({ setPriceIndication(Number(formState.limitPrice || offsetLimitPrice)); } // set market price slippage result: based on trade simulation end price - else if (simulationResult?.result) { + else if (priceTab === 'Swap' && simulationResult?.result) { const events = simulationResult.result?.events.map(mapEventAttributes); // determine is this swap produced a tranche key (it should not) const resultTrancheKey = events.find( From b8ac61477a9b32a6ac22d5349ce2a63e79c0e728 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Tue, 4 Jun 2024 03:03:25 +1000 Subject: [PATCH 175/192] fix: prevent slippage tolerance result from looking negative --- src/components/cards/LimitOrderCard.tsx | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/components/cards/LimitOrderCard.tsx b/src/components/cards/LimitOrderCard.tsx index fd818cb7b..3d1096654 100644 --- a/src/components/cards/LimitOrderCard.tsx +++ b/src/components/cards/LimitOrderCard.tsx @@ -774,7 +774,19 @@ function LimitOrder({ tokenA, tokenB )?.toNumber(); - setPriceIndication(endDisplayPrice || currentPriceAtoB || undefined); + // prevent showing "incorrect side" price by limiting to current price + // note: this isn't strictly correct but looks better: a race condition + // can form because endDisplayPrice is a chain response result and + // currentPriceAtoB is an indexer response result. + const possibleDisplayPrices: number[] = [ + endDisplayPrice, + currentPriceAtoB, + ].filter((price): price is number => typeof price === 'number'); + setPriceIndication( + (buyMode + ? Math.max(...possibleDisplayPrices) + : Math.min(...possibleDisplayPrices)) || undefined + ); } // set market price if available: no simulation result else { From 408a2b4fc588b3103d84fa47e9c460729141f1b0 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Tue, 4 Jun 2024 03:15:11 +1000 Subject: [PATCH 176/192] fix: remove connection lines/areas when chart is being removed --- src/pages/Orderbook/OrderbookChart.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/pages/Orderbook/OrderbookChart.tsx b/src/pages/Orderbook/OrderbookChart.tsx index 74e55021e..220d2482b 100644 --- a/src/pages/Orderbook/OrderbookChart.tsx +++ b/src/pages/Orderbook/OrderbookChart.tsx @@ -519,7 +519,13 @@ export default function OrderBookChart({ timeout = setTimeout(checkChartAxis, isFocused ? 30 : 100); }; let timeout = setTimeout(checkChartAxis, 0); - return () => clearTimeout(timeout); + return () => { + clearTimeout(timeout); + // remove chart axis when chart is removed + if (!isChartReady(chart)) { + setPriceAxis?.(undefined); + } + }; } }, [chart, isFocused, setPriceAxis]); From 076a82b339a88f8ee36d4f3919aa19f596da859f Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Tue, 4 Jun 2024 03:21:26 +1000 Subject: [PATCH 177/192] feat: add empty data states for recent orders list --- src/pages/Orderbook/OrderbookTradesList.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/pages/Orderbook/OrderbookTradesList.tsx b/src/pages/Orderbook/OrderbookTradesList.tsx index f83e8cdfd..723faa679 100644 --- a/src/pages/Orderbook/OrderbookTradesList.tsx +++ b/src/pages/Orderbook/OrderbookTradesList.tsx @@ -28,7 +28,7 @@ export default function OrderBookTradesList({ tokenA: Token; tokenB: Token; }) { - const { data } = useTransactionTableData({ + const { data, isLoading } = useTransactionTableData({ tokenA, tokenB, action: 'PlaceLimitOrder', @@ -109,6 +109,11 @@ export default function OrderBookTradesList({ return null; } })} + {(!tradeList || !tradeList.length) && ( +
{isLoading ? 'Loading...' : 'No Data'}
From 4c37a03a827832b00b4e91e3974f4e081c782994 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Tue, 4 Jun 2024 04:01:06 +1000 Subject: [PATCH 178/192] feat: make trades list refresh occasionally --- src/pages/Pool/hooks/useTransactionTableData.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/Pool/hooks/useTransactionTableData.ts b/src/pages/Pool/hooks/useTransactionTableData.ts index ca3419628..564e2186d 100644 --- a/src/pages/Pool/hooks/useTransactionTableData.ts +++ b/src/pages/Pool/hooks/useTransactionTableData.ts @@ -76,6 +76,7 @@ export default function useTransactionTableData({ pageOffset, ], enabled: !!(tokenIdA && tokenIdB), + refetchInterval: 10000, queryFn: async (): Promise => { const invertedOrder = guessInvertedOrder([tokenIdA, tokenIdB]); From d9f99edcfaf184b9a4a9b48386c069f9786743d4 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Tue, 4 Jun 2024 04:37:27 +1000 Subject: [PATCH 179/192] feat: abstract useOnDexMsg to add general subscriptions to dex actions in tables --- .../{useOnUserDexMsg.ts => useOnDexMsg.ts} | 36 ++++++++++++------- src/lib/web3/hooks/useUserLimitOrders.ts | 2 +- src/pages/Orderbook/OrderbookFooter.tsx | 2 +- src/pages/Orderbook/OrderbookTradesList.tsx | 15 +++++++- src/pages/Pool/PoolOverview.tsx | 13 +++++++ .../Pool/hooks/useTransactionTableData.ts | 1 - 6 files changed, 52 insertions(+), 17 deletions(-) rename src/lib/web3/hooks/{useOnUserDexMsg.ts => useOnDexMsg.ts} (56%) diff --git a/src/lib/web3/hooks/useOnUserDexMsg.ts b/src/lib/web3/hooks/useOnDexMsg.ts similarity index 56% rename from src/lib/web3/hooks/useOnUserDexMsg.ts rename to src/lib/web3/hooks/useOnDexMsg.ts index 38234269f..333f236eb 100644 --- a/src/lib/web3/hooks/useOnUserDexMsg.ts +++ b/src/lib/web3/hooks/useOnDexMsg.ts @@ -5,12 +5,11 @@ import { MessageActionEvent, TendermintTxData } from '../events'; import { useDeepCompareMemoize } from 'use-deep-compare-effect'; import subscriber from '../subscriptionManager'; -export default function useOnUserDexMsg( +export default function useOnDexMsg( actionOrActions: T['attributes']['action'] | Array, - callback: (tx: TendermintTxData, dexEvent: T) => void + callback: (tx: TendermintTxData, dexEvent: T) => void, + filterMessages?: Partial | null | undefined ) { - const { address } = useWeb3(); - const callbackRef = useRef(callback); callbackRef.current = callback; @@ -18,13 +17,13 @@ export default function useOnUserDexMsg( Array.isArray(actionOrActions) ? actionOrActions : [actionOrActions] ); + const memoizedfilterMessages = useDeepCompareMemoize(filterMessages); + // subscribe to updates to the user's limit orders useEffect(() => { - if (address) { - const onUserDexMsg = ( - _event: MessageActionEvent, - tx: TendermintTxData - ) => { + // allow memoizedfilterMessages === null to cancel effect + if (memoizedfilterMessages !== null) { + const onDexMsg = (_event: MessageActionEvent, tx: TendermintTxData) => { const events = tx.value.TxResult.result.events.map(mapEventAttributes); const dexEvent = events.find( (event): event is T => @@ -38,12 +37,23 @@ export default function useOnUserDexMsg( } }; // subscribe to all the user's dex messages - subscriber.subscribeMessage(onUserDexMsg, { - message: { module: 'dex', Creator: address }, + subscriber.subscribeMessage(onDexMsg, { + message: { module: 'dex', ...memoizedfilterMessages }, }); return () => { - subscriber.unsubscribeMessage(onUserDexMsg); + subscriber.unsubscribeMessage(onDexMsg); }; } - }, [callback, address, actions]); + }, [callback, memoizedfilterMessages, actions]); +} + +export function useOnUserDexMsg( + actionOrActions: T['attributes']['action'] | Array, + callback: (tx: TendermintTxData, dexEvent: T) => void +) { + const { address } = useWeb3(); + const filterToUserMsgs: Partial | null = address + ? { Creator: address } + : null; + return useOnDexMsg(actionOrActions, callback, filterToUserMsgs); } diff --git a/src/lib/web3/hooks/useUserLimitOrders.ts b/src/lib/web3/hooks/useUserLimitOrders.ts index ae17b6269..6ad06fe47 100644 --- a/src/lib/web3/hooks/useUserLimitOrders.ts +++ b/src/lib/web3/hooks/useUserLimitOrders.ts @@ -10,7 +10,7 @@ import { useDexRestClientPromise } from '../clients/restClients'; import { useWeb3 } from '../useWeb3'; import { useFetchAllPaginatedPages } from './useQueries'; import { useSwrResponseFromReactQuery } from './useSWR'; -import useOnUserDexMsg from './useOnUserDexMsg'; +import { useOnUserDexMsg } from './useOnDexMsg'; const LimitOrderActions: Array = [ 'PlaceLimitOrder', diff --git a/src/pages/Orderbook/OrderbookFooter.tsx b/src/pages/Orderbook/OrderbookFooter.tsx index f7a0defea..1b85c1ae9 100644 --- a/src/pages/Orderbook/OrderbookFooter.tsx +++ b/src/pages/Orderbook/OrderbookFooter.tsx @@ -43,7 +43,7 @@ import { } from '../../lib/web3/hooks/useTranches'; import { useWithdrawFilledLimitOrder } from './useWithdrawFilledLimitOrder'; import { useCancelLimitOrder } from './useCancelLimitOrder'; -import useOnUserDexMsg from '../../lib/web3/hooks/useOnUserDexMsg'; +import { useOnUserDexMsg } from '../../lib/web3/hooks/useOnDexMsg'; import { getTokenPairID } from '../../lib/web3/utils/pairs'; import { tickIndexToPrice } from '../../lib/web3/utils/ticks'; diff --git a/src/pages/Orderbook/OrderbookTradesList.tsx b/src/pages/Orderbook/OrderbookTradesList.tsx index 723faa679..292b0f791 100644 --- a/src/pages/Orderbook/OrderbookTradesList.tsx +++ b/src/pages/Orderbook/OrderbookTradesList.tsx @@ -10,10 +10,13 @@ import { useSimplePrice } from '../../lib/tokenPrices'; import { Token, getTokenId, getTokenValue } from '../../lib/web3/utils/tokens'; import { + DexPlaceLimitOrderEvent, getLastPrice, getSpentTokenAmount, mapEventAttributes, } from '../../lib/web3/utils/events'; +import useOnDexMsg from '../../lib/web3/hooks/useOnDexMsg'; +import { useOrderedTokenPair } from '../../lib/web3/hooks/useTokenPairs'; import './OrderbookTradesList.scss'; @@ -28,13 +31,23 @@ export default function OrderBookTradesList({ tokenA: Token; tokenB: Token; }) { - const { data, isLoading } = useTransactionTableData({ + const { data, isLoading, refetch } = useTransactionTableData({ tokenA, tokenB, action: 'PlaceLimitOrder', pageSize, }); + // subscribe to updates of the pair's limit orders + const denoms = useOrderedTokenPair([getTokenId(tokenA), getTokenId(tokenB)]); + useOnDexMsg( + 'PlaceLimitOrder', + () => { + refetch({ cancelRefetch: false }); + }, + denoms ? { TokenZero: denoms[0], TokenOne: denoms[1] } : null + ); + const [tradeList, setTradeList] = useState>([]); useEffect(() => { if (data) { diff --git a/src/pages/Pool/PoolOverview.tsx b/src/pages/Pool/PoolOverview.tsx index c7b2ef4ee..87e8ef2ab 100644 --- a/src/pages/Pool/PoolOverview.tsx +++ b/src/pages/Pool/PoolOverview.tsx @@ -36,6 +36,9 @@ import { import useTransactionTableData, { Tx } from './hooks/useTransactionTableData'; import { useUserHasDeposits } from '../../lib/web3/hooks/useUserDeposits'; import { useSimplePrice } from '../../lib/tokenPrices'; +import { useOrderedTokenPair } from '../../lib/web3/hooks/useTokenPairs'; +import useOnDexMsg from '../../lib/web3/hooks/useOnDexMsg'; + import { formatAmount, formatCurrency } from '../../lib/utils/number'; import { formatRelativeTime } from '../../lib/utils/time'; @@ -281,6 +284,16 @@ function TransactionsTable({ }) { const query = useTransactionTableData({ tokenA, tokenB, action }); + // subscribe to updates of the pair's limit orders + const denoms = useOrderedTokenPair([getTokenId(tokenA), getTokenId(tokenB)]); + useOnDexMsg( + 'PlaceLimitOrder', + () => { + query.refetch({ cancelRefetch: false }); + }, + denoms ? { TokenZero: denoms[0], TokenOne: denoms[1] } : null + ); + const columns = useMemo(() => { return transactionTableHeadings.map( (heading: TransactionTableColumnKey) => { diff --git a/src/pages/Pool/hooks/useTransactionTableData.ts b/src/pages/Pool/hooks/useTransactionTableData.ts index 564e2186d..ca3419628 100644 --- a/src/pages/Pool/hooks/useTransactionTableData.ts +++ b/src/pages/Pool/hooks/useTransactionTableData.ts @@ -76,7 +76,6 @@ export default function useTransactionTableData({ pageOffset, ], enabled: !!(tokenIdA && tokenIdB), - refetchInterval: 10000, queryFn: async (): Promise => { const invertedOrder = guessInvertedOrder([tokenIdA, tokenIdB]); From 46aae64dec5cbedb605bf3316cd36654d57da1ef Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Tue, 4 Jun 2024 05:26:44 +1000 Subject: [PATCH 180/192] feat: add click chart to set limit price --- src/pages/Orderbook/Orderbook.tsx | 1 + src/pages/Orderbook/OrderbookChart.tsx | 28 ++++++++++++++++++++++++-- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/pages/Orderbook/Orderbook.tsx b/src/pages/Orderbook/Orderbook.tsx index 48496fc7e..9a3215af4 100644 --- a/src/pages/Orderbook/Orderbook.tsx +++ b/src/pages/Orderbook/Orderbook.tsx @@ -340,6 +340,7 @@ function Orderbook() { ? priceIndication : undefined } + setLimitPrice={setLimitPrice} setPriceAxis={setChartPriceAxis} /> )} diff --git a/src/pages/Orderbook/OrderbookChart.tsx b/src/pages/Orderbook/OrderbookChart.tsx index 220d2482b..55341e32e 100644 --- a/src/pages/Orderbook/OrderbookChart.tsx +++ b/src/pages/Orderbook/OrderbookChart.tsx @@ -15,6 +15,7 @@ import { VisiblePriceRange, PriceScaleMode, IChartWidgetApi, + CrossHairMovedEventParams, } from 'charting_library'; import { Token, getTokenId } from '../../lib/web3/utils/tokens'; @@ -105,12 +106,14 @@ export default function OrderBookChart({ tokenA, tokenB, priceIndication, + setLimitPrice, setPriceAxis, }: { tokenA: Token; tokenB: Token; priceIndication?: number | undefined; setPriceIndication?: React.Dispatch>; + setLimitPrice?: (limitPrice: string) => void; setPriceAxis?: React.Dispatch< React.SetStateAction >; @@ -453,8 +456,29 @@ export default function OrderBookChart({ if (defaultWidgetOptions.settings_overrides) { tvWidget.applyOverrides(defaultWidgetOptions.settings_overrides); } + + const chart = tvWidget.activeChart(); + + // track chart onClick price + if (setLimitPrice) { + let crosshair: CrossHairMovedEventParams | undefined; + let lastMouseDownAt: number = 0; + chart.crossHairMoved().subscribe(null, (e) => (crosshair = e)); + tvWidget.subscribe( + 'mouse_down', + () => (lastMouseDownAt = Date.now()) + ); + tvWidget.subscribe('mouse_up', () => { + if (Date.now() - lastMouseDownAt < 100) { + if (crosshair?.price) { + setLimitPrice?.(new BigNumber(crosshair.price).toPrecision(6)); + } + } + }); + } + // store current widget - setChart(tvWidget.activeChart()); + setChart(chart); }); // return method to cleanup widget and data subscribers @@ -468,7 +492,7 @@ export default function OrderBookChart({ }); }; } - }, [navigate, tokenIdA, tokenIdB, tokenPairID, tokenPairs]); + }, [navigate, setLimitPrice, tokenIdA, tokenIdB, tokenPairID, tokenPairs]); useEffect(() => { if (priceIndication && chart && isChartReady(chart)) { From a9876a5ae9548fa6e77dd1a244835d07c2a031f8 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Tue, 4 Jun 2024 05:49:47 +1000 Subject: [PATCH 181/192] fix: avoid chart re-rendering with cached chart pairs --- src/pages/Orderbook/OrderbookChart.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pages/Orderbook/OrderbookChart.tsx b/src/pages/Orderbook/OrderbookChart.tsx index 55341e32e..450120646 100644 --- a/src/pages/Orderbook/OrderbookChart.tsx +++ b/src/pages/Orderbook/OrderbookChart.tsx @@ -139,6 +139,8 @@ export default function OrderBookChart({ useMemo>(() => { return tokenPairReserves && tokenByDenom ? tokenPairReserves + // sort consistently to avoid re-rendering + .sort(([a], [b]) => a.localeCompare(b)) // find the tokens that match our known pair token IDs .map(([denom0, denom1]) => { return [tokenByDenom.get(denom0), tokenByDenom.get(denom1)]; From b612bc8ade82ae145ede4590e9f01ab3a2079082 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Tue, 4 Jun 2024 05:58:57 +1000 Subject: [PATCH 182/192] feat: remove tokenPairs dependency from Orderbook chart --- src/pages/Orderbook/OrderbookChart.tsx | 72 ++------------------------ 1 file changed, 3 insertions(+), 69 deletions(-) diff --git a/src/pages/Orderbook/OrderbookChart.tsx b/src/pages/Orderbook/OrderbookChart.tsx index 450120646..e6cfd1b63 100644 --- a/src/pages/Orderbook/OrderbookChart.tsx +++ b/src/pages/Orderbook/OrderbookChart.tsx @@ -1,7 +1,6 @@ import BigNumber from 'bignumber.js'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import { useDeepCompareMemoize } from 'use-deep-compare-effect'; import { widget, IBasicDataFeed, @@ -9,7 +8,6 @@ import { DatafeedConfiguration, ResolutionString, LibrarySymbolInfo, - SearchSymbolResultItem, Bar, Timezone, VisiblePriceRange, @@ -20,8 +18,6 @@ import { import { Token, getTokenId } from '../../lib/web3/utils/tokens'; -import { useTokenByDenom } from '../../lib/web3/hooks/useDenomClients'; -import useTokenPairs from '../../lib/web3/hooks/useTokenPairs'; import { tickIndexToPrice } from '../../lib/web3/utils/ticks'; import { TimeSeriesRow } from '../../components/stats/utils'; import { @@ -129,30 +125,6 @@ export default function OrderBookChart({ // store chart widget when ready const [chart, setChart] = useState(); - const { data: tokenPairReserves } = useTokenPairs(); - const { data: tokenByDenom } = useTokenByDenom( - tokenPairReserves?.flatMap(([denom0, denom1]) => [denom0, denom1]) - ); - - // memoize tokenPairs so we don't trigger the graph re-rendering too often - const tokenPairs = useDeepCompareMemoize( - useMemo>(() => { - return tokenPairReserves && tokenByDenom - ? tokenPairReserves - // sort consistently to avoid re-rendering - .sort(([a], [b]) => a.localeCompare(b)) - // find the tokens that match our known pair token IDs - .map(([denom0, denom1]) => { - return [tokenByDenom.get(denom0), tokenByDenom.get(denom1)]; - }) - // remove pairs with unfound tokens - .filter<[Token, Token]>((tokenPair): tokenPair is [Token, Token] => - tokenPair.every(Boolean) - ) - : []; - }, [tokenByDenom, tokenPairReserves]) - ); - // tokenPairID is made of symbols, which is different to token paths const tokenPairID = useMemo(() => { if (tokenA && tokenB) { @@ -222,12 +194,6 @@ export default function OrderBookChart({ return url; }; - // skip if token pairs list is not yet ready - // this can cause multiple restarts of the charts until it is ready - if (!tokenPairs.length) { - return; - } - // don't create options unless ID requirements are satisfied if (chartRef.current && tokenPairID && tokenIdA && tokenIdB) { const datafeed: IBasicDataFeed = { @@ -275,44 +241,12 @@ export default function OrderBookChart({ }, 0); }, searchSymbols: ( - userInput = '', + _userInput, _exchange, // will always be "Neutron" _symbolType, // will always be "crypto" onResultReadyCallback ) => { - const tokens = Array.from(new Set(tokenPairs.flatMap((v) => v))); - const inputs = userInput.toLowerCase().split('/'); - const filteredTokens = tokens.filter((token) => { - // return a match if any of the inputs given match a token - return inputs.some((input) => { - return ( - token.chain.chain_name.toLowerCase().includes(input) || - token.name.toLowerCase().includes(input) || - token.base.toLowerCase().includes(input) || - token.display.toLowerCase().includes(input) || - token.symbol.toLowerCase().includes(input) || - token.keywords?.map((v) => v.toLowerCase()).includes(input) - ); - }); - }); - - const items: SearchSymbolResultItem[] = tokenPairs - .filter(([tokenA, tokenB]) => { - return ( - filteredTokens.includes(tokenA) || - filteredTokens.includes(tokenB) - ); - }) - .map(([tokenA, tokenB]) => { - return { - exchange: 'Neutron Dex', - symbol: `${tokenA.symbol}/${tokenB.symbol}`, - full_name: `${tokenA.symbol}/${tokenB.symbol}`, - description: `Neutron Dex pair of ${tokenA.name} and ${tokenB.name}`, - type: 'crypto', - }; - }); - onResultReadyCallback(items); + onResultReadyCallback([]); }, getBars: async ( symbolInfo, @@ -494,7 +428,7 @@ export default function OrderBookChart({ }); }; } - }, [navigate, setLimitPrice, tokenIdA, tokenIdB, tokenPairID, tokenPairs]); + }, [navigate, setLimitPrice, tokenIdA, tokenIdB, tokenPairID]); useEffect(() => { if (priceIndication && chart && isChartReady(chart)) { From 20d81985088fc3e3fc5d264ad52bc53e1bfdc5d4 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Tue, 4 Jun 2024 21:00:42 +1000 Subject: [PATCH 183/192] feat: flip or reset order trade state when changing tokens --- src/components/cards/LimitOrderCard.tsx | 39 +++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/components/cards/LimitOrderCard.tsx b/src/components/cards/LimitOrderCard.tsx index 3d1096654..e26678903 100644 --- a/src/components/cards/LimitOrderCard.tsx +++ b/src/components/cards/LimitOrderCard.tsx @@ -201,6 +201,45 @@ function LimitOrder({ const formState = useContext(LimitOrderFormContext); const formSetState = useContext(LimitOrderFormSetContext); + // keep card trade state after switching token order + // or reset state if changing pairs + const previousPageStateRef = useRef([tokenA, tokenB]); + useEffect(() => { + const [previousTokenA, previousTokenB] = previousPageStateRef.current; + // change the card state if the tokens have changed + if (previousTokenA && previousTokenB && tokenA && tokenB) { + // flip trade state if token pair is exactly flipped + if ( + getTokenId(previousTokenA) === getTokenId(tokenB) && + getTokenId(previousTokenB) === getTokenId(tokenA) + ) { + // flip limit price + formSetState.setLimitPrice?.((limitPrice) => { + return Number(limitPrice) > 0 + ? new BigNumber(1).div(limitPrice).precision(6).toFixed() + : limitPrice; + }); + // flip buy/sell + setModeTab((modeTab) => (modeTab === 'Buy' ? 'Sell' : 'Buy')); + } + // partially "reset" form state for a different pair + else if ( + getTokenId(previousTokenA) !== getTokenId(tokenA) || + getTokenId(previousTokenB) !== getTokenId(tokenB) + ) { + // remove limit price + formSetState.setLimitPrice?.(''); + // set market price + setLimitOption(0); + // zero the amounts + formSetState.setAmountInOut?.(['', '']); + // everything else can stay: expiry, Swap/Limit, Buy/Sell + } + } + // update values in state + previousPageStateRef.current = [tokenA, tokenB]; + }, [formSetState, tokenA, tokenB]); + const [expiration, setExpiration] = useState('none'); // allow an expiration change to change the custom time amount and period const switchExpiration = useCallback( From b5e7868d90e28f7f549432be5bca20aac378eb68 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Tue, 4 Jun 2024 21:11:54 +1000 Subject: [PATCH 184/192] fix: change all usages of BigNumber .toPrecision method to .precision - .toPrecision can return scientitific notation strings which will not pass validation on some numeric input fields - not all need to be changed but its best to change all anyway --- src/lib/utils/number.ts | 2 +- src/pages/Orderbook/OrderbookChart.tsx | 4 +++- src/pages/Orderbook/useBuckets.ts | 14 ++++++++------ 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/lib/utils/number.ts b/src/lib/utils/number.ts index 9861f1607..05756fba4 100644 --- a/src/lib/utils/number.ts +++ b/src/lib/utils/number.ts @@ -47,7 +47,7 @@ export function formatMaximumSignificantDecimals( ) { const bigValue = new BigNumber(value); const roundingFunction = BigNumber.ROUND_HALF_UP; - const roundedValue = bigValue.abs().toPrecision(6, roundingFunction); + const roundedValue = bigValue.abs().precision(6, roundingFunction); const roundedOrderOfMagnitude = Math.floor( Math.log10(Number(roundedValue) || 1) ); diff --git a/src/pages/Orderbook/OrderbookChart.tsx b/src/pages/Orderbook/OrderbookChart.tsx index e6cfd1b63..929b409c8 100644 --- a/src/pages/Orderbook/OrderbookChart.tsx +++ b/src/pages/Orderbook/OrderbookChart.tsx @@ -407,7 +407,9 @@ export default function OrderBookChart({ tvWidget.subscribe('mouse_up', () => { if (Date.now() - lastMouseDownAt < 100) { if (crosshair?.price) { - setLimitPrice?.(new BigNumber(crosshair.price).toPrecision(6)); + setLimitPrice?.( + new BigNumber(crosshair.price).precision(6).toFixed() + ); } } }); diff --git a/src/pages/Orderbook/useBuckets.ts b/src/pages/Orderbook/useBuckets.ts index 042c708d9..daa2f1f4c 100644 --- a/src/pages/Orderbook/useBuckets.ts +++ b/src/pages/Orderbook/useBuckets.ts @@ -87,20 +87,22 @@ export default function useBucketsByPriceResolution( const outerBound = precision > 0 ? inverseDirection - ? Number( - displayPrice.toPrecision(precision, BigNumber.ROUND_UP) - ) - : Number( - displayPrice.toPrecision(precision, BigNumber.ROUND_DOWN) - ) + ? displayPrice + .precision(precision, BigNumber.ROUND_UP) + .toNumber() + : displayPrice + .precision(precision, BigNumber.ROUND_DOWN) + .toNumber() : 0; // get inner bound based on outer bound, but limit to current price const innerBound = inverseDirection ? Math.max( + // use BigNumber to avoid float imprecision (eg: 0.2+0.1) new BigNumber(outerBound).minus(bucketResolution).toNumber(), currentPrice ) : Math.min( + // use BigNumber to avoid float imprecision (eg: 0.2+0.1) new BigNumber(outerBound).plus(bucketResolution).toNumber(), currentPrice ); From b8a8f36c4e1100bb3ff177d3bcc4579128ac6b14 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Wed, 5 Jun 2024 02:34:35 +1000 Subject: [PATCH 185/192] fix: account=null and account=undefined react-queries were colliding --- src/pages/Orderbook/OrderbookFooter.tsx | 2 +- src/pages/Pool/hooks/useTransactionTableData.ts | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/pages/Orderbook/OrderbookFooter.tsx b/src/pages/Orderbook/OrderbookFooter.tsx index 1b85c1ae9..c661e9321 100644 --- a/src/pages/Orderbook/OrderbookFooter.tsx +++ b/src/pages/Orderbook/OrderbookFooter.tsx @@ -100,7 +100,7 @@ function OrderbookFooterTable({ tokenA, tokenB, action: 'PlaceLimitOrder', - account: address, + account: address || null, pageSize, }); diff --git a/src/pages/Pool/hooks/useTransactionTableData.ts b/src/pages/Pool/hooks/useTransactionTableData.ts index ca3419628..b346da573 100644 --- a/src/pages/Pool/hooks/useTransactionTableData.ts +++ b/src/pages/Pool/hooks/useTransactionTableData.ts @@ -71,6 +71,9 @@ export default function useTransactionTableData({ tokenIdA, tokenIdB, action, + // note: it is important to distinguish between null and undefined here + // it seems that react-query counts them are the same query key + typeof account, account, pageSize, pageOffset, @@ -80,6 +83,8 @@ export default function useTransactionTableData({ const invertedOrder = guessInvertedOrder([tokenIdA, tokenIdB]); // disable if account is specifically `null` (eg. wallet is not connected) + // note: this is important so there is a solid answer to the request + // disabling the query for account=null will return a loading state if (account === null) { return { txs: [], From 639d5c38af16aa82243afce50544ef0ade4702dd Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Wed, 5 Jun 2024 02:49:48 +1000 Subject: [PATCH 186/192] fix: make zero-result hook behavior more obvious: allow null parameter - null parameter means to return an empty data set - a non-null parameter means to request a data set - hopefully this is clearer --- src/pages/Orderbook/OrderbookFooter.tsx | 20 ++++++--- .../Pool/hooks/useTransactionTableData.ts | 43 ++++++++++--------- 2 files changed, 35 insertions(+), 28 deletions(-) diff --git a/src/pages/Orderbook/OrderbookFooter.tsx b/src/pages/Orderbook/OrderbookFooter.tsx index c661e9321..df9767e70 100644 --- a/src/pages/Orderbook/OrderbookFooter.tsx +++ b/src/pages/Orderbook/OrderbookFooter.tsx @@ -96,13 +96,19 @@ function OrderbookFooterTable({ filterToStatus?: StatusFilter; }) { const { address } = useWeb3(); - const { data: txs, refetch } = useTransactionTableData({ - tokenA, - tokenB, - action: 'PlaceLimitOrder', - account: address || null, - pageSize, - }); + const { data: txs, refetch } = useTransactionTableData( + address + ? // show user's limit orders when connected + { + tokenA, + tokenB, + action: 'PlaceLimitOrder', + account: address, + pageSize, + } + : // don't show any data when not connected + null + ); // subscribe to updates to the user's limit orders useOnUserDexMsg('PlaceLimitOrder', () => { diff --git a/src/pages/Pool/hooks/useTransactionTableData.ts b/src/pages/Pool/hooks/useTransactionTableData.ts index b346da573..f7d36a917 100644 --- a/src/pages/Pool/hooks/useTransactionTableData.ts +++ b/src/pages/Pool/hooks/useTransactionTableData.ts @@ -46,21 +46,25 @@ export interface GetTxsEventResponseManuallyType { const blockTimestamps: { [height: string]: string } = {}; -export default function useTransactionTableData({ - tokenA, - tokenB, - account, - action, - pageSize = 10, - orderByAscending, -}: { - tokenA: Token; - tokenB: Token; - account?: WalletAddress | null; - action?: DexMessageAction; - pageSize?: number; - orderByAscending?: boolean; -}) { +export default function useTransactionTableData( + // passing opts=null means to abort: don't request, return an empty result + opts: { + tokenA: Token; + tokenB: Token; + account?: WalletAddress | null; + action?: DexMessageAction; + pageSize?: number; + orderByAscending?: boolean; + } | null +) { + const { + tokenA, + tokenB, + account, + action, + pageSize = 10, + orderByAscending, + } = opts || {}; const tokenIdA = getTokenId(tokenA) || ''; const tokenIdB = getTokenId(tokenB) || ''; @@ -71,21 +75,18 @@ export default function useTransactionTableData({ tokenIdA, tokenIdB, action, - // note: it is important to distinguish between null and undefined here - // it seems that react-query counts them are the same query key - typeof account, account, pageSize, pageOffset, ], - enabled: !!(tokenIdA && tokenIdB), + enabled: !opts || !!(tokenIdA && tokenIdB), queryFn: async (): Promise => { const invertedOrder = guessInvertedOrder([tokenIdA, tokenIdB]); - // disable if account is specifically `null` (eg. wallet is not connected) + // disable if opts is specifically not set (eg. wallet is not connected) // note: this is important so there is a solid answer to the request // disabling the query for account=null will return a loading state - if (account === null) { + if (!opts) { return { txs: [], total_count: '0', From 8e4deac7619a8dc4d74763c663194b561005a62d Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Wed, 5 Jun 2024 02:53:47 +1000 Subject: [PATCH 187/192] feat: ensure Expiry shortcuts are open for all limit orders: - not just GOOD_TIL_TIME but also GOOD_TIL_CANCELLED --- src/components/cards/LimitOrderCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/cards/LimitOrderCard.tsx b/src/components/cards/LimitOrderCard.tsx index e26678903..3132f9977 100644 --- a/src/components/cards/LimitOrderCard.tsx +++ b/src/components/cards/LimitOrderCard.tsx @@ -980,7 +980,7 @@ function LimitOrder({ >
- +
Expiry
From 7693a6a04f5d59aab21cee66e82546e913ebccc8 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Wed, 5 Jun 2024 02:59:36 +1000 Subject: [PATCH 188/192] refactor: allow useTransactionTableData page state to be set by parent --- .../Pool/hooks/useTransactionTableData.ts | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/src/pages/Pool/hooks/useTransactionTableData.ts b/src/pages/Pool/hooks/useTransactionTableData.ts index f7d36a917..67d796551 100644 --- a/src/pages/Pool/hooks/useTransactionTableData.ts +++ b/src/pages/Pool/hooks/useTransactionTableData.ts @@ -1,4 +1,3 @@ -import { useState } from 'react'; import { useQuery } from '@tanstack/react-query'; import { Token, getTokenId } from '../../../lib/web3/utils/tokens'; @@ -53,6 +52,7 @@ export default function useTransactionTableData( tokenB: Token; account?: WalletAddress | null; action?: DexMessageAction; + page?: number; pageSize?: number; orderByAscending?: boolean; } | null @@ -62,23 +62,15 @@ export default function useTransactionTableData( tokenB, account, action, + page = 1, pageSize = 10, orderByAscending, } = opts || {}; const tokenIdA = getTokenId(tokenA) || ''; const tokenIdB = getTokenId(tokenB) || ''; - const [pageOffset] = useState(0); return useQuery({ - queryKey: [ - 'events', - tokenIdA, - tokenIdB, - action, - account, - pageSize, - pageOffset, - ], + queryKey: ['events', tokenIdA, tokenIdB, action, account, page, pageSize], enabled: !opts || !!(tokenIdA && tokenIdB), queryFn: async (): Promise => { const invertedOrder = guessInvertedOrder([tokenIdA, tokenIdB]); @@ -112,7 +104,7 @@ export default function useTransactionTableData( * action ? `message.action='${action}'` : '', * ].filter(Boolean), * orderBy: cosmos.tx.v1beta1.OrderBySDKType.ORDER_BY_ASC, - * page: Long.fromString(pageOffset + 1), + * page: Long.fromString(page), * limit: Long.fromString(pageSize), * }); * @@ -137,7 +129,7 @@ export default function useTransactionTableData( .join(' AND ') )}"`, `per_page=${pageSize}`, - `page=${pageOffset + 1}`, + `page=${page}`, `order_by="${orderByAscending ? 'asc' : 'desc'}"`, ].join('&')}` ); From 1d19005afea57a2fe9642f22e21126022acb5036 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Wed, 5 Jun 2024 07:31:00 +1000 Subject: [PATCH 189/192] fix: use consistent end display price of txs calculations --- src/components/cards/LimitOrderCard.tsx | 53 +++----------- src/lib/web3/utils/events.ts | 79 +++++++++++++++------ src/pages/Orderbook/OrderbookTradesList.tsx | 19 ++--- 3 files changed, 80 insertions(+), 71 deletions(-) diff --git a/src/components/cards/LimitOrderCard.tsx b/src/components/cards/LimitOrderCard.tsx index 3132f9977..4d1d6b756 100644 --- a/src/components/cards/LimitOrderCard.tsx +++ b/src/components/cards/LimitOrderCard.tsx @@ -62,15 +62,12 @@ import { immediateOrderTypes, } from '../../lib/web3/utils/limitOrders'; import { - DexPlaceLimitOrderEvent, DexTickUpdateEvent, - DexTickUpdateReservesEvent, - DexTickUpdateTrancheEvent, + getTxEventsEndDisplayPrice, mapEventAttributes, } from '../../lib/web3/utils/events'; import { displayPriceToTickIndex, - tickIndexToDisplayPrice, tickIndexToPrice, } from '../../lib/web3/utils/ticks'; import { guessInvertedOrder } from '../../lib/web3/utils/pairs'; @@ -776,49 +773,21 @@ function LimitOrder({ setPriceIndication(Number(formState.limitPrice || offsetLimitPrice)); } // set market price slippage result: based on trade simulation end price - else if (priceTab === 'Swap' && simulationResult?.result) { - const events = simulationResult.result?.events.map(mapEventAttributes); - // determine is this swap produced a tranche key (it should not) - const resultTrancheKey = events.find( - (event): event is DexPlaceLimitOrderEvent => - (event as DexPlaceLimitOrderEvent).attributes.action === - 'PlaceLimitOrder' - )?.attributes.TrancheKey; - // get price events - const orderPriceEvents = events - // match reserve updates - .filter( - (event): event is DexTickUpdateReservesEvent => - // match reserves event - !!(event as DexTickUpdateReservesEvent).attributes.TickIndex && - // unmatch result tranch event - // note: this should be removable after the chain update to sdk-50 - (!resultTrancheKey || - (event as DexTickUpdateTrancheEvent).attributes.TrancheKey !== - resultTrancheKey) - ); - // determine the resulting price - const orderPriceEventIndexes = orderPriceEvents?.map( - (event) => - Number(event.attributes.TickIndex) * - (event.attributes.TokenIn === tokenA.base ? -1 : 1) - ); - const endPriceTickIndex = buyMode - ? Math.max(...orderPriceEventIndexes) - : Math.min(...orderPriceEventIndexes); - const endDisplayPrice = - Number.isFinite(endPriceTickIndex) && - tickIndexToDisplayPrice( - new BigNumber(endPriceTickIndex), - tokenA, - tokenB - )?.toNumber(); + else if ( + priceTab === 'Swap' && + tokenA && + tokenB && + simulationResult?.result + ) { + // get end price of simulated tx + const events = simulationResult.result.events.map(mapEventAttributes); + const displayPrice = getTxEventsEndDisplayPrice(events, tokenA, tokenB); // prevent showing "incorrect side" price by limiting to current price // note: this isn't strictly correct but looks better: a race condition // can form because endDisplayPrice is a chain response result and // currentPriceAtoB is an indexer response result. const possibleDisplayPrices: number[] = [ - endDisplayPrice, + displayPrice?.toNumber(), currentPriceAtoB, ].filter((price): price is number => typeof price === 'number'); setPriceIndication( diff --git a/src/lib/web3/utils/events.ts b/src/lib/web3/utils/events.ts index 43a8f95e5..be60c7502 100644 --- a/src/lib/web3/utils/events.ts +++ b/src/lib/web3/utils/events.ts @@ -1,7 +1,7 @@ import BigNumber from 'bignumber.js'; import { Event, parseCoins } from '@cosmjs/stargate'; -import { tickIndexToPrice } from './ticks'; +import { tickIndexToDisplayPrice } from './ticks'; import { WalletAddress } from './address'; import { Token, TokenID, getTokenId } from './tokens'; @@ -270,26 +270,65 @@ export interface IBCReceivePacketEvent { attributes: IBCPacketEventAttributes; } -export function getLastPrice( +function getDexActionEvent( + action: T['attributes']['action'], + events: ChainEvent[] +): T | undefined { + // get the original request event + return events.find( + (event): event is T => (event as T).attributes.action === action + ); +} + +function getTxEventsEndTickIndexInToOut(events: ChainEvent[]): number { + // get the original request event + const placeLimitOrderEvent = getDexActionEvent( + 'PlaceLimitOrder', + events + ); + // find the request token in + const tokenIn = placeLimitOrderEvent?.attributes.TokenIn; + // determine is this swap produced a tranche key (it should not) + const resultTrancheKey = placeLimitOrderEvent?.attributes.TrancheKey; + // get price events + const orderPriceEvents = events + // match reserve updates + .filter( + (event): event is DexTickUpdateReservesEvent => + // match reserves event + !!(event as DexTickUpdateReservesEvent).attributes.TickIndex && + // match TickUpdate for swapped denom (get sale prices) + (event as DexTickUpdateReservesEvent).attributes.TokenIn !== tokenIn && + // unmatch result tranch event + // note: this should be removable after the chain update to sdk-50 + (!resultTrancheKey || + (event as DexTickUpdateTrancheEvent).attributes.TrancheKey !== + resultTrancheKey) + ); + // return end price + const lastEvent = orderPriceEvents.at(-1); + return lastEvent ? Number(lastEvent.attributes.TickIndex) : NaN; +} + +export function getTxEventsEndDisplayPrice( events: ChainEvent[], - { tokenA, tokenB }: { tokenA: Token; tokenB: Token } -) { - const lastTickUpdate = events - .slice() - .reverse() - .find((event): event is DexTickUpdateEvent => { - return ( - (event.type === 'message' || event.type === 'TickUpdate') && - event.attributes.action === 'TickUpdate' - ); - }); - const tickIndex = lastTickUpdate - ? new BigNumber(lastTickUpdate.attributes.TickIndex) - : undefined; - const forward = lastTickUpdate?.attributes.TokenZero === getTokenId(tokenA); - const reverse = lastTickUpdate?.attributes.TokenZero === getTokenId(tokenB); - return tickIndex && (forward || reverse) - ? tickIndexToPrice(forward ? tickIndex : tickIndex.negated()) + tokenA: Token, + tokenB: Token +): BigNumber | undefined { + // get tx tokenIn + const tokenIn = getDexActionEvent( + 'PlaceLimitOrder', + events + )?.attributes.TokenIn; + // get tickIndex in AtoB direction + const endTickIndexAtoB = + tokenIn !== undefined && + new BigNumber(getTxEventsEndTickIndexInToOut(events)).multipliedBy( + getTokenId(tokenA) === tokenIn ? 1 : -1 + ); + // return display price if found + return endTickIndexAtoB !== false && endTickIndexAtoB.isFinite() + ? tickIndexToDisplayPrice(endTickIndexAtoB, tokenA, tokenB) : undefined; } diff --git a/src/pages/Orderbook/OrderbookTradesList.tsx b/src/pages/Orderbook/OrderbookTradesList.tsx index 292b0f791..fdbdcf9a6 100644 --- a/src/pages/Orderbook/OrderbookTradesList.tsx +++ b/src/pages/Orderbook/OrderbookTradesList.tsx @@ -1,3 +1,4 @@ +import BigNumber from 'bignumber.js'; import { useEffect, useMemo, useState } from 'react'; import useTransactionTableData, { @@ -11,8 +12,8 @@ import { Token, getTokenId, getTokenValue } from '../../lib/web3/utils/tokens'; import { DexPlaceLimitOrderEvent, - getLastPrice, getSpentTokenAmount, + getTxEventsEndDisplayPrice, mapEventAttributes, } from '../../lib/web3/utils/events'; import useOnDexMsg from '../../lib/web3/hooks/useOnDexMsg'; @@ -160,18 +161,18 @@ function OrderbookTradeListRow({ return previousTx?.tx_result.events.map(mapEventAttributes) || []; }, [previousTx]); - const lastPrice = useMemo(() => { - return getLastPrice(events, { tokenA, tokenB }); + const lastDisplayPrice = useMemo((): BigNumber | undefined => { + return getTxEventsEndDisplayPrice(events, tokenA, tokenB); }, [events, tokenA, tokenB]); const prevPrice = useMemo(() => { - return getLastPrice(previousEvents, { tokenA, tokenB }); + return getTxEventsEndDisplayPrice(previousEvents, tokenA, tokenB); }, [previousEvents, tokenA, tokenB]); const diff = useMemo(() => { - return lastPrice !== undefined && prevPrice !== undefined - ? lastPrice.minus(prevPrice).toNumber() + return lastDisplayPrice !== undefined && prevPrice !== undefined + ? lastDisplayPrice.minus(prevPrice).toNumber() : 0; - }, [lastPrice, prevPrice]); + }, [lastDisplayPrice, prevPrice]); const value = useMemo(() => { const amountSpentA = getSpentTokenAmount(events, { matchToken: tokenA }); @@ -199,8 +200,8 @@ function OrderbookTradeListRow({ )} - {lastPrice - ? formatAmount(lastPrice?.toNumber(), { + {lastDisplayPrice + ? formatAmount(lastDisplayPrice?.toNumber(), { minimumFractionDigits: priceDecimalPlaces, maximumFractionDigits: priceDecimalPlaces, }) From 4305d0a51678562a019d1c090a5d7e85cdc048fe Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Wed, 5 Jun 2024 10:18:51 +1000 Subject: [PATCH 190/192] fix: use consistent end display price of txs calculations, part 2 --- src/components/cards/LimitOrderCard.tsx | 43 ++++++++++--------------- src/lib/web3/utils/events.ts | 4 +-- 2 files changed, 19 insertions(+), 28 deletions(-) diff --git a/src/components/cards/LimitOrderCard.tsx b/src/components/cards/LimitOrderCard.tsx index 4d1d6b756..ea75255b3 100644 --- a/src/components/cards/LimitOrderCard.tsx +++ b/src/components/cards/LimitOrderCard.tsx @@ -62,12 +62,13 @@ import { immediateOrderTypes, } from '../../lib/web3/utils/limitOrders'; import { - DexTickUpdateEvent, getTxEventsEndDisplayPrice, + getTxEventsEndTickIndexInToOut, mapEventAttributes, } from '../../lib/web3/utils/events'; import { displayPriceToTickIndex, + priceToTickIndex, tickIndexToPrice, } from '../../lib/web3/utils/ticks'; import { guessInvertedOrder } from '../../lib/web3/utils/pairs'; @@ -588,34 +589,24 @@ function LimitOrder({ // define function to get the current market limit index with a tolerance const getMarketLimitIndex = () => { - // calculate last price out from result - const lastPriceEvent = simulationResult?.result?.events.findLast( - (event) => event.type === 'TickUpdate' - ); - - // if market limit simulation made no update to the liquidity then skip - if (!lastPriceEvent) { - return; - } - - const denomIn = getTokenId(tokenIn); - - // calculate tolerance from user slippage settings + // calculate tolerance (in ticks) from user slippage settings // set tiny minimum of tolerance as the frontend calculations // don't always exactly align with the backend calculations - const tolerance = Math.max(1e-12, Number(formState.slippage) || 0); - const toleranceFactor = 1 + tolerance; + const slippage = Number(formState.slippage) / 100 || 0; + const toleranceTicks = priceToTickIndex(new BigNumber(1 + slippage)) + .decimalPlaces(0, BigNumber.ROUND_UP) + .toNumber(); + // calculate last price out from matching event results - const lastPrice = - mapEventAttributes(lastPriceEvent)?.attributes; - const direction = - lastPrice.TokenIn === denomIn - ? lastPrice.TokenIn === lastPrice.TokenZero - : lastPrice.TokenIn === lastPrice.TokenOne; - - return direction - ? Math.floor(Number(lastPrice.TickIndex) / toleranceFactor) - : Math.floor(Number(lastPrice.TickIndex) * toleranceFactor); + const events = simulationResult?.result?.events.map(mapEventAttributes); + const lastTickIndex = events && getTxEventsEndTickIndexInToOut(events); + + // return the limit with tolerance + return ( + lastTickIndex && + // apply tolerance in the in->out direction to the result tick index + lastTickIndex + toleranceTicks + ); }; // find limit price from custom limit price or limit price offset diff --git a/src/lib/web3/utils/events.ts b/src/lib/web3/utils/events.ts index be60c7502..b75974978 100644 --- a/src/lib/web3/utils/events.ts +++ b/src/lib/web3/utils/events.ts @@ -270,7 +270,7 @@ export interface IBCReceivePacketEvent { attributes: IBCPacketEventAttributes; } -function getDexActionEvent( +export function getDexActionEvent( action: T['attributes']['action'], events: ChainEvent[] ): T | undefined { @@ -280,7 +280,7 @@ function getDexActionEvent( ); } -function getTxEventsEndTickIndexInToOut(events: ChainEvent[]): number { +export function getTxEventsEndTickIndexInToOut(events: ChainEvent[]): number { // get the original request event const placeLimitOrderEvent = getDexActionEvent( 'PlaceLimitOrder', From 3aea800f691300e77658af0027dde1b330922f4f Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Wed, 5 Jun 2024 10:20:18 +1000 Subject: [PATCH 191/192] refactor: rename form slippage to include percentage indication --- src/components/cards/LimitOrderCard.tsx | 6 +++--- src/components/cards/LimitOrderContext.tsx | 21 ++++++++++++++------- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/components/cards/LimitOrderCard.tsx b/src/components/cards/LimitOrderCard.tsx index ea75255b3..852d154eb 100644 --- a/src/components/cards/LimitOrderCard.tsx +++ b/src/components/cards/LimitOrderCard.tsx @@ -592,7 +592,7 @@ function LimitOrder({ // calculate tolerance (in ticks) from user slippage settings // set tiny minimum of tolerance as the frontend calculations // don't always exactly align with the backend calculations - const slippage = Number(formState.slippage) / 100 || 0; + const slippage = Number(formState.slippagePercent) / 100 || 0; const toleranceTicks = priceToTickIndex(new BigNumber(1 + slippage)) .decimalPlaces(0, BigNumber.ROUND_UP) .toNumber(); @@ -650,7 +650,7 @@ function LimitOrder({ }, [ formState.limitPrice, - formState.slippage, + formState.slippagePercent, formState.execution, formState.timeAmount, formState.timePeriod, @@ -763,7 +763,7 @@ function LimitOrder({ if (priceTab === 'Limit' && limitOption !== 0) { setPriceIndication(Number(formState.limitPrice || offsetLimitPrice)); } - // set market price slippage result: based on trade simulation end price + // set market price "slippage" result: based on trade simulation end price else if ( priceTab === 'Swap' && tokenA && diff --git a/src/components/cards/LimitOrderContext.tsx b/src/components/cards/LimitOrderContext.tsx index cb5178830..87b161f10 100644 --- a/src/components/cards/LimitOrderContext.tsx +++ b/src/components/cards/LimitOrderContext.tsx @@ -18,7 +18,7 @@ interface FormState { timeAmount: string; timePeriod: TimePeriod; execution: AllowedLimitOrderTypeKey; - slippage: string; + slippagePercent: string; } interface FormSetState { setAmountInOut: Dispatch>; @@ -26,7 +26,7 @@ interface FormSetState { setTimeAmount: Dispatch>; setTimePeriod: Dispatch>; setExecution: Dispatch>; - setSlippage: Dispatch>; + setSlippagePercent: Dispatch>; } export const LimitOrderFormContext = createContext>({}); @@ -46,7 +46,7 @@ export function LimitOrderContextProvider({ const [timeAmount, setTimeAmount] = useState('1'); const [timePeriod, setTimePeriod] = useState('hours'); const [execution, setExecution] = useState(defaultExecutionType); - const [slippage, setSlippage] = useState(''); + const [slippagePercent, setSlippagePercent] = useState('0.5'); const state = useMemo(() => { const [amountIn, amountOut] = amountInOut; @@ -57,9 +57,16 @@ export function LimitOrderContextProvider({ timeAmount, timePeriod, execution, - slippage, + slippagePercent, }; - }, [amountInOut, limitPrice, timeAmount, timePeriod, execution, slippage]); + }, [ + amountInOut, + limitPrice, + timeAmount, + timePeriod, + execution, + slippagePercent, + ]); const setState = useMemo(() => { return { @@ -68,7 +75,7 @@ export function LimitOrderContextProvider({ setTimeAmount, setTimePeriod, setExecution, - setSlippage, + setSlippagePercent, }; }, [ setAmountInOut, @@ -76,7 +83,7 @@ export function LimitOrderContextProvider({ setTimeAmount, setTimePeriod, setExecution, - setSlippage, + setSlippagePercent, ]); return ( From 85919acf479af5ff4d2cc017ceda2512fa57472d Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Wed, 5 Jun 2024 11:30:19 +1000 Subject: [PATCH 192/192] fix: replace some tickIndex->basePrice with tickIndex->displayPrice - in the Orderbook mostly display tokens are expected --- src/pages/Orderbook/OrderbookChart.tsx | 48 +++++++++++++++---------- src/pages/Orderbook/OrderbookFooter.tsx | 18 +++++++--- 2 files changed, 43 insertions(+), 23 deletions(-) diff --git a/src/pages/Orderbook/OrderbookChart.tsx b/src/pages/Orderbook/OrderbookChart.tsx index 929b409c8..f50f866fe 100644 --- a/src/pages/Orderbook/OrderbookChart.tsx +++ b/src/pages/Orderbook/OrderbookChart.tsx @@ -18,7 +18,7 @@ import { import { Token, getTokenId } from '../../lib/web3/utils/tokens'; -import { tickIndexToPrice } from '../../lib/web3/utils/ticks'; +import { tickIndexToDisplayPrice } from '../../lib/web3/utils/ticks'; import { TimeSeriesRow } from '../../components/stats/utils'; import { IndexerPage, @@ -114,9 +114,6 @@ export default function OrderBookChart({ React.SetStateAction >; }) { - const tokenIdA = getTokenId(tokenA); - const tokenIdB = getTokenId(tokenB); - const navigate = useNavigate(); // find chart container to fit @@ -133,6 +130,9 @@ export default function OrderBookChart({ }, [tokenA, tokenB]); useEffect(() => { + const tokenIdA = getTokenId(tokenA); + const tokenIdB = getTokenId(tokenB); + const supportedResolutions: ResolutionString[] = [ '1S', // second '1', // minute @@ -430,7 +430,32 @@ export default function OrderBookChart({ }); }; } - }, [navigate, setLimitPrice, tokenIdA, tokenIdB, tokenPairID]); + + // add specific helper function to convert between display tokens + function tickIndexToDisplayPriceAtoB(value: number): number { + return ( + tickIndexToDisplayPrice( + new BigNumber(value), + tokenA, + tokenB + )?.toNumber() ?? NaN + ); + } + + // add specific getBarFromTimeSeriesRow for context's tokenA and tokenB + function getBarFromTimeSeriesRow([ + unixTimestamp, + [open, high, low, close], + ]: TimeSeriesRow) { + return { + time: unixTimestamp * 1000, + open: tickIndexToDisplayPriceAtoB(open), + high: tickIndexToDisplayPriceAtoB(high), + low: tickIndexToDisplayPriceAtoB(low), + close: tickIndexToDisplayPriceAtoB(close), + }; + } + }, [navigate, setLimitPrice, tokenA, tokenB, tokenPairID]); useEffect(() => { if (priceIndication && chart && isChartReady(chart)) { @@ -500,16 +525,3 @@ export default function OrderBookChart({ >
); } - -function getBarFromTimeSeriesRow([ - unixTimestamp, - [open, high, low, close], -]: TimeSeriesRow) { - return { - time: unixTimestamp * 1000, - open: tickIndexToPrice(new BigNumber(open)).toNumber(), - high: tickIndexToPrice(new BigNumber(high)).toNumber(), - low: tickIndexToPrice(new BigNumber(low)).toNumber(), - close: tickIndexToPrice(new BigNumber(close)).toNumber(), - }; -} diff --git a/src/pages/Orderbook/OrderbookFooter.tsx b/src/pages/Orderbook/OrderbookFooter.tsx index df9767e70..b2c486131 100644 --- a/src/pages/Orderbook/OrderbookFooter.tsx +++ b/src/pages/Orderbook/OrderbookFooter.tsx @@ -46,7 +46,7 @@ import { useCancelLimitOrder } from './useCancelLimitOrder'; import { useOnUserDexMsg } from '../../lib/web3/hooks/useOnDexMsg'; import { getTokenPairID } from '../../lib/web3/utils/pairs'; -import { tickIndexToPrice } from '../../lib/web3/utils/ticks'; +import { tickIndexToDisplayPrice } from '../../lib/web3/utils/ticks'; import './OrderbookFooter.scss'; @@ -301,11 +301,13 @@ function PriceColumn({
{isOrdered ? '<' : '>'} {formatCurrency( - tickIndexToPrice( + tickIndexToDisplayPrice( new BigNumber(isOrdered ? 1 : -1).multipliedBy( getPlaceLimitOrderEvent(tx)?.attributes.LimitTick ?? NaN - ) - ).toNumber() + ), + context.tokenA, + context.tokenB + )?.toNumber() || '-' )}
)} @@ -345,7 +347,13 @@ function getDisplayReservesMain(tx: Tx, context: OrderbookFooterTableContext) { return !isOrdered ? reservesIn : new BigNumber(reservesIn || 0) - .dividedBy(tickIndexToPrice(new BigNumber(attributes?.LimitTick || 0))) + .dividedBy( + tickIndexToDisplayPrice( + new BigNumber(attributes?.LimitTick || 0), + context.tokenA, + context.tokenB + ) || 0 + ) .toNumber(); }