Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Batch creation #1629

Merged
merged 40 commits into from
Feb 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
0be29ab
setup cart logic
GrandSchtroumpf Jan 10, 2025
dd56466
improve cart interaction
GrandSchtroumpf Jan 10, 2025
eedc129
setup batcher contract
GrandSchtroumpf Jan 10, 2025
cb61218
update edit button
GrandSchtroumpf Jan 13, 2025
79c25eb
add confirm modal
GrandSchtroumpf Jan 13, 2025
9d3e1ca
add strategy card warning & error
GrandSchtroumpf Jan 13, 2025
bd2d0bd
add approval
GrandSchtroumpf Jan 13, 2025
427c7fe
remove edit strategy from batch
GrandSchtroumpf Jan 13, 2025
912f16b
rename icon
GrandSchtroumpf Jan 13, 2025
08e4763
Change icon
GrandSchtroumpf Jan 13, 2025
0df52e2
Merge remote-tracking branch 'origin/main' into issue-#1560
GrandSchtroumpf Jan 13, 2025
81753ab
empty busget should be a warning
GrandSchtroumpf Jan 13, 2025
0049932
fix typo
GrandSchtroumpf Jan 13, 2025
4e9f38c
add batchCreateBuySellStrategies
GrandSchtroumpf Jan 14, 2025
594a363
reorganise add to cart
GrandSchtroumpf Jan 14, 2025
67e7df7
do not display cart if there is no batcher
GrandSchtroumpf Jan 14, 2025
2caaa35
request changes
GrandSchtroumpf Jan 14, 2025
4c19f43
Use on cart per user
GrandSchtroumpf Jan 14, 2025
e6a910e
change cta in approval
GrandSchtroumpf Jan 14, 2025
9194502
add border
GrandSchtroumpf Jan 15, 2025
988af4d
add page indicator
GrandSchtroumpf Jan 15, 2025
fdc76d9
remove modal
GrandSchtroumpf Jan 15, 2025
28f922c
add confirmation text & notification
GrandSchtroumpf Jan 15, 2025
95f4967
request changes
GrandSchtroumpf Jan 15, 2025
3f361f0
request changes
GrandSchtroumpf Jan 17, 2025
b1ec9e5
Merge remote-tracking branch 'origin/main' into issue-#1560
GrandSchtroumpf Jan 22, 2025
c5202f1
Capitalize
GrandSchtroumpf Jan 22, 2025
3b4fe2f
e2e await for price in tooltips
GrandSchtroumpf Jan 22, 2025
b214b82
fix warning for overlapping strategy in cart
GrandSchtroumpf Jan 22, 2025
c76ada7
Merge remote-tracking branch 'origin/main' into issue-#1560
GrandSchtroumpf Feb 3, 2025
4b3aa2e
add config to batch
GrandSchtroumpf Feb 3, 2025
dd6d6ac
remove batcher abi
GrandSchtroumpf Feb 4, 2025
1fb2029
Merge remote-tracking branch 'origin/main' into issue-#1560
GrandSchtroumpf Feb 4, 2025
7ab7570
Merge remote-tracking branch 'origin/main' into issue-#1560
GrandSchtroumpf Feb 4, 2025
09c3e97
update SDK
GrandSchtroumpf Feb 4, 2025
63e4c48
Always display checkbox
GrandSchtroumpf Feb 5, 2025
3806533
[CI] Update Screenshots
GrandSchtroumpf Feb 5, 2025
5201884
Merge remote-tracking branch 'origin/main' into issue-#1560
GrandSchtroumpf Feb 5, 2025
95ddf95
change test
GrandSchtroumpf Feb 5, 2025
bb90cdd
[CI] Update Screenshots
GrandSchtroumpf Feb 5, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
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.
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