Murphy Logo Murphy

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

pnpm add @solana/wallet-adapter-base @solana/wallet-adapter-react @solana/wallet-adapter-react-ui @solana/wallet-adapter-wallets @lazorkit/wallet next-themes

Install TXN Settings

Install Murphy TXN Settings

pnpm dlx shadcn@canary add https://www.murphyai.dev/r/txn-settings.json

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

pnpm dlx shadcn@canary add https://www.murphyai.dev/r/connect-wallet-button.json

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..."
  }}
/>
PropTypeDefault
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

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.