Layerswap widget integration (#929)

* Add layerswap widget

* update @layerswap/widget version to 0.1.11

* fix: update network logo references from 'ethereum' to 'linea'

* update @layerswap/widget to 0.1.14

* Refactored evm wallet hook

* Renamed nav

* update the pnpm-lock.yml

* fix: lint code

---------

Co-authored-by: Aren <aren@bransfer.io>
Co-authored-by: Victorien Gauch <85494462+VGau@users.noreply.github.com>
Co-authored-by: VGau <victorien.gauch@consensys.net>
This commit is contained in:
Babken Mesropyan
2025-05-07 19:25:45 +04:00
committed by GitHub
parent 262a9f73ed
commit 570c7c75f6
8 changed files with 1853 additions and 70 deletions

View File

@@ -29,6 +29,7 @@
"@dynamic-labs/solana": "4.10.2",
"@dynamic-labs/wagmi-connector": "4.10.2",
"@headlessui/react": "2.1.9",
"@layerswap/widget": "^0.1.14",
"@lifi/widget": "3.18.2",
"@solana/wallet-adapter-base": "0.9.24",
"@solana/wallet-adapter-react": "0.15.36",

View File

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

View File

@@ -0,0 +1,10 @@
import styles from "./page.module.scss";
import { Widget } from "@/components/layerswap/widget";
export default function Home() {
return (
<section className={styles["content-wrapper"]}>
<Widget />
</section>
);
}

View File

@@ -9,6 +9,10 @@ const NavData = [
title: "All Bridges",
href: "/",
},
{
title: "CEX",
href: "/layerswap",
},
{
title: "Native Bridge",
href: "/native-bridge",

View File

@@ -0,0 +1,9 @@
"use client";
import { WalletHooksProvider } from "@layerswap/widget";
import useCustomEVM from "./useCustomEvm";
import { ReactNode } from "react";
export default function CustomHooks({ children }: { children: ReactNode }) {
const customEvm = useCustomEVM();
return <WalletHooksProvider overides={{ evm: customEvm }}>{children}</WalletHooksProvider>;
}

View File

@@ -0,0 +1,171 @@
import { useAccount, useSwitchAccount } from "wagmi";
import { useCallback, useEffect, useMemo } from "react";
import {
useUserWallets,
useDynamicContext,
dynamicEvents,
Wallet as DynamicWallet,
} from "@dynamic-labs/sdk-react-core";
import {
WalletProvider,
Wallet,
resolveWalletConnectorIcon,
useSettingsState,
NetworkWithTokens,
NetworkType,
} from "@layerswap/widget";
export default function useEVM(): WalletProvider {
const name = "EVM";
const id = "evm";
// wagmi
const { connectors: activeConnectors } = useSwitchAccount();
const { connector: activeConnector, address: activeAddress } = useAccount();
// Dynamic SDK
const { setShowAuthFlow, handleLogOut } = useDynamicContext();
const userWallets = useUserWallets();
// Layerswap settings
const { networks } = useSettingsState();
// Gather the EVMtype network names
const evmNetworkNames = useMemo(
() => networks.filter((n) => n.type === NetworkType.EVM).map((n) => n.name),
[networks],
);
// Supported-networks
const supportedNetworks = useMemo(
() => ({
asSource: evmNetworkNames,
autofill: evmNetworkNames,
withdrawal: evmNetworkNames,
}),
[evmNetworkNames],
);
// Clean up dynamicEvents listeners on unmount
useEffect(() => {
return () => {
dynamicEvents.removeAllListeners("walletAdded");
dynamicEvents.removeAllListeners("authFlowCancelled");
};
}, []);
// connectWallet: log out existing, show authFlow, wait for event, then resolve
const connectWallet = useCallback(async (): Promise<Wallet | undefined> => {
if (userWallets.length) {
await handleLogOut();
}
const newDynWallet = await new Promise<DynamicWallet>((resolve, reject) => {
setShowAuthFlow(true);
const onAdded = (w: DynamicWallet) => {
cleanup();
resolve(w);
};
const onCancelled = () => {
cleanup();
reject(new Error("User cancelled the connection"));
};
const cleanup = () => {
dynamicEvents.off("walletAdded", onAdded);
dynamicEvents.off("authFlowCancelled", onCancelled);
};
dynamicEvents.on("walletAdded", onAdded);
dynamicEvents.on("authFlowCancelled", onCancelled);
});
return resolveWallet({
connection: newDynWallet,
activeConnection:
activeConnector && activeAddress ? { id: activeConnector.id, address: activeAddress } : undefined,
networks,
supportedNetworks,
disconnect: handleLogOut,
providerName: name,
});
}, [userWallets, handleLogOut, setShowAuthFlow, activeConnector, activeAddress, networks, supportedNetworks]);
// Logout
const disconnectWallets = useCallback(async () => {
await handleLogOut();
}, [handleLogOut]);
// Map wagmi connectors → Dynamic SDK wallets → our Wallet shape
const connectedWallets: Wallet[] = useMemo(
() =>
activeConnectors
.map((conn) => {
const dyn = userWallets.find((w) => true);
if (!dyn) return;
return resolveWallet({
connection: dyn,
activeConnection:
activeConnector && activeAddress ? { id: activeConnector.id, address: activeAddress } : undefined,
networks,
supportedNetworks,
disconnect: disconnectWallets,
providerName: name,
});
})
.filter(Boolean) as Wallet[],
[activeConnectors, userWallets, activeConnector, activeAddress, networks, supportedNetworks, disconnectWallets],
);
const logo = networks.find((n) => n.name.toLowerCase().includes("linea"))?.logo;
return {
connectWallet,
connectConnector: connectWallet,
activeWallet: connectedWallets.find((w) => w.isActive),
connectedWallets,
asSourceSupportedNetworks: supportedNetworks.asSource,
autofillSupportedNetworks: supportedNetworks.autofill,
withdrawalSupportedNetworks: supportedNetworks.withdrawal,
name,
id,
providerIcon: logo,
};
}
/** Reusable helper to turn a DynamicWallet + context into our `Wallet` shape */
function resolveWallet(props: {
connection: DynamicWallet;
activeConnection?: { id: string; address: string };
networks: NetworkWithTokens[];
supportedNetworks: {
asSource: string[];
autofill: string[];
withdrawal: string[];
};
disconnect: () => Promise<void>;
providerName: string;
}): Wallet | undefined {
const { connection, activeConnection, networks, supportedNetworks, disconnect, providerName } = props;
const connectorName = connection.connector.name;
const address = connection.address;
if (!connectorName || !address) return;
const isActive = activeConnection?.address === address;
const displayName = `${connectorName} ${providerName}`;
const networkIcon = networks.find((n) => n.name.toLowerCase().includes("linea"))?.logo;
return {
id: connectorName,
isActive,
address,
addresses: [address],
displayName,
providerName,
icon: resolveWalletConnectorIcon({ connector: connectorName, address }),
disconnect: () => disconnect(),
asSourceSupportedNetworks: supportedNetworks.asSource,
autofillSupportedNetworks: supportedNetworks.autofill,
withdrawalSupportedNetworks: supportedNetworks.withdrawal,
networkIcon,
};
}

View File

@@ -0,0 +1,58 @@
import "@layerswap/widget/index.css";
import { Swap, LayerswapProvider, GetSettings, ThemeData } from "@layerswap/widget";
import CustomHooks from "./custom-hooks";
export async function Widget() {
const settings = await GetSettings();
return (
<LayerswapProvider integrator="linea" themeData={themeData} settings={settings} apiKey={"YOUR_API_KEY"}>
<CustomHooks>
<Swap
featuredNetwork={{
initialDirection: "to",
network: "LINEA_MAINNET",
oppositeDirectionOverrides: "onlyExchanges",
}}
/>
</CustomHooks>
</LayerswapProvider>
);
}
// TODO: Implement theme colors
const themeData: ThemeData = {
placeholderText: "134, 134, 134",
actionButtonText: "255, 255, 255",
buttonTextColor: "17, 17, 17",
logo: "255, 0, 147",
borderRadius: "large",
primary: {
DEFAULT: "97, 26, 239",
"50": "215, 198, 251",
"100": "202, 179, 250",
"200": "176, 140, 247",
"300": "150, 102, 244",
"400": "123, 64, 242",
"500": "97, 26, 239",
"600": "74, 14, 195",
"700": "54, 10, 143",
"800": "34, 6, 90",
"900": "14, 3, 38",
text: "18, 18, 18",
textMuted: "86, 97, 123",
},
secondary: {
DEFAULT: "248, 247, 241",
"50": "49, 60, 155",
"100": "46, 59, 147",
"200": "134, 134, 134",
"300": "139, 139, 139",
"400": "220, 219, 214",
"500": "228, 227, 219",
"600": "240, 240, 235",
"700": "248, 247, 241",
"800": "243, 244, 246",
"900": "255, 255, 255",
"950": "255, 255, 255",
text: "82, 82, 82",
},
};

1660
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff