Feat: Add Solana support in the bridge UI (#836)

* feat: add solona support in the LiFi widget

* fix: update dockerfile and update bridge ui version

* fix: handle cases where user is connected to non EVM network in native bridge

* fix: bridge ui e2e tests issue
This commit is contained in:
Victorien Gauch
2025-04-04 14:12:30 +02:00
committed by GitHub
parent cca1681026
commit 1d4deeaf19
18 changed files with 1250 additions and 400 deletions

View File

@@ -55,6 +55,7 @@ jobs:
env:
NEXT_PUBLIC_WALLET_CONNECT_ID: ${{ secrets.PUBLIC_WALLET_CONNECT_ID }}
NEXT_PUBLIC_INFURA_ID: ${{ secrets.PUBLIC_BRIDGE_UI_INFURA_ID }}
NEXT_PUBLIC_ALCHEMY_ID: ${{ secrets.PUBLIC_BRIDGE_UI_ALCHEMY_ID }}
NEXT_PUBLIC_DYNAMIC_ENVIRONMENT_ID: ${{ secrets.PUBLIC_DYNAMIC_SANDBOX_ENVIRONMENT_ID }}
NEXT_PUBLIC_LIFI_API_KEY: ${{ secrets.PUBLIC_LIFI_API_KEY }}
NEXT_PUBLIC_ONRAMPER_API_KEY: ${{ secrets.PUBLIC_ONRAMPER_API_KEY }}
@@ -80,6 +81,7 @@ jobs:
NEXT_PUBLIC_LIFI_API_KEY: "placeholder"
NEXT_PUBLIC_WALLET_CONNECT_ID: "placeholder"
NEXT_PUBLIC_DYNAMIC_ENVIRONMENT_ID: "placeholder"
NEXT_PUBLIC_ALCHEMY_ID: "placeholder"
# Prerequisite - Testing wallet must have >0 ETH and USDC on Sepolia
- name: Run E2E tests

View File

@@ -60,6 +60,7 @@ jobs:
ENV_FILE=./bridge-ui/.env.production
NEXT_PUBLIC_WALLET_CONNECT_ID=${{ env.NEXT_PUBLIC_WALLET_CONNECT_ID }}
NEXT_PUBLIC_INFURA_ID=${{ env.NEXT_PUBLIC_INFURA_ID }}
NEXT_PUBLIC_ALCHEMY_ID=${{ env.NEXT_PUBLIC_ALCHEMY_ID }}
NEXT_PUBLIC_DYNAMIC_ENVIRONMENT_ID=${{ env.NEXT_PUBLIC_DYNAMIC_ENVIRONMENT_ID }}
NEXT_PUBLIC_LIFI_API_KEY=${{ env.NEXT_PUBLIC_LIFI_API_KEY }}
NEXT_PUBLIC_ONRAMPER_API_KEY=${{ env.NEXT_PUBLIC_ONRAMPER_API_KEY }}
@@ -68,6 +69,7 @@ jobs:
env:
NEXT_PUBLIC_WALLET_CONNECT_ID: ${{ secrets.PUBLIC_WALLET_CONNECT_ID }}
NEXT_PUBLIC_INFURA_ID: ${{ secrets.PUBLIC_BRIDGE_UI_INFURA_ID }}
NEXT_PUBLIC_ALCHEMY_ID: ${{ secrets.PUBLIC_BRIDGE_UI_ALCHEMY_ID }}
NEXT_PUBLIC_DYNAMIC_ENVIRONMENT_ID: ${{ secrets.PUBLIC_DYNAMIC_ENVIRONMENT_ID }}
NEXT_PUBLIC_LIFI_API_KEY: ${{ secrets.PUBLIC_LIFI_API_KEY }}
NEXT_PUBLIC_ONRAMPER_API_KEY: ${{ secrets.PUBLIC_ONRAMPER_API_KEY }}

View File

@@ -8,11 +8,14 @@ FROM base AS builder
ARG NEXT_PUBLIC_WALLET_CONNECT_ID
ARG NEXT_PUBLIC_INFURA_ID
ARG NEXT_PUBLIC_ALCHEMY_ID
ARG NEXT_PUBLIC_DYNAMIC_ENVIRONMENT_ID
ARG NEXT_PUBLIC_LIFI_API_KEY
ARG NEXT_PUBLIC_ONRAMPER_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_ALCHEMY_ID=$NEXT_PUBLIC_ALCHEMY_ID
ENV NEXT_PUBLIC_DYNAMIC_ENVIRONMENT_ID=$NEXT_PUBLIC_DYNAMIC_ENVIRONMENT_ID
ENV NEXT_PUBLIC_LIFI_API_KEY=$NEXT_PUBLIC_LIFI_API_KEY
ENV NEXT_PUBLIC_ONRAMPER_API_KEY=$NEXT_PUBLIC_ONRAMPER_API_KEY

View File

@@ -1,3 +1,12 @@
<a name="v2.3.0"></a>
# [v2.3.0] - 04 Apr 2025
# Feat: Add Solana support in the LiFi widget
Description:
- Add Solana support in the LiFi widget
<a name="v2.2.0"></a>
# [v2.2.0] - 31 Mar 2025

View File

@@ -1,6 +1,6 @@
{
"name": "bridge-ui",
"version": "2.2.0",
"version": "2.3.0",
"private": true,
"type": "module",
"scripts": {
@@ -24,13 +24,17 @@
},
"dependencies": {
"@consensys/linea-sdk": "0.3.0",
"@dynamic-labs/ethereum": "4.9.11",
"@dynamic-labs/sdk-react-core": "4.9.11",
"@dynamic-labs/wagmi-connector": "4.9.11",
"@dynamic-labs/ethereum": "4.10.2",
"@dynamic-labs/sdk-react-core": "4.10.2",
"@dynamic-labs/solana": "4.10.2",
"@dynamic-labs/wagmi-connector": "4.10.2",
"@headlessui/react": "2.1.9",
"@lifi/widget": "3.18.1",
"@tanstack/react-query": "5.69.0",
"@wagmi/connectors": "5.7.11",
"@lifi/widget": "3.18.2",
"@solana/wallet-adapter-base": "0.9.24",
"@solana/wallet-adapter-react": "0.15.36",
"@solana/web3.js": "1.98.0",
"@tanstack/react-query": "5.71.5",
"@wagmi/connectors": "5.7.12",
"@wagmi/core": "2.16.7",
"auto-zustand-selectors-hook": "3.0.1",
"clsx": "2.1.1",
@@ -45,8 +49,8 @@
"react-icons": "5.5.0",
"sass": "1.86.0",
"sharp": "0.33.5",
"viem": "2.23.13",
"wagmi": "2.14.15",
"viem": "2.25.0",
"wagmi": "2.14.16",
"zod": "3.24.2",
"zustand": "4.5.4"
},

View File

@@ -5,14 +5,19 @@ import Bridge from "../form";
import TransactionHistory from "../transaction-history";
import { useNativeBridgeNavigationStore } from "@/stores";
import BridgeSkeleton from "./skeleton";
import WrongNetwork from "../wrong-network";
export default function BridgeLayout() {
const isTransactionHistoryOpen = useNativeBridgeNavigationStore.useIsTransactionHistoryOpen();
const { sdkHasLoaded } = useDynamicContext();
const { sdkHasLoaded, primaryWallet } = useDynamicContext();
if (!sdkHasLoaded) {
return <BridgeSkeleton />;
}
if (primaryWallet && primaryWallet.connector.connectedChain !== "EVM") {
return <WrongNetwork />;
}
return isTransactionHistoryOpen ? <TransactionHistory /> : <Bridge />;
}

View File

@@ -3,14 +3,16 @@ import TransactionCircleIcon from "@/assets/icons/transaction-circle.svg";
export default function WrongNetwork() {
return (
<div className={styles["content"]}>
<span className={styles["icon"]}>
<TransactionCircleIcon />
</span>
<p className={styles["title"]}>Please switch network.</p>
<p className={styles["description"]}>
The native bridge only supports the following networks: Ethereum, Sepolia, Linea Sepolia and Linea mainnet.
</p>
<div className={styles["wrong-network-wrapper"]}>
<div className={styles["content"]}>
<span className={styles["icon"]}>
<TransactionCircleIcon />
</span>
<p className={styles["title"]}>Please switch network.</p>
<p className={styles["description"]}>
This bridge doesn&apos;t work with Solana. Please switch to the Ethereum or Linea network.
</p>
</div>
</div>
);
}

View File

@@ -1,3 +1,9 @@
.wrong-network-wrapper {
background-color: var(--v2-color-white);
border-radius: 0.625rem;
padding: 1.5rem;
}
.content {
padding: 1.5rem;
background-color: var(--v2-color-white);

View File

@@ -1,7 +1,7 @@
"use client";
import { zeroAddress } from "viem";
import { useDynamicContext } from "@dynamic-labs/sdk-react-core";
import { useDynamicContext, useIsLoggedIn } from "@/lib/dynamic";
import { ChainId, LiFiWidget, WidgetSkeleton, type WidgetConfig } from "@/lib/lifi";
import { ClientOnly } from "../client-only";
import atypTextFont from "@/assets/fonts/atypText";
@@ -125,6 +125,7 @@ const widgetConfig: Partial<WidgetConfig> = {
hiddenUI: ["appearance", "language"],
sdkConfig: {
rpcUrls: {
[ChainId.SOL]: [CHAINS_RPC_URLS[ChainId.SOL]],
[ChainId.ETH]: [CHAINS_RPC_URLS[ChainId.ETH]],
[ChainId.LNA]: [CHAINS_RPC_URLS[ChainId.LNA]],
[ChainId.ARB]: [CHAINS_RPC_URLS[ChainId.ARB]],
@@ -143,7 +144,6 @@ const widgetConfig: Partial<WidgetConfig> = {
chains: {
deny: [
ChainId.BTC,
ChainId.SOL,
ChainId.PZE,
ChainId.MOR,
ChainId.FUS,
@@ -170,7 +170,8 @@ const widgetConfig: Partial<WidgetConfig> = {
};
export function Widget() {
const { setShowAuthFlow } = useDynamicContext();
const { setShowAuthFlow, setShowDynamicUserProfile } = useDynamicContext();
const isLoggedIn = useIsLoggedIn();
return (
<div>
@@ -180,7 +181,7 @@ export function Widget() {
...widgetConfig,
walletConfig: {
onConnect() {
setShowAuthFlow(true);
isLoggedIn ? setShowDynamicUserProfile(true) : setShowAuthFlow(true);
},
},
}}

View File

@@ -30,6 +30,7 @@ export const configSchema = z
// Feature toggle for CCTPV2 for USDC transfers
isCctpEnabled: z.boolean(),
infuraApiKey: z.string().nonempty(),
alchemyApiKey: z.string().nonempty(),
dynamicEnvironmentId: z.string().nonempty(),
lifiApiKey: z.string().nonempty(),
onRamperApiKey: z.string().nonempty(),

View File

@@ -67,6 +67,7 @@ export const config: Config = {
},
isCctpEnabled: process.env.NEXT_PUBLIC_IS_CCTP_ENABLED === "true",
infuraApiKey: process.env.NEXT_PUBLIC_INFURA_ID ?? "",
alchemyApiKey: process.env.NEXT_PUBLIC_ALCHEMY_ID ?? "",
dynamicEnvironmentId: process.env.NEXT_PUBLIC_DYNAMIC_ENVIRONMENT_ID ?? "",
lifiApiKey: process.env.NEXT_PUBLIC_LIFI_API_KEY ?? "",
onRamperApiKey: process.env.NEXT_PUBLIC_ONRAMPER_API_KEY ?? "",

View File

@@ -53,7 +53,9 @@ export const CHAINS = [
zksync,
] as const;
export const CHAINS_IDS = CHAINS.map((chain) => chain.id);
const SOLANA_CHAIN = 1151111081099710 as const;
export const CHAINS_IDS = [...CHAINS.map((chain) => chain.id), SOLANA_CHAIN];
export const CHAINS_RPC_URLS: Record<(typeof CHAINS_IDS)[number], string> = {
[mainnet.id]: `https://mainnet.infura.io/v3/${config.infuraApiKey}`,
@@ -80,6 +82,7 @@ export const CHAINS_RPC_URLS: Record<(typeof CHAINS_IDS)[number], string> = {
[sei.id]: `https://evm-rpc.sei-apis.com`,
[sonic.id]: `https://rpc.soniclabs.com`,
[zksync.id]: `https://zksync-mainnet.infura.io/v3/${config.infuraApiKey}`,
[SOLANA_CHAIN]: `https://solana-mainnet.g.alchemy.com/v2/${config.alchemyApiKey}`,
};
export const NATIVE_BRIDGE_SUPPORTED_CHAIN_IDS = [mainnet.id, linea.id, lineaSepolia.id, sepolia.id] as const;

View File

@@ -0,0 +1,49 @@
"use client";
import { type PropsWithChildren, useCallback, useEffect, useMemo } from "react";
import { type Wallet, useDynamicContext, useDynamicEvents, type SolanaWalletConnector } from "@/lib/dynamic";
import { useWallet } from "@/lib/solana";
const getSolanaConnector = (wallet: Wallet | null): SolanaWalletConnector | undefined => {
if (wallet?.connector.connectedChain === "SOL") {
return wallet.connector as SolanaWalletConnector;
}
};
type DynamicSolanaProviderProps = PropsWithChildren;
export function DynamicSolanaProvider({ children }: DynamicSolanaProviderProps) {
const { disconnect, select, wallets } = useWallet();
const { primaryWallet } = useDynamicContext();
useDynamicEvents("logout", () => {
disconnect();
});
useEffect(() => {
if (primaryWallet?.connector.connectedChain !== "SOL") {
disconnect();
}
}, [primaryWallet?.connector.connectedChain, disconnect]);
// eslint-disable-next-line react-hooks/exhaustive-deps
const solanaWallet = useMemo(() => getSolanaConnector(primaryWallet), [primaryWallet?.connector.connectedChain]);
const handleConnectedSolanaWallet = useCallback(async () => {
if (!solanaWallet) {
return;
}
const wallet = wallets.find((wallet) => wallet.adapter.name === solanaWallet.name);
if (wallet) {
select(wallet.adapter.name);
}
}, [solanaWallet, wallets, select]);
useEffect(() => {
handleConnectedSolanaWallet();
}, [handleConnectedSolanaWallet]);
return children;
}

View File

@@ -0,0 +1,21 @@
"use client";
import type { PropsWithChildren } from "react";
import { type Adapter, WalletAdapterNetwork, ConnectionProvider, WalletProvider, clusterApiUrl } from "@/lib/solana";
import { DynamicSolanaProvider } from "./dynamic-solana-provider";
const endpoint = clusterApiUrl(WalletAdapterNetwork.Mainnet);
const wallets: Adapter[] = [];
type SolanaWalletProviderProps = PropsWithChildren;
export function SolanaWalletProvider({ children }: SolanaWalletProviderProps) {
return (
<ConnectionProvider endpoint={endpoint}>
<WalletProvider wallets={wallets} autoConnect>
<DynamicSolanaProvider>{children}</DynamicSolanaProvider>
</WalletProvider>
</ConnectionProvider>
);
}

View File

@@ -1,14 +1,18 @@
"use client";
import { ReactNode } from "react";
import { PropsWithChildren } from "react";
import { WagmiProvider } from "wagmi";
import { DynamicWagmiConnector, EthereumWalletConnectors, DynamicContextProvider } from "@/lib/dynamic";
import {
DynamicWagmiConnector,
EthereumWalletConnectors,
DynamicContextProvider,
SolanaWalletConnectors,
} from "@/lib/dynamic";
import { config as wagmiConfig } from "@/lib/wagmi";
import { config } from "@/config";
import { SolanaWalletProvider } from "./solana-provider";
type Web3ProviderProps = {
children: ReactNode;
};
type Web3ProviderProps = PropsWithChildren;
export const cssOverrides = `
.connect-button {
@@ -64,15 +68,17 @@ export function Web3Provider({ children }: Web3ProviderProps) {
<DynamicContextProvider
settings={{
environmentId: config.dynamicEnvironmentId,
walletConnectors: [EthereumWalletConnectors],
walletConnectors: [EthereumWalletConnectors, SolanaWalletConnectors],
initialAuthenticationMode: "connect-only",
mobileExperience: "redirect",
appName: "Linea Bridge",
cssOverrides,
}}
>
<WagmiProvider config={wagmiConfig}>
<DynamicWagmiConnector>{children}</DynamicWagmiConnector>
<WagmiProvider config={wagmiConfig} reconnectOnMount={false}>
<DynamicWagmiConnector>
<SolanaWalletProvider>{children}</SolanaWalletProvider>
</DynamicWagmiConnector>
</WagmiProvider>
</DynamicContextProvider>
);

View File

@@ -3,3 +3,4 @@
export * from "@dynamic-labs/sdk-react-core";
export * from "@dynamic-labs/ethereum";
export * from "@dynamic-labs/wagmi-connector";
export * from "@dynamic-labs/solana";

View File

@@ -0,0 +1,5 @@
"use client";
export * from "@solana/wallet-adapter-base";
export * from "@solana/wallet-adapter-react";
export * from "@solana/web3.js";

1467
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff