├── .prettierrc.json ├── public ├── favicon.ico ├── logo192.png ├── robots.txt ├── icons │ └── tokens │ │ └── matic.png ├── fonts │ └── modern-era │ │ ├── ModernEra-Bold.ttf │ │ ├── ModernEra-Bold.woff │ │ ├── ModernEra-Bold.woff2 │ │ ├── ModernEra-Medium.ttf │ │ ├── ModernEra-Medium.woff │ │ ├── ModernEra-Medium.woff2 │ │ ├── ModernEra-Regular.ttf │ │ ├── ModernEra-Regular.woff │ │ ├── ModernEra-ExtraBold.ttf │ │ ├── ModernEra-ExtraBold.woff │ │ ├── ModernEra-Regular.woff2 │ │ └── ModernEra-ExtraBold.woff2 └── manifest.json ├── sonar-project.properties ├── src ├── utils │ ├── feature-toggles.ts │ ├── browser.ts │ ├── tokens.ts │ ├── type-safety.ts │ ├── addresses.ts │ ├── time.ts │ ├── fees.ts │ ├── amounts.ts │ ├── types.ts │ └── labels.ts ├── assets │ └── icons │ │ ├── checkbox-unchecked.svg │ │ ├── caret-down.svg │ │ ├── xmark.svg │ │ ├── caret-right.svg │ │ ├── arrow-down.svg │ │ ├── arrow-left.svg │ │ ├── arrow-right.svg │ │ ├── new-window.svg │ │ ├── currency-conversion.svg │ │ ├── delete.svg │ │ ├── currencies │ │ ├── jpy.svg │ │ ├── cny.svg │ │ └── gbp.svg │ │ ├── checkbox-checked.svg │ │ ├── success.svg │ │ ├── clock.svg │ │ ├── tokens │ │ └── erc20-icon.svg │ │ ├── copy.svg │ │ ├── info.svg │ │ ├── l1-bridge.svg │ │ ├── warning.svg │ │ ├── error.svg │ │ ├── search.svg │ │ ├── magnifying-glass.svg │ │ ├── l2-bridge.svg │ │ ├── spinner.svg │ │ ├── logout.svg │ │ ├── chains │ │ └── polygon-zkevm.svg │ │ ├── walletconnect.svg │ │ ├── polygon-hermez.svg │ │ ├── setting.svg │ │ └── metamask.svg ├── views │ ├── bridge-details │ │ ├── components │ │ │ └── chain │ │ │ │ ├── chain.styles.ts │ │ │ │ └── chain.tsx │ │ └── bridge-details.styles.ts │ ├── shared │ │ ├── portal │ │ │ ├── portal.styles.ts │ │ │ └── portal.view.tsx │ │ ├── page-loader │ │ │ ├── page-loader.styles.ts │ │ │ └── page-loader.view.tsx │ │ ├── card │ │ │ ├── card.styles.ts │ │ │ └── card.view.tsx │ │ ├── token-balance │ │ │ ├── token-balance.styles.ts │ │ │ └── token-balance.view.tsx │ │ ├── icon │ │ │ ├── icon.styles.ts │ │ │ └── icon.view.tsx │ │ ├── external-link │ │ │ ├── external-link.styles.ts │ │ │ └── external-link.view.tsx │ │ ├── error-message │ │ │ ├── error-message.styles.ts │ │ │ └── error-message.view.tsx │ │ ├── info-banner │ │ │ ├── info-banner.styles.ts │ │ │ └── info-banner.view.tsx │ │ ├── typography │ │ │ ├── typography.view.tsx │ │ │ └── typography.styles.ts │ │ ├── private-route │ │ │ └── private-route.view.tsx │ │ ├── spinner │ │ │ ├── spinner.styles.ts │ │ │ └── spinner.view.tsx │ │ ├── network-selector │ │ │ ├── network-selector.styles.ts │ │ │ └── network-selector.view.tsx │ │ ├── button │ │ │ ├── button.styles.ts │ │ │ └── button.view.tsx │ │ ├── network-box │ │ │ └── network-box.styles.ts │ │ ├── confirmation-modal │ │ │ ├── confirmation-modal.styles.ts │ │ │ └── confirmation-modal.view.tsx │ │ ├── header │ │ │ ├── header.view.tsx │ │ │ └── header.styles.ts │ │ ├── chain-list │ │ │ ├── chain-list.view.tsx │ │ │ └── chain-list.styles.ts │ │ └── snackbar │ │ │ ├── snackbar.styles.ts │ │ │ └── snackbar.view.tsx │ ├── bridge-confirmation │ │ ├── components │ │ │ ├── approval-info │ │ │ │ ├── approval-info.styles.ts │ │ │ │ └── approval-info.view.tsx │ │ │ └── bridge-button │ │ │ │ └── bridge-button.view.tsx │ │ └── bridge-confirmation.styles.ts │ ├── activity │ │ ├── components │ │ │ ├── infinite-scroll │ │ │ │ ├── infinite-scroll.styles.ts │ │ │ │ └── infinite-scroll.view.tsx │ │ │ └── bridge-card │ │ │ │ └── bridge-card.styles.ts │ │ └── activity.styles.ts │ ├── core │ │ ├── layout │ │ │ ├── layout.styles.ts │ │ │ └── layout.view.tsx │ │ └── router │ │ │ └── router.view.tsx │ ├── home │ │ ├── components │ │ │ ├── token-selector │ │ │ │ └── token-selector.styles.ts │ │ │ ├── token-info │ │ │ │ ├── token-info.styles.ts │ │ │ │ └── token-info.view.tsx │ │ │ ├── header │ │ │ │ ├── header.styles.ts │ │ │ │ └── header.view.tsx │ │ │ ├── amount-input │ │ │ │ ├── amount-input.styles.ts │ │ │ │ └── amount-input.view.tsx │ │ │ ├── text-match-form │ │ │ │ ├── text-match-form.styles.ts │ │ │ │ └── text-match-form.view.tsx │ │ │ ├── token-adder │ │ │ │ ├── token-adder.styles.ts │ │ │ │ └── token-adder.view.tsx │ │ │ ├── token-selector-header │ │ │ │ ├── token-selector-header.view.tsx │ │ │ │ └── token-selector-header.styles.ts │ │ │ ├── token-info-table │ │ │ │ └── token-info-table.styles.ts │ │ │ ├── deposit-warning-modal │ │ │ │ ├── deposit-warning-modal.styles.ts │ │ │ │ └── deposit-warning-modal.view.tsx │ │ │ ├── bridge-form │ │ │ │ └── bridge-form.styles.ts │ │ │ └── token-list │ │ │ │ └── token-list.styles.ts │ │ ├── home.styles.ts │ │ └── home.view.tsx │ ├── login │ │ ├── components │ │ │ ├── wallet-icon │ │ │ │ ├── wallet-icon.styles.ts │ │ │ │ └── wallet-icon.view.tsx │ │ │ └── wallet-list │ │ │ │ ├── wallet-list.styles.ts │ │ │ │ └── wallet-list.view.tsx │ │ └── login.styles.ts │ ├── network-error │ │ ├── network-error.styles.ts │ │ └── network-error.view.tsx │ ├── app.view.tsx │ ├── settings │ │ ├── settings.styles.ts │ │ └── settings.view.tsx │ └── app.styles.ts ├── adapters │ ├── browser.ts │ ├── tokens.ts │ └── fiat-exchange-rates-api.ts ├── hooks │ ├── use-is-mounted.ts │ ├── use-call-if-mounted.ts │ ├── use-debounce.ts │ ├── use-intersection.ts │ └── use-debounced-block.ts ├── main.tsx ├── styles │ └── theme.ts ├── contexts │ ├── form.context.tsx │ ├── error.context.tsx │ ├── env.context.tsx │ └── ui.context.tsx ├── routes.ts └── vite-env.d.ts ├── .prettierignore ├── tsconfig.node.json ├── .github ├── PULL_REQUEST_TEMPLATE │ ├── main.md │ └── develop.md ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature.md │ └── bug.md ├── PULL_REQUEST_TEMPLATE.md ├── workflows │ ├── sonarqube.yml │ └── push-docker.yml └── CONTIBUTING.md ├── Dockerfile ├── .gitignore ├── .dockerignore ├── deployment └── nginx.conf ├── SECURITY.md ├── tsconfig.json ├── scripts ├── generate-contract-types.sh └── deploy.sh ├── vite.config.ts ├── index.html ├── .env.example ├── package.json └── README.md /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "proseWrap": "always" 4 | } 5 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xPolygon/zkevm-bridge-ui/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xPolygon/zkevm-bridge-ui/HEAD/public/logo192.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: / 4 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | sonar.projectKey=0xPolygonHermez_zkevm-bridge-ui 2 | sonar.organization=0xpolygonhermez 3 | -------------------------------------------------------------------------------- /public/icons/tokens/matic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xPolygon/zkevm-bridge-ui/HEAD/public/icons/tokens/matic.png -------------------------------------------------------------------------------- /public/fonts/modern-era/ModernEra-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xPolygon/zkevm-bridge-ui/HEAD/public/fonts/modern-era/ModernEra-Bold.ttf -------------------------------------------------------------------------------- /public/fonts/modern-era/ModernEra-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xPolygon/zkevm-bridge-ui/HEAD/public/fonts/modern-era/ModernEra-Bold.woff -------------------------------------------------------------------------------- /public/fonts/modern-era/ModernEra-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xPolygon/zkevm-bridge-ui/HEAD/public/fonts/modern-era/ModernEra-Bold.woff2 -------------------------------------------------------------------------------- /public/fonts/modern-era/ModernEra-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xPolygon/zkevm-bridge-ui/HEAD/public/fonts/modern-era/ModernEra-Medium.ttf -------------------------------------------------------------------------------- /public/fonts/modern-era/ModernEra-Medium.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xPolygon/zkevm-bridge-ui/HEAD/public/fonts/modern-era/ModernEra-Medium.woff -------------------------------------------------------------------------------- /public/fonts/modern-era/ModernEra-Medium.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xPolygon/zkevm-bridge-ui/HEAD/public/fonts/modern-era/ModernEra-Medium.woff2 -------------------------------------------------------------------------------- /public/fonts/modern-era/ModernEra-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xPolygon/zkevm-bridge-ui/HEAD/public/fonts/modern-era/ModernEra-Regular.ttf -------------------------------------------------------------------------------- /public/fonts/modern-era/ModernEra-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xPolygon/zkevm-bridge-ui/HEAD/public/fonts/modern-era/ModernEra-Regular.woff -------------------------------------------------------------------------------- /public/fonts/modern-era/ModernEra-ExtraBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xPolygon/zkevm-bridge-ui/HEAD/public/fonts/modern-era/ModernEra-ExtraBold.ttf -------------------------------------------------------------------------------- /public/fonts/modern-era/ModernEra-ExtraBold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xPolygon/zkevm-bridge-ui/HEAD/public/fonts/modern-era/ModernEra-ExtraBold.woff -------------------------------------------------------------------------------- /public/fonts/modern-era/ModernEra-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xPolygon/zkevm-bridge-ui/HEAD/public/fonts/modern-era/ModernEra-Regular.woff2 -------------------------------------------------------------------------------- /public/fonts/modern-era/ModernEra-ExtraBold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xPolygon/zkevm-bridge-ui/HEAD/public/fonts/modern-era/ModernEra-ExtraBold.woff2 -------------------------------------------------------------------------------- /src/utils/feature-toggles.ts: -------------------------------------------------------------------------------- 1 | import { Env } from "src/domain"; 2 | 3 | export const areSettingsVisible = (env: Env): boolean => { 4 | return env.fiatExchangeRates.areEnabled; 5 | }; 6 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore folders 2 | node-modules 3 | dist 4 | .husky 5 | .github 6 | deployment 7 | public 8 | scripts 9 | src/types 10 | 11 | # Ignore files 12 | package-lock.json -------------------------------------------------------------------------------- /src/assets/icons/checkbox-unchecked.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/views/bridge-details/components/chain/chain.styles.ts: -------------------------------------------------------------------------------- 1 | import { createUseStyles } from "react-jss"; 2 | 3 | export const useChainStyles = createUseStyles({ 4 | polygonZkEvmChain: { 5 | height: 20, 6 | width: 20, 7 | }, 8 | }); 9 | -------------------------------------------------------------------------------- /src/views/shared/portal/portal.styles.ts: -------------------------------------------------------------------------------- 1 | import { createUseStyles } from "react-jss"; 2 | 3 | export const usePortalStyles = createUseStyles({ 4 | fullScreenModal: { 5 | bottom: 0, 6 | left: 0, 7 | position: "fixed", 8 | right: 0, 9 | top: 0, 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /src/adapters/browser.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | import { RouterState } from "src/domain"; 4 | import { StrictSchema } from "src/utils/type-safety"; 5 | 6 | const routerStateParser = StrictSchema()(z.object({ redirectUrl: z.string() })); 7 | 8 | export { routerStateParser }; 9 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "es6", 5 | "moduleResolution": "node", 6 | "allowSyntheticDefaultImports": true, 7 | "resolveJsonModule": true 8 | }, 9 | "include": ["vite.config.ts", ".eslintrc.json"] 10 | } 11 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/main.md: -------------------------------------------------------------------------------- 1 | ### List of PRs 2 | 3 | 4 | 5 | ## Checklist 6 | 7 | These are the criteria that every PR should meet, please check them off as you 8 | review them: 9 | 10 | - [ ] Respect code style and lint 11 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:alpine 2 | 3 | RUN apk add --update nodejs npm 4 | 5 | WORKDIR /app 6 | 7 | COPY package.json package-lock.json ./ 8 | COPY scripts ./scripts 9 | COPY abis ./abis 10 | 11 | RUN npm install 12 | 13 | COPY . . 14 | 15 | WORKDIR / 16 | 17 | ENTRYPOINT ["/bin/sh", "/app/scripts/deploy.sh"] 18 | -------------------------------------------------------------------------------- /src/views/shared/page-loader/page-loader.styles.ts: -------------------------------------------------------------------------------- 1 | import { createUseStyles } from "react-jss"; 2 | 3 | export const usePageLoaderStyles = createUseStyles({ 4 | root: { 5 | alignItems: "center", 6 | display: "flex", 7 | flex: 1, 8 | justifyContent: "center", 9 | width: "100%", 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | npm-debug.log* 3 | yarn-debug.log* 4 | yarn-error.log* 5 | vite.config.ts.timestamp* 6 | 7 | # Editors 8 | .vscode/* 9 | 10 | # Dependencies 11 | node_modules 12 | 13 | # Environment 14 | .env 15 | 16 | # Artifacts 17 | dist 18 | 19 | # System files 20 | **/.DS_Store 21 | 22 | # Generated types 23 | src/types 24 | -------------------------------------------------------------------------------- /src/views/shared/card/card.styles.ts: -------------------------------------------------------------------------------- 1 | import { createUseStyles } from "react-jss"; 2 | 3 | import { Theme } from "src/styles/theme"; 4 | 5 | export const useCardStyles = createUseStyles((theme: Theme) => ({ 6 | card: { 7 | background: theme.palette.white, 8 | borderRadius: 16, 9 | overflow: "hidden", 10 | }, 11 | })); 12 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | npm-debug.log* 3 | yarn-debug.log* 4 | yarn-error.log* 5 | vite.config.ts.timestamp* 6 | 7 | # Editors 8 | .vscode/* 9 | 10 | # Dependencies 11 | node_modules 12 | 13 | # Environment 14 | .env 15 | 16 | # Artifacts 17 | dist 18 | 19 | # System files 20 | **/.DS_Store 21 | 22 | # Generated types 23 | src/types 24 | -------------------------------------------------------------------------------- /src/assets/icons/caret-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/views/shared/token-balance/token-balance.styles.ts: -------------------------------------------------------------------------------- 1 | import { createUseStyles } from "react-jss"; 2 | 3 | import { Theme } from "src/styles/theme"; 4 | 5 | export const useTokenBalanceStyles = createUseStyles((theme: Theme) => ({ 6 | loader: { 7 | alignItems: "center", 8 | display: "flex", 9 | gap: theme.spacing(0.25), 10 | }, 11 | })); 12 | -------------------------------------------------------------------------------- /src/assets/icons/xmark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/icons/caret-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/views/bridge-confirmation/components/approval-info/approval-info.styles.ts: -------------------------------------------------------------------------------- 1 | import { createUseStyles } from "react-jss"; 2 | 3 | import { Theme } from "src/styles/theme"; 4 | 5 | export const useApprovalInfoStyles = createUseStyles((theme: Theme) => ({ 6 | approvalInfo: { 7 | alignItems: "center", 8 | display: "flex", 9 | gap: theme.spacing(1), 10 | }, 11 | })); 12 | -------------------------------------------------------------------------------- /src/hooks/use-is-mounted.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useRef } from "react"; 2 | 3 | export const useIsMounted = () => { 4 | const isMounted = useRef(false); 5 | 6 | useEffect(() => { 7 | isMounted.current = true; 8 | 9 | return () => { 10 | isMounted.current = false; 11 | }; 12 | }, []); 13 | 14 | return useCallback(() => isMounted.current, []); 15 | }; 16 | -------------------------------------------------------------------------------- /src/views/shared/icon/icon.styles.ts: -------------------------------------------------------------------------------- 1 | import { createUseStyles } from "react-jss"; 2 | 3 | export const useIconStyles = createUseStyles({ 4 | icon: (size: number) => ({ 5 | height: size, 6 | width: size, 7 | }), 8 | roundedWrapper: { 9 | alignItems: "center", 10 | borderRadius: "50%", 11 | display: "flex", 12 | justifyContent: "center", 13 | overflow: "hidden", 14 | }, 15 | }); 16 | -------------------------------------------------------------------------------- /src/hooks/use-call-if-mounted.ts: -------------------------------------------------------------------------------- 1 | import { useCallback } from "react"; 2 | 3 | import { useIsMounted } from "src/hooks/use-is-mounted"; 4 | 5 | export function useCallIfMounted(): (callback: () => void) => void { 6 | const isMounted = useIsMounted(); 7 | return useCallback( 8 | (callback: () => void) => { 9 | if (isMounted()) { 10 | callback(); 11 | } 12 | }, 13 | [isMounted] 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/views/shared/external-link/external-link.styles.ts: -------------------------------------------------------------------------------- 1 | import { createUseStyles } from "react-jss"; 2 | 3 | import { Theme } from "src/styles/theme"; 4 | 5 | export const useExternalLinkStyles = createUseStyles((theme: Theme) => ({ 6 | link: { 7 | "&:hover": { 8 | color: theme.palette.primary.dark, 9 | }, 10 | color: theme.palette.primary.main, 11 | transition: theme.hoverTransition, 12 | }, 13 | })); 14 | -------------------------------------------------------------------------------- /deployment/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80 default; 3 | server_name localhost _; 4 | 5 | location / { 6 | root /usr/share/nginx/html; 7 | index index.html; 8 | # Redirect all requests to index.html 9 | try_files $uri /index.html =404; 10 | } 11 | 12 | error_page 500 502 503 504 /50x.html; 13 | location = /50x.html { 14 | root /usr/share/nginx/html; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/views/shared/page-loader/page-loader.view.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import { usePageLoaderStyles } from "src/views/shared/page-loader/page-loader.styles"; 3 | import { Spinner } from "src/views/shared/spinner/spinner.view"; 4 | 5 | export const PageLoader: FC = () => { 6 | const classes = usePageLoaderStyles(); 7 | 8 | return ( 9 |
10 | 11 |
12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Polygon zkEVM Docs 4 | url: https://docs.hermez.io 5 | about: Official Polygon zkEVM Documentation 6 | - name: Telegram Group 7 | url: https://t.me/PolygonHermezChat 8 | about: Official Polygon zkEVM Telegram Group 9 | - name: Discord Community 10 | url: https://discord.gg/0xPolygon 11 | about: Official Polygon zkEVM Discord Community 12 | -------------------------------------------------------------------------------- /src/utils/browser.ts: -------------------------------------------------------------------------------- 1 | function copyToClipboard(text: string): void { 2 | const textArea = document.createElement("textarea"); 3 | 4 | textArea.value = text; 5 | textArea.style.position = "fixed"; 6 | textArea.style.opacity = "0"; 7 | document.body.appendChild(textArea); 8 | textArea.focus(); 9 | textArea.select(); 10 | document.execCommand("copy"); 11 | document.body.removeChild(textArea); 12 | } 13 | 14 | export { copyToClipboard }; 15 | -------------------------------------------------------------------------------- /src/views/shared/error-message/error-message.styles.ts: -------------------------------------------------------------------------------- 1 | import { createUseStyles } from "react-jss"; 2 | 3 | import { Theme } from "src/styles/theme"; 4 | 5 | export const useErrorMessageStyles = createUseStyles((theme: Theme) => ({ 6 | error: { 7 | color: theme.palette.error.main, 8 | lineHeight: "26px", 9 | textAlign: "center", 10 | whiteSpace: "break-spaces", 11 | }, 12 | errorWrapper: { 13 | textAlign: "center", 14 | }, 15 | })); 16 | -------------------------------------------------------------------------------- /src/assets/icons/arrow-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/hooks/use-debounce.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | export function useDebounce(value: T, delay: number): T { 4 | const [debouncedValue, setDebouncedValue] = useState(value); 5 | 6 | useEffect(() => { 7 | const timeoutID = setTimeout(() => { 8 | setDebouncedValue(value); 9 | }, delay); 10 | 11 | return () => { 12 | clearTimeout(timeoutID); 13 | }; 14 | }, [value, delay]); 15 | 16 | return debouncedValue; 17 | } 18 | -------------------------------------------------------------------------------- /src/assets/icons/arrow-left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/views/activity/components/infinite-scroll/infinite-scroll.styles.ts: -------------------------------------------------------------------------------- 1 | import { createUseStyles } from "react-jss"; 2 | 3 | import { Theme } from "src/styles/theme"; 4 | 5 | export const useInfiniteScrollStyles = createUseStyles((theme: Theme) => ({ 6 | root: { 7 | width: "100%", 8 | }, 9 | spinnerWrapper: { 10 | alignItems: "center", 11 | display: "flex", 12 | justifyContent: "center", 13 | paddingTop: theme.spacing(3), 14 | width: "100%", 15 | }, 16 | })); 17 | -------------------------------------------------------------------------------- /src/assets/icons/arrow-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | 3 | "short_name": "Polygon zkEVM Bridge", 4 | "name": "Polygon zkEVM Bridge", 5 | "start_url": ".", 6 | "display": "standalone", 7 | "theme_color": "#000000", 8 | "background_color": "#ffffff", 9 | "icons": [ 10 | { 11 | "src": "favicon.ico", 12 | "sizes": "64x64 32x32 24x24 16x16", 13 | "type": "image/x-icon" 14 | }, 15 | { 16 | "src": "logo192.png", 17 | "type": "image/png", 18 | "sizes": "192x192" 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /src/assets/icons/new-window.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/icons/currency-conversion.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/utils/tokens.ts: -------------------------------------------------------------------------------- 1 | import { constants as ethersConstants } from "ethers"; 2 | 3 | import { Chain, Token } from "src/domain"; 4 | 5 | const selectTokenAddress = (token: Token, chain: Chain): string => { 6 | return token.wrappedToken && chain.chainId === token.wrappedToken.chainId 7 | ? token.wrappedToken.address 8 | : token.address; 9 | }; 10 | 11 | const isTokenEther = (token: Token): boolean => { 12 | return token.address === ethersConstants.AddressZero; 13 | }; 14 | 15 | export { isTokenEther, selectTokenAddress }; 16 | -------------------------------------------------------------------------------- /src/views/shared/card/card.view.tsx: -------------------------------------------------------------------------------- 1 | import { FC, PropsWithChildren } from "react"; 2 | 3 | import { useCardStyles } from "src/views/shared/card/card.styles"; 4 | 5 | type CardProps = PropsWithChildren<{ 6 | className?: string; 7 | onClick?: () => void; 8 | }>; 9 | 10 | export const Card: FC = ({ children, className, onClick }) => { 11 | const classes = useCardStyles(); 12 | 13 | return ( 14 |
15 | {children} 16 |
17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /src/views/shared/info-banner/info-banner.styles.ts: -------------------------------------------------------------------------------- 1 | import { createUseStyles } from "react-jss"; 2 | 3 | import { Theme } from "src/styles/theme"; 4 | 5 | export const useInfoBannerStyles = createUseStyles((theme: Theme) => ({ 6 | infoBanner: { 7 | background: theme.palette.grey.main, 8 | borderRadius: "8px", 9 | display: "flex", 10 | gap: theme.spacing(1), 11 | maxWidth: theme.maxWidth, 12 | padding: theme.spacing(2), 13 | width: "100%", 14 | }, 15 | message: { 16 | color: theme.palette.black, 17 | }, 18 | })); 19 | -------------------------------------------------------------------------------- /src/views/shared/external-link/external-link.view.tsx: -------------------------------------------------------------------------------- 1 | import { FC, PropsWithChildren } from "react"; 2 | 3 | import { useExternalLinkStyles } from "src/views/shared/external-link/external-link.styles"; 4 | 5 | interface ExternalLinkProps { 6 | href: string; 7 | } 8 | 9 | export const ExternalLink: FC> = ({ children, href }) => { 10 | const classes = useExternalLinkStyles(); 11 | 12 | return ( 13 | 14 | {children} 15 | 16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Request a feature 3 | about: Report a missing feature - e.g. as a step before submitting a PR 4 | title: '' 5 | labels: 'type: enhancement' 6 | assignees: '' 7 | --- 8 | 9 | ## Rationale 10 | 11 | 13 | 14 | ## Implementation 15 | 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Report a bug 3 | about: Something with zkevm-bridge-ui is not working as expected 4 | title: '' 5 | labels: 'type: bug' 6 | assignees: '' 7 | --- 8 | 9 | ## Summary of Bug 10 | 11 | 12 | 13 | ### Steps to Reproduce 14 | 15 | 16 | 17 | ### Browser information 18 | 19 | - Browser: `Chrome` 20 | - Device/OS: `Desktop/Windows 10` 21 | 22 | ### Additional Information & Screenshots: 23 | 24 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Polygon Technology Security Information 2 | 3 | ## Link to vulnerability disclosure details (Bug Bounty). 4 | - Websites and Applications: https://hackerone.com/polygon-technology 5 | - Smart Contracts: https://immunefi.com/bounty/polygon 6 | 7 | ## Languages that our team speaks and understands. 8 | Preferred-Languages: en 9 | 10 | ## Security-related job openings at Polygon. 11 | https://polygon.technology/careers 12 | 13 | ## Polygon security contact details. 14 | security@polygon.technology 15 | 16 | ## The URL for accessing the security.txt file. 17 | Canonical: https://polygon.technology/security.txt 18 | -------------------------------------------------------------------------------- /src/assets/icons/delete.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/currencies/jpy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/views/shared/typography/typography.view.tsx: -------------------------------------------------------------------------------- 1 | import { FC, PropsWithChildren } from "react"; 2 | 3 | import { useTypographyStyles } from "src/views/shared/typography/typography.styles"; 4 | 5 | export type TypographyProps = PropsWithChildren<{ 6 | className?: string; 7 | type: "h1" | "h2" | "body1" | "body2"; 8 | }>; 9 | 10 | export const Typography: FC = ({ children, className, type }) => { 11 | const classes = useTypographyStyles(); 12 | const Component = type === "body1" || type === "body2" ? "p" : type; 13 | 14 | return {children}; 15 | }; 16 | -------------------------------------------------------------------------------- /src/assets/icons/checkbox-checked.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | Closes #. _in case of a bug fix, this should point to a bug or any other related issue(s)_ 4 | 5 | ### What does this PR does? 6 | 7 | 9 | 10 | ### How to test? 11 | 12 | 13 | 14 | ## Checklist 15 | 16 | These are the criteria that every PR should meet, please check them off as you 17 | review them: 18 | 19 | - [ ] Respect code style and lint 20 | - [ ] Update documentation (if needed) 21 | -------------------------------------------------------------------------------- /.github/workflows/sonarqube.yml: -------------------------------------------------------------------------------- 1 | name: Security Build 2 | on: 3 | push: 4 | branches: 5 | - main 6 | - develop 7 | - staging 8 | workflow_dispatch: {} 9 | pull_request: 10 | types: [opened, synchronize, reopened] 11 | 12 | jobs: 13 | sonarcloud: 14 | name: SonarCloud 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v3 18 | with: 19 | fetch-depth: 0 20 | - name: SonarCloud Scan 21 | uses: SonarSource/sonarcloud-github-action@master 22 | env: 23 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 24 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 25 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/develop.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | Closes #. _in case of a bug fix, this should point to a bug or any other related issue(s)_ 4 | 5 | ### What does this PR does? 6 | 7 | 9 | 10 | ### How to test? 11 | 12 | 13 | 14 | ## Checklist 15 | 16 | These are the criteria that every PR should meet, please check them off as you 17 | review them: 18 | 19 | - [ ] Respect code style and lint 20 | - [ ] Update documentation (if needed) 21 | -------------------------------------------------------------------------------- /src/hooks/use-intersection.ts: -------------------------------------------------------------------------------- 1 | export const useIntersection = ({ 2 | className, 3 | observed, 4 | target, 5 | }: { 6 | className: string; 7 | observed: React.RefObject; 8 | target: React.RefObject; 9 | }) => { 10 | const observer = new IntersectionObserver((entries) => { 11 | const entry = entries[0]; 12 | if (entry && entry.boundingClientRect.y < 0) { 13 | target.current && target.current.classList.add(className); 14 | } else { 15 | target.current && target.current.classList.remove(className); 16 | } 17 | }); 18 | if (observed.current) { 19 | observer.observe(observed.current); 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /src/views/core/layout/layout.styles.ts: -------------------------------------------------------------------------------- 1 | import { createUseStyles } from "react-jss"; 2 | 3 | import { Theme } from "src/styles/theme"; 4 | 5 | export const useLayoutStyles = createUseStyles((theme: Theme) => ({ 6 | container: { 7 | display: "flex", 8 | flex: 1, 9 | flexDirection: "column", 10 | margin: [0, "auto"], 11 | paddingBottom: theme.spacing(2), 12 | width: "100%", 13 | }, 14 | layout: { 15 | background: theme.palette.grey.light, 16 | display: "flex", 17 | flexDirection: "column", 18 | minHeight: "100vh", 19 | width: "100%", 20 | }, 21 | linkContainer: { 22 | marginTop: theme.spacing(2), 23 | }, 24 | })); 25 | -------------------------------------------------------------------------------- /src/utils/type-safety.ts: -------------------------------------------------------------------------------- 1 | import { ZodSchema, ZodTypeDef } from "zod"; 2 | 3 | import { Exact } from "src/domain"; 4 | 5 | export const StrictSchema: () => ( 6 | u: Exact, ZodSchema> extends true 7 | ? Exact, Required> extends true 8 | ? Exact, Required> extends true 9 | ? ZodSchema 10 | : never 11 | : never 12 | : never 13 | ) => ZodSchema = 14 | () => 15 | (u: unknown) => 16 | // eslint-disable-next-line no-type-assertion/no-type-assertion 17 | u as ZodSchema; 18 | -------------------------------------------------------------------------------- /src/views/bridge-confirmation/components/approval-info/approval-info.view.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | 3 | import { ReactComponent as InfoIcon } from "src/assets/icons/info.svg"; 4 | import { useApprovalInfoStyles } from "src/views/bridge-confirmation/components/approval-info/approval-info.styles"; 5 | import { Typography } from "src/views/shared/typography/typography.view"; 6 | 7 | export const ApprovalInfo: FC = () => { 8 | const classes = useApprovalInfoStyles(); 9 | 10 | return ( 11 |
12 | 13 | Approval is needed once per token 14 |
15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /src/views/home/components/token-selector/token-selector.styles.ts: -------------------------------------------------------------------------------- 1 | import { createUseStyles } from "react-jss"; 2 | 3 | import { Theme } from "src/styles/theme"; 4 | 5 | export const useTokenSelectorStyles = createUseStyles((theme: Theme) => ({ 6 | background: { 7 | alignItems: "center", 8 | background: theme.palette.transparency, 9 | display: "flex", 10 | height: "100vh", 11 | justifyContent: "center", 12 | padding: [0, theme.spacing(1)], 13 | width: "100%", 14 | }, 15 | card: { 16 | display: "flex", 17 | flexDirection: "column", 18 | height: 515, 19 | maxWidth: 500, 20 | padding: theme.spacing(2), 21 | width: "100%", 22 | }, 23 | })); 24 | -------------------------------------------------------------------------------- /src/views/shared/icon/icon.view.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | 3 | import { useIconStyles } from "src/views/shared/icon/icon.styles"; 4 | 5 | interface IconProps { 6 | className?: string; 7 | isRounded?: boolean; 8 | size?: number; 9 | url: string; 10 | } 11 | 12 | export const Icon: FC = ({ className, isRounded, size, url }) => { 13 | const classes = useIconStyles(size || 16); 14 | 15 | return isRounded ? ( 16 |
17 | 18 |
19 | ) : ( 20 | 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from "react"; 2 | import { createRoot } from "react-dom/client"; 3 | import { ThemeProvider } from "react-jss"; 4 | import { BrowserRouter } from "react-router-dom"; 5 | import "normalize.css/normalize.css"; 6 | 7 | import { theme } from "src/styles/theme"; 8 | import { App } from "src/views/app.view"; 9 | 10 | const container = document.getElementById("root"); 11 | 12 | if (container === null) { 13 | throw new Error("Root container doesn't exist"); 14 | } 15 | 16 | createRoot(container).render( 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | ); 25 | -------------------------------------------------------------------------------- /src/assets/icons/success.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/views/shared/error-message/error-message.view.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | 3 | import { useErrorMessageStyles } from "src/views/shared/error-message/error-message.styles"; 4 | import { Typography } from "src/views/shared/typography/typography.view"; 5 | 6 | interface ErrorMessageProps { 7 | className?: string; 8 | error: string; 9 | type?: "body1" | "body2"; 10 | } 11 | 12 | export const ErrorMessage: FC = ({ className, error, type = "body1" }) => { 13 | const classes = useErrorMessageStyles(); 14 | 15 | return ( 16 | 17 | {error} 18 | 19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "useDefineForClassFields": true, 5 | "lib": ["dom", "dom.iterable", "esnext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": true, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "module": "esnext", 14 | "moduleResolution": "node", 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "noEmit": true, 18 | "jsx": "react-jsx", 19 | "baseUrl": "." 20 | }, 21 | "include": ["src", "vite.config.ts"], 22 | "paths": { 23 | "src/*": ["src/*"] 24 | }, 25 | "references": [{ "path": "./tsconfig.node.json" }] 26 | } 27 | -------------------------------------------------------------------------------- /src/views/shared/info-banner/info-banner.view.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | 3 | import { ReactComponent as InfoIcon } from "src/assets/icons/info.svg"; 4 | import { useInfoBannerStyles } from "src/views/shared/info-banner/info-banner.styles"; 5 | import { Typography } from "src/views/shared/typography/typography.view"; 6 | 7 | interface InfoBannerProps { 8 | className?: string; 9 | message: string; 10 | } 11 | 12 | export const InfoBanner: FC = ({ className, message }) => { 13 | const classes = useInfoBannerStyles(); 14 | 15 | return ( 16 |
17 | 18 | 19 | {message} 20 | 21 |
22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /src/assets/icons/clock.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/icons/currencies/cny.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/views/login/components/wallet-icon/wallet-icon.styles.ts: -------------------------------------------------------------------------------- 1 | import { createUseStyles } from "react-jss"; 2 | 3 | import { Theme } from "src/styles/theme"; 4 | 5 | interface WalletIconStylesProps { 6 | size: "sm" | "lg"; 7 | } 8 | 9 | export const useWalletIconStyles = createUseStyles((theme: Theme) => ({ 10 | metaMaskIcon: { 11 | background: "#fbe6df", 12 | }, 13 | walletConnectIcon: { 14 | background: "#e2f0ff", 15 | }, 16 | walletIcon: ({ size }: WalletIconStylesProps) => ({ 17 | alignItems: "center", 18 | borderRadius: "50%", 19 | display: "flex", 20 | height: size === "sm" ? theme.spacing(6) : theme.spacing(7.5), 21 | justifyContent: "center", 22 | padding: theme.spacing(1), 23 | width: size === "sm" ? theme.spacing(6) : theme.spacing(7.5), 24 | }), 25 | })); 26 | -------------------------------------------------------------------------------- /src/assets/icons/tokens/erc20-icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/views/shared/portal/portal.view.tsx: -------------------------------------------------------------------------------- 1 | import { FC, PropsWithChildren, useLayoutEffect } from "react"; 2 | import { createPortal } from "react-dom"; 3 | 4 | import { usePortalStyles } from "src/views/shared/portal/portal.styles"; 5 | 6 | export const Portal: FC = ({ children }) => { 7 | const classes = usePortalStyles(); 8 | const portalRoot = document.querySelector("#fullscreen-modal"); 9 | const divElement = document.createElement("div"); 10 | 11 | divElement.classList.add(classes.fullScreenModal); 12 | 13 | useLayoutEffect(() => { 14 | if (portalRoot) { 15 | portalRoot.appendChild(divElement); 16 | 17 | return () => { 18 | portalRoot.removeChild(divElement); 19 | }; 20 | } 21 | }, [portalRoot, divElement]); 22 | 23 | return createPortal(children, divElement); 24 | }; 25 | -------------------------------------------------------------------------------- /src/assets/icons/currencies/gbp.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /scripts/generate-contract-types.sh: -------------------------------------------------------------------------------- 1 | #/bin/bash 2 | 3 | BASE_PATH="src/types/contracts" 4 | 5 | # Clean up 6 | rm -rf $BASE_PATH 7 | 8 | # Generate Price Oracle contract types 9 | npx typechain --target ethers-v5 --out-dir $BASE_PATH/uniswap-v2-router-02 "abis/uniswap-v2-router-02.json" 10 | npx typechain --target ethers-v5 --out-dir $BASE_PATH/uniswap-v2-pair "abis/uniswap-v2-pair.json" 11 | 12 | # Generate zkEVM contract types 13 | npx typechain --target ethers-v5 --out-dir $BASE_PATH/bridge "abis/bridge.json" 14 | npx typechain --target ethers-v5 --out-dir $BASE_PATH/proof-of-efficiency "abis/proof-of-efficiency.json" 15 | npx typechain --target ethers-v5 --out-dir $BASE_PATH/rollup-manager "abis/rollup-manager.json" 16 | 17 | # Generate ERC-20 contract types 18 | npx typechain --target ethers-v5 --out-dir $BASE_PATH/erc-20 "abis/erc-20.json" 19 | -------------------------------------------------------------------------------- /src/views/login/components/wallet-list/wallet-list.styles.ts: -------------------------------------------------------------------------------- 1 | import { createUseStyles } from "react-jss"; 2 | 3 | import { Theme } from "src/styles/theme"; 4 | 5 | export const useWalletListStyles = createUseStyles((theme: Theme) => ({ 6 | wallet: { 7 | "&:hover": { 8 | background: "#e2e5ee", 9 | }, 10 | alignItems: "center", 11 | cursor: "pointer", 12 | display: "flex", 13 | justifyContent: "space-between", 14 | padding: [theme.spacing(3), theme.spacing(4)], 15 | transition: theme.hoverTransition, 16 | }, 17 | walletIcon: { 18 | marginRight: theme.spacing(2), 19 | }, 20 | walletInfo: { 21 | flex: 1, 22 | }, 23 | walletList: { 24 | listStyle: "none", 25 | margin: 0, 26 | paddingLeft: 0, 27 | }, 28 | walletName: { 29 | marginBottom: theme.spacing(1), 30 | }, 31 | })); 32 | -------------------------------------------------------------------------------- /src/assets/icons/copy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/info.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import react from "@vitejs/plugin-react"; 3 | import { defineConfig } from "vite"; 4 | import checker from "vite-plugin-checker"; 5 | import svgr from "vite-plugin-svgr"; 6 | 7 | // eslint-disable-next-line import/no-default-export 8 | export default defineConfig({ 9 | build: { 10 | sourcemap: true, 11 | }, 12 | define: { 13 | bridgeVersion: JSON.stringify(process.env.npm_package_version), 14 | }, 15 | plugins: [ 16 | react({ 17 | fastRefresh: false, 18 | }), 19 | svgr(), 20 | checker({ 21 | eslint: { lintCommand: 'eslint "./src/**/*.{ts,tsx}"' }, 22 | overlay: false, 23 | typescript: true, 24 | }), 25 | ], 26 | resolve: { 27 | alias: [{ find: "src", replacement: path.resolve(__dirname, "src") }], 28 | }, 29 | server: { 30 | open: true, 31 | }, 32 | }); 33 | -------------------------------------------------------------------------------- /src/views/shared/private-route/private-route.view.tsx: -------------------------------------------------------------------------------- 1 | import { FC, PropsWithChildren } from "react"; 2 | import { Navigate, useLocation } from "react-router-dom"; 3 | 4 | import { useProvidersContext } from "src/contexts/providers.context"; 5 | import { routes } from "src/routes"; 6 | 7 | export const PrivateRoute: FC = ({ children }) => { 8 | const { connectedProvider } = useProvidersContext(); 9 | const { pathname, search } = useLocation(); 10 | 11 | switch (connectedProvider.status) { 12 | case "pending": 13 | case "loading": { 14 | return null; 15 | } 16 | case "failed": { 17 | return ( 18 | 19 | ); 20 | } 21 | case "reloading": 22 | case "successful": { 23 | return <>{children}; 24 | } 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /src/assets/icons/l1-bridge.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/assets/icons/warning.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/utils/addresses.ts: -------------------------------------------------------------------------------- 1 | import { utils as ethersUtils } from "ethers"; 2 | 3 | const getPartiallyHiddenEthereumAddress = (ethereumAddress: string): string => { 4 | const firstAddressSlice = ethereumAddress.slice(0, 6); 5 | const secondAddressSlice = ethereumAddress.slice(ethereumAddress.length - 4); 6 | 7 | return `${firstAddressSlice} ・・・ ${secondAddressSlice}`; 8 | }; 9 | 10 | const getShortenedEthereumAddress = (ethereumAddress: string): string => { 11 | const firstAddressSlice = ethereumAddress.slice(0, 7); 12 | const secondAddressSlice = ethereumAddress.slice(ethereumAddress.length - 5); 13 | 14 | return `${firstAddressSlice}...${secondAddressSlice}`; 15 | }; 16 | 17 | const getChecksumAddress = (ethereumAddress: string): string => { 18 | return ethersUtils.getAddress(ethereumAddress); 19 | }; 20 | 21 | export { getPartiallyHiddenEthereumAddress, getShortenedEthereumAddress, getChecksumAddress }; 22 | -------------------------------------------------------------------------------- /src/styles/theme.ts: -------------------------------------------------------------------------------- 1 | export const theme = { 2 | breakpoints: { 3 | upSm: "@media (min-width: 480px)", 4 | }, 5 | hoverTransition: "all 150ms", 6 | maxWidth: 644, 7 | palette: { 8 | black: "#0a0b0d", 9 | error: { 10 | light: "rgba(232,67,12,0.1)", 11 | main: "#e8430d", 12 | }, 13 | grey: { 14 | dark: "#78798d", 15 | light: "#f0f1f6", 16 | main: "#e2e5ee", 17 | veryDark: "#363740", 18 | }, 19 | primary: { 20 | dark: "#5a1cc3", 21 | main: "#7b3fe4", 22 | }, 23 | success: { 24 | light: "rgba(0,255,0,0.1)", 25 | main: "#1ccc8d", 26 | }, 27 | transparency: "rgba(8,17,50,0.5)", 28 | warning: { 29 | light: "rgba(225,126,38,0.1)", 30 | main: "#e17e26", 31 | }, 32 | white: "#ffffff", 33 | }, 34 | spacing: (value: number): number => value * 8, 35 | }; 36 | 37 | export type Theme = typeof theme; 38 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 12 | 13 | Polygon zkEVM Bridge 14 | 15 | 16 | 17 |
18 |
19 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/views/home/components/token-info/token-info.styles.ts: -------------------------------------------------------------------------------- 1 | import { createUseStyles } from "react-jss"; 2 | 3 | import { Theme } from "src/styles/theme"; 4 | 5 | export const useTokenInfoStyles = createUseStyles((theme: Theme) => ({ 6 | removeTokenButton: { 7 | "&:hover": { 8 | backgroundColor: theme.palette.grey.main, 9 | }, 10 | backgroundColor: theme.palette.grey.light, 11 | border: "none", 12 | borderRadius: 9, 13 | color: theme.palette.black, 14 | cursor: "pointer", 15 | display: "flex", 16 | fontSize: "20px", 17 | gap: theme.spacing(2), 18 | justifyContent: "center", 19 | lineHeight: "24px", 20 | padding: theme.spacing(1.5), 21 | transition: theme.hoverTransition, 22 | }, 23 | tokenInfo: { 24 | display: "flex", 25 | flex: 1, 26 | flexDirection: "column", 27 | }, 28 | tokenInfoTable: { 29 | flex: 1, 30 | marginTop: theme.spacing(2), 31 | }, 32 | })); 33 | -------------------------------------------------------------------------------- /src/views/home/home.styles.ts: -------------------------------------------------------------------------------- 1 | import { createUseStyles } from "react-jss"; 2 | 3 | import { Theme } from "src/styles/theme"; 4 | 5 | export const useHomeStyles = createUseStyles((theme: Theme) => ({ 6 | contentWrapper: { 7 | display: "flex", 8 | flexDirection: "column", 9 | padding: [0, theme.spacing(2)], 10 | }, 11 | ethereumAddress: { 12 | alignItems: "center", 13 | backgroundColor: theme.palette.grey.main, 14 | borderRadius: 56, 15 | display: "flex", 16 | margin: [theme.spacing(3), "auto", theme.spacing(3)], 17 | padding: [theme.spacing(1.25), theme.spacing(3)], 18 | [theme.breakpoints.upSm]: { 19 | margin: [theme.spacing(3), "auto", theme.spacing(5)], 20 | }, 21 | }, 22 | metaMaskIcon: { 23 | marginRight: theme.spacing(1), 24 | width: 20, 25 | }, 26 | networkBoxWrapper: { 27 | margin: [0, "auto", theme.spacing(3)], 28 | maxWidth: theme.maxWidth, 29 | width: "100%", 30 | }, 31 | })); 32 | -------------------------------------------------------------------------------- /.github/CONTIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thank you for considering to help out with the source code! We welcome 4 | contributions from anyone on the internet, and are grateful for even the 5 | smallest of fixes! 6 | 7 | If you'd like to contribute to zkevm-bridge-ui, please fork, fix, commit and send a 8 | pull request for the maintainers to review and merge into the main code base. If 9 | you wish to submit more complex changes though, please check up with the core 10 | devs first on [issues](https://github.com/0xPolygonHermez/zkevm-bridge-ui/issues) to 11 | ensure those changes are in line with the general philosophy of the project 12 | and/or get some early feedback which can make both your efforts much lighter as 13 | well as our review and merge procedures quick and simple. 14 | 15 | ## Coding guidelines 16 | 17 | Please make sure your contributions adhere to our coding guidelines: 18 | 19 | * Pull requests need to be based on and opened against the `develop` branch. -------------------------------------------------------------------------------- /src/views/shared/spinner/spinner.styles.ts: -------------------------------------------------------------------------------- 1 | import { createUseStyles } from "react-jss"; 2 | 3 | import { Theme } from "src/styles/theme"; 4 | 5 | interface StyleProps { 6 | color?: string; 7 | size: number; 8 | } 9 | 10 | export const useSpinnerStyles = createUseStyles((theme: Theme) => ({ 11 | "@keyframes spin": { 12 | from: { transform: "rotate(0deg)" }, 13 | to: { transform: "rotate(360deg)" }, 14 | }, 15 | bottomCircle: ({ color = theme.palette.grey.dark }: StyleProps) => ({ 16 | stroke: color, 17 | strokeOpacity: 0.2, 18 | }), 19 | root: ({ size }: StyleProps) => ({ 20 | height: size, 21 | overflow: "hidden", 22 | width: size, 23 | }), 24 | svg: { 25 | animation: "$spin 0.8s linear infinite", 26 | }, 27 | topCircle: ({ color = theme.palette.grey.dark }: StyleProps) => ({ 28 | stroke: color, 29 | strokeDasharray: "30px 200px", 30 | strokeDashoffset: "0px", 31 | strokeLinecap: "round", 32 | }), 33 | })); 34 | -------------------------------------------------------------------------------- /src/views/shared/network-selector/network-selector.styles.ts: -------------------------------------------------------------------------------- 1 | import { createUseStyles } from "react-jss"; 2 | 3 | import { Theme } from "src/styles/theme"; 4 | 5 | export const useNetworkSelectorStyles = createUseStyles((theme: Theme) => ({ 6 | networkButton: { 7 | "&:hover": { 8 | backgroundColor: theme.palette.grey.main, 9 | }, 10 | alignItems: "center", 11 | background: theme.palette.white, 12 | border: "none", 13 | borderRadius: 8, 14 | cursor: "pointer", 15 | display: "flex", 16 | gap: theme.spacing(1), 17 | justifyContent: "space-between", 18 | maxWidth: 200, 19 | padding: theme.spacing(1.25), 20 | transition: theme.hoverTransition, 21 | }, 22 | networkButtonText: { 23 | display: "none", 24 | fontSize: "14px !important", 25 | overflow: "hidden", 26 | textOverflow: "ellipsis", 27 | whiteSpace: "nowrap", 28 | [theme.breakpoints.upSm]: { 29 | display: "block", 30 | }, 31 | }, 32 | })); 33 | -------------------------------------------------------------------------------- /src/contexts/form.context.tsx: -------------------------------------------------------------------------------- 1 | import { FC, PropsWithChildren, createContext, useContext, useMemo, useState } from "react"; 2 | 3 | import { FormData } from "src/domain"; 4 | 5 | interface FormContext { 6 | formData?: FormData; 7 | setFormData: (formData?: FormData) => void; 8 | } 9 | 10 | const formContextDefaultValue: FormContext = { 11 | setFormData: () => { 12 | console.error("The form context is not yet ready"); 13 | }, 14 | }; 15 | 16 | const formContext = createContext(formContextDefaultValue); 17 | 18 | const FormProvider: FC = (props) => { 19 | const [formData, setFormData] = useState(); 20 | 21 | const value = useMemo(() => { 22 | return { formData, setFormData }; 23 | }, [formData]); 24 | 25 | return ; 26 | }; 27 | 28 | const useFormContext = (): FormContext => { 29 | return useContext(formContext); 30 | }; 31 | 32 | export { FormProvider, useFormContext }; 33 | -------------------------------------------------------------------------------- /src/adapters/tokens.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | import ethereumErc20Tokens from "src/assets/ethereum-erc20-tokens.json"; 4 | import { Token } from "src/domain"; 5 | import { StrictSchema } from "src/utils/type-safety"; 6 | 7 | export const getEthereumErc20Tokens = (): Promise => { 8 | const decodedEthereumErc20Tokens = z.array(tokenParser).safeParse(ethereumErc20Tokens); 9 | return decodedEthereumErc20Tokens.success 10 | ? Promise.resolve(decodedEthereumErc20Tokens.data) 11 | : Promise.reject(decodedEthereumErc20Tokens.error); 12 | }; 13 | 14 | export const tokenParser = StrictSchema>()( 15 | z.object({ 16 | address: z.string(), 17 | chainId: z.number(), 18 | decimals: z.number(), 19 | logoURI: z.string(), 20 | name: z.string(), 21 | symbol: z.string(), 22 | wrappedToken: z 23 | .object({ 24 | address: z.string(), 25 | chainId: z.number(), 26 | }) 27 | .optional(), 28 | }) 29 | ); 30 | -------------------------------------------------------------------------------- /src/views/shared/button/button.styles.ts: -------------------------------------------------------------------------------- 1 | import { createUseStyles } from "react-jss"; 2 | 3 | import { Theme } from "src/styles/theme"; 4 | 5 | export const useButtonStyles = createUseStyles((theme: Theme) => ({ 6 | button: { 7 | "&:disabled": { 8 | backgroundColor: theme.palette.grey.dark, 9 | cursor: "default", 10 | opacity: 0.4, 11 | }, 12 | "&:hover&:not(:disabled)": { 13 | backgroundColor: theme.palette.primary.dark, 14 | }, 15 | alignItems: "center", 16 | backgroundColor: theme.palette.primary.main, 17 | border: "none", 18 | borderRadius: 80, 19 | color: theme.palette.white, 20 | cursor: "pointer", 21 | display: "flex", 22 | fontSize: "20px", 23 | justifyContent: "center", 24 | lineHeight: "24px", 25 | minWidth: "260px", 26 | padding: [theme.spacing(2), theme.spacing(10)], 27 | transition: theme.hoverTransition, 28 | }, 29 | paddedSpinner: { 30 | paddingLeft: theme.spacing(1.5), 31 | }, 32 | })); 33 | -------------------------------------------------------------------------------- /src/views/shared/button/button.view.tsx: -------------------------------------------------------------------------------- 1 | import { FC, PropsWithChildren } from "react"; 2 | 3 | import { useButtonStyles } from "src/views/shared/button/button.styles"; 4 | import { Spinner } from "src/views/shared/spinner/spinner.view"; 5 | 6 | type ButtonProps = PropsWithChildren<{ 7 | disabled?: boolean; 8 | isLoading?: boolean; 9 | onClick?: () => void; 10 | type?: "submit"; 11 | }>; 12 | 13 | export const Button: FC = ({ children, disabled, isLoading, onClick, type }) => { 14 | const addSpinnerSpacing = children !== undefined; 15 | const classes = useButtonStyles(); 16 | 17 | return ( 18 | 31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /src/hooks/use-debounced-block.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | import { AsyncTask, ConnectedProvider } from "src/domain"; 4 | import { useDebounce } from "src/hooks/use-debounce"; 5 | import { isAsyncTaskDataAvailable } from "src/utils/types"; 6 | 7 | const DEBOUNCE_TIME_IN_MS = 750; 8 | 9 | export const useDebouncedBlock = (connectedProvider: AsyncTask) => { 10 | const [blockNumber, setBlockNumber] = useState(); 11 | 12 | useEffect(() => { 13 | if (isAsyncTaskDataAvailable(connectedProvider)) { 14 | void connectedProvider.data.provider 15 | .getBlockNumber() 16 | .then(setBlockNumber) 17 | .then(() => { 18 | connectedProvider.data.provider.on("block", setBlockNumber); 19 | }); 20 | 21 | return () => { 22 | connectedProvider.data.provider.off("block", setBlockNumber); 23 | }; 24 | } 25 | }, [connectedProvider]); 26 | 27 | return useDebounce(blockNumber, DEBOUNCE_TIME_IN_MS); 28 | }; 29 | -------------------------------------------------------------------------------- /src/views/shared/typography/typography.styles.ts: -------------------------------------------------------------------------------- 1 | import { createUseStyles } from "react-jss"; 2 | 3 | import { Theme } from "src/styles/theme"; 4 | 5 | export const useTypographyStyles = createUseStyles((theme: Theme) => ({ 6 | body1: { 7 | color: theme.palette.black, 8 | fontSize: 16, 9 | fontWeight: 500, 10 | lineHeight: "20px", 11 | marginBottom: 0, 12 | marginTop: 0, 13 | }, 14 | body2: { 15 | color: theme.palette.grey.dark, 16 | fontSize: 14, 17 | fontWeight: 400, 18 | lineHeight: "18px", 19 | marginBottom: 0, 20 | marginTop: 0, 21 | }, 22 | h1: { 23 | color: theme.palette.black, 24 | fontSize: 24, 25 | fontWeight: 700, 26 | lineHeight: "28px", 27 | marginBottom: 0, 28 | marginTop: 0, 29 | }, 30 | h2: { 31 | color: theme.palette.black, 32 | fontSize: 16, 33 | fontWeight: 500, 34 | lineHeight: "20px", 35 | marginBottom: 0, 36 | marginTop: 0, 37 | [theme.breakpoints.upSm]: { 38 | fontSize: 20, 39 | lineHeight: "24px", 40 | }, 41 | }, 42 | })); 43 | -------------------------------------------------------------------------------- /src/views/shared/spinner/spinner.view.tsx: -------------------------------------------------------------------------------- 1 | import { useSpinnerStyles } from "src/views/shared/spinner/spinner.styles"; 2 | 3 | const SIZE = 44; 4 | const THICKNESS = 3; 5 | 6 | interface SpinnerProps { 7 | color?: string; 8 | size?: number; 9 | } 10 | 11 | export const Spinner = ({ color, size }: SpinnerProps): JSX.Element => { 12 | const classes = useSpinnerStyles({ color, size: size !== undefined ? size : 48 }); 13 | 14 | return ( 15 |
16 | 17 | 25 | 33 | 34 |
35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /src/routes.ts: -------------------------------------------------------------------------------- 1 | export type RouteId = 2 | | "activity" 3 | | "bridgeConfirmation" 4 | | "bridgeDetails" 5 | | "home" 6 | | "login" 7 | | "networkError" 8 | | "settings"; 9 | 10 | export const routes: { 11 | [P in RouteId]: { 12 | id: P; 13 | isPrivate: boolean; 14 | path: string; 15 | }; 16 | } = { 17 | activity: { 18 | id: "activity", 19 | isPrivate: true, 20 | path: "/activity", 21 | }, 22 | bridgeConfirmation: { 23 | id: "bridgeConfirmation", 24 | isPrivate: true, 25 | path: "/bridge-confirmation", 26 | }, 27 | bridgeDetails: { 28 | id: "bridgeDetails", 29 | isPrivate: true, 30 | path: "/bridge-details/:bridgeId", 31 | }, 32 | home: { 33 | id: "home", 34 | isPrivate: true, 35 | path: "/", 36 | }, 37 | login: { 38 | id: "login", 39 | isPrivate: false, 40 | path: "/login", 41 | }, 42 | networkError: { 43 | id: "networkError", 44 | isPrivate: false, 45 | path: "/network-error", 46 | }, 47 | settings: { 48 | id: "settings", 49 | isPrivate: true, 50 | path: "/settings", 51 | }, 52 | }; 53 | -------------------------------------------------------------------------------- /src/assets/icons/error.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/views/bridge-details/components/chain/chain.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | 3 | import { ReactComponent as EthChainIcon } from "src/assets/icons/chains/ethereum.svg"; 4 | import { ReactComponent as PolygonZkEVMChainIcon } from "src/assets/icons/chains/polygon-zkevm.svg"; 5 | import * as domain from "src/domain"; 6 | import { useChainStyles } from "src/views/bridge-details/components/chain/chain.styles"; 7 | import { Typography } from "src/views/shared/typography/typography.view"; 8 | 9 | interface ChainProps { 10 | chain: domain.Chain; 11 | className?: string; 12 | } 13 | 14 | export const Chain: FC = ({ chain, className }) => { 15 | const classes = useChainStyles(); 16 | 17 | if (chain.key === "ethereum") { 18 | return ( 19 | 20 | {chain.name} 21 | 22 | ); 23 | } else { 24 | return ( 25 | 26 | {chain.name} 27 | 28 | ); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /src/views/home/components/header/header.styles.ts: -------------------------------------------------------------------------------- 1 | import { createUseStyles } from "react-jss"; 2 | 3 | import { Theme } from "src/styles/theme"; 4 | 5 | export const useHeaderStyles = createUseStyles((theme: Theme) => ({ 6 | activityLabel: { 7 | display: "none", 8 | [theme.breakpoints.upSm]: { 9 | display: "block", 10 | }, 11 | }, 12 | block: { 13 | display: "flex", 14 | flex: 1, 15 | gap: theme.spacing(0.75), 16 | }, 17 | centerBlock: { 18 | justifyContent: "center", 19 | }, 20 | header: { 21 | alignItems: "center", 22 | display: "flex", 23 | margin: [theme.spacing(2), "auto", 0], 24 | width: "100%", 25 | }, 26 | leftBlock: { 27 | justifyContent: "left", 28 | }, 29 | link: { 30 | "&:hover": { 31 | backgroundColor: theme.palette.grey.main, 32 | }, 33 | alignItems: "center", 34 | borderRadius: 8, 35 | display: "flex", 36 | gap: theme.spacing(1), 37 | padding: [theme.spacing(0.75), theme.spacing(1)], 38 | transition: theme.hoverTransition, 39 | }, 40 | logo: { 41 | height: 56, 42 | }, 43 | rightBlock: { 44 | justifyContent: "end", 45 | }, 46 | })); 47 | -------------------------------------------------------------------------------- /src/views/network-error/network-error.styles.ts: -------------------------------------------------------------------------------- 1 | import { createUseStyles } from "react-jss"; 2 | 3 | import { Theme } from "src/styles/theme"; 4 | 5 | export const useNetworkErrorStyles = createUseStyles((theme: Theme) => ({ 6 | button: { 7 | "&:hover": { 8 | backgroundColor: theme.palette.primary.dark, 9 | }, 10 | backgroundColor: theme.palette.primary.main, 11 | border: "none", 12 | borderRadius: 8, 13 | color: theme.palette.white, 14 | cursor: "pointer", 15 | marginTop: theme.spacing(4), 16 | padding: [theme.spacing(1), theme.spacing(5)], 17 | }, 18 | logo: { 19 | marginBottom: theme.spacing(4), 20 | maxWidth: "300px", 21 | width: "100%", 22 | }, 23 | textBox: { 24 | backgroundColor: theme.palette.white, 25 | borderRadius: 8, 26 | display: "flex", 27 | flexDirection: "column", 28 | gap: theme.spacing(2), 29 | padding: theme.spacing(3), 30 | textAlign: "center", 31 | }, 32 | wrapper: { 33 | alignItems: "center", 34 | display: "flex", 35 | flex: 1, 36 | flexDirection: "column", 37 | justifyContent: "center", 38 | marginBottom: theme.spacing(10), 39 | }, 40 | })); 41 | -------------------------------------------------------------------------------- /src/views/home/components/amount-input/amount-input.styles.ts: -------------------------------------------------------------------------------- 1 | import { createUseStyles } from "react-jss"; 2 | 3 | import { Theme } from "src/styles/theme"; 4 | 5 | export const useAmountInputStyles = createUseStyles((theme: Theme) => ({ 6 | amountInput: { 7 | "&:disabled": { 8 | backgroundColor: "transparent", 9 | }, 10 | border: "none", 11 | borderRadius: 8, 12 | fontSize: "20px", 13 | lineHeight: "24px", 14 | outline: "none", 15 | textAlign: "right", 16 | width: "100%", 17 | [theme.breakpoints.upSm]: { 18 | fontSize: (value: number) => (value < 16 ? "40px" : "30px"), 19 | lineHeight: "40px", 20 | }, 21 | }, 22 | maxButton: { 23 | "&:not(:disabled)": { 24 | cursor: "pointer", 25 | }, 26 | background: "none", 27 | border: "none", 28 | color: theme.palette.black, 29 | padding: theme.spacing(1), 30 | }, 31 | maxText: { 32 | color: theme.palette.black, 33 | }, 34 | wrapper: { 35 | alignItems: "center", 36 | display: "flex", 37 | flex: 1, 38 | marginLeft: theme.spacing(1), 39 | [theme.breakpoints.upSm]: { 40 | marginLeft: theme.spacing(2.5), 41 | }, 42 | }, 43 | })); 44 | -------------------------------------------------------------------------------- /src/views/login/components/wallet-icon/wallet-icon.view.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | 3 | import { ReactComponent as MetaMaskIcon } from "src/assets/icons/metamask.svg"; 4 | import { ReactComponent as WalletConnectIcon } from "src/assets/icons/walletconnect.svg"; 5 | import { WalletName } from "src/domain"; 6 | import { useWalletIconStyles } from "src/views/login/components/wallet-icon/wallet-icon.styles"; 7 | 8 | interface WalletIconProps { 9 | className?: string; 10 | size: "sm" | "lg"; 11 | walletName: WalletName; 12 | } 13 | 14 | export const WalletIcon: FC = ({ className, size, walletName }) => { 15 | const classes = useWalletIconStyles({ size }); 16 | 17 | switch (walletName) { 18 | case WalletName.METAMASK: { 19 | return ( 20 |
21 | 22 |
23 | ); 24 | } 25 | case WalletName.WALLET_CONNECT: { 26 | return ( 27 |
28 | 29 |
30 | ); 31 | } 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /src/views/home/components/text-match-form/text-match-form.styles.ts: -------------------------------------------------------------------------------- 1 | import { createUseStyles } from "react-jss"; 2 | 3 | import { Theme } from "src/styles/theme"; 4 | 5 | export const useTextMatchFormStyles = createUseStyles((theme: Theme) => ({ 6 | checkbox: { 7 | height: 18, 8 | width: 18, 9 | }, 10 | checkboxLabel: { 11 | userSelect: "none", 12 | }, 13 | checkboxWrapper: { 14 | alignItems: "center", 15 | display: "flex", 16 | gap: theme.spacing(1.5), 17 | marginBottom: theme.spacing(4), 18 | }, 19 | form: { 20 | display: "flex", 21 | flexDirection: "column", 22 | width: "100%", 23 | }, 24 | input: { 25 | "&:focus": { 26 | borderColor: theme.palette.black, 27 | outline: "none", 28 | }, 29 | "&:hover": { 30 | borderColor: theme.palette.black, 31 | }, 32 | appearance: "none", 33 | borderColor: theme.palette.grey.dark, 34 | borderRadius: 8, 35 | borderStyle: "solid", 36 | fontSize: 18, 37 | fontWeight: 500, 38 | marginBottom: theme.spacing(3), 39 | marginTop: theme.spacing(1), 40 | padding: [theme.spacing(1.5), theme.spacing(1.75)], 41 | transition: theme.hoverTransition, 42 | }, 43 | })); 44 | -------------------------------------------------------------------------------- /src/utils/time.ts: -------------------------------------------------------------------------------- 1 | interface TimeFromNowParams { 2 | timestamp: number; 3 | } 4 | 5 | export const getTimeFromNow = ({ timestamp }: TimeFromNowParams): string => { 6 | const day = 24 * 60 * 60 * 1000; 7 | const now = Date.now(); 8 | const diff = now - timestamp; 9 | if (diff > day) { 10 | return formatDate({ timestamp }); 11 | } else { 12 | const seconds = Math.floor(diff / 1000); 13 | if (seconds < 60) { 14 | return `${seconds}s ago`; 15 | } else if (seconds < 3600) { 16 | const minutes = Math.floor(seconds / 60); 17 | return `${minutes}m ago`; 18 | } else if (seconds < 86400) { 19 | const hours = Math.floor(seconds / 3600); 20 | return `${hours}h ago`; 21 | } else { 22 | const days = Math.floor(seconds / 86400); 23 | return `${days}d ago`; 24 | } 25 | } 26 | }; 27 | 28 | interface FormatDateParams { 29 | timestamp: number; 30 | } 31 | 32 | export const formatDate = ({ timestamp }: FormatDateParams): string => { 33 | const date = new Date(timestamp); 34 | return date.toLocaleDateString(undefined, { 35 | day: "numeric", 36 | hour: "numeric", 37 | minute: "numeric", 38 | month: "short", 39 | weekday: "short", 40 | year: "numeric", 41 | }); 42 | }; 43 | -------------------------------------------------------------------------------- /src/views/home/components/token-adder/token-adder.styles.ts: -------------------------------------------------------------------------------- 1 | import { createUseStyles } from "react-jss"; 2 | 3 | import { Theme } from "src/styles/theme"; 4 | 5 | export const useTokenAdderStyles = createUseStyles((theme: Theme) => ({ 6 | addTokenButton: { 7 | "&:hover": { 8 | backgroundColor: theme.palette.primary.dark, 9 | }, 10 | backgroundColor: theme.palette.primary.main, 11 | border: "none", 12 | borderRadius: 80, 13 | color: theme.palette.white, 14 | cursor: "pointer", 15 | fontSize: "20px", 16 | lineHeight: "24px", 17 | padding: theme.spacing(2), 18 | transition: theme.hoverTransition, 19 | }, 20 | disclaimerBox: { 21 | alignItems: "center", 22 | backgroundColor: theme.palette.grey.light, 23 | borderRadius: 8, 24 | display: "flex", 25 | marginTop: theme.spacing(2), 26 | padding: theme.spacing(2), 27 | }, 28 | disclaimerBoxMessage: { 29 | color: theme.palette.grey.dark, 30 | }, 31 | disclaimerBoxWarningIcon: { 32 | marginRight: theme.spacing(1), 33 | }, 34 | tokenAdder: { 35 | display: "flex", 36 | flex: 1, 37 | flexDirection: "column", 38 | }, 39 | tokenInfoTable: { 40 | flex: 1, 41 | marginTop: theme.spacing(3), 42 | }, 43 | })); 44 | -------------------------------------------------------------------------------- /src/views/home/components/token-selector-header/token-selector-header.view.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | 3 | import { ReactComponent as ArrowLeftIcon } from "src/assets/icons/arrow-left.svg"; 4 | import { ReactComponent as XMarkIcon } from "src/assets/icons/xmark.svg"; 5 | import { useTokenSelectorHeaderStyles } from "src/views/home/components/token-selector-header/token-selector-header.styles"; 6 | import { Typography } from "src/views/shared/typography/typography.view"; 7 | 8 | interface TokenSelectorHeaderProps { 9 | onClose?: () => void; 10 | onGoBack?: () => void; 11 | title: string; 12 | } 13 | 14 | export const TokenSelectorHeader: FC = ({ onClose, onGoBack, title }) => { 15 | const classes = useTokenSelectorHeaderStyles(); 16 | 17 | return ( 18 |
19 | {onGoBack && ( 20 | 23 | )} 24 | {title} 25 | {onClose && ( 26 | 29 | )} 30 |
31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /src/contexts/error.context.tsx: -------------------------------------------------------------------------------- 1 | import { FC, PropsWithChildren, createContext, useCallback, useContext, useMemo } from "react"; 2 | 3 | import { parseError } from "src/adapters/error"; 4 | import { useUIContext } from "src/contexts/ui.context"; 5 | 6 | interface ErrorContext { 7 | notifyError: (error: unknown) => void; 8 | } 9 | 10 | const errorContextNotReadyErrorMsg = "The error context is not yet ready"; 11 | 12 | const errorContextDefaultValue: ErrorContext = { 13 | notifyError: () => { 14 | console.error(errorContextNotReadyErrorMsg); 15 | }, 16 | }; 17 | 18 | const errorContext = createContext(errorContextDefaultValue); 19 | 20 | const ErrorProvider: FC = (props) => { 21 | const { openSnackbar } = useUIContext(); 22 | 23 | const notifyError = useCallback( 24 | (error: unknown): void => { 25 | void parseError(error) 26 | .then((parsed) => openSnackbar({ parsed, type: "error" })) 27 | .catch(console.error); 28 | }, 29 | [openSnackbar] 30 | ); 31 | 32 | const value = useMemo(() => ({ notifyError }), [notifyError]); 33 | 34 | return ; 35 | }; 36 | 37 | const useErrorContext = (): ErrorContext => { 38 | return useContext(errorContext); 39 | }; 40 | 41 | export { ErrorProvider, useErrorContext }; 42 | -------------------------------------------------------------------------------- /src/assets/icons/search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/magnifying-glass.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/views/login/components/wallet-list/wallet-list.view.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | 3 | import { ReactComponent as CaretRightIcon } from "src/assets/icons/caret-right.svg"; 4 | import { WalletName } from "src/domain"; 5 | import { WalletIcon } from "src/views/login/components/wallet-icon/wallet-icon.view"; 6 | import { useWalletListStyles } from "src/views/login/components/wallet-list/wallet-list.styles"; 7 | import { Typography } from "src/views/shared/typography/typography.view"; 8 | 9 | interface WalletListProps { 10 | onSelectWallet: (walletName: WalletName) => void; 11 | } 12 | 13 | export const WalletList: FC = ({ onSelectWallet }) => { 14 | const classes = useWalletListStyles(); 15 | 16 | return ( 17 |
    18 |
  • onSelectWallet(WalletName.METAMASK)} 21 | role="button" 22 | > 23 | 24 |
    25 | 26 | {WalletName.METAMASK} 27 | 28 | Connect using web wallet 29 |
    30 | 31 |
  • 32 |
33 | ); 34 | }; 35 | -------------------------------------------------------------------------------- /src/views/shared/network-box/network-box.styles.ts: -------------------------------------------------------------------------------- 1 | import { createUseStyles } from "react-jss"; 2 | 3 | import { Theme } from "src/styles/theme"; 4 | 5 | export const useNetworkBoxStyles = createUseStyles((theme: Theme) => ({ 6 | button: { 7 | "&:disabled": { 8 | cursor: "inherit", 9 | opacity: 0.75, 10 | }, 11 | "&:hover:not(:disabled)": { 12 | background: theme.palette.grey.main, 13 | }, 14 | alignItems: "center", 15 | appearance: "none", 16 | background: theme.palette.grey.light, 17 | border: "none", 18 | borderRadius: 8, 19 | cursor: "pointer", 20 | display: "flex", 21 | padding: [theme.spacing(1), theme.spacing(1.5)], 22 | transition: theme.hoverTransition, 23 | }, 24 | buttonIcon: { 25 | marginRight: theme.spacing(1), 26 | width: 20, 27 | }, 28 | buttons: { 29 | alignItems: "center", 30 | display: "flex", 31 | gap: theme.spacing(4), 32 | textAlign: "center", 33 | }, 34 | link: { 35 | color: theme.palette.primary.dark, 36 | }, 37 | list: { 38 | paddingLeft: theme.spacing(2), 39 | width: "100%", 40 | wordBreak: "break-word", 41 | }, 42 | listItem: { 43 | padding: [theme.spacing(0.25), 0], 44 | }, 45 | networkBox: { 46 | alignItems: "center", 47 | display: "flex", 48 | flexDirection: "column", 49 | padding: theme.spacing(2), 50 | }, 51 | })); 52 | -------------------------------------------------------------------------------- /src/views/login/login.styles.ts: -------------------------------------------------------------------------------- 1 | import { createUseStyles } from "react-jss"; 2 | 3 | import { Theme } from "src/styles/theme"; 4 | 5 | export const useLoginStyles = createUseStyles((theme: Theme) => ({ 6 | appName: { 7 | background: theme.palette.grey.main, 8 | borderRadius: 56, 9 | margin: "0px auto", 10 | marginBottom: theme.spacing(5), 11 | padding: [theme.spacing(1.25), theme.spacing(4)], 12 | }, 13 | card: { 14 | display: "flex", 15 | flexDirection: "column", 16 | margin: [0, "auto", theme.spacing(3)], 17 | }, 18 | cardHeader: { 19 | padding: [theme.spacing(3), theme.spacing(4), theme.spacing(2)], 20 | }, 21 | cardHeaderCentered: { 22 | textAlign: "center", 23 | }, 24 | cardWrap: { 25 | margin: [theme.spacing(3), 0], 26 | width: "100%", 27 | }, 28 | contentWrapper: { 29 | alignItems: "center", 30 | display: "flex", 31 | flexDirection: "column", 32 | margin: "auto", 33 | maxWidth: theme.maxWidth, 34 | width: "100%", 35 | }, 36 | login: { 37 | display: "flex", 38 | flexDirection: "column", 39 | padding: [0, theme.spacing(2)], 40 | }, 41 | logo: { 42 | height: 120, 43 | marginBottom: theme.spacing(3), 44 | marginTop: theme.spacing(8), 45 | }, 46 | networkBoxWrapper: { 47 | margin: [0, "auto", theme.spacing(3)], 48 | maxWidth: theme.maxWidth, 49 | width: "100%", 50 | }, 51 | })); 52 | -------------------------------------------------------------------------------- /src/views/app.view.tsx: -------------------------------------------------------------------------------- 1 | import { BridgeProvider } from "src/contexts/bridge.context"; 2 | import { EnvProvider } from "src/contexts/env.context"; 3 | import { ErrorProvider } from "src/contexts/error.context"; 4 | import { FormProvider } from "src/contexts/form.context"; 5 | import { PriceOracleProvider } from "src/contexts/price-oracle.context"; 6 | import { ProvidersProvider } from "src/contexts/providers.context"; 7 | import { TokensProvider } from "src/contexts/tokens.context"; 8 | import { UIProvider } from "src/contexts/ui.context"; 9 | import { useAppStyles } from "src/views/app.styles"; 10 | import { Layout } from "src/views/core/layout/layout.view"; 11 | import { Router } from "src/views/core/router/router.view"; 12 | 13 | export const App = (): JSX.Element => { 14 | useAppStyles(); 15 | 16 | return ( 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | import { 5 | TransactionReceipt, 6 | TransactionResponse, 7 | TypeSafeTransactionReceipt, 8 | TypeSafeTransactionResponse, 9 | } from "@ethersproject/abstract-provider"; 10 | import { ExternalProvider } from "@ethersproject/providers"; 11 | 12 | type Nullable = { 13 | [Property in keyof Type]: Type[Property] | null; 14 | }; 15 | 16 | declare global { 17 | interface Window { 18 | ethereum?: ExternalProvider; 19 | } 20 | 21 | const bridgeVersion: string; 22 | } 23 | 24 | // Module augmentation does not allow changing the types of existing properties (https://github.com/microsoft/TypeScript/issues/36146) 25 | // but the types of Ethers v5 are broken (https://github.com/ethers-io/ethers.js/issues/2325#issuecomment-977620740), so we provide here 26 | // a typesafe version of the types and functions that we use in this project 27 | 28 | declare module "@ethersproject/abstract-provider" { 29 | type TypeSafeTransactionResponse = Nullable; 30 | type TypeSafeTransactionReceipt = Nullable; 31 | } 32 | 33 | declare module "@ethersproject/providers" { 34 | interface BaseProvider { 35 | getTransaction(transactionHash: string): Promise; 36 | getTransactionReceipt(transactionHash: string): Promise; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/assets/icons/l2-bridge.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/views/bridge-confirmation/components/bridge-button/bridge-button.view.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | 3 | import { AsyncTask, Token } from "src/domain"; 4 | import { Button } from "src/views/shared/button/button.view"; 5 | 6 | interface BridgeButtonProps { 7 | approvalTask: AsyncTask; 8 | isDisabled?: boolean; 9 | isTxApprovalRequired: boolean; 10 | onApprove: () => void; 11 | onBridge: () => void; 12 | token: Token; 13 | } 14 | 15 | export const BridgeButton: FC = ({ 16 | approvalTask, 17 | isDisabled = false, 18 | isTxApprovalRequired, 19 | onApprove, 20 | onBridge, 21 | token, 22 | }) => { 23 | const bridgeButton = ( 24 | 27 | ); 28 | 29 | if (isTxApprovalRequired) { 30 | switch (approvalTask.status) { 31 | case "pending": { 32 | return ( 33 | 36 | ); 37 | } 38 | case "loading": 39 | case "reloading": { 40 | return ; 41 | } 42 | case "failed": { 43 | return ; 44 | } 45 | case "successful": { 46 | return bridgeButton; 47 | } 48 | } 49 | } else { 50 | return bridgeButton; 51 | } 52 | }; 53 | -------------------------------------------------------------------------------- /src/utils/fees.ts: -------------------------------------------------------------------------------- 1 | import { 2 | TypeSafeTransactionReceipt, 3 | TypeSafeTransactionResponse, 4 | } from "@ethersproject/abstract-provider"; 5 | import { BigNumber } from "ethers"; 6 | import { EIP1559GasType, Gas, LegacyGasType } from "src/domain"; 7 | 8 | type CalculateTransactionReceiptFeeParams = 9 | | { 10 | txReceipt: TypeSafeTransactionReceipt; 11 | type: EIP1559GasType; 12 | } 13 | | { 14 | txReceipt: TypeSafeTransactionReceipt; 15 | txResponse: TypeSafeTransactionResponse; 16 | type: LegacyGasType; 17 | }; 18 | 19 | export const calculateMaxTxFee = (gas: Gas): BigNumber => { 20 | switch (gas.type) { 21 | case "eip-1559": { 22 | return gas.data.gasLimit.mul(gas.data.maxFeePerGas); 23 | } 24 | case "legacy": { 25 | return gas.data.gasLimit.mul(gas.data.gasPrice); 26 | } 27 | } 28 | }; 29 | 30 | export const calculateTransactionReceiptFee = ( 31 | params: CalculateTransactionReceiptFeeParams 32 | ): BigNumber | undefined => { 33 | if (params.type === "eip-1559") { 34 | const { effectiveGasPrice, gasUsed } = params.txReceipt; 35 | 36 | if (!effectiveGasPrice || !gasUsed) { 37 | return undefined; 38 | } 39 | 40 | return gasUsed.mul(effectiveGasPrice); 41 | } else { 42 | const { gasUsed } = params.txReceipt; 43 | const { gasPrice } = params.txResponse; 44 | 45 | if (!gasUsed || !gasPrice) { 46 | return undefined; 47 | } 48 | 49 | return gasUsed.mul(gasPrice); 50 | } 51 | }; 52 | -------------------------------------------------------------------------------- /src/views/shared/confirmation-modal/confirmation-modal.styles.ts: -------------------------------------------------------------------------------- 1 | import { createUseStyles } from "react-jss"; 2 | 3 | import { Theme } from "src/styles/theme"; 4 | 5 | export const useConfirmationModalStyles = createUseStyles((theme: Theme) => ({ 6 | background: { 7 | alignItems: "center", 8 | background: theme.palette.transparency, 9 | display: "flex", 10 | height: "100vh", 11 | justifyContent: "center", 12 | width: "100%", 13 | [theme.breakpoints.upSm]: { 14 | alignItems: "flex-start", 15 | }, 16 | }, 17 | cancelButton: { 18 | "&:hover": { 19 | color: theme.palette.black, 20 | }, 21 | background: "transparent", 22 | border: 0, 23 | color: theme.palette.grey.dark, 24 | cursor: "pointer", 25 | margin: [theme.spacing(1), "auto", theme.spacing(-1), "auto"], 26 | padding: theme.spacing(1), 27 | transition: theme.hoverTransition, 28 | }, 29 | card: { 30 | display: "flex", 31 | flexDirection: "column", 32 | marginLeft: theme.spacing(1), 33 | marginRight: theme.spacing(1), 34 | maxWidth: 510, 35 | padding: theme.spacing(3), 36 | width: "100%", 37 | [theme.breakpoints.upSm]: { 38 | marginTop: theme.spacing(30), 39 | padding: theme.spacing(4), 40 | }, 41 | }, 42 | textContainer: { 43 | "& p": { 44 | lineHeight: "24px", 45 | textAlign: "center", 46 | }, 47 | margin: [theme.spacing(3), 0, [theme.spacing(4)]], 48 | }, 49 | title: { 50 | textAlign: "center", 51 | }, 52 | })); 53 | -------------------------------------------------------------------------------- /src/utils/amounts.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber, ethers } from "ethers"; 2 | import { formatUnits, parseUnits } from "ethers/lib/utils"; 3 | 4 | import { FIAT_DISPLAY_PRECISION, TOKEN_DISPLAY_PRECISION } from "src/constants"; 5 | import { Token } from "src/domain"; 6 | 7 | export const formatTokenAmount = (value: BigNumber, token: Token): string => { 8 | const amount = ethers.utils.formatUnits(value, token.decimals); 9 | const [whole, decimals = ""] = amount.split("."); 10 | const trimmed = 11 | decimals.length > TOKEN_DISPLAY_PRECISION 12 | ? decimals.slice(0, TOKEN_DISPLAY_PRECISION) 13 | : decimals; 14 | return trimmed === "" || trimmed === "0" ? whole : `${whole}.${trimmed}`; 15 | }; 16 | 17 | export const formatFiatAmount = (value: BigNumber): string => { 18 | const [whole, decimals = ""] = formatUnits(value, FIAT_DISPLAY_PRECISION).split("."); 19 | const trimmed = 20 | decimals.length > FIAT_DISPLAY_PRECISION ? decimals.slice(0, FIAT_DISPLAY_PRECISION) : decimals; 21 | return trimmed === "" || trimmed === "0" ? whole : `${whole}.${trimmed}`; 22 | }; 23 | 24 | interface Amount { 25 | precision: number; 26 | value: BigNumber; 27 | } 28 | 29 | export const multiplyAmounts = (a: Amount, b: Amount, outputPrecision: number): BigNumber => { 30 | const result = formatUnits(a.value.mul(b.value), a.precision + b.precision); 31 | const [whole, decimals = ""] = result.split("."); 32 | const trimmedDecimals = 33 | decimals.length > outputPrecision ? decimals.slice(0, outputPrecision) : decimals; 34 | return parseUnits(`${whole}.${trimmedDecimals}`, outputPrecision); 35 | }; 36 | -------------------------------------------------------------------------------- /src/utils/types.ts: -------------------------------------------------------------------------------- 1 | import * as errorAdapter from "src/adapters/error"; 2 | import { 3 | AsyncTask, 4 | EthersInsufficientFundsError, 5 | LoadingMoreItemsAsyncTask, 6 | MetaMaskResourceUnavailableError, 7 | MetaMaskUnknownChainError, 8 | MetaMaskUserRejectedRequestError, 9 | ReloadingAsyncTask, 10 | SuccessfulAsyncTask, 11 | } from "src/domain"; 12 | 13 | export function isAsyncTaskDataAvailable( 14 | task: AsyncTask 15 | ): task is P extends true 16 | ? SuccessfulAsyncTask | ReloadingAsyncTask | LoadingMoreItemsAsyncTask 17 | : SuccessfulAsyncTask | ReloadingAsyncTask { 18 | return ( 19 | task.status === "successful" || 20 | task.status === "reloading" || 21 | task.status === "loading-more-items" 22 | ); 23 | } 24 | 25 | export function isMetaMaskUserRejectedRequestError( 26 | error: unknown 27 | ): error is MetaMaskUserRejectedRequestError { 28 | return errorAdapter.metaMaskUserRejectedRequestError.safeParse(error).success; 29 | } 30 | 31 | export function isMetaMaskResourceUnavailableError( 32 | error: unknown 33 | ): error is MetaMaskResourceUnavailableError { 34 | return errorAdapter.metaMaskResourceUnavailableError.safeParse(error).success; 35 | } 36 | 37 | export function isMetaMaskUnknownChainError(error: unknown): error is MetaMaskUnknownChainError { 38 | return errorAdapter.metaMaskUnknownChainError.safeParse(error).success; 39 | } 40 | 41 | export function isEthersInsufficientFundsError( 42 | error: unknown 43 | ): error is EthersInsufficientFundsError { 44 | return errorAdapter.ethersInsufficientFundsError.safeParse(error).success; 45 | } 46 | -------------------------------------------------------------------------------- /src/assets/icons/spinner.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/views/shared/header/header.view.tsx: -------------------------------------------------------------------------------- 1 | import { FC, ReactElement } from "react"; 2 | import { Link } from "react-router-dom"; 3 | 4 | import { ReactComponent as ArrowLeftIcon } from "src/assets/icons/arrow-left.svg"; 5 | import { RouterState } from "src/domain"; 6 | import { routes } from "src/routes"; 7 | import { useHeaderStyles } from "src/views/shared/header/header.styles"; 8 | import { NetworkSelector } from "src/views/shared/network-selector/network-selector.view"; 9 | import { Typography } from "src/views/shared/typography/typography.view"; 10 | 11 | interface HeaderProps { 12 | Subtitle?: ReactElement; 13 | backTo: { routeKey: keyof typeof routes; state?: RouterState }; 14 | title: string; 15 | } 16 | 17 | export const Header: FC = ({ backTo, Subtitle, title }) => { 18 | const classes = useHeaderStyles(); 19 | const route = routes[backTo.routeKey].path; 20 | 21 | return ( 22 |
23 |
24 |
25 | 26 | 27 | 28 |
29 |
30 | {title} 31 |
32 |
33 | 34 |
35 |
36 | {Subtitle &&
{Subtitle}
} 37 |
38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /src/contexts/env.context.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | FC, 3 | PropsWithChildren, 4 | createContext, 5 | useContext, 6 | useEffect, 7 | useMemo, 8 | useState, 9 | } from "react"; 10 | import { useLocation, useNavigate } from "react-router-dom"; 11 | 12 | import { loadEnv } from "src/adapters/env"; 13 | import { providerError } from "src/adapters/error"; 14 | import { useErrorContext } from "src/contexts/error.context"; 15 | import { Env } from "src/domain"; 16 | import { routes } from "src/routes"; 17 | 18 | const envContext = createContext(undefined); 19 | 20 | const EnvProvider: FC = (props) => { 21 | const [env, setEnv] = useState(); 22 | const { notifyError } = useErrorContext(); 23 | const navigate = useNavigate(); 24 | const location = useLocation(); 25 | 26 | useEffect(() => { 27 | if (!env) { 28 | loadEnv() 29 | .then(setEnv) 30 | .catch((e) => { 31 | const error = providerError.safeParse(e); 32 | 33 | if (location.pathname !== routes.networkError.path) { 34 | if (error.success) { 35 | navigate(routes.networkError.path, { state: error.data }); 36 | } else { 37 | notifyError(e); 38 | } 39 | } 40 | }); 41 | } 42 | }, [env, location, navigate, notifyError]); 43 | 44 | const value = useMemo(() => { 45 | return env; 46 | }, [env]); 47 | 48 | return ; 49 | }; 50 | 51 | const useEnvContext = (): Env | undefined => { 52 | return useContext(envContext); 53 | }; 54 | 55 | export { EnvProvider, useEnvContext }; 56 | -------------------------------------------------------------------------------- /src/assets/icons/logout.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/views/home/components/token-selector-header/token-selector-header.styles.ts: -------------------------------------------------------------------------------- 1 | import { createUseStyles } from "react-jss"; 2 | 3 | import { Theme } from "src/styles/theme"; 4 | 5 | export const useTokenSelectorHeaderStyles = createUseStyles((theme: Theme) => ({ 6 | backButton: { 7 | "&:hover": { 8 | background: theme.palette.grey.main, 9 | }, 10 | alignItems: "center", 11 | background: theme.palette.grey.light, 12 | border: 0, 13 | borderRadius: "50%", 14 | cursor: "pointer", 15 | display: "flex", 16 | height: theme.spacing(4), 17 | justifyContent: "center", 18 | left: 0, 19 | padding: 0, 20 | position: "absolute", 21 | transition: theme.hoverTransition, 22 | width: theme.spacing(4), 23 | }, 24 | backButtonIcon: { 25 | height: 16, 26 | width: 16, 27 | }, 28 | closeButton: { 29 | "&:hover": { 30 | background: theme.palette.grey.main, 31 | }, 32 | alignItems: "center", 33 | background: theme.palette.grey.light, 34 | border: 0, 35 | borderRadius: "50%", 36 | cursor: "pointer", 37 | display: "flex", 38 | height: theme.spacing(4), 39 | justifyContent: "center", 40 | padding: 0, 41 | position: "absolute", 42 | right: 0, 43 | transition: theme.hoverTransition, 44 | width: theme.spacing(4), 45 | }, 46 | closeButtonIcon: { 47 | height: 16, 48 | width: 16, 49 | }, 50 | tokenSelectorHeader: { 51 | alignItems: "center", 52 | display: "flex", 53 | justifyContent: "center", 54 | marginBottom: theme.spacing(1), 55 | padding: [theme.spacing(0.5), 0], 56 | position: "relative", 57 | }, 58 | })); 59 | -------------------------------------------------------------------------------- /src/views/shared/token-balance/token-balance.view.tsx: -------------------------------------------------------------------------------- 1 | import { BigNumber } from "ethers"; 2 | import { FC } from "react"; 3 | 4 | import { Token } from "src/domain"; 5 | import { formatTokenAmount } from "src/utils/amounts"; 6 | import { isAsyncTaskDataAvailable } from "src/utils/types"; 7 | import { Spinner } from "src/views/shared/spinner/spinner.view"; 8 | import { useTokenBalanceStyles } from "src/views/shared/token-balance/token-balance.styles"; 9 | import { Typography, TypographyProps } from "src/views/shared/typography/typography.view"; 10 | 11 | interface TokenBalanceProps { 12 | spinnerSize: number; 13 | token: Token; 14 | typographyProps: TypographyProps; 15 | } 16 | 17 | export const TokenBalance: FC = ({ spinnerSize, token, typographyProps }) => { 18 | const classes = useTokenBalanceStyles(); 19 | const loader = ( 20 |
21 | 22 |  {token.symbol} 23 |
24 | ); 25 | 26 | if (!token.balance) { 27 | return loader; 28 | } 29 | 30 | switch (token.balance.status) { 31 | case "pending": 32 | case "loading": 33 | case "reloading": { 34 | return loader; 35 | } 36 | case "successful": 37 | case "failed": { 38 | const formattedTokenAmount = formatTokenAmount( 39 | isAsyncTaskDataAvailable(token.balance) ? token.balance.data : BigNumber.from(0), 40 | token 41 | ); 42 | 43 | return ( 44 | {`${formattedTokenAmount} ${token.symbol}`} 45 | ); 46 | } 47 | } 48 | }; 49 | -------------------------------------------------------------------------------- /src/views/settings/settings.styles.ts: -------------------------------------------------------------------------------- 1 | import { createUseStyles } from "react-jss"; 2 | 3 | import { Theme } from "src/styles/theme"; 4 | 5 | export const useSettingsStyles = createUseStyles((theme: Theme) => ({ 6 | card: { 7 | margin: [theme.spacing(5), "auto", 0, "auto"], 8 | maxWidth: theme.maxWidth, 9 | }, 10 | contentWrapper: { 11 | padding: [0, theme.spacing(2)], 12 | }, 13 | conversionIcon: { 14 | marginRight: theme.spacing(1), 15 | }, 16 | currencies: { 17 | display: "flex", 18 | flexWrap: "wrap", 19 | gap: theme.spacing(2), 20 | marginTop: theme.spacing(2), 21 | width: "100%", 22 | [theme.breakpoints.upSm]: { 23 | width: "80%", 24 | }, 25 | }, 26 | currenciesRow: { 27 | padding: theme.spacing(2), 28 | [theme.breakpoints.upSm]: { 29 | padding: theme.spacing(3), 30 | }, 31 | }, 32 | currencyBox: { 33 | "&:hover": { 34 | backgroundColor: theme.palette.grey.main, 35 | }, 36 | alignItems: "center", 37 | backgroundColor: theme.palette.grey.light, 38 | border: `1px solid ${theme.palette.white}`, 39 | borderRadius: 8, 40 | cursor: "pointer", 41 | display: "flex", 42 | padding: theme.spacing(1.5), 43 | width: theme.spacing(15), 44 | }, 45 | currencySelected: { 46 | borderColor: theme.palette.black, 47 | }, 48 | currencyText: { 49 | flex: 1, 50 | marginLeft: theme.spacing(1), 51 | }, 52 | radioInput: { 53 | cursor: "pointer", 54 | opacity: 0, 55 | position: "absolute", 56 | }, 57 | row: { 58 | alignItems: "center", 59 | display: "flex", 60 | marginBottom: theme.spacing(2), 61 | }, 62 | })); 63 | -------------------------------------------------------------------------------- /src/views/home/components/token-info/token-info.view.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | 3 | import { isChainCustomToken } from "src/adapters/storage"; 4 | import { ReactComponent as DeleteIcon } from "src/assets/icons/delete.svg"; 5 | import { Chain, Token } from "src/domain"; 6 | import { useTokenInfoStyles } from "src/views/home/components/token-info/token-info.styles"; 7 | import { TokenInfoTable } from "src/views/home/components/token-info-table/token-info-table.view"; 8 | import { TokenSelectorHeader } from "src/views/home/components/token-selector-header/token-selector-header.view"; 9 | import { Typography } from "src/views/shared/typography/typography.view"; 10 | 11 | interface TokenInfoProps { 12 | chain: Chain; 13 | onClose: () => void; 14 | onNavigateToTokenList: () => void; 15 | onRemoveToken: (token: Token) => void; 16 | token: Token; 17 | } 18 | 19 | export const TokenInfo: FC = ({ 20 | chain, 21 | onClose, 22 | onNavigateToTokenList, 23 | onRemoveToken, 24 | token, 25 | }) => { 26 | const classes = useTokenInfoStyles(); 27 | 28 | const isImportedCustomToken = isChainCustomToken(token, chain); 29 | 30 | return ( 31 |
32 | 33 | 34 | {isImportedCustomToken && ( 35 | 38 | )} 39 |
40 | ); 41 | }; 42 | -------------------------------------------------------------------------------- /src/views/home/components/token-adder/token-adder.view.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import { ReactComponent as WarningIcon } from "src/assets/icons/warning.svg"; 3 | import { Token } from "src/domain"; 4 | import { useTokenAdderStyles } from "src/views/home/components/token-adder/token-adder.styles"; 5 | 6 | import { TokenInfoTable } from "src/views/home/components/token-info-table/token-info-table.view"; 7 | import { TokenSelectorHeader } from "src/views/home/components/token-selector-header/token-selector-header.view"; 8 | import { Typography } from "src/views/shared/typography/typography.view"; 9 | 10 | interface TokenAdderProps { 11 | onAddToken: (token: Token) => void; 12 | onClose: () => void; 13 | onNavigateToTokenList: () => void; 14 | token: Token; 15 | } 16 | 17 | export const TokenAdder: FC = ({ 18 | onAddToken, 19 | onClose, 20 | onNavigateToTokenList, 21 | token, 22 | }) => { 23 | const classes = useTokenAdderStyles(); 24 | 25 | return ( 26 |
27 | 28 |
29 | 30 | 31 | Interact carefully with any new or suspicious token 32 | 33 |
34 | 35 | 38 |
39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /src/assets/icons/chains/polygon-zkevm.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/views/shared/header/header.styles.ts: -------------------------------------------------------------------------------- 1 | import { createUseStyles } from "react-jss"; 2 | 3 | import { Theme } from "src/styles/theme"; 4 | 5 | export const useHeaderStyles = createUseStyles((theme: Theme) => ({ 6 | block: { 7 | display: "flex", 8 | flex: 1, 9 | gap: theme.spacing(0.75), 10 | }, 11 | bottomRow: { 12 | marginTop: theme.spacing(1.25), 13 | }, 14 | centerBlock: { 15 | justifyContent: "center", 16 | }, 17 | header: { 18 | alignItems: "center", 19 | display: "flex", 20 | flexDirection: "column", 21 | justifyContent: "center", 22 | margin: [theme.spacing(1.5), "auto", 0], 23 | position: "relative", 24 | width: "100%", 25 | [theme.breakpoints.upSm]: { 26 | margin: [theme.spacing(3), "auto", 0], 27 | }, 28 | }, 29 | icon: { 30 | height: theme.spacing(2), 31 | width: theme.spacing(2), 32 | [theme.breakpoints.upSm]: { 33 | height: theme.spacing(2.5), 34 | width: theme.spacing(2.5), 35 | }, 36 | }, 37 | leftBlock: { 38 | justifyContent: "left", 39 | }, 40 | rightBlock: { 41 | justifyContent: "end", 42 | }, 43 | sideButton: { 44 | "&:hover": { 45 | backgroundColor: theme.palette.grey.main, 46 | }, 47 | alignItems: "center", 48 | backgroundColor: theme.palette.white, 49 | borderRadius: 50, 50 | cursor: "pointer ", 51 | display: "flex", 52 | justifyContent: "center", 53 | padding: theme.spacing(1), 54 | transition: theme.hoverTransition, 55 | [theme.breakpoints.upSm]: { 56 | padding: theme.spacing(1.25), 57 | }, 58 | }, 59 | topRow: { 60 | alignItems: "center", 61 | display: "flex", 62 | justifyContent: "center", 63 | width: "100%", 64 | }, 65 | })); 66 | -------------------------------------------------------------------------------- /src/utils/labels.ts: -------------------------------------------------------------------------------- 1 | import { Bridge, Chain, Currency, EthereumChainId } from "src/domain"; 2 | 3 | export function getBridgeStatus(status: Bridge["status"], from: Bridge["from"]): string { 4 | switch (status) { 5 | case "pending": { 6 | return "Processing"; 7 | } 8 | case "initiated": { 9 | if (from.key === "ethereum") { 10 | return "Processing"; 11 | } else { 12 | return "Initiated"; 13 | } 14 | } 15 | case "on-hold": { 16 | if (from.key === "ethereum") { 17 | return "Processing"; 18 | } else { 19 | return "On Hold"; 20 | } 21 | } 22 | case "completed": { 23 | return "Completed"; 24 | } 25 | } 26 | } 27 | 28 | export function getEthereumNetworkName(chainId: number): string { 29 | switch (chainId) { 30 | case EthereumChainId.GOERLI: { 31 | return "Goerli Testnet"; 32 | } 33 | default: { 34 | return "Ethereum"; 35 | } 36 | } 37 | } 38 | 39 | export function getDeploymentName(chain: Chain): string | undefined { 40 | switch (chain.chainId) { 41 | case EthereumChainId.MAINNET: { 42 | return "Mainnet Beta"; 43 | } 44 | case EthereumChainId.GOERLI: { 45 | return "Testnet"; 46 | } 47 | default: { 48 | return undefined; 49 | } 50 | } 51 | } 52 | 53 | type CurrencySymbol = "€" | "$" | "¥" | "£"; 54 | 55 | export function getCurrencySymbol(currency: Currency): CurrencySymbol { 56 | switch (currency) { 57 | case Currency.EUR: { 58 | return "€"; 59 | } 60 | case Currency.USD: { 61 | return "$"; 62 | } 63 | case Currency.GBP: { 64 | return "£"; 65 | } 66 | case Currency.JPY: 67 | case Currency.CNY: { 68 | return "¥"; 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/views/shared/confirmation-modal/confirmation-modal.view.tsx: -------------------------------------------------------------------------------- 1 | import { FC, MouseEvent, ReactNode } from "react"; 2 | import { Button } from "src/views/shared/button/button.view"; 3 | 4 | import { Card } from "src/views/shared/card/card.view"; 5 | import { useConfirmationModalStyles } from "src/views/shared/confirmation-modal/confirmation-modal.styles"; 6 | import { Portal } from "src/views/shared/portal/portal.view"; 7 | import { Typography } from "src/views/shared/typography/typography.view"; 8 | 9 | interface ConfirmationModalProps { 10 | message: ReactNode; 11 | onClose: () => void; 12 | onConfirm: () => void; 13 | showCancelButton?: boolean; 14 | title?: string; 15 | } 16 | 17 | export const ConfirmationModal: FC = ({ 18 | message, 19 | onClose, 20 | onConfirm, 21 | showCancelButton = true, 22 | title, 23 | }) => { 24 | const classes = useConfirmationModalStyles(); 25 | 26 | const onOutsideClick = (event: MouseEvent) => { 27 | if (event.target === event.currentTarget) { 28 | onClose(); 29 | } 30 | }; 31 | 32 | return ( 33 | 34 |
35 | 36 | {title && ( 37 | 38 | {title} 39 | 40 | )} 41 |
{message}
42 | 43 | {showCancelButton && ( 44 | 47 | )} 48 |
49 |
50 |
51 | ); 52 | }; 53 | -------------------------------------------------------------------------------- /src/views/home/components/token-info-table/token-info-table.styles.ts: -------------------------------------------------------------------------------- 1 | import { createUseStyles } from "react-jss"; 2 | 3 | import { Theme } from "src/styles/theme"; 4 | 5 | export const useTokenInfoTableStyles = createUseStyles((theme: Theme) => ({ 6 | alignRow: { 7 | alignItems: "center", 8 | display: "flex", 9 | gap: theme.spacing(1), 10 | }, 11 | button: { 12 | "&:hover": { 13 | background: theme.palette.grey.main, 14 | }, 15 | background: "transparent", 16 | border: "none", 17 | borderRadius: "50%", 18 | cursor: "pointer", 19 | display: "flex", 20 | padding: theme.spacing(1), 21 | }, 22 | chainIcon: { 23 | height: 20, 24 | width: 20, 25 | }, 26 | copyIcon: { 27 | "& path": { 28 | fill: theme.palette.grey.dark, 29 | }, 30 | height: 16, 31 | width: 16, 32 | }, 33 | newWindowIcon: { 34 | "& path": { 35 | fill: theme.palette.grey.dark, 36 | stroke: theme.palette.grey.dark, 37 | strokeWidth: 1, 38 | }, 39 | height: 16, 40 | width: 16, 41 | }, 42 | row: { 43 | alignItems: "flex-start", 44 | display: "flex", 45 | flexDirection: "column", 46 | gap: theme.spacing(1), 47 | justifyContent: "space-between", 48 | padding: [theme.spacing(1), 0], 49 | [theme.breakpoints.upSm]: { 50 | alignItems: "center", 51 | flexDirection: "row", 52 | height: 48, 53 | padding: 0, 54 | }, 55 | }, 56 | rowRightBlock: { 57 | alignItems: "center", 58 | display: "flex", 59 | }, 60 | tokenAddress: { 61 | alignItems: "center", 62 | display: "flex", 63 | gap: theme.spacing(1), 64 | paddingRight: theme.spacing(1), 65 | }, 66 | wrapper: { 67 | background: theme.palette.white, 68 | }, 69 | })); 70 | -------------------------------------------------------------------------------- /src/views/shared/chain-list/chain-list.view.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | 3 | import { ReactComponent as XMarkIcon } from "src/assets/icons/xmark.svg"; 4 | import { Chain } from "src/domain"; 5 | import { Card } from "src/views/shared/card/card.view"; 6 | import { useListStyles } from "src/views/shared/chain-list/chain-list.styles"; 7 | import { Portal } from "src/views/shared/portal/portal.view"; 8 | import { Typography } from "src/views/shared/typography/typography.view"; 9 | 10 | interface ChainListProps { 11 | chains: Chain[]; 12 | onClick: (chain: Chain) => void; 13 | onClose: () => void; 14 | } 15 | 16 | export const ChainList: FC = ({ chains, onClick, onClose }) => { 17 | const classes = useListStyles(); 18 | 19 | const onOutsideClick = (event: React.MouseEvent) => { 20 | if (event.target !== event.currentTarget) { 21 | return; 22 | } 23 | onClose(); 24 | }; 25 | 26 | return ( 27 | 28 |
29 | 30 |
31 | Select chain 32 | 35 |
36 |
37 | {chains.map((chain) => ( 38 | 42 | ))} 43 |
44 |
45 |
46 |
47 | ); 48 | }; 49 | -------------------------------------------------------------------------------- /src/views/home/components/text-match-form/text-match-form.view.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useState } from "react"; 2 | 3 | import { useTextMatchFormStyles } from "src/views/home/components/text-match-form/text-match-form.styles"; 4 | import { Button } from "src/views/shared/button/button.view"; 5 | import { Typography } from "src/views/shared/typography/typography.view"; 6 | 7 | interface TextMatchFormProps { 8 | onSubmit: (hideDepositWarning: boolean) => void; 9 | text: string; 10 | } 11 | 12 | export const TextMatchForm: FC = ({ onSubmit, text }) => { 13 | const classes = useTextMatchFormStyles(); 14 | const [isCheckboxChecked, setIsCheckboxChecked] = useState(false); 15 | const [inputValue, setInputValue] = useState(""); 16 | 17 | return ( 18 |
{ 21 | event.preventDefault(); 22 | onSubmit(isCheckboxChecked); 23 | }} 24 | > 25 | setInputValue(event.target.value)} 29 | type="text" 30 | value={inputValue} 31 | /> 32 |
33 | setIsCheckboxChecked(!isCheckboxChecked)} 38 | type="checkbox" 39 | /> 40 | 43 |
44 | 47 |
48 | ); 49 | }; 50 | -------------------------------------------------------------------------------- /src/contexts/ui.context.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | FC, 3 | PropsWithChildren, 4 | createContext, 5 | useCallback, 6 | useContext, 7 | useMemo, 8 | useState, 9 | } from "react"; 10 | 11 | import { Message } from "src/domain"; 12 | 13 | type SnackbarState = 14 | | { 15 | status: "closed"; 16 | } 17 | | { 18 | message: Message; 19 | status: "open"; 20 | }; 21 | 22 | interface UIContext { 23 | closeSnackbar: () => void; 24 | openSnackbar: (message: Message) => void; 25 | snackbar: SnackbarState; 26 | } 27 | 28 | const uiContextNotReadyErrorMsg = "The ui context is not yet ready"; 29 | 30 | const uiContextDefaultValue: UIContext = { 31 | closeSnackbar: () => { 32 | console.error(uiContextNotReadyErrorMsg); 33 | }, 34 | openSnackbar: () => { 35 | console.error(uiContextNotReadyErrorMsg); 36 | }, 37 | snackbar: { status: "closed" }, 38 | }; 39 | 40 | const uiContext = createContext(uiContextDefaultValue); 41 | 42 | const UIProvider: FC = (props) => { 43 | const [snackbar, setSnackbar] = useState({ 44 | status: "closed", 45 | }); 46 | 47 | const openSnackbar = useCallback( 48 | (message: Message): void => 49 | setSnackbar({ 50 | message, 51 | status: "open", 52 | }), 53 | [] 54 | ); 55 | 56 | const closeSnackbar = useCallback( 57 | (): void => 58 | setSnackbar({ 59 | status: "closed", 60 | }), 61 | [] 62 | ); 63 | 64 | const value = useMemo( 65 | () => ({ closeSnackbar, openSnackbar, snackbar }), 66 | [openSnackbar, closeSnackbar, snackbar] 67 | ); 68 | 69 | return ; 70 | }; 71 | 72 | const useUIContext = (): UIContext => { 73 | return useContext(uiContext); 74 | }; 75 | 76 | export { UIProvider, useUIContext }; 77 | -------------------------------------------------------------------------------- /src/views/home/components/header/header.view.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import { Link } from "react-router-dom"; 3 | 4 | import { ReactComponent as ClockIcon } from "src/assets/icons/clock.svg"; 5 | import { ReactComponent as SettingIcon } from "src/assets/icons/setting.svg"; 6 | import { ReactComponent as PolygonZkEVMLogo } from "src/assets/polygon-zkevm-logo.svg"; 7 | import { useEnvContext } from "src/contexts/env.context"; 8 | import { routes } from "src/routes"; 9 | import { areSettingsVisible } from "src/utils/feature-toggles"; 10 | import { useHeaderStyles } from "src/views/home/components/header/header.styles"; 11 | import { NetworkSelector } from "src/views/shared/network-selector/network-selector.view"; 12 | import { Typography } from "src/views/shared/typography/typography.view"; 13 | 14 | export const Header: FC = () => { 15 | const classes = useHeaderStyles(); 16 | const env = useEnvContext(); 17 | 18 | if (!env) { 19 | return null; 20 | } 21 | 22 | return ( 23 |
24 |
25 | {areSettingsVisible(env) && ( 26 | 27 | 28 | 29 | )} 30 | 31 | 32 | 33 | Activity 34 | 35 | 36 |
37 |
38 | 39 |
40 |
41 | 42 |
43 |
44 | ); 45 | }; 46 | -------------------------------------------------------------------------------- /src/views/network-error/network-error.view.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useEffect } from "react"; 2 | import { Navigate, useLocation, useNavigate } from "react-router-dom"; 3 | 4 | import { providerError } from "src/adapters/error"; 5 | import { ReactComponent as PolygonZkEVMLogo } from "src/assets/polygon-zkevm-logo.svg"; 6 | import { useEnvContext } from "src/contexts/env.context"; 7 | import { ProviderError } from "src/domain"; 8 | import { routes } from "src/routes"; 9 | import { useNetworkErrorStyles } from "src/views/network-error/network-error.styles"; 10 | import { Typography } from "src/views/shared/typography/typography.view"; 11 | 12 | export const NetworkError: FC = () => { 13 | const classes = useNetworkErrorStyles(); 14 | const navigate = useNavigate(); 15 | const { state } = useLocation(); 16 | const env = useEnvContext(); 17 | 18 | useEffect(() => { 19 | if (env) { 20 | navigate(routes.home.path); 21 | } 22 | }, [env, navigate]); 23 | 24 | const parsedProviderError = providerError.safeParse(state); 25 | 26 | return parsedProviderError.success ? ( 27 |
28 | 29 |
30 | Network Error 31 | 32 | {parsedProviderError.data === ProviderError.Ethereum 33 | ? "We cannot connect to the Ethereum node." 34 | : "We cannot connect to the Polygon zkEVM node."} 35 | 36 | It will be operative again soon 37 |
38 | 41 |
42 | ) : ( 43 | 44 | ); 45 | }; 46 | -------------------------------------------------------------------------------- /src/views/shared/snackbar/snackbar.styles.ts: -------------------------------------------------------------------------------- 1 | import { createUseStyles } from "react-jss"; 2 | 3 | import { Theme } from "src/styles/theme"; 4 | 5 | export const useSnackbarStyles = createUseStyles((theme: Theme) => ({ 6 | closeButton: { 7 | "&:hover": { 8 | background: theme.palette.grey.veryDark, 9 | }, 10 | backgroundColor: "transparent", 11 | border: "none", 12 | borderRadius: "50%", 13 | cursor: "pointer", 14 | marginLeft: theme.spacing(2.5), 15 | padding: theme.spacing(0.75), 16 | transition: theme.hoverTransition, 17 | }, 18 | closeIcon: { 19 | "& rect": { 20 | fill: theme.palette.white, 21 | }, 22 | alignItems: "center", 23 | display: "flex", 24 | height: 16, 25 | justifyContent: "center", 26 | width: 16, 27 | }, 28 | message: { 29 | color: theme.palette.white, 30 | flex: 1, 31 | lineHeight: "24px", 32 | margin: [0, theme.spacing(1.5)], 33 | whiteSpace: "break-spaces", 34 | }, 35 | messageIcon: { 36 | height: 24, 37 | width: 24, 38 | }, 39 | reportButton: { 40 | "&:hover": { 41 | backgroundColor: theme.palette.grey.dark, 42 | }, 43 | backgroundColor: theme.palette.grey.veryDark, 44 | border: 0, 45 | borderRadius: 12, 46 | color: theme.palette.white, 47 | cursor: "pointer", 48 | padding: `${theme.spacing(0.5)}px ${theme.spacing(2)}px`, 49 | transition: theme.hoverTransition, 50 | }, 51 | root: { 52 | bottom: theme.spacing(3), 53 | left: 0, 54 | position: "fixed", 55 | right: 0, 56 | width: "100%", 57 | }, 58 | wrapper: { 59 | alignItems: "center", 60 | background: theme.palette.black, 61 | borderRadius: 16, 62 | display: "flex", 63 | justifyContent: "center", 64 | margin: "0 auto", 65 | maxWidth: "644px", 66 | padding: `${theme.spacing(2)}px ${theme.spacing(3)}px`, 67 | width: "90%", 68 | }, 69 | })); 70 | -------------------------------------------------------------------------------- /src/views/home/components/deposit-warning-modal/deposit-warning-modal.styles.ts: -------------------------------------------------------------------------------- 1 | import { createUseStyles } from "react-jss"; 2 | 3 | import { Theme } from "src/styles/theme"; 4 | 5 | export const useDepositWarningModalStyles = createUseStyles((theme: Theme) => ({ 6 | background: { 7 | alignItems: "center", 8 | background: theme.palette.transparency, 9 | display: "flex", 10 | height: "100vh", 11 | justifyContent: "center", 12 | width: "100%", 13 | [theme.breakpoints.upSm]: { 14 | alignItems: "flex-start", 15 | }, 16 | }, 17 | cancelButton: { 18 | "&:hover": { 19 | color: theme.palette.black, 20 | }, 21 | background: "transparent", 22 | border: 0, 23 | color: theme.palette.grey.dark, 24 | cursor: "pointer", 25 | margin: [theme.spacing(1), "auto", theme.spacing(-1), "auto"], 26 | padding: theme.spacing(1), 27 | transition: theme.hoverTransition, 28 | }, 29 | card: { 30 | display: "flex", 31 | flexDirection: "column", 32 | marginLeft: theme.spacing(1), 33 | marginRight: theme.spacing(1), 34 | maxWidth: 510, 35 | padding: theme.spacing(3), 36 | width: "100%", 37 | [theme.breakpoints.upSm]: { 38 | marginTop: theme.spacing(30), 39 | padding: theme.spacing(4), 40 | }, 41 | }, 42 | exampleText: { 43 | userSelect: "none", 44 | }, 45 | forbiddenText: { 46 | lineHeight: "24px", 47 | marginBottom: theme.spacing(4), 48 | marginTop: theme.spacing(2), 49 | textAlign: "center", 50 | }, 51 | link: { 52 | "&:hover": { 53 | color: theme.palette.primary.dark, 54 | }, 55 | color: theme.palette.primary.main, 56 | transition: theme.hoverTransition, 57 | }, 58 | title: { 59 | marginBottom: theme.spacing(1), 60 | textAlign: "center", 61 | }, 62 | warningText: { 63 | lineHeight: "24px", 64 | margin: [theme.spacing(2), 0], 65 | textAlign: "center", 66 | }, 67 | })); 68 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Important: Inline comments are currently not supported in this env file 2 | 3 | # ETHEREUM 4 | VITE_ETHEREUM_RPC_URL=http://localhost:8545 5 | VITE_ETHEREUM_EXPLORER_URL=https://goerli.etherscan.io 6 | VITE_ETHEREUM_BRIDGE_CONTRACT_ADDRESS=0x0165878A594ca255338adfa4d48449f69242Eb8F 7 | VITE_ETHEREUM_FORCE_UPDATE_GLOBAL_EXIT_ROOT=true 8 | VITE_ETHEREUM_PROOF_OF_EFFICIENCY_CONTRACT_ADDRESS=0x8dA3b8020401851438eEe8bB434c57b54999935c 9 | VITE_ETHEREUM_ROLLUP_MANAGER_ADDRESS=0xe40DF1be0d6C310Fb549059E43caf211f13dEB47 10 | 11 | # POLYGON ZK EVM 12 | VITE_POLYGON_ZK_EVM_RPC_URL=http://localhost:8123 13 | VITE_POLYGON_ZK_EVM_EXPLORER_URL=http://localhost:4000 14 | VITE_POLYGON_ZK_EVM_BRIDGE_CONTRACT_ADDRESS=0x9d98deabc42dd696deb9e40b4f1cab7ddbf55988 15 | VITE_POLYGON_ZK_EVM_NETWORK_ID=1 16 | 17 | # BRIDGE API 18 | VITE_BRIDGE_API_URL=http://localhost:8080 19 | 20 | # FIAT EXCHANGE RATES API 21 | VITE_ENABLE_FIAT_EXCHANGE_RATES=true 22 | VITE_FIAT_EXCHANGE_RATES_API_URL=https://api.exchangeratesapi.io/v1/latest 23 | VITE_FIAT_EXCHANGE_RATES_API_KEY= 24 | VITE_FIAT_EXCHANGE_RATES_ETHEREUM_USDC_ADDRESS=0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 25 | 26 | # OUTDATED NETWORK MODAL 27 | VITE_ENABLE_OUTDATED_NETWORK_MODAL=true 28 | VITE_OUTDATED_NETWORK_MODAL_TITLE=Welcome to the new Polygon zkEVM testnet! 29 | VITE_OUTDATED_NETWORK_MODAL_MESSAGE_PARAGRAPH_1=To connect, follow the instructions available at this site. 30 | VITE_OUTDATED_NETWORK_MODAL_MESSAGE_PARAGRAPH_2=The previous network is still available until January 5th. You can access using the information available at: 31 | VITE_OUTDATED_NETWORK_MODAL_URL=https://deprecated.zkevm-test.net 32 | 33 | # DEPOSIT WARNING 34 | VITE_ENABLE_DEPOSIT_WARNING=true 35 | 36 | # REPORT FORM 37 | VITE_ENABLE_REPORT_FORM=true 38 | VITE_REPORT_FORM_URL=https://docs.google.com/forms/d/xxxxxx/viewform 39 | VITE_REPORT_FORM_ERROR_ENTRY=entry.xxxxxx 40 | VITE_REPORT_FORM_PLATFORM_ENTRY=entry.xxxxxx 41 | VITE_REPORT_FORM_URL_ENTRY=entry.xxxxxx 42 | -------------------------------------------------------------------------------- /src/views/activity/components/infinite-scroll/infinite-scroll.view.tsx: -------------------------------------------------------------------------------- 1 | import { FC, PropsWithChildren, useEffect, useRef, useState } from "react"; 2 | 3 | import { useInfiniteScrollStyles } from "src/views/activity/components/infinite-scroll/infinite-scroll.styles"; 4 | import { Spinner } from "src/views/shared/spinner/spinner.view"; 5 | 6 | const TRESHOLD = 0.9; 7 | 8 | type InfiniteScrollProps = PropsWithChildren<{ 9 | isLoading: boolean; 10 | onLoadNextPage: () => void; 11 | }>; 12 | 13 | export const InfiniteScroll: FC = ({ 14 | children, 15 | isLoading, 16 | onLoadNextPage, 17 | }) => { 18 | const classes = useInfiniteScrollStyles(); 19 | const [scrollReachedEnd, setScrollReachedEnd] = useState(false); 20 | const ref = useRef(null); 21 | 22 | useEffect(() => { 23 | const onScroll = () => { 24 | if ( 25 | ref.current && 26 | ref.current.getBoundingClientRect().bottom * TRESHOLD <= window.innerHeight 27 | ) { 28 | setScrollReachedEnd(true); 29 | } 30 | }; 31 | 32 | if (ref.current !== null) { 33 | document.addEventListener("scroll", onScroll); 34 | 35 | return () => document.removeEventListener("scroll", onScroll); 36 | } 37 | }, [ref]); 38 | 39 | useEffect(() => { 40 | if (scrollReachedEnd && !isLoading) { 41 | onLoadNextPage(); 42 | } 43 | setScrollReachedEnd(false); 44 | }, [isLoading, scrollReachedEnd, onLoadNextPage]); 45 | 46 | useEffect(() => { 47 | if (ref.current) { 48 | const isScrollVisible = ref.current.getBoundingClientRect().height > window.innerHeight; 49 | if (isScrollVisible === false) { 50 | onLoadNextPage(); 51 | } 52 | } 53 | }, [onLoadNextPage]); 54 | 55 | return ( 56 |
57 | {children} 58 | {isLoading && ( 59 |
60 | 61 |
62 | )} 63 |
64 | ); 65 | }; 66 | -------------------------------------------------------------------------------- /src/views/activity/activity.styles.ts: -------------------------------------------------------------------------------- 1 | import { createUseStyles } from "react-jss"; 2 | 3 | import { Theme } from "src/styles/theme"; 4 | 5 | export const useActivityStyles = createUseStyles((theme: Theme) => ({ 6 | bridgeCardwrapper: { 7 | "&:not(:last-child)": { 8 | marginBottom: theme.spacing(2), 9 | }, 10 | }, 11 | contentWrapper: { 12 | display: "flex", 13 | flex: 1, 14 | flexDirection: "column", 15 | padding: [0, theme.spacing(2)], 16 | }, 17 | emptyMessage: { 18 | alignSelf: "center", 19 | maxWidth: theme.maxWidth, 20 | padding: [50, theme.spacing(2)], 21 | textAlign: "center", 22 | width: "100%", 23 | [theme.breakpoints.upSm]: { 24 | padding: 100, 25 | }, 26 | }, 27 | filterBox: { 28 | "&:not(:first-of-type)": { 29 | marginLeft: theme.spacing(2), 30 | }, 31 | alignItems: "center", 32 | backgroundColor: "transparent", 33 | borderRadius: 8, 34 | cursor: "pointer", 35 | display: "flex", 36 | padding: [[theme.spacing(0.75), theme.spacing(1)]], 37 | transition: theme.hoverTransition, 38 | }, 39 | filterBoxes: { 40 | display: "flex", 41 | margin: [theme.spacing(5), "auto", theme.spacing(2)], 42 | maxWidth: theme.maxWidth, 43 | width: "100%", 44 | }, 45 | filterBoxLabel: { 46 | padding: [theme.spacing(0), theme.spacing(1)], 47 | }, 48 | filterBoxSelected: { 49 | backgroundColor: theme.palette.white, 50 | color: theme.palette.grey.dark, 51 | }, 52 | filterNumberBox: { 53 | alignItems: "center", 54 | backgroundColor: theme.palette.grey.main, 55 | borderRadius: 6, 56 | display: "flex", 57 | padding: [theme.spacing(0.25), theme.spacing(1)], 58 | }, 59 | filterNumberBoxSelected: { 60 | backgroundColor: theme.palette.grey.light, 61 | }, 62 | stickyContent: { 63 | background: theme.palette.grey.light, 64 | position: "sticky", 65 | top: 0, 66 | zIndex: 1, 67 | }, 68 | stickyContentBorder: { 69 | borderBottom: `${theme.palette.grey.main} 1px solid`, 70 | }, 71 | })); 72 | -------------------------------------------------------------------------------- /src/views/core/router/router.view.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentType, FC } from "react"; 2 | import { Navigate, Route, Routes } from "react-router-dom"; 3 | 4 | import { useEnvContext } from "src/contexts/env.context"; 5 | import { RouteId, routes } from "src/routes"; 6 | import { areSettingsVisible } from "src/utils/feature-toggles"; 7 | import { Activity } from "src/views/activity/activity.view"; 8 | import { BridgeConfirmation } from "src/views/bridge-confirmation/bridge-confirmation.view"; 9 | import { BridgeDetails } from "src/views/bridge-details/bridge-details.view"; 10 | import { Home } from "src/views/home/home.view"; 11 | import { Login } from "src/views/login/login.view"; 12 | import { NetworkError } from "src/views/network-error/network-error.view"; 13 | import { Settings } from "src/views/settings/settings.view"; 14 | import { PrivateRoute } from "src/views/shared/private-route/private-route.view"; 15 | 16 | const components: Record = { 17 | activity: Activity, 18 | bridgeConfirmation: BridgeConfirmation, 19 | bridgeDetails: BridgeDetails, 20 | home: Home, 21 | login: Login, 22 | networkError: NetworkError, 23 | settings: Settings, 24 | }; 25 | 26 | export const Router: FC = () => { 27 | const env = useEnvContext(); 28 | 29 | const filteredRoutes = 30 | !env || areSettingsVisible(env) 31 | ? routes 32 | : Object.values(routes).filter((route) => route.path !== routes.settings.path); 33 | 34 | return ( 35 | 36 | {Object.values(filteredRoutes).map(({ id, isPrivate, path }) => { 37 | const Component = components[id]; 38 | return ( 39 | 43 | 44 | 45 | ) : ( 46 | 47 | ) 48 | } 49 | key={path} 50 | path={path} 51 | /> 52 | ); 53 | })} 54 | } path="*" /> 55 | 56 | ); 57 | }; 58 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zkevm-bridge-ui", 3 | "private": true, 4 | "version": "0.1.0", 5 | "type": "module", 6 | "scripts": { 7 | "generate-contract-types": "./scripts/generate-contract-types.sh", 8 | "prepare": "husky install", 9 | "postinstall": "npm run generate-contract-types", 10 | "format": "prettier --write .", 11 | "lint": "eslint './src/**/*.{ts,tsx}'", 12 | "lint:fix": "npm run lint -- --fix", 13 | "dev": "vite", 14 | "build": "vite build", 15 | "preview": "vite preview" 16 | }, 17 | "dependencies": { 18 | "@walletconnect/ethereum-provider": "^1.7.8", 19 | "axios": "^0.27.2", 20 | "ethers": "^5.7.2", 21 | "events": "^3.3.0", 22 | "normalize.css": "^8.0.1", 23 | "platform": "^1.3.6", 24 | "react": "^18.2.0", 25 | "react-dom": "^18.2.0", 26 | "react-jss": "^10.9.1", 27 | "react-router-dom": "^6.3.0", 28 | "stacktrace-js": "^2.0.2", 29 | "zod": "^3.20.2" 30 | }, 31 | "devDependencies": { 32 | "@iden3/eslint-config-react-ts": "^1.4.0", 33 | "@typechain/ethers-v5": "^10.1.0", 34 | "@types/node": "^18.6.2", 35 | "@types/platform": "^1.3.4", 36 | "@types/react": "^18.0.15", 37 | "@types/react-dom": "^18.0.6", 38 | "@vitejs/plugin-react": "^2.0.0", 39 | "eslint": "^8.20.0", 40 | "husky": "^8.0.1", 41 | "prettier": "^2.7.1", 42 | "typescript": "^4.6.4", 43 | "vite": "^3.0.0", 44 | "vite-plugin-checker": "^0.4.9", 45 | "vite-plugin-svgr": "^2.2.1" 46 | }, 47 | "eslintConfig": { 48 | "extends": "@iden3/eslint-config-react-ts", 49 | "ignorePatterns": [ 50 | "dist" 51 | ] 52 | }, 53 | "browserslist": { 54 | "production": [ 55 | ">0.2%", 56 | "not dead", 57 | "not op_mini all" 58 | ], 59 | "development": [ 60 | "last 1 chrome version", 61 | "last 1 firefox version", 62 | "last 1 safari version" 63 | ] 64 | }, 65 | "lint-staged": { 66 | "*.{ts,ts,json}": [ 67 | "eslint --fix", 68 | "prettier --write" 69 | ], 70 | "*.{html,md}": [ 71 | "prettier --write" 72 | ] 73 | }, 74 | "engines": { 75 | "node": ">=16", 76 | "npm": ">=8" 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/views/shared/chain-list/chain-list.styles.ts: -------------------------------------------------------------------------------- 1 | import { createUseStyles } from "react-jss"; 2 | 3 | import { Theme } from "src/styles/theme"; 4 | 5 | export const useListStyles = createUseStyles((theme: Theme) => ({ 6 | background: { 7 | alignItems: "center", 8 | background: theme.palette.transparency, 9 | display: "flex", 10 | height: "100vh", 11 | justifyContent: "center", 12 | padding: [0, theme.spacing(1)], 13 | width: "100%", 14 | }, 15 | button: { 16 | "&:hover": { 17 | background: theme.palette.grey.main, 18 | }, 19 | "&:not(:first-of-type)": { 20 | marginTop: theme.spacing(1), 21 | }, 22 | alignItems: "center", 23 | background: theme.palette.grey.light, 24 | border: "none", 25 | borderRadius: 8, 26 | cursor: "pointer", 27 | display: "flex", 28 | gap: theme.spacing(1), 29 | padding: theme.spacing(2), 30 | transition: theme.hoverTransition, 31 | }, 32 | card: { 33 | maxWidth: 426, 34 | padding: theme.spacing(2), 35 | width: "100%", 36 | }, 37 | closeButton: { 38 | "&:hover": { 39 | background: theme.palette.grey.main, 40 | }, 41 | alignItems: "center", 42 | background: theme.palette.grey.light, 43 | border: 0, 44 | borderRadius: "50%", 45 | cursor: "pointer", 46 | display: "flex", 47 | height: theme.spacing(4), 48 | justifyContent: "center", 49 | padding: 0, 50 | position: "absolute", 51 | right: 0, 52 | transition: theme.hoverTransition, 53 | width: theme.spacing(4), 54 | }, 55 | closeButtonIcon: { 56 | height: 16, 57 | width: 16, 58 | }, 59 | header: { 60 | alignItems: "center", 61 | display: "flex", 62 | justifyContent: "center", 63 | marginBottom: theme.spacing(2), 64 | padding: [theme.spacing(0.5), 0], 65 | position: "relative", 66 | }, 67 | icon: { 68 | height: "24px", 69 | width: "24px", 70 | }, 71 | list: { 72 | "&::-webkit-scrollbar": { 73 | width: "4px", 74 | }, 75 | "&::-webkit-scrollbar-thumb": { 76 | backgroundColor: theme.palette.grey.main, 77 | }, 78 | "&::-webkit-scrollbar-thumb:hover": { 79 | backgroundColor: theme.palette.grey.dark, 80 | }, 81 | display: "flex", 82 | flexDirection: "column", 83 | maxHeight: 270, 84 | overflowY: "auto", 85 | }, 86 | })); 87 | -------------------------------------------------------------------------------- /src/assets/icons/walletconnect.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | WalletConnect 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.github/workflows/push-docker.yml: -------------------------------------------------------------------------------- 1 | name: Build & push (develop) 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*.*.*' # semver-like tags (glob, no regex) 7 | workflow_dispatch: 8 | inputs: 9 | tag: 10 | description: 'Image tag to publish (e.g., v1.2.3 or test-123). If empty, uses the pushed tag or falls back to short SHA.' 11 | required: false 12 | type: string 13 | 14 | permissions: 15 | contents: read 16 | packages: write 17 | 18 | env: 19 | REGISTRY: ghcr.io 20 | IMAGE_NAME: ${{ github.repository }} 21 | 22 | # Avoid parallel builds for the same ref 23 | concurrency: 24 | group: ${{ github.workflow }}-${{ github.ref }} 25 | cancel-in-progress: false 26 | 27 | jobs: 28 | build: 29 | runs-on: ubuntu-latest 30 | steps: 31 | - name: Checkout 32 | uses: actions/checkout@v4 33 | 34 | - name: Set up QEMU 35 | uses: docker/setup-qemu-action@v3 36 | 37 | - name: Set up Docker Buildx 38 | uses: docker/setup-buildx-action@v3 39 | 40 | - name: Login to GHCR 41 | uses: docker/login-action@v3 42 | with: 43 | registry: ghcr.io 44 | username: ${{ github.actor }} 45 | password: ${{ secrets.GITHUB_TOKEN }} 46 | 47 | # Derive image tags/labels 48 | - name: Docker meta 49 | id: meta 50 | uses: docker/metadata-action@v5 51 | with: 52 | images: | 53 | ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 54 | tags: | 55 | # latest if the push is to develop 56 | type=raw,value=latest,enable=${{ github.ref == 'refs/heads/develop' }} 57 | # always short sha 58 | type=sha,format=short 59 | # if the event is a tag, use semver (remove the 'v' if it exists) 60 | type=semver,pattern={{version}},enable=${{ startsWith(github.ref, 'refs/tags/') }} 61 | labels: | 62 | org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }} 63 | 64 | 65 | - name: Build and push 66 | uses: docker/build-push-action@v6 67 | with: 68 | platforms: linux/amd64,linux/arm64 69 | push: true 70 | # BuildKit cache 71 | cache-from: type=gha 72 | cache-to: type=gha,mode=max 73 | # SLSA provenance & SBOM (if supported by your Buildx builder) 74 | provenance: true 75 | sbom: true 76 | tags: ${{ steps.meta.outputs.tags }} 77 | labels: ${{ steps.meta.outputs.labels }} 78 | -------------------------------------------------------------------------------- /src/views/home/components/deposit-warning-modal/deposit-warning-modal.view.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import { 3 | DEPOSIT_CHECK_WORD, 4 | POLYGON_PRIVACY_POLICY_URL, 5 | POLYGON_TERMS_AND_CONDITIONS_URL, 6 | POLYGON_ZKEVM_RISK_DISCLOSURES_URL, 7 | } from "src/constants"; 8 | 9 | import { FormData } from "src/domain"; 10 | import { useDepositWarningModalStyles } from "src/views/home/components/deposit-warning-modal/deposit-warning-modal.styles"; 11 | import { TextMatchForm } from "src/views/home/components/text-match-form/text-match-form.view"; 12 | import { Card } from "src/views/shared/card/card.view"; 13 | import { ExternalLink } from "src/views/shared/external-link/external-link.view"; 14 | import { Portal } from "src/views/shared/portal/portal.view"; 15 | import { Typography } from "src/views/shared/typography/typography.view"; 16 | 17 | interface DepositWarningModalProps { 18 | formData: FormData; 19 | onAccept: (formData: FormData, hideDepositWarning: boolean) => void; 20 | onCancel: () => void; 21 | } 22 | 23 | export const DepositWarningModal: FC = ({ 24 | formData, 25 | onAccept, 26 | onCancel, 27 | }) => { 28 | const classes = useDepositWarningModalStyles(); 29 | 30 | return ( 31 | 32 |
33 | 34 | 35 | Warning 36 | 37 | 38 | You are about to transfer tokens using the Polygon zkEVM Mainnet Beta. There are risks 39 | associated with your use of the Mainnet Beta here. You agree to the{" "} 40 | Terms of Use, 41 | including{" "} 42 | those risks, and 43 | the Privacy Policy. 44 |
45 |
46 | To do so, type {DEPOSIT_CHECK_WORD} below to 47 | continue. 48 |
49 | onAccept(formData, hideDepositWarning)} 51 | text={DEPOSIT_CHECK_WORD} 52 | /> 53 | 56 |
57 |
58 |
59 | ); 60 | }; 61 | -------------------------------------------------------------------------------- /src/views/app.styles.ts: -------------------------------------------------------------------------------- 1 | import { createUseStyles } from "react-jss"; 2 | 3 | import { Theme } from "src/styles/theme"; 4 | 5 | export const useAppStyles = createUseStyles((theme: Theme) => ({ 6 | "@font-face": [ 7 | { 8 | fallbacks: [ 9 | { src: "url('/fonts/modern-era/ModernEra-Regular.woff') format('woff')" }, 10 | { src: "url('/fonts/modern-era/ModernEra-Regular.ttf') format('truetype')" }, 11 | ], 12 | fontFamily: "Modern Era", 13 | fontStyle: "normal", 14 | fontWeight: 400, 15 | src: "url('/fonts/modern-era/ModernEra-Regular.woff2') format('woff2')", 16 | }, 17 | { 18 | fallbacks: [ 19 | { src: "url('/fonts/modern-era/ModernEra-Medium.woff') format('woff')" }, 20 | { src: "url('/fonts/modern-era/ModernEra-Medium.ttf') format('truetype')" }, 21 | ], 22 | fontFamily: "Modern Era", 23 | fontStyle: "normal", 24 | fontWeight: 500, 25 | src: "url('/fonts/modern-era/ModernEra-Medium.woff2') format('woff2')", 26 | }, 27 | { 28 | fallbacks: [ 29 | { src: "url('/fonts/modern-era/ModernEra-Bold.woff') format('woff')" }, 30 | { src: "url('/fonts/modern-era/ModernEra-Bold.ttf') format('truetype')" }, 31 | ], 32 | fontFamily: "Modern Era", 33 | fontStyle: "normal", 34 | fontWeight: 700, 35 | src: "url('/fonts/modern-era/ModernEra-Bold.woff2') format('woff2')", 36 | }, 37 | { 38 | fallbacks: [ 39 | { src: "url('/fonts/modern-era/ModernEra-ExtraBold.woff') format('woff')" }, 40 | { src: "url('/fonts/modern-era/ModernEra-ExtraBold.ttf') format('truetype')" }, 41 | ], 42 | fontFamily: "Modern Era", 43 | fontStyle: "normal", 44 | fontWeight: 800, 45 | src: "url('/fonts/modern-era/ModernEra-ExtraBold.woff2') format('woff2')", 46 | }, 47 | ], 48 | "@global": { 49 | "#app-root": { 50 | alignItems: "center", 51 | display: "flex", 52 | flex: 1, 53 | flexDirection: "column", 54 | position: "relative", 55 | zIndex: 0, 56 | }, 57 | "#portal-root": { 58 | zIndex: 1, 59 | }, 60 | "*": { 61 | boxSizing: "border-box", 62 | }, 63 | a: { 64 | color: "inherit", 65 | textDecoration: "none", 66 | }, 67 | body: { 68 | color: theme.palette.black, 69 | display: "flex", 70 | flexDirection: "column", 71 | fontFamily: "Modern Era", 72 | fontSize: 16, 73 | minHeight: "100vh", 74 | }, 75 | "input[type='search']::-webkit-search-cancel-button": { 76 | "-webkit-appearance": "none", 77 | }, 78 | }, 79 | })); 80 | -------------------------------------------------------------------------------- /src/views/shared/network-selector/network-selector.view.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useEffect, useState } from "react"; 2 | 3 | import { ReactComponent as CaretDown } from "src/assets/icons/caret-down.svg"; 4 | import { useEnvContext } from "src/contexts/env.context"; 5 | import { useErrorContext } from "src/contexts/error.context"; 6 | import { useProvidersContext } from "src/contexts/providers.context"; 7 | import { Chain } from "src/domain"; 8 | import { useCallIfMounted } from "src/hooks/use-call-if-mounted"; 9 | import { isMetaMaskUserRejectedRequestError } from "src/utils/types"; 10 | import { ChainList } from "src/views/shared/chain-list/chain-list.view"; 11 | import { useNetworkSelectorStyles } from "src/views/shared/network-selector/network-selector.styles"; 12 | import { Typography } from "src/views/shared/typography/typography.view"; 13 | 14 | export const NetworkSelector: FC = () => { 15 | const classes = useNetworkSelectorStyles(); 16 | const env = useEnvContext(); 17 | const { changeNetwork, connectedProvider } = useProvidersContext(); 18 | const { notifyError } = useErrorContext(); 19 | const callIfMounted = useCallIfMounted(); 20 | 21 | const [isOpen, setIsOpen] = useState(false); 22 | const [selectedChain, setSelectedChain] = useState(); 23 | 24 | useEffect(() => { 25 | if (env && connectedProvider.status === "successful") { 26 | const selectedChain = env.chains.find( 27 | (chain) => chain.chainId === connectedProvider.data.chainId 28 | ); 29 | if (selectedChain) { 30 | setSelectedChain(selectedChain); 31 | } 32 | } 33 | }, [connectedProvider, env]); 34 | 35 | if (!env || !selectedChain) { 36 | return null; 37 | } 38 | 39 | return ( 40 | <> 41 | 53 | {isOpen && ( 54 | { 57 | changeNetwork(chain).catch((error) => { 58 | callIfMounted(() => { 59 | if (isMetaMaskUserRejectedRequestError(error) === false) { 60 | notifyError(error); 61 | } 62 | }); 63 | }); 64 | setIsOpen(false); 65 | }} 66 | onClose={() => { 67 | setIsOpen(false); 68 | }} 69 | /> 70 | )} 71 | 72 | ); 73 | }; 74 | -------------------------------------------------------------------------------- /src/views/shared/snackbar/snackbar.view.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useEffect } from "react"; 2 | 3 | import { ReactComponent as ErrorIcon } from "src/assets/icons/error.svg"; 4 | import { ReactComponent as SuccessIcon } from "src/assets/icons/success.svg"; 5 | import { ReactComponent as CloseIcon } from "src/assets/icons/xmark.svg"; 6 | import { SNACKBAR_AUTO_HIDE_DURATION } from "src/constants"; 7 | import { Env, Message, ReportFormEnvEnabled } from "src/domain"; 8 | import { useSnackbarStyles } from "src/views/shared/snackbar/snackbar.styles"; 9 | 10 | interface SnackbarProps { 11 | message: Message; 12 | onClose: () => void; 13 | onReport: (error: string, reportForm: ReportFormEnvEnabled) => void; 14 | reportForm: Env["reportForm"]; 15 | } 16 | 17 | export const Snackbar: FC = ({ message, onClose, onReport, reportForm }) => { 18 | const classes = useSnackbarStyles(); 19 | 20 | const Icon = ({ message }: { message: Message }): JSX.Element => { 21 | switch (message.type) { 22 | case "error": 23 | case "error-msg": { 24 | return ; 25 | } 26 | case "success-msg": { 27 | return ; 28 | } 29 | } 30 | }; 31 | 32 | useEffect(() => { 33 | if (message.type !== "error") { 34 | const closingTimeoutId = setTimeout(onClose, SNACKBAR_AUTO_HIDE_DURATION); 35 | return () => clearTimeout(closingTimeoutId); 36 | } 37 | }, [message.type, onClose]); 38 | 39 | if (message.type !== "error") { 40 | return ( 41 |
42 |
43 | 44 |

{message.text}

45 |
46 |
47 | ); 48 | } else { 49 | const { parsed, text } = message; 50 | 51 | return ( 52 |
53 |
54 | 55 |

56 | {text || reportForm.isEnabled 57 | ? "An error occurred. Would you mind reporting it?" 58 | : "An error occurred. You can see the details in the console"} 59 |

60 | {reportForm.isEnabled && ( 61 | 69 | )} 70 | 73 |
74 |
75 | ); 76 | } 77 | }; 78 | -------------------------------------------------------------------------------- /src/views/home/components/amount-input/amount-input.view.tsx: -------------------------------------------------------------------------------- 1 | import { BigNumber } from "ethers"; 2 | import { parseUnits } from "ethers/lib/utils"; 3 | import { ChangeEvent, FC, useEffect, useState } from "react"; 4 | 5 | import { Token } from "src/domain"; 6 | import { formatTokenAmount } from "src/utils/amounts"; 7 | import { useAmountInputStyles } from "src/views/home/components/amount-input/amount-input.styles"; 8 | import { Typography } from "src/views/shared/typography/typography.view"; 9 | 10 | interface AmountInputProps { 11 | balance: BigNumber; 12 | onChange: (params: { amount?: BigNumber; error?: string }) => void; 13 | token: Token; 14 | value?: BigNumber; 15 | } 16 | 17 | export const AmountInput: FC = ({ balance, onChange, token, value }) => { 18 | const defaultInputValue = value ? formatTokenAmount(value, token) : ""; 19 | const [inputValue, setInputValue] = useState(defaultInputValue); 20 | const classes = useAmountInputStyles(inputValue.length); 21 | 22 | const processOnChangeCallback = (amount?: BigNumber) => { 23 | if (amount) { 24 | const error = amount.gt(balance) ? "Insufficient balance" : undefined; 25 | 26 | return onChange({ amount, error }); 27 | } else { 28 | return onChange({}); 29 | } 30 | }; 31 | 32 | const onInputChange = (event: ChangeEvent) => { 33 | const value = event.target.value; 34 | const decimals = token.decimals; 35 | const regexToken = `^(?!0\\d|\\.)\\d*(?:\\.\\d{0,${decimals}})?$`; 36 | const INPUT_REGEX = new RegExp(regexToken); 37 | const isInputValid = INPUT_REGEX.test(value); 38 | const amount = value.length > 0 && isInputValid ? parseUnits(value, token.decimals) : undefined; 39 | 40 | if (isInputValid) { 41 | setInputValue(value); 42 | processOnChangeCallback(amount); 43 | } 44 | }; 45 | 46 | const onMax = () => { 47 | if (balance.gt(0)) { 48 | setInputValue(formatTokenAmount(balance, token)); 49 | processOnChangeCallback(balance); 50 | } else { 51 | setInputValue(""); 52 | processOnChangeCallback(); 53 | } 54 | }; 55 | 56 | useEffect(() => { 57 | // Reset the input when the chain or the token are changed 58 | if (value === undefined) { 59 | setInputValue(""); 60 | } 61 | }, [value]); 62 | 63 | return ( 64 |
65 | 70 | 77 |
78 | ); 79 | }; 80 | -------------------------------------------------------------------------------- /src/views/bridge-details/bridge-details.styles.ts: -------------------------------------------------------------------------------- 1 | import { createUseStyles } from "react-jss"; 2 | 3 | import { Theme } from "src/styles/theme"; 4 | 5 | export const useBridgeDetailsStyles = createUseStyles((theme: Theme) => ({ 6 | alignRow: { 7 | alignItems: "center", 8 | display: "flex", 9 | gap: theme.spacing(1), 10 | }, 11 | balance: { 12 | alignItems: "center", 13 | borderBottom: `1px solid ${theme.palette.grey.light}`, 14 | display: "flex", 15 | flexDirection: "column", 16 | gap: theme.spacing(1), 17 | justifyContent: "center", 18 | marginBottom: theme.spacing(1.5), 19 | paddingBottom: theme.spacing(3), 20 | }, 21 | card: { 22 | margin: [theme.spacing(5), "auto", 0], 23 | maxWidth: theme.maxWidth, 24 | padding: theme.spacing(3), 25 | width: "100%", 26 | }, 27 | contentWrapper: { 28 | padding: [0, theme.spacing(2)], 29 | }, 30 | dotCompleted: { 31 | backgroundColor: theme.palette.success.main, 32 | borderRadius: "50%", 33 | height: 6, 34 | width: 6, 35 | }, 36 | dotOnHold: { 37 | backgroundColor: theme.palette.error.main, 38 | borderRadius: "50%", 39 | height: 6, 40 | width: 6, 41 | }, 42 | dotProcessing: { 43 | backgroundColor: theme.palette.warning.main, 44 | borderRadius: "50%", 45 | height: 6, 46 | width: 6, 47 | }, 48 | explorerButton: { 49 | "&:hover": { 50 | backgroundColor: theme.palette.grey.main, 51 | }, 52 | alignItems: "center", 53 | backgroundColor: theme.palette.grey.light, 54 | border: "none", 55 | borderRadius: 8, 56 | cursor: "pointer", 57 | display: "flex", 58 | gap: theme.spacing(1), 59 | padding: [theme.spacing(1), theme.spacing(2)], 60 | }, 61 | fiat: { 62 | color: theme.palette.grey.dark, 63 | fontSize: 14, 64 | }, 65 | finaliseRow: { 66 | alignItems: "center", 67 | display: "flex", 68 | flexDirection: "column", 69 | gap: theme.spacing(1), 70 | justifyContent: "center", 71 | margin: [theme.spacing(3), 0], 72 | [theme.breakpoints.upSm]: { 73 | margin: [theme.spacing(6), 0], 74 | }, 75 | }, 76 | finaliseSpinner: { 77 | "& path": { 78 | fill: theme.palette.white, 79 | }, 80 | }, 81 | lastRow: { 82 | paddingBottom: 0, 83 | }, 84 | row: { 85 | alignItems: "flex-start", 86 | display: "flex", 87 | flexDirection: "column", 88 | gap: theme.spacing(1), 89 | justifyContent: "space-between", 90 | padding: [theme.spacing(2), 0], 91 | [theme.breakpoints.upSm]: { 92 | alignItems: "center", 93 | flexDirection: "row", 94 | padding: [theme.spacing(2.5), 0], 95 | }, 96 | }, 97 | tokenIcon: { 98 | height: 48, 99 | margin: [theme.spacing(1), 0, theme.spacing(2)], 100 | width: 48, 101 | }, 102 | })); 103 | -------------------------------------------------------------------------------- /src/assets/icons/polygon-hermez.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/views/core/layout/layout.view.tsx: -------------------------------------------------------------------------------- 1 | import { FC, PropsWithChildren, useEffect, useState } from "react"; 2 | 3 | import { reportError } from "src/adapters/error"; 4 | import { useEnvContext } from "src/contexts/env.context"; 5 | import { useUIContext } from "src/contexts/ui.context"; 6 | import { useLayoutStyles } from "src/views/core/layout/layout.styles"; 7 | import { ConfirmationModal } from "src/views/shared/confirmation-modal/confirmation-modal.view"; 8 | import { ExternalLink } from "src/views/shared/external-link/external-link.view"; 9 | import { Snackbar } from "src/views/shared/snackbar/snackbar.view"; 10 | import { Typography } from "src/views/shared/typography/typography.view"; 11 | 12 | export const Layout: FC = ({ children }) => { 13 | const classes = useLayoutStyles(); 14 | const { closeSnackbar, snackbar } = useUIContext(); 15 | const [showNetworkOutdatedModal, setShowNetworkOutdatedModal] = useState(false); 16 | const env = useEnvContext(); 17 | 18 | const onCloseSnackbar = closeSnackbar; 19 | const onReportFromSnackbar = reportError; 20 | 21 | useEffect(() => { 22 | if (env) { 23 | setShowNetworkOutdatedModal(env.outdatedNetworkModal.isEnabled); 24 | } 25 | }, [env]); 26 | 27 | return ( 28 | <> 29 |
30 |
{children}
31 |
32 | {env && snackbar.status === "open" && ( 33 | 39 | )} 40 | {env && showNetworkOutdatedModal && env.outdatedNetworkModal.isEnabled && ( 41 | 44 | {env.outdatedNetworkModal.messageParagraph1 && ( 45 | {env.outdatedNetworkModal.messageParagraph1} 46 | )} 47 | {env.outdatedNetworkModal.messageParagraph2 && ( 48 | <> 49 |
50 | {env.outdatedNetworkModal.messageParagraph2} 51 | 52 | )} 53 |
54 | {env.outdatedNetworkModal.url && ( 55 | 56 | 57 | {env.outdatedNetworkModal.url} 58 | 59 | 60 | )} 61 | 62 | } 63 | onClose={() => setShowNetworkOutdatedModal(false)} 64 | onConfirm={() => setShowNetworkOutdatedModal(false)} 65 | showCancelButton={false} 66 | title={env.outdatedNetworkModal.title} 67 | /> 68 | )} 69 | 70 | ); 71 | }; 72 | -------------------------------------------------------------------------------- /src/views/bridge-confirmation/bridge-confirmation.styles.ts: -------------------------------------------------------------------------------- 1 | import { createUseStyles } from "react-jss"; 2 | 3 | import { Theme } from "src/styles/theme"; 4 | 5 | export const useBridgeConfirmationStyles = createUseStyles((theme: Theme) => ({ 6 | arrowIcon: { 7 | transform: "rotate(90deg)", 8 | [theme.breakpoints.upSm]: { 9 | margin: [0, theme.spacing(1)], 10 | transform: "none", 11 | }, 12 | }, 13 | button: { 14 | alignItems: "center", 15 | display: "flex", 16 | flexDirection: "column", 17 | gap: theme.spacing(2), 18 | justifyContent: "center", 19 | marginTop: theme.spacing(3), 20 | [theme.breakpoints.upSm]: { 21 | marginTop: theme.spacing(6), 22 | }, 23 | }, 24 | card: { 25 | alignItems: "center", 26 | display: "flex", 27 | flexDirection: "column", 28 | margin: [theme.spacing(3), "auto", 0], 29 | maxWidth: theme.maxWidth, 30 | padding: theme.spacing(2), 31 | width: "100%", 32 | [theme.breakpoints.upSm]: { 33 | margin: [theme.spacing(6), "auto", 0], 34 | padding: theme.spacing(3), 35 | }, 36 | }, 37 | chainBox: { 38 | alignItems: "center", 39 | backgroundColor: theme.palette.grey.light, 40 | borderRadius: 56, 41 | display: "flex", 42 | flex: 1, 43 | gap: theme.spacing(1), 44 | justifyContent: "center", 45 | maxWidth: 240, 46 | padding: [theme.spacing(1), theme.spacing(2)], 47 | }, 48 | chainName: { 49 | overflow: "hidden", 50 | textOverflow: "ellipsis", 51 | whiteSpace: "nowrap", 52 | }, 53 | chainsRow: { 54 | alignItems: "center", 55 | borderBottom: `1px solid ${theme.palette.grey.light}`, 56 | display: "flex", 57 | flexDirection: "column", 58 | justifyContent: "space-between", 59 | marginBottom: theme.spacing(2), 60 | paddingBottom: theme.spacing(2), 61 | paddingTop: theme.spacing(2), 62 | width: "100%", 63 | [theme.breakpoints.upSm]: { 64 | flexDirection: "row", 65 | paddingBottom: theme.spacing(4), 66 | paddingTop: theme.spacing(3), 67 | }, 68 | }, 69 | contentWrapper: { 70 | padding: [0, theme.spacing(2)], 71 | }, 72 | error: { 73 | marginTop: theme.spacing(2), 74 | }, 75 | fee: { 76 | alignItems: "center", 77 | display: "flex", 78 | gap: theme.spacing(1), 79 | }, 80 | feeBlock: { 81 | alignItems: "center", 82 | display: "flex", 83 | flexDirection: "column", 84 | gap: theme.spacing(1), 85 | }, 86 | fiat: { 87 | marginTop: theme.spacing(1), 88 | }, 89 | infoMessage: { 90 | alignItems: "center", 91 | display: "flex", 92 | gap: theme.spacing(1), 93 | }, 94 | tokenIcon: { 95 | marginBottom: theme.spacing(1), 96 | marginTop: theme.spacing(0), 97 | [theme.breakpoints.upSm]: { 98 | marginBottom: theme.spacing(3), 99 | marginTop: theme.spacing(1), 100 | }, 101 | }, 102 | })); 103 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zkEVM Bridge UI 2 | 3 | The zkEVM Bridge UI provides a simple user interface to bridge ETH and your favorite ERC-20 tokens 4 | from Ethereum to the Polygon zkEVM and back. 5 | 6 | ## Development 7 | 8 | Clone the repo: 9 | 10 | ```sh 11 | git clone git@github.com:0xPolygonHermez/zkevm-bridge-ui.git 12 | ``` 13 | 14 | Move into the project directory: 15 | 16 | ```sh 17 | cd zkevm-bridge-ui 18 | ``` 19 | 20 | Install project dependencies: 21 | 22 | ```sh 23 | npm install 24 | ``` 25 | 26 | Finally, to be able to run the project, you need to create a `.env` file which should contain all 27 | the required environment variables. 28 | 29 | If you want to create it from scratch, you can copy the `.env.example` and then override each 30 | environment variable by your own: 31 | 32 | ```sh 33 | cp .env.example .env 34 | ``` 35 | 36 | If you want to see token prices converted to your local fiat currency in the UI you'll need to 37 | register [here](https://exchangeratesapi.io) to obtain an API Key. Once you get it, you need to set 38 | the `VITE_ENABLE_FIAT_EXCHANGE_RATES` env var to `true` and fill this required env vars as well: 39 | 40 | - `VITE_FIAT_EXCHANGE_RATES_API_URL` 41 | - `VITE_FIAT_EXCHANGE_RATES_API_KEY` 42 | - `VITE_FIAT_EXCHANGE_RATES_ETHEREUM_USDC_ADDRESS` 43 | 44 | If you just want to omit fiat conversion you can just disable this feature by setting the 45 | `VITE_ENABLE_FIAT_EXCHANGE_RATES` env var to `false`. 46 | 47 | Finally, to run the UI in development mode, you just need to run: 48 | 49 | ```sh 50 | npm run dev 51 | ``` 52 | 53 | ## Docker image 54 | 55 | A [GitHub action](.github/workflows/push-docker-develop.yml) is already configured to automatically 56 | generate and push images to DockerHub on updates to the **develop** and **main** branches. 57 | 58 | To locally generate a Docker image of the zkEVM Bridge UI, you can just run the following command: 59 | 60 | ```sh 61 | docker build . -t zkevm-bridge-ui:local 62 | ``` 63 | 64 | The Docker image won't build the UI until you run it, in order to be able to use dynamic environment 65 | variables and facilitate the deployment process. The env vars that you need to pass to the 66 | `docker run` cmd are the same as those in the `.env.example` file but without the `VITE` prefix. 67 | 68 | Example: 69 | 70 | ```sh 71 | docker run \ 72 | -e ETHEREUM_RPC_URL=http://localhost:8545 \ 73 | -e ETHEREUM_EXPLORER_URL=https://goerli.etherscan.io \ 74 | -e ETHEREUM_BRIDGE_CONTRACT_ADDRESS=0x0165878A594ca255338adfa4d48449f69242Eb8F \ 75 | -e ETHEREUM_FORCE_UPDATE_GLOBAL_EXIT_ROOT=true \ 76 | -e ETHEREUM_PROOF_OF_EFFICIENCY_CONTRACT_ADDRESS=0x8dA3b8020401851438eEe8bB434c57b54999935c \ 77 | -e POLYGON_ZK_EVM_RPC_URL=http://localhost:8123 \ 78 | -e POLYGON_ZK_EVM_EXPLORER_URL=http://localhost:4000 \ 79 | -e POLYGON_ZK_EVM_BRIDGE_CONTRACT_ADDRESS=0x9d98deabc42dd696deb9e40b4f1cab7ddbf55988 \ 80 | -e POLYGON_ZK_EVM_NETWORK_ID=1 \ 81 | -e BRIDGE_API_URL=http://localhost:8080 \ 82 | -e ENABLE_FIAT_EXCHANGE_RATES=false \ 83 | -e ENABLE_OUTDATED_NETWORK_MODAL=false \ 84 | -e ENABLE_DEPOSIT_WARNING=true \ 85 | -e ENABLE_REPORT_FORM=false \ 86 | -p 8080:80 zkevm-bridge-ui:local 87 | ``` 88 | -------------------------------------------------------------------------------- /src/views/activity/components/bridge-card/bridge-card.styles.ts: -------------------------------------------------------------------------------- 1 | import { createUseStyles } from "react-jss"; 2 | 3 | import { Theme } from "src/styles/theme"; 4 | 5 | export const useBridgeCardStyles = createUseStyles((theme: Theme) => ({ 6 | amount: { 7 | alignItems: "center", 8 | display: "flex", 9 | justifyContent: "center", 10 | }, 11 | bottom: { 12 | alignItems: "center", 13 | borderTop: [1, "solid", theme.palette.grey.light], 14 | display: "flex", 15 | justifyContent: "space-between", 16 | marginTop: theme.spacing(2), 17 | paddingTop: theme.spacing(2), 18 | }, 19 | card: { 20 | "&:hover": { 21 | backgroundColor: theme.palette.grey.main, 22 | }, 23 | cursor: "pointer", 24 | margin: "auto", 25 | maxWidth: theme.maxWidth, 26 | padding: [theme.spacing(2), theme.spacing(3)], 27 | transition: theme.hoverTransition, 28 | }, 29 | circle: { 30 | alignItems: "center", 31 | backgroundColor: theme.palette.grey.light, 32 | borderRadius: "100%", 33 | display: "flex", 34 | height: theme.spacing(6), 35 | justifyContent: "center", 36 | width: theme.spacing(6), 37 | }, 38 | fiat: { 39 | color: theme.palette.grey.dark, 40 | fontSize: 14, 41 | }, 42 | finaliseButton: { 43 | "&:disabled": { 44 | backgroundColor: theme.palette.grey.dark, 45 | cursor: "initial", 46 | opacity: 0.4, 47 | }, 48 | "&:hover&:not(:disabled)": { 49 | backgroundColor: theme.palette.primary.dark, 50 | }, 51 | backgroundColor: theme.palette.primary.main, 52 | border: "none", 53 | borderRadius: 32, 54 | color: theme.palette.white, 55 | cursor: "pointer", 56 | fontWeight: 700, 57 | lineHeight: "20px", 58 | padding: [theme.spacing(0.75), theme.spacing(3)], 59 | }, 60 | greenStatus: { 61 | backgroundColor: theme.palette.success.light, 62 | color: theme.palette.success.main, 63 | }, 64 | info: { 65 | display: "flex", 66 | flex: 1, 67 | flexDirection: "column", 68 | marginLeft: theme.spacing(2), 69 | }, 70 | infoContainer: { 71 | alignItems: "center", 72 | display: "flex", 73 | flex: 1, 74 | }, 75 | label: { 76 | marginRight: "auto", 77 | }, 78 | pendingStatus: { 79 | backgroundColor: theme.palette.warning.light, 80 | color: theme.palette.warning.main, 81 | }, 82 | row: { 83 | "&:not(:first-of-type)": { 84 | marginTop: theme.spacing(1), 85 | }, 86 | alignItems: "center", 87 | display: "flex", 88 | justifyContent: "space-between", 89 | }, 90 | statusBox: { 91 | borderRadius: 8, 92 | fontSize: 14, 93 | marginRight: "auto", 94 | padding: [theme.spacing(0.5), theme.spacing(1)], 95 | }, 96 | steps: { 97 | color: theme.palette.grey.dark, 98 | fontSize: 14, 99 | marginBottom: theme.spacing(2), 100 | marginTop: 0, 101 | }, 102 | token: { 103 | alignItems: "center", 104 | display: "flex", 105 | }, 106 | tokenIcon: { 107 | marginRight: theme.spacing(1), 108 | }, 109 | top: { 110 | display: "flex", 111 | flexDirection: "column", 112 | }, 113 | })); 114 | -------------------------------------------------------------------------------- /src/adapters/fiat-exchange-rates-api.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { z } from "zod"; 3 | 4 | import { Currency, FiatExchangeRates } from "src/domain"; 5 | import { StrictSchema } from "src/utils/type-safety"; 6 | 7 | interface GetFiatExchangeRatesSuccessResponse { 8 | rates: FiatExchangeRates; 9 | } 10 | 11 | interface GetFiatExchangeRatesUnsuccessResponse { 12 | error: { 13 | code: number; 14 | info: string; 15 | type: string; 16 | }; 17 | } 18 | 19 | interface GetFiatExchangeRatesError { 20 | error: { 21 | code: string; 22 | message: string; 23 | }; 24 | } 25 | 26 | const fiatExchangeRatesKeyParser = StrictSchema()( 27 | z.union([ 28 | z.literal("EUR"), 29 | z.literal("USD"), 30 | z.literal("JPY"), 31 | z.literal("GBP"), 32 | z.literal("CNY"), 33 | ]) 34 | ); 35 | 36 | const getFiatExchangeRatesSuccessResponseParser = 37 | StrictSchema()( 38 | z.object({ rates: z.record(fiatExchangeRatesKeyParser, z.number()) }) 39 | ); 40 | 41 | const getFiatExchangeRatesUnsuccessResponseParser = 42 | StrictSchema()( 43 | z.object({ 44 | error: z.object({ 45 | code: z.number(), 46 | info: z.string(), 47 | type: z.string(), 48 | }), 49 | }) 50 | ); 51 | 52 | const getFiatExchangeRatesErrorParser = StrictSchema()( 53 | z.object({ 54 | error: z.object({ 55 | code: z.string(), 56 | message: z.string(), 57 | }), 58 | }) 59 | ); 60 | 61 | interface GetFiatExchangeRatesParams { 62 | apiKey: string; 63 | apiUrl: string; 64 | } 65 | 66 | const getFiatExchangeRates = ({ 67 | apiKey, 68 | apiUrl, 69 | }: GetFiatExchangeRatesParams): Promise => { 70 | const params = { 71 | access_key: apiKey, 72 | base: Currency.USD, 73 | symbols: Object.values(Currency).join(","), 74 | }; 75 | 76 | return axios 77 | .request({ 78 | baseURL: apiUrl, 79 | method: "GET", 80 | params, 81 | }) 82 | .then((res) => { 83 | const parsedSuccessResponse = getFiatExchangeRatesSuccessResponseParser.safeParse(res.data); 84 | const parsedUnsuccessResponse = getFiatExchangeRatesUnsuccessResponseParser.safeParse( 85 | res.data 86 | ); 87 | if (parsedSuccessResponse.success) { 88 | return parsedSuccessResponse.data.rates; 89 | } else if (parsedUnsuccessResponse.success) { 90 | throw `Fiat Exchange Rates API error: (${parsedUnsuccessResponse.data.error.code}) ${parsedUnsuccessResponse.data.error.info}`; 91 | } else { 92 | throw parsedSuccessResponse.error; 93 | } 94 | }) 95 | .catch((error) => { 96 | if (axios.isAxiosError(error) && error.response) { 97 | const parsedError = getFiatExchangeRatesErrorParser.safeParse(error.response.data); 98 | if (parsedError.success) { 99 | throw `Fiat Exchange Rates API error: (${parsedError.data.error.code}) ${parsedError.data.error.message}`; 100 | } else { 101 | throw error; 102 | } 103 | } else { 104 | throw error; 105 | } 106 | }); 107 | }; 108 | 109 | export { getFiatExchangeRates }; 110 | -------------------------------------------------------------------------------- /src/views/home/home.view.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { useNavigate } from "react-router-dom"; 3 | import { getIsDepositWarningDismissed, setIsDepositWarningDismissed } from "src/adapters/storage"; 4 | 5 | import { ReactComponent as MetaMaskIcon } from "src/assets/icons/metamask.svg"; 6 | import { useEnvContext } from "src/contexts/env.context"; 7 | import { useFormContext } from "src/contexts/form.context"; 8 | import { useProvidersContext } from "src/contexts/providers.context"; 9 | import { FormData, ModalState } from "src/domain"; 10 | import { routes } from "src/routes"; 11 | import { getPartiallyHiddenEthereumAddress } from "src/utils/addresses"; 12 | import { BridgeForm } from "src/views/home/components/bridge-form/bridge-form.view"; 13 | import { DepositWarningModal } from "src/views/home/components/deposit-warning-modal/deposit-warning-modal.view"; 14 | import { Header } from "src/views/home/components/header/header.view"; 15 | import { useHomeStyles } from "src/views/home/home.styles"; 16 | import { NetworkBox } from "src/views/shared/network-box/network-box.view"; 17 | import { Typography } from "src/views/shared/typography/typography.view"; 18 | 19 | export const Home = (): JSX.Element => { 20 | const classes = useHomeStyles(); 21 | const navigate = useNavigate(); 22 | const env = useEnvContext(); 23 | const { formData, setFormData } = useFormContext(); 24 | const { connectedProvider } = useProvidersContext(); 25 | const [depositWarningModal, setDepositWarningModal] = useState>({ 26 | status: "closed", 27 | }); 28 | 29 | const onSubmitForm = (formData: FormData, hideDepositWarning?: boolean) => { 30 | if (hideDepositWarning) { 31 | setIsDepositWarningDismissed(hideDepositWarning); 32 | } 33 | setFormData(formData); 34 | navigate(routes.bridgeConfirmation.path); 35 | }; 36 | 37 | const onCheckShowDepositWarningAndSubmitForm = (formData: FormData) => { 38 | const isDepositWarningDismissed = getIsDepositWarningDismissed(); 39 | 40 | if ( 41 | env && 42 | env.isDepositWarningEnabled && 43 | !isDepositWarningDismissed && 44 | formData.from.key === "ethereum" 45 | ) { 46 | setDepositWarningModal({ 47 | data: formData, 48 | status: "open", 49 | }); 50 | } else { 51 | onSubmitForm(formData); 52 | } 53 | }; 54 | 55 | const onResetForm = () => { 56 | setFormData(undefined); 57 | }; 58 | 59 | return ( 60 |
61 |
62 | {connectedProvider.status === "successful" && ( 63 | <> 64 |
65 | 66 | 67 | {getPartiallyHiddenEthereumAddress(connectedProvider.data.account)} 68 | 69 |
70 |
71 | 72 |
73 | 79 | {depositWarningModal.status === "open" && ( 80 | setDepositWarningModal({ status: "closed" })} 84 | /> 85 | )} 86 | 87 | )} 88 |
89 | ); 90 | }; 91 | -------------------------------------------------------------------------------- /src/views/settings/settings.view.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useState } from "react"; 2 | 3 | import { getCurrency, setCurrency } from "src/adapters/storage"; 4 | import { ReactComponent as CheckedIcon } from "src/assets/icons/checkbox-checked.svg"; 5 | import { ReactComponent as UncheckedIcon } from "src/assets/icons/checkbox-unchecked.svg"; 6 | import { ReactComponent as CnyIcon } from "src/assets/icons/currencies/cny.svg"; 7 | import { ReactComponent as EurIcon } from "src/assets/icons/currencies/eur.svg"; 8 | import { ReactComponent as GbpIcon } from "src/assets/icons/currencies/gbp.svg"; 9 | import { ReactComponent as JpyIcon } from "src/assets/icons/currencies/jpy.svg"; 10 | import { ReactComponent as UsdIcon } from "src/assets/icons/currencies/usd.svg"; 11 | import { ReactComponent as ConversionIcon } from "src/assets/icons/currency-conversion.svg"; 12 | import { useEnvContext } from "src/contexts/env.context"; 13 | import { Currency } from "src/domain"; 14 | import { useSettingsStyles } from "src/views/settings/settings.styles"; 15 | import { Card } from "src/views/shared/card/card.view"; 16 | import { Header } from "src/views/shared/header/header.view"; 17 | import { Typography } from "src/views/shared/typography/typography.view"; 18 | 19 | export const Settings: FC = () => { 20 | const classes = useSettingsStyles(); 21 | const env = useEnvContext(); 22 | const [preferredCurrency, setPreferredCurrency] = useState(getCurrency()); 23 | const currencies = [ 24 | { icon: , id: Currency.EUR }, 25 | { icon: , id: Currency.USD }, 26 | { icon: , id: Currency.GBP }, 27 | { icon: , id: Currency.CNY }, 28 | { icon: , id: Currency.JPY }, 29 | ]; 30 | 31 | const onChangePreferredCurrency = (currency: Currency) => { 32 | setPreferredCurrency(currency); 33 | setCurrency(currency); 34 | }; 35 | 36 | if (!env) { 37 | return null; 38 | } 39 | 40 | return ( 41 |
42 |
Polygon zkEVM Bridge v{bridgeVersion}} 45 | title="Settings" 46 | /> 47 | 48 | {env.fiatExchangeRates.areEnabled && ( 49 |
50 | 51 | 52 | Currency conversion 53 | 54 | Select a currency for conversion display 55 |
56 | {currencies.map((currency) => ( 57 | 74 | ))} 75 |
76 |
77 | )} 78 |
79 |
80 | ); 81 | }; 82 | -------------------------------------------------------------------------------- /src/views/home/components/bridge-form/bridge-form.styles.ts: -------------------------------------------------------------------------------- 1 | import { createUseStyles } from "react-jss"; 2 | 3 | import { Theme } from "src/styles/theme"; 4 | 5 | export const useBridgeFormStyles = createUseStyles((theme: Theme) => ({ 6 | arrowDownIcon: { 7 | backgroundColor: theme.palette.grey.main, 8 | borderRadius: "50%", 9 | display: "flex", 10 | [theme.breakpoints.upSm]: { 11 | height: 40, 12 | padding: theme.spacing(0.5), 13 | width: 40, 14 | }, 15 | }, 16 | arrowRow: { 17 | display: "flex", 18 | justifyContent: "center", 19 | margin: [theme.spacing(1), 0], 20 | [theme.breakpoints.upSm]: { 21 | margin: [theme.spacing(2), 0], 22 | }, 23 | }, 24 | button: { 25 | alignItems: "center", 26 | display: "flex", 27 | flexDirection: "column", 28 | gap: theme.spacing(2), 29 | margin: [theme.spacing(5), "auto"], 30 | }, 31 | card: { 32 | padding: [theme.spacing(2), theme.spacing(3)], 33 | }, 34 | form: { 35 | margin: "auto", 36 | maxWidth: theme.maxWidth, 37 | }, 38 | fromChain: { 39 | "&:hover": { 40 | backgroundColor: theme.palette.grey.light, 41 | }, 42 | alignItems: "center", 43 | background: "none", 44 | border: "none", 45 | borderRadius: 8, 46 | cursor: "pointer", 47 | display: "flex", 48 | gap: theme.spacing(0.75), 49 | marginBottom: -theme.spacing(0.75), 50 | marginLeft: -theme.spacing(1.25), 51 | marginTop: theme.spacing(0.5), 52 | padding: [theme.spacing(0.75), theme.spacing(1.25)], 53 | transition: theme.hoverTransition, 54 | [theme.breakpoints.upSm]: { 55 | gap: theme.spacing(1.25), 56 | }, 57 | }, 58 | icons: { 59 | height: 20, 60 | width: 20, 61 | [theme.breakpoints.upSm]: { 62 | height: 24, 63 | width: 24, 64 | }, 65 | }, 66 | leftBox: { 67 | alignItems: "flex-start", 68 | display: "flex", 69 | flexDirection: "column", 70 | justifyContent: "space-between", 71 | }, 72 | middleRow: { 73 | borderTop: `1px solid ${theme.palette.grey.light}`, 74 | marginTop: theme.spacing(1.25), 75 | padding: [theme.spacing(2), 0, 0], 76 | }, 77 | rightBox: { 78 | alignItems: "flex-end", 79 | display: "flex", 80 | flexDirection: "column", 81 | justifyContent: "space-between", 82 | }, 83 | row: { 84 | display: "flex", 85 | justifyContent: "space-between", 86 | paddingBottom: theme.spacing(0.5), 87 | }, 88 | spinner: { 89 | margin: "auto", 90 | marginTop: theme.spacing(7), 91 | }, 92 | toChain: { 93 | alignItems: "center", 94 | background: "none", 95 | border: "none", 96 | borderRadius: 8, 97 | display: "flex", 98 | gap: theme.spacing(0.75), 99 | marginBottom: -theme.spacing(0.75), 100 | marginLeft: -theme.spacing(1.25), 101 | marginTop: theme.spacing(0.5), 102 | padding: [theme.spacing(0.75), theme.spacing(1.25)], 103 | [theme.breakpoints.upSm]: { 104 | gap: theme.spacing(1.25), 105 | }, 106 | }, 107 | tokenSelector: { 108 | "&:hover": { 109 | backgroundColor: theme.palette.grey.main, 110 | }, 111 | alignItems: "center", 112 | backgroundColor: theme.palette.grey.light, 113 | border: "none", 114 | borderRadius: 8, 115 | cursor: "pointer", 116 | display: "flex", 117 | gap: theme.spacing(1), 118 | padding: [theme.spacing(1), theme.spacing(1.25)], 119 | transition: theme.hoverTransition, 120 | [theme.breakpoints.upSm]: { 121 | backgroundColor: theme.palette.grey.light, 122 | gap: theme.spacing(2), 123 | padding: [theme.spacing(1.5), theme.spacing(2)], 124 | }, 125 | }, 126 | })); 127 | -------------------------------------------------------------------------------- /src/assets/icons/setting.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/views/home/components/token-list/token-list.styles.ts: -------------------------------------------------------------------------------- 1 | import { createUseStyles } from "react-jss"; 2 | 3 | import { Theme } from "src/styles/theme"; 4 | 5 | export const useTokenListStyles = createUseStyles((theme: Theme) => ({ 6 | addTokenButton: { 7 | "&:hover": { 8 | background: theme.palette.grey.main, 9 | }, 10 | background: theme.palette.white, 11 | border: "none", 12 | borderRadius: 8, 13 | cursor: "pointer", 14 | minWidth: 120, 15 | padding: 8, 16 | position: "absolute", 17 | right: 10, 18 | top: 10, 19 | }, 20 | centeredElement: { 21 | alignItems: "center", 22 | display: "flex", 23 | height: "100%", 24 | justifyContent: "center", 25 | textAlign: "center", 26 | }, 27 | clearSearchButton: { 28 | "&:hover": { 29 | background: theme.palette.black, 30 | }, 31 | alignItems: "center", 32 | background: theme.palette.grey.dark, 33 | border: 0, 34 | borderRadius: "50%", 35 | cursor: "pointer", 36 | display: "flex", 37 | height: 16, 38 | justifyContent: "center", 39 | padding: theme.spacing(0.5), 40 | transition: theme.hoverTransition, 41 | width: 16, 42 | }, 43 | clearSearchButtonIcon: { 44 | "& rect": { 45 | fill: theme.palette.white, 46 | stroke: theme.palette.white, 47 | strokeWidth: 2, 48 | }, 49 | }, 50 | list: { 51 | "&::-webkit-scrollbar": { 52 | width: "4px", 53 | }, 54 | "&::-webkit-scrollbar-thumb": { 55 | backgroundColor: theme.palette.grey.main, 56 | }, 57 | "&::-webkit-scrollbar-thumb:hover": { 58 | backgroundColor: theme.palette.grey.dark, 59 | }, 60 | height: "100%", 61 | overflowY: "auto", 62 | }, 63 | searchIcon: { 64 | marginRight: theme.spacing(1.25), 65 | }, 66 | searchInput: { 67 | border: 0, 68 | outline: 0, 69 | padding: [theme.spacing(2), 0], 70 | width: "100%", 71 | }, 72 | searchInputContainer: { 73 | alignItems: "center", 74 | borderBottom: `1px solid ${theme.palette.grey.light}`, 75 | display: "flex", 76 | marginBottom: theme.spacing(2), 77 | width: "100%", 78 | }, 79 | tokenBalance: { 80 | color: theme.palette.black, 81 | }, 82 | tokenBalanceWrapper: { 83 | marginLeft: "auto", 84 | }, 85 | tokenButton: { 86 | "&:hover": { 87 | background: theme.palette.grey.main, 88 | }, 89 | alignItems: "center", 90 | background: theme.palette.grey.light, 91 | border: "none", 92 | borderRadius: 8, 93 | cursor: "pointer", 94 | justifyContent: "space-between", 95 | overflow: "hidden", 96 | padding: theme.spacing(2), 97 | transition: theme.hoverTransition, 98 | width: "100%", 99 | }, 100 | tokenButtonWrapper: { 101 | "&:not(:first-of-type)": { 102 | marginTop: theme.spacing(1), 103 | }, 104 | position: "relative", 105 | }, 106 | tokenIcon: { 107 | marginRight: theme.spacing(1), 108 | }, 109 | tokenInfo: { 110 | alignItems: "center", 111 | display: "flex", 112 | }, 113 | tokenInfoButton: { 114 | "&:hover": { 115 | background: theme.palette.grey.main, 116 | }, 117 | background: "transparent", 118 | border: "none", 119 | borderRadius: "50%", 120 | cursor: "pointer", 121 | height: 32, 122 | padding: 8, 123 | position: "absolute", 124 | right: theme.spacing(2), 125 | top: "50%", 126 | transform: "translateY(-50%)", 127 | width: 32, 128 | }, 129 | tokenInfoButtonIcon: { 130 | "& path": { 131 | fill: theme.palette.grey.dark, 132 | }, 133 | }, 134 | tokenInfoWithBalance: { 135 | alignItems: "center", 136 | display: "flex", 137 | marginRight: 48, 138 | }, 139 | tokenList: { 140 | display: "flex", 141 | flexDirection: "column", 142 | height: "100%", 143 | }, 144 | })); 145 | -------------------------------------------------------------------------------- /scripts/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | ENV_FILENAME="/app/.env" 3 | 4 | # Create .env file 5 | touch $ENV_FILENAME 6 | 7 | # ETHEREUM env vars 8 | echo "VITE_ETHEREUM_RPC_URL=$ETHEREUM_RPC_URL" >> $ENV_FILENAME 9 | echo "VITE_ETHEREUM_EXPLORER_URL=$ETHEREUM_EXPLORER_URL" >> $ENV_FILENAME 10 | echo "VITE_ETHEREUM_BRIDGE_CONTRACT_ADDRESS=$ETHEREUM_BRIDGE_CONTRACT_ADDRESS" >> $ENV_FILENAME 11 | echo "VITE_ETHEREUM_FORCE_UPDATE_GLOBAL_EXIT_ROOT=$ETHEREUM_FORCE_UPDATE_GLOBAL_EXIT_ROOT" >> $ENV_FILENAME 12 | echo "VITE_ETHEREUM_PROOF_OF_EFFICIENCY_CONTRACT_ADDRESS=$ETHEREUM_PROOF_OF_EFFICIENCY_CONTRACT_ADDRESS" >> $ENV_FILENAME 13 | echo "VITE_ETHEREUM_ROLLUP_MANAGER_ADDRESS=$ETHEREUM_ROLLUP_MANAGER_ADDRESS" >> $ENV_FILENAME 14 | 15 | # POLYGON ZK EVM env vars 16 | echo "VITE_POLYGON_ZK_EVM_RPC_URL=$POLYGON_ZK_EVM_RPC_URL" >> $ENV_FILENAME 17 | echo "VITE_POLYGON_ZK_EVM_EXPLORER_URL=$POLYGON_ZK_EVM_EXPLORER_URL" >> $ENV_FILENAME 18 | echo "VITE_POLYGON_ZK_EVM_BRIDGE_CONTRACT_ADDRESS=$POLYGON_ZK_EVM_BRIDGE_CONTRACT_ADDRESS" >> $ENV_FILENAME 19 | echo "VITE_POLYGON_ZK_EVM_NETWORK_ID=$POLYGON_ZK_EVM_NETWORK_ID" >> $ENV_FILENAME 20 | 21 | # BRIDGE API env vars 22 | echo "VITE_BRIDGE_API_URL=$BRIDGE_API_URL" >> $ENV_FILENAME 23 | 24 | # FIAT EXCHANGE RATES API env vars 25 | echo "VITE_ENABLE_FIAT_EXCHANGE_RATES=$ENABLE_FIAT_EXCHANGE_RATES" >> $ENV_FILENAME 26 | 27 | if [ ! -z "$FIAT_EXCHANGE_RATES_API_URL" ]; 28 | then 29 | echo "VITE_FIAT_EXCHANGE_RATES_API_URL=$FIAT_EXCHANGE_RATES_API_URL" >> $ENV_FILENAME 30 | fi 31 | 32 | if [ ! -z "$FIAT_EXCHANGE_RATES_API_KEY" ]; 33 | then 34 | echo "VITE_FIAT_EXCHANGE_RATES_API_KEY=$FIAT_EXCHANGE_RATES_API_KEY" >> $ENV_FILENAME 35 | fi 36 | 37 | if [ ! -z "$FIAT_EXCHANGE_RATES_ETHEREUM_USDC_ADDRESS" ]; 38 | then 39 | echo "VITE_FIAT_EXCHANGE_RATES_ETHEREUM_USDC_ADDRESS=$FIAT_EXCHANGE_RATES_ETHEREUM_USDC_ADDRESS" >> $ENV_FILENAME 40 | fi 41 | 42 | # OUTDATED NETWORK MODAL 43 | echo "VITE_ENABLE_OUTDATED_NETWORK_MODAL=$ENABLE_OUTDATED_NETWORK_MODAL" >> $ENV_FILENAME 44 | 45 | if [ ! -z "$OUTDATED_NETWORK_MODAL_TITLE" ]; 46 | then 47 | echo "VITE_OUTDATED_NETWORK_MODAL_TITLE=$OUTDATED_NETWORK_MODAL_TITLE" >> $ENV_FILENAME 48 | fi 49 | 50 | if [ ! -z "$OUTDATED_NETWORK_MODAL_MESSAGE_PARAGRAPH_1" ]; 51 | then 52 | echo "VITE_OUTDATED_NETWORK_MODAL_MESSAGE_PARAGRAPH_1=$OUTDATED_NETWORK_MODAL_MESSAGE_PARAGRAPH_1" >> $ENV_FILENAME 53 | fi 54 | 55 | if [ ! -z "$OUTDATED_NETWORK_MODAL_MESSAGE_PARAGRAPH_2" ]; 56 | then 57 | echo "VITE_OUTDATED_NETWORK_MODAL_MESSAGE_PARAGRAPH_2=$OUTDATED_NETWORK_MODAL_MESSAGE_PARAGRAPH_2" >> $ENV_FILENAME 58 | fi 59 | 60 | if [ ! -z "$OUTDATED_NETWORK_MODAL_URL" ]; 61 | then 62 | echo "VITE_OUTDATED_NETWORK_MODAL_URL=$OUTDATED_NETWORK_MODAL_URL" >> $ENV_FILENAME 63 | fi 64 | 65 | # DEPOSIT WARNING 66 | echo "VITE_ENABLE_DEPOSIT_WARNING=$ENABLE_DEPOSIT_WARNING" >> $ENV_FILENAME 67 | 68 | # REPORT FORM 69 | echo "VITE_ENABLE_REPORT_FORM=$ENABLE_REPORT_FORM" >> $ENV_FILENAME 70 | 71 | if [ ! -z "$REPORT_FORM_URL" ]; 72 | then 73 | echo "VITE_REPORT_FORM_URL=$REPORT_FORM_URL" >> $ENV_FILENAME 74 | fi 75 | 76 | if [ ! -z "$REPORT_FORM_ERROR_ENTRY" ]; 77 | then 78 | echo "VITE_REPORT_FORM_ERROR_ENTRY=$REPORT_FORM_ERROR_ENTRY" >> $ENV_FILENAME 79 | fi 80 | 81 | if [ ! -z "$REPORT_FORM_PLATFORM_ENTRY" ]; 82 | then 83 | echo "VITE_REPORT_FORM_PLATFORM_ENTRY=$REPORT_FORM_PLATFORM_ENTRY" >> $ENV_FILENAME 84 | fi 85 | 86 | if [ ! -z "$REPORT_FORM_URL_ENTRY" ]; 87 | then 88 | echo "VITE_REPORT_FORM_URL_ENTRY=$REPORT_FORM_URL_ENTRY" >> $ENV_FILENAME 89 | fi 90 | 91 | echo "Generated .env file:" 92 | echo "$(cat /app/.env)" 93 | 94 | # Build app 95 | cd /app && npm run build 96 | 97 | # Copy nginx config 98 | cp /app/deployment/nginx.conf /etc/nginx/conf.d/default.conf 99 | 100 | # Copy app dist 101 | cp -r /app/dist/. /usr/share/nginx/html 102 | 103 | # Delete source code 104 | rm -rf /app 105 | 106 | # Run nginx 107 | nginx -g 'daemon off;' 108 | -------------------------------------------------------------------------------- /src/assets/icons/metamask.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 50 | 51 | 52 | 54 | 56 | 57 | 58 | 59 | 61 | 62 | --------------------------------------------------------------------------------