Skip to content

Commit 122b15f

Browse files
norugpullgrinry
andauthored
SOV-5058: ERC-20 bridge (#1089)
* feat: erc20 bridge initilized * feat(web): bridge flow sdk * fix: build error * fix: chain id type * feat: add missing files * fix: bridge ui issues * fix: aggregator balance issue * fix: review screen data * feat: bridge receive flow * fix: bridge send flow issues * fix: ethereum asset addresses * fix: assets invalid contract address. * fix: erc20 bridge improvements * fix: erc20 tx hash link * fix: adjust erc20 bridge * fix: erc20 bridge issues * fix: bridge issues * fix: erc20 bridge issues * fix: update bridge service * fix: bridge ui issues * fix: bridge receiver address * fix: remove un-used types * fix: bridge issues * fix: erc20 bridge switch network to RSK on close * fix: remove BSC and ETH from dropdown * fix: remove switch network from erc20 bridge --------- Co-authored-by: Rytis Grincevicius <[email protected]>
1 parent a3de1ac commit 122b15f

File tree

72 files changed

+6582
-28
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

72 files changed

+6582
-28
lines changed

apps/frontend/src/app/1_atoms/GoBackButton/GoBackButton.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,18 @@ import { Icon, IconNames } from '@sovryn/ui';
44

55
type GoBackButtonProps = {
66
onClick: () => void;
7+
disabled?: boolean;
78
};
89

9-
export const GoBackButton: React.FC<GoBackButtonProps> = ({ onClick }) => (
10-
<button onClick={onClick} className="mb-12 md:mb-0">
10+
export const GoBackButton: React.FC<GoBackButtonProps> = ({
11+
onClick,
12+
disabled,
13+
}) => (
14+
<button
15+
onClick={onClick}
16+
disabled={disabled}
17+
className="mb-12 md:mb-0 disabled:opacity-60"
18+
>
1119
<Icon icon={IconNames.ARROW_LEFT} className="w-3 h-3" />
1220
</button>
1321
);
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import React from 'react';
2+
3+
import classNames from 'classnames';
4+
5+
export interface StepperProps {
6+
steps: number;
7+
activeStep: number;
8+
className?: string;
9+
}
10+
11+
export const Stepper: React.FC<StepperProps> = ({
12+
steps,
13+
activeStep,
14+
className,
15+
}) => (
16+
<div className={classNames('flex items-center gap-2 w-full', className)}>
17+
{Array.from({ length: steps }, (_, index) => (
18+
<div
19+
className={classNames('h-1 flex-1 rounded-full transition-colors', {
20+
'bg-primary-20': index < activeStep,
21+
'bg-gray-40': index >= activeStep,
22+
})}
23+
key={index}
24+
/>
25+
))}
26+
</div>
27+
);

apps/frontend/src/app/2_molecules/NetworkPicker/NetworkPicker.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export const NetworkPicker: FC<NetworkPickerProps> = ({ className }) => {
3535
dropdownClassName="z-[10000000]"
3636
>
3737
<Menu>
38-
{APP_CHAIN_LIST.map(item => (
38+
{APP_CHAIN_LIST.filter(item => item.isVisible).map(item => (
3939
<MenuItem
4040
key={item.id}
4141
text={
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { QueryClient } from '@tanstack/react-query';
2+
3+
export const ACTIVE_CLASSNAME = 'border-t-primary-30';
4+
5+
export const queryClient = new QueryClient();
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import { QueryClientProvider } from '@tanstack/react-query';
2+
3+
import React, { useCallback, useEffect, useMemo, useState } from 'react';
4+
5+
import { t } from 'i18next';
6+
7+
import {
8+
AddressBadge,
9+
Button,
10+
ButtonStyle,
11+
Dialog,
12+
DialogSize,
13+
Heading,
14+
Tabs,
15+
VerticalTabs,
16+
} from '@sovryn/ui';
17+
18+
import { RSK_CHAIN_ID } from '../../../config/chains';
19+
20+
import { MobileCloseButton } from '../../1_atoms/MobileCloseButton/MobileCloseButton';
21+
import { useAccount } from '../../../hooks/useAccount';
22+
import { useChainStore } from '../../../hooks/useChainStore';
23+
import { useIsMobile } from '../../../hooks/useIsMobile';
24+
import { translations } from '../../../locales/i18n';
25+
import { ACTIVE_CLASSNAME, queryClient } from './ERC20BridgeDialog.constants';
26+
import { ReceiveFlow } from './components/ReceiveFlow/ReceiveFlow';
27+
import { SendFlow } from './components/SendFlow/SendFlow';
28+
import { ReceiveFlowContextProvider } from './contextproviders/ReceiveFlowContext';
29+
import { SendFlowContextProvider } from './contextproviders/SendFlowContext';
30+
31+
const translation = translations.erc20Bridge.mainScreen;
32+
33+
type ERC20BridgeDialogProps = {
34+
isOpen: boolean;
35+
onClose: () => void;
36+
step?: number;
37+
};
38+
39+
export const ERC20BridgeDialog: React.FC<ERC20BridgeDialogProps> = ({
40+
isOpen,
41+
onClose,
42+
step = 0,
43+
}) => {
44+
const [index, setIndex] = useState(step);
45+
const { account } = useAccount();
46+
const { isMobile } = useIsMobile();
47+
const { currentChainId, setCurrentChainId } = useChainStore();
48+
const isWrongChain = RSK_CHAIN_ID !== currentChainId;
49+
50+
const handleClose = useCallback(() => {
51+
if (isWrongChain) {
52+
setCurrentChainId(RSK_CHAIN_ID);
53+
}
54+
onClose();
55+
}, [onClose, isWrongChain, setCurrentChainId]);
56+
57+
useEffect(() => {
58+
setIndex(step);
59+
}, [step]);
60+
61+
const items = useMemo(() => {
62+
return [
63+
{
64+
label: t(translation.tabs.receiveLabel),
65+
infoText: t(translation.tabs.receiveInfoText),
66+
content: (
67+
<ReceiveFlowContextProvider>
68+
<ReceiveFlow onClose={handleClose} />
69+
<MobileCloseButton onClick={handleClose} />
70+
</ReceiveFlowContextProvider>
71+
),
72+
activeClassName: ACTIVE_CLASSNAME,
73+
dataAttribute: 'erc20-bridge-receive',
74+
},
75+
{
76+
label: t(translation.tabs.sendLabel),
77+
infoText: t(translation.tabs.sendInfoText),
78+
content: (
79+
<SendFlowContextProvider>
80+
<SendFlow onClose={handleClose} />
81+
<MobileCloseButton onClick={handleClose} />
82+
</SendFlowContextProvider>
83+
),
84+
activeClassName: ACTIVE_CLASSNAME,
85+
dataAttribute: 'erc20-bridge-send',
86+
},
87+
];
88+
}, [handleClose]);
89+
90+
const onChangeIndex = useCallback((index: number | null) => {
91+
index !== null ? setIndex(index) : setIndex(0);
92+
}, []);
93+
94+
const dialogSize = useMemo(
95+
() => (isMobile ? DialogSize.md : DialogSize.xl3),
96+
[isMobile],
97+
);
98+
99+
useEffect(() => {
100+
setIndex(0);
101+
}, [account]);
102+
103+
return (
104+
<Dialog
105+
isOpen={isOpen}
106+
width={dialogSize}
107+
className="p-4 flex items-center sm:p-0"
108+
disableFocusTrap
109+
closeOnEscape={false}
110+
>
111+
<QueryClientProvider client={queryClient}>
112+
<Tabs
113+
index={index}
114+
items={items}
115+
onChange={onChangeIndex}
116+
className="w-full md:hidden"
117+
contentClassName="pt-9 px-6 pb-7 h-full"
118+
/>
119+
<VerticalTabs
120+
items={items}
121+
onChange={onChangeIndex}
122+
selectedIndex={index}
123+
tabsClassName="min-h-[42rem] h-auto self-stretch block pt-0 relative flex-1"
124+
headerClassName="pb-0 pt-5"
125+
footerClassName="absolute bottom-5 left-5"
126+
contentClassName="px-9 pb-10 pt-6 flex-1"
127+
className="hidden md:flex"
128+
header={() => (
129+
<>
130+
<div className="rounded bg-gray-60 px-2 py-1 w-fit mb-9">
131+
<AddressBadge address={account} />
132+
</div>
133+
<Heading className="mb-20">{t(translation.title)}</Heading>
134+
</>
135+
)}
136+
footer={() => (
137+
<Button
138+
text={t(translations.common.buttons.close)}
139+
onClick={handleClose}
140+
style={ButtonStyle.ghost}
141+
dataAttribute="erc20-bridge-close"
142+
/>
143+
)}
144+
/>
145+
</QueryClientProvider>
146+
</Dialog>
147+
);
148+
};
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import React from 'react';
2+
3+
import { t } from 'i18next';
4+
import { Trans } from 'react-i18next';
5+
6+
import { Heading, HeadingType, Link } from '@sovryn/ui';
7+
8+
import { HELPDESK_LINK } from '../../../../constants/links';
9+
import { translations } from '../../../../locales/i18n';
10+
11+
type InstructionsProps = {
12+
isReceive?: boolean;
13+
};
14+
15+
export const Instructions: React.FC<InstructionsProps> = () => {
16+
return (
17+
<>
18+
<Heading type={HeadingType.h2} className="font-medium leading-[1.375rem]">
19+
{t(translations.erc20Bridge.instructions.title)}:
20+
</Heading>
21+
22+
<ul className="list-disc list-inside text-xs leading-5 font-medium text-gray-30 mt-6 mb-12">
23+
<li className="mb-2">
24+
{t(translations.erc20Bridge.instructions['1'])}
25+
</li>
26+
<li className="mb-2">
27+
{t(translations.erc20Bridge.instructions['2'])}
28+
</li>
29+
<li className="mb-2">
30+
{t(translations.erc20Bridge.instructions['3'])}
31+
</li>
32+
<li className="mb-2">
33+
{t(translations.erc20Bridge.instructions['4'])}
34+
</li>
35+
<li className="mb-2">
36+
<Trans
37+
i18nKey={t(translations.erc20Bridge.instructions['5'])}
38+
tOptions={{ hours: 1.5 }}
39+
components={[
40+
<Link
41+
text={t(
42+
translations.erc20Bridge.instructions.createSupportTicketCta,
43+
)}
44+
className="md:ml-4"
45+
href={HELPDESK_LINK}
46+
/>,
47+
]}
48+
/>
49+
</li>
50+
</ul>
51+
</>
52+
);
53+
};
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import React, { useCallback, useState } from 'react';
2+
3+
import { t } from 'i18next';
4+
5+
import { ChainId } from '@sovryn/ethers-provider';
6+
import { Accordion, SimpleTable, SimpleTableRow } from '@sovryn/ui';
7+
8+
import { AmountRenderer } from '../../../2_molecules/AmountRenderer/AmountRenderer';
9+
import { getTokenDisplayName } from '../../../../constants/tokens';
10+
import { translations } from '../../../../locales/i18n';
11+
import { useBridgeLimits } from '../hooks/useBridgeLimits';
12+
13+
const translation = translations.erc20Bridge.limits;
14+
15+
type LimitsProps = {
16+
sourceChain?: ChainId;
17+
targetChain?: ChainId;
18+
asset?: string;
19+
className?: string;
20+
};
21+
22+
export const Limits: React.FC<LimitsProps> = ({
23+
sourceChain,
24+
targetChain,
25+
asset,
26+
className,
27+
}) => {
28+
const [open, setOpen] = useState(false);
29+
const { data: limits } = useBridgeLimits(sourceChain, targetChain, asset);
30+
const onClick = useCallback((toOpen: boolean) => setOpen(toOpen), []);
31+
32+
if (!asset) {
33+
return null;
34+
}
35+
36+
return (
37+
<>
38+
<Accordion
39+
label={t(translation.title)}
40+
disabled={!asset || !sourceChain || !targetChain}
41+
children={
42+
limits ? (
43+
<SimpleTable border>
44+
<SimpleTableRow
45+
label={t(translation.minimumAmount)}
46+
value={
47+
<AmountRenderer
48+
value={limits.minPerToken}
49+
decimals={0}
50+
suffix={getTokenDisplayName(asset)}
51+
/>
52+
}
53+
/>
54+
<SimpleTableRow
55+
label={t(translation.maximumAmount)}
56+
value={
57+
<AmountRenderer
58+
value={limits.maxTokensAllowed}
59+
decimals={0}
60+
suffix={getTokenDisplayName(asset)}
61+
/>
62+
}
63+
/>
64+
<SimpleTableRow
65+
label={t(translation.dailyLimit)}
66+
value={
67+
<AmountRenderer
68+
value={limits.dailyLimit}
69+
decimals={0}
70+
suffix={getTokenDisplayName(asset)}
71+
/>
72+
}
73+
/>
74+
<SimpleTableRow
75+
label={t(translation.dailyLimitSpent)}
76+
value={
77+
<AmountRenderer
78+
value={limits.spentToday}
79+
decimals={0}
80+
suffix={getTokenDisplayName(asset)}
81+
/>
82+
}
83+
/>
84+
<SimpleTableRow
85+
label={t(translation.fee)}
86+
value={
87+
<AmountRenderer
88+
value={limits.feePerToken}
89+
decimals={0}
90+
suffix={getTokenDisplayName(asset)}
91+
/>
92+
}
93+
/>
94+
</SimpleTable>
95+
) : (
96+
<span className="h-10 w-full block bg-gray-50 rounded animate-pulse" />
97+
)
98+
}
99+
className={className}
100+
open={open}
101+
onClick={onClick}
102+
labelClassName="font-medium"
103+
/>
104+
</>
105+
);
106+
};

0 commit comments

Comments
 (0)