diff --git a/e2e/screenshots/simulator/recurring/Recurring limit limit/form.jpg b/e2e/screenshots/simulator/recurring/Recurring limit limit/form.jpg index f3c47ed17..6d9a18b96 100644 Binary files a/e2e/screenshots/simulator/recurring/Recurring limit limit/form.jpg and b/e2e/screenshots/simulator/recurring/Recurring limit limit/form.jpg differ diff --git a/e2e/screenshots/simulator/recurring/Recurring limit limit/simulator-input-price.jpg b/e2e/screenshots/simulator/recurring/Recurring limit limit/simulator-input-price.jpg index ed4c4c146..c3b1bf725 100644 Binary files a/e2e/screenshots/simulator/recurring/Recurring limit limit/simulator-input-price.jpg and b/e2e/screenshots/simulator/recurring/Recurring limit limit/simulator-input-price.jpg differ diff --git a/e2e/screenshots/simulator/recurring/Recurring limit limit/simulator-results-animation.jpg b/e2e/screenshots/simulator/recurring/Recurring limit limit/simulator-results-animation.jpg index 83c3a2370..5f4830fe0 100644 Binary files a/e2e/screenshots/simulator/recurring/Recurring limit limit/simulator-results-animation.jpg and b/e2e/screenshots/simulator/recurring/Recurring limit limit/simulator-results-animation.jpg differ diff --git a/e2e/screenshots/simulator/recurring/Recurring limit range/form.jpg b/e2e/screenshots/simulator/recurring/Recurring limit range/form.jpg index 6537dcdd9..d9c738abc 100644 Binary files a/e2e/screenshots/simulator/recurring/Recurring limit range/form.jpg and b/e2e/screenshots/simulator/recurring/Recurring limit range/form.jpg differ diff --git a/e2e/screenshots/simulator/recurring/Recurring limit range/simulator-input-price.jpg b/e2e/screenshots/simulator/recurring/Recurring limit range/simulator-input-price.jpg index b8c2c38ac..f6edc094b 100644 Binary files a/e2e/screenshots/simulator/recurring/Recurring limit range/simulator-input-price.jpg and b/e2e/screenshots/simulator/recurring/Recurring limit range/simulator-input-price.jpg differ diff --git a/e2e/screenshots/simulator/recurring/Recurring limit range/simulator-results-summary.jpg b/e2e/screenshots/simulator/recurring/Recurring limit range/simulator-results-summary.jpg index 110c8134d..899ea664c 100644 Binary files a/e2e/screenshots/simulator/recurring/Recurring limit range/simulator-results-summary.jpg and b/e2e/screenshots/simulator/recurring/Recurring limit range/simulator-results-summary.jpg differ diff --git a/e2e/screenshots/simulator/recurring/Recurring range limit/form.jpg b/e2e/screenshots/simulator/recurring/Recurring range limit/form.jpg index 1577bf541..15b0dcc8d 100644 Binary files a/e2e/screenshots/simulator/recurring/Recurring range limit/form.jpg and b/e2e/screenshots/simulator/recurring/Recurring range limit/form.jpg differ diff --git a/e2e/screenshots/simulator/recurring/Recurring range limit/simulator-input-price.jpg b/e2e/screenshots/simulator/recurring/Recurring range limit/simulator-input-price.jpg index 8d30f6e34..696fd8029 100644 Binary files a/e2e/screenshots/simulator/recurring/Recurring range limit/simulator-input-price.jpg and b/e2e/screenshots/simulator/recurring/Recurring range limit/simulator-input-price.jpg differ diff --git a/e2e/screenshots/simulator/recurring/Recurring range limit/simulator-results-summary.jpg b/e2e/screenshots/simulator/recurring/Recurring range limit/simulator-results-summary.jpg index f6ac4183f..fd0f19f3f 100644 Binary files a/e2e/screenshots/simulator/recurring/Recurring range limit/simulator-results-summary.jpg and b/e2e/screenshots/simulator/recurring/Recurring range limit/simulator-results-summary.jpg differ diff --git a/e2e/screenshots/simulator/recurring/Recurring range range/form.jpg b/e2e/screenshots/simulator/recurring/Recurring range range/form.jpg index ab0a7aa36..83b90f319 100644 Binary files a/e2e/screenshots/simulator/recurring/Recurring range range/form.jpg and b/e2e/screenshots/simulator/recurring/Recurring range range/form.jpg differ diff --git a/e2e/screenshots/simulator/recurring/Recurring range range/simulator-input-price.jpg b/e2e/screenshots/simulator/recurring/Recurring range range/simulator-input-price.jpg index aecd2c71c..0ba9020ee 100644 Binary files a/e2e/screenshots/simulator/recurring/Recurring range range/simulator-input-price.jpg and b/e2e/screenshots/simulator/recurring/Recurring range range/simulator-input-price.jpg differ diff --git a/e2e/screenshots/simulator/recurring/Recurring range range/simulator-results-animation.jpg b/e2e/screenshots/simulator/recurring/Recurring range range/simulator-results-animation.jpg index a1aa98801..56dbbe3d6 100644 Binary files a/e2e/screenshots/simulator/recurring/Recurring range range/simulator-results-animation.jpg and b/e2e/screenshots/simulator/recurring/Recurring range range/simulator-results-animation.jpg differ diff --git a/e2e/screenshots/simulator/recurring/Recurring range range/simulator-results-summary.jpg b/e2e/screenshots/simulator/recurring/Recurring range range/simulator-results-summary.jpg index 0174716b8..c5010ead2 100644 Binary files a/e2e/screenshots/simulator/recurring/Recurring range range/simulator-results-summary.jpg and b/e2e/screenshots/simulator/recurring/Recurring range range/simulator-results-summary.jpg differ diff --git a/e2e/screenshots/strategy/disposable/Disposable buy limit/create/form.jpg b/e2e/screenshots/strategy/disposable/Disposable buy limit/create/form.jpg index 32d7b15a0..5a92b04dc 100644 Binary files a/e2e/screenshots/strategy/disposable/Disposable buy limit/create/form.jpg and b/e2e/screenshots/strategy/disposable/Disposable buy limit/create/form.jpg differ diff --git a/e2e/screenshots/strategy/disposable/Disposable buy limit/deposit/form.jpg b/e2e/screenshots/strategy/disposable/Disposable buy limit/deposit/form.jpg index 311468a0d..3808709ea 100644 Binary files a/e2e/screenshots/strategy/disposable/Disposable buy limit/deposit/form.jpg and b/e2e/screenshots/strategy/disposable/Disposable buy limit/deposit/form.jpg differ diff --git a/e2e/screenshots/strategy/disposable/Disposable buy limit/editPrices/form.jpg b/e2e/screenshots/strategy/disposable/Disposable buy limit/editPrices/form.jpg index b5cdaccfb..49a6742c9 100644 Binary files a/e2e/screenshots/strategy/disposable/Disposable buy limit/editPrices/form.jpg and b/e2e/screenshots/strategy/disposable/Disposable buy limit/editPrices/form.jpg differ diff --git a/e2e/screenshots/strategy/disposable/Disposable buy limit/withdraw/form.jpg b/e2e/screenshots/strategy/disposable/Disposable buy limit/withdraw/form.jpg index f71ca2d63..21212475e 100644 Binary files a/e2e/screenshots/strategy/disposable/Disposable buy limit/withdraw/form.jpg and b/e2e/screenshots/strategy/disposable/Disposable buy limit/withdraw/form.jpg differ diff --git a/e2e/screenshots/strategy/disposable/Disposable buy range/create/form.jpg b/e2e/screenshots/strategy/disposable/Disposable buy range/create/form.jpg index a4bff563f..c383b4852 100644 Binary files a/e2e/screenshots/strategy/disposable/Disposable buy range/create/form.jpg and b/e2e/screenshots/strategy/disposable/Disposable buy range/create/form.jpg differ diff --git a/e2e/screenshots/strategy/disposable/Disposable buy range/deposit/form.jpg b/e2e/screenshots/strategy/disposable/Disposable buy range/deposit/form.jpg index e8d989ca5..e528976c7 100644 Binary files a/e2e/screenshots/strategy/disposable/Disposable buy range/deposit/form.jpg and b/e2e/screenshots/strategy/disposable/Disposable buy range/deposit/form.jpg differ diff --git a/e2e/screenshots/strategy/disposable/Disposable buy range/editPrices/form.jpg b/e2e/screenshots/strategy/disposable/Disposable buy range/editPrices/form.jpg index 0720957c7..6d184c488 100644 Binary files a/e2e/screenshots/strategy/disposable/Disposable buy range/editPrices/form.jpg and b/e2e/screenshots/strategy/disposable/Disposable buy range/editPrices/form.jpg differ diff --git a/e2e/screenshots/strategy/disposable/Disposable buy range/withdraw/form.jpg b/e2e/screenshots/strategy/disposable/Disposable buy range/withdraw/form.jpg index c1af62137..c880e0f62 100644 Binary files a/e2e/screenshots/strategy/disposable/Disposable buy range/withdraw/form.jpg and b/e2e/screenshots/strategy/disposable/Disposable buy range/withdraw/form.jpg differ diff --git a/e2e/screenshots/strategy/disposable/Disposable sell limit/create/form.jpg b/e2e/screenshots/strategy/disposable/Disposable sell limit/create/form.jpg index d5713cb33..ad6bebd94 100644 Binary files a/e2e/screenshots/strategy/disposable/Disposable sell limit/create/form.jpg and b/e2e/screenshots/strategy/disposable/Disposable sell limit/create/form.jpg differ diff --git a/e2e/screenshots/strategy/disposable/Disposable sell limit/deposit/form.jpg b/e2e/screenshots/strategy/disposable/Disposable sell limit/deposit/form.jpg index edfaae038..496b30f30 100644 Binary files a/e2e/screenshots/strategy/disposable/Disposable sell limit/deposit/form.jpg and b/e2e/screenshots/strategy/disposable/Disposable sell limit/deposit/form.jpg differ diff --git a/e2e/screenshots/strategy/disposable/Disposable sell limit/editPrices/form.jpg b/e2e/screenshots/strategy/disposable/Disposable sell limit/editPrices/form.jpg index b82bab1e4..54cc1199d 100644 Binary files a/e2e/screenshots/strategy/disposable/Disposable sell limit/editPrices/form.jpg and b/e2e/screenshots/strategy/disposable/Disposable sell limit/editPrices/form.jpg differ diff --git a/e2e/screenshots/strategy/disposable/Disposable sell limit/withdraw/form.jpg b/e2e/screenshots/strategy/disposable/Disposable sell limit/withdraw/form.jpg index 402eab9f8..4dc20a66c 100644 Binary files a/e2e/screenshots/strategy/disposable/Disposable sell limit/withdraw/form.jpg and b/e2e/screenshots/strategy/disposable/Disposable sell limit/withdraw/form.jpg differ diff --git a/e2e/screenshots/strategy/disposable/Disposable sell range/create/form.jpg b/e2e/screenshots/strategy/disposable/Disposable sell range/create/form.jpg index 998b0c710..f0dc380b2 100644 Binary files a/e2e/screenshots/strategy/disposable/Disposable sell range/create/form.jpg and b/e2e/screenshots/strategy/disposable/Disposable sell range/create/form.jpg differ diff --git a/e2e/screenshots/strategy/disposable/Disposable sell range/deposit/form.jpg b/e2e/screenshots/strategy/disposable/Disposable sell range/deposit/form.jpg index 5c9df7ce2..fbc4a3e8a 100644 Binary files a/e2e/screenshots/strategy/disposable/Disposable sell range/deposit/form.jpg and b/e2e/screenshots/strategy/disposable/Disposable sell range/deposit/form.jpg differ diff --git a/e2e/screenshots/strategy/disposable/Disposable sell range/editPrices/form.jpg b/e2e/screenshots/strategy/disposable/Disposable sell range/editPrices/form.jpg index 58d75f45c..f5acedb0f 100644 Binary files a/e2e/screenshots/strategy/disposable/Disposable sell range/editPrices/form.jpg and b/e2e/screenshots/strategy/disposable/Disposable sell range/editPrices/form.jpg differ diff --git a/e2e/screenshots/strategy/disposable/Disposable sell range/withdraw/form.jpg b/e2e/screenshots/strategy/disposable/Disposable sell range/withdraw/form.jpg index 105e68c63..c4899d026 100644 Binary files a/e2e/screenshots/strategy/disposable/Disposable sell range/withdraw/form.jpg and b/e2e/screenshots/strategy/disposable/Disposable sell range/withdraw/form.jpg differ diff --git a/e2e/screenshots/strategy/overlapping/Overlapping/create/form.jpg b/e2e/screenshots/strategy/overlapping/Overlapping/create/form.jpg index d2833696e..2342e3377 100644 Binary files a/e2e/screenshots/strategy/overlapping/Overlapping/create/form.jpg and b/e2e/screenshots/strategy/overlapping/Overlapping/create/form.jpg differ diff --git a/e2e/screenshots/strategy/recurring/Recurring limit limit/create/form.jpg b/e2e/screenshots/strategy/recurring/Recurring limit limit/create/form.jpg index df46a36d0..805fa9736 100644 Binary files a/e2e/screenshots/strategy/recurring/Recurring limit limit/create/form.jpg and b/e2e/screenshots/strategy/recurring/Recurring limit limit/create/form.jpg differ diff --git a/e2e/screenshots/strategy/recurring/Recurring limit limit/create/my-strategy.jpg b/e2e/screenshots/strategy/recurring/Recurring limit limit/create/my-strategy.jpg index 22b7201b4..14ab32428 100644 Binary files a/e2e/screenshots/strategy/recurring/Recurring limit limit/create/my-strategy.jpg and b/e2e/screenshots/strategy/recurring/Recurring limit limit/create/my-strategy.jpg differ diff --git a/e2e/screenshots/strategy/recurring/Recurring limit limit/deposit/form.jpg b/e2e/screenshots/strategy/recurring/Recurring limit limit/deposit/form.jpg index bc3426363..b47df47fc 100644 Binary files a/e2e/screenshots/strategy/recurring/Recurring limit limit/deposit/form.jpg and b/e2e/screenshots/strategy/recurring/Recurring limit limit/deposit/form.jpg differ diff --git a/e2e/screenshots/strategy/recurring/Recurring limit limit/editPrices/form.jpg b/e2e/screenshots/strategy/recurring/Recurring limit limit/editPrices/form.jpg index ec96236b9..776ce132c 100644 Binary files a/e2e/screenshots/strategy/recurring/Recurring limit limit/editPrices/form.jpg and b/e2e/screenshots/strategy/recurring/Recurring limit limit/editPrices/form.jpg differ diff --git a/e2e/screenshots/strategy/recurring/Recurring limit limit/renew/form.jpg b/e2e/screenshots/strategy/recurring/Recurring limit limit/renew/form.jpg index fb5308e53..8a0570c05 100644 Binary files a/e2e/screenshots/strategy/recurring/Recurring limit limit/renew/form.jpg and b/e2e/screenshots/strategy/recurring/Recurring limit limit/renew/form.jpg differ diff --git a/e2e/screenshots/strategy/recurring/Recurring limit limit/withdraw/form.jpg b/e2e/screenshots/strategy/recurring/Recurring limit limit/withdraw/form.jpg index 52d7aec09..a798e55a6 100644 Binary files a/e2e/screenshots/strategy/recurring/Recurring limit limit/withdraw/form.jpg and b/e2e/screenshots/strategy/recurring/Recurring limit limit/withdraw/form.jpg differ diff --git a/e2e/screenshots/strategy/recurring/Recurring limit range/create/form.jpg b/e2e/screenshots/strategy/recurring/Recurring limit range/create/form.jpg index cc796f050..cbe9f54d0 100644 Binary files a/e2e/screenshots/strategy/recurring/Recurring limit range/create/form.jpg and b/e2e/screenshots/strategy/recurring/Recurring limit range/create/form.jpg differ diff --git a/e2e/screenshots/strategy/recurring/Recurring limit range/deposit/form.jpg b/e2e/screenshots/strategy/recurring/Recurring limit range/deposit/form.jpg index bbb871485..10edcf61e 100644 Binary files a/e2e/screenshots/strategy/recurring/Recurring limit range/deposit/form.jpg and b/e2e/screenshots/strategy/recurring/Recurring limit range/deposit/form.jpg differ diff --git a/e2e/screenshots/strategy/recurring/Recurring limit range/editPrices/form.jpg b/e2e/screenshots/strategy/recurring/Recurring limit range/editPrices/form.jpg index 8395e7298..d6563ac7f 100644 Binary files a/e2e/screenshots/strategy/recurring/Recurring limit range/editPrices/form.jpg and b/e2e/screenshots/strategy/recurring/Recurring limit range/editPrices/form.jpg differ diff --git a/e2e/screenshots/strategy/recurring/Recurring limit range/renew/form.jpg b/e2e/screenshots/strategy/recurring/Recurring limit range/renew/form.jpg index 794ad17e7..f6aa66516 100644 Binary files a/e2e/screenshots/strategy/recurring/Recurring limit range/renew/form.jpg and b/e2e/screenshots/strategy/recurring/Recurring limit range/renew/form.jpg differ diff --git a/e2e/screenshots/strategy/recurring/Recurring limit range/withdraw/form.jpg b/e2e/screenshots/strategy/recurring/Recurring limit range/withdraw/form.jpg index 1952973d6..d27c8ea6f 100644 Binary files a/e2e/screenshots/strategy/recurring/Recurring limit range/withdraw/form.jpg and b/e2e/screenshots/strategy/recurring/Recurring limit range/withdraw/form.jpg differ diff --git a/e2e/screenshots/strategy/recurring/Recurring range limit/create/form.jpg b/e2e/screenshots/strategy/recurring/Recurring range limit/create/form.jpg index 502dba98f..8826a3c42 100644 Binary files a/e2e/screenshots/strategy/recurring/Recurring range limit/create/form.jpg and b/e2e/screenshots/strategy/recurring/Recurring range limit/create/form.jpg differ diff --git a/e2e/screenshots/strategy/recurring/Recurring range limit/create/my-strategy.jpg b/e2e/screenshots/strategy/recurring/Recurring range limit/create/my-strategy.jpg index 3671d6b2e..05ac6b35e 100644 Binary files a/e2e/screenshots/strategy/recurring/Recurring range limit/create/my-strategy.jpg and b/e2e/screenshots/strategy/recurring/Recurring range limit/create/my-strategy.jpg differ diff --git a/e2e/screenshots/strategy/recurring/Recurring range limit/editPrices/form.jpg b/e2e/screenshots/strategy/recurring/Recurring range limit/editPrices/form.jpg index c7ec3229d..5d52b545d 100644 Binary files a/e2e/screenshots/strategy/recurring/Recurring range limit/editPrices/form.jpg and b/e2e/screenshots/strategy/recurring/Recurring range limit/editPrices/form.jpg differ diff --git a/e2e/screenshots/strategy/recurring/Recurring range limit/renew/form.jpg b/e2e/screenshots/strategy/recurring/Recurring range limit/renew/form.jpg index 369fa69e5..b788fa5b9 100644 Binary files a/e2e/screenshots/strategy/recurring/Recurring range limit/renew/form.jpg and b/e2e/screenshots/strategy/recurring/Recurring range limit/renew/form.jpg differ diff --git a/e2e/screenshots/strategy/recurring/Recurring range limit/withdraw/form.jpg b/e2e/screenshots/strategy/recurring/Recurring range limit/withdraw/form.jpg index dad2b4f31..6b6e997b2 100644 Binary files a/e2e/screenshots/strategy/recurring/Recurring range limit/withdraw/form.jpg and b/e2e/screenshots/strategy/recurring/Recurring range limit/withdraw/form.jpg differ diff --git a/e2e/screenshots/strategy/recurring/Recurring range range/create/form.jpg b/e2e/screenshots/strategy/recurring/Recurring range range/create/form.jpg index 71a2a6705..b0d378f65 100644 Binary files a/e2e/screenshots/strategy/recurring/Recurring range range/create/form.jpg and b/e2e/screenshots/strategy/recurring/Recurring range range/create/form.jpg differ diff --git a/e2e/screenshots/strategy/recurring/Recurring range range/deposit/form.jpg b/e2e/screenshots/strategy/recurring/Recurring range range/deposit/form.jpg index cafdc3928..9bf794fab 100644 Binary files a/e2e/screenshots/strategy/recurring/Recurring range range/deposit/form.jpg and b/e2e/screenshots/strategy/recurring/Recurring range range/deposit/form.jpg differ diff --git a/e2e/screenshots/strategy/recurring/Recurring range range/renew/form.jpg b/e2e/screenshots/strategy/recurring/Recurring range range/renew/form.jpg index 93d3dc2f2..6c2fecbb7 100644 Binary files a/e2e/screenshots/strategy/recurring/Recurring range range/renew/form.jpg and b/e2e/screenshots/strategy/recurring/Recurring range range/renew/form.jpg differ diff --git a/e2e/screenshots/strategy/recurring/Recurring range range/withdraw/form.jpg b/e2e/screenshots/strategy/recurring/Recurring range range/withdraw/form.jpg index ae3f54326..dc284e0d7 100644 Binary files a/e2e/screenshots/strategy/recurring/Recurring range range/withdraw/form.jpg and b/e2e/screenshots/strategy/recurring/Recurring range range/withdraw/form.jpg differ diff --git a/src/assets/icons/action.svg b/src/assets/icons/action.svg index 85a541bce..533f47604 100644 --- a/src/assets/icons/action.svg +++ b/src/assets/icons/action.svg @@ -1,5 +1,5 @@ - - + + \ No newline at end of file diff --git a/src/components/activity/ActivityFilter.tsx b/src/components/activity/ActivityFilter.tsx index 4d554711d..0a30eb9c4 100644 --- a/src/components/activity/ActivityFilter.tsx +++ b/src/components/activity/ActivityFilter.tsx @@ -63,14 +63,14 @@ export const ActivityFilter: FC = (props) => { const updateParams = () => { const selector = `input[form="${formId}"]`; const inputs = document.querySelectorAll(selector); - const params: Record = {}; + const params: Record = {}; for (const input of inputs) { const name = input.name; - params[name] ||= []; if (input.type === 'checkbox') { - if (input.checked) params[name].push(input.value); + params[name] ||= []; + if (input.checked) (params[name] as string[]).push(input.value); } else { - params[name].push(input.value); + params[name] = input.value; } } setSearchParams(params); diff --git a/src/components/common/datePicker/DateRangePicker.tsx b/src/components/common/datePicker/DateRangePicker.tsx index 36577e228..d36a4b6bc 100644 --- a/src/components/common/datePicker/DateRangePicker.tsx +++ b/src/components/common/datePicker/DateRangePicker.tsx @@ -43,6 +43,7 @@ interface Props { options?: Omit; required?: boolean; form?: string; + disabled?: boolean; } const displayRange = (start?: Date, end?: Date) => { @@ -65,9 +66,12 @@ export const DateRangePicker = memo((props: Omit) => { 'hover:bg-background-800', hasDates ? 'border-primary active:border-primary-light' - : 'border-background-800 hover:border-background-700 active:border-background-600' + : 'border-background-800 hover:border-background-700 active:border-background-600', + props.disabled && + 'cursor-not-allowed border-background-800 hover:border-background-800 hover:bg-transparent active:border-background-800' )} data-testid="date-picker-button" + disabled={props.disabled} > ) => { > {displayRange(props.start, props.end)} - + {!props.disabled && ( + + )} ); diff --git a/src/components/simulator/input/BuySellBlockNew/LimitRangeSection.tsx b/src/components/simulator/input/BuySellBlockNew/LimitRangeSection.tsx index 1706112dd..c914c7dae 100644 --- a/src/components/simulator/input/BuySellBlockNew/LimitRangeSection.tsx +++ b/src/components/simulator/input/BuySellBlockNew/LimitRangeSection.tsx @@ -32,13 +32,12 @@ export const LimitRangeSection: FC = ({ ignoreMarketPriceWarning, }) => { const { isRange } = order; - const { marketPricePercentage, isOrderAboveOrBelowMarketPrice } = - useMarketIndication({ - base, - quote, - order: { ...order, price: order.min }, - buy, - }); + const { isOrderAboveOrBelowMarketPrice } = useMarketIndication({ + base, + quote, + order: { ...order, price: order.min }, + buy, + }); const overlappingOrdersPricesMessage = 'Notice: your Buy and Sell orders overlap'; @@ -81,7 +80,6 @@ export const LimitRangeSection: FC = ({ quote={quote} base={base} buy={buy} - marketPricePercentages={marketPricePercentage} ignoreMarketPriceWarning={ignoreMarketPriceWarning} isOrdersReversed={isOrdersReversed} warnings={getWarnings()} @@ -98,7 +96,6 @@ export const LimitRangeSection: FC = ({ error={order.priceError} setPriceError={setPriceError} buy={buy} - marketPricePercentage={marketPricePercentage} ignoreMarketPriceWarning={ignoreMarketPriceWarning} isOrdersReversed={isOrdersReversed} warnings={getWarnings()} diff --git a/src/components/simulator/input/SimInputChart.tsx b/src/components/simulator/input/SimInputChart.tsx index 5281fb370..03f7e7dd3 100644 --- a/src/components/simulator/input/SimInputChart.tsx +++ b/src/components/simulator/input/SimInputChart.tsx @@ -7,7 +7,10 @@ import { } from 'components/common/datePicker/DateRangePicker'; import { IconTitleText } from 'components/common/iconTitleText/IconTitleText'; import { datePickerDisabledDays } from 'components/simulator/result/SimResultChartHeader'; -import { SimulatorInputDispatch } from 'hooks/useSimulatorInput'; +import { + SimulatorInputOverlappingValues, + SimulatorOverlappingInputDispatch, +} from 'hooks/useSimulatorOverlappingInput'; import { StrategyInputValues } from 'hooks/useStrategyInput'; import { ChartPrices, @@ -15,9 +18,8 @@ import { OnPriceUpdates, } from 'components/simulator/input/d3Chart'; import { useCompareTokenPrice } from 'libs/queries/extApi/tokenPrice'; -import { StrategyDirection } from 'libs/routing'; -import { SafeDecimal } from 'libs/safedecimal'; -import { Dispatch, SetStateAction, useCallback, useEffect } from 'react'; +import { SimulatorType } from 'libs/routing/routes/sim'; +import { useCallback } from 'react'; import { ReactComponent as IconPlus } from 'assets/icons/plus.svg'; import { CandlestickData, D3ChartSettingsProps, D3ChartWrapper } from 'libs/d3'; import { fromUnixUTC, toUnixUTC } from '../utils'; @@ -25,16 +27,15 @@ import { useStore } from 'store'; import { startOfDay, sub } from 'date-fns'; interface Props { - state: StrategyInputValues; - dispatch: SimulatorInputDispatch; - initBuyRange: boolean; - initSellRange: boolean; - setInitBuyRange: Dispatch>; - setInitSellRange: Dispatch>; + state: StrategyInputValues | SimulatorInputOverlappingValues; + dispatch: SimulatorOverlappingInputDispatch; + isLimit?: { buy: boolean; sell: boolean }; + spread?: string; bounds: ChartPrices; data?: CandlestickData[]; isLoading: boolean; isError: boolean; + simulationType: SimulatorType; } const chartSettings: D3ChartSettingsProps = { @@ -49,14 +50,13 @@ const chartSettings: D3ChartSettingsProps = { export const SimInputChart = ({ state, dispatch, - initBuyRange, - initSellRange, - setInitBuyRange, - setInitSellRange, + isLimit, + spread, bounds, isLoading, isError, data, + simulationType, }: Props) => { const { debug } = useStore(); const marketPrice = useCompareTokenPrice( @@ -64,56 +64,6 @@ export const SimInputChart = ({ state.quoteToken?.address ); - const handleDefaultValues = useCallback( - (type: StrategyDirection) => { - const init = type === 'buy' ? initBuyRange : initSellRange; - const setInit = type === 'buy' ? setInitBuyRange : setInitSellRange; - - if (!marketPrice || !init) return; - setInit(false); - - if (!(!state[type].max && !state[type].min)) { - return; - } - - const operation = type === 'buy' ? 'minus' : 'plus'; - - const multiplierMax = type === 'buy' ? 0.1 : 0.2; - const multiplierMin = type === 'buy' ? 0.2 : 0.1; - - const max = new SafeDecimal(marketPrice) - [operation](marketPrice * multiplierMax) - .toFixed(); - - const min = new SafeDecimal(marketPrice) - [operation](marketPrice * multiplierMin) - .toFixed(); - - if (state[type].isRange) { - dispatch(`${type}Max`, max); - dispatch(`${type}Min`, min); - } else { - const value = type === 'buy' ? max : min; - dispatch(`${type}Max`, value); - dispatch(`${type}Min`, value); - } - }, - [ - dispatch, - initBuyRange, - initSellRange, - marketPrice, - setInitBuyRange, - setInitSellRange, - state, - ] - ); - - useEffect(() => { - handleDefaultValues('buy'); - handleDefaultValues('sell'); - }, [handleDefaultValues]); - const prices = { buy: { min: state.buy.min, @@ -155,7 +105,7 @@ export const SimInputChart = ({ ); return ( -
+

Price Chart

)} diff --git a/src/components/simulator/input/SimInputOverlapping.tsx b/src/components/simulator/input/SimInputOverlapping.tsx deleted file mode 100644 index 86b84ab45..000000000 --- a/src/components/simulator/input/SimInputOverlapping.tsx +++ /dev/null @@ -1,7 +0,0 @@ -export const SimInputOverlapping = () => { - return ( -

- coming soon -

- ); -}; diff --git a/src/components/simulator/input/SimInputStrategyType.tsx b/src/components/simulator/input/SimInputStrategyType.tsx index d0717eb2b..6550c1edc 100644 --- a/src/components/simulator/input/SimInputStrategyType.tsx +++ b/src/components/simulator/input/SimInputStrategyType.tsx @@ -6,17 +6,18 @@ import { cn } from 'utils/helpers'; import { SimulatorType } from 'libs/routing/routes/sim'; import { Link } from 'libs/routing'; -interface Props { - strategyType: SimulatorType; -} - interface ItemProps { label: SimulatorType; svg: JSX.Element; tooltipText: string; } -export const SimInputStrategyType: FC = ({ strategyType }) => { +interface Props { + baseToken?: string; + quoteToken?: string; +} + +export const SimInputStrategyType: FC = ({ baseToken, quoteToken }) => { const items: ItemProps[] = [ { label: 'recurring', @@ -46,33 +47,38 @@ export const SimInputStrategyType: FC = ({ strategyType }) => { role="tab" id={'tab-' + label} aria-controls={'panel-' + label} - aria-selected={strategyType === label} key={label} - to="/simulate/$simulationType" + to={`/simulate/${label}`} + search={{ baseToken, quoteToken }} className={cn( 'flex h-full w-full flex-row items-center justify-center gap-8 rounded-10 bg-black px-8 py-16 text-14 font-weight-500 outline-white/60', 'md:px-12', - 'focus-visible:outline focus-visible:outline-1', - strategyType === label ? 'outline outline-1 outline-white' : '' + 'focus-visible:outline focus-visible:outline-1' )} + activeProps={{ className: 'outline outline-1 outline-white' }} replace={true} resetScroll={false} params={{ simulationType: label }} data-testid={`select-type-${label}`} > - {svg} - - {label} - - - {tooltipText}
} - iconClassName="!h-12 !w-12 text-white/60" - /> + {({ isActive }) => { + return ( + <> + {svg} + + {label} + + {tooltipText}
} + iconClassName="!h-12 !w-12 text-white/60" + /> + + ); + }} ))} diff --git a/src/components/simulator/input/SimInputTokenSelection.tsx b/src/components/simulator/input/SimInputTokenSelection.tsx index 317bc25f2..90ea4d1fa 100644 --- a/src/components/simulator/input/SimInputTokenSelection.tsx +++ b/src/components/simulator/input/SimInputTokenSelection.tsx @@ -1,46 +1,29 @@ -import { Dispatch, FC, SetStateAction } from 'react'; +import { useNavigate } from '@tanstack/react-router'; +import { useTokens } from 'hooks/useTokens'; +import { FC } from 'react'; import { Tooltip } from 'components/common/tooltip/Tooltip'; import { SelectTokenButton } from 'components/common/selectToken'; import { ReactComponent as IconArrow } from 'assets/icons/arrowDown.svg'; -import { Token } from 'libs/tokens'; import { useModal } from 'hooks/useModal'; -import { StrategyInputDispatch } from 'hooks/useStrategyInput'; import { WarningMessageWithIcon } from 'components/common/WarningMessageWithIcon'; import { cn } from 'utils/helpers'; interface Props { - base: Token | undefined; - quote: Token | undefined; + baseToken?: string; + quoteToken?: string; noPriceHistory: boolean; - dispatch: StrategyInputDispatch; - setInitBuyRange: Dispatch>; - setInitSellRange: Dispatch>; } export const SimInputTokenSelection: FC = ({ - base, - quote, + baseToken, + quoteToken, noPriceHistory, - dispatch, - setInitBuyRange, - setInitSellRange, }) => { + const navigate = useNavigate(); const { openModal } = useModal(); - - const swapTokens = () => { - if (base && quote) { - dispatch('baseToken', quote.address); - dispatch('quoteToken', base.address); - dispatch('buyMax', ''); - dispatch('buyMin', ''); - dispatch('sellMax', ''); - dispatch('sellMin', ''); - dispatch('buyBudget', ''); - dispatch('sellBudget', ''); - setInitBuyRange(true); - setInitSellRange(true); - } - }; + const { getTokenById } = useTokens(); + const base = getTokenById(baseToken); + const quote = getTokenById(quoteToken); return (
= ({ onClick={() => { openModal('tokenLists', { onClick: (token) => { - dispatch('baseToken', token.address); - dispatch('buyMax', ''); - dispatch('buyMin', ''); - dispatch('sellMax', ''); - dispatch('sellMin', ''); - dispatch('buyBudget', ''); - dispatch('sellBudget', ''); - setInitBuyRange(true); - setInitSellRange(true); + navigate({ + search: { baseToken: token.address, quoteToken }, + params: {}, + }); }, excludedTokens: [quote?.address ?? ''], isBaseToken: true, @@ -104,7 +82,12 @@ export const SimInputTokenSelection: FC = ({
); } diff --git a/src/components/simulator/result/SimResultChartHeader.tsx b/src/components/simulator/result/SimResultChartHeader.tsx index 13a268c1a..90332e65c 100644 --- a/src/components/simulator/result/SimResultChartHeader.tsx +++ b/src/components/simulator/result/SimResultChartHeader.tsx @@ -67,9 +67,10 @@ export const SimResultChartHeader = ({ presets={datePickerPresets} options={{ disabled: datePickerDisabledDays }} required + disabled={simulationType === 'overlapping'} /> ); - }, [endUnix, onDatePickerConfirm, startUnix]); + }, [endUnix, onDatePickerConfirm, simulationType, startUnix]); return (
diff --git a/src/components/simulator/result/SimResultSummary.tsx b/src/components/simulator/result/SimResultSummary.tsx index 421040f3d..d207fc27b 100644 --- a/src/components/simulator/result/SimResultSummary.tsx +++ b/src/components/simulator/result/SimResultSummary.tsx @@ -69,8 +69,7 @@ export const SimResultSummary = ({ search={{ base: state.baseToken.address, quote: state.quoteToken.address, - strategyType: - strategyType === 'recurring' ? 'recurring' : undefined, + strategyType: 'recurring', strategySettings: strategyType === 'recurring' ? 'range' : 'overlapping', buyMin: state.buy.min, diff --git a/src/components/simulator/result/SimulatorProvider.tsx b/src/components/simulator/result/SimulatorProvider.tsx index acc7abba3..1adab6b99 100644 --- a/src/components/simulator/result/SimulatorProvider.tsx +++ b/src/components/simulator/result/SimulatorProvider.tsx @@ -4,8 +4,7 @@ import { } from 'hooks/useStrategyInput'; import { useTokens } from 'hooks/useTokens'; import { SimulatorData, SimulatorReturn, useGetSimulator } from 'libs/queries'; -import { useSearch } from 'libs/routing'; -import { StrategyInputSearch } from 'libs/routing/routes/sim'; +import { SimulatorResultSearch, useSearch } from 'libs/routing'; import { isNil } from 'lodash'; import { createContext, @@ -22,7 +21,7 @@ import { wait } from 'utils/helpers'; type SimulationStatus = 'running' | 'paused' | 'ended' | 'idle'; interface SimulatorProviderCTX extends Partial { - search: StrategyInputSearch; + search: SimulatorResultSearch; state: StrategyInputValues; status: SimulationStatus; start: () => void; diff --git a/src/components/strategies/common/BudgetInput.tsx b/src/components/strategies/common/BudgetInput.tsx index 3ddb9b231..9153b3ccf 100644 --- a/src/components/strategies/common/BudgetInput.tsx +++ b/src/components/strategies/common/BudgetInput.tsx @@ -1,5 +1,5 @@ +import { Tooltip } from 'components/common/tooltip/Tooltip'; import { FC, ReactNode, useId } from 'react'; -import { OrderCreate } from '../create/useOrder'; import { TokenInputField } from 'components/common/TokenInputField/TokenInputField'; import { ReactComponent as IconWarning } from 'assets/icons/warning.svg'; import { Token } from 'libs/tokens'; @@ -8,36 +8,54 @@ import { UseQueryResult } from '@tanstack/react-query'; interface Props { id?: string; children?: ReactNode; - order: OrderCreate; + value: string; + error?: string; token: Token; - query: UseQueryResult; + query?: UseQueryResult; onChange: (value: string) => void; disabled?: boolean; withoutWallet?: boolean; 'data-testid'?: string; + title: string; + titleTooltip: string; } export const BudgetInput: FC = (props) => { - const { id, order, token, query, children, onChange } = props; + const { + id, + value, + error, + token, + query, + children, + onChange, + title, + titleTooltip, + } = props; const inputId = useId(); - const balance = query.data ?? '0'; + const balance = query?.data ?? '0'; return (
+ - {!!order.budgetError && ( + {!!error && ( = (props) => { className="flex items-center gap-10 font-mono text-12 text-error" > - {order.budgetError} + {error} )} {children} diff --git a/src/components/strategies/create/BuySellBlock/InputLimit.tsx b/src/components/strategies/create/BuySellBlock/InputLimit.tsx index f0d4dbcb0..c9b22bd40 100644 --- a/src/components/strategies/create/BuySellBlock/InputLimit.tsx +++ b/src/components/strategies/create/BuySellBlock/InputLimit.tsx @@ -19,7 +19,7 @@ type InputLimitProps = { warnings?: string[]; setPriceError?: (error: string) => void; buy?: boolean; - marketPricePercentage: MarketPricePercentage; + marketPricePercentage?: MarketPricePercentage; ignoreMarketPriceWarning?: boolean; isOrdersReversed: boolean; }; @@ -115,11 +115,13 @@ export const InputLimit: FC = ({ {fiatAsString} - + {marketPricePercentage && ( + + )}

{error ? ( diff --git a/src/components/strategies/create/BuySellBlock/InputRange.tsx b/src/components/strategies/create/BuySellBlock/InputRange.tsx index 8d572c989..18a89059d 100644 --- a/src/components/strategies/create/BuySellBlock/InputRange.tsx +++ b/src/components/strategies/create/BuySellBlock/InputRange.tsx @@ -1,9 +1,9 @@ -import { ChangeEvent, FocusEvent, FC, useEffect, useId } from 'react'; -import { carbonEvents } from 'services/events'; +import { ChangeEvent, FocusEvent, FC, useId, useEffect } from 'react'; import { Token } from 'libs/tokens'; import { useFiatCurrency } from 'hooks/useFiatCurrency'; import { Tooltip } from 'components/common/tooltip/Tooltip'; import { MarketPriceIndication } from 'components/strategies/marketPriceIndication'; +import { carbonEvents } from 'services/events'; import { formatNumber, sanitizeNumber } from 'utils/helpers'; import { decimalNumberValidationRegex } from 'utils/inputsValidations'; import { MarketPricePercentage } from 'components/strategies/marketPriceIndication/useMarketIndication'; @@ -23,7 +23,7 @@ type InputRangeProps = { error?: string; warnings?: string[]; setRangeError: (error: string) => void; - marketPricePercentages: MarketPricePercentage; + marketPricePercentages?: MarketPricePercentage; ignoreMarketPriceWarning?: boolean; isOrdersReversed?: boolean; }; @@ -70,7 +70,7 @@ export const InputRange: FC = ({ message: errorMessage, }); } - }, [min, max, setRangeError, buy, isOrdersReversed]); + }, [min, max, setRangeError, isOrdersReversed, buy]); const handleChangeMin = (e: ChangeEvent) => { setMin(sanitizeNumber(e.target.value)); @@ -141,12 +141,14 @@ export const InputRange: FC = ({ {getFiatAsString(min)} - + {marketPricePercentages && ( + + )}

= ({

{getFiatAsString(max)}

- + {marketPricePercentages && ( + + )}
diff --git a/src/components/strategies/create/overlapping/CreateOverlappingStrategy.tsx b/src/components/strategies/create/overlapping/CreateOverlappingStrategy.tsx index 6db0f5c72..be11d586b 100644 --- a/src/components/strategies/create/overlapping/CreateOverlappingStrategy.tsx +++ b/src/components/strategies/create/overlapping/CreateOverlappingStrategy.tsx @@ -280,8 +280,8 @@ export const CreateOverlappingStrategy: FC = ( /> = (props) => { const budgetTooSmall = isOverlappingBudgetTooSmall(order0, order1); const buyBudgetId = useId(); const sellBudgetId = useId(); + const { user } = useWeb3(); - const checkInsufficientBalance = (balance: string, order: OrderCreate) => { - if (new SafeDecimal(balance).lt(order.budget)) { + const checkInsufficientBalance = ( + balance: string, + order: OrderCreate, + user?: string + ) => { + if (new SafeDecimal(balance).lt(order.budget) && !!user) { order.setBudgetError('Insufficient balance'); } else { order.setBudgetError(''); @@ -51,16 +57,16 @@ export const CreateOverlappingStrategyBudget: FC = (props) => { // Check for error when buy budget changes useEffect(() => { const balance = token1BalanceQuery.data ?? '0'; - checkInsufficientBalance(balance, order0); + checkInsufficientBalance(balance, order0, user); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [order0.budget, token1BalanceQuery.data]); + }, [order0.budget, token1BalanceQuery.data, user]); // Check for error when sell budget changes useEffect(() => { const balance = token0BalanceQuery.data ?? '0'; - checkInsufficientBalance(balance, order1); + checkInsufficientBalance(balance, order1, user); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [order1.budget, token0BalanceQuery.data]); + }, [order1.budget, token0BalanceQuery.data, user]); const onBuyBudgetChange = (value: string) => { order0.setBudget(value); @@ -76,26 +82,33 @@ export const CreateOverlappingStrategyBudget: FC = (props) => { if (!quote || !base) return <>; return ( <> - - {minAboveMarket && } {maxBelowMarket && } + + {minAboveMarket && } + {budgetTooSmall && ( ) => { e.preventDefault(); if (type === 'withdraw') { - depositOrWithdrawFunds(); + const withdrawAll = + (order0.budget || '0') === strategy.order0.balance && + (order1.budget || '0') === strategy.order1.balance; + if (withdrawAll) { + openWithdrawModal(); + } else { + depositOrWithdrawFunds(); + } } else { if (approval.approvalRequired) { openModal('txConfirm', { @@ -138,6 +151,21 @@ export const EditStrategyBudgetContent = ({ return MarginalPriceOptions.reset; }; + const { deleteStrategy } = useDeleteStrategy(); + + const openWithdrawModal = () => { + openModal('withdrawOrDelete', { + onWithdraw: depositOrWithdrawFunds, + onDelete: () => + deleteStrategy(strategy, setIsProcessing, () => + carbonEvents.strategyEdit.strategyDelete({ + strategyId: strategy.id, + ...strategyEventData, + }) + ), + }); + }; + const depositOrWithdrawFunds = () => { const buyOption = getMarginalOption(order0); const sellOption = getMarginalOption(order1); diff --git a/src/components/strategies/edit/EditStrategyHeader.tsx b/src/components/strategies/edit/EditStrategyHeader.tsx index 44d0320cf..ad1da678e 100644 --- a/src/components/strategies/edit/EditStrategyHeader.tsx +++ b/src/components/strategies/edit/EditStrategyHeader.tsx @@ -19,8 +19,8 @@ export const EditStrategyHeader = ({ const titleByType: { [key in EditTypes]: string } = { renew: 'Renew Strategy', editPrices: 'Edit Prices', - deposit: 'Deposit Budget', - withdraw: 'Withdraw Budget', + deposit: 'Deposit Budgets', + withdraw: 'Withdraw Budgets', }; return ( diff --git a/src/components/strategies/edit/overlapping/DepositOverlappingStrategy.tsx b/src/components/strategies/edit/overlapping/DepositOverlappingStrategy.tsx index 65e1d4d40..b58618d0b 100644 --- a/src/components/strategies/edit/overlapping/DepositOverlappingStrategy.tsx +++ b/src/components/strategies/edit/overlapping/DepositOverlappingStrategy.tsx @@ -160,16 +160,35 @@ export const DepositOverlappingStrategy: FC = (props) => {
-

Deposit Budget

+

Deposit Budgets

+ + + + @@ -180,19 +199,6 @@ export const DepositOverlappingStrategy: FC = (props) => { /> - - - - {budgetTooSmall && ( = (props) => {

Edit Budget

- - - - @@ -194,6 +179,28 @@ export const EditOverlappingStrategyBudget: FC = (props) => { {maxBelowMarket && } + + + + + {!minAboveMarket && !maxBelowMarket && (

The required 2nd budget will be calculated to maintain overlapping @@ -226,7 +233,8 @@ const Explanation: FC<{ base?: Token; buy?: boolean }> = ({ base, buy }) => {

The market price is outside the ranges you set for  {buy ? 'buying' : 'selling'}  - {base?.symbol}. Budget for buying {base?.symbol} is not required.  + {base?.symbol}. Budget for {buy ? 'buying' : 'selling'} {base?.symbol} is + not required.  = (props) => { /> = (props) => { const { strategy, order0, order1 } = props; const { base, quote } = strategy; - const buyId = useId(); - const sellId = useId(); const tokenBaseBalanceQuery = useGetTokenBalance(base); const tokenQuoteBalanceQuery = useGetTokenBalance(quote); @@ -163,17 +161,40 @@ export const WithdrawOverlappingStrategy: FC = (props) => {

-

Withdraw Budget

+

Withdraw Budgets

+ + + + = (props) => { buy /> - - - {budgetTooSmall && ( = (props) => { htmlFor={`${buyBudgetId} ${sellBudgetId}`} /> )} -
+ {!withdrawAll && ( +
+ +

+ Price range and liquidity spread remain unchanged.  + + Learn More + + +

+
+ )}
); diff --git a/src/components/strategies/overlapping/OverlappingStrategySpread.tsx b/src/components/strategies/overlapping/OverlappingStrategySpread.tsx index 731b6429e..ee4d26a92 100644 --- a/src/components/strategies/overlapping/OverlappingStrategySpread.tsx +++ b/src/components/strategies/overlapping/OverlappingStrategySpread.tsx @@ -2,15 +2,12 @@ import { useRef, FC, KeyboardEvent, - Dispatch, - SetStateAction, useState, FocusEvent, ChangeEvent, } from 'react'; import { ReactComponent as IconWarning } from 'assets/icons/warning.svg'; import { cn, formatNumber, sanitizeNumber } from 'utils/helpers'; -import { OrderCreate } from '../create/useOrder'; import { getMaxSpread } from 'components/strategies/overlapping/utils'; import styles from './OverlappingStrategySpread.module.css'; @@ -19,9 +16,9 @@ interface Props { defaultValue: number; options: number[]; spread: number; - order0: OrderCreate; - order1: OrderCreate; - setSpread: Dispatch>; + buyMin: number; + sellMax: number; + setSpread: (value: number) => void; } const getWarning = (maxSpread: number) => { @@ -31,13 +28,10 @@ const getWarning = (maxSpread: number) => { const round = (value: number) => Math.round(value * 100) / 100; export const OverlappingStrategySpread: FC = (props) => { - const { defaultValue, options, spread, setSpread } = props; + const { defaultValue, options, spread, setSpread, buyMin, sellMax } = props; const root = useRef(null); const inOptions = options.includes(spread); const hasError = spread <= 0 || spread > 100; - const { order0, order1 } = props; - const buyMin = Number(order0.min); - const sellMax = Number(order1.max); const [warning, setWarning] = useState(''); const selectSpread = (value: number) => { diff --git a/src/components/strategies/overview/strategyBlock/StrategyBlockManage.tsx b/src/components/strategies/overview/strategyBlock/StrategyBlockManage.tsx index de692f0d6..f3a81cd2f 100644 --- a/src/components/strategies/overview/strategyBlock/StrategyBlockManage.tsx +++ b/src/components/strategies/overview/strategyBlock/StrategyBlockManage.tsx @@ -85,6 +85,48 @@ export const StrategyBlockManage: FC = (props) => { }, }); } + const isDisposable = + +strategy.order0.startRate === 0 || + +strategy.order0.endRate === 0 || + +strategy.order1.startRate === 0 || + +strategy.order1.endRate === 0; + + if (!isDisposable) { + items.push({ + id: 'simulate', + name: 'Simulate Strategy', + action: () => { + if (isOverlapping) { + navigate({ + to: '/simulate/overlapping', + search: { + baseToken: strategy.base.address, + quoteToken: strategy.quote.address, + buyMin: strategy.order0.startRate, + sellMax: strategy.order1.endRate, + }, + }); + } else { + navigate({ + to: '/simulate/recurring', + search: { + baseToken: strategy.base.address, + quoteToken: strategy.quote.address, + buyMin: strategy.order0.startRate, + buyMax: strategy.order0.endRate, + buyBudget: strategy.order0.balance, + buyIsRange: strategy.order0.endRate !== strategy.order0.startRate, + sellMin: strategy.order1.startRate, + sellMax: strategy.order1.endRate, + sellBudget: strategy.order1.balance, + sellIsRange: + strategy.order1.endRate !== strategy.order1.startRate, + }, + }); + } + }, + }); + } if (isExplorer) { items.push({ diff --git a/src/components/strategies/overview/strategyBlock/utils.ts b/src/components/strategies/overview/strategyBlock/utils.ts index 661f4209e..1c7db7a14 100644 --- a/src/components/strategies/overview/strategyBlock/utils.ts +++ b/src/components/strategies/overview/strategyBlock/utils.ts @@ -44,6 +44,7 @@ const tooltipTextByStrategyEditOptionsId = { renewStrategy: 'Renew an inactive strategy', manageNotifications: 'Get notified when someone trades against your strategy', walletOwner: 'View all strategies from the owner of this strategy', + simulate: 'Run a simulation on this strategy', }; export const getTooltipTextByStrategyEditOptionsId = ( diff --git a/src/hooks/useSimulatorOverlappingInput.ts b/src/hooks/useSimulatorOverlappingInput.ts new file mode 100644 index 000000000..b65308c1f --- /dev/null +++ b/src/hooks/useSimulatorOverlappingInput.ts @@ -0,0 +1,160 @@ +import { useNavigate } from '@tanstack/react-router'; +import { useDebouncedValue } from 'hooks/useDebouncedValue'; +import { StrategyInputOrder } from 'hooks/useStrategyInput'; +import { ChartPrices } from 'components/simulator/input/d3Chart/D3ChartCandlesticks'; +import { useTokens } from 'hooks/useTokens'; +import { SimulatorInputOverlappingSearch } from 'libs/routing/routes/sim'; +import { Token } from 'libs/tokens'; +import { useCallback, useMemo, useState } from 'react'; + +export interface InternalSimulatorOverlappingInput + extends SimulatorInputOverlappingSearch { + buyMax?: string; + sellMin?: string; + buyBudget?: string; + sellBudget?: string; + sellBudgetError?: string; + buyBudgetError?: string; + buyPriceError?: string; + sellPriceError?: string; +} + +export type SimulatorOverlappingInputDispatch = < + T extends InternalSimulatorOverlappingInput, + K extends keyof T +>( + key: K, + value: T[K], + setBounds?: boolean +) => void; + +interface Props { + searchState: SimulatorInputOverlappingSearch; +} + +export interface SimulatorInputOverlappingValues { + baseToken?: Token; + quoteToken?: Token; + buy: Omit; + sell: Omit; + start?: string; + end?: string; + spread: string; +} + +export const useSimulatorOverlappingInput = ({ searchState }: Props) => { + const { getTokenById } = useTokens(); + const navigate = useNavigate(); + const [_state, setState] = + useState(searchState); + + const baseToken = useMemo( + () => getTokenById(_state.baseToken), + [_state.baseToken, getTokenById] + ); + const quoteToken = useMemo( + () => getTokenById(_state.quoteToken), + [_state.quoteToken, getTokenById] + ); + + const state = buildStrategyInputState(_state, baseToken, quoteToken); + + const setSearch = useCallback( + (search: InternalSimulatorOverlappingInput) => { + const { + buyMax, + sellMin, + buyBudget, + sellBudget, + buyBudgetError, + sellBudgetError, + buyPriceError, + sellPriceError, + ...newSearch + } = search; + + void navigate({ + search: newSearch, + params: {}, + replace: true, + resetScroll: false, + }); + }, + [navigate] + ); + + useDebouncedValue(_state, 300, { cb: setSearch }); + + const [bounds, setBounds] = useState({ + buy: { min: searchState.buyMin || '', max: '' }, + sell: { min: '', max: searchState.sellMax || '' }, + }); + + const dispatch: SimulatorOverlappingInputDispatch = useCallback( + (key, value, updateBounds = true) => { + setState((state) => ({ ...state, [key]: value })); + + if (!updateBounds) { + return; + } + + switch (key) { + case 'buyMin': + setBounds((bounds) => ({ + ...bounds, + buy: { ...bounds.buy, min: value as string }, + })); + break; + case 'buyMax': + setBounds((bounds) => ({ + ...bounds, + buy: { ...bounds.buy, max: value as string }, + })); + break; + case 'sellMin': + setBounds((bounds) => ({ + ...bounds, + sell: { ...bounds.sell, min: value as string }, + })); + break; + case 'sellMax': + setBounds((bounds) => ({ + ...bounds, + sell: { ...bounds.sell, max: value as string }, + })); + break; + } + }, + [] + ); + + return { dispatch, state, bounds, searchState }; +}; + +export const buildStrategyInputState = ( + state: InternalSimulatorOverlappingInput, + baseToken?: Token, + quoteToken?: Token +): SimulatorInputOverlappingValues => { + return { + baseToken, + quoteToken, + buy: { + min: state.buyMin || '', + max: state.buyMax || '', + budget: state.buyBudget || '', + budgetError: state.buyBudgetError, + priceError: state.buyPriceError, + }, + sell: { + min: state.sellMin || '', + max: state.sellMax || '', + budget: state.sellBudget || '', + budgetError: state.sellBudgetError, + priceError: state.sellPriceError, + }, + start: state.start || undefined, + end: state.end || undefined, + spread: state.spread || '', + }; +}; diff --git a/src/hooks/useStrategyInput.ts b/src/hooks/useStrategyInput.ts index ed64cf472..1772b3b18 100644 --- a/src/hooks/useStrategyInput.ts +++ b/src/hooks/useStrategyInput.ts @@ -4,7 +4,6 @@ import { useTokens } from 'hooks/useTokens'; import { StrategyInputSearch } from 'libs/routing/routes/sim'; import { Token } from 'libs/tokens'; import { useCallback, useMemo, useState } from 'react'; -import { stringToBoolean } from 'utils/helpers'; export interface InternalStrategyInput extends StrategyInputSearch { sellBudgetError?: string; @@ -37,6 +36,7 @@ export interface StrategyInputValues { sell: StrategyInputOrder; start?: string; end?: string; + overlappingSpread?: string; } interface Props { @@ -46,11 +46,7 @@ interface Props { export const useStrategyInput = ({ searchState }: Props) => { const { getTokenById } = useTokens(); const navigate = useNavigate(); - const [_state, setState] = useState({ - ...searchState, - sellIsRange: stringToBoolean(String(searchState.sellIsRange), true), - buyIsRange: stringToBoolean(String(searchState.buyIsRange), true), - }); + const [_state, setState] = useState(searchState); const baseToken = useMemo( () => getTokenById(_state.baseToken), diff --git a/src/libs/modals/modals/ModalWithdrawOrDelete.tsx b/src/libs/modals/modals/ModalWithdrawOrDelete.tsx new file mode 100644 index 000000000..a79363b3c --- /dev/null +++ b/src/libs/modals/modals/ModalWithdrawOrDelete.tsx @@ -0,0 +1,51 @@ +import { useModal } from 'hooks/useModal'; +import { ModalFC } from 'libs/modals/modals.types'; +import { Button } from 'components/common/button'; +import { IconTitleText } from 'components/common/iconTitleText/IconTitleText'; +import { ModalOrMobileSheet } from 'libs/modals/ModalOrMobileSheet'; +import { ReactComponent as IconDelete } from 'assets/icons/delete.svg'; + +export type ModalWithdrawOrDeleteData = { + onDelete: () => void; + onWithdraw: () => void; +}; + +export const ModalWithdrawOrDelete: ModalFC = ({ + id, + data: { onDelete, onWithdraw }, +}) => { + const { closeModal } = useModal(); + + return ( + +
+ } + title="This strategy will become inactive once the budget is removed" + text="Delete this strategy to keep things tidy" + /> +
+ + +
+ ); +}; diff --git a/src/libs/modals/modals/index.ts b/src/libs/modals/modals/index.ts index 1a6e921f0..23969c2b3 100644 --- a/src/libs/modals/modals/index.ts +++ b/src/libs/modals/modals/index.ts @@ -1,3 +1,7 @@ +import { + ModalWithdrawOrDelete, + ModalWithdrawOrDeleteData, +} from 'libs/modals/modals/ModalWithdrawOrDelete'; import { ModalWallet } from 'libs/modals/modals/WalletModal/ModalWallet'; import { ModalTokenList, @@ -71,6 +75,7 @@ export interface ModalSchema { confirmWithdrawStrategy: ModalConfirmWithdrawData; confirmDeleteStrategy: ModalConfirmDeleteData; simulatorDisclaimer: ModalSimulatorDisclaimerData; + withdrawOrDelete: ModalWithdrawOrDeleteData; } // Step 2: Create component in modals/modals folder @@ -93,4 +98,5 @@ export const MODAL_COMPONENTS: TModals = { confirmWithdrawStrategy: (props) => ModalConfirmWithdraw(props), confirmDeleteStrategy: (props) => ModalConfirmDelete(props), simulatorDisclaimer: (props) => ModalSimulatorDisclaimer(props), + withdrawOrDelete: (props) => ModalWithdrawOrDelete(props), }; diff --git a/src/libs/queries/extApi/simulator.ts b/src/libs/queries/extApi/simulator.ts index c7b84deb7..5a1c2f930 100644 --- a/src/libs/queries/extApi/simulator.ts +++ b/src/libs/queries/extApi/simulator.ts @@ -56,45 +56,70 @@ export type SimulatorReturn = { gains?: number; }; +export interface SimulatorDataNew { + date: number; + price: string; + sell: string; + buy: string; + portfolioOverHodlInPercent: string; + portfolioValueInQuote: string; + hodlValueInQuote: string; + baseBalance: string; + basePortion: string; + quoteBalance: string; + quotePortion: string; +} + +interface SimulatorBoundsNew { + sellMin: string; + sellMax: string; + buyMin: string; + buyMax: string; +} + +export interface SimulatorReturnNew { + data: Array; + bounds: SimulatorBoundsNew; + roiInPercent: string; + gainsInQuote: string; +} + export type SimulatorAPIParams = Omit< SimulatorResultSearch, - 'buyIsRange' | 'sellIsRange' + 'buyIsRange' | 'sellIsRange' | 'type' | 'overlappingSpread' >; -export const useGetSimulator = (params: SimulatorAPIParams) => { +export const useGetSimulator = (search: SimulatorResultSearch) => { return useQuery( - QueryKey.simulator(params), + QueryKey.simulator(search), async () => { try { + const { buyIsRange, sellIsRange, type, spread, ...params } = search; const res = await carbonApi.getSimulator(params); + // TODO cleanup schema const data: SimulatorReturn = { - data: res.dates.map((d, i) => ({ - date: d, - price: Number(res.prices[i]), - ask: Number(res.ask[i]), - bid: Number(res.bid[i]), - balanceRISK: Number(res.RISK.balance[i]), - portionRISK: Number(res.portfolio_risk[i]), - balanceCASH: Number(res.CASH.balance[i]), - portionCASH: Number(res.portfolio_cash[i]), - portfolioValue: Number(res.portfolio_value[i]), - hodlValue: Number(res.hodl_value[i]), - portfolioOverHodl: Number(res.portfolio_over_hodl[i]), + data: res.data.map((x) => ({ + date: x.date, + price: Number(x.price), + ask: Number(x.sell), + bid: Number(x.buy), + balanceRISK: Number(x.baseBalance), + portionRISK: Number(x.basePortion), + balanceCASH: Number(x.quoteBalance), + portionCASH: Number(x.quotePortion), + portfolioValue: Number(x.portfolioValueInQuote), + hodlValue: Number(x.hodlValueInQuote), + portfolioOverHodl: Number(x.portfolioOverHodlInPercent), })), - roi: Number( - res.portfolio_over_hodl[res.portfolio_over_hodl.length - 1] - ), - gains: Number( - Number(res.portfolio_value[res.portfolio_value.length - 1]) - - Number(res.hodl_value[res.hodl_value.length - 1]) - ), bounds: { - askMax: Number(res.max_ask), - askMin: Number(res.min_ask), - bidMax: Number(res.max_bid), - bidMin: Number(res.min_bid), + askMax: Number(res.bounds.sellMax), + askMin: Number(res.bounds.sellMin), + bidMax: Number(res.bounds.buyMax), + bidMin: Number(res.bounds.buyMin), }, + roi: Number(res.roiInPercent), + gains: Number(res.gainsInQuote), }; return data; diff --git a/src/libs/queries/queryKey.ts b/src/libs/queries/queryKey.ts index 11d93131c..022cdc401 100644 --- a/src/libs/queries/queryKey.ts +++ b/src/libs/queries/queryKey.ts @@ -1,6 +1,6 @@ import { MatchActionBNStr, TokenPair } from '@bancor/carbon-sdk'; -import { SimulatorAPIParams } from 'libs/queries/extApi/simulator'; import { TokenPriceHistorySearch } from 'libs/queries/extApi/tokenPrice'; +import { SimulatorResultSearch } from 'libs/routing'; import { buildTokenPairKey } from 'utils/helpers'; import { QueryActivityParams } from './extApi/activity'; @@ -16,7 +16,7 @@ export namespace QueryKey { ]; export const roi = () => [...extAPI, 'roi']; - export const simulator = (params: SimulatorAPIParams) => [ + export const simulator = (params: SimulatorResultSearch) => [ ...extAPI, 'simulator', params, diff --git a/src/libs/routing/externalLinks.ts b/src/libs/routing/externalLinks.ts index fabe20005..06fcc60ee 100644 --- a/src/libs/routing/externalLinks.ts +++ b/src/libs/routing/externalLinks.ts @@ -19,4 +19,6 @@ export const externalLinks = { 'https://home.treasury.gov/policy-issues/financial-sanctions/recent-actions/20220808', terms: config.appUrl + '/terms', privacy: config.appUrl + '/privacy', + whatIsOverlapping: + 'https://faq.carbondefi.xyz/what-is-an-overlapping-strategy#overlapping-budget-dynamics', }; diff --git a/src/libs/routing/router.tsx b/src/libs/routing/router.tsx index 19f9f11c6..cbdfb7790 100644 --- a/src/libs/routing/router.tsx +++ b/src/libs/routing/router.tsx @@ -1,18 +1,10 @@ import { Router } from '@tanstack/react-router'; import { routeTree } from 'libs/routing/routes'; +import { parseSearchWith } from 'libs/routing/utils'; export const router = new Router({ routeTree, - parseSearch: (searchStr) => { - const searchParams = new URLSearchParams(searchStr); - return Object.fromEntries(searchParams.entries()); - }, - stringifySearch: (search) => { - const searchParams = new URLSearchParams(); - for (const key in search) searchParams.set(key, search[key]); - const searchStr = searchParams.toString(); - return searchStr ? `?${searchStr}` : ''; - }, + parseSearch: parseSearchWith(JSON.parse), }); declare module '@tanstack/react-router' { diff --git a/src/libs/routing/routes/index.ts b/src/libs/routing/routes/index.ts index 0520960b6..f910614ef 100644 --- a/src/libs/routing/routes/index.ts +++ b/src/libs/routing/routes/index.ts @@ -23,8 +23,9 @@ import { } from 'libs/routing/routes/myStrategies'; import { rootRoute } from 'libs/routing/routes/root'; import { - simulatorInputRoute, - simulatorRedirect, + simulatorInputOverlappingRoute, + simulatorInputRecurringRoute, + simulatorInputRootRoute, simulatorResultRoute, simulatorRootRoute, } from 'libs/routing/routes/sim'; @@ -67,8 +68,10 @@ export const routeTree = rootRoute.addChildren([ strategyActivityPage, ]), simulatorRootRoute.addChildren([ - simulatorRedirect, - simulatorInputRoute, + simulatorInputRootRoute.addChildren([ + simulatorInputRecurringRoute, + simulatorInputOverlappingRoute, + ]), simulatorResultRoute, ]), ]); diff --git a/src/libs/routing/routes/sim.tsx b/src/libs/routing/routes/sim.tsx index c91ca818a..1faf93cb0 100644 --- a/src/libs/routing/routes/sim.tsx +++ b/src/libs/routing/routes/sim.tsx @@ -1,11 +1,14 @@ import { redirect, Route } from '@tanstack/react-router'; import { SimulatorProvider } from 'components/simulator/result/SimulatorProvider'; +import { endOfDay, getUnixTime, startOfDay, sub } from 'date-fns'; import { rootRoute } from 'libs/routing/routes/root'; import { validAddress, validBoolean, validNumber } from 'libs/routing/utils'; -import { defaultEnd, defaultStart, SimulatorPage } from 'pages/simulator'; +import { SimulatorPage } from 'pages/simulator'; +import { SimulatorInputOverlappingPage } from 'pages/simulator/overlapping'; +import { SimulatorInputRecurringPage } from 'pages/simulator/recurring'; import { SimulatorResultPage } from 'pages/simulator/result'; import { config } from 'services/web3/config'; -import { roundSearchParam, stringToBoolean } from 'utils/helpers'; +import { roundSearchParam } from 'utils/helpers'; import * as v from 'valibot'; export const simulatorRootRoute = new Route({ @@ -13,48 +16,34 @@ export const simulatorRootRoute = new Route({ path: '/simulate', }); -export interface StrategyInputSearch { +export interface StrategyInputBase { baseToken?: string; quoteToken?: string; - sellBudget?: string; - sellMax?: string; - sellMin?: string; - sellIsRange?: boolean; - buyMax?: string; - buyMin?: string; - buyBudget?: string; - buyIsRange?: boolean; start?: string; end?: string; } -export const simulatorRedirect = new Route({ +export const simulatorInputRootRoute = new Route({ getParentRoute: () => simulatorRootRoute, path: '/', - beforeLoad: () => { - redirect({ - to: '/simulate/$simulationType', - params: { simulationType: 'recurring' }, - throw: true, - replace: true, - }); - }, - component: SimulatorPage, -}); - -export type SimulatorType = 'recurring' | 'overlapping'; - -export const simulatorInputRoute = new Route({ - getParentRoute: () => simulatorRootRoute, - path: '$simulationType', component: SimulatorPage, - parseParams: (params: Record) => { - return { simulationType: params.simulationType as SimulatorType }; + beforeLoad: ({ location }) => { + if (location.pathname === '/simulate' || location.pathname === '/simulate/') + redirect({ + to: '/simulate/recurring', + throw: true, + replace: true, + }); }, - validateSearch: (search: Record): StrategyInputSearch => { + validateSearch: (search: Record): StrategyInputBase => { const start = - Number(search.start) > 0 ? search.start : defaultStart().toString(); - const end = Number(search.end) > 0 ? search.end : defaultEnd().toString(); + Number(search.start) > 0 + ? (search.start as string) + : getUnixTime(startOfDay(sub(new Date(), { days: 364 }))).toString(); + const end = + Number(search.end) > 0 + ? (search.end as string) + : getUnixTime(endOfDay(new Date())).toString(); if (Number(start) >= Number(end)) { throw new Error('Invalid date range'); @@ -66,6 +55,34 @@ export const simulatorInputRoute = new Route({ const quoteToken = v.is(validAddress, search.quoteToken) ? search.quoteToken : config.tokens.USDC; + + return { + baseToken, + quoteToken, + start, + end, + }; + }, +}); + +export interface StrategyInputSearch extends StrategyInputBase { + sellBudget?: string; + sellMax?: string; + sellMin?: string; + sellIsRange?: boolean; + buyMax?: string; + buyMin?: string; + buyBudget?: string; + buyIsRange?: boolean; +} + +export type SimulatorType = 'recurring' | 'overlapping'; + +export const simulatorInputRecurringRoute = new Route({ + getParentRoute: () => simulatorInputRootRoute, + path: 'recurring', + component: SimulatorInputRecurringPage, + validateSearch: (search: Record): StrategyInputSearch => { const sellMax = v.is(validNumber, search.sellMax) ? roundSearchParam(search.sellMax) : ''; @@ -84,12 +101,12 @@ export const simulatorInputRoute = new Route({ const buyBudget = v.is(validNumber, search.buyBudget) ? roundSearchParam(search.buyBudget) : ''; - const sellIsRange = stringToBoolean(search.sellIsRange, true); - const buyIsRange = stringToBoolean(search.buyIsRange, true); + const sellIsRange = + typeof search.sellIsRange === 'boolean' ? search.sellIsRange : true; + const buyIsRange = + typeof search.buyIsRange === 'boolean' ? search.buyIsRange : true; return { - baseToken, - quoteToken, sellMax: sellIsRange ? sellMax : sellMin, sellMin, sellBudget, @@ -98,13 +115,45 @@ export const simulatorInputRoute = new Route({ buyBudget, sellIsRange, buyIsRange, - start, - end, }; }, }); -export type SimulatorResultSearch = Required; +export interface SimulatorInputOverlappingSearch extends StrategyInputBase { + sellMax?: string; + buyMin?: string; + spread?: string; +} + +export const simulatorInputOverlappingRoute = new Route({ + getParentRoute: () => simulatorInputRootRoute, + path: 'overlapping', + component: SimulatorInputOverlappingPage, + validateSearch: ( + search: Record + ): SimulatorInputOverlappingSearch => { + const sellMax = v.is(validNumber, search.sellMax) + ? roundSearchParam(search.sellMax) + : ''; + const buyMin = v.is(validNumber, search.buyMin) + ? roundSearchParam(search.buyMin) + : ''; + const spread = v.is(validNumber, search.spread) ? search.spread : '1'; + + return { + sellMax, + buyMin, + spread, + }; + }, +}); + +export type SimulatorResultSearch = Required & { + type: SimulatorType; + buyMarginal?: string; + sellMarginal?: string; + spread?: string; +}; export const simulatorResultRoute = new Route({ getParentRoute: () => simulatorRootRoute, @@ -163,11 +212,17 @@ export const simulatorResultRoute = new Route({ sellMax: search.sellMax, sellMin: search.sellMin, sellBudget: search.sellBudget || '0', - sellIsRange: stringToBoolean(search.sellIsRange), + // TODO add validation + sellMarginal: search.sellMarginal || '0', + sellIsRange: search.sellIsRange, buyMax: search.buyMax, buyMin: search.buyMin, + buyMarginal: search.buyMarginal || '0', buyBudget: search.buyBudget || '0', - buyIsRange: stringToBoolean(search.buyIsRange), + buyIsRange: search.buyIsRange, + // TODO add validation + spread: search.spread, + type: search.type as SimulatorType, }; }, // @ts-ignore diff --git a/src/libs/routing/utils.ts b/src/libs/routing/utils.ts index 849dd7317..42210903c 100644 --- a/src/libs/routing/utils.ts +++ b/src/libs/routing/utils.ts @@ -94,8 +94,8 @@ export const validAddress = v.string([ } }), ]); -export const validBoolean = v.string([ - v.custom((value) => value === 'true' || value === 'false'), +export const validBoolean = v.boolean([ + v.custom((value) => value === true || value === false), ]); export const validateSearchParams = ( diff --git a/src/pages/simulator/index.tsx b/src/pages/simulator/index.tsx index 5a2eb0de9..947d2a7fb 100644 --- a/src/pages/simulator/index.tsx +++ b/src/pages/simulator/index.tsx @@ -1,16 +1,11 @@ -import { useNavigate, useParams, useSearch } from '@tanstack/react-router'; -import { SimInputChart } from 'components/simulator/input/SimInputChart'; -import { SimInputOverlapping } from 'components/simulator/input/SimInputOverlapping'; -import { SimInputRecurring } from 'components/simulator/input/SimInputRecurring'; +import { Outlet } from '@tanstack/react-router'; import { SimInputStrategyType } from 'components/simulator/input/SimInputStrategyType'; import { SimInputTokenSelection } from 'components/simulator/input/SimInputTokenSelection'; import { useSimDisclaimer } from 'components/simulator/input/useSimDisclaimer'; import { useBreakpoints } from 'hooks/useBreakpoints'; -import { useSimulatorInput } from 'hooks/useSimulatorInput'; -import { FormEvent, useState } from 'react'; +import { simulatorInputRootRoute } from 'libs/routing/routes/sim'; import { SimulatorMobilePlaceholder } from 'components/simulator/mobile-placeholder'; import { useGetTokenPriceHistory } from 'libs/queries/extApi/tokenPrice'; -import { Button } from 'components/common/button'; import { getUnixTime, subDays } from 'date-fns'; export const defaultStart = () => getUnixTime(subDays(new Date(), 364)); @@ -18,108 +13,30 @@ export const defaultEnd = () => getUnixTime(new Date()); export const SimulatorPage = () => { useSimDisclaimer(); + const searchState = simulatorInputRootRoute.useSearch(); + const { isError } = useGetTokenPriceHistory(searchState); const { aboveBreakpoint } = useBreakpoints(); - const navigate = useNavigate(); - const { simulationType } = useParams({ from: '/simulate/$simulationType' }); - const searchState = useSearch({ - from: '/simulate/$simulationType', - }); - const { dispatch, state, bounds } = useSimulatorInput({ searchState }); - const { data, isLoading, isError } = useGetTokenPriceHistory({ - baseToken: state.baseToken?.address, - quoteToken: state.quoteToken?.address, - start: state.start, - end: state.end, - }); - - const [initBuyRange, setInitBuyRange] = useState(true); - const [initSellRange, setInitSellRange] = useState(true); - - const noBudget = Number(state.buy.budget) + Number(state.sell.budget) <= 0; - const noBudgetText = - !isError && noBudget && 'Please add Sell and/or Buy budgets'; - const loadingText = isLoading && 'Loading price history...'; - const priceError = state.buy.priceError || state.sell.priceError; - - const btnDisabled = isLoading || isError || noBudget || !!priceError; if (!aboveBreakpoint('md')) return ; - const submit = (e: FormEvent) => { - e.preventDefault(); - if (isLoading || isError || noBudget) return; - const start = state.start ?? defaultStart(); - const end = state.end ?? defaultEnd(); - navigate({ - to: '/simulate/result', - search: { - baseToken: state.baseToken?.address || '', - quoteToken: state.quoteToken?.address || '', - buyMin: state.buy.min, - buyMax: state.buy.max, - buyBudget: state.buy.budget, - buyIsRange: state.buy.isRange, - sellMin: state.sell.min, - sellMax: state.sell.max, - sellBudget: state.sell.budget, - sellIsRange: state.sell.isRange, - start: start.toString(), - end: end.toString(), - }, - }); - }; - return ( <>

Simulate Strategy

-
-
- - {!isError && ( - <> - - {simulationType === 'recurring' ? ( - - ) : ( - - )} - - )} - {simulationType === 'recurring' && ( - - )} - - - + {!isError && ( +
+ + + +
+ )}
); diff --git a/src/pages/simulator/overlapping/index.tsx b/src/pages/simulator/overlapping/index.tsx new file mode 100644 index 000000000..eb28e7a39 --- /dev/null +++ b/src/pages/simulator/overlapping/index.tsx @@ -0,0 +1,125 @@ +import { calculateOverlappingPrices } from '@bancor/carbon-sdk/strategy-management'; +import { useNavigate } from '@tanstack/react-router'; +import { Button } from 'components/common/button'; +import { CreateOverlappingStrategy } from 'components/simulator/input/overlapping/CreateOverlappingStrategy'; +import { SimInputChart } from 'components/simulator/input/SimInputChart'; +import { useSimulatorOverlappingInput } from 'hooks/useSimulatorOverlappingInput'; +import { useGetTokenPriceHistory } from 'libs/queries/extApi/tokenPrice'; +import { simulatorInputOverlappingRoute } from 'libs/routing/routes/sim'; +import { defaultEnd, defaultStart } from 'pages/simulator/index'; +import { FormEvent, useEffect } from 'react'; +import { roundSearchParam } from 'utils/helpers'; + +export const SimulatorInputOverlappingPage = () => { + const searchState = simulatorInputOverlappingRoute.useSearch(); + + const { dispatch, state, bounds } = useSimulatorOverlappingInput({ + searchState, + }); + + const { data, isLoading, isError } = useGetTokenPriceHistory({ + baseToken: searchState.baseToken, + quoteToken: searchState.quoteToken, + start: searchState.start, + end: searchState.end, + }); + + useEffect(() => { + if (searchState.sellMax) return; + dispatch('baseToken', searchState.baseToken); + dispatch('quoteToken', searchState.quoteToken); + dispatch('spread', '1'); + dispatch('buyMax', '', true); + dispatch('buyMin', '', true); + dispatch('sellMax', '', true); + dispatch('sellMin', '', true); + dispatch('sellBudget', ''); + dispatch('sellBudgetError', ''); + dispatch('sellPriceError', ''); + dispatch('buyBudget', ''); + dispatch('buyBudgetError', ''); + dispatch('buyPriceError', ''); + }, [ + dispatch, + searchState.baseToken, + searchState.quoteToken, + searchState.sellMax, + ]); + + const noBudget = Number(state.buy.budget) + Number(state.sell.budget) <= 0; + const noBudgetText = + !isError && noBudget && 'Please add Sell and/or Buy budgets'; + const loadingText = isLoading && 'Loading price history...'; + const priceError = state.buy.priceError || state.sell.priceError; + const btnDisabled = isLoading || isError || noBudget || !!priceError; + + const navigate = useNavigate(); + + const submit = (e: FormEvent) => { + e.preventDefault(); + if (isLoading || isError || noBudget) return; + const start = state.start ?? defaultStart(); + const end = state.end ?? defaultEnd(); + + const { buyPriceMarginal, sellPriceMarginal } = calculateOverlappingPrices( + state.buy.min, + state.sell.max, + data[0].open.toString(), + state.spread + ); + + navigate({ + to: '/simulate/result', + search: { + baseToken: state.baseToken?.address || '', + quoteToken: state.quoteToken?.address || '', + buyMin: roundSearchParam(state.buy.min), + buyMax: roundSearchParam(state.buy.max), + buyBudget: roundSearchParam(state.buy.budget), + buyMarginal: roundSearchParam(buyPriceMarginal), + buyIsRange: true, + sellMin: roundSearchParam(state.sell.min), + sellMax: roundSearchParam(state.sell.max), + sellBudget: roundSearchParam(state.sell.budget), + sellMarginal: roundSearchParam(sellPriceMarginal), + sellIsRange: true, + start: start.toString(), + end: end.toString(), + type: 'overlapping', + spread: state.spread, + }, + }); + }; + + return ( + <> +
+ dispatch('spread', v.toString())} + /> + + + + + + ); +}; diff --git a/src/pages/simulator/recurring/index.tsx b/src/pages/simulator/recurring/index.tsx new file mode 100644 index 000000000..776f83546 --- /dev/null +++ b/src/pages/simulator/recurring/index.tsx @@ -0,0 +1,171 @@ +import { useNavigate } from '@tanstack/react-router'; +import { Button } from 'components/common/button'; +import { SimInputChart } from 'components/simulator/input/SimInputChart'; +import { SimInputRecurring } from 'components/simulator/input/SimInputRecurring'; +import { useSimulatorInput } from 'hooks/useSimulatorInput'; +import { + useCompareTokenPrice, + useGetTokenPriceHistory, +} from 'libs/queries/extApi/tokenPrice'; +import { StrategyDirection } from 'libs/routing'; +import { simulatorInputRecurringRoute } from 'libs/routing/routes/sim'; +import { SafeDecimal } from 'libs/safedecimal'; +import { defaultEnd, defaultStart } from 'pages/simulator/index'; +import { FormEvent, useCallback, useEffect, useState } from 'react'; + +export const SimulatorInputRecurringPage = () => { + const searchState = simulatorInputRecurringRoute.useSearch(); + + const { dispatch, state, bounds } = useSimulatorInput({ + searchState, + }); + + const [initBuyRange, setInitBuyRange] = useState(true); + const [initSellRange, setInitSellRange] = useState(true); + const { data, isLoading, isError } = useGetTokenPriceHistory({ + baseToken: searchState.baseToken, + quoteToken: searchState.quoteToken, + start: searchState.start, + end: searchState.end, + }); + const marketPrice = useCompareTokenPrice( + state.baseToken?.address, + state.quoteToken?.address + ); + + const handleDefaultValues = useCallback( + (type: StrategyDirection) => { + const init = type === 'buy' ? initBuyRange : initSellRange; + const setInit = type === 'buy' ? setInitBuyRange : setInitSellRange; + + if (!marketPrice || !init) return; + setInit(false); + + if (!(!state[type].max && !state[type].min)) { + return; + } + + const operation = type === 'buy' ? 'minus' : 'plus'; + + const multiplierMax = type === 'buy' ? 0.1 : 0.2; + const multiplierMin = type === 'buy' ? 0.2 : 0.1; + + const max = new SafeDecimal(marketPrice) + [operation](marketPrice * multiplierMax) + .toFixed(); + + const min = new SafeDecimal(marketPrice) + [operation](marketPrice * multiplierMin) + .toFixed(); + + if (state[type].isRange) { + dispatch(`${type}Max`, max); + dispatch(`${type}Min`, min); + } else { + const value = type === 'buy' ? max : min; + dispatch(`${type}Max`, value); + dispatch(`${type}Min`, value); + } + }, + [ + dispatch, + initBuyRange, + initSellRange, + marketPrice, + setInitBuyRange, + setInitSellRange, + state, + ] + ); + + useEffect(() => { + handleDefaultValues('buy'); + handleDefaultValues('sell'); + }, [handleDefaultValues]); + + useEffect(() => { + if (initBuyRange || initSellRange) return; + dispatch('baseToken', searchState.baseToken); + dispatch('quoteToken', searchState.quoteToken); + dispatch('sellMax', ''); + dispatch('sellMin', ''); + dispatch('sellBudget', ''); + dispatch('sellBudgetError', ''); + dispatch('sellPriceError', ''); + dispatch('sellIsRange', true); + dispatch('buyMax', ''); + dispatch('buyMin', ''); + dispatch('buyBudget', ''); + dispatch('buyBudgetError', ''); + dispatch('buyPriceError', ''); + dispatch('buyIsRange', true); + setInitBuyRange(true); + setInitSellRange(true); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [dispatch, searchState.baseToken, searchState.quoteToken]); + + const noBudget = Number(state.buy.budget) + Number(state.sell.budget) <= 0; + const noBudgetText = + !isError && noBudget && 'Please add Sell and/or Buy budgets'; + const loadingText = isLoading && 'Loading price history...'; + const priceError = state.buy.priceError || state.sell.priceError; + const btnDisabled = isLoading || isError || noBudget || !!priceError; + + const navigate = useNavigate(); + + const submit = (e: FormEvent) => { + e.preventDefault(); + if (isLoading || isError || noBudget) return; + const start = state.start ?? defaultStart(); + const end = state.end ?? defaultEnd(); + + navigate({ + to: '/simulate/result', + search: { + baseToken: state.baseToken?.address || '', + quoteToken: state.quoteToken?.address || '', + buyMin: state.buy.min, + buyMax: state.buy.max, + buyBudget: state.buy.budget, + buyIsRange: state.buy.isRange, + sellMin: state.sell.min, + sellMax: state.sell.max, + sellBudget: state.sell.budget, + sellIsRange: state.sell.isRange, + start: start.toString(), + end: end.toString(), + type: 'recurring', + }, + }); + }; + + return ( + <> +
+ + + + + + + ); +}; diff --git a/src/pages/simulator/result/index.tsx b/src/pages/simulator/result/index.tsx index c7385ac7f..fac1a72f0 100644 --- a/src/pages/simulator/result/index.tsx +++ b/src/pages/simulator/result/index.tsx @@ -9,7 +9,7 @@ import { THREE_SECONDS_IN_MS } from 'utils/time'; export const SimulatorResultPage = () => { const { status, isSuccess, start, ...ctx } = useSimulator(); - const simulationType = 'recurring'; + const simulationType = ctx.search.type; const handleAnimationStart = useCallback(() => { if (!isSuccess || ['running', 'ended', 'paused'].includes(status)) { @@ -27,17 +27,51 @@ export const SimulatorResultPage = () => { return (
- -
- -
- Simulate Strategy - + {simulationType === 'recurring' && ( + +
+ +
+ Simulate Strategy + + )} + {simulationType === 'overlapping' && ( + +
+ +
+ Simulate Strategy + + )}
(endpoint: string, params: Object = {}): Promise => { const api = lsService.getItem('carbonApi') || config.carbonApi; const url = new URL(api + endpoint); for (const [key, value] of Object.entries(params)) { - url.searchParams.set(key, value); + value !== 'undefined' && url.searchParams.set(key, value); } const response = await fetch(url); const result = await response.json(); @@ -80,12 +80,8 @@ const carbonApi = { }, getSimulator: async ( params: SimulatorAPIParams - ): Promise => { - return get('simulate-create-strategy', { - ...params, - baseBudget: params.sellBudget, - quoteBudget: params.buyBudget, - }); + ): Promise => { + return get('simulator/create', params); }, getActivity: async (params: QueryActivityParams) => { return get('activity', params); diff --git a/src/utils/helpers/schema.ts b/src/utils/helpers/schema.ts index a17372cfc..959143da5 100644 --- a/src/utils/helpers/schema.ts +++ b/src/utils/helpers/schema.ts @@ -7,12 +7,12 @@ export const identity = (value: string) => value; export const toBoolean = (fallback: boolean = false) => (value: string = '') => { - return value ? Boolean(value) : fallback; + return value ? value : fallback; }; export const toString = (fallback: string = '') => (value: string = '') => { - return value ? String(value) : fallback; + return value ? value : fallback; }; export const toNumber = (fallback: number = 0) => @@ -22,7 +22,7 @@ export const toNumber = export const toArray = (fallback: string[] = []) => (value: string = '') => { - return value ? value.split(',') : fallback; + return value ? (value as unknown as string[]) : fallback; }; export const toLiteral = (literals: T[], fallback: T) => @@ -32,7 +32,7 @@ export const toLiteral = export const toDate = (fallback?: Date) => (value: string = '') => { - return value ? new Date(value) : fallback; + return value ? value : fallback; }; export type SearchParams = Partial<{ [key in keyof T]: string;