Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into issue-#1648
Browse files Browse the repository at this point in the history
  • Loading branch information
GrandSchtroumpf committed Feb 6, 2025
2 parents cba5eb0 + 05a8ecc commit 0955b83
Show file tree
Hide file tree
Showing 71 changed files with 922 additions and 118 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,7 @@ The file `common.ts` with type [`AppConfig`](src/config/types.ts) contains impor
- `currencyMenu`: Display the currency menu to switch between currencies.
- `showTerms`: Display the terms page & links. ⚠️ If you set it to true, you need to change the content of the page ⚠️
- `showPrivacy`: Display the privacy page & links. ⚠️ If you set it to true, you need to change the content of the page ⚠️
- `showCart`: Allow to use the cart page to batch create strategies. You need to set the batcher contract address under `addresses.carbon.batcher`.

#### Gas token different than native token

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified e2e/screenshots/strategy/overlapping/Overlapping/create/form.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.105-DEV",
"@cloudflare/workers-types": "^4.20230717.0",
"@ethersproject/abi": "^5.0.0",
"@ethersproject/bytes": "^5.0.0",
Expand Down
3 changes: 3 additions & 0 deletions src/assets/icons/cart.svg
28 changes: 28 additions & 0 deletions src/components/cart/CartList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { FC } from 'react';
import { CartStrategy } from 'libs/queries';
import { CartStrategyItems } from './CartStrategy';
import { cn } from 'utils/helpers';
import styles from 'components/strategies/overview/StrategyContent.module.css';

interface Props {
strategies: CartStrategy[];
}

export const CartList: FC<Props> = ({ strategies }) => {
return (
<ul className={cn('grid gap-20', styles.strategyList)}>
{strategies.map((strategy, i) => {
const className = i < 12 ? styles.animateItem : '';
const style = { ['--delay' as any]: `${i * 50}ms` };
return (
<CartStrategyItems
key={strategy.id}
strategy={strategy}
style={style}
className={className}
/>
);
})}
</ul>
);
};
121 changes: 121 additions & 0 deletions src/components/cart/CartStrategy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { PairName } from 'components/common/DisplayPair';
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 IconTrash } from 'assets/icons/trash.svg';
import { CartStrategy } from 'libs/queries';
import { CSSProperties, FC } from 'react';
import { cn } from 'utils/helpers';
import {
isOverlappingStrategy,
isZero,
outSideMarketWarning,
} from 'components/strategies/common/utils';
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;
className?: string;
style?: CSSProperties;
}

const getWarning = (strategy: CartStrategy, marketPrice?: number) => {
const { base, order0, order1 } = strategy;
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> = (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 () => {
if (!user) return;
removeStrategyFromCart(user, strategy);
};

return (
<li
id={strategy.id}
style={style}
className={cn(
'rounded-10 bg-background-900 grid grid-cols-1 grid-rows-[auto_auto_auto] gap-16 p-24',
className
)}
>
<header className="flex items-center gap-16">
<TokensOverlap size={40} tokens={[base, quote]} />
<h3 className="text-18 flex gap-6" data-testid="token-pair">
<PairName baseToken={base} quoteToken={quote} />
</h3>
<div role="menubar" className="ml-auto flex gap-8">
<button
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}
>
<IconTrash className="size-16" />
</button>
</div>
</header>
<div className="relative">
<StrategyBlockBudget strategy={strategy} />
{warningMsg && (
<div className="rounded-8 border-warning absolute inset-0 grid place-items-center border-2 bg-black/60 p-8 backdrop-blur-sm">
<Warning message={warningMsg} />
</div>
)}
</div>
<div className="rounded-8 border-background-800 grid grid-cols-2 grid-rows-[auto_auto] border-2">
<StrategyBlockBuySell
strategy={strategy}
buy
className="border-background-800 border-r-2"
/>
<StrategyBlockBuySell strategy={strategy} />
<div className="border-background-800 col-start-1 col-end-3 border-t-2">
<StrategyGraph strategy={strategy} />
</div>
</div>
</li>
);
};
25 changes: 25 additions & 0 deletions src/components/cart/EmptyCart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
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 (
<section className="gap-30 py-50 border-background-800 animate-fade relative mx-auto grid h-[600px] w-full max-w-[1240px] place-items-center content-center rounded border-2 px-20 text-center">
<div className="bg-background-800 grid rounded-full p-16 [grid-template-areas:'stack']">
<CartIcon className="size-40 place-self-center text-white [grid-area:stack]" />
<span className="bg-success-light grid size-16 place-items-center justify-self-end rounded-full text-[8px] leading-[1.4] text-black [grid-area:stack]">
0
</span>
</div>
<h2 className="max-w-[440px] text-[32px] leading-[36px]">
Your Cart is Empty
</h2>
<Link
to="/trade/disposable"
className={buttonStyles({ variant: 'success' })}
>
Create Strategy
</Link>
</section>
);
};
181 changes: 181 additions & 0 deletions src/components/cart/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
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';
import { useWagmi } from 'libs/wagmi';
import strategyStyle from 'components/strategies/overview/StrategyContent.module.css';
import formStyle from 'components/strategies/common/form.module.css';

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<Cart>([]);
const { user } = useWagmi();
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(() => {
if (!user) return setCart([]);
const carts = lsService.getItem('carts') ?? {};
setCart(carts[user] ?? []);
}, [user]);

useEffect(() => {
const handler = (event: StorageEvent) => {
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);
});

return useMemo(() => {
const prices: Record<string, number | undefined> = {};
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]);
};

export const addStrategyToCart = (
user: string,
params: CreateStrategyParams
) => {
const id = crypto.randomUUID();
const carts = lsService.getItem('carts') ?? {};
carts[user] ||= [];
carts[user].push({ id, ...params });
lsService.setItem('carts', carts);

// Animation
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 source = document.querySelector<HTMLElement>(`.${formStyle.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);
const animation = 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)',
}
);
return animation.finished;
};

export const removeStrategyFromCart = async (
user: string,
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('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`;
const elements = document.querySelectorAll<HTMLElement>(selector);
const boxes = new Map<HTMLElement, DOMRect>();
for (const el of elements) {
boxes.set(el, el.getBoundingClientRect());
}
let attempts = 0;
const checkChange = () => {
if (attempts > 10) return;
attempts++;
const updated = document.querySelectorAll<HTMLElement>(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 clearCart = (user: string) => {
const current = lsService.getItem('carts') ?? {};
current[user] = [];
lsService.setItem('carts', current);
};
1 change: 1 addition & 0 deletions src/components/common/approval/ApproveToken.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export const ApproveToken: FC<Props> = ({

const onApprove = async (e: FormEvent) => {
e.preventDefault();
e.stopPropagation();
if (!data || !token) {
return console.error('No data loaded');
}
Expand Down
Loading

0 comments on commit 0955b83

Please sign in to comment.