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/**/**/**/**/*.bin binary
prover/prover-assets/kzgsrs/* 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_WALLET_CONNECT_ID: ${{ secrets.PUBLIC_WALLET_CONNECT_ID }}
NEXT_PUBLIC_INFURA_ID: ${{ secrets.PUBLIC_BRIDGE_UI_INFURA_ID }} NEXT_PUBLIC_INFURA_ID: ${{ secrets.PUBLIC_BRIDGE_UI_INFURA_ID }}
NEXT_PUBLIC_DYNAMIC_ENVIRONMENT_ID: ${{ secrets.PUBLIC_DYNAMIC_ENVIRONMENT_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 - name: Install linux dependencies
run: | run: |

View File

@@ -56,18 +56,19 @@ jobs:
file: ./bridge-ui/Dockerfile file: ./bridge-ui/Dockerfile
push: ${{ env.DOCKERHUB_USERNAME != '' && env.DOCKERHUB_TOKEN != '' }} push: ${{ env.DOCKERHUB_USERNAME != '' && env.DOCKERHUB_TOKEN != '' }}
tags: consensys/linea-bridge-ui:${{ env.DOCKER_TAG }} tags: consensys/linea-bridge-ui:${{ env.DOCKER_TAG }}
# Env file dedicated for dev
build-args: | build-args: |
ENV_FILE=./bridge-ui/.env.production ENV_FILE=./bridge-ui/.env.production
NEXT_PUBLIC_WALLET_CONNECT_ID=${{ secrets.PUBLIC_WALLET_CONNECT_ID }} NEXT_PUBLIC_WALLET_CONNECT_ID=${{ env.NEXT_PUBLIC_WALLET_CONNECT_ID }}
NEXT_PUBLIC_INFURA_ID=${{ secrets.PUBLIC_BRIDGE_UI_INFURA_ID }} NEXT_PUBLIC_INFURA_ID=${{ env.NEXT_PUBLIC_INFURA_ID }}
NEXT_PUBLIC_DYNAMIC_ENVIRONMENT_ID=${{ secrets.PUBLIC_DYNAMIC_ENVIRONMENT_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-from: type=registry,ref=consensys/linea-bridge-ui:buildcache
cache-to: type=registry,ref=consensys/linea-bridge-ui:buildcache,mode=max cache-to: type=registry,ref=consensys/linea-bridge-ui:buildcache,mode=max
env: env:
NEXT_PUBLIC_WALLET_CONNECT_ID: ${{ secrets.PUBLIC_WALLET_CONNECT_ID }} NEXT_PUBLIC_WALLET_CONNECT_ID: ${{ secrets.PUBLIC_WALLET_CONNECT_ID }}
NEXT_PUBLIC_INFURA_ID: ${{ secrets.PUBLIC_BRIDGE_UI_INFURA_ID }} NEXT_PUBLIC_INFURA_ID: ${{ secrets.PUBLIC_BRIDGE_UI_INFURA_ID }}
NEXT_PUBLIC_DYNAMIC_ENVIRONMENT_ID: ${{ secrets.PUBLIC_DYNAMIC_ENVIRONMENT_ID }} NEXT_PUBLIC_DYNAMIC_ENVIRONMENT_ID: ${{ secrets.PUBLIC_DYNAMIC_ENVIRONMENT_ID }}
NEXT_PUBLIC_LIFI_API_KEY: ${{ secrets.PUBLIC_LIFI_API_KEY }}
test-build: test-build:
if: github.event.pull_request.head.repo.fork == true 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_GAS_ESTIMATED=100000
NEXT_PUBLIC_MAINNET_DEFAULT_GAS_LIMIT_SURPLUS=6000 NEXT_PUBLIC_MAINNET_DEFAULT_GAS_LIMIT_SURPLUS=6000
NEXT_PUBLIC_MAINNET_PROFIT_MARGIN=2 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_L1_TOKEN_BRIDGE=0x5A0a48389BB0f12E5e017116c1105da97E129142
NEXT_PUBLIC_SEPOLIA_LINEA_TOKEN_BRIDGE=0x93DcAdf238932e6e6a85852caC89cBd71798F463 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_GAS_ESTIMATED=100000
NEXT_PUBLIC_SEPOLIA_DEFAULT_GAS_LIMIT_SURPLUS=6000 NEXT_PUBLIC_SEPOLIA_DEFAULT_GAS_LIMIT_SURPLUS=6000
NEXT_PUBLIC_SEPOLIA_PROFIT_MARGIN=2 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_WALLET_CONNECT_ID=<GITHUB_SECRET>
NEXT_PUBLIC_DYNAMIC_ENVIRONMENT_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_GAS_ESTIMATED=100000
NEXT_PUBLIC_MAINNET_DEFAULT_GAS_LIMIT_SURPLUS=6000 NEXT_PUBLIC_MAINNET_DEFAULT_GAS_LIMIT_SURPLUS=6000
NEXT_PUBLIC_MAINNET_PROFIT_MARGIN=2 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_L1_TOKEN_BRIDGE=0x5A0a48389BB0f12E5e017116c1105da97E129142
NEXT_PUBLIC_SEPOLIA_LINEA_TOKEN_BRIDGE=0x93DcAdf238932e6e6a85852caC89cBd71798F463 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_GAS_ESTIMATED=100000
NEXT_PUBLIC_SEPOLIA_DEFAULT_GAS_LIMIT_SURPLUS=6000 NEXT_PUBLIC_SEPOLIA_DEFAULT_GAS_LIMIT_SURPLUS=6000
NEXT_PUBLIC_SEPOLIA_PROFIT_MARGIN=2 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_WALLET_CONNECT_ID=<YOUR__WALLET_CONNECT_ID>
NEXT_PUBLIC_DYNAMIC_ENVIRONMENT_ID=<YOUR_DYNAMIC_ENVIRONMENT_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_WALLET_CONNECT_ID
ARG NEXT_PUBLIC_INFURA_ID ARG NEXT_PUBLIC_INFURA_ID
ARG NEXT_PUBLIC_DYNAMIC_ENVIRONMENT_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_WALLET_CONNECT_ID=$NEXT_PUBLIC_WALLET_CONNECT_ID
ENV NEXT_PUBLIC_INFURA_ID=$NEXT_PUBLIC_INFURA_ID ENV NEXT_PUBLIC_INFURA_ID=$NEXT_PUBLIC_INFURA_ID
ENV NEXT_PUBLIC_DYNAMIC_ENVIRONMENT_ID=$NEXT_PUBLIC_DYNAMIC_ENVIRONMENT_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 ARG ENV_FILE
WORKDIR /app WORKDIR /app

View File

@@ -21,13 +21,14 @@
}, },
"dependencies": { "dependencies": {
"@consensys/linea-sdk": "0.3.0", "@consensys/linea-sdk": "0.3.0",
"@dynamic-labs/ethereum": "4.6.3", "@dynamic-labs/ethereum": "4.9.5",
"@dynamic-labs/sdk-react-core": "4.6.3", "@dynamic-labs/sdk-react-core": "4.9.5",
"@dynamic-labs/wagmi-connector": "4.6.3", "@dynamic-labs/wagmi-connector": "4.9.5",
"@headlessui/react": "2.1.9", "@headlessui/react": "2.1.9",
"@tanstack/react-query": "5.62.16", "@lifi/widget": "3.18.1",
"@wagmi/connectors": "5.1.15", "@tanstack/react-query": "5.69.0",
"@wagmi/core": "2.16.3", "@wagmi/connectors": "5.7.11",
"@wagmi/core": "2.16.7",
"auto-zustand-selectors-hook": "3.0.1", "auto-zustand-selectors-hook": "3.0.1",
"clsx": "2.1.1", "clsx": "2.1.1",
"date-fns": "4.1.0", "date-fns": "4.1.0",
@@ -35,14 +36,14 @@
"loglevel": "1.9.2", "loglevel": "1.9.2",
"next": "14.2.24", "next": "14.2.24",
"next-seo": "6.6.0", "next-seo": "6.6.0",
"pino-pretty": "11.2.2", "pino-pretty": "13.0.0",
"react": "18.3.1", "react": "18.3.1",
"react-dom": "18.3.1", "react-dom": "18.3.1",
"react-icons": "5.3.0", "react-icons": "5.5.0",
"sass": "1.83.3", "sass": "1.86.0",
"sharp": "0.33.5", "sharp": "0.33.5",
"viem": "2.22.4", "viem": "2.23.13",
"wagmi": "2.14.6", "wagmi": "2.14.15",
"zod": "3.24.2", "zod": "3.24.2",
"zustand": "4.5.4" "zustand": "4.5.4"
}, },
@@ -53,9 +54,9 @@
"@types/fs-extra": "11.0.4", "@types/fs-extra": "11.0.4",
"@types/react": "18.3.11", "@types/react": "18.3.11",
"@types/react-dom": "18.3.0", "@types/react-dom": "18.3.0",
"autoprefixer": "10.4.20", "autoprefixer": "10.4.21",
"dotenv": "16.4.5", "dotenv": "16.4.7",
"eslint-config-next": "14.2.15", "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"]}> <ul className={styles["list"]}>
{faqList.map((faq, index) => ( {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> </ul>
</div> </div>

View File

@@ -21,11 +21,11 @@ const metadata: Metadata = {
export default function RootLayout({ children }: { children: React.ReactNode }) { export default function RootLayout({ children }: { children: React.ReactNode }) {
return ( return (
<html lang="en" data-theme="v2"> <html lang="en" data-theme="v2" className={clsx(atypFont.variable, atypTextFont.variable)}>
<title>{metadata.title?.toString()}</title> <title>{metadata.title?.toString()}</title>
<meta name="description" content={metadata.description?.toString()} key="desc" /> <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 }} /> <noscript dangerouslySetInnerHTML={{ __html: gtmNoScript }} />
<Providers> <Providers>
@@ -45,8 +45,8 @@ export default function RootLayout({ children }: { children: React.ReactNode })
</svg> </svg>
</body> </body>
<Script id="usabilla" dangerouslySetInnerHTML={{ __html: usabillaBeScript }} /> <Script id="usabilla" dangerouslySetInnerHTML={{ __html: usabillaBeScript }} strategy="lazyOnload" />
<Script id="gtm" dangerouslySetInnerHTML={{ __html: gtmScript }} /> <Script id="gtm" dangerouslySetInnerHTML={{ __html: gtmScript }} strategy="lazyOnload" />
</html> </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"; "use client";
import InternalNav from "@/components/internal-nav"; import InternalNav from "@/components/internal-nav";
import BridgeLayout from "@/components/bridge/bridge-layout";
import styles from "./page.module.scss"; 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 ( return (
<> <section className={styles["content-wrapper"]}>
<TopBanner <InternalNav />
text="Bridging USDC (USDC.e) is temporarily disabled until March 26. Learn more in our announcement here." <Widget />
href="https://x.com/LineaBuild/status/1901347758230958528" </section>
/>
<section className={styles["content-wrapper"]}>
<InternalNav />
<BridgeLayout />
</section>
</>
); );
} }

View File

@@ -3,81 +3,26 @@ import localFont from "next/font/local";
const atypFont = localFont({ const atypFont = localFont({
display: "swap", display: "swap",
src: [ 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", path: "../../../public/fonts/AtypDisplay-Regular-subset.woff2",
weight: "400", weight: "400",
style: "normal", 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", path: "../../../public/fonts/AtypDisplay-Medium-subset.woff2",
weight: "500", weight: "500",
style: "normal", 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", path: "../../../public/fonts/AtypDisplay-Semibold-subset.woff2",
weight: "600", weight: "600",
style: "normal", 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", path: "../../../public/fonts/AtypDisplay-Bold-subset.woff2",
weight: "700", weight: "700",
style: "normal", 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", variable: "--font-atyp",
}); });

View File

@@ -3,86 +3,26 @@ import localFont from "next/font/local";
const atypTextFont = localFont({ const atypTextFont = localFont({
display: "swap", display: "swap",
src: [ 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", path: "../../../public/fonts/AtypText-Regular-subset.woff2",
weight: "400", weight: "400",
style: "normal", 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", path: "../../../public/fonts/AtypText-Medium-subset.woff2",
weight: "500", weight: "500",
style: "normal", 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", path: "../../../public/fonts/AtypText-Semibold-subset.woff2",
weight: "600", weight: "600",
style: "normal", 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", path: "../../../public/fonts/AtypText-Bold-subset.woff2",
weight: "700", weight: "700",
style: "normal", 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", variable: "--font-atyp-text",
}); });

View File

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

View File

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

View File

@@ -12,12 +12,12 @@ export default function BridgeMode() {
return ( return (
<div className={styles.container}> <div className={styles.container}>
<button type="button" className={styles.button}> <div className={styles.button}>
<div className={styles["selected-label"]}> <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> <span>{label}</span>
</div> </div>
</button> </div>
</div> </div>
); );
} }

View File

@@ -55,7 +55,7 @@ export default function CurrencyDropdown({ disabled }: Props) {
{isOpen && ( {isOpen && (
<div ref={dropdownRef} className={styles.dropdown}> <div ref={dropdownRef} className={styles.dropdown}>
{supportedCurrencies.map((option) => ( {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> <span className={styles.flag}>{option.flag}</span>
{option.label} {option.label}
</div> </div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -244,6 +244,22 @@ export const MENUS = [
url: "https://linea.build/assets", url: "https://linea.build/assets",
external: true, 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: { submenusRight: {
__id: "SQOTqw3aK6kNo3oUGGh7W", __id: "SQOTqw3aK6kNo3oUGGh7W",

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,5 @@
import { Web3Provider } from "@/contexts/web3.context"; import { Web3Provider } from "@/contexts/web3.context";
import { QueryProvider } from "@/contexts/query.context";
import { TokenStoreProvider } from "@/stores"; import { TokenStoreProvider } from "@/stores";
import { getTokenConfig } from "@/services/tokenService"; import { getTokenConfig } from "@/services/tokenService";
@@ -16,8 +17,10 @@ export async function Providers({ children }: ProvidersProps) {
const tokensStoreInitialState = await getTokenStoreInitialState(); const tokensStoreInitialState = await getTokenStoreInitialState();
return ( return (
<Web3Provider> <QueryProvider>
<TokenStoreProvider initialState={tokensStoreInitialState}>{children}</TokenStoreProvider> <Web3Provider>
</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 { useState } from "react";
import Link from "next/link"; import Link from "next/link";
import UnionIcon from "@/assets/icons/union.svg"; 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 CloseIcon from "@/assets/icons/x-circle.svg";
import styles from "./top-banner.module.scss"; import styles from "./top-banner.module.scss";
import Image from "next/image";
type Props = { type Props = {
text: string; text: string;
@@ -22,7 +21,16 @@ export default function TopBanner({ text, href }: Props) {
return ( return (
<div className={styles["banner-wrapper"]}> <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"]}> <div className={styles["banner"]}>
<Link href={href} target="_blank" rel="noopener noreferrer" className={styles["inner"]} passHref> <Link href={href} target="_blank" rel="noopener noreferrer" className={styles["inner"]} passHref>
<span>{text}</span> <span>{text}</span>
@@ -30,7 +38,16 @@ export default function TopBanner({ text, href }: Props) {
</Link> </Link>
</div> </div>
<CloseIcon onClick={handleClose} className={styles["close-icon"]} /> <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> </div>
); );
} }

View File

@@ -23,12 +23,19 @@ const chainConfigSchema = z.object({
export const configSchema = z export const configSchema = z
.object({ .object({
chains: z.record(z.string().regex(/^\d+$/), chainConfigSchema), chains: z.record(z.string().regex(/^\d+$/), chainConfigSchema),
walletConnectId: z.string(), walletConnectId: z.string().nonempty(),
storage: z.object({ storage: z.object({
minVersion: z.number().positive().int(), minVersion: z.number().positive().int(),
}), }),
// Feature toggle for CCTPV2 for USDC transfers // Feature toggle for CCTPV2 for USDC transfers
isCctpEnabled: z.boolean(), 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(); .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, 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", 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> { 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"; "use client";
import { DynamicWagmiConnector, EthereumWalletConnectors, DynamicContextProvider } from "@/lib/dynamic";
import { ReactNode } from "react"; import { ReactNode } from "react";
import { config } from "@/lib/wagmi";
import { WagmiProvider } from "wagmi"; import { WagmiProvider } from "wagmi";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { DynamicWagmiConnector, EthereumWalletConnectors, DynamicContextProvider } from "@/lib/dynamic";
import { config as wagmiConfig } from "@/lib/wagmi";
const queryClient = new QueryClient(); import { config } from "@/config";
type Web3ProviderProps = { type Web3ProviderProps = {
children: ReactNode; children: ReactNode;
@@ -65,17 +63,15 @@ export function Web3Provider({ children }: Web3ProviderProps) {
return ( return (
<DynamicContextProvider <DynamicContextProvider
settings={{ settings={{
environmentId: process.env.NEXT_PUBLIC_DYNAMIC_ENVIRONMENT_ID!, environmentId: config.dynamicEnvironmentId,
walletConnectors: [EthereumWalletConnectors], walletConnectors: [EthereumWalletConnectors],
mobileExperience: "redirect", mobileExperience: "redirect",
appName: "Linea Bridge", appName: "Linea Bridge",
cssOverrides, cssOverrides,
}} }}
> >
<WagmiProvider config={config}> <WagmiProvider config={wagmiConfig}>
<QueryClientProvider client={queryClient}> <DynamicWagmiConnector>{children}</DynamicWagmiConnector>
<DynamicWagmiConnector>{children}</DynamicWagmiConnector>
</QueryClientProvider>
</WagmiProvider> </WagmiProvider>
</DynamicContextProvider> </DynamicContextProvider>
); );

View File

@@ -1,7 +1,7 @@
import { useEstimateFeesPerGas, useWatchBlockNumber } from "wagmi"; 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" }); const { data, refetch } = useEstimateFeesPerGas({ chainId, type: "eip1559" });
useWatchBlockNumber({ useWatchBlockNumber({

View File

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

View File

@@ -3,7 +3,7 @@
import { useChainStore } from "@/stores"; import { useChainStore } from "@/stores";
import { getCctpFee } from "@/services/cctp"; import { getCctpFee } from "@/services/cctp";
import { useQuery } from "@tanstack/react-query"; 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 useCctpSrcDomain = () => {
const fromChain = useChainStore.useFromChain(); const fromChain = useChainStore.useFromChain();

View File

@@ -4,7 +4,7 @@ import { encodeFunctionData, padHex, zeroHash } from "viem";
import { useFormStore, useChainStore } from "@/stores"; import { useFormStore, useChainStore } from "@/stores";
import { isCctp } from "@/utils/tokens"; import { isCctp } from "@/utils/tokens";
import { useCctpFee, useCctpDestinationDomain } from "./useCctpUtilHooks"; import { useCctpFee, useCctpDestinationDomain } from "./useCctpUtilHooks";
import { CCTP_MIN_FINALITY_THRESHOLD } from "@/utils"; import { CCTP_MIN_FINALITY_THRESHOLD } from "@/constants";
type UseDepositForBurnTxArgs = { type UseDepositForBurnTxArgs = {
allowance?: bigint; 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 { useMemo } from "react";
import { LineaSDK, Network } from "@consensys/linea-sdk"; 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 { L1MessageServiceContract, L2MessageServiceContract } from "@consensys/linea-sdk/dist/lib/contracts";
import { useChainStore } from "@/stores"; import { useChainStore } from "@/stores";
import { CHAINS_RPC_URLS } from "@/constants";
export interface LineaSDKContracts { export interface LineaSDKContracts {
L1: L1MessageServiceContract; L1: L1MessageServiceContract;
@@ -15,11 +17,11 @@ const useLineaSDK = () => {
let l1RpcUrl; let l1RpcUrl;
let l2RpcUrl; let l2RpcUrl;
if (fromChain.testnet) { if (fromChain.testnet) {
l1RpcUrl = `https://sepolia.infura.io/v3/${process.env.NEXT_PUBLIC_INFURA_ID}`; l1RpcUrl = CHAINS_RPC_URLS[sepolia.id];
l2RpcUrl = `https://linea-sepolia.infura.io/v3/${process.env.NEXT_PUBLIC_INFURA_ID}`; l2RpcUrl = CHAINS_RPC_URLS[lineaSepolia.id];
} else { } else {
l1RpcUrl = `https://mainnet.infura.io/v3/${process.env.NEXT_PUBLIC_INFURA_ID}`; l1RpcUrl = CHAINS_RPC_URLS[mainnet.id];
l2RpcUrl = `https://linea-mainnet.infura.io/v3/${process.env.NEXT_PUBLIC_INFURA_ID}`; l2RpcUrl = CHAINS_RPC_URLS[linea.id];
} }
const sdk = new LineaSDK({ const sdk = new LineaSDK({

View File

@@ -2,7 +2,7 @@ import { useMemo } from "react";
import { useChainStore, useTokenStore } from "@/stores"; import { useChainStore, useTokenStore } from "@/stores";
import { ChainLayer, Token } from "@/types"; import { ChainLayer, Token } from "@/types";
import { config } from "@/config"; import { config } from "@/config";
import { USDC_SYMBOL } from "@/utils"; import { USDC_SYMBOL } from "@/constants";
const useTokens = (): Token[] => { const useTokens = (): Token[] => {
const tokensList = useTokenStore((state) => state.tokensList); 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 { http, createConfig } from "wagmi";
import { linea, lineaSepolia, mainnet, sepolia } from "wagmi/chains"; import { CHAINS, CHAINS_IDS, CHAINS_RPC_URLS } from "@/constants";
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];
export const config = createConfig({ export const config = createConfig({
chains, chains: CHAINS,
multiInjectedProviderDiscovery: false, multiInjectedProviderDiscovery: false,
transports: { transports: generateWagmiTransports(CHAINS_IDS),
[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 }),
},
}); });
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" { declare module "wagmi" {
interface Register { interface Register {
config: typeof config; config: typeof config;

View File

@@ -12,9 +12,9 @@ enum NetworkTypes {
export async function getTokens(networkTypes: NetworkTypes): Promise<GithubTokenListToken[]> { export async function getTokens(networkTypes: NetworkTypes): Promise<GithubTokenListToken[]> {
try { try {
// Fetch the JSON data from the URL. // 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) { 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); 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"; import { Address } from "viem";
export type SupportedChainIds = (typeof NATIVE_BRIDGE_SUPPORTED_CHAIN_IDS)[number];
export enum ChainLayer { export enum ChainLayer {
L1 = "L1", L1 = "L1",
L2 = "L2", L2 = "L2",
} }
export type Chain = { export type Chain = {
id: SupportedChainId; id: SupportedChainIds;
name: string; name: string;
iconPath: string; iconPath: string;
nativeCurrency: { name: string; symbol: string; decimals: number }; nativeCurrency: { name: string; symbol: string; decimals: number };

View File

@@ -1,5 +1,5 @@
export { type LinkBlock, type AssetType, Theme } from "./ui"; 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 TransactionType, TransactionStatus } from "./transaction";
export { type Token, type GithubTokenListToken, type NetworkTokens } from "./token"; export { type Token, type GithubTokenListToken, type NetworkTokens } from "./token";
export { BridgeProvider } from "./providers"; export { BridgeProvider } from "./providers";

View File

@@ -4,15 +4,11 @@ import { GetPublicClientReturnType } from "@wagmi/core";
import { fetchCctpAttestationByTxHash, reattestCctpV2PreFinalityMessage } from "@/services/cctp"; import { fetchCctpAttestationByTxHash, reattestCctpV2PreFinalityMessage } from "@/services/cctp";
import { getPublicClient } from "@wagmi/core"; import { getPublicClient } from "@wagmi/core";
import { config as wagmiConfig } from "@/lib/wagmi"; import { config as wagmiConfig } from "@/lib/wagmi";
import {
// Current fast transfer route fee for mainnet CCTP_V2_EXPIRATION_BLOCK_LENGTH,
export const CCTP_TRANSFER_MAX_FEE_FALLBACK = 100n; CCTP_V2_EXPIRATION_BLOCK_OFFSET,
// 1000 Fast transfer, 2000 Standard transfer CCTP_V2_MESSAGE_HEADER_LENGTH,
export const CCTP_MIN_FINALITY_THRESHOLD = 1000; } from "@/constants";
// 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;
const isCctpNonceUsed = async ( const isCctpNonceUsed = async (
client: GetPublicClientReturnType, client: GetPublicClientReturnType,

View File

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

View File

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

View File

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

View File

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

View File

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

3556
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff