Feat: add lifi and small fixes (#798)

* fix: add gitattribute rules for woff2 files

* feat: add lifi widget + fixes minor issues

* fix: remove unused packages + clean constants declaration and config

* fix: update dockerfile and github ci workflows

* fix: env variable naming issue

* fix: bridge mode alt value issue + remove button component
This commit is contained in:
Victorien Gauch
2025-03-24 14:28:42 +01:00
committed by GitHub
parent 03dc277dda
commit 5b09005765
94 changed files with 2565 additions and 1808 deletions

2
.gitattributes vendored
View File

@@ -24,3 +24,5 @@ prover/prover-assets/**/**/**/*.bin binary
prover/prover-assets/**/**/**/**/*.bin binary
prover/prover-assets/kzgsrs/* binary
*.woff2 binary

View File

@@ -39,6 +39,7 @@ jobs:
NEXT_PUBLIC_WALLET_CONNECT_ID: ${{ secrets.PUBLIC_WALLET_CONNECT_ID }}
NEXT_PUBLIC_INFURA_ID: ${{ secrets.PUBLIC_BRIDGE_UI_INFURA_ID }}
NEXT_PUBLIC_DYNAMIC_ENVIRONMENT_ID: ${{ secrets.PUBLIC_DYNAMIC_ENVIRONMENT_ID }}
NEXT_PUBLIC_LIFI_API_KEY: ${{ secrets.PUBLIC_LIFI_API_KEY }}
- name: Install linux dependencies
run: |

View File

@@ -56,18 +56,19 @@ jobs:
file: ./bridge-ui/Dockerfile
push: ${{ env.DOCKERHUB_USERNAME != '' && env.DOCKERHUB_TOKEN != '' }}
tags: consensys/linea-bridge-ui:${{ env.DOCKER_TAG }}
# Env file dedicated for dev
build-args: |
ENV_FILE=./bridge-ui/.env.production
NEXT_PUBLIC_WALLET_CONNECT_ID=${{ secrets.PUBLIC_WALLET_CONNECT_ID }}
NEXT_PUBLIC_INFURA_ID=${{ secrets.PUBLIC_BRIDGE_UI_INFURA_ID }}
NEXT_PUBLIC_DYNAMIC_ENVIRONMENT_ID=${{ secrets.PUBLIC_DYNAMIC_ENVIRONMENT_ID }}
NEXT_PUBLIC_WALLET_CONNECT_ID=${{ env.NEXT_PUBLIC_WALLET_CONNECT_ID }}
NEXT_PUBLIC_INFURA_ID=${{ env.NEXT_PUBLIC_INFURA_ID }}
NEXT_PUBLIC_DYNAMIC_ENVIRONMENT_ID=${{ env.NEXT_PUBLIC_DYNAMIC_ENVIRONMENT_ID }}
NEXT_PUBLIC_LIFI_API_KEY=${{ env.NEXT_PUBLIC_LIFI_API_KEY }}
cache-from: type=registry,ref=consensys/linea-bridge-ui:buildcache
cache-to: type=registry,ref=consensys/linea-bridge-ui:buildcache,mode=max
env:
NEXT_PUBLIC_WALLET_CONNECT_ID: ${{ secrets.PUBLIC_WALLET_CONNECT_ID }}
NEXT_PUBLIC_INFURA_ID: ${{ secrets.PUBLIC_BRIDGE_UI_INFURA_ID }}
NEXT_PUBLIC_DYNAMIC_ENVIRONMENT_ID: ${{ secrets.PUBLIC_DYNAMIC_ENVIRONMENT_ID }}
NEXT_PUBLIC_LIFI_API_KEY: ${{ secrets.PUBLIC_LIFI_API_KEY }}
test-build:
if: github.event.pull_request.head.repo.fork == true

View File

@@ -7,7 +7,7 @@ NEXT_PUBLIC_MAINNET_LINEA_USDC_BRIDGE=0xA2Ee6Fce4ACB62D95448729cDb781e3BEb62504A
NEXT_PUBLIC_MAINNET_GAS_ESTIMATED=100000
NEXT_PUBLIC_MAINNET_DEFAULT_GAS_LIMIT_SURPLUS=6000
NEXT_PUBLIC_MAINNET_PROFIT_MARGIN=2
MAINNET_TOKEN_LIST=https://raw.githubusercontent.com/Consensys/linea-token-list/main/json/linea-mainnet-token-shortlist.json
NEXT_PUBLIC_MAINNET_TOKEN_LIST=https://raw.githubusercontent.com/Consensys/linea-token-list/main/json/linea-mainnet-token-shortlist.json
NEXT_PUBLIC_SEPOLIA_L1_TOKEN_BRIDGE=0x5A0a48389BB0f12E5e017116c1105da97E129142
NEXT_PUBLIC_SEPOLIA_LINEA_TOKEN_BRIDGE=0x93DcAdf238932e6e6a85852caC89cBd71798F463
@@ -18,7 +18,7 @@ NEXT_PUBLIC_SEPOLIA_LINEA_USDC_BRIDGE=0x39fd5cF710314341d35f9Dca20c1daa059Acb843
NEXT_PUBLIC_SEPOLIA_GAS_ESTIMATED=100000
NEXT_PUBLIC_SEPOLIA_DEFAULT_GAS_LIMIT_SURPLUS=6000
NEXT_PUBLIC_SEPOLIA_PROFIT_MARGIN=2
SEPOLIA_TOKEN_LIST=https://raw.githubusercontent.com/Consensys/linea-token-list/main/json/linea-sepolia-token-shortlist.json
NEXT_PUBLIC_SEPOLIA_TOKEN_LIST=https://raw.githubusercontent.com/Consensys/linea-token-list/main/json/linea-sepolia-token-shortlist.json
NEXT_PUBLIC_WALLET_CONNECT_ID=<GITHUB_SECRET>
NEXT_PUBLIC_DYNAMIC_ENVIRONMENT_ID=<GITHUB_SECRET>

View File

@@ -7,7 +7,7 @@ NEXT_PUBLIC_MAINNET_LINEA_USDC_BRIDGE=0xA2Ee6Fce4ACB62D95448729cDb781e3BEb62504A
NEXT_PUBLIC_MAINNET_GAS_ESTIMATED=100000
NEXT_PUBLIC_MAINNET_DEFAULT_GAS_LIMIT_SURPLUS=6000
NEXT_PUBLIC_MAINNET_PROFIT_MARGIN=2
MAINNET_TOKEN_LIST=https://raw.githubusercontent.com/Consensys/linea-token-list/main/json/linea-mainnet-token-shortlist.json
NEXT_PUBLIC_MAINNET_TOKEN_LIST=https://raw.githubusercontent.com/Consensys/linea-token-list/main/json/linea-mainnet-token-shortlist.json
NEXT_PUBLIC_SEPOLIA_L1_TOKEN_BRIDGE=0x5A0a48389BB0f12E5e017116c1105da97E129142
NEXT_PUBLIC_SEPOLIA_LINEA_TOKEN_BRIDGE=0x93DcAdf238932e6e6a85852caC89cBd71798F463
@@ -18,7 +18,7 @@ NEXT_PUBLIC_SEPOLIA_LINEA_USDC_BRIDGE=0x39fd5cF710314341d35f9Dca20c1daa059Acb843
NEXT_PUBLIC_SEPOLIA_GAS_ESTIMATED=100000
NEXT_PUBLIC_SEPOLIA_DEFAULT_GAS_LIMIT_SURPLUS=6000
NEXT_PUBLIC_SEPOLIA_PROFIT_MARGIN=2
SEPOLIA_TOKEN_LIST=https://raw.githubusercontent.com/Consensys/linea-token-list/main/json/linea-sepolia-token-shortlist.json
NEXT_PUBLIC_SEPOLIA_TOKEN_LIST=https://raw.githubusercontent.com/Consensys/linea-token-list/main/json/linea-sepolia-token-shortlist.json
NEXT_PUBLIC_WALLET_CONNECT_ID=<YOUR__WALLET_CONNECT_ID>
NEXT_PUBLIC_DYNAMIC_ENVIRONMENT_ID=<YOUR_DYNAMIC_ENVIRONMENT_ID>

View File

@@ -9,9 +9,11 @@ FROM base AS builder
ARG NEXT_PUBLIC_WALLET_CONNECT_ID
ARG NEXT_PUBLIC_INFURA_ID
ARG NEXT_PUBLIC_DYNAMIC_ENVIRONMENT_ID
ARG NEXT_PUBLIC_LIFI_API_KEY
ENV NEXT_PUBLIC_WALLET_CONNECT_ID=$NEXT_PUBLIC_WALLET_CONNECT_ID
ENV NEXT_PUBLIC_INFURA_ID=$NEXT_PUBLIC_INFURA_ID
ENV NEXT_PUBLIC_DYNAMIC_ENVIRONMENT_ID=$NEXT_PUBLIC_DYNAMIC_ENVIRONMENT_ID
ENV NEXT_PUBLIC_LIFI_API_KEY=$NEXT_PUBLIC_LIFI_API_KEY
ARG ENV_FILE
WORKDIR /app

View File

@@ -21,13 +21,14 @@
},
"dependencies": {
"@consensys/linea-sdk": "0.3.0",
"@dynamic-labs/ethereum": "4.6.3",
"@dynamic-labs/sdk-react-core": "4.6.3",
"@dynamic-labs/wagmi-connector": "4.6.3",
"@dynamic-labs/ethereum": "4.9.5",
"@dynamic-labs/sdk-react-core": "4.9.5",
"@dynamic-labs/wagmi-connector": "4.9.5",
"@headlessui/react": "2.1.9",
"@tanstack/react-query": "5.62.16",
"@wagmi/connectors": "5.1.15",
"@wagmi/core": "2.16.3",
"@lifi/widget": "3.18.1",
"@tanstack/react-query": "5.69.0",
"@wagmi/connectors": "5.7.11",
"@wagmi/core": "2.16.7",
"auto-zustand-selectors-hook": "3.0.1",
"clsx": "2.1.1",
"date-fns": "4.1.0",
@@ -35,14 +36,14 @@
"loglevel": "1.9.2",
"next": "14.2.24",
"next-seo": "6.6.0",
"pino-pretty": "11.2.2",
"pino-pretty": "13.0.0",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-icons": "5.3.0",
"sass": "1.83.3",
"react-icons": "5.5.0",
"sass": "1.86.0",
"sharp": "0.33.5",
"viem": "2.22.4",
"wagmi": "2.14.6",
"viem": "2.23.13",
"wagmi": "2.14.15",
"zod": "3.24.2",
"zustand": "4.5.4"
},
@@ -53,9 +54,9 @@
"@types/fs-extra": "11.0.4",
"@types/react": "18.3.11",
"@types/react-dom": "18.3.0",
"autoprefixer": "10.4.20",
"dotenv": "16.4.5",
"autoprefixer": "10.4.21",
"dotenv": "16.4.7",
"eslint-config-next": "14.2.15",
"postcss": "8.4.47"
"postcss": "8.5.3"
}
}

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

@@ -68,7 +68,12 @@ export default function FaqPage() {
<ul className={styles["list"]}>
{faqList.map((faq, index) => (
<FaqItem key={index} data={faq} isOpen={openIndex === index} onToggle={() => handleToggle(index)} />
<FaqItem
key={`faq-item-${index}`}
data={faq}
isOpen={openIndex === index}
onToggle={() => handleToggle(index)}
/>
))}
</ul>
</div>

View File

@@ -21,11 +21,11 @@ const metadata: Metadata = {
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" data-theme="v2">
<html lang="en" data-theme="v2" className={clsx(atypFont.variable, atypTextFont.variable)}>
<title>{metadata.title?.toString()}</title>
<meta name="description" content={metadata.description?.toString()} key="desc" />
<body className={clsx(atypFont.variable, atypTextFont.variable, atypFont.className, atypTextFont.className)}>
<body>
<noscript dangerouslySetInnerHTML={{ __html: gtmNoScript }} />
<Providers>
@@ -45,8 +45,8 @@ export default function RootLayout({ children }: { children: React.ReactNode })
</svg>
</body>
<Script id="usabilla" dangerouslySetInnerHTML={{ __html: usabillaBeScript }} />
<Script id="gtm" dangerouslySetInnerHTML={{ __html: gtmScript }} />
<Script id="usabilla" dangerouslySetInnerHTML={{ __html: usabillaBeScript }} strategy="lazyOnload" />
<Script id="gtm" dangerouslySetInnerHTML={{ __html: gtmScript }} strategy="lazyOnload" />
</html>
);
}

View File

@@ -0,0 +1,10 @@
.content-wrapper {
max-width: 29.25rem;
margin: 0 auto 3.75rem;
width: calc(100% - 3rem);
@include bp("tablet") {
width: 100%;
}
}

View File

@@ -0,0 +1,21 @@
"use client";
import InternalNav from "@/components/internal-nav";
import BridgeLayout from "@/components/bridge/bridge-layout";
import styles from "./page.module.scss";
import TopBanner from "@/components/top-banner";
export default function Home() {
return (
<>
<TopBanner
text="Bridging USDC (USDC.e) is temporarily disabled until March 26. Learn more in our announcement here."
href="https://x.com/LineaBuild/status/1901347758230958528"
/>
<section className={styles["content-wrapper"]}>
<InternalNav />
<BridgeLayout />
</section>
</>
);
}

View File

@@ -1,21 +1,14 @@
"use client";
import InternalNav from "@/components/internal-nav";
import BridgeLayout from "@/components/bridge/bridge-layout";
import styles from "./page.module.scss";
import TopBanner from "@/components/top-banner";
import { Widget } from "@/components/lifi/widget";
export default function Home() {
export default function Page() {
return (
<>
<TopBanner
text="Bridging USDC (USDC.e) is temporarily disabled until March 26. Learn more in our announcement here."
href="https://x.com/LineaBuild/status/1901347758230958528"
/>
<section className={styles["content-wrapper"]}>
<InternalNav />
<BridgeLayout />
</section>
</>
<section className={styles["content-wrapper"]}>
<InternalNav />
<Widget />
</section>
);
}

View File

@@ -3,81 +3,26 @@ import localFont from "next/font/local";
const atypFont = localFont({
display: "swap",
src: [
{
path: "../../../public/fonts/AtypDisplay-Light-subset.woff2",
weight: "300",
style: "normal",
},
{
path: "../../../public/fonts/AtypDisplay-Light.woff2",
weight: "300",
style: "normal",
},
{
path: "../../../public/fonts/AtypDisplay-LightItalic.woff2",
weight: "300",
style: "italic",
},
{
path: "../../../public/fonts/AtypDisplay-Regular-subset.woff2",
weight: "400",
style: "normal",
},
{
path: "../../../public/fonts/AtypDisplay-Regular.woff2",
weight: "400",
style: "normal",
},
{
path: "../../../public/fonts/AtypDisplay-Italic.woff2",
weight: "400",
style: "italic",
},
{
path: "../../../public/fonts/AtypDisplay-Medium-subset.woff2",
weight: "500",
style: "normal",
},
{
path: "../../../public/fonts/AtypDisplay-Medium.woff2",
weight: "500",
style: "normal",
},
{
path: "../../../public/fonts/AtypDisplay-MediumItalic.woff2",
weight: "500",
style: "italic",
},
{
path: "../../../public/fonts/AtypDisplay-Semibold-subset.woff2",
weight: "600",
style: "normal",
},
{
path: "../../../public/fonts/AtypDisplay-Semibold.woff2",
weight: "600",
style: "normal",
},
{
path: "../../../public/fonts/AtypDisplay-SemiboldItalic.woff2",
weight: "600",
style: "italic",
},
{
path: "../../../public/fonts/AtypDisplay-Bold-subset.woff2",
weight: "700",
style: "normal",
},
{
path: "../../../public/fonts/AtypDisplay-Bold.woff2",
weight: "700",
style: "normal",
},
{
path: "../../../public/fonts/AtypDisplay-BoldItalic.woff2",
weight: "700",
style: "italic",
},
],
variable: "--font-atyp",
});

View File

@@ -3,86 +3,26 @@ import localFont from "next/font/local";
const atypTextFont = localFont({
display: "swap",
src: [
{
path: "../../../public/fonts/AtypText-Light-subset.woff2",
weight: "300",
style: "normal",
},
{
path: "../../../public/fonts/AtypText-LightItalic.woff2",
weight: "300",
style: "italic",
},
{
path: "../../../public/fonts/AtypText-Light.woff2",
weight: "300",
style: "normal",
},
{
path: "../../../public/fonts/AtypText-Regular-subset.woff2",
weight: "400",
style: "normal",
},
{
path: "../../../public/fonts/AtypText-Regular.woff2",
weight: "400",
style: "normal",
},
{
path: "../../../public/fonts/AtypText-Italic-subset.woff2",
weight: "400",
style: "italic",
},
{
path: "../../../public/fonts/AtypText-Italic.woff2",
weight: "400",
style: "italic",
},
{
path: "../../../public/fonts/AtypText-Medium-subset.woff2",
weight: "500",
style: "normal",
},
{
path: "../../../public/fonts/AtypText-Medium.woff2",
weight: "500",
style: "normal",
},
{
path: "../../../public/fonts/AtypText-MediumItalic.woff2",
weight: "500",
style: "italic",
},
{
path: "../../../public/fonts/AtypText-Semibold-subset.woff2",
weight: "600",
style: "normal",
},
{
path: "../../../public/fonts/AtypText-Semibold.woff2",
weight: "600",
style: "normal",
},
{
path: "../../../public/fonts/AtypText-SemiboldItalic.woff2",
weight: "600",
style: "italic",
},
{
path: "../../../public/fonts/AtypText-Bold-subset.woff2",
weight: "700",
style: "normal",
},
{
path: "../../../public/fonts/AtypText-Bold.woff2",
weight: "700",
style: "normal",
},
{
path: "../../../public/fonts/AtypText-BoldItalic.woff2",
weight: "700",
style: "italic",
},
],
variable: "--font-atyp-text",
});

View File

@@ -1,20 +1,17 @@
"use client";
import { useDynamicContext, useIsLoggedIn } from "@/lib/dynamic";
import { useDynamicContext } from "@/lib/dynamic";
import { useAccount } from "wagmi";
import Bridge from "../form";
import TransactionHistory from "../transaction-history";
import { supportedChainIds } from "@/lib/wagmi";
import { useTokens } from "@/hooks";
import { useChainStore, FormStoreProvider, FormState, useNativeBridgeNavigationStore } from "@/stores";
import { ChainLayer } from "@/types";
import WrongNetwork from "../wrong-network";
import BridgeSkeleton from "./skeleton";
export default function BridgeLayout() {
const isTransactionHistoryOpen = useNativeBridgeNavigationStore.useIsTransactionHistoryOpen();
const { chain, address } = useAccount();
const isLoggedIn = useIsLoggedIn();
const { address } = useAccount();
const { sdkHasLoaded } = useDynamicContext();
const tokens = useTokens();
const fromChain = useChainStore.useFromChain();
@@ -23,10 +20,6 @@ export default function BridgeLayout() {
return <BridgeSkeleton />;
}
if (isLoggedIn && (!chain?.id || !supportedChainIds.includes(chain.id))) {
return <WrongNetwork />;
}
if (isTransactionHistoryOpen) {
return <TransactionHistory />;
}

View File

@@ -11,7 +11,6 @@
column-gap: 0.5rem;
justify-content: space-between;
align-items: center;
cursor: pointer;
font-size: 0.875rem;
}

View File

@@ -12,12 +12,12 @@ export default function BridgeMode() {
return (
<div className={styles.container}>
<button type="button" className={styles.button}>
<div className={styles.button}>
<div className={styles["selected-label"]}>
<Image src={logoSrc} width={16} height={16} alt="{label}" />
<Image src={logoSrc} width={16} height={16} alt={label} />
<span>{label}</span>
</div>
</button>
</div>
</div>
);
}

View File

@@ -55,7 +55,7 @@ export default function CurrencyDropdown({ disabled }: Props) {
{isOpen && (
<div ref={dropdownRef} className={styles.dropdown}>
{supportedCurrencies.map((option) => (
<div key={option.value} onClick={() => handleSelect(option)} className={styles.option}>
<div key={`currency-${option.value}`} onClick={() => handleSelect(option)} className={styles.option}>
<span className={styles.flag}>{option.flag}</span>
{option.label}
</div>

View File

@@ -44,7 +44,7 @@ export function DestinationAddress() {
<div className={styles["destination-address"]}>
<div className={styles["headline"]}>
<p className={styles.title}>Send to wallet</p>
{address !== inputValue && !error && (
{address !== inputValue && !error && isAddress(inputValue) && (
<Link
href={`${toChain.blockExplorers?.default.url ?? ""}/address/${inputValue}`}
target="_blank"

View File

@@ -1,8 +1,8 @@
import Modal from "@/components/modal";
import styles from "./manual-claim.module.scss";
import Link from "next/link";
import Button from "@/components/ui/button";
import { useNativeBridgeNavigationStore } from "@/stores";
type Props = {
isModalOpen: boolean;
@@ -10,12 +10,24 @@ type Props = {
};
export default function ManualClaim({ isModalOpen, onCloseModal }: Props) {
const setIsTransactionHistoryOpen = useNativeBridgeNavigationStore.useSetIsTransactionHistoryOpen();
return (
<Modal title="Manual claim on destination" isOpen={isModalOpen} onClose={onCloseModal}>
<div className={styles["modal-inner"]}>
<p className={styles["text"]}>
You will need to claim your transaction on the destination chain with an additional transaction that requires
ETH on the destination chain. This can be done on the <Link href="/transactions">Transaction page</Link>.
ETH on the destination chain. This can be done on the{" "}
<Button
variant="link"
onClick={() => {
setIsTransactionHistoryOpen(true);
onCloseModal();
}}
>
Transaction page
</Button>
.
</p>
<Button fullWidth onClick={onCloseModal}>
OK

View File

@@ -11,7 +11,8 @@
text-align: center;
line-height: 1.4;
a {
button {
font-size: 1rem;
color: var(--v2-color-indigo);
text-decoration: none;
}

View File

@@ -42,7 +42,7 @@ export default function SelectNetwork({ isModalOpen, onCloseModal, onClick, netw
filteredNetworks.map((network, index: number) => {
return (
<NetworkDetails
key={index}
key={`select-network-${index}`}
name={network.name}
onClickNetwork={() => {
onClick(network);

View File

@@ -23,7 +23,7 @@ export default function ListTransaction({ transactions }: Props) {
<>
<ul className={styles["list"]}>
{transactions.map((item, index) => (
<Transaction key={`${item.bridgingTx}-${index}`} onClick={handleClickTransaction} {...item} />
<Transaction key={`transaction-${item.bridgingTx}-${index}`} onClick={handleClickTransaction} {...item} />
))}
</ul>
<TransactionDetails

View File

@@ -4,11 +4,11 @@ import styles from "./skeleton-loader.module.scss";
export default function SkeletonLoader() {
return (
<div className={styles.container}>
{Array.from({ length: 3 }).map((_, i) => (
<div key={i} className={styles.group}>
{Array.from({ length: 3 }).map((_, index) => (
<div key={`skeleton-item-${index}`} className={styles.group}>
<div className={styles["skeleton-item"]}>
{Array.from({ length: 2 }).map((_, i) => (
<div key={i} className={styles["skeleton-group"]}>
<div key={`skeleton-group-${i}`} className={styles["skeleton-group"]}>
<div className={clsx(styles.skeleton, "pulsating")} />
<div className={clsx(styles.skeleton, "pulsating")} />
</div>

View File

@@ -244,6 +244,22 @@ export const MENUS = [
url: "https://linea.build/assets",
external: true,
},
{
__id: "01wv6FiyW8WmBwVSiELs0L",
__typename: "link",
name: "Privacy Policy",
label: "Privacy Policy",
url: "https://consensys.io/privacy-notice",
external: true,
},
{
__id: "01wv6FiyW8WmBwVSiELs0L",
__typename: "link",
name: "Terms of service",
label: "Terms of service",
url: "https://linea.build/terms-of-service",
external: true,
},
],
submenusRight: {
__id: "SQOTqw3aK6kNo3oUGGh7W",

View File

@@ -25,7 +25,7 @@ export const DesktopNavigation = ({ menus, theme = Theme.default }: Props) => {
<nav className={styles["nav-wrapper"]}>
<ul className={`${styles.navigation} ${styles[theme]}`}>
{menus.map((menu, index) => (
<MenuItem key={index} menu={filterMobileOnly(menu)} />
<MenuItem key={`menu-item-${index}`} menu={filterMobileOnly(menu)} />
))}
<li>
<HeaderConnect />
@@ -87,8 +87,8 @@ function MenuItem({ menu }: MenuItemProps) {
{menu.submenusLeft && (
<ul className={styles.submenu}>
{menu.submenusLeft.map((submenu, key) => (
<li className={styles.submenuItem} key={key}>
{menu.submenusLeft.map((submenu, index) => (
<li className={styles.submenuItem} key={`${menu.name}-submenuleft-{${index}`}>
<Link href={submenu.url as string} target={submenu.external ? "_blank" : "_self"}>
{submenu.label}
{submenu.external && (
@@ -102,7 +102,7 @@ function MenuItem({ menu }: MenuItemProps) {
{menu.submenusRight && (
<ul className={styles.right}>
{menu.submenusRight?.submenusLeft?.map((submenu, subIndex) => (
<li className={styles.submenuItem} key={subIndex}>
<li className={styles.submenuItem} key={`${menu.name}-submenuright-submenuleft-${subIndex}`}>
<Link
href={submenu.url as string}
target={submenu.external ? "_blank" : "_self"}

View File

@@ -81,7 +81,7 @@ export const MobileNavigation = ({ menus, theme = "default" }: Props) => {
{menus.map((menu, index) => (
<li
className={clsx(styles.menuItem, { [styles.active]: activeMenu === index })}
key={index}
key={`mobile-menu-item-${index}`}
onClick={() => handleToggleMenu(menu, index)}
>
{menu.url ? (
@@ -107,7 +107,7 @@ export const MobileNavigation = ({ menus, theme = "default" }: Props) => {
{menu.submenusLeft.map((submenu, subIndex) => (
<li
className={styles.submenuItem}
key={subIndex}
key={`mobile-submenuleft-item-${subIndex}`}
onClick={(e) => {
e.stopPropagation();
handleToggleMenu(submenu, -1);
@@ -128,7 +128,10 @@ export const MobileNavigation = ({ menus, theme = "default" }: Props) => {
{menu.submenusRight && (
<ul className={`${styles.submenu} ${styles.right}`}>
{menu.submenusRight?.submenusLeft?.map((submenu, subIndex) => (
<li className={styles.submenuItem} key={subIndex}>
<li
className={styles.submenuItem}
key={`mobile-submenuright-submenuleft-item-${subIndex}`}
>
<Link
href={submenu.url as string}
target={submenu.external ? "_blank" : "_self"}

View File

@@ -6,9 +6,13 @@ import NavItem from "./item";
const NavData = [
{
title: "Native Bridge",
title: "All Bridges",
href: "/",
},
{
title: "Native Bridge",
href: "/native-bridge",
},
];
export default function InternalNav() {
@@ -18,7 +22,7 @@ export default function InternalNav() {
<div className={styles["wrapper"]}>
<div className={styles["list-nav"]}>
{NavData.map((item, index) => (
<NavItem key={index} {...item} active={pathnane === item.href} />
<NavItem key={`internal-nav-item-${index}`} {...item} active={pathnane === item.href} />
))}
</div>
</div>

View File

@@ -45,8 +45,9 @@ export function Layout({ children }: { children: React.ReactNode }) {
src={"/images/illustration/illustration-mobile.svg"}
role="presentation"
alt="illustration mobile"
width={428}
height={359}
width={0}
height={0}
style={{ width: "100%", height: "auto", objectFit: "cover" }}
priority
/>
</div>
@@ -69,6 +70,7 @@ export function Layout({ children }: { children: React.ReactNode }) {
alt="illustration left"
width={300}
height={445}
priority
/>
<Image
className="right-illustration"
@@ -77,14 +79,17 @@ export function Layout({ children }: { children: React.ReactNode }) {
alt="illustration right"
width={610}
height={842}
priority
/>
<Image
className={clsx("mobile-illustration", { hidden: pathname === "/faq" })}
src={"/images/illustration/illustration-mobile.svg"}
role="presentation"
alt="illustration mobile"
width={428}
height={359}
width={0}
height={0}
style={{ width: "100%", height: "auto", objectFit: "cover" }}
priority
/>
</div>
</div>

View File

@@ -1,4 +1,5 @@
import { Web3Provider } from "@/contexts/web3.context";
import { QueryProvider } from "@/contexts/query.context";
import { TokenStoreProvider } from "@/stores";
import { getTokenConfig } from "@/services/tokenService";
@@ -16,8 +17,10 @@ export async function Providers({ children }: ProvidersProps) {
const tokensStoreInitialState = await getTokenStoreInitialState();
return (
<Web3Provider>
<TokenStoreProvider initialState={tokensStoreInitialState}>{children}</TokenStoreProvider>
</Web3Provider>
<QueryProvider>
<Web3Provider>
<TokenStoreProvider initialState={tokensStoreInitialState}>{children}</TokenStoreProvider>
</Web3Provider>
</QueryProvider>
);
}

View File

@@ -0,0 +1,11 @@
import { type PropsWithChildren } from "react";
import { useHydrated } from "@/hooks";
interface ClientOnlyProps extends PropsWithChildren {
fallback?: React.ReactNode;
}
export function ClientOnly({ children, fallback = null }: ClientOnlyProps) {
const hydrated = useHydrated();
return hydrated ? children : fallback;
}

View File

@@ -0,0 +1,171 @@
"use client";
import { useDynamicContext } from "@dynamic-labs/sdk-react-core";
import { ChainId, LiFiWidget, WidgetSkeleton, type WidgetConfig } from "@/lib/lifi";
import { ClientOnly } from "../client-only";
import atypTextFont from "@/assets/fonts/atypText";
import { CHAINS_RPC_URLS } from "@/constants";
import { config } from "@/config";
const widgetConfig: Partial<WidgetConfig> = {
variant: "compact",
subvariant: "default",
appearance: "light",
integrator: "Linea",
theme: {
palette: {
primary: {
main: "#6119ef",
},
secondary: {
main: "#6119ef",
},
background: {
default: "#ffffff",
paper: "#f8f7f2",
},
text: {
primary: "#121212",
secondary: "#525252",
},
grey: {
200: "#f5f5f5",
300: "#f1f1f1",
700: "#525252",
800: "#222222",
},
},
shape: {
borderRadius: 10,
borderRadiusSecondary: 30,
borderRadiusTertiary: 24,
},
typography: {
fontFamily: atypTextFont.style.fontFamily,
body1: {
fontSize: "0.875rem",
},
body2: {
fontSize: "0.875rem",
},
},
container: {
borderRadius: "0.625rem",
maxHeight: "80vh",
maxWidth: "29.25rem",
minWidth: "none",
fontSize: "0.875rem",
},
components: {
MuiButton: {
styleOverrides: {
root: {
fontSize: "0.875rem",
},
},
},
MuiIconButton: {
styleOverrides: {
root: {
fontSize: "0.875rem",
},
},
},
MuiAppBar: {
styleOverrides: {
root: {
display: "flex",
fontSize: "0.875rem",
justifyContent: "flex-end",
["p"]: {
visibility: "hidden",
fontSize: "0.875rem",
},
["p:before"]: {
content: '""',
visibility: "visible",
},
},
},
},
MuiCard: {
styleOverrides: {
root: {
fontSize: "0.875rem",
},
},
defaultProps: {
variant: "elevation",
},
},
MuiInputCard: "",
},
},
hiddenUI: ["appearance", "language"],
sdkConfig: {
rpcUrls: {
[ChainId.ETH]: [CHAINS_RPC_URLS[ChainId.ETH]],
[ChainId.LNA]: [CHAINS_RPC_URLS[ChainId.LNA]],
[ChainId.ARB]: [CHAINS_RPC_URLS[ChainId.ARB]],
[ChainId.AVA]: [CHAINS_RPC_URLS[ChainId.AVA]],
[ChainId.BAS]: [CHAINS_RPC_URLS[ChainId.BAS]],
[ChainId.BLS]: [CHAINS_RPC_URLS[ChainId.BLS]],
[ChainId.BSC]: [CHAINS_RPC_URLS[ChainId.BSC]],
[ChainId.CEL]: [CHAINS_RPC_URLS[ChainId.CEL]],
[ChainId.MNT]: [CHAINS_RPC_URLS[ChainId.MNT]],
[ChainId.OPT]: [CHAINS_RPC_URLS[ChainId.OPT]],
[ChainId.POL]: [CHAINS_RPC_URLS[ChainId.POL]],
[ChainId.SCL]: [CHAINS_RPC_URLS[ChainId.SCL]],
[ChainId.ERA]: [CHAINS_RPC_URLS[ChainId.ERA]],
},
},
chains: {
deny: [
ChainId.BTC,
ChainId.SOL,
ChainId.PZE,
ChainId.MOR,
ChainId.FUS,
ChainId.BOB,
ChainId.MAM,
ChainId.LSK,
ChainId.UNI,
ChainId.IMX,
ChainId.GRA,
ChainId.TAI,
ChainId.SOE,
ChainId.FRA,
ChainId.ABS,
ChainId.RSK,
ChainId.WCC,
ChainId.BER,
ChainId.KAI,
],
},
bridges: {
allow: ["stargateV2", "stargateV2Bus", "across", "hop", "squid", "relay"],
},
apiKey: config.lifiApiKey,
};
export function Widget() {
const { setShowAuthFlow } = useDynamicContext();
return (
<div>
<ClientOnly fallback={<WidgetSkeleton config={widgetConfig} />}>
<LiFiWidget
config={{
...widgetConfig,
walletConfig: {
onConnect() {
setShowAuthFlow(true);
},
},
}}
integrator="linea"
/>
</ClientOnly>
</div>
);
}

View File

@@ -1,10 +1,9 @@
import { useState } from "react";
import Link from "next/link";
import UnionIcon from "@/assets/icons/union.svg";
import LeftIllustration from "./illustration/left.svg";
import RightIllustration from "./illustration/right.svg";
import CloseIcon from "@/assets/icons/x-circle.svg";
import styles from "./top-banner.module.scss";
import Image from "next/image";
type Props = {
text: string;
@@ -22,7 +21,16 @@ export default function TopBanner({ text, href }: Props) {
return (
<div className={styles["banner-wrapper"]}>
<LeftIllustration className={styles["left-illustration"]} />
<Image
className={styles["left-illustration"]}
src={"/images/illustration/banner/left.svg"}
role="presentation"
alt="banner illustration left"
width={0}
height={0}
style={{ width: "56px", height: "100%" }}
priority
/>
<div className={styles["banner"]}>
<Link href={href} target="_blank" rel="noopener noreferrer" className={styles["inner"]} passHref>
<span>{text}</span>
@@ -30,7 +38,16 @@ export default function TopBanner({ text, href }: Props) {
</Link>
</div>
<CloseIcon onClick={handleClose} className={styles["close-icon"]} />
<RightIllustration className={styles["right-illustration"]} />
<Image
className={styles["right-illustration"]}
src={"/images/illustration/banner/right.svg"}
role="presentation"
alt="banner illustration right"
width={0}
height={0}
style={{ width: "221px", height: "100%" }}
priority
/>
</div>
);
}

View File

@@ -23,12 +23,19 @@ const chainConfigSchema = z.object({
export const configSchema = z
.object({
chains: z.record(z.string().regex(/^\d+$/), chainConfigSchema),
walletConnectId: z.string(),
walletConnectId: z.string().nonempty(),
storage: z.object({
minVersion: z.number().positive().int(),
}),
// Feature toggle for CCTPV2 for USDC transfers
isCctpEnabled: z.boolean(),
infuraApiKey: z.string().nonempty(),
dynamicEnvironmentId: z.string().nonempty(),
lifiApiKey: z.string().nonempty(),
tokenListUrls: z.object({
mainnet: z.string().trim().url(),
sepolia: z.string().trim().url(),
}),
})
.strict();

View File

@@ -66,6 +66,13 @@ export const config: Config = {
minVersion: process.env.NEXT_PUBLIC_STORAGE_MIN_VERSION ? parseInt(process.env.NEXT_PUBLIC_STORAGE_MIN_VERSION) : 1,
},
isCctpEnabled: process.env.NEXT_PUBLIC_IS_CCTP_ENABLED === "true",
infuraApiKey: process.env.NEXT_PUBLIC_INFURA_ID ?? "",
dynamicEnvironmentId: process.env.NEXT_PUBLIC_DYNAMIC_ENVIRONMENT_ID ?? "",
lifiApiKey: process.env.NEXT_PUBLIC_LIFI_API_KEY ?? "",
tokenListUrls: {
mainnet: process.env.NEXT_PUBLIC_MAINNET_TOKEN_LIST ?? "",
sepolia: process.env.NEXT_PUBLIC_SEPOLIA_TOKEN_LIST ?? "",
},
};
export async function getConfiguration(): Promise<Config> {

View File

@@ -0,0 +1,8 @@
// Current fast transfer route fee for mainnet
export const CCTP_TRANSFER_MAX_FEE_FALLBACK = 100n;
// 1000 Fast transfer, 2000 Standard transfer
export const CCTP_MIN_FINALITY_THRESHOLD = 1000;
// https://developers.circle.com/stablecoins/message-format, add 2 for '0x' prefix
export const CCTP_V2_MESSAGE_HEADER_LENGTH = 298;
export const CCTP_V2_EXPIRATION_BLOCK_OFFSET = 2 + 344 * 2;
export const CCTP_V2_EXPIRATION_BLOCK_LENGTH = 64;

View File

@@ -0,0 +1,85 @@
import { config } from "@/config";
import {
arbitrum,
aurora,
avalanche,
base,
blast,
bsc,
celo,
cronos,
fantom,
gnosis,
ink,
linea,
lineaSepolia,
mainnet,
mantle,
mode,
moonbeam,
optimism,
polygon,
scroll,
sei,
sepolia,
sonic,
zksync,
} from "viem/chains";
export const CHAINS = [
mainnet,
sepolia,
linea,
lineaSepolia,
arbitrum,
aurora,
avalanche,
base,
blast,
bsc,
celo,
cronos,
fantom,
gnosis,
ink,
mantle,
mode,
moonbeam,
optimism,
polygon,
scroll,
sei,
sonic,
zksync,
] as const;
export const CHAINS_IDS = CHAINS.map((chain) => chain.id);
export const CHAINS_RPC_URLS: Record<(typeof CHAINS_IDS)[number], string> = {
[mainnet.id]: `https://mainnet.infura.io/v3/${config.infuraApiKey}`,
[sepolia.id]: `https://sepolia.infura.io/v3/${config.infuraApiKey}`,
[linea.id]: `https://linea-mainnet.infura.io/v3/${config.infuraApiKey}`,
[lineaSepolia.id]: `https://linea-sepolia.infura.io/v3/${config.infuraApiKey}`,
[arbitrum.id]: `https://arbitrum-mainnet.infura.io/v3/${config.infuraApiKey}`,
[aurora.id]: `https://mainnet.aurora.dev`,
[avalanche.id]: `https://avalanche-mainnet.infura.io/v3/${config.infuraApiKey}`,
[base.id]: `https://base-mainnet.infura.io/v3/${config.infuraApiKey}`,
[blast.id]: `https://blast-mainnet.infura.io/v3/${config.infuraApiKey}`,
[bsc.id]: `https://bsc-mainnet.infura.io/v3/${config.infuraApiKey}`,
[celo.id]: `https://celo-mainnet.infura.io/v3/${config.infuraApiKey}`,
[cronos.id]: `https://evm.cronos.org`,
[fantom.id]: `https://rpc.ankr.com/fantom`,
[gnosis.id]: `https://rpc.gnosischain.com`,
[ink.id]: `https://rpc-gel.inkonchain.com`,
[mantle.id]: `https://mantle-mainnet.infura.io/v3/${config.infuraApiKey}`,
[mode.id]: `https://mainnet.mode.network`,
[moonbeam.id]: `https://rpc.testnet.moonbeam.network`,
[optimism.id]: `https://optimism-mainnet.infura.io/v3/${config.infuraApiKey}`,
[polygon.id]: `https://polygon-mainnet.infura.io/v3/${config.infuraApiKey}`,
[scroll.id]: `https://scroll-mainnet.infura.io/v3/${config.infuraApiKey}`,
[sei.id]: `https://evm-rpc.sei-apis.com`,
[sonic.id]: `https://rpc.soniclabs.com`,
[zksync.id]: `https://zksync-mainnet.infura.io/v3/${config.infuraApiKey}`,
};
export const NATIVE_BRIDGE_SUPPORTED_CHAIN_IDS = [mainnet.id, linea.id, lineaSepolia.id, sepolia.id] as const;

View File

@@ -0,0 +1,4 @@
export * from "./chains";
export * from "./tokens";
export * from "./message";
export * from "./cctp";

View File

@@ -0,0 +1 @@
export const INBOX_L1L2_MESSAGE_STATUS_MAPPING_SLOT = 176n;

View File

@@ -0,0 +1 @@
export const USDC_SYMBOL = "USDC";

View File

@@ -0,0 +1,14 @@
"use client";
import { ReactNode } from "react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
type Web3ProviderProps = {
children: ReactNode;
};
const queryClient = new QueryClient();
export function QueryProvider({ children }: Web3ProviderProps) {
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
}

View File

@@ -1,12 +1,10 @@
"use client";
import { DynamicWagmiConnector, EthereumWalletConnectors, DynamicContextProvider } from "@/lib/dynamic";
import { ReactNode } from "react";
import { config } from "@/lib/wagmi";
import { WagmiProvider } from "wagmi";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
const queryClient = new QueryClient();
import { DynamicWagmiConnector, EthereumWalletConnectors, DynamicContextProvider } from "@/lib/dynamic";
import { config as wagmiConfig } from "@/lib/wagmi";
import { config } from "@/config";
type Web3ProviderProps = {
children: ReactNode;
@@ -65,17 +63,15 @@ export function Web3Provider({ children }: Web3ProviderProps) {
return (
<DynamicContextProvider
settings={{
environmentId: process.env.NEXT_PUBLIC_DYNAMIC_ENVIRONMENT_ID!,
environmentId: config.dynamicEnvironmentId,
walletConnectors: [EthereumWalletConnectors],
mobileExperience: "redirect",
appName: "Linea Bridge",
cssOverrides,
}}
>
<WagmiProvider config={config}>
<QueryClientProvider client={queryClient}>
<DynamicWagmiConnector>{children}</DynamicWagmiConnector>
</QueryClientProvider>
<WagmiProvider config={wagmiConfig}>
<DynamicWagmiConnector>{children}</DynamicWagmiConnector>
</WagmiProvider>
</DynamicContextProvider>
);

View File

@@ -1,7 +1,7 @@
import { useEstimateFeesPerGas, useWatchBlockNumber } from "wagmi";
import { SupportedChainId } from "@/lib/wagmi";
import { SupportedChainIds } from "@/types";
const useFeeData = (chainId: SupportedChainId) => {
const useFeeData = (chainId: SupportedChainIds) => {
const { data, refetch } = useEstimateFeesPerGas({ chainId, type: "eip1559" });
useWatchBlockNumber({

View File

@@ -16,3 +16,4 @@ export { default as useTokenBalance } from "./useTokenBalance";
export { default as useTokenPrices } from "./useTokenPrices";
export { default as useTokens } from "./useTokens";
export { default as useTransactionHistory } from "./useTransactionHistory";
export { default as useHydrated } from "./useHydrated";

View File

@@ -3,7 +3,7 @@
import { useChainStore } from "@/stores";
import { getCctpFee } from "@/services/cctp";
import { useQuery } from "@tanstack/react-query";
import { CCTP_TRANSFER_MAX_FEE_FALLBACK } from "@/utils/cctp";
import { CCTP_TRANSFER_MAX_FEE_FALLBACK } from "@/constants";
const useCctpSrcDomain = () => {
const fromChain = useChainStore.useFromChain();

View File

@@ -4,7 +4,7 @@ import { encodeFunctionData, padHex, zeroHash } from "viem";
import { useFormStore, useChainStore } from "@/stores";
import { isCctp } from "@/utils/tokens";
import { useCctpFee, useCctpDestinationDomain } from "./useCctpUtilHooks";
import { CCTP_MIN_FINALITY_THRESHOLD } from "@/utils";
import { CCTP_MIN_FINALITY_THRESHOLD } from "@/constants";
type UseDepositForBurnTxArgs = {
allowance?: bigint;

View File

@@ -0,0 +1,15 @@
import { useSyncExternalStore } from "react";
function subscribe() {
return () => {};
}
const useHydrated = () => {
return useSyncExternalStore(
subscribe,
() => true,
() => false,
);
};
export default useHydrated;

View File

@@ -1,7 +1,9 @@
import { useMemo } from "react";
import { LineaSDK, Network } from "@consensys/linea-sdk";
import { linea, lineaSepolia, mainnet, sepolia } from "viem/chains";
import { L1MessageServiceContract, L2MessageServiceContract } from "@consensys/linea-sdk/dist/lib/contracts";
import { useChainStore } from "@/stores";
import { CHAINS_RPC_URLS } from "@/constants";
export interface LineaSDKContracts {
L1: L1MessageServiceContract;
@@ -15,11 +17,11 @@ const useLineaSDK = () => {
let l1RpcUrl;
let l2RpcUrl;
if (fromChain.testnet) {
l1RpcUrl = `https://sepolia.infura.io/v3/${process.env.NEXT_PUBLIC_INFURA_ID}`;
l2RpcUrl = `https://linea-sepolia.infura.io/v3/${process.env.NEXT_PUBLIC_INFURA_ID}`;
l1RpcUrl = CHAINS_RPC_URLS[sepolia.id];
l2RpcUrl = CHAINS_RPC_URLS[lineaSepolia.id];
} else {
l1RpcUrl = `https://mainnet.infura.io/v3/${process.env.NEXT_PUBLIC_INFURA_ID}`;
l2RpcUrl = `https://linea-mainnet.infura.io/v3/${process.env.NEXT_PUBLIC_INFURA_ID}`;
l1RpcUrl = CHAINS_RPC_URLS[mainnet.id];
l2RpcUrl = CHAINS_RPC_URLS[linea.id];
}
const sdk = new LineaSDK({

View File

@@ -2,7 +2,7 @@ import { useMemo } from "react";
import { useChainStore, useTokenStore } from "@/stores";
import { ChainLayer, Token } from "@/types";
import { config } from "@/config";
import { USDC_SYMBOL } from "@/utils";
import { USDC_SYMBOL } from "@/constants";
const useTokens = (): Token[] => {
const tokensList = useTokenStore((state) => state.tokensList);

View File

@@ -0,0 +1,3 @@
"use client";
export * from "@lifi/widget";

View File

@@ -1,22 +1,26 @@
import { http, createConfig } from "wagmi";
import { linea, lineaSepolia, mainnet, sepolia } from "wagmi/chains";
export const chains = [mainnet, linea, lineaSepolia, sepolia] as const;
export const supportedChainIds = [mainnet.id, linea.id, lineaSepolia.id, sepolia.id] as const;
export type SupportedChainId = (typeof supportedChainIds)[number];
import { CHAINS, CHAINS_IDS, CHAINS_RPC_URLS } from "@/constants";
export const config = createConfig({
chains,
chains: CHAINS,
multiInjectedProviderDiscovery: false,
transports: {
[mainnet.id]: http(`https://mainnet.infura.io/v3/${process.env.NEXT_PUBLIC_INFURA_ID}`, { batch: true }),
[sepolia.id]: http(`https://sepolia.infura.io/v3/${process.env.NEXT_PUBLIC_INFURA_ID}`, { batch: true }),
[linea.id]: http(`https://linea-mainnet.infura.io/v3/${process.env.NEXT_PUBLIC_INFURA_ID}`, { batch: true }),
[lineaSepolia.id]: http(`https://linea-sepolia.infura.io/v3/${process.env.NEXT_PUBLIC_INFURA_ID}`, { batch: true }),
},
transports: generateWagmiTransports(CHAINS_IDS),
});
function generateWagmiTransports(chainIds: (typeof CHAINS_IDS)[number][]) {
return chainIds.reduce(
(acc, chainId) => {
acc[chainId] = generateWagmiTransport(chainId);
return acc;
},
{} as Record<(typeof chainIds)[number], ReturnType<typeof generateWagmiTransport>>,
);
}
function generateWagmiTransport(chainId: (typeof CHAINS_IDS)[number]) {
return http(CHAINS_RPC_URLS[chainId], { batch: true });
}
declare module "wagmi" {
interface Register {
config: typeof config;

View File

@@ -12,9 +12,9 @@ enum NetworkTypes {
export async function getTokens(networkTypes: NetworkTypes): Promise<GithubTokenListToken[]> {
try {
// Fetch the JSON data from the URL.
let url = process.env.MAINNET_TOKEN_LIST ? (process.env.MAINNET_TOKEN_LIST as string) : "";
let url = config.tokenListUrls.mainnet;
if (networkTypes === NetworkTypes.SEPOLIA) {
url = process.env.SEPOLIA_TOKEN_LIST ? (process.env.SEPOLIA_TOKEN_LIST as string) : "";
url = config.tokenListUrls.sepolia;
}
const response = await fetch(url);

View File

@@ -1,13 +1,15 @@
import { SupportedChainId } from "@/lib/wagmi";
import { NATIVE_BRIDGE_SUPPORTED_CHAIN_IDS } from "@/constants";
import { Address } from "viem";
export type SupportedChainIds = (typeof NATIVE_BRIDGE_SUPPORTED_CHAIN_IDS)[number];
export enum ChainLayer {
L1 = "L1",
L2 = "L2",
}
export type Chain = {
id: SupportedChainId;
id: SupportedChainIds;
name: string;
iconPath: string;
nativeCurrency: { name: string; symbol: string; decimals: number };

View File

@@ -1,5 +1,5 @@
export { type LinkBlock, type AssetType, Theme } from "./ui";
export { type Chain, ChainLayer } from "./chain";
export { type Chain, ChainLayer, type SupportedChainIds } from "./chain";
export { type TransactionType, TransactionStatus } from "./transaction";
export { type Token, type GithubTokenListToken, type NetworkTokens } from "./token";
export { BridgeProvider } from "./providers";

View File

@@ -4,15 +4,11 @@ import { GetPublicClientReturnType } from "@wagmi/core";
import { fetchCctpAttestationByTxHash, reattestCctpV2PreFinalityMessage } from "@/services/cctp";
import { getPublicClient } from "@wagmi/core";
import { config as wagmiConfig } from "@/lib/wagmi";
// Current fast transfer route fee for mainnet
export const CCTP_TRANSFER_MAX_FEE_FALLBACK = 100n;
// 1000 Fast transfer, 2000 Standard transfer
export const CCTP_MIN_FINALITY_THRESHOLD = 1000;
// https://developers.circle.com/stablecoins/message-format, add 2 for '0x' prefix
const CCTP_V2_MESSAGE_HEADER_LENGTH = 298;
const CCTP_V2_EXPIRATION_BLOCK_OFFSET = 2 + 344 * 2;
const CCTP_V2_EXPIRATION_BLOCK_LENGTH = 64;
import {
CCTP_V2_EXPIRATION_BLOCK_LENGTH,
CCTP_V2_EXPIRATION_BLOCK_OFFSET,
CCTP_V2_MESSAGE_HEADER_LENGTH,
} from "@/constants";
const isCctpNonceUsed = async (
client: GetPublicClientReturnType,

View File

@@ -1,17 +1,15 @@
import { Address } from "viem";
import { linea, mainnet, Chain as ViemChain, sepolia, lineaSepolia } from "viem/chains";
import { SupportedChainId } from "@/lib/wagmi";
import { config } from "@/config";
import { Chain, ChainLayer } from "@/types";
import { Chain, ChainLayer, SupportedChainIds } from "@/types";
export const generateChain = (chain: ViemChain): Chain => {
return {
id: chain.id as SupportedChainId,
id: chain.id as SupportedChainIds,
name: chain.id !== lineaSepolia.id ? chain.name : "Linea Sepolia",
iconPath: config.chains[chain.id].iconPath,
nativeCurrency: chain.nativeCurrency,
blockExplorers: chain.blockExplorers,
// Possibly the wrong assumption to fallback to 'false', but fallback to 'true' makes the app crash mysteriously
testnet: Boolean(chain.testnet),
layer: getChainNetworkLayer(chain.id),
messageServiceAddress: config.chains[chain.id].messageServiceAddress as Address,

View File

@@ -1,10 +1,9 @@
import { BridgeTransaction } from "@/types";
import { SupportedChainId } from "@/lib/wagmi";
import { BridgeTransaction, SupportedChainIds } from "@/types";
export function getCompleteTxStoreKeyForTx(transaction: BridgeTransaction): string {
return getCompleteTxStoreKey(transaction.fromChain.id, transaction.bridgingTx);
}
export function getCompleteTxStoreKey(fromChainId: SupportedChainId, bridgingTxHash: string): string {
export function getCompleteTxStoreKey(fromChainId: SupportedChainIds, bridgingTxHash: string): string {
return `${fromChainId}-${bridgingTxHash}`;
}

View File

@@ -4,11 +4,6 @@ export { estimateEthGasFee, estimateERC20GasFee } from "./fees";
export { formatAddress, formatBalance, formatHex, formatTimestamp, safeGetAddress } from "./format";
export { fetchTransactionsHistory } from "./history";
export { computeMessageHash, computeMessageStorageSlot, isCctpV2BridgeMessage, isNativeBridgeMessage } from "./message";
export { isEth, isCctp, USDC_SYMBOL } from "./tokens";
export { isEth, isCctp } from "./tokens";
export { isEmptyObject } from "./utils";
export {
CCTP_TRANSFER_MAX_FEE_FALLBACK,
CCTP_MIN_FINALITY_THRESHOLD,
getCctpTransactionStatus,
getCctpMessageByTxHash,
} from "./cctp";
export { getCctpTransactionStatus, getCctpMessageByTxHash } from "./cctp";

View File

@@ -1,7 +1,6 @@
import { keccak256, encodeAbiParameters, Address } from "viem";
import { CctpV2BridgeMessage, NativeBridgeMessage } from "@/types";
const INBOX_L1L2_MESSAGE_STATUS_MAPPING_SLOT = 176n;
import { INBOX_L1L2_MESSAGE_STATUS_MAPPING_SLOT } from "@/constants";
export function computeMessageHash(
from: Address,

View File

@@ -19,5 +19,3 @@ export const isCctp = (token: Token) => {
isAddress(token.L2)
);
};
export const USDC_SYMBOL = "USDC";

3556
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff