From 0be29ab6707d0999d70ba723fc720cc4c0733632 Mon Sep 17 00:00:00 2001 From: Grandschtroumpf Date: Fri, 10 Jan 2025 11:42:27 +0100 Subject: [PATCH 01/27] setup cart logic --- src/assets/icons/cart.svg | 11 +++ .../core/menu/mainMenu/MainMenuCart.tsx | 70 +++++++++++++++++++ .../core/menu/mainMenu/MainMenuRight.tsx | 2 + src/components/debug/DebugCreateStrategy.tsx | 10 +-- .../strategies/common/form.module.css | 7 ++ .../strategies/create/CreateForm.tsx | 50 ++++++++++++- .../strategies/create/useCreateStrategy.ts | 41 ++++++----- src/libs/queries/sdk/strategy.ts | 10 ++- src/libs/routing/routes/cart.ts | 9 +++ src/libs/routing/routes/index.ts | 2 + src/pages/cart/index.tsx | 17 +++++ src/services/localeStorage/index.ts | 2 + src/utils/managedLocalStorage.ts | 13 +++- 13 files changed, 210 insertions(+), 34 deletions(-) create mode 100644 src/assets/icons/cart.svg create mode 100644 src/components/core/menu/mainMenu/MainMenuCart.tsx create mode 100644 src/libs/routing/routes/cart.ts create mode 100644 src/pages/cart/index.tsx diff --git a/src/assets/icons/cart.svg b/src/assets/icons/cart.svg new file mode 100644 index 000000000..3bca9cbd9 --- /dev/null +++ b/src/assets/icons/cart.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/components/core/menu/mainMenu/MainMenuCart.tsx b/src/components/core/menu/mainMenu/MainMenuCart.tsx new file mode 100644 index 000000000..51c7c9846 --- /dev/null +++ b/src/components/core/menu/mainMenu/MainMenuCart.tsx @@ -0,0 +1,70 @@ +import { Link } from '@tanstack/react-router'; +import { ReactComponent as CartIcon } from 'assets/icons/cart.svg'; +import { useEffect, useState } from 'react'; +import { lsService } from 'services/localeStorage'; +import style from 'components/strategies/common/form.module.css'; + +const getTranslate = (target: HTMLElement, elRect: DOMRect) => { + const { top, height, left, width } = target.getBoundingClientRect(); + const centerX = left + width / 2; + const centerY = top + height / 2; + const radius = elRect.width / 2; + const translateX = centerX - elRect.left - radius; + const translateY = centerY - elRect.top - radius; + return `translate(${translateX}px, ${translateY}px)`; +}; + +const runAnimation = () => { + const source = document.querySelector(`.${style.addCart}`); + const target = document.getElementById('menu-cart-link'); + const el = document.getElementById('animate-cart-indicator'); + if (!source || !target || !el) return; + const currentRect = el.getBoundingClientRect(); + const sourceTranslate = getTranslate(source, currentRect); + const targetTranslate = getTranslate(target, currentRect); + el.animate( + [ + { opacity: 0, transform: `${sourceTranslate} scale(15)` }, + { opacity: 1, transform: sourceTranslate }, + { opacity: 1, transform: targetTranslate }, + { opacity: 0, transform: `${targetTranslate} scale(5)` }, + ], + { + duration: 1000, + easing: 'cubic-bezier(0,.6,1,.4)', + } + ); +}; + +export const MainMenuCart = () => { + const [cart, setCart] = useState(lsService.getItem('cart') ?? []); + useEffect(() => { + const handler = (event: StorageEvent) => { + if (event.key !== lsService.keyFormatter('cart')) return; + const next = JSON.parse(event.newValue ?? '[]'); + if (next.length > cart.length) runAnimation(); + setCart(next); + }; + window.addEventListener('storage', handler); + return () => window.removeEventListener('storage', handler); + }); + + return ( + + + {!!cart.length && ( + + {cart.length} + + )} +
+ + ); +}; diff --git a/src/components/core/menu/mainMenu/MainMenuRight.tsx b/src/components/core/menu/mainMenu/MainMenuRight.tsx index 4536f1462..370afeb5d 100644 --- a/src/components/core/menu/mainMenu/MainMenuRight.tsx +++ b/src/components/core/menu/mainMenu/MainMenuRight.tsx @@ -8,6 +8,7 @@ import { MainMenuRightBurger } from './MainMenuRightBurger'; import { useBurgerMenuItems } from './MainMenuRightBurger/useBurgerMenuItems'; import { MainMenuRightChainSelector } from './MainMenuRightChainSelector'; import { networks } from 'config'; +import { MainMenuCart } from './MainMenuCart'; const TenderlyForkAlert = () => { return IS_TENDERLY_FORK ? ( @@ -24,6 +25,7 @@ export const MainMenuRight: FC = () => { return (
+ {aboveBreakpoint('md') && ( diff --git a/src/components/debug/DebugCreateStrategy.tsx b/src/components/debug/DebugCreateStrategy.tsx index 3d90d2166..6c9a648ec 100644 --- a/src/components/debug/DebugCreateStrategy.tsx +++ b/src/components/debug/DebugCreateStrategy.tsx @@ -119,8 +119,8 @@ export const DebugCreateStrategy = () => { const base = selectedTokens[0]; const quote = selectedTokens[1]; const strategy: CreateStrategyParams = { - base, - quote, + base: base.address, + quote: quote.address, order0: { max: buyMax, min: buyMin, @@ -160,11 +160,7 @@ export const DebugCreateStrategy = () => { await queryClient.invalidateQueries({ queryKey: QueryKey.balance(user, quote.address), }); - console.log( - 'created strategy', - strategy.base.address, - strategy.quote.address - ); + console.log('created strategy', strategy.base, strategy.quote); } catch (e) { console.error( 'create strategy failed for ', diff --git a/src/components/strategies/common/form.module.css b/src/components/strategies/common/form.module.css index 4ff2864d4..98bd9df01 100644 --- a/src/components/strategies/common/form.module.css +++ b/src/components/strategies/common/form.module.css @@ -11,6 +11,13 @@ border: none; } +.form:invalid .add-cart, +.form:has(:global(.error-message)) .add-cart, +.form:has(:global(.loading-message)) .add-cart { + opacity: 0.4; + cursor: not-allowed; +} + .form:has(:global(.error-message)) .approve-warnings, .form:not(:has(:global(.warning-message))) .approve-warnings { display: none; diff --git a/src/components/strategies/create/CreateForm.tsx b/src/components/strategies/create/CreateForm.tsx index e4139c723..5d17b7891 100644 --- a/src/components/strategies/create/CreateForm.tsx +++ b/src/components/strategies/create/CreateForm.tsx @@ -1,15 +1,16 @@ -import { FC, FormEvent, ReactNode, useEffect } from 'react'; +import { FC, FormEvent, MouseEvent, ReactNode, useEffect } from 'react'; import { Token } from 'libs/tokens'; import { createStrategyEvents } from 'services/events/strategyEvents'; -import { useSearch } from '@tanstack/react-router'; +import { useNavigate, useSearch } from '@tanstack/react-router'; import { Button } from 'components/common/button'; -import { useCreateStrategy } from './useCreateStrategy'; +import { toCreateStrategyParams, useCreateStrategy } from './useCreateStrategy'; import { getStatusTextByTxStatus } from '../utils'; import { useModal } from 'hooks/useModal'; import { cn } from 'utils/helpers'; import { useWagmi } from 'libs/wagmi'; import { BaseOrder } from 'components/strategies/common/types'; import { StrategyType } from 'libs/routing'; +import { lsService } from 'services/localeStorage'; import style from 'components/strategies/common/form.module.css'; interface FormProps { @@ -27,6 +28,7 @@ export const CreateForm: FC = (props) => { const { openModal } = useModal(); const { user } = useWagmi(); const search = useSearch({ strict: false }) as any; + const nav = useNavigate(); const { isLoading, isProcessing, isAwaiting, createStrategy } = useCreateStrategy({ type, base, quote, order0, order1 }); @@ -54,6 +56,34 @@ export const CreateForm: FC = (props) => { return !form.querySelector('#approve-warnings')?.checked; }; + const addToCart = (e: MouseEvent) => { + const form = e.currentTarget.form!; + if (!form.checkValidity()) return; + if (!!form.querySelector('.loading-message')) return; + if (!!form.querySelector('.error-message')) return; + const current = lsService.getItem('cart') ?? []; + const next = [ + ...current, + toCreateStrategyParams(base, quote, order0, order1), + ]; + lsService.setItem('cart', next); + // Dispatch event to cart + const event = new CustomEvent('storage:cart', { detail: next }); + document.dispatchEvent(event); + // Remove budget + nav({ + to: '.', + search: (s) => { + delete s.budget; + delete s.buyBudget; + delete s.sellBudget; + return s; + }, + replace: false, + resetScroll: false, + }); + }; + const create = (e: FormEvent) => { e.preventDefault(); if (isDisabled(e.currentTarget)) return; @@ -85,6 +115,20 @@ export const CreateForm: FC = (props) => { "I've reviewed the warning(s) but choose to proceed."} + + {user ? ( + {/* TODO: create link based on strategy params */} + + + +
+ + +
+ + +
+ +
+
+ + ); +}; diff --git a/src/components/cart/EmptyCart.tsx b/src/components/cart/EmptyCart.tsx new file mode 100644 index 000000000..cfbf7e1b2 --- /dev/null +++ b/src/components/cart/EmptyCart.tsx @@ -0,0 +1,26 @@ +import { Link } from '@tanstack/react-router'; +import { buttonStyles } from 'components/common/button/buttonStyles'; +import { ReactComponent as CartIcon } from 'assets/icons/cart.svg'; + +export const EmptyCart = () => { + return ( +
+
+ + + 0 + +
+

+ No Strategies Found +

+

Your cart is empty

+ + Create Strategy + +
+ ); +}; diff --git a/src/components/cart/utils.ts b/src/components/cart/utils.ts new file mode 100644 index 000000000..0ec9edb59 --- /dev/null +++ b/src/components/cart/utils.ts @@ -0,0 +1,66 @@ +import { useFiatCurrency } from 'hooks/useFiatCurrency'; +import { useTokens } from 'hooks/useTokens'; +import { + CartStrategy, + CreateStrategyOrder, + CreateStrategyParams, +} from 'libs/queries'; +import { useGetMultipleTokenPrices } from 'libs/queries/extApi/tokenPrice'; +import { SafeDecimal } from 'libs/safedecimal'; +import { useEffect, useMemo, useState } from 'react'; +import { lsService } from 'services/localeStorage'; + +export type Cart = (CreateStrategyParams & { id: string })[]; + +const toOrder = (sdkOrder: CreateStrategyOrder) => ({ + balance: sdkOrder.budget, + startRate: sdkOrder.min, + endRate: sdkOrder.max, + marginalRate: sdkOrder.marginalPrice, +}); + +export const useStrategyCart = () => { + const [cart, setCart] = useState(lsService.getItem('cart') ?? []); + const { getTokenById } = useTokens(); + const { selectedFiatCurrency } = useFiatCurrency(); + + const tokens = cart.map(({ base, quote }) => [base, quote]).flat(); + const addresses = Array.from(new Set(tokens)); + const priceQueries = useGetMultipleTokenPrices(addresses); + + useEffect(() => { + const handler = (event: StorageEvent) => { + if (event.key !== lsService.keyFormatter('cart')) return; + const next = JSON.parse(event.newValue ?? '[]'); + setCart(next); + }; + window.addEventListener('storage', handler); + return () => window.removeEventListener('storage', handler); + }); + + return useMemo(() => { + const prices: Record = {}; + for (let i = 0; i < priceQueries.length; i++) { + const address = addresses[i]; + const price = priceQueries[i].data?.[selectedFiatCurrency]; + prices[address] = price; + } + return cart.map((strategy): CartStrategy => { + const basePrice = new SafeDecimal(prices[strategy.base] ?? 0); + const quotePrice = new SafeDecimal(prices[strategy.quote] ?? 0); + const base = basePrice.times(strategy.order1.budget); + const quote = quotePrice.times(strategy.order0.budget); + const total = base.plus(quote); + return { + // temporary id for react key + id: strategy.id, + // We know the tokens are imported because cart comes from localstorage + base: getTokenById(strategy.base)!, + quote: getTokenById(strategy.quote)!, + order0: toOrder(strategy.order0), + order1: toOrder(strategy.order1), + fiatBudget: { base, quote, total }, + }; + }); + }, [addresses, cart, getTokenById, priceQueries, selectedFiatCurrency]); +}; diff --git a/src/components/core/menu/mainMenu/MainMenuCart.tsx b/src/components/core/menu/mainMenu/MainMenuCart.tsx index 51c7c9846..1ede8cdb2 100644 --- a/src/components/core/menu/mainMenu/MainMenuCart.tsx +++ b/src/components/core/menu/mainMenu/MainMenuCart.tsx @@ -37,13 +37,15 @@ const runAnimation = () => { }; export const MainMenuCart = () => { - const [cart, setCart] = useState(lsService.getItem('cart') ?? []); + const [cartSize, setCartsize] = useState( + lsService.getItem('cart')?.length ?? 0 + ); useEffect(() => { const handler = (event: StorageEvent) => { if (event.key !== lsService.keyFormatter('cart')) return; const next = JSON.parse(event.newValue ?? '[]'); - if (next.length > cart.length) runAnimation(); - setCart(next); + if (next.length > cartSize) runAnimation(); + setCartsize(next.length); }; window.addEventListener('storage', handler); return () => window.removeEventListener('storage', handler); @@ -55,10 +57,10 @@ export const MainMenuCart = () => { to="/cart" className="bg-background-800 grid size-40 rounded-full p-10 [grid-template-areas:'stack']" > - - {!!cart.length && ( + + {!!cartSize && ( - {cart.length} + {cartSize} )}
= (props) => { if (!form.checkValidity()) return; if (!!form.querySelector('.loading-message')) return; if (!!form.querySelector('.error-message')) return; - const current = lsService.getItem('cart') ?? []; - const next = [ - ...current, - toCreateStrategyParams(base, quote, order0, order1), - ]; - lsService.setItem('cart', next); - // Dispatch event to cart - const event = new CustomEvent('storage:cart', { detail: next }); - document.dispatchEvent(event); + const list = lsService.getItem('cart') ?? []; + list.push({ + id: crypto.randomUUID(), + ...toCreateStrategyParams(base, quote, order0, order1), + }); + lsService.setItem('cart', list); // Remove budget nav({ to: '.', diff --git a/src/components/strategies/overview/StrategyContent.tsx b/src/components/strategies/overview/StrategyContent.tsx index 3239f7d70..c608eacb2 100644 --- a/src/components/strategies/overview/StrategyContent.tsx +++ b/src/components/strategies/overview/StrategyContent.tsx @@ -7,8 +7,8 @@ import { cn } from 'utils/helpers'; import { StrategyTable } from './StrategyTable'; import { StrategyLayout } from '../StrategySelectLayout'; import { lsService } from 'services/localeStorage'; -import styles from './StrategyContent.module.css'; import { useBreakpoints } from 'hooks/useBreakpoints'; +import styles from './StrategyContent.module.css'; type Props = { strategies: StrategyWithFiat[]; diff --git a/src/components/strategies/overview/strategyBlock/StrategyBlockBudget.tsx b/src/components/strategies/overview/strategyBlock/StrategyBlockBudget.tsx index 3358125b5..d86af1e5c 100644 --- a/src/components/strategies/overview/strategyBlock/StrategyBlockBudget.tsx +++ b/src/components/strategies/overview/strategyBlock/StrategyBlockBudget.tsx @@ -1,10 +1,10 @@ import { FC } from 'react'; import { cn, prettifyNumber } from 'utils/helpers'; import { useFiatCurrency } from 'hooks/useFiatCurrency'; -import { StrategyWithFiat } from 'libs/queries'; +import { CartStrategy } from 'libs/queries'; interface Props { - strategy: StrategyWithFiat; + strategy: CartStrategy; } export const StrategyBlockBudget: FC = ({ strategy }) => { @@ -17,12 +17,7 @@ export const StrategyBlockBudget: FC = ({ strategy }) => { }); return ( -
+

Total Budget

diff --git a/src/components/strategies/overview/strategyBlock/StrategyBlockBuySell.tsx b/src/components/strategies/overview/strategyBlock/StrategyBlockBuySell.tsx index c8c5cd505..bb74bfe84 100644 --- a/src/components/strategies/overview/strategyBlock/StrategyBlockBuySell.tsx +++ b/src/components/strategies/overview/strategyBlock/StrategyBlockBuySell.tsx @@ -1,5 +1,5 @@ import { FC } from 'react'; -import { StrategyWithFiat } from 'libs/queries'; +import { CartStrategy } from 'libs/queries'; import { useFiatCurrency } from 'hooks/useFiatCurrency'; import { LogoImager } from 'components/common/imager/Imager'; import { Tooltip } from 'components/common/tooltip/Tooltip'; @@ -7,7 +7,7 @@ import { ReactComponent as WarningIcon } from 'assets/icons/warning.svg'; import { cn, getFiatDisplayValue, prettifyNumber } from 'utils/helpers'; export const StrategyBlockBuySell: FC<{ - strategy: StrategyWithFiat; + strategy: CartStrategy; buy?: boolean; className?: string; }> = ({ strategy, buy = false, className }) => { @@ -15,7 +15,6 @@ export const StrategyBlockBuySell: FC<{ const otherToken = buy ? strategy.quote : strategy.base; const order = buy ? strategy.order0 : strategy.order1; const testIdPrefix = `${buy ? 'buy' : 'sell'}`; - const active = strategy.status === 'active'; const otherTokenFiat = useFiatCurrency(otherToken); const currency = otherTokenFiat.selectedFiatCurrency; const prettifiedBudget = prettifyNumber(order.balance, { abbreviate: true }); @@ -28,13 +27,7 @@ export const StrategyBlockBuySell: FC<{ const noCurrencyTooltip = `There is no ${currency} value for this token.`; return ( -
+
{buy ? (

Buy {token.symbol}

diff --git a/src/components/strategies/overview/strategyBlock/StrategyBlockInfo.tsx b/src/components/strategies/overview/strategyBlock/StrategyBlockInfo.tsx index 5efff25d3..9fe6bc7a4 100644 --- a/src/components/strategies/overview/strategyBlock/StrategyBlockInfo.tsx +++ b/src/components/strategies/overview/strategyBlock/StrategyBlockInfo.tsx @@ -2,6 +2,7 @@ import { FC } from 'react'; import { StrategyWithFiat } from 'libs/queries'; import { StrategyBlockTradeCount } from 'components/strategies/overview/strategyBlock/StrategyBlockTradeCount'; import { StrategyBlockBudget } from 'components/strategies/overview/strategyBlock/StrategyBlockBudget'; +import { cn } from 'utils/helpers'; interface Props { strategy: StrategyWithFiat; @@ -9,7 +10,11 @@ interface Props { export const StrategyBlockInfo: FC = ({ strategy }) => { return ( -
+
diff --git a/src/components/strategies/overview/strategyBlock/StrategyBlockTradeCount.tsx b/src/components/strategies/overview/strategyBlock/StrategyBlockTradeCount.tsx index 4ce7fd55a..c2ecbcc90 100644 --- a/src/components/strategies/overview/strategyBlock/StrategyBlockTradeCount.tsx +++ b/src/components/strategies/overview/strategyBlock/StrategyBlockTradeCount.tsx @@ -1,5 +1,5 @@ import { FC } from 'react'; -import { cn, prettifyNumber } from 'utils/helpers'; +import { prettifyNumber } from 'utils/helpers'; import { StrategyWithFiat } from 'libs/queries'; interface Props { @@ -12,12 +12,7 @@ export const StrategyBlockTradeCount: FC = ({ strategy }) => { decimals: 0, }); return ( -
+

Trades

{count}

diff --git a/src/components/strategies/overview/strategyBlock/StrategyGraph.tsx b/src/components/strategies/overview/strategyBlock/StrategyGraph.tsx index f5b731d58..d1d5c2432 100644 --- a/src/components/strategies/overview/strategyBlock/StrategyGraph.tsx +++ b/src/components/strategies/overview/strategyBlock/StrategyGraph.tsx @@ -1,5 +1,5 @@ import { FC, useId } from 'react'; -import { Strategy } from 'libs/queries'; +import { BaseStrategy } from 'libs/queries'; import { cn, prettifyNumber, sanitizeNumber } from 'utils/helpers'; import { FloatTooltip, @@ -16,11 +16,11 @@ import { getRoundedSpread } from 'components/strategies/overlapping/utils'; import style from './StrategyGraph.module.css'; interface Props { - strategy: Strategy; + strategy: BaseStrategy; className?: string; } -const isSmallRange = ({ order0, order1 }: Strategy) => { +const isSmallRange = ({ order0, order1 }: BaseStrategy) => { const allPrices = new Set([ order0.startRate, order0.endRate, @@ -573,7 +573,7 @@ export const CurrentPrice: FC = ({ }; interface OrderTooltipProps { - strategy: Strategy; + strategy: BaseStrategy; buy?: boolean; } diff --git a/src/libs/queries/sdk/strategy.ts b/src/libs/queries/sdk/strategy.ts index d6a422652..fcc55e211 100644 --- a/src/libs/queries/sdk/strategy.ts +++ b/src/libs/queries/sdk/strategy.ts @@ -31,13 +31,17 @@ export interface Order { marginalRate: string; } -export interface Strategy { +export interface BaseStrategy { id: string; - idDisplay: string; base: Token; quote: Token; order0: Order; order1: Order; +} + +export interface Strategy extends BaseStrategy { + id: string; + idDisplay: string; status: StrategyStatus; encoded: EncodedStrategyBNStr; } @@ -51,6 +55,14 @@ export interface StrategyWithFiat extends Strategy { tradeCount: number; } +export interface CartStrategy extends BaseStrategy { + fiatBudget: { + total: SafeDecimal; + quote: SafeDecimal; + base: SafeDecimal; + }; +} + interface StrategiesHelperProps { strategies: SDKStrategy[]; getTokenById: (id: string) => Token | undefined; @@ -273,7 +285,7 @@ export const useTokenStrategies = (token?: string) => { }); }; -interface CreateStrategyOrder { +export interface CreateStrategyOrder { budget: string; min: string; max: string; diff --git a/src/pages/cart/index.tsx b/src/pages/cart/index.tsx index 2fd8920c4..22f565d82 100644 --- a/src/pages/cart/index.tsx +++ b/src/pages/cart/index.tsx @@ -1,17 +1,23 @@ -import { useEffect, useState } from 'react'; -import { lsService } from 'services/localeStorage'; +import { CartList } from 'components/cart/CartList'; +import { EmptyCart } from 'components/cart/EmptyCart'; +import { useStrategyCart } from 'components/cart/utils'; +import { Tooltip } from 'components/common/tooltip/Tooltip'; export const CartPage = () => { - const [cart, setCart] = useState(lsService.getItem('cart') ?? []); - useEffect(() => { - const handler = (event: StorageEvent) => { - if (event.key !== lsService.keyFormatter('cart')) return; - const next = JSON.parse(event.newValue ?? '[]'); - setCart(next); - }; - window.addEventListener('storage', handler); - return () => window.removeEventListener('storage', handler); - }); + const strategies = useStrategyCart(); - if (!cart.length) return; + const content = strategies.length ? ( + + ) : ( + + ); + return ( +
+

+ Create multiple strategies + +

+ {content} +
+ ); }; diff --git a/src/services/localeStorage/index.ts b/src/services/localeStorage/index.ts index ad7ed42d2..7bb20eb5c 100644 --- a/src/services/localeStorage/index.ts +++ b/src/services/localeStorage/index.ts @@ -18,7 +18,7 @@ import { import { NotificationPreference } from 'libs/notifications/NotificationPreferences'; import { AppConfig } from 'config/types'; import { StrategyLayout } from 'components/strategies/StrategySelectLayout'; -import { CreateStrategyParams } from 'libs/queries'; +import { Cart } from 'components/cart/utils'; // ************************** / // BEWARE!! Keys are not to be removed or changed without setting a proper clean-up and migration logic in place!! Same for changing the app version! @@ -55,7 +55,7 @@ interface LocalStorageSchema { configOverride: Partial; featureFlags: string[]; strategyLayout: StrategyLayout; - cart: CreateStrategyParams[]; + cart: Cart; } enum EnumStrategySort { diff --git a/tailwind.config.ts b/tailwind.config.ts index 4fa7f4fdf..1a8457044 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -79,7 +79,7 @@ export default { ping: 'ping 1s cubic-bezier(0, 0, 0.2, 1) infinite', pulse: 'pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite', bounce: 'bounce 1s infinite', - fade: 'fade var(--duration, 0) var(--delay, 0s)', + fade: 'fade var(--duration, 200ms) var(--delay, 0s)', slideUp: 'fade var(--duration, 400ms) var(--delay, 0s) var(--easing, cubic-bezier(0.16, 1, 0.3, 1)) both, translateY var(--duration, 400ms) var(--delay, 0s) var(--easing, cubic-bezier(0.16, 1, 0.3, 1)) both', scaleUp: From eedc129580928a4a8e622c42f3c5c9187be3d86b Mon Sep 17 00:00:00 2001 From: Grandschtroumpf Date: Fri, 10 Jan 2025 15:49:45 +0100 Subject: [PATCH 03/27] setup batcher contract --- src/abis/batcher.json | 610 ++++++++++++++++++++++ src/components/debug/DebugTenderlyRPC.tsx | 21 +- src/config/configSchema.ts | 1 + src/config/ethereum/common.ts | 1 + src/hooks/useContract.ts | 22 +- src/libs/wagmi/useWagmiTenderly.ts | 7 +- src/libs/wagmi/wagmi.types.ts | 3 +- src/services/localeStorage/index.ts | 1 + 8 files changed, 656 insertions(+), 10 deletions(-) create mode 100644 src/abis/batcher.json diff --git a/src/abis/batcher.json b/src/abis/batcher.json new file mode 100644 index 000000000..6fdda5a5f --- /dev/null +++ b/src/abis/batcher.json @@ -0,0 +1,610 @@ +[ + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "previousAdmin", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "AdminChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "beacon", + "type": "address" + } + ], + "name": "BeaconUpgraded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "implementation", + "type": "address" + } + ], + "name": "Upgraded", + "type": "event" + }, + { + "stateMutability": "payable", + "type": "fallback" + }, + { + "inputs": [], + "name": "admin", + "outputs": [ + { + "internalType": "address", + "name": "admin_", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "implementation", + "outputs": [ + { + "internalType": "address", + "name": "implementation_", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + } + ], + "name": "upgradeTo", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "upgradeToAndCall", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "stateMutability": "payable", + "type": "receive" + }, + { + "inputs": [], + "name": "AccessDenied", + "type": "error" + }, + { + "inputs": [], + "name": "AlreadyInitialized", + "type": "error" + }, + { + "inputs": [], + "name": "InsufficientNativeTokenSent", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidAddress", + "type": "error" + }, + { + "inputs": [], + "name": "ZeroValue", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "Token", + "name": "token", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "caller", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "target", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "FundsWithdrawn", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint8", + "name": "version", + "type": "uint8" + } + ], + "name": "Initialized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "previousAdminRole", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "newAdminRole", + "type": "bytes32" + } + ], + "name": "RoleAdminChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + } + ], + "name": "RoleGranted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + } + ], + "name": "RoleRevoked", + "type": "event" + }, + { + "inputs": [], + "name": "DEFAULT_ADMIN_ROLE", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "Token[2]", + "name": "tokens", + "type": "address[2]" + }, + { + "components": [ + { + "internalType": "uint128", + "name": "y", + "type": "uint128" + }, + { + "internalType": "uint128", + "name": "z", + "type": "uint128" + }, + { + "internalType": "uint64", + "name": "A", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "B", + "type": "uint64" + } + ], + "internalType": "struct Order[2]", + "name": "orders", + "type": "tuple[2]" + } + ], + "internalType": "struct StrategyData[]", + "name": "strategies", + "type": "tuple[]" + } + ], + "name": "batchCreate", + "outputs": [ + { + "internalType": "uint256[]", + "name": "", + "type": "uint256[]" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + } + ], + "name": "getRoleAdmin", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "index", + "type": "uint256" + } + ], + "name": "getRoleMember", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + } + ], + "name": "getRoleMemberCount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "grantRole", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "hasRole", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "name": "onERC721Received", + "outputs": [ + { + "internalType": "bytes4", + "name": "", + "type": "bytes4" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bool", + "name": "checkVersion", + "type": "bool" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "postUpgrade", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "renounceRole", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "revokeRole", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "roleAdmin", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes4", + "name": "interfaceId", + "type": "bytes4" + } + ], + "name": "supportsInterface", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "version", + "outputs": [ + { + "internalType": "uint16", + "name": "", + "type": "uint16" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "Token", + "name": "token", + "type": "address" + }, + { + "internalType": "address payable", + "name": "target", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "withdrawFunds", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_logic", + "type": "address" + }, + { + "internalType": "address", + "name": "admin_", + "type": "address" + }, + { + "internalType": "bytes", + "name": "_data", + "type": "bytes" + } + ], + "stateMutability": "payable", + "type": "constructor" + } +] \ No newline at end of file diff --git a/src/components/debug/DebugTenderlyRPC.tsx b/src/components/debug/DebugTenderlyRPC.tsx index 3f3096b38..b299043bd 100644 --- a/src/components/debug/DebugTenderlyRPC.tsx +++ b/src/components/debug/DebugTenderlyRPC.tsx @@ -4,8 +4,8 @@ import { lsService } from 'services/localeStorage'; import { Button } from 'components/common/button'; import { Input, Label } from 'components/common/inputField'; import { Checkbox } from 'components/common/Checkbox/Checkbox'; -import config from 'config'; import { tenderlyRpc } from 'utils/tenderly'; +import config from 'config'; export const DebugTenderlyRPC = () => { const { handleTenderlyRPC, isUncheckedSigner, setIsUncheckedSigner } = @@ -17,14 +17,21 @@ export const DebugTenderlyRPC = () => { const [carbonControllerInput, setCarbonControllerInput] = useState( config.addresses.carbon.carbonController ); - const [voucherAddressInput, setVoucherAddressInput] = useState( config.addresses.carbon.voucher ); + const [batcherAddressInput, setBatcherAddressInput] = useState( + config.addresses.carbon.batcher + ); const submit = (e: FormEvent) => { e.preventDefault(); - handleTenderlyRPC(urlInput, carbonControllerInput, voucherAddressInput); + handleTenderlyRPC( + urlInput, + carbonControllerInput, + voucherAddressInput, + batcherAddressInput + ); lsService.setItem('carbonApi', backendUrl); }; @@ -59,6 +66,14 @@ export const DebugTenderlyRPC = () => { fullWidth /> + + )} diff --git a/src/config/configSchema.ts b/src/config/configSchema.ts index 529778896..79add464b 100644 --- a/src/config/configSchema.ts +++ b/src/config/configSchema.ts @@ -75,6 +75,7 @@ export const AppConfigSchema = v.object({ carbon: v.object({ carbonController: v.string(), voucher: v.string(), + batcher: v.optional(v.string()), }), }), utils: v.union([ diff --git a/src/config/ethereum/common.ts b/src/config/ethereum/common.ts index 4a09950f4..0d7d643e8 100644 --- a/src/config/ethereum/common.ts +++ b/src/config/ethereum/common.ts @@ -112,6 +112,7 @@ export const commonConfig: AppConfig = { carbon: { carbonController: '0xC537e898CD774e2dCBa3B14Ea6f34C93d5eA45e1', voucher: '0x3660F04B79751e31128f6378eAC70807e38f554E', + batcher: '0x03ede52e1174bE0956f3d3e0e86E05626974Bee2', }, }, utils: { diff --git a/src/hooks/useContract.ts b/src/hooks/useContract.ts index dbf5a6fbd..6366e9377 100644 --- a/src/hooks/useContract.ts +++ b/src/hooks/useContract.ts @@ -1,22 +1,34 @@ import { useWagmi } from 'libs/wagmi'; -import { Token__factory, Voucher__factory } from 'abis/types'; +import { Token__factory, Voucher__factory, Batcher__factory } from 'abis/types'; import { useCallback } from 'react'; import { useQuery } from '@tanstack/react-query'; import config from 'config'; export const useVoucher = () => { const { provider, signer } = useWagmi(); + const address = config.addresses.carbon.voucher; return useQuery({ queryKey: ['contract', 'voucher'], queryFn: () => ({ - read: Voucher__factory.connect( - config.addresses.carbon.voucher, - provider! - ), + read: Voucher__factory.connect(address, provider!), write: Voucher__factory.connect(config.addresses.carbon.voucher, signer!), }), }); }; + +export const useBatcher = () => { + const { provider, signer } = useWagmi(); + const address = config.addresses.carbon.batcher; + return useQuery({ + queryKey: ['contract', 'batcher'], + queryFn: () => ({ + read: Batcher__factory.connect(address!, provider!), + write: Batcher__factory.connect(address!, signer!), + }), + enabled: !!address, + }); +}; + export const useContract = () => { const { provider, signer } = useWagmi(); diff --git a/src/libs/wagmi/useWagmiTenderly.ts b/src/libs/wagmi/useWagmiTenderly.ts index ab6b1c6e3..b831c539a 100644 --- a/src/libs/wagmi/useWagmiTenderly.ts +++ b/src/libs/wagmi/useWagmiTenderly.ts @@ -12,7 +12,8 @@ export const useWagmiTenderly = () => { const handleTenderlyRPC = ( url?: string, carbonController?: string, - voucherAddress?: string + voucherAddress?: string, + batcherAddress?: string ) => { url ? lsService.setItem('tenderlyRpc', url) @@ -26,6 +27,10 @@ export const useWagmiTenderly = () => { ? lsService.setItem('voucherContractAddress', voucherAddress) : lsService.removeItem('voucherContractAddress'); + batcherAddress + ? lsService.setItem('batcherContractAddress', batcherAddress) + : lsService.removeItem('batcherContractAddress'); + lsService.removeItem('sdkCompressedCacheData'); lsService.removeItem('tokenPairsCache'); !url && lsService.removeItem('isUncheckedSigner'); diff --git a/src/libs/wagmi/wagmi.types.ts b/src/libs/wagmi/wagmi.types.ts index dc60df928..de04422cb 100644 --- a/src/libs/wagmi/wagmi.types.ts +++ b/src/libs/wagmi/wagmi.types.ts @@ -32,7 +32,8 @@ export interface CarbonWagmiProviderContext { handleTenderlyRPC: ( url?: string, carbonController?: string, - voucherAddress?: string + voucherAddress?: string, + batcherAddress?: string ) => void; setImposterAccount: (account?: string) => void; disconnect: () => Promise; diff --git a/src/services/localeStorage/index.ts b/src/services/localeStorage/index.ts index 7bb20eb5c..5e84e47e6 100644 --- a/src/services/localeStorage/index.ts +++ b/src/services/localeStorage/index.ts @@ -43,6 +43,7 @@ interface LocalStorageSchema { strategyOverviewFilter: StrategyFilter; strategyOverviewSort: StrategySort; voucherContractAddress: string; + batcherContractAddress: string; tokenListCache: { tokens: Token[]; timestamp: number }; sdkCompressedCacheData: string; tokenPairsCache: { pairs: TradePair[]; timestamp: number }; From cb612184402ba83db5fd2b1e2775505c647ab5c6 Mon Sep 17 00:00:00 2001 From: Grandschtroumpf Date: Mon, 13 Jan 2025 10:44:04 +0100 Subject: [PATCH 04/27] update edit button --- src/components/cart/CartStrategy.tsx | 11 ++++++----- src/components/strategies/common/utils.ts | 6 +++--- .../strategies/create/useDuplicateStrategy.ts | 19 ++++++++++++------- .../ModalDuplicateStrategy.tsx | 2 +- 4 files changed, 22 insertions(+), 16 deletions(-) diff --git a/src/components/cart/CartStrategy.tsx b/src/components/cart/CartStrategy.tsx index 025365030..ceeb8e959 100644 --- a/src/components/cart/CartStrategy.tsx +++ b/src/components/cart/CartStrategy.tsx @@ -8,7 +8,7 @@ import { ReactComponent as EditIcon } from 'assets/icons/edit.svg'; import { CartStrategy } from 'libs/queries'; import { CSSProperties, FC, useId } from 'react'; import { cn } from 'utils/helpers'; -import { Link } from '@tanstack/react-router'; +import { useDuplicate } from 'components/strategies/create/useDuplicateStrategy'; interface Props { onRemove: () => void; @@ -20,6 +20,7 @@ interface Props { export const CartStrategyItems: FC = (props) => { const { strategy, style, className, onRemove } = props; const { base, quote } = strategy; + const duplicate = useDuplicate(); const id = useId(); const remove = async () => { @@ -56,13 +57,13 @@ export const CartStrategyItems: FC = (props) => { > - {/* TODO: create link based on strategy params */} - duplicate(strategy)} className="size-38 rounded-6 border-background-800 grid place-items-center border-2 hover:bg-white/10 active:bg-white/20" > - +
diff --git a/src/components/strategies/common/utils.ts b/src/components/strategies/common/utils.ts index ec9614337..50418d0d1 100644 --- a/src/components/strategies/common/utils.ts +++ b/src/components/strategies/common/utils.ts @@ -1,4 +1,4 @@ -import { Order, Strategy } from 'libs/queries'; +import { BaseStrategy, Order, Strategy } from 'libs/queries'; import { SafeDecimal } from 'libs/safedecimal'; import { Token } from 'libs/tokens'; import { formatNumber } from 'utils/helpers'; @@ -23,7 +23,7 @@ export const isOverlappingStrategy = ({ order0, order1 }: StrategyInput) => { return buyMax.gte(sellMin); }; -export const isDisposableStrategy = (strategy: Strategy) => { +export const isDisposableStrategy = (strategy: BaseStrategy) => { // If strategy is inactive, consider it as a recurring if (isEmptyOrder(strategy.order0) && isEmptyOrder(strategy.order1)) { return false; @@ -35,7 +35,7 @@ export const isDisposableStrategy = (strategy: Strategy) => { return false; }; -export const getStrategyType = (strategy: Strategy) => { +export const getStrategyType = (strategy: BaseStrategy) => { if (isOverlappingStrategy(strategy)) return 'overlapping'; if (isDisposableStrategy(strategy)) return 'disposable'; return 'recurring'; diff --git a/src/components/strategies/create/useDuplicateStrategy.ts b/src/components/strategies/create/useDuplicateStrategy.ts index 1efd27973..3379d593f 100644 --- a/src/components/strategies/create/useDuplicateStrategy.ts +++ b/src/components/strategies/create/useDuplicateStrategy.ts @@ -1,14 +1,19 @@ -import { StrategyType, useNavigate } from 'libs/routing'; -import { Strategy } from 'libs/queries'; +import { useNavigate } from 'libs/routing'; +import { BaseStrategy } from 'libs/queries'; import { getRoundedSpread } from 'components/strategies/overlapping/utils'; -import { isLimitOrder } from 'components/strategies/common/utils'; +import { + getStrategyType, + isLimitOrder, +} from 'components/strategies/common/utils'; import { NATIVE_TOKEN_ADDRESS, isGasTokenToHide } from 'utils/tokens'; -export const useDuplicate = (type: StrategyType) => { +export const useDuplicate = () => { const navigate = useNavigate(); - return ({ base: rawBase, quote: rawQuote, order0, order1 }: Strategy) => { - let baseAddress = rawBase.address; - let quoteAddress = rawQuote.address; + return (strategy: BaseStrategy) => { + const type = getStrategyType(strategy); + const { base, quote, order0, order1 } = strategy; + let baseAddress = base.address; + let quoteAddress = quote.address; // Force native token address if gas token is different if (isGasTokenToHide(baseAddress)) baseAddress = NATIVE_TOKEN_ADDRESS; diff --git a/src/libs/modals/modals/ModalDuplicateStrategy/ModalDuplicateStrategy.tsx b/src/libs/modals/modals/ModalDuplicateStrategy/ModalDuplicateStrategy.tsx index 7bb90ced1..206110d4f 100644 --- a/src/libs/modals/modals/ModalDuplicateStrategy/ModalDuplicateStrategy.tsx +++ b/src/libs/modals/modals/ModalDuplicateStrategy/ModalDuplicateStrategy.tsx @@ -22,7 +22,7 @@ export const ModalDuplicateStrategy: ModalFC = ({ }) => { const navigate = useNavigate(); const strategyType = getStrategyType(strategy); - const duplicate = useDuplicate(strategyType); + const duplicate = useDuplicate(); const { closeModal } = useModal(); const undercutDifference = 0.001; From 79c25ebd1f76ef254ac02ba55f478029f7fcd36c Mon Sep 17 00:00:00 2001 From: Grandschtroumpf Date: Mon, 13 Jan 2025 11:03:26 +0100 Subject: [PATCH 05/27] add confirm modal --- src/components/cart/CartList.tsx | 44 ------- src/components/cart/CartStrategy.tsx | 19 +-- .../ModalCart/ModalCartConfirmDelete.tsx | 120 ++++++++++++++++++ src/libs/modals/modals/index.ts | 6 + 4 files changed, 132 insertions(+), 57 deletions(-) create mode 100644 src/libs/modals/modals/ModalCart/ModalCartConfirmDelete.tsx diff --git a/src/components/cart/CartList.tsx b/src/components/cart/CartList.tsx index 224baefa8..d0052ea8c 100644 --- a/src/components/cart/CartList.tsx +++ b/src/components/cart/CartList.tsx @@ -2,56 +2,13 @@ import { FC } from 'react'; import { CartStrategy } from 'libs/queries'; import { CartStrategyItems } from './CartStrategy'; import { cn } from 'utils/helpers'; -import { lsService } from 'services/localeStorage'; import styles from 'components/strategies/overview/StrategyContent.module.css'; interface Props { strategies: CartStrategy[]; } -const flip = (selector: string) => { - const elements = document.querySelectorAll(selector); - const boxes = new Map(); - for (const el of elements) { - boxes.set(el, el.getBoundingClientRect()); - } - let attempts = 0; - const checkChange = () => { - if (attempts > 10) return; - attempts++; - const updated = document.querySelectorAll(selector); - if (elements.length === updated.length) { - return requestAnimationFrame(checkChange); - } - for (const [el, box] of boxes.entries()) { - const newBox = el.getBoundingClientRect(); - if (box.top === newBox.top && box.left === newBox.left) continue; - const keyframes = [ - // eslint-disable-next-line prettier/prettier - { - transform: `translate(${box.left - newBox.left}px, ${ - box.top - newBox.top - }px)`, - }, - { transform: `translate(0px, 0px)` }, - ]; - el.animate(keyframes, { - duration: 300, - easing: 'cubic-bezier(.85, 0, .15, 1)', - }); - } - }; - requestAnimationFrame(checkChange); -}; - export const CartList: FC = ({ strategies }) => { - const removeAt = (index: number) => { - const current = lsService.getItem('cart'); - if (!current) return; - current.splice(index, 1); - lsService.setItem('cart', current); - flip(`.${styles.strategyList} > li`); - }; return (
    {strategies.map((strategy, i) => { @@ -63,7 +20,6 @@ export const CartList: FC = ({ strategies }) => { strategy={strategy} style={style} className={className} - onRemove={() => removeAt(i)} /> ); })} diff --git a/src/components/cart/CartStrategy.tsx b/src/components/cart/CartStrategy.tsx index ceeb8e959..2c21e784b 100644 --- a/src/components/cart/CartStrategy.tsx +++ b/src/components/cart/CartStrategy.tsx @@ -6,37 +6,30 @@ import { StrategyGraph } from 'components/strategies/overview/strategyBlock/Stra import { ReactComponent as DeleteIcon } from 'assets/icons/delete.svg'; import { ReactComponent as EditIcon } from 'assets/icons/edit.svg'; import { CartStrategy } from 'libs/queries'; -import { CSSProperties, FC, useId } from 'react'; +import { CSSProperties, FC } from 'react'; import { cn } from 'utils/helpers'; import { useDuplicate } from 'components/strategies/create/useDuplicateStrategy'; +import { useModal } from 'hooks/useModal'; interface Props { - onRemove: () => void; strategy: CartStrategy; className?: string; style?: CSSProperties; } export const CartStrategyItems: FC = (props) => { - const { strategy, style, className, onRemove } = props; + const { strategy, style, className } = props; const { base, quote } = strategy; + const { openModal } = useModal(); const duplicate = useDuplicate(); - const id = useId(); const remove = async () => { - const keyframes = { opacity: 0, transform: 'scale(0.9)' }; - const option = { - duration: 200, - easing: 'cubic-bezier(.55, 0, 1, .45)', - fill: 'forwards' as const, - }; - await document.getElementById(id)?.animate(keyframes, option).finished; - onRemove(); + openModal('confirmDeleteCartStrategy', { strategy }); }; return (
  • { + const elements = document.querySelectorAll(selector); + const boxes = new Map(); + for (const el of elements) { + boxes.set(el, el.getBoundingClientRect()); + } + let attempts = 0; + const checkChange = () => { + if (attempts > 10) return; + attempts++; + const updated = document.querySelectorAll(selector); + if (elements.length === updated.length) { + return requestAnimationFrame(checkChange); + } + for (const [el, box] of boxes.entries()) { + const newBox = el.getBoundingClientRect(); + if (box.top === newBox.top && box.left === newBox.left) continue; + const keyframes = [ + // eslint-disable-next-line prettier/prettier + { + transform: `translate(${box.left - newBox.left}px, ${ + box.top - newBox.top + }px)`, + }, + { transform: `translate(0px, 0px)` }, + ]; + el.animate(keyframes, { + duration: 300, + easing: 'cubic-bezier(.85, 0, .15, 1)', + }); + } + }; + requestAnimationFrame(checkChange); +}; + +export const ModalCartConfirmDelete: ModalFC = ({ + id, + data, +}) => { + const { closeModal } = useModal(); + const { strategy } = data; + const duplicate = useDuplicate(); + + const onClick = async () => { + closeModal(id); + + // Animate leaving strategy + const keyframes = { opacity: 0, transform: 'scale(0.9)' }; + const option = { + duration: 200, + easing: 'cubic-bezier(.55, 0, 1, .45)', + fill: 'forwards' as const, + }; + await document.getElementById(id)?.animate(keyframes, option).finished; + + // Delete from localstorage + const current = lsService.getItem('cart') ?? []; + const next = current.filter(({ id }) => id !== strategy.id); + lsService.setItem('cart', next); + + // Animate remaining strategies + flip(`.${styles.strategyList} > li`); + }; + + const edit = () => { + closeModal(id); + duplicate(strategy); + }; + + return ( + + } + title="Are you sure you would like to delete your strategy?" + text="Deleting your strategy will result in all strategy data being lost and impossible to restore. All funds will be withdrawn to your wallet." + /> +
    +

    Did you know?

    + +

    + Editing prices is cheaper and keeps your strategy working for you. +

    +
    + + +
    + ); +}; diff --git a/src/libs/modals/modals/index.ts b/src/libs/modals/modals/index.ts index 23969c2b3..9826c51dd 100644 --- a/src/libs/modals/modals/index.ts +++ b/src/libs/modals/modals/index.ts @@ -56,6 +56,10 @@ import { ModalSimulatorDisclaimer, ModalSimulatorDisclaimerData, } from 'libs/modals/modals/ModalSimulatorDisclaimer'; +import { + ModalCartConfirmDelete, + ModalCartConfirmDeleteData, +} from './ModalCart/ModalCartConfirmDelete'; // Step 1: Add modal key and data type to schema export interface ModalSchema { @@ -76,6 +80,7 @@ export interface ModalSchema { confirmDeleteStrategy: ModalConfirmDeleteData; simulatorDisclaimer: ModalSimulatorDisclaimerData; withdrawOrDelete: ModalWithdrawOrDeleteData; + confirmDeleteCartStrategy: ModalCartConfirmDeleteData; } // Step 2: Create component in modals/modals folder @@ -99,4 +104,5 @@ export const MODAL_COMPONENTS: TModals = { confirmDeleteStrategy: (props) => ModalConfirmDelete(props), simulatorDisclaimer: (props) => ModalSimulatorDisclaimer(props), withdrawOrDelete: (props) => ModalWithdrawOrDelete(props), + confirmDeleteCartStrategy: (props) => ModalCartConfirmDelete(props), }; From 9d3e1ca490598b26c0956f19b03518b256b5cbdd Mon Sep 17 00:00:00 2001 From: Grandschtroumpf Date: Mon, 13 Jan 2025 12:15:46 +0100 Subject: [PATCH 06/27] add strategy card warning & error --- src/components/cart/CartStrategy.tsx | 57 ++++++++++++++-- .../strategies/create/CreateForm.tsx | 37 +++++----- .../strategies/create/useDuplicateStrategy.ts | 67 ++++++++++++++++++- .../strategies/edit/EditStrategyForm.tsx | 4 +- .../ModalCart/ModalCartConfirmDelete.tsx | 4 +- src/libs/routing/routes/trade.ts | 3 + src/pages/cart/index.tsx | 56 +++++++++++++++- 7 files changed, 201 insertions(+), 27 deletions(-) diff --git a/src/components/cart/CartStrategy.tsx b/src/components/cart/CartStrategy.tsx index 2c21e784b..604b77e8b 100644 --- a/src/components/cart/CartStrategy.tsx +++ b/src/components/cart/CartStrategy.tsx @@ -8,8 +8,14 @@ import { ReactComponent as EditIcon } from 'assets/icons/edit.svg'; import { CartStrategy } from 'libs/queries'; import { CSSProperties, FC } from 'react'; import { cn } from 'utils/helpers'; -import { useDuplicate } from 'components/strategies/create/useDuplicateStrategy'; +import { useCartDuplicate } from 'components/strategies/create/useDuplicateStrategy'; import { useModal } from 'hooks/useModal'; +import { + isZero, + outSideMarketWarning, +} from 'components/strategies/common/utils'; +import { Warning } from 'components/common/WarningMessageWithIcon'; +import { useMarketPrice } from 'hooks/useMarketPrice'; interface Props { strategy: CartStrategy; @@ -17,11 +23,40 @@ interface Props { style?: CSSProperties; } +const getWarning = (strategy: CartStrategy, marketPrice?: number) => { + const { base, order0, order1 } = strategy; + const buyOutsideMarket = outSideMarketWarning({ + base, + marketPrice, + min: order0.startRate, + max: order0.endRate, + buy: true, + }); + if (buyOutsideMarket) return buyOutsideMarket; + const sellOutsideMarket = outSideMarketWarning({ + base, + marketPrice, + min: order1.startRate, + max: order1.endRate, + buy: false, + }); + if (sellOutsideMarket) return sellOutsideMarket; +}; +const getError = (strategy: CartStrategy) => { + if (!isZero(strategy.order0.balance)) return ''; + if (!isZero(strategy.order1.balance)) return ''; + return 'Please note that your strategy will be inactive as it will not have any budget.'; +}; + export const CartStrategyItems: FC = (props) => { const { strategy, style, className } = props; const { base, quote } = strategy; const { openModal } = useModal(); - const duplicate = useDuplicate(); + const duplicate = useCartDuplicate(); + const { marketPrice } = useMarketPrice({ base, quote }); + + const warningMsg = getWarning(strategy, marketPrice); + const errorMsg = getError(strategy); const remove = async () => { openModal('confirmDeleteCartStrategy', { strategy }); @@ -46,20 +81,34 @@ export const CartStrategyItems: FC = (props) => { role="menuitem" type="button" className="size-38 rounded-6 border-background-800 grid place-items-center border-2 hover:bg-white/10 active:bg-white/20" + aria-label="Delete strategy" onClick={remove} >
- +
+ + {!errorMsg && warningMsg && ( +
+ +
+ )} + {errorMsg && ( +
+ +
+ )} +
= (props) => { if (!form.checkValidity()) return; if (!!form.querySelector('.loading-message')) return; if (!!form.querySelector('.error-message')) return; + const id = search.strategyCartId || crypto.randomUUID(); + const strategy = toCreateStrategyParams(base, quote, order0, order1); const list = lsService.getItem('cart') ?? []; - list.push({ - id: crypto.randomUUID(), - ...toCreateStrategyParams(base, quote, order0, order1), - }); + const index = list.findIndex((s) => s.id === id); + if (index === -1) { + list.push({ id, ...strategy }); + // Remove budget + nav({ + to: '.', + search: (s) => { + delete s.budget; + delete s.buyBudget; + delete s.sellBudget; + return s; + }, + replace: false, + resetScroll: false, + }); + } else { + list[index] = { id, ...strategy }; + nav({ to: '/cart' }); + } lsService.setItem('cart', list); - // Remove budget - nav({ - to: '.', - search: (s) => { - delete s.budget; - delete s.buyBudget; - delete s.sellBudget; - return s; - }, - replace: false, - resetScroll: false, - }); }; const create = (e: FormEvent) => { diff --git a/src/components/strategies/create/useDuplicateStrategy.ts b/src/components/strategies/create/useDuplicateStrategy.ts index 3379d593f..6a5a6475f 100644 --- a/src/components/strategies/create/useDuplicateStrategy.ts +++ b/src/components/strategies/create/useDuplicateStrategy.ts @@ -1,5 +1,5 @@ import { useNavigate } from 'libs/routing'; -import { BaseStrategy } from 'libs/queries'; +import { BaseStrategy, CartStrategy } from 'libs/queries'; import { getRoundedSpread } from 'components/strategies/overlapping/utils'; import { getStrategyType, @@ -68,3 +68,68 @@ export const useDuplicate = () => { } }; }; + +export const useCartDuplicate = () => { + const navigate = useNavigate(); + return (strategy: CartStrategy) => { + const type = getStrategyType(strategy); + const { base, quote, order0, order1 } = strategy; + let baseAddress = base.address; + let quoteAddress = quote.address; + + // Force native token address if gas token is different + if (isGasTokenToHide(baseAddress)) baseAddress = NATIVE_TOKEN_ADDRESS; + if (isGasTokenToHide(quoteAddress)) quoteAddress = NATIVE_TOKEN_ADDRESS; + + switch (type) { + case 'disposable': { + const isBuyEmpty = !+order0.endRate; + const order = isBuyEmpty ? order1 : order0; + return navigate({ + to: '/trade/disposable', + search: { + base: baseAddress, + quote: quoteAddress, + min: order.startRate, + max: order.endRate, + budget: order.balance, + settings: isLimitOrder(order) ? 'limit' : 'range', + direction: isBuyEmpty ? 'sell' : 'buy', + strategyCartId: strategy.id, + }, + }); + } + case 'overlapping': { + return navigate({ + to: '/trade/overlapping', + search: { + base: baseAddress, + quote: quoteAddress, + min: order0.startRate, + max: order1.endRate, + spread: getRoundedSpread({ order0, order1 }).toString(), + strategyCartId: strategy.id, + }, + }); + } + case 'recurring': { + return navigate({ + to: '/trade/recurring', + search: { + base: baseAddress, + quote: quoteAddress, + buyMin: order0.startRate, + buyMax: order0.endRate, + buyBudget: order0.balance, + buySettings: isLimitOrder(order0) ? 'limit' : 'range', + sellMin: order1.startRate, + sellMax: order1.endRate, + sellBudget: order1.balance, + sellSettings: isLimitOrder(order1) ? 'limit' : 'range', + strategyCartId: strategy.id, + }, + }); + } + } + }; +}; diff --git a/src/components/strategies/edit/EditStrategyForm.tsx b/src/components/strategies/edit/EditStrategyForm.tsx index 1d099b4ae..c57d3f124 100644 --- a/src/components/strategies/edit/EditStrategyForm.tsx +++ b/src/components/strategies/edit/EditStrategyForm.tsx @@ -29,10 +29,10 @@ import { getDeposit } from './utils'; import { useApproval } from 'hooks/useApproval'; import { useEditStrategyCtx } from './EditStrategyContext'; import { useDeleteStrategy } from '../useDeleteStrategy'; -import style from 'components/strategies/common/form.module.css'; -import config from 'config'; import { hasNoBudget } from '../overlapping/utils'; import { StrategyUpdate } from '@bancor/carbon-sdk'; +import style from 'components/strategies/common/form.module.css'; +import config from 'config'; interface EditOrders { buy: BaseOrder; diff --git a/src/libs/modals/modals/ModalCart/ModalCartConfirmDelete.tsx b/src/libs/modals/modals/ModalCart/ModalCartConfirmDelete.tsx index 620691e61..913b28526 100644 --- a/src/libs/modals/modals/ModalCart/ModalCartConfirmDelete.tsx +++ b/src/libs/modals/modals/ModalCart/ModalCartConfirmDelete.tsx @@ -8,7 +8,7 @@ import { ReactComponent as IconTrash } from 'assets/icons/trash.svg'; import { cn } from 'utils/helpers'; import { Button } from 'components/common/button'; import { lsService } from 'services/localeStorage'; -import { useDuplicate } from 'components/strategies/create/useDuplicateStrategy'; +import { useCartDuplicate } from 'components/strategies/create/useDuplicateStrategy'; import styles from 'components/strategies/overview/StrategyContent.module.css'; export interface ModalCartConfirmDeleteData { @@ -56,7 +56,7 @@ export const ModalCartConfirmDelete: ModalFC = ({ }) => { const { closeModal } = useModal(); const { strategy } = data; - const duplicate = useDuplicate(); + const duplicate = useCartDuplicate(); const onClick = async () => { closeModal(id); diff --git a/src/libs/routing/routes/trade.ts b/src/libs/routing/routes/trade.ts index f043f0dba..b6432bc06 100644 --- a/src/libs/routing/routes/trade.ts +++ b/src/libs/routing/routes/trade.ts @@ -100,6 +100,7 @@ const disposablePage = createRoute({ max: v.optional(validInputNumber), budget: v.optional(validInputNumber), marginalPrice: v.optional(v.enum(MarginalPriceOptions)), + strategyCartId: v.optional(v.string()), }), }); @@ -118,6 +119,7 @@ const recurringPage = createRoute({ sellBudget: v.optional(validInputNumber), sellSettings: v.optional(v.picklist(['limit', 'range'])), sellMarginalPrice: v.optional(v.enum(MarginalPriceOptions)), + strategyCartId: v.optional(v.string()), }), }); @@ -133,6 +135,7 @@ const overlappingPage = createRoute({ budget: v.optional(validNumber), anchor: v.optional(v.picklist(['buy', 'sell'])), chartType: v.optional(v.picklist(['history', 'range'])), + strategyCartId: v.optional(v.string()), }), }); diff --git a/src/pages/cart/index.tsx b/src/pages/cart/index.tsx index 22f565d82..9e1d1bc96 100644 --- a/src/pages/cart/index.tsx +++ b/src/pages/cart/index.tsx @@ -1,10 +1,32 @@ import { CartList } from 'components/cart/CartList'; import { EmptyCart } from 'components/cart/EmptyCart'; import { useStrategyCart } from 'components/cart/utils'; +import { Button } from 'components/common/button'; import { Tooltip } from 'components/common/tooltip/Tooltip'; +import { useBatcher } from 'hooks/useContract'; +import { useWagmi } from 'libs/wagmi'; +import { cn } from 'utils/helpers'; +import { FormEvent } from 'react'; +import style from 'components/strategies/common/form.module.css'; export const CartPage = () => { const strategies = useStrategyCart(); + const { user } = useWagmi(); + const { data: contract } = useBatcher(); + + const submit = async (e: FormEvent) => { + e.preventDefault(); + const form = e.currentTarget; + if (!!form.querySelector('.error-message')) return; + const warnings = form.querySelector('.warning-message'); + if (warnings) { + const approve = form.querySelector('#approve-warnings'); + if (approve && !approve.checked) return; + } + // const strategiesData = toStrategyData(strategies); + // const tx = await contract?.write.batchCreate(strategiesData); + // await tx?.wait(); + }; const content = strategies.length ? ( @@ -12,12 +34,42 @@ export const CartPage = () => { ); return ( -
+

Create multiple strategies

{content} -
+ + + ); }; From bd2d0bd28d7226d1067472d7b2bcbf9449723900 Mon Sep 17 00:00:00 2001 From: Grandschtroumpf Date: Mon, 13 Jan 2025 13:27:54 +0100 Subject: [PATCH 07/27] add approval --- .../strategies/create/CreateForm.tsx | 64 +++++++++---- src/pages/cart/index.tsx | 92 +++++++++++++++++-- 2 files changed, 126 insertions(+), 30 deletions(-) diff --git a/src/components/strategies/create/CreateForm.tsx b/src/components/strategies/create/CreateForm.tsx index 89de1e192..db856bd75 100644 --- a/src/components/strategies/create/CreateForm.tsx +++ b/src/components/strategies/create/CreateForm.tsx @@ -1,7 +1,7 @@ import { FC, FormEvent, MouseEvent, ReactNode, useEffect } from 'react'; import { Token } from 'libs/tokens'; import { createStrategyEvents } from 'services/events/strategyEvents'; -import { useNavigate, useSearch } from '@tanstack/react-router'; +import { Link, useNavigate, useSearch } from 'libs/routing'; import { Button } from 'components/common/button'; import { toCreateStrategyParams, useCreateStrategy } from './useCreateStrategy'; import { getStatusTextByTxStatus } from '../utils'; @@ -12,6 +12,7 @@ import { BaseOrder } from 'components/strategies/common/types'; import { StrategyType } from 'libs/routing'; import { lsService } from 'services/localeStorage'; import style from 'components/strategies/common/form.module.css'; +import { buttonStyles } from 'components/common/button/buttonStyles'; interface FormProps { type: StrategyType; @@ -27,7 +28,7 @@ export const CreateForm: FC = (props) => { const { base, quote, order0, order1, type, children } = props; const { openModal } = useModal(); const { user } = useWagmi(); - const search = useSearch({ strict: false }) as any; + const search = useSearch({ strict: false }) as { strategyCartId?: string }; const nav = useNavigate(); const { isLoading, isProcessing, isAwaiting, createStrategy } = @@ -35,7 +36,7 @@ export const CreateForm: FC = (props) => { useEffect(() => { const timeout = setTimeout(() => { - createStrategyEvents.change(type, search); + createStrategyEvents.change(type, search as any); }, 1000); return () => clearTimeout(timeout); }, [type, search]); @@ -89,7 +90,7 @@ export const CreateForm: FC = (props) => { const create = (e: FormEvent) => { e.preventDefault(); if (isDisabled(e.currentTarget)) return; - createStrategyEvents.submit(type, search); + createStrategyEvents.submit(type, search as any); createStrategy(); }; @@ -117,21 +118,43 @@ export const CreateForm: FC = (props) => { "I've reviewed the warning(s) but choose to proceed."} - - - {user ? ( + {!search.strategyCartId && ( + + )} + {search.strategyCartId && ( + <> + + + Cancel + + + )} + {user && !search.strategyCartId && ( - ) : ( + )} + {!user && ( -
diff --git a/src/components/cart/utils.ts b/src/components/cart/utils.ts index 0ec9edb59..80a53d135 100644 --- a/src/components/cart/utils.ts +++ b/src/components/cart/utils.ts @@ -9,6 +9,7 @@ import { useGetMultipleTokenPrices } from 'libs/queries/extApi/tokenPrice'; import { SafeDecimal } from 'libs/safedecimal'; import { useEffect, useMemo, useState } from 'react'; import { lsService } from 'services/localeStorage'; +import style from 'components/strategies/overview/StrategyContent.module.css'; export type Cart = (CreateStrategyParams & { id: string })[]; @@ -64,3 +65,55 @@ export const useStrategyCart = () => { }); }, [addresses, cart, getTokenById, priceQueries, selectedFiatCurrency]); }; + +export const removeStrategyFromCart = async (strategy: CartStrategy) => { + // Animate leaving strategy + const keyframes = { opacity: 0, transform: 'scale(0.9)' }; + const option = { + duration: 200, + easing: 'cubic-bezier(.55, 0, 1, .45)', + fill: 'forwards' as const, + }; + await document.getElementById(strategy.id)?.animate(keyframes, option) + .finished; + + // Delete from localstorage + const current = lsService.getItem('cart') ?? []; + const next = current.filter(({ id }) => id !== strategy.id); + lsService.setItem('cart', next); + + // Animate remaining strategies + const selector = `.${style.strategyList} > li`; + const elements = document.querySelectorAll(selector); + const boxes = new Map(); + for (const el of elements) { + boxes.set(el, el.getBoundingClientRect()); + } + let attempts = 0; + const checkChange = () => { + if (attempts > 10) return; + attempts++; + const updated = document.querySelectorAll(selector); + if (elements.length === updated.length) { + return requestAnimationFrame(checkChange); + } + for (const [el, box] of boxes.entries()) { + const newBox = el.getBoundingClientRect(); + if (box.top === newBox.top && box.left === newBox.left) continue; + const keyframes = [ + // eslint-disable-next-line prettier/prettier + { + transform: `translate(${box.left - newBox.left}px, ${ + box.top - newBox.top + }px)`, + }, + { transform: `translate(0px, 0px)` }, + ]; + el.animate(keyframes, { + duration: 300, + easing: 'cubic-bezier(.85, 0, .15, 1)', + }); + } + }; + requestAnimationFrame(checkChange); +}; diff --git a/src/components/strategies/create/CreateForm.tsx b/src/components/strategies/create/CreateForm.tsx index db856bd75..482f080c9 100644 --- a/src/components/strategies/create/CreateForm.tsx +++ b/src/components/strategies/create/CreateForm.tsx @@ -1,7 +1,7 @@ import { FC, FormEvent, MouseEvent, ReactNode, useEffect } from 'react'; import { Token } from 'libs/tokens'; import { createStrategyEvents } from 'services/events/strategyEvents'; -import { Link, useNavigate, useSearch } from 'libs/routing'; +import { useNavigate, useSearch } from 'libs/routing'; import { Button } from 'components/common/button'; import { toCreateStrategyParams, useCreateStrategy } from './useCreateStrategy'; import { getStatusTextByTxStatus } from '../utils'; @@ -12,7 +12,6 @@ import { BaseOrder } from 'components/strategies/common/types'; import { StrategyType } from 'libs/routing'; import { lsService } from 'services/localeStorage'; import style from 'components/strategies/common/form.module.css'; -import { buttonStyles } from 'components/common/button/buttonStyles'; interface FormProps { type: StrategyType; @@ -28,7 +27,7 @@ export const CreateForm: FC = (props) => { const { base, quote, order0, order1, type, children } = props; const { openModal } = useModal(); const { user } = useWagmi(); - const search = useSearch({ strict: false }) as { strategyCartId?: string }; + const search = useSearch({ from: '/trade' }); const nav = useNavigate(); const { isLoading, isProcessing, isAwaiting, createStrategy } = @@ -62,29 +61,23 @@ export const CreateForm: FC = (props) => { if (!form.checkValidity()) return; if (!!form.querySelector('.loading-message')) return; if (!!form.querySelector('.error-message')) return; - const id = search.strategyCartId || crypto.randomUUID(); + const id = crypto.randomUUID(); const strategy = toCreateStrategyParams(base, quote, order0, order1); const list = lsService.getItem('cart') ?? []; - const index = list.findIndex((s) => s.id === id); - if (index === -1) { - list.push({ id, ...strategy }); - // Remove budget - nav({ - to: '.', - search: (s) => { - delete s.budget; - delete s.buyBudget; - delete s.sellBudget; - return s; - }, - replace: false, - resetScroll: false, - }); - } else { - list[index] = { id, ...strategy }; - nav({ to: '/cart' }); - } + list.push({ id, ...strategy }); lsService.setItem('cart', list); + // Remove budget + nav({ + to: '.', + search: (s) => { + delete s.budget; + delete s.buyBudget; + delete s.sellBudget; + return s; + }, + replace: false, + resetScroll: false, + }); }; const create = (e: FormEvent) => { @@ -118,43 +111,20 @@ export const CreateForm: FC = (props) => { "I've reviewed the warning(s) but choose to proceed."} - {!search.strategyCartId && ( - - )} - {search.strategyCartId && ( - <> - - - Cancel - - - )} - {user && !search.strategyCartId && ( + + {user && (
From 08e47631da8438274331b5681f5c14e44641318e Mon Sep 17 00:00:00 2001 From: Grandschtroumpf Date: Mon, 13 Jan 2025 17:12:52 +0100 Subject: [PATCH 10/27] Change icon --- src/components/cart/CartStrategy.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/cart/CartStrategy.tsx b/src/components/cart/CartStrategy.tsx index 8d943c98e..34a63322e 100644 --- a/src/components/cart/CartStrategy.tsx +++ b/src/components/cart/CartStrategy.tsx @@ -3,7 +3,7 @@ import { TokensOverlap } from 'components/common/tokensOverlap'; import { StrategyBlockBudget } from 'components/strategies/overview/strategyBlock/StrategyBlockBudget'; import { StrategyBlockBuySell } from 'components/strategies/overview/strategyBlock/StrategyBlockBuySell'; import { StrategyGraph } from 'components/strategies/overview/strategyBlock/StrategyGraph'; -import { ReactComponent as IconDelete } from 'assets/icons/delete.svg'; +import { ReactComponent as IconTrash } from 'assets/icons/trash.svg'; import { CartStrategy } from 'libs/queries'; import { CSSProperties, FC } from 'react'; import { cn } from 'utils/helpers'; @@ -80,7 +80,7 @@ export const CartStrategyItems: FC = (props) => { aria-label="Delete strategy" onClick={remove} > - + From 81753abb959291168c2215cc1c5bd9c8018c2d86 Mon Sep 17 00:00:00 2001 From: Grandschtroumpf Date: Mon, 13 Jan 2025 17:16:41 +0100 Subject: [PATCH 11/27] empty busget should be a warning --- src/components/cart/CartStrategy.tsx | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/src/components/cart/CartStrategy.tsx b/src/components/cart/CartStrategy.tsx index 34a63322e..6ccd232e5 100644 --- a/src/components/cart/CartStrategy.tsx +++ b/src/components/cart/CartStrategy.tsx @@ -39,11 +39,9 @@ const getWarning = (strategy: CartStrategy, marketPrice?: number) => { buy: false, }); if (sellOutsideMarket) return sellOutsideMarket; -}; -const getError = (strategy: CartStrategy) => { - if (!isZero(strategy.order0.balance)) return ''; - if (!isZero(strategy.order1.balance)) return ''; - return 'Please note that your strategy will be inactive as it will not have any budget.'; + if (!isZero(order0.balance) && isZero(order1.balance)) { + return 'Please note that your strategy will be inactive as it will not have any budget.'; + } }; export const CartStrategyItems: FC = (props) => { @@ -52,7 +50,6 @@ export const CartStrategyItems: FC = (props) => { const { marketPrice } = useMarketPrice({ base, quote }); const warningMsg = getWarning(strategy, marketPrice); - const errorMsg = getError(strategy); const remove = async () => { removeStrategyFromCart(strategy); @@ -86,16 +83,11 @@ export const CartStrategyItems: FC = (props) => {
- {!errorMsg && warningMsg && ( + {warningMsg && (
)} - {errorMsg && ( -
- -
- )}
Date: Mon, 13 Jan 2025 17:17:07 +0100 Subject: [PATCH 12/27] fix typo --- src/components/cart/CartStrategy.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/cart/CartStrategy.tsx b/src/components/cart/CartStrategy.tsx index 6ccd232e5..faff34050 100644 --- a/src/components/cart/CartStrategy.tsx +++ b/src/components/cart/CartStrategy.tsx @@ -39,7 +39,7 @@ const getWarning = (strategy: CartStrategy, marketPrice?: number) => { buy: false, }); if (sellOutsideMarket) return sellOutsideMarket; - if (!isZero(order0.balance) && isZero(order1.balance)) { + if (isZero(order0.balance) && isZero(order1.balance)) { return 'Please note that your strategy will be inactive as it will not have any budget.'; } }; From 4e9f38ce13e6d2890340ac886235e1ab60542706 Mon Sep 17 00:00:00 2001 From: Grandschtroumpf Date: Tue, 14 Jan 2025 10:18:24 +0100 Subject: [PATCH 13/27] add batchCreateBuySellStrategies --- package.json | 2 +- .../core/menu/mainMenu/MainMenuCart.tsx | 1 + src/pages/cart/index.tsx | 47 ++++++++++++++----- src/workers/sdk.ts | 3 ++ yarn.lock | 8 ++-- 5 files changed, 44 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index 07f3f7fe5..1af071700 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "private": true, "dependencies": { "@babel/core": "^7.0.0-0", - "@bancor/carbon-sdk": "0.0.103-DEV", + "@bancor/carbon-sdk": "0.0.104-TEST", "@cloudflare/workers-types": "^4.20230717.0", "@ethersproject/abi": "^5.0.0", "@ethersproject/bytes": "^5.0.0", diff --git a/src/components/core/menu/mainMenu/MainMenuCart.tsx b/src/components/core/menu/mainMenu/MainMenuCart.tsx index 1ede8cdb2..611a1ae71 100644 --- a/src/components/core/menu/mainMenu/MainMenuCart.tsx +++ b/src/components/core/menu/mainMenu/MainMenuCart.tsx @@ -56,6 +56,7 @@ export const MainMenuCart = () => { id="menu-cart-link" to="/cart" className="bg-background-800 grid size-40 rounded-full p-10 [grid-template-areas:'stack']" + aria-label="Cart page" > {!!cartSize && ( diff --git a/src/pages/cart/index.tsx b/src/pages/cart/index.tsx index e64a476e7..b901a4adc 100644 --- a/src/pages/cart/index.tsx +++ b/src/pages/cart/index.tsx @@ -2,18 +2,19 @@ import { CartList } from 'components/cart/CartList'; import { EmptyCart } from 'components/cart/EmptyCart'; import { useStrategyCart } from 'components/cart/utils'; import { Button } from 'components/common/button'; -import { Tooltip } from 'components/common/tooltip/Tooltip'; import { useWagmi } from 'libs/wagmi'; import { cn } from 'utils/helpers'; -import { FormEvent, useMemo } from 'react'; +import { FormEvent, useMemo, useState } from 'react'; import { ApprovalToken, useApproval } from 'hooks/useApproval'; import { CartStrategy, useGetTokenBalances } from 'libs/queries'; import { SafeDecimal } from 'libs/safedecimal'; import { Token } from 'libs/tokens'; import { Warning } from 'components/common/WarningMessageWithIcon'; +import { useModal } from 'hooks/useModal'; +import { carbonSDK } from 'libs/sdk'; +import { useNavigate } from '@tanstack/react-router'; import style from 'components/strategies/common/form.module.css'; import config from 'config'; -import { useModal } from 'hooks/useModal'; const spenderAddress = config.addresses.carbon.carbonController; const getApproveTokens = (strategies: CartStrategy[]) => { @@ -52,8 +53,11 @@ const useHasInsufficientFunds = (approvalTokens: ApprovalToken[]) => { export const CartPage = () => { const strategies = useStrategyCart(); - const { user } = useWagmi(); + const { user, signer } = useWagmi(); const { openModal } = useModal(); + const nav = useNavigate({ from: '/cart' }); + const [pending, setPending] = useState(false); + const approvalTokens = useMemo(() => { return getApproveTokens(strategies); }, [strategies]); @@ -61,7 +65,7 @@ export const CartPage = () => { const approval = useApproval(approvalTokens); const funds = useHasInsufficientFunds(approvalTokens); - const submit = async (e: FormEvent) => { + const submit = (e: FormEvent) => { e.preventDefault(); const form = e.currentTarget; if (!!form.querySelector('.error-message')) return; @@ -71,7 +75,29 @@ export const CartPage = () => { if (approve && !approve.checked) return; } - const create = () => {}; + const create = async () => { + setPending(true); + try { + const params = strategies.map(({ base, quote, order0, order1 }) => ({ + baseToken: base.address, + quoteToken: quote.address, + buyPriceLow: order0.startRate, + buyPriceMarginal: order0.marginalRate || order0.endRate, + buyPriceHigh: order0.endRate, + buyBudget: order0.balance, + sellPriceLow: order1.startRate, + sellPriceMarginal: order1.marginalRate || order1.startRate, + sellPriceHigh: order1.endRate, + sellBudget: order1.balance, + })); + console.log('batchCreateBuySellStrategies', params); + const unsignedTx = await carbonSDK.batchCreateBuySellStrategies(params); + await signer!.sendTransaction(unsignedTx); + nav({ to: '/' }); + } finally { + setPending(false); + } + }; if (approval.approvalRequired) { return openModal('txConfirm', { @@ -80,10 +106,9 @@ export const CartPage = () => { buttonLabel: 'Create Strategy', context: 'createStrategy', }); + } else { + create(); } - // const strategiesData = toStrategyData(strategies); - // const tx = await contract?.write.batchCreate(strategiesData); - // await tx?.wait(); }; if (!strategies.length) { @@ -91,7 +116,6 @@ export const CartPage = () => {

Create multiple strategies -

@@ -108,7 +132,6 @@ export const CartPage = () => { >

Create multiple strategies -

{funds.isInsufficient && ( @@ -136,7 +159,7 @@ export const CartPage = () => { {user && ( - + <> + + + )} {!user && ( + {config.addresses.carbon.batcher && ( + + )}
-

- No Strategies Found -

Your cart is empty

{ > {!!cartSize && ( - + {cartSize} )} diff --git a/src/pages/cart/index.tsx b/src/pages/cart/index.tsx index fe69fcb9c..a3ecb3ed4 100644 --- a/src/pages/cart/index.tsx +++ b/src/pages/cart/index.tsx @@ -117,7 +117,7 @@ export const CartPage = () => { return (

- Create multiple strategies + Create Multiple Strategies

@@ -133,7 +133,7 @@ export const CartPage = () => { onSubmit={submit} >

- Create multiple strategies + Create Multiple Strategies

{funds.isInsufficient && ( From 4c19f433bf67429daa0956c6d86424f34a48a9b3 Mon Sep 17 00:00:00 2001 From: Grandschtroumpf Date: Tue, 14 Jan 2025 15:14:54 +0100 Subject: [PATCH 17/27] Use on cart per user --- src/components/cart/CartStrategy.tsx | 5 +- src/components/cart/utils.ts | 47 ++++++++++++++----- .../core/menu/mainMenu/MainMenuCart.tsx | 19 +++++--- .../menu/mainMenu/MainMenuRightWallet.tsx | 6 +-- .../strategies/create/CreateForm.tsx | 3 +- .../ModalCart/ModalCartConfirmDelete.tsx | 4 +- src/pages/cart/index.tsx | 5 +- src/services/localeStorage/index.ts | 2 +- 8 files changed, 63 insertions(+), 28 deletions(-) diff --git a/src/components/cart/CartStrategy.tsx b/src/components/cart/CartStrategy.tsx index faff34050..360163706 100644 --- a/src/components/cart/CartStrategy.tsx +++ b/src/components/cart/CartStrategy.tsx @@ -14,6 +14,7 @@ import { import { Warning } from 'components/common/WarningMessageWithIcon'; import { useMarketPrice } from 'hooks/useMarketPrice'; import { removeStrategyFromCart } from './utils'; +import { useWagmi } from 'libs/wagmi'; interface Props { strategy: CartStrategy; @@ -48,11 +49,13 @@ export const CartStrategyItems: FC = (props) => { const { strategy, style, className } = props; const { base, quote } = strategy; const { marketPrice } = useMarketPrice({ base, quote }); + const { user } = useWagmi(); const warningMsg = getWarning(strategy, marketPrice); const remove = async () => { - removeStrategyFromCart(strategy); + if (!user) return; + removeStrategyFromCart(user, strategy); }; return ( diff --git a/src/components/cart/utils.ts b/src/components/cart/utils.ts index 5b36bbc1d..0f0e0b870 100644 --- a/src/components/cart/utils.ts +++ b/src/components/cart/utils.ts @@ -9,6 +9,7 @@ import { useGetMultipleTokenPrices } from 'libs/queries/extApi/tokenPrice'; import { SafeDecimal } from 'libs/safedecimal'; import { useEffect, useMemo, useState } from 'react'; import { lsService } from 'services/localeStorage'; +import { useWagmi } from 'libs/wagmi'; import strategyStyle from 'components/strategies/overview/StrategyContent.module.css'; import formStyle from 'components/strategies/common/form.module.css'; @@ -22,7 +23,8 @@ const toOrder = (sdkOrder: CreateStrategyOrder) => ({ }); export const useStrategyCart = () => { - const [cart, setCart] = useState(lsService.getItem('cart') ?? []); + const [cart, setCart] = useState([]); + const { user } = useWagmi(); const { getTokenById } = useTokens(); const { selectedFiatCurrency } = useFiatCurrency(); @@ -30,11 +32,18 @@ export const useStrategyCart = () => { const addresses = Array.from(new Set(tokens)); const priceQueries = useGetMultipleTokenPrices(addresses); + useEffect(() => { + if (!user) return setCart([]); + const carts = lsService.getItem('carts') ?? {}; + setCart(carts[user] ?? []); + }, [user]); + useEffect(() => { const handler = (event: StorageEvent) => { - if (event.key !== lsService.keyFormatter('cart')) return; - const next = JSON.parse(event.newValue ?? '[]'); - setCart(next); + if (event.key !== lsService.keyFormatter('carts')) return; + if (!user) return; + const next = JSON.parse(event.newValue ?? '{}'); + setCart(next[user] ?? []); }; window.addEventListener('storage', handler); return () => window.removeEventListener('storage', handler); @@ -67,11 +76,15 @@ export const useStrategyCart = () => { }, [addresses, cart, getTokenById, priceQueries, selectedFiatCurrency]); }; -export const addStrategyToCart = (params: CreateStrategyParams) => { +export const addStrategyToCart = ( + user: string, + params: CreateStrategyParams +) => { const id = crypto.randomUUID(); - const list = lsService.getItem('cart') ?? []; - list.push({ id, ...params }); - lsService.setItem('cart', list); + const carts = lsService.getItem('carts') ?? {}; + carts[user] ||= []; + carts[user].push({ id, ...params }); + lsService.setItem('carts', carts); // Animation const getTranslate = (target: HTMLElement, elRect: DOMRect) => { @@ -105,7 +118,10 @@ export const addStrategyToCart = (params: CreateStrategyParams) => { return animation.finished; }; -export const removeStrategyFromCart = async (strategy: CartStrategy) => { +export const removeStrategyFromCart = async ( + user: string, + strategy: CartStrategy +) => { // Animate leaving strategy const keyframes = { opacity: 0, transform: 'scale(0.9)' }; const option = { @@ -117,9 +133,10 @@ export const removeStrategyFromCart = async (strategy: CartStrategy) => { .finished; // Delete from localstorage - const current = lsService.getItem('cart') ?? []; - const next = current.filter(({ id }) => id !== strategy.id); - lsService.setItem('cart', next); + const current = lsService.getItem('carts') ?? {}; + if (!current[user]?.length) return; + current[user] = current[user].filter(({ id }) => id !== strategy.id); + lsService.setItem('carts', current); // Animate remaining strategies const selector = `.${strategyStyle.strategyList} > li`; @@ -156,3 +173,9 @@ export const removeStrategyFromCart = async (strategy: CartStrategy) => { }; requestAnimationFrame(checkChange); }; + +export const clearCart = (user: string) => { + const current = lsService.getItem('carts') ?? {}; + current[user] = []; + lsService.setItem('carts', current); +}; diff --git a/src/components/core/menu/mainMenu/MainMenuCart.tsx b/src/components/core/menu/mainMenu/MainMenuCart.tsx index 17c7ae10a..62e32de7d 100644 --- a/src/components/core/menu/mainMenu/MainMenuCart.tsx +++ b/src/components/core/menu/mainMenu/MainMenuCart.tsx @@ -1,17 +1,24 @@ import { Link } from '@tanstack/react-router'; import { ReactComponent as CartIcon } from 'assets/icons/cart.svg'; +import { useWagmi } from 'libs/wagmi'; import { useEffect, useState } from 'react'; import { lsService } from 'services/localeStorage'; export const MainMenuCart = () => { - const [cartSize, setCartsize] = useState( - lsService.getItem('cart')?.length ?? 0 - ); + const { user } = useWagmi(); + const [cartSize, setCartsize] = useState(0); + useEffect(() => { + if (!user) return setCartsize(0); + const carts = lsService.getItem('carts') ?? {}; + setCartsize((carts[user] ?? []).length); + }, [user]); + useEffect(() => { const handler = (event: StorageEvent) => { - if (event.key !== lsService.keyFormatter('cart')) return; - const next = JSON.parse(event.newValue ?? '[]'); - setCartsize(next.length); + if (event.key !== lsService.keyFormatter('carts')) return; + if (!user) return; + const next = JSON.parse(event.newValue ?? '{}'); + setCartsize((next[user] ?? []).length); }; window.addEventListener('storage', handler); return () => window.removeEventListener('storage', handler); diff --git a/src/components/core/menu/mainMenu/MainMenuRightWallet.tsx b/src/components/core/menu/mainMenu/MainMenuRightWallet.tsx index 99adbf80e..abae20bf1 100644 --- a/src/components/core/menu/mainMenu/MainMenuRightWallet.tsx +++ b/src/components/core/menu/mainMenu/MainMenuRightWallet.tsx @@ -14,7 +14,7 @@ import { cn, shortenString } from 'utils/helpers'; import { useGetEnsFromAddress } from 'libs/queries/chain/ens'; import { WalletIcon } from 'components/common/WalletIcon'; -const iconProps = { className: 'w-20' }; +const iconProps = { className: 'w-20 hidden lg:block' }; export const MainMenuRightWallet: FC = () => { const { @@ -78,7 +78,7 @@ export const MainMenuRightWallet: FC = () => { {...attr} className={cn( buttonStyles({ variant: buttonVariant }), - 'flex items-center space-x-10 pl-20' + 'flex items-center gap-10 px-12' )} onClick={(e) => { carbonEvents.navigation.navWalletClick(undefined); @@ -100,7 +100,7 @@ export const MainMenuRightWallet: FC = () => { -

- Editing prices is cheaper and keeps your strategy working for you. -

- - - - - ); -}; diff --git a/src/libs/modals/modals/index.ts b/src/libs/modals/modals/index.ts index 9826c51dd..23969c2b3 100644 --- a/src/libs/modals/modals/index.ts +++ b/src/libs/modals/modals/index.ts @@ -56,10 +56,6 @@ import { ModalSimulatorDisclaimer, ModalSimulatorDisclaimerData, } from 'libs/modals/modals/ModalSimulatorDisclaimer'; -import { - ModalCartConfirmDelete, - ModalCartConfirmDeleteData, -} from './ModalCart/ModalCartConfirmDelete'; // Step 1: Add modal key and data type to schema export interface ModalSchema { @@ -80,7 +76,6 @@ export interface ModalSchema { confirmDeleteStrategy: ModalConfirmDeleteData; simulatorDisclaimer: ModalSimulatorDisclaimerData; withdrawOrDelete: ModalWithdrawOrDeleteData; - confirmDeleteCartStrategy: ModalCartConfirmDeleteData; } // Step 2: Create component in modals/modals folder @@ -104,5 +99,4 @@ export const MODAL_COMPONENTS: TModals = { confirmDeleteStrategy: (props) => ModalConfirmDelete(props), simulatorDisclaimer: (props) => ModalSimulatorDisclaimer(props), withdrawOrDelete: (props) => ModalWithdrawOrDelete(props), - confirmDeleteCartStrategy: (props) => ModalCartConfirmDelete(props), }; From 28f922c7a31651171fce38b81981409ea9ae3d2d Mon Sep 17 00:00:00 2001 From: Grandschtroumpf Date: Wed, 15 Jan 2025 10:13:44 +0100 Subject: [PATCH 22/27] add confirmation text & notification --- src/libs/notifications/data.ts | 14 ++++++++++++++ src/pages/cart/index.tsx | 25 +++++++++++++++++++------ 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/src/libs/notifications/data.ts b/src/libs/notifications/data.ts index 56804c402..fcdbbb564 100644 --- a/src/libs/notifications/data.ts +++ b/src/libs/notifications/data.ts @@ -9,6 +9,7 @@ export interface NotificationSchema { approveError: { symbol: string }; trade: { txHash: string; amount: string; from: string; to: string }; createStrategy: { txHash: string }; + createBatchStrategy: { txHash: string }; pauseStrategy: { txHash: string }; renewStrategy: { txHash: string }; editStrategyName: { txHash: string }; @@ -82,6 +83,19 @@ export const NOTIFICATIONS_MAP: NotificationsMap = { showAlert: true, testid: 'create-strategy', }), + createBatchStrategy: (data) => ({ + type: 'tx', + status: 'pending', + title: 'Pending Confirmation', + description: 'New batch of strategies are being created.', + successTitle: 'Success', + successDesc: 'New batch of strategies were successfully created.', + failedTitle: 'Transaction Failed', + failedDesc: 'New batch of strategy creation has failed.', + txHash: data.txHash, + showAlert: true, + testid: 'create-batch-strategy', + }), pauseStrategy: (data) => ({ type: 'tx', status: 'pending', diff --git a/src/pages/cart/index.tsx b/src/pages/cart/index.tsx index 64a729d9b..7fe1abc5d 100644 --- a/src/pages/cart/index.tsx +++ b/src/pages/cart/index.tsx @@ -13,6 +13,7 @@ import { Warning } from 'components/common/WarningMessageWithIcon'; import { useModal } from 'hooks/useModal'; import { carbonSDK } from 'libs/sdk'; import { useNavigate } from '@tanstack/react-router'; +import { useNotifications } from 'hooks/useNotifications'; import style from 'components/strategies/common/form.module.css'; import config from 'config'; @@ -56,8 +57,11 @@ export const CartPage = () => { const strategies = useStrategyCart(); const { user, signer } = useWagmi(); const { openModal } = useModal(); + const { dispatchNotification } = useNotifications(); + const nav = useNavigate({ from: '/cart' }); - const [pending, setPending] = useState(false); + const [confirmation, setConfirmation] = useState(false); + const [processing, setProcessing] = useState(false); const approvalTokens = useMemo(() => { return getApproveTokens(strategies); @@ -77,7 +81,7 @@ export const CartPage = () => { } const create = async () => { - setPending(true); + setConfirmation(true); try { const params = strategies.map(({ base, quote, order0, order1 }) => ({ baseToken: base.address, @@ -92,11 +96,16 @@ export const CartPage = () => { sellBudget: order1.balance, })); const unsignedTx = await carbonSDK.batchCreateBuySellStrategies(params); - await signer!.sendTransaction(unsignedTx); + const tx = await signer!.sendTransaction(unsignedTx); + setConfirmation(false); + setProcessing(true); + dispatchNotification('createBatchStrategy', { txHash: tx.hash }); + await tx.wait(); clearCart(user!); nav({ to: '/' }); } finally { - setPending(false); + setConfirmation(false); + setProcessing(false); } }; @@ -160,9 +169,13 @@ export const CartPage = () => { From 95f49674055db8a4a71b1b696842157784e4156e Mon Sep 17 00:00:00 2001 From: Grandschtroumpf Date: Wed, 15 Jan 2025 10:27:43 +0100 Subject: [PATCH 23/27] request changes --- src/components/cart/EmptyCart.tsx | 3 +++ src/pages/cart/index.tsx | 5 +---- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/cart/EmptyCart.tsx b/src/components/cart/EmptyCart.tsx index d26bb87d5..cfbf7e1b2 100644 --- a/src/components/cart/EmptyCart.tsx +++ b/src/components/cart/EmptyCart.tsx @@ -11,6 +11,9 @@ export const EmptyCart = () => { 0
+

+ No Strategies Found +

Your cart is empty

{ if (!strategies.length) { return (
-

- Create Multiple Strategies -

); @@ -175,7 +172,7 @@ export const CartPage = () => { confirmation ? 'Waiting for Confirmation' : 'Processing' } variant="success" - className="mt-20 place-self-center" + className="mt-10 place-self-center" > Sign all strategies From 3f361f01566368d9bc064db7a1f19da103c91b2d Mon Sep 17 00:00:00 2001 From: Grandschtroumpf Date: Fri, 17 Jan 2025 10:50:39 +0100 Subject: [PATCH 24/27] request changes --- src/components/cart/EmptyCart.tsx | 3 +-- src/components/core/menu/mainMenu/MainMenuCart.tsx | 4 ++-- src/libs/modals/modals/ModalConfirm/ModalConfirm.tsx | 3 ++- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/cart/EmptyCart.tsx b/src/components/cart/EmptyCart.tsx index cfbf7e1b2..63b7c7aec 100644 --- a/src/components/cart/EmptyCart.tsx +++ b/src/components/cart/EmptyCart.tsx @@ -12,9 +12,8 @@ export const EmptyCart = () => {

- No Strategies Found + Your Cart is Empty

-

Your cart is empty

{ {!!cartSize && ( - + {cartSize} )} diff --git a/src/libs/modals/modals/ModalConfirm/ModalConfirm.tsx b/src/libs/modals/modals/ModalConfirm/ModalConfirm.tsx index b026b7cad..92673c9cf 100644 --- a/src/libs/modals/modals/ModalConfirm/ModalConfirm.tsx +++ b/src/libs/modals/modals/ModalConfirm/ModalConfirm.tsx @@ -52,7 +52,7 @@ export const ModalConfirm: ModalFC = ({ data-testid="approval-modal" >

Approve Tokens

-
    +
      {approvalQuery.map(({ data, isPending, error }, i) => (
    • = ({ await onConfirm(); handleAfterConfirmationEvent(eventData, context); }} + className="shrink-0" data-testid="approve-submit" > {buttonLabel} From c5202f13d70210c8a7ee6ce918d0c6bedd3f7898 Mon Sep 17 00:00:00 2001 From: Grandschtroumpf Date: Wed, 22 Jan 2025 15:17:27 +0100 Subject: [PATCH 25/27] Capitalize --- src/components/strategies/create/CreateForm.tsx | 2 +- src/pages/cart/index.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/strategies/create/CreateForm.tsx b/src/components/strategies/create/CreateForm.tsx index d6d6bff11..1983d816f 100644 --- a/src/components/strategies/create/CreateForm.tsx +++ b/src/components/strategies/create/CreateForm.tsx @@ -135,7 +135,7 @@ export const CreateForm: FC = (props) => { onClick={addToCart} data-testid="add-strategy-to-cart" > - Add to cart + Add to Cart )} ); From 3b4fe2fe0ff5f0f3eab051d6c27291e854b4fddc Mon Sep 17 00:00:00 2001 From: Grandschtroumpf Date: Wed, 22 Jan 2025 15:47:58 +0100 Subject: [PATCH 26/27] e2e await for price in tooltips --- e2e/tests/strategy/disposable/duplicate.ts | 6 +++--- e2e/tests/strategy/disposable/undercut.ts | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/e2e/tests/strategy/disposable/duplicate.ts b/e2e/tests/strategy/disposable/duplicate.ts index 1bc924860..6596f881f 100644 --- a/e2e/tests/strategy/disposable/duplicate.ts +++ b/e2e/tests/strategy/disposable/duplicate.ts @@ -47,10 +47,10 @@ export const duplicate = (testCase: CreateStrategyTestCase) => { const tooltip = await duplicate.priceTooltip(direction); if (setting === 'limit') { - expect(tooltip.price()).toHaveText(output.min); + await expect(tooltip.price()).toHaveText(output.min); } else { - expect(tooltip.minPrice()).toHaveText(output.min); - expect(tooltip.maxPrice()).toHaveText(output.max); + await expect(tooltip.minPrice()).toHaveText(output.min); + await expect(tooltip.maxPrice()).toHaveText(output.max); } await tooltip.waitForDetached(); }); diff --git a/e2e/tests/strategy/disposable/undercut.ts b/e2e/tests/strategy/disposable/undercut.ts index 2a7c15f55..0246f5168 100644 --- a/e2e/tests/strategy/disposable/undercut.ts +++ b/e2e/tests/strategy/disposable/undercut.ts @@ -48,10 +48,10 @@ export const undercut = (testCase: CreateStrategyTestCase) => { const tooltip = await duplicate.priceTooltip(direction); if (setting === 'limit') { - expect(tooltip.price()).toHaveText(output.min); + await expect(tooltip.price()).toHaveText(output.min); } else { - expect(tooltip.minPrice()).toHaveText(output.min); - expect(tooltip.maxPrice()).toHaveText(output.max); + await expect(tooltip.minPrice()).toHaveText(output.min); + await expect(tooltip.maxPrice()).toHaveText(output.max); } await tooltip.waitForDetached(); }); From b214b82b7052a19cd783721746dff1f9df5b7eba Mon Sep 17 00:00:00 2001 From: Grandschtroumpf Date: Wed, 22 Jan 2025 16:32:30 +0100 Subject: [PATCH 27/27] fix warning for overlapping strategy in cart --- src/components/cart/CartStrategy.tsx | 45 ++++++++++++++++++---------- 1 file changed, 29 insertions(+), 16 deletions(-) diff --git a/src/components/cart/CartStrategy.tsx b/src/components/cart/CartStrategy.tsx index 360163706..873efda5d 100644 --- a/src/components/cart/CartStrategy.tsx +++ b/src/components/cart/CartStrategy.tsx @@ -8,6 +8,7 @@ import { CartStrategy } from 'libs/queries'; import { CSSProperties, FC } from 'react'; import { cn } from 'utils/helpers'; import { + isOverlappingStrategy, isZero, outSideMarketWarning, } from 'components/strategies/common/utils'; @@ -15,6 +16,10 @@ import { Warning } from 'components/common/WarningMessageWithIcon'; import { useMarketPrice } from 'hooks/useMarketPrice'; import { removeStrategyFromCart } from './utils'; import { useWagmi } from 'libs/wagmi'; +import { + isMaxBelowMarket, + isMinAboveMarket, +} from 'components/strategies/overlapping/utils'; interface Props { strategy: CartStrategy; @@ -24,25 +29,33 @@ interface Props { const getWarning = (strategy: CartStrategy, marketPrice?: number) => { const { base, order0, order1 } = strategy; - const buyOutsideMarket = outSideMarketWarning({ - base, - marketPrice, - min: order0.startRate, - max: order0.endRate, - buy: true, - }); - if (buyOutsideMarket) return buyOutsideMarket; - const sellOutsideMarket = outSideMarketWarning({ - base, - marketPrice, - min: order1.startRate, - max: order1.endRate, - buy: false, - }); - if (sellOutsideMarket) return sellOutsideMarket; if (isZero(order0.balance) && isZero(order1.balance)) { return 'Please note that your strategy will be inactive as it will not have any budget.'; } + if (isOverlappingStrategy(strategy)) { + const aboveMarket = isMinAboveMarket(order0); + const belowMarket = isMaxBelowMarket(order1); + if (aboveMarket || belowMarket) { + return 'Notice: your strategy is “out of the money” and will be traded when the market price moves into your price range.'; + } + } else { + const buyOutsideMarket = outSideMarketWarning({ + base, + marketPrice, + min: order0.startRate, + max: order0.endRate, + buy: true, + }); + if (buyOutsideMarket) return buyOutsideMarket; + const sellOutsideMarket = outSideMarketWarning({ + base, + marketPrice, + min: order1.startRate, + max: order1.endRate, + buy: false, + }); + if (sellOutsideMarket) return sellOutsideMarket; + } }; export const CartStrategyItems: FC = (props) => {