Connect Wallet Button
Solana wallet connection component with LazorKit Passkey support and shadcn UI interface
Connect Wallet Button
Installation
Install dependencies
Start by installing @solana/wallet-adapter-base, wallet-adapter-wallets, and @lazorkit/wallet for Passkey
Install TXN Settings
Install Murphy TXN Settings
Create Network Toggle Component
Create a new file components/layout/network-toggle.tsx:
"use client"
import { useCluster } from "@/components/providers/cluster-provider"
import { Switch } from "@/components/ui/switch"
export function NetworkToggle() {
  const { cluster, setCluster } = useCluster()
  const isMainnet = cluster === "mainnet"
  return (
    <div className="flex items-center gap-2 ml-2">
      <span className="text-xs text-muted-foreground">DEV</span>
      <Switch
        id="network-switch"
        checked={isMainnet}
        onCheckedChange={(checked) => setCluster(checked ? "mainnet" : "devnet")}
        className="scale-75"
      />
      <span className="text-xs text-muted-foreground">MAIN</span>
    </div>
  )
}Create Cluster Provider
Create a new file components/providers/cluster-provider.tsx:
"use client";
import React, { createContext, useContext, useState, useEffect, ReactNode } from "react";
type Cluster = "mainnet" | "devnet";
interface ClusterContextType {
  cluster: Cluster;
  setCluster: (cluster: Cluster) => void;
  isMainnet: boolean;
}
const ClusterContext = createContext<ClusterContextType | undefined>(undefined);
interface ClusterProviderProps {
  children: ReactNode;
  defaultCluster?: Cluster;
}
export function ClusterProvider({ children, defaultCluster = "mainnet" }: ClusterProviderProps) {
  const [cluster, setCluster] = useState<Cluster>(defaultCluster);
  // Update environment variable when cluster changes
  useEffect(() => {
    if (typeof window !== "undefined") {
      window.localStorage.setItem("NEXT_PUBLIC_CLUSTER", cluster);
      
      // Update the global environment variable for components that read it
      (window as any).NEXT_PUBLIC_CLUSTER = cluster;
      
      // Dispatch a custom event to notify components of cluster change
      window.dispatchEvent(new CustomEvent("clusterChanged", { detail: { cluster } }));
    }
  }, [cluster]);
  // Initialize cluster from localStorage on mount
  useEffect(() => {
    if (typeof window !== "undefined") {
      const savedCluster = window.localStorage.getItem("NEXT_PUBLIC_CLUSTER") as Cluster;
      if (savedCluster && (savedCluster === "mainnet" || savedCluster === "devnet")) {
        setCluster(savedCluster);
      }
    }
  }, []);
  const value: ClusterContextType = {
    cluster,
    setCluster,
    isMainnet: cluster === "mainnet",
  };
  return (
    <ClusterContext.Provider value={value}>
      {children}
    </ClusterContext.Provider>
  );
}
export function useCluster() {
  const context = useContext(ClusterContext);
  if (context === undefined) {
    throw new Error("useCluster must be used within a ClusterProvider");
  }
  return context;
}Wrap Your App with ClusterProvider
Update your app's root layout or the specific page where you want to use the network toggle:
import { ClusterProvider } from "@/components/providers/cluster-provider"
import { NetworkToggle } from "@/components/layout/network-toggle"
export default function Layout({ children }) {
  return (
    <ClusterProvider defaultCluster="devnet">
      <div>
        <div className="flex items-center">
          <span className="font-bold">Murphy</span>
          <NetworkToggle />
        </div>
        {children}
      </div>
    </ClusterProvider>
  )
}Create Wallet Provider
components/providers/wallet-provider.tsx
"use client"
import React, { useState, useMemo, createContext, useCallback, useEffect } from "react"
import { WalletAdapterNetwork } from "@solana/wallet-adapter-base"
import type { Adapter } from "@solana/wallet-adapter-base"
import {
  WalletProvider as SolanaWalletProvider,
  ConnectionProvider as SolanaConnectionProvider,
  ConnectionProviderProps,
} from "@solana/wallet-adapter-react"
import { WalletModalProvider } from "@solana/wallet-adapter-react-ui"
import { PhantomWalletAdapter } from "@solana/wallet-adapter-wallets"
import { TxnSettingsProvider } from "@/components/ui/murphy/txn-settings"
import { ClientLazorKitProvider } from "./client-lazorkit-provider"
import { LazorKitWalletProvider } from "./lazorkit-wallet-context"
import "@solana/wallet-adapter-react-ui/styles.css"
// Constants
const DEFAULT_MAINNET_RPC = process.env.NEXT_PUBLIC_SOLANA_RPC_URL || "https://api.mainnet-beta.solana.com"
const DEFAULT_DEVNET_RPC = process.env.NEXT_PUBLIC_SOLANA_RPC_URL_DEVNET || "https://api.devnet.solana.com"
// Create wrapper components
const ConnectionProviderWrapper = (props: ConnectionProviderProps) => (
  <SolanaConnectionProvider {...props} />
)
const WalletProviderWrapper = (props: any) => (
  <SolanaWalletProvider {...props} />
)
interface WalletProviderProps {
  children: React.ReactNode
  network?: WalletAdapterNetwork
  endpoint?: string
  wallets?: Adapter[]
  autoConnect?: boolean
}
interface ModalContextState {
  isOpen: boolean
  setIsOpen: (open: boolean) => void
  endpoint?: string
  switchToNextEndpoint: () => void
  availableEndpoints: string[]
  currentEndpointIndex: number
  isMainnet: boolean
  walletType: 'standard' | 'lazorkit'
  setWalletType: (type: 'standard' | 'lazorkit') => void
  networkType: WalletAdapterNetwork
}
export const ModalContext = createContext<ModalContextState>({
  isOpen: false,
  setIsOpen: () => null,
  endpoint: undefined,
  switchToNextEndpoint: () => null,
  availableEndpoints: [],
  currentEndpointIndex: 0,
  isMainnet: false, // Changed default to false for devnet
  walletType: 'standard',
  setWalletType: () => null,
  networkType: WalletAdapterNetwork.Devnet, // Changed default to Devnet
})
export const WalletProvider = ({ children, ...props }: WalletProviderProps) => {
  const [currentEndpointIndex, setCurrentEndpointIndex] = useState(0)
  const [isOpen, setIsOpen] = useState(false)
  const [walletType, setWalletType] = useState<'standard' | 'lazorkit'>(() => {
    if (typeof window !== 'undefined') {
      const savedType = localStorage.getItem('walletType')
      return (savedType as 'standard' | 'lazorkit') || 'standard'
    }
    return 'standard'
  })
  // Network detection - default to devnet for LazorKit beta
  const isMainnet = useMemo(() => {
    if (walletType === 'lazorkit') return false // Force devnet for LazorKit
    const mainnetEnv = process.env.NEXT_PUBLIC_USE_MAINNET
    return mainnetEnv === undefined ? false : mainnetEnv === "true" // Default to devnet
  }, [walletType])
  const networkType = useMemo(
    () => isMainnet ? WalletAdapterNetwork.Mainnet : WalletAdapterNetwork.Devnet,
    [isMainnet]
  )
  // RPC endpoints management
  const publicRPCs = useMemo(
    () => [isMainnet ? DEFAULT_MAINNET_RPC : DEFAULT_DEVNET_RPC],
    [isMainnet]
  )
  const endpoint = useMemo(() => {
    if (props.endpoint) {
      return props.endpoint
    }
    return publicRPCs[currentEndpointIndex]
  }, [props.endpoint, publicRPCs, currentEndpointIndex])
  // Endpoint switching with error handling
  const switchToNextEndpoint = useCallback(() => {
    setCurrentEndpointIndex((prevIndex) => {
      const nextIndex = (prevIndex + 1) % publicRPCs.length
      console.log(
        `Switching RPC endpoint from ${publicRPCs[prevIndex]} to ${publicRPCs[nextIndex]}`
      )
      return nextIndex
    })
  }, [publicRPCs])
  // Wallet adapters
  const wallets = useMemo(
    () => props.wallets || [new PhantomWalletAdapter()],
    [props.wallets]
  )
  // Persist wallet type
  useEffect(() => {
    if (typeof window !== 'undefined') {
      localStorage.setItem('walletType', walletType)
    }
  }, [walletType])
  // Auto-connect effect
  useEffect(() => {
    if (props.autoConnect && walletType === 'lazorkit') {
      // Attempt to reconnect LazorKit wallet on mount
      const reconnectLazorKit = async () => {
        try {
          // The actual reconnection will be handled by the LazorKitWalletProvider
          console.log('Attempting to reconnect LazorKit wallet...')
        } catch (error) {
          console.error('Failed to reconnect LazorKit wallet:', error)
        }
      }
      reconnectLazorKit()
    }
  }, [props.autoConnect, walletType])
  // Context value memoization
  const contextValue = useMemo(() => ({
    isOpen,
    setIsOpen,
    endpoint,
    switchToNextEndpoint,
    availableEndpoints: publicRPCs,
    currentEndpointIndex,
    isMainnet,
    walletType,
    setWalletType,
    networkType,
  }), [
    isOpen,
    endpoint,
    switchToNextEndpoint,
    publicRPCs,
    currentEndpointIndex,
    isMainnet,
    walletType,
    networkType,
  ])
  return (
    <ModalContext.Provider value={contextValue}>
      <ConnectionProviderWrapper endpoint={endpoint}>
        <WalletProviderWrapper 
          wallets={wallets} 
          autoConnect={props.autoConnect}
          onError={(error: Error) => {
            console.error('Wallet error:', error)
            // Attempt to switch endpoint on connection errors
            if (error.message.includes('connection') || error.message.includes('network')) {
              switchToNextEndpoint()
            }
          }}
        >
          <WalletModalProvider>
            <ClientLazorKitProvider>
              <LazorKitWalletProvider>
                <TxnSettingsProvider>{children}</TxnSettingsProvider>
              </LazorKitWalletProvider>
            </ClientLazorKitProvider>
          </WalletModalProvider>
        </WalletProviderWrapper>
      </ConnectionProviderWrapper>
    </ModalContext.Provider>
  )
}Create LazorKit Wallet Context
components/providers/lazorkit-wallet-context.tsx
// components/providers/lazorkit-wallet-context.tsx
"use client"
import React, { createContext, useContext, useCallback, useState, useEffect, useMemo } from "react"
import { useWallet as useLazorKitWallet, WalletAccount } from "@lazorkit/wallet"
import { Transaction, PublicKey, TransactionInstruction } from "@solana/web3.js"
// Custom error class for LazorKit errors
class LazorKitError extends Error {
  constructor(message: string, public code?: string, public isAccountNotFound: boolean = false) {
    super(message)
    this.name = 'LazorKitError'
  }
}
// Extended WalletAccount to include createSmartWallet
interface ExtendedWalletAccount extends WalletAccount {
  createSmartWallet?: () => Promise<void>
}
// Connect Response type for createPasskeyOnly
interface ConnectResponse {
  publicKey: string
  credentialId: string
  isCreated: boolean
  connectionType: 'create' | 'get'
  timestamp: number
}
// Extended LazorKit wallet interface
interface ExtendedLazorKitWallet {
  smartWalletPubkey: PublicKey | null
  isConnected: boolean
  isLoading: boolean
  isConnecting: boolean
  isSigning: boolean
  error: Error | null
  account: WalletAccount | null
  connect: () => Promise<WalletAccount>
  disconnect: () => Promise<void>
  signTransaction: (instruction: TransactionInstruction) => Promise<Transaction>
  signAndSendTransaction: (instruction: TransactionInstruction) => Promise<string>
  createPasskeyOnly: () => Promise<ConnectResponse>
  createSmartWalletOnly: (passkeyData: ConnectResponse) => Promise<{smartWalletAddress: string, account: WalletAccount}>
  reconnect: () => Promise<WalletAccount>
}
interface LazorKitWalletContextState {
  smartWalletPubkey: PublicKey | null
  isConnected: boolean
  isLoading: boolean
  isConnecting: boolean
  isSigning: boolean
  error: Error | null
  account: ExtendedWalletAccount | null
  connect: () => Promise<ExtendedWalletAccount>
  disconnect: () => Promise<void>
  reconnect: () => Promise<ExtendedWalletAccount>
  signTransaction: (instruction: TransactionInstruction) => Promise<Transaction>
  signAndSendTransaction: (instruction: TransactionInstruction) => Promise<string>
  createPasskeyOnly: () => Promise<ConnectResponse>
  createSmartWalletOnly: (passkeyData: ConnectResponse) => Promise<{smartWalletAddress: string, account: ExtendedWalletAccount}>
  clearError: () => void
}
const defaultContext: LazorKitWalletContextState = {
  smartWalletPubkey: null,
  isConnected: false,
  isLoading: false,
  isConnecting: false,
  isSigning: false,
  error: null,
  account: null,
  connect: async () => { throw new LazorKitError("LazorKitWalletContext not initialized") },
  disconnect: async () => { throw new LazorKitError("LazorKitWalletContext not initialized") },
  reconnect: async () => { throw new LazorKitError("LazorKitWalletContext not initialized") },
  signTransaction: async () => { throw new LazorKitError("LazorKitWalletContext not initialized") },
  signAndSendTransaction: async () => { throw new LazorKitError("LazorKitWalletContext not initialized") },
  createPasskeyOnly: async () => { throw new LazorKitError("LazorKitWalletContext not initialized") },
  createSmartWalletOnly: async () => { throw new LazorKitError("LazorKitWalletContext not initialized") },
  clearError: () => {}
}
export const LazorKitWalletContext = createContext<LazorKitWalletContextState>(defaultContext)
export const useLazorKitWalletContext = () => {
  const context = useContext(LazorKitWalletContext)
  if (!context) {
    throw new LazorKitError("useLazorKitWalletContext must be used within a LazorKitWalletProvider")
  }
  return context
}
// Utility function for error handling
const handleError = (err: unknown): Error => {
  if (err instanceof Error) {
    // Check for specific error types
    if (err.message.includes('Account does not exist') || 
        err.message.includes('has no data')) {
      return new LazorKitError(
        "Smart wallet needs to be initialized. Please try connecting again.", 
        'ACCOUNT_NOT_FOUND',
        true
      )
    }
    if (err.message.includes('NO_STORED_CREDENTIALS')) {
      return new LazorKitError("No stored credentials found", 'NO_STORED_CREDENTIALS')
    }
    if (err.message.includes('INVALID_CREDENTIALS')) {
      return new LazorKitError("Invalid credentials", 'INVALID_CREDENTIALS')
    }
    return err
  }
  return new LazorKitError(err instanceof Object ? JSON.stringify(err) : String(err))
}
export function LazorKitWalletProvider({ children }: { children: React.ReactNode }) {
  const wallet = useLazorKitWallet() as unknown as ExtendedLazorKitWallet
  const [isConnecting, setIsConnecting] = useState(false)
  const [error, setError] = useState<Error | null>(null)
  const [retryCount, setRetryCount] = useState(0)
  const MAX_RETRIES = 3
  const clearError = useCallback(() => setError(null), [])
  // Auto-retry connection on certain errors
  useEffect(() => {
    if (error && retryCount < MAX_RETRIES && !isConnecting) {
      const timer = setTimeout(() => {
        console.log(`Retrying connection (attempt ${retryCount + 1}/${MAX_RETRIES})`)
        setRetryCount(prev => prev + 1)
        connect()
      }, Math.min(1000 * Math.pow(2, retryCount), 8000)) // Exponential backoff
      return () => clearTimeout(timer)
    }
  }, [error, retryCount, isConnecting])
  const connect = useCallback(async () => {
    if (isConnecting) return wallet.account as ExtendedWalletAccount
    
    try {
      setIsConnecting(true)
      setError(null)
      
      // First try reconnecting with stored credentials
      try {
        const reconnectedAccount = await wallet.reconnect()
        setRetryCount(0)
        return reconnectedAccount as ExtendedWalletAccount
      } catch (reconnectError) {
        // If reconnect fails, try new connection
        try {
          const newAccount = await wallet.connect()
          setRetryCount(0)
          return newAccount as ExtendedWalletAccount
        } catch (connectError) {
          throw handleError(connectError)
        }
      }
    } catch (err) {
      const error = handleError(err)
      setError(error)
      throw error
    } finally {
      setIsConnecting(false)
    }
  }, [wallet.connect, wallet.reconnect, wallet.account, isConnecting])
  const disconnect = useCallback(async () => {
    try {
      setError(null)
      await wallet.disconnect()
      setRetryCount(0)
    } catch (err) {
      const error = handleError(err)
      setError(error)
      throw error
    }
  }, [wallet.disconnect])
  const reconnect = useCallback(async () => {
    try {
      setError(null)
      return await wallet.reconnect() as ExtendedWalletAccount
    } catch (err) {
      const error = handleError(err)
      setError(error)
      throw error
    }
  }, [wallet.reconnect])
  const createPasskeyOnly = useCallback(async () => {
    try {
      setError(null)
      return await wallet.createPasskeyOnly()
    } catch (err) {
      const error = handleError(err)
      setError(error)
      throw error
    }
  }, [wallet.createPasskeyOnly])
  const createSmartWalletOnly = useCallback(async (passkeyData: ConnectResponse) => {
    try {
      setError(null)
      return await wallet.createSmartWalletOnly(passkeyData)
    } catch (err) {
      const error = handleError(err)
      setError(error)
      throw error
    }
  }, [wallet.createSmartWalletOnly])
  const signTransaction = useCallback(async (instruction: TransactionInstruction) => {
    try {
      setError(null)
      return await wallet.signTransaction(instruction)
    } catch (err) {
      const error = handleError(err)
      setError(error)
      throw error
    }
  }, [wallet.signTransaction])
  const signAndSendTransaction = useCallback(async (instruction: TransactionInstruction) => {
    try {
      setError(null)
      return await wallet.signAndSendTransaction(instruction)
    } catch (err) {
      const error = handleError(err)
      setError(error)
      throw error
    }
  }, [wallet.signAndSendTransaction])
  // Memoize the context value to prevent unnecessary re-renders
  const value = useMemo(() => ({
    smartWalletPubkey: wallet.smartWalletPubkey,
    isConnected: wallet.isConnected,
    isLoading: wallet.isLoading,
    isConnecting,
    isSigning: wallet.isSigning,
    error,
    account: wallet.account as ExtendedWalletAccount,
    connect,
    disconnect,
    reconnect,
    signTransaction,
    signAndSendTransaction,
    createPasskeyOnly,
    createSmartWalletOnly,
    clearError
  }), [
    wallet.smartWalletPubkey,
    wallet.isConnected,
    wallet.isLoading,
    isConnecting,
    wallet.isSigning,
    error,
    wallet.account,
    connect,
    disconnect,
    reconnect,
    signTransaction,
    signAndSendTransaction,
    createPasskeyOnly,
    createSmartWalletOnly,
    clearError
  ])
  return (
    <LazorKitWalletContext.Provider value={value}>
      {children}
    </LazorKitWalletContext.Provider>
  )
}Create LazorKit Wallet Client Provider
components/providers/client-lazorkit-provider.tsx
"use client"
import React from "react"
import { LazorkitProvider } from "@lazorkit/wallet"
const DEFAULT_RPC_URL = "https://api.devnet.solana.com" // Changed to devnet as per docs
const DEFAULT_IPFS_URL = "https://portal.lazor.sh"
const DEFAULT_PAYMASTER_URL = "https://lazorkit-paymaster.onrender.com"
export function ClientLazorKitProvider({ children }: { children: React.ReactNode }) {
  // Validate and use environment variables with fallbacks
  const rpcUrl = process.env.LAZORKIT_RPC_URL || DEFAULT_RPC_URL
  const ipfsUrl = process.env.LAZORKIT_PORTAL_URL || DEFAULT_IPFS_URL
  const paymasterUrl = process.env.LAZORKIT_PAYMASTER_URL || DEFAULT_PAYMASTER_URL
  // Enable debug mode in development
  const debug = process.env.NODE_ENV === 'development'
  // Log configuration in development
  if (debug) {
    console.debug('LazorKit Provider Configuration:', {
      rpcUrl,
      ipfsUrl,
      paymasterUrl,
      debug
    })
  }
  return (
    <LazorkitProvider
      rpcUrl={rpcUrl}
      ipfsUrl={ipfsUrl}
      paymasterUrl={paymasterUrl}
    >
      {children}
    </LazorkitProvider>
  )
}Create file .env
.env
# Using mainnet or testnet
NEXT_PUBLIC_USE_MAINNET=false
# Solana RPC URLs, from alchemy.com or providers
NEXT_PUBLIC_SOLANA_RPC_URL="https://solana-mainnet.g.alchemy.com/v2/your-alchemy-api-key"
NEXT_PUBLIC_SOLANA_RPC_URL_DEVNET="https://solana-devnet.g.alchemy.com/v2/your-alchemy-api-key"
LAZORKIT_RPC_URL="https://api.devnet.solana.com"
LAZORKIT_PORTAL_URL="https://portal.lazor.sh"
LAZORKIT_PAYMASTER_URL="https://lazorkit-paymaster.onrender.com"Update root layout
Add WalletProvider into root layout
import type React from "react"
import { ThemeProvider } from 'next-themes'
import "@/app/globals.css"
import { WalletProvider } from "@/components/providers/wallet-provider";
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en" suppressHydrationWarning>
      <head />
      <body>
        <ThemeProvider attribute="class" defaultTheme="system" enableSystem>
          <WalletProvider>
            {children}
          </WalletProvider>
        </ThemeProvider>
      </body>
    </html>
  )
}Add Connect Wallet Button
After successful installation, you can find the Connect Wallet Button file in components/ui/murphy
Usage basic
"use client"
import { ConnectWalletButton } from "@/components/ui/murphy/connect-wallet-button"
export default function MyPage() {
  return (
    <div className="flex h-screen items-center justify-center">
      <div className="container mx-auto max-w-md p-8">
        <h1 className="mb-6 text-2xl font-bold text-center">Connect your Solana wallet</h1>
        <ConnectWalletButton className="w-full" />
      </div>
    </div>
  )
}Customization
You can customize the button appearance by passing props:
// Basic customization
<ConnectWalletButton 
  variant="outline" 
  size="lg" 
  className="w-full md:w-auto" 
/>
// With custom labels
<ConnectWalletButton 
  variant="outline"
  size="lg"
  className="w-full md:w-auto"
  labels={{
    "has-wallet": "Connect Wallet",
    "change-wallet": "Change Wallet",
    "copy-address": "Copy Address",
    "disconnect": "Disconnect",
    "lazorkit-wallet": "Use Passkey",
    "standard-wallet": "Use Wallet",
    "connecting": "Connecting...",
    "initializing": "Initializing...",
    "creating-passkey": "Creating Passkey...",
    "creating-smart-wallet": "Creating Smart Wallet..."
  }}
/>| Prop | Type | Default | 
|---|---|---|
labels? | Partial<typeof LABELS> | LABELS | 
asChild? | boolean | false | 
className? | string | - | 
size? | string | default | 
variant? | string | default | 
LazorKit Passkey Integration
The Connect Wallet Button integrates both standard Solana wallets and LazorKit Passkey authentication:
Features
- Standard Wallets: Supports Phantom, Solflare, and other Solana wallets
 - Passkey Support: Passwordless authentication via LazorKit
 - Network Detection: Automatic network switching between Mainnet and Devnet
 - Wallet Address Display: Shows truncated wallet address when connected
 - Copy Address: Easy wallet address copying
 - Responsive Design: Adapts to different screen sizes
 
Modal Interface
When clicked, opens a modal with two tabs:
- Standard Wallet: For connecting traditional Solana wallets
 - Passkey: For connecting via LazorKit's passwordless authentication
 
Network Support
- Standard wallets work on both Mainnet and Devnet
 - LazorKit Passkey currently supports Devnet only (beta)
 
Example with Full Configuration
<ConnectWalletButton 
  variant="outline"
  size="lg"
  className="w-full md:w-auto"
  labels={{
    "lazorkit-wallet": "Use Passkey",
    "standard-wallet": "Use Wallet",
    "has-wallet": "Connect Wallet",
    "change-wallet": "Change Wallet",
    "disconnect": "Disconnect",
    "copy-address": "Copy Address",
    "connecting": "Connecting...",
    "initializing": "Initializing Smart Wallet...",
    "creating-passkey": "Creating Passkey...",
    "creating-smart-wallet": "Creating Smart Wallet..."
  }}
/>Note: When using LazorKit Passkey, make sure your environment is properly configured for Devnet during the beta period.