Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions app/build/bridge/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useCallback, useEffect, useState } from "react";
import Image from "next/image";
import { ChevronDown, ArrowLeftRight, CreditCard, Loader2, ExternalLink, CheckCircle2, AlertTriangle } from "lucide-react";
import { useAccount, useWalletClient, useSwitchChain, useChainId } from "wagmi";
import { useAppKit } from "@reown/appkit/react";
import { openWallet } from "@/lib/appkit";
import { createPublicClient, http, parseEther, type PublicClient } from "viem";
import { BRIDGE_ROUTE, HYPERLANE_ROUTER_ABI, ERC20_ABI, addressToBytes32 } from "lightnode-sdk";
import { CodeTabs } from "@/components/build/console/code-tabs";
Expand Down Expand Up @@ -134,7 +134,6 @@ async function resolveFeeParams(pub: PublicClient): Promise<GasFeeParams | undef

export default function BridgePanel() {
const { address, isConnected } = useAccount();
const { open } = useAppKit();
const [dir, setDir] = useState<Dir>("eth-to-lc");
const [amount, setAmount] = useState("");
const [bal, setBal] = useState<Balances | null>(null);
Expand Down Expand Up @@ -245,7 +244,7 @@ export default function BridgePanel() {

const executeBridge = async () => {
if (!isConnected || !walletClient || !address) {
open();
openWallet();
return;
}
const amt = validateAmount();
Expand Down Expand Up @@ -339,7 +338,7 @@ export default function BridgePanel() {
<span className="font-mono text-content-primary">{shortAddr(address)}</span>
</span>
) : (
<button type="button" onClick={() => open()} className="inline-flex items-center gap-1.5 text-sm text-content-soft transition-colors hover:text-content-primary">
<button type="button" onClick={() => openWallet()} className="inline-flex items-center gap-1.5 text-sm text-content-soft transition-colors hover:text-content-primary">
Connect Wallet <ChevronDown className="size-3.5" />
</button>
)}
Expand Down
5 changes: 2 additions & 3 deletions app/build/dao/voting-power-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { ShieldCheck, AlertTriangle, ExternalLink, Wallet } from "lucide-react";
import { useAccount } from "wagmi";
import { useAppKit } from "@reown/appkit/react";
import { openWallet } from "@/lib/appkit";
import { short } from "@/components/build/console/panel-kit";
import { DAO_VOTE_UI, DELEGATION_UI, loadVotingPower, type DaoChain, type VotingPowerReads } from "./dao-chain";
import { delegationStatus, formatLcaiWei } from "./dao-math";
Expand All @@ -17,7 +17,6 @@ const SYMBOL: Record<DaoChain, string> = { ethereum: "LCAIB", lightchain: "LCAI"
*/
export function VotingPowerCard({ chain }: { chain: DaoChain }) {
const { address, isConnected } = useAccount();
const { open } = useAppKit();
const [reads, setReads] = useState<VotingPowerReads | null>(null);
const [loading, setLoading] = useState(false);
// Stale-read guard: a slow read for the previous chain/account must not
Expand Down Expand Up @@ -49,7 +48,7 @@ export function VotingPowerCard({ chain }: { chain: DaoChain }) {
return (
<button
type="button"
onClick={() => open()}
onClick={() => openWallet()}
className="flex w-full items-center justify-between gap-3 rounded-2xl border border-bdr-soft bg-card/60 px-4 py-3.5 text-left backdrop-blur-sm transition-colors hover:border-primary/40"
>
<span className="flex items-center gap-2.5 text-sm text-content-soft">
Expand Down
19 changes: 12 additions & 7 deletions components/connect-button.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client";

import { useAppKit, useAppKitAccount, useAppKitNetwork } from "@reown/appkit/react";
import { useSwitchChain } from "wagmi";
import { useAccount, useChainId, useSwitchChain } from "wagmi";
import { openWallet, prefetchWallet } from "@/lib/appkit";
import { Wallet, AlertTriangle, ChevronDown, Loader2 } from "lucide-react";
import { Button, type ButtonProps } from "@/components/ui/button";
import { NETWORKS } from "@/lib/network";
Expand All @@ -22,15 +22,20 @@ function avatarGradient(addr: string): string {

/** Reown AppKit connect flow rendered with LightNode's own styling. */
export function ConnectButton({ size = "default" }: { size?: ButtonProps["size"] }) {
const { open } = useAppKit();
const { isConnected, address } = useAppKitAccount();
const { chainId } = useAppKitNetwork();
// Read state from wagmi, not AppKit. These hooks come from the eager bundle
// (WagmiProvider already wraps the tree), so this button renders - and shows
// a reconnected address after a reload - without pulling in the wallet modal.
// Using AppKit's equivalents here would force every page to load it again.
const { isConnected, address } = useAccount();
const chainId = useChainId();
const { switchChain, isPending } = useSwitchChain();
const { network } = useNetwork();

if (!isConnected) {
return (
<Button variant="gradient" size={size} onClick={() => open()}>
// Hover starts the chunk download, so the modal is usually ready by the
// time the click lands.
<Button variant="gradient" size={size} onMouseEnter={prefetchWallet} onClick={() => openWallet()}>
<Wallet /> Connect wallet
</Button>
);
Expand All @@ -53,7 +58,7 @@ export function ConnectButton({ size = "default" }: { size?: ButtonProps["size"]

return (
<button
onClick={() => open({ view: "Account" })}
onClick={() => openWallet({ view: "Account" })}
className="group inline-flex items-center gap-2 rounded-full border border-bdr-soft bg-surface-base-subtle py-1 pl-1 pr-2.5 transition-colors hover:border-primary/40 hover:bg-surface-base-faint"
>
<span
Expand Down
38 changes: 10 additions & 28 deletions components/providers.tsx
Original file line number Diff line number Diff line change
@@ -1,39 +1,21 @@
"use client";

import { createAppKit } from "@reown/appkit/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { WagmiProvider } from "wagmi";
import { useState } from "react";
import { wagmiAdapter, projectId, networks } from "@/lib/wagmi";
import { wagmiAdapter } from "@/lib/wagmi";
import { NetworkProvider } from "@/lib/network-context";
import { AutoUpdate } from "@/components/auto-update";

// AppKit is created once at module scope (matches LightChain's own chat setup).
createAppKit({
adapters: [wagmiAdapter],
projectId,
networks,
defaultNetwork: networks[0],
metadata: {
name: "LightNode",
description: "The open toolkit for LightChain AI: SDK, wallet, worker onboarding, and live dashboards.",
url: "https://lightnode.app",
icons: ["https://lightnode.app/lightnode-mark.png"],
},
themeMode: "dark",
// Coinbase/Base connectors bundle their own IndexedDB telemetry, which throws
// in the desktop WebView and isn't usable there anyway. Keep injected
// (MetaMask) + WalletConnect, which cover the real flows.
enableCoinbase: false,
themeVariables: {
"--w3m-accent": "#7064e9",
"--w3m-color-mix": "#7064e9",
"--w3m-color-mix-strength": 12,
"--w3m-font-family": "var(--font-inter), ui-sans-serif, system-ui, sans-serif",
"--w3m-border-radius-master": "2.5px",
},
features: { analytics: false, email: true, socials: ["google", "x", "github", "discord"] },
});
// AppKit is NOT created here. This provider wraps every route, so building the
// modal at module scope made each page download and execute the whole wallet
// stack before it could render - 1,395 KB of chunk, 53% of /onboard, on pages
// where nobody had touched a wallet. It is created on the first openWallet()
// call instead; see lib/appkit.ts.
//
// WagmiProvider stays eager: connection state has to survive a reload and be
// readable by any page (useAccount / useChainId), and its config is what the
// lazily-created modal later attaches to.

export function Providers({ children }: { children: React.ReactNode }) {
const [queryClient] = useState(() => new QueryClient());
Expand Down
90 changes: 90 additions & 0 deletions lib/appkit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/**
* Lazily-created AppKit modal.
*
* `createAppKit()` used to run at module scope in components/providers.tsx,
* which app/layout.tsx renders for EVERY route. So a visitor reading the
* landing page - or an operator on /onboard who has not touched a wallet -
* still downloaded, parsed and executed the whole wallet stack: AppKit's UI,
* WalletConnect, and the Coinbase SDK that `enableCoinbase: false` disables at
* runtime but does not remove from the bundle. Measured against production:
* 1,395 KB in a single chunk, 53% of everything /onboard shipped.
*
* Nothing about that stack is needed until someone actually opens the wallet
* modal, so it now loads on the first `openWallet()` call. The import is cached
* in a module-level promise: concurrent clicks share one download, and
* `createAppKit` runs exactly once (calling it twice registers a second modal
* and the account state desynchronises).
*
* Reading connection state does NOT go through here. Components use wagmi's
* own `useAccount` / `useChainId`, which are already in the eager bundle
* because WagmiProvider wraps the tree - so the connect button renders, and
* shows a connected address across reloads, without pulling AppKit at all.
*/
import type { AppKit } from "@reown/appkit";

let modalPromise: Promise<AppKit> | null = null;

/** Options accepted by AppKit's `open()`, kept structural to avoid the import. */
export type OpenWalletOptions = { view?: "Account" | "Connect" | "Networks" };

async function createModal(): Promise<AppKit> {
// Both imports are dynamic on purpose: lib/wagmi.ts constructs the
// WagmiAdapter, which drags in @wagmi/connectors, so importing it eagerly
// here would defeat the split.
const [{ createAppKit }, { wagmiAdapter, projectId, networks }] = await Promise.all([
import("@reown/appkit/react"),
import("./wagmi"),
]);

return createAppKit({
adapters: [wagmiAdapter],
projectId,
networks,
defaultNetwork: networks[0],
metadata: {
name: "LightNode",
description: "The open toolkit for LightChain AI: SDK, wallet, worker onboarding, and live dashboards.",
url: "https://lightnode.app",
icons: ["https://lightnode.app/lightnode-mark.png"],
},
themeMode: "dark",
// Coinbase/Base connectors bundle their own IndexedDB telemetry, which
// throws in the desktop WebView and isn't usable there anyway. Keep
// injected (MetaMask) + WalletConnect, which cover the real flows.
enableCoinbase: false,
themeVariables: {
"--w3m-accent": "#7064e9",
"--w3m-color-mix": "#7064e9",
"--w3m-color-mix-strength": 12,
"--w3m-font-family": "var(--font-inter), ui-sans-serif, system-ui, sans-serif",
"--w3m-border-radius-master": "2.5px",
},
features: { analytics: false, email: true, socials: ["google", "x", "github", "discord"] },
});
}

/**
* Open the wallet modal, creating it on first use.
*
* Rejections are swallowed deliberately: a failed chunk fetch (offline, CDN
* blip) must not surface as an unhandled rejection from a click handler. The
* cache is cleared so the next click retries rather than reusing a promise that
* can never resolve.
*/
export async function openWallet(options?: OpenWalletOptions): Promise<void> {
try {
modalPromise ??= createModal();
const modal = await modalPromise;
await modal.open(options);
} catch {
modalPromise = null;
}
}

/** Warm the chunk without opening anything - e.g. on hover of a connect CTA. */
export function prefetchWallet(): void {
modalPromise ??= createModal();
void modalPromise.catch(() => {
modalPromise = null;
});
}
Loading