Get the FREE Ultimate OpenClaw Setup Guide →

Walletconnect

npx machina-cli add skill hadv/claude-skill-set/walletconnect --openclaw
Files (1)
SKILL.md
54.6 KB

WalletConnect Integration Skill

Overview

Expert knowledge for implementing WalletConnect v2 (Sign protocol) in web applications, particularly for wallet applications that need to connect to dApps. This skill covers the complete lifecycle from initialization to session management and request handling.

⚠️ IMPORTANT UPDATE (2025): WalletConnect has been rebranded to Reown. The @walletconnect/web3wallet package is deprecated in favor of @reown/walletkit. This skill covers both the legacy WalletConnect v2 packages (still supported until February 2025) and migration guidance to Reown packages. See Migration Guide section below.

Table of Contents

Quick Start

Get up and running with WalletConnect v2 in 5 minutes:

# Install packages (legacy - still supported until Feb 2025)
npm install @walletconnect/web3wallet @walletconnect/core @walletconnect/utils

# OR use new Reown packages (recommended)
npm install @reown/walletkit

Minimal Working Example (JavaScript):

import { Web3Wallet } from '@walletconnect/web3wallet'
import { Core } from '@walletconnect/core'

// 1. Initialize
const core = new Core({ projectId: 'YOUR_PROJECT_ID' })
const web3wallet = await Web3Wallet.init({
  core,
  metadata: {
    name: 'My Wallet',
    description: 'My Wallet Description',
    url: 'https://mywallet.com',
    icons: ['https://mywallet.com/icon.png']
  }
})

// 2. Listen for session proposals
web3wallet.on('session_proposal', async (proposal) => {
  // Show approval UI to user, then:
  const session = await web3wallet.approveSession({
    id: proposal.id,
    namespaces: {
      eip155: {
        chains: ['eip155:1'],
        methods: ['eth_sendTransaction', 'personal_sign'],
        events: ['chainChanged', 'accountsChanged'],
        accounts: ['eip155:1:0xYourAddress']
      }
    }
  })
})

// 3. Pair with dApp
await web3wallet.core.pairing.pair({ uri: 'wc:...' })

Core Concepts

WalletConnect v2 Architecture

  • Web3Wallet: Wallet-side SDK for receiving connection requests from dApps
  • Core: Underlying protocol layer handling pairing, sessions, and messaging
  • Sign Protocol: The protocol for signing transactions and messages
  • Pairing: Initial connection establishment using URI
  • Session: Persistent connection between wallet and dApp
  • Namespaces: Define supported chains, methods, and events (e.g., eip155 for Ethereum)

Key Components

  1. Project ID: Required from WalletConnect Cloud (https://cloud.walletconnect.com)
  2. Metadata: Wallet information (name, description, URL, icons)
  3. Event Listeners: Handle proposals, requests, and disconnections
  4. Session Management: Approve/reject proposals, handle active sessions

Installation & Setup

1. Installation

Legacy WalletConnect v2 Packages (supported until February 2025):

npm install @walletconnect/web3wallet @walletconnect/core @walletconnect/utils

Package Versions (tested and working):

  • @walletconnect/web3wallet: 1.16.1
  • @walletconnect/core: 2.21.0
  • @walletconnect/utils: 2.21.0

New Reown Packages (recommended):

npm install @reown/walletkit

Note: Always check for the latest compatible versions using npm info @walletconnect/web3wallet or npm info @reown/walletkit. The WalletConnect v2 packages will reach end-of-life on February 18, 2025.

2. TypeScript Setup

Type Definitions:

import type {
  Web3Wallet,
  Web3WalletTypes
} from '@walletconnect/web3wallet'
import type { SessionTypes, ProposalTypes, SignClientTypes } from '@walletconnect/types'

// Session Proposal Type
type SessionProposal = SignClientTypes.EventArguments['session_proposal']

// Session Request Type
type SessionRequest = SignClientTypes.EventArguments['session_request']

// Namespace Configuration Type
interface Namespaces {
  [namespace: string]: {
    chains: string[]
    methods: string[]
    events: string[]
    accounts: string[]
  }
}

// Session Type
type Session = SessionTypes.Struct

// Common Request Methods
type RequestMethod =
  | 'eth_sendTransaction'
  | 'eth_signTransaction'
  | 'eth_sign'
  | 'personal_sign'
  | 'eth_signTypedData'
  | 'eth_signTypedData_v4'

// Transaction Request Params
interface TransactionRequest {
  from: string
  to: string
  value?: string
  data?: string
  gas?: string
  gasPrice?: string
  nonce?: string
}

// Response Format
interface JsonRpcResponse<T = any> {
  id: number
  jsonrpc: '2.0'
  result?: T
  error?: {
    code: number
    message: string
  }
}

3. Initialization (React Context Pattern)

import { Web3Wallet } from '@walletconnect/web3wallet'
import { Core } from '@walletconnect/core'

// Initialize Core first
const core = new Core({
  projectId: 'YOUR_PROJECT_ID', // Get from https://cloud.walletconnect.com
})

// Initialize Web3Wallet with Core
const web3wallet = await Web3Wallet.init({
  core,
  metadata: {
    name: 'Your Wallet Name',
    description: 'Your wallet description',
    url: 'https://yourwallet.com',
    icons: ['https://yourwallet.com/logo.png'],
  },
})

Critical Implementation Details:

  • Use refs to prevent multiple initializations in React (StrictMode causes double renders)
  • Initialize Core separately before Web3Wallet
  • Store project ID in environment variables
  • Validate project ID before initialization

4. Event Listeners Setup

JavaScript:

// Session proposal - when dApp wants to connect
web3wallet.on('session_proposal', (proposal) => {
  console.log('Session proposal received:', proposal)
  // Show UI to user for approval
  setPendingProposal(proposal)
})

// Session request - when dApp requests transaction/signature
web3wallet.on('session_request', (request) => {
  console.log('Session request received:', request)
  // Show UI to user for approval
  setPendingRequest(request)
})

// Session delete - when session is terminated
web3wallet.on('session_delete', ({ topic }) => {
  console.log('Session deleted:', topic)
  // Update UI to remove session
  updateActiveSessions()
})

// Session update - when session chains/accounts change
web3wallet.on('session_update', ({ topic, params }) => {
  console.log('Session updated:', topic, params)
  updateActiveSessions()
})

TypeScript:

import type { Web3Wallet } from '@walletconnect/web3wallet'
import type { SignClientTypes } from '@walletconnect/types'

const setupEventListeners = (web3wallet: Web3Wallet) => {
  // Session proposal
  web3wallet.on('session_proposal',
    (proposal: SignClientTypes.EventArguments['session_proposal']) => {
      console.log('Session proposal received:', proposal)
      setPendingProposal(proposal)
    }
  )

  // Session request
  web3wallet.on('session_request',
    (request: SignClientTypes.EventArguments['session_request']) => {
      console.log('Session request received:', request)
      setPendingRequest(request)
    }
  )

  // Session delete
  web3wallet.on('session_delete',
    ({ topic }: { topic: string }) => {
      console.log('Session deleted:', topic)
      updateActiveSessions()
    }
  )

  // Session update
  web3wallet.on('session_update',
    ({ topic, params }: SignClientTypes.EventArguments['session_update']) => {
      console.log('Session updated:', topic, params)
      updateActiveSessions()
    }
  )
}

// Cleanup function
const cleanupEventListeners = (web3wallet: Web3Wallet) => {
  web3wallet.removeAllListeners('session_proposal')
  web3wallet.removeAllListeners('session_request')
  web3wallet.removeAllListeners('session_delete')
  web3wallet.removeAllListeners('session_update')
}

4. Pairing with dApp

// User scans QR code or pastes URI from dApp
const uri = 'wc:...' // WalletConnect URI from dApp

// Use core.pairing.pair (not web3wallet.pair) for better event handling
await web3wallet.core.pairing.pair({ uri })

// This triggers 'session_proposal' event

URI Validation:

  • Must start with wc:
  • Contains pairing topic and relay information
  • One-time use only

5. Approving Session Proposal

const approveSession = async (proposal, accountAddress, chainId) => {
  const { id, params } = proposal

  // Build namespaces manually for full control
  const namespaces = {
    eip155: {
      chains: [`eip155:${chainId}`], // e.g., 'eip155:11155111' for Sepolia
      methods: [
        'eth_sendTransaction',
        'eth_signTransaction',
        'eth_sign',
        'personal_sign',
        'eth_signTypedData',
        'eth_signTypedData_v4',
      ],
      events: ['chainChanged', 'accountsChanged'],
      accounts: [`eip155:${chainId}:${accountAddress}`],
    },
  }

  const session = await web3wallet.approveSession({
    id,
    namespaces,
  })

  return session
}

Namespace Structure:

  • chains: Array of supported chains in CAIP-2 format (eip155:${chainId})
  • methods: Array of JSON-RPC methods wallet supports
  • events: Array of events wallet will emit
  • accounts: Array of accounts in CAIP-10 format (eip155:${chainId}:${address})

6. Rejecting Session Proposal

await web3wallet.rejectSession({
  id: proposal.id,
  reason: {
    code: 5000,
    message: 'User rejected',
  },
})

Standard Error Codes:

  • 5000: User rejected
  • 5001: User rejected methods
  • 5002: User rejected chains

7. Handling Session Requests

const handleSessionRequest = async (request) => {
  const { topic, params, id } = request
  const { request: { method, params: methodParams } } = params

  try {
    let result

    switch (method) {
      case 'eth_sendTransaction':
        result = await handleSendTransaction(methodParams[0])
        break

      case 'personal_sign':
        result = await handlePersonalSign(methodParams[0], methodParams[1])
        break

      case 'eth_signTypedData':
      case 'eth_signTypedData_v4':
        result = await handleSignTypedData(methodParams[0], methodParams[1])
        break

      default:
        throw new Error(`Unsupported method: ${method}`)
    }

    // Send success response
    await web3wallet.respondSessionRequest({
      topic,
      response: {
        id,
        jsonrpc: '2.0',
        result,
      },
    })
  } catch (error) {
    // Send error response
    await web3wallet.respondSessionRequest({
      topic,
      response: {
        id,
        jsonrpc: '2.0',
        error: {
          code: 5000,
          message: error.message,
        },
      },
    })
  }
}

8. Method Implementations

eth_sendTransaction

const handleSendTransaction = async (tx) => {
  // tx contains: { to, value, data, from, gas, gasPrice }

  // For EOA wallets:
  const txHash = await signer.sendTransaction(tx)
  return txHash

  // For smart contract wallets (ERC-4337):
  // Build UserOperation, sign, and send to bundler
  // Return transaction hash after confirmation
}

personal_sign

const handlePersonalSign = async (message, address) => {
  // message is hex-encoded
  const signature = await signer.signMessage(ethers.getBytes(message))
  return signature
}

eth_signTypedData / eth_signTypedData_v4

const handleSignTypedData = async (address, typedData) => {
  const parsedData = JSON.parse(typedData)
  const signature = await signer.signTypedData(
    parsedData.domain,
    parsedData.types,
    parsedData.message
  )
  return signature
}

9. Session Management

Get Active Sessions

const activeSessions = web3wallet.getActiveSessions()
// Returns object with topic as key, session as value
const sessionArray = Object.values(activeSessions)

Disconnect Session

await web3wallet.disconnectSession({
  topic,
  reason: {
    code: 6000,
    message: 'User disconnected',
  },
})

Common Patterns & Best Practices

React Context Pattern

// Create context for app-wide access
const WalletConnectContext = createContext()

export const WalletConnectProvider = ({ children }) => {
  const [web3wallet, setWeb3wallet] = useState(null)
  const [isInitialized, setIsInitialized] = useState(false)
  const [sessions, setSessions] = useState([])
  const [pendingProposal, setPendingProposal] = useState(null)
  const [pendingRequest, setPendingRequest] = useState(null)

  // Initialize in useEffect
  // Set up event listeners
  // Provide methods via context

  return (
    <WalletConnectContext.Provider value={{
      web3wallet,
      isInitialized,
      sessions,
      pendingProposal,
      pendingRequest,
      pair,
      approveSession,
      rejectSession,
      respondSessionRequest,
      disconnectSession,
    }}>
      {children}
    </WalletConnectContext.Provider>
  )
}

Prevent Multiple Initializations

const initializingRef = useRef(false)
const initializedRef = useRef(false)

useEffect(() => {
  const initWalletConnect = async () => {
    if (initializedRef.current || initializingRef.current) {
      return // Already initialized or initializing
    }

    initializingRef.current = true

    try {
      // Initialize...
      initializedRef.current = true
    } finally {
      initializingRef.current = false
    }
  }

  initWalletConnect()
}, []) // Empty deps - run once

Session Persistence

Sessions are automatically persisted by WalletConnect SDK in IndexedDB. On page reload:

// Load existing sessions after initialization
const activeSessions = web3wallet.getActiveSessions()
setSessions(Object.values(activeSessions))

Common Issues & Solutions

Issue: Multiple Initializations in React StrictMode

Solution: Use refs to track initialization state (see pattern above)

Issue: Event listeners not firing

Solution: Use web3wallet.core.pairing.pair({ uri }) instead of web3wallet.pair(uri)

Issue: "Invalid project ID" error

Solution:

Issue: Session proposal not showing

Solution: Ensure event listeners are set up before pairing

Issue: "Unsupported method" errors

Solution: Only include methods you actually support in namespace definition

Security Considerations

  1. Always show user what they're signing: Display decoded transaction/message details
  2. Validate requests: Check sender, method, parameters before processing
  3. User confirmation required: Never auto-approve transactions or signatures
  4. Session review: Allow users to see and disconnect active sessions
  5. Error handling: Don't expose sensitive information in error messages
  6. Rate limiting: Consider limiting requests per session
  7. Timeout handling: Set reasonable timeouts for user approval

Smart Contract Wallet Integration (ERC-4337)

When integrating WalletConnect with Account Abstraction wallets:

Transaction Flow

  1. Receive Request: dApp sends eth_sendTransaction via WalletConnect
  2. Build UserOperation: Convert transaction to UserOperation format
  3. Sign UserOperation: Use passkey/owner to sign
  4. Submit to Bundler: Send signed UserOp to bundler
  5. Wait for Confirmation: Monitor transaction status
  6. Return Hash: Send transaction hash back to dApp

Example Implementation

const handleSendTransaction = async (tx) => {
  // Build UserOperation from transaction
  const userOp = await sdk.buildUserOperation(
    accountAddress,
    tx.to,
    tx.value || '0',
    tx.data || '0x'
  )

  // Sign with passkey (and owner if 2FA enabled)
  const signedUserOp = await sdk.signUserOperation(
    userOp,
    passkeyCredential,
    twoFactorEnabled ? ownerSigner : null
  )

  // Send to bundler
  const userOpHash = await sdk.sendUserOperation(signedUserOp)

  // Wait for receipt
  const receipt = await sdk.waitForUserOperationReceipt(userOpHash)

  // Return transaction hash to dApp
  return receipt.transactionHash
}

Message Signing for Smart Wallets

Smart contract wallets should implement EIP-1271 for signature validation:

const handlePersonalSign = async (message, address) => {
  // Option 1: Sign with owner key (simpler, but less secure)
  const signature = await ownerSigner.signMessage(ethers.getBytes(message))

  // Option 2: Use EIP-1271 (recommended for production)
  // Implement isValidSignature on smart contract
  // Return signature that contract can verify

  return signature
}

UI/UX Best Practices

Session Proposal Modal

  • Show dApp name, icon, and URL
  • Display requested permissions (chains, methods)
  • Show which account will be connected
  • Clear approve/reject buttons
  • Loading states during approval

Session Request Modal

  • Show dApp making the request
  • Decode and display transaction details:
    • Recipient address
    • Amount (in ETH, not wei)
    • Function being called
    • Gas estimates
  • For message signing: show decoded message
  • Clear approve/reject buttons
  • Loading states during signing

Active Sessions List

  • Show all connected dApps
  • Display dApp icon, name, URL
  • Show connected chains
  • Disconnect button for each session
  • Empty state when no sessions

Testing

Test with WalletConnect Example dApp

https://react-app.walletconnect.com/

Manual Testing Checklist

  • Initialize WalletConnect successfully
  • Pair with dApp using URI
  • Approve session proposal
  • Reject session proposal
  • Send transaction request
  • Sign message request
  • Sign typed data request
  • Disconnect session from wallet
  • Disconnect session from dApp
  • Session persists after page reload
  • Multiple concurrent sessions work
  • Error handling for invalid URIs
  • Error handling for rejected requests
  • Timeout handling for user approval

Performance Considerations

  1. Lazy Loading: Only initialize WalletConnect when needed
  2. Event Cleanup: Remove event listeners on unmount
  3. Session Caching: Cache active sessions to avoid repeated queries
  4. Debouncing: Debounce UI updates from events
  5. Background Processing: Handle requests in background when possible

References

Real-World Example

Based on the ΞTHΛURΛ wallet implementation, here's a complete working example:

Context Provider

import React, { createContext, useContext, useState, useEffect, useCallback, useRef } from 'react'
import { Web3Wallet } from '@walletconnect/web3wallet'
import { Core } from '@walletconnect/core'

const WalletConnectContext = createContext()

export const useWalletConnect = () => {
  const context = useContext(WalletConnectContext)
  if (!context) {
    throw new Error('useWalletConnect must be used within WalletConnectProvider')
  }
  return context
}

export const WalletConnectProvider = ({ children }) => {
  const [web3wallet, setWeb3wallet] = useState(null)
  const [isInitialized, setIsInitialized] = useState(false)
  const [sessions, setSessions] = useState([])
  const [pendingProposal, setPendingProposal] = useState(null)
  const [pendingRequest, setPendingRequest] = useState(null)
  const initializingRef = useRef(false)
  const initializedRef = useRef(false)

  useEffect(() => {
    const initWalletConnect = async () => {
      if (initializedRef.current || initializingRef.current) {
        return
      }

      initializingRef.current = true

      try {
        const projectId = import.meta.env.VITE_WALLETCONNECT_PROJECT_ID

        if (!projectId || projectId === 'YOUR_PROJECT_ID') {
          console.warn('WalletConnect Project ID not set')
          return
        }

        const core = new Core({ projectId })
        const wallet = await Web3Wallet.init({
          core,
          metadata: {
            name: 'Your Wallet',
            description: 'Your wallet description',
            url: 'https://yourwallet.com',
            icons: ['https://yourwallet.com/logo.png'],
          },
        })

        // Set up event listeners
        wallet.on('session_proposal', setPendingProposal)
        wallet.on('session_request', setPendingRequest)
        wallet.on('session_delete', ({ topic }) => {
          setSessions(Object.values(wallet.getActiveSessions()))
        })

        // Load existing sessions
        setSessions(Object.values(wallet.getActiveSessions()))

        setWeb3wallet(wallet)
        setIsInitialized(true)
        initializedRef.current = true
      } catch (err) {
        console.error('Failed to initialize WalletConnect:', err)
      } finally {
        initializingRef.current = false
      }
    }

    initWalletConnect()
  }, [])

  const pair = useCallback(async (uri) => {
    if (!web3wallet) throw new Error('WalletConnect not initialized')
    await web3wallet.core.pairing.pair({ uri })
  }, [web3wallet])

  const approveSession = useCallback(async (proposal, accountAddress, chainId) => {
    if (!web3wallet) throw new Error('WalletConnect not initialized')

    const namespaces = {
      eip155: {
        chains: [`eip155:${chainId}`],
        methods: ['eth_sendTransaction', 'personal_sign', 'eth_signTypedData_v4'],
        events: ['chainChanged', 'accountsChanged'],
        accounts: [`eip155:${chainId}:${accountAddress}`],
      },
    }

    const session = await web3wallet.approveSession({
      id: proposal.id,
      namespaces,
    })

    setSessions(Object.values(web3wallet.getActiveSessions()))
    setPendingProposal(null)
    return session
  }, [web3wallet])

  const rejectSession = useCallback(async (proposal) => {
    if (!web3wallet) throw new Error('WalletConnect not initialized')

    await web3wallet.rejectSession({
      id: proposal.id,
      reason: { code: 5000, message: 'User rejected' },
    })

    setPendingProposal(null)
  }, [web3wallet])

  const respondSessionRequest = useCallback(async (topic, response) => {
    if (!web3wallet) throw new Error('WalletConnect not initialized')

    await web3wallet.respondSessionRequest({ topic, response })
    setPendingRequest(null)
  }, [web3wallet])

  const disconnectSession = useCallback(async (topic) => {
    if (!web3wallet) throw new Error('WalletConnect not initialized')

    await web3wallet.disconnectSession({
      topic,
      reason: { code: 6000, message: 'User disconnected' },
    })

    setSessions(Object.values(web3wallet.getActiveSessions()))
  }, [web3wallet])

  return (
    <WalletConnectContext.Provider value={{
      web3wallet,
      isInitialized,
      sessions,
      pendingProposal,
      pendingRequest,
      pair,
      approveSession,
      rejectSession,
      respondSessionRequest,
      disconnectSession,
    }}>
      {children}
    </WalletConnectContext.Provider>
  )
}

This skill is based on real-world production experience with WalletConnect v2 in the ΞTHΛURΛ wallet project.

Advanced Topics

Multi-Chain Support

Supporting Multiple Chains Simultaneously:

// TypeScript example with multi-chain support
const approveMultiChainSession = async (
  proposal: SessionProposal,
  accounts: { chainId: number; address: string }[]
) => {
  const { id, params } = proposal

  // Build multi-chain namespaces
  const chains = accounts.map(acc => `eip155:${acc.chainId}`)
  const accountsFormatted = accounts.map(
    acc => `eip155:${acc.chainId}:${acc.address}`
  )

  const namespaces = {
    eip155: {
      chains,
      methods: [
        'eth_sendTransaction',
        'eth_signTransaction',
        'personal_sign',
        'eth_signTypedData_v4',
      ],
      events: ['chainChanged', 'accountsChanged'],
      accounts: accountsFormatted,
    },
  }

  const session = await web3wallet.approveSession({
    id,
    namespaces,
  })

  return session
}

// Example: Approve with Ethereum Mainnet + Polygon
await approveMultiChainSession(proposal, [
  { chainId: 1, address: '0xYourAddress' },      // Ethereum Mainnet
  { chainId: 137, address: '0xYourAddress' },    // Polygon
  { chainId: 11155111, address: '0xYourAddress' } // Sepolia Testnet
])

Switching Chains During Active Session:

const updateSessionChains = async (
  topic: string,
  newChains: { chainId: number; address: string }[]
) => {
  const session = web3wallet.getActiveSessions()[topic]

  if (!session) {
    throw new Error('Session not found')
  }

  const chains = newChains.map(c => `eip155:${c.chainId}`)
  const accounts = newChains.map(c => `eip155:${c.chainId}:${c.address}`)

  const updatedNamespaces = {
    ...session.namespaces,
    eip155: {
      ...session.namespaces.eip155,
      chains,
      accounts,
    },
  }

  await web3wallet.updateSession({
    topic,
    namespaces: updatedNamespaces,
  })

  // Emit chainChanged event to dApp
  await web3wallet.emitSessionEvent({
    topic,
    event: {
      name: 'chainChanged',
      data: newChains[0].chainId,
    },
    chainId: `eip155:${newChains[0].chainId}`,
  })
}

Error Handling & Debugging

Comprehensive Error Handling

URI Validation and Pairing Errors:

const pairWithValidation = async (uri: string): Promise<void> => {
  try {
    // Validate URI format
    if (!uri || !uri.startsWith('wc:')) {
      throw new Error('Invalid WalletConnect URI format. Must start with "wc:"')
    }

    // Check if URI has already been used
    const pairings = web3wallet.core.pairing.getPairings()
    const existingPairing = pairings.find(p => p.topic === extractTopicFromUri(uri))
    if (existingPairing) {
      throw new Error('This pairing URI has already been used')
    }

    // Attempt pairing with timeout
    const pairingPromise = web3wallet.core.pairing.pair({ uri })
    const timeoutPromise = new Promise((_, reject) =>
      setTimeout(() => reject(new Error('Pairing timeout after 30s')), 30000)
    )

    await Promise.race([pairingPromise, timeoutPromise])
    console.log('Pairing successful')

  } catch (error) {
    if (error instanceof Error) {
      // Handle specific error types
      if (error.message.includes('No matching key')) {
        throw new Error('Invalid or expired pairing URI')
      } else if (error.message.includes('timeout')) {
        throw new Error('Connection timeout. Please check your internet connection')
      } else if (error.message.includes('Network')) {
        throw new Error('Network error. Please try again')
      }
    }
    throw error
  }
}

// Helper function
const extractTopicFromUri = (uri: string): string => {
  const params = new URLSearchParams(uri.split('?')[1])
  return params.get('topic') || ''
}

Session Request Error Handling:

const handleSessionRequestWithErrorHandling = async (
  request: SessionRequest
): Promise<void> => {
  const { topic, params, id } = request
  const { request: { method, params: methodParams } } = params

  try {
    // Validate session exists
    const session = web3wallet.getActiveSessions()[topic]
    if (!session) {
      throw new Error('Session not found or expired')
    }

    // Validate method is supported
    const supportedMethods = session.namespaces.eip155?.methods || []
    if (!supportedMethods.includes(method)) {
      throw new Error(`Method ${method} not supported in this session`)
    }

    // Add timeout for user approval
    const userApprovalPromise = getUserApproval(request)
    const timeoutPromise = new Promise((_, reject) =>
      setTimeout(() => reject(new Error('User approval timeout')), 5 * 60 * 1000) // 5 min
    )

    const approved = await Promise.race([userApprovalPromise, timeoutPromise])

    if (!approved) {
      throw new Error('User rejected the request')
    }

    // Execute request
    let result
    switch (method) {
      case 'eth_sendTransaction':
        result = await handleSendTransaction(methodParams[0])
        break
      case 'personal_sign':
        result = await handlePersonalSign(methodParams[0], methodParams[1])
        break
      default:
        throw new Error(`Unsupported method: ${method}`)
    }

    // Send success response
    await web3wallet.respondSessionRequest({
      topic,
      response: {
        id,
        jsonrpc: '2.0',
        result,
      },
    })

  } catch (error) {
    console.error('Session request error:', error)

    // Determine error code
    let errorCode = 5000 // Generic error
    let errorMessage = 'Request failed'

    if (error instanceof Error) {
      if (error.message.includes('rejected')) {
        errorCode = 5000
        errorMessage = 'User rejected the request'
      } else if (error.message.includes('timeout')) {
        errorCode = 5001
        errorMessage = 'Request timeout'
      } else if (error.message.includes('insufficient funds')) {
        errorCode = 5002
        errorMessage = 'Insufficient funds for transaction'
      } else if (error.message.includes('gas')) {
        errorCode = 5003
        errorMessage = 'Gas estimation failed'
      } else {
        errorMessage = error.message
      }
    }

    // Send error response
    await web3wallet.respondSessionRequest({
      topic,
      response: {
        id,
        jsonrpc: '2.0',
        error: {
          code: errorCode,
          message: errorMessage,
        },
      },
    })
  }
}

Common Error Codes

CodeMeaningSolution
5000User rejectedUser declined the request
5001User rejected methodsRequested methods not approved
5002User rejected chainsRequested chains not approved
5003User rejected eventsRequested events not approved
6000User disconnectedUser terminated the session
1000Invalid requestMalformed request parameters
1001Method not foundUnsupported method
1002Invalid paramsInvalid method parameters

Debugging Tips

Enable Debug Logging:

// Set environment variable for detailed logs
localStorage.setItem('debug', '@walletconnect/*')

// Or in your app initialization
if (process.env.NODE_ENV === 'development') {
  localStorage.setItem('debug', '@walletconnect/*')
}

Inspect IndexedDB for Session Data:

// Open browser DevTools > Application > IndexedDB > wc@2:*
// Look for:
// - wc@2:core:pairing - Active pairings
// - wc@2:client:session - Active sessions
// - wc@2:core:messages - Message queue

// Programmatically inspect
const inspectStorage = async () => {
  const pairings = web3wallet.core.pairing.getPairings()
  const sessions = web3wallet.getActiveSessions()

  console.log('Pairings:', pairings)
  console.log('Sessions:', sessions)
  console.log('Pending requests:', web3wallet.getPendingSessionRequests())
}

Network Inspection:

// Monitor WebSocket connection
web3wallet.core.relayer.on('relayer_connect', () => {
  console.log('✅ Connected to WalletConnect relay')
})

web3wallet.core.relayer.on('relayer_disconnect', () => {
  console.log('❌ Disconnected from WalletConnect relay')
})

web3wallet.core.relayer.on('relayer_error', (error) => {
  console.error('Relay error:', error)
})

// Check connection status
const isConnected = web3wallet.core.relayer.connected
console.log('Relay connected:', isConnected)

Common Console Errors and Solutions:

  1. "No matching key. pairing topic doesn't exist"

    • Cause: URI has already been used or is invalid
    • Solution: Generate a new pairing URI from the dApp
  2. "Missing or invalid. session topic doesn't exist"

    • Cause: Session has expired or been deleted
    • Solution: Re-establish connection with dApp
  3. "Unsupported chains"

    • Cause: Wallet doesn't support requested chains
    • Solution: Add chain support or reject with clear message
  4. "Invalid project ID"

  5. "WebSocket connection failed"

    • Cause: Network issues or firewall blocking WebSocket
    • Solution: Check internet connection and firewall settings

Testing

Unit Testing with Jest

Mock Setup:

// __mocks__/@walletconnect/web3wallet.ts
export const mockWeb3Wallet = {
  init: jest.fn(),
  on: jest.fn(),
  off: jest.fn(),
  removeAllListeners: jest.fn(),
  approveSession: jest.fn(),
  rejectSession: jest.fn(),
  respondSessionRequest: jest.fn(),
  disconnectSession: jest.fn(),
  getActiveSessions: jest.fn(() => ({})),
  getPendingSessionRequests: jest.fn(() => []),
  updateSession: jest.fn(),
  emitSessionEvent: jest.fn(),
  core: {
    pairing: {
      pair: jest.fn(),
      getPairings: jest.fn(() => []),
    },
    relayer: {
      on: jest.fn(),
      connected: true,
    },
  },
}

export const Web3Wallet = {
  init: jest.fn(() => Promise.resolve(mockWeb3Wallet)),
}

export const Core = jest.fn()

Test Examples:

import { render, waitFor, fireEvent } from '@testing-library/react'
import { WalletConnectProvider, useWalletConnect } from './WalletConnectContext'
import { mockWeb3Wallet } from './__mocks__/@walletconnect/web3wallet'

describe('WalletConnect Integration', () => {
  beforeEach(() => {
    jest.clearAllMocks()
  })

  test('initializes WalletConnect on mount', async () => {
    const { result } = renderHook(() => useWalletConnect(), {
      wrapper: WalletConnectProvider,
    })

    await waitFor(() => {
      expect(result.current.isInitialized).toBe(true)
    })

    expect(Web3Wallet.init).toHaveBeenCalledWith({
      core: expect.any(Object),
      metadata: expect.objectContaining({
        name: expect.any(String),
        description: expect.any(String),
      }),
    })
  })

  test('handles session proposal correctly', async () => {
    const mockProposal = {
      id: 1,
      params: {
        proposer: {
          metadata: {
            name: 'Test dApp',
            description: 'Test Description',
            url: 'https://test.com',
            icons: ['https://test.com/icon.png'],
          },
        },
        requiredNamespaces: {
          eip155: {
            chains: ['eip155:1'],
            methods: ['eth_sendTransaction'],
            events: ['chainChanged'],
          },
        },
      },
    }

    const { result } = renderHook(() => useWalletConnect(), {
      wrapper: WalletConnectProvider,
    })

    await waitFor(() => expect(result.current.isInitialized).toBe(true))

    // Simulate session proposal event
    const proposalHandler = mockWeb3Wallet.on.mock.calls.find(
      call => call[0] === 'session_proposal'
    )[1]

    proposalHandler(mockProposal)

    await waitFor(() => {
      expect(result.current.pendingProposal).toEqual(mockProposal)
    })
  })

  test('approves session with correct namespaces', async () => {
    const mockProposal = { id: 1, params: {} }
    const accountAddress = '0x1234567890123456789012345678901234567890'
    const chainId = 1

    mockWeb3Wallet.approveSession.mockResolvedValue({
      topic: 'test-topic',
      acknowledged: Promise.resolve(),
    })

    const { result } = renderHook(() => useWalletConnect(), {
      wrapper: WalletConnectProvider,
    })

    await waitFor(() => expect(result.current.isInitialized).toBe(true))

    await result.current.approveSession(mockProposal, accountAddress, chainId)

    expect(mockWeb3Wallet.approveSession).toHaveBeenCalledWith({
      id: 1,
      namespaces: {
        eip155: {
          chains: ['eip155:1'],
          methods: expect.arrayContaining(['eth_sendTransaction', 'personal_sign']),
          events: expect.arrayContaining(['chainChanged', 'accountsChanged']),
          accounts: [`eip155:1:${accountAddress}`],
        },
      },
    })
  })

  test('handles pairing errors gracefully', async () => {
    const invalidUri = 'invalid-uri'

    mockWeb3Wallet.core.pairing.pair.mockRejectedValue(
      new Error('Invalid URI format')
    )

    const { result } = renderHook(() => useWalletConnect(), {
      wrapper: WalletConnectProvider,
    })

    await waitFor(() => expect(result.current.isInitialized).toBe(true))

    await expect(result.current.pair(invalidUri)).rejects.toThrow('Invalid URI format')
  })
})

Integration Testing

E2E Test with Playwright:

import { test, expect } from '@playwright/test'

test.describe('WalletConnect Integration', () => {
  test('should connect to dApp and approve session', async ({ page, context }) => {
    // Start wallet app
    await page.goto('http://localhost:3000')

    // Wait for WalletConnect to initialize
    await page.waitForSelector('[data-testid="wc-initialized"]')

    // Open new page for dApp
    const dAppPage = await context.newPage()
    await dAppPage.goto('https://react-app.walletconnect.com/')

    // Click connect button on dApp
    await dAppPage.click('button:has-text("Connect")')

    // Get WalletConnect URI
    const uri = await dAppPage.locator('[data-testid="wc-uri"]').textContent()

    // Paste URI in wallet
    await page.fill('[data-testid="wc-uri-input"]', uri)
    await page.click('[data-testid="wc-pair-button"]')

    // Wait for session proposal
    await page.waitForSelector('[data-testid="session-proposal-modal"]')

    // Verify dApp details
    const dAppName = await page.locator('[data-testid="dapp-name"]').textContent()
    expect(dAppName).toBe('React App')

    // Approve session
    await page.click('[data-testid="approve-session-button"]')

    // Verify connection on dApp
    await dAppPage.waitForSelector('[data-testid="connected"]', { timeout: 10000 })
    const connectedAddress = await dAppPage.locator('[data-testid="address"]').textContent()
    expect(connectedAddress).toMatch(/^0x[a-fA-F0-9]{40}$/)
  })

  test('should handle transaction request', async ({ page }) => {
    // Assume already connected from previous test

    // Trigger transaction from dApp
    // ... (dApp interaction)

    // Wait for transaction request in wallet
    await page.waitForSelector('[data-testid="transaction-request-modal"]')

    // Verify transaction details
    const recipient = await page.locator('[data-testid="tx-to"]').textContent()
    const value = await page.locator('[data-testid="tx-value"]').textContent()

    expect(recipient).toMatch(/^0x[a-fA-F0-9]{40}$/)
    expect(value).toBeTruthy()

    // Approve transaction
    await page.click('[data-testid="approve-tx-button"]')

    // Wait for confirmation
    await page.waitForSelector('[data-testid="tx-confirmed"]')
  })
})

Manual Testing Checklist

Basic Functionality:

  • WalletConnect initializes successfully
  • Can pair with dApp using QR code
  • Can pair with dApp using pasted URI
  • Session proposal modal displays correct dApp information
  • Can approve session proposal
  • Can reject session proposal
  • Active sessions list shows connected dApps
  • Can disconnect session from wallet
  • Sessions persist after page reload

Request Handling:

  • eth_sendTransaction request displays correctly
  • personal_sign request displays correctly
  • eth_signTypedData_v4 request displays correctly
  • Can approve transaction requests
  • Can reject transaction requests
  • Transaction confirmation shows in dApp
  • Error messages display for failed transactions

Multi-Chain:

  • Can approve session with multiple chains
  • Can switch chains during active session
  • Requests route to correct chain
  • Chain switching emits correct events

Error Handling:

  • Invalid URI shows error message
  • Expired URI shows error message
  • Network errors handled gracefully
  • Timeout errors handled gracefully
  • User rejection sends correct error to dApp

Performance:

  • Initialization completes within 3 seconds
  • Pairing completes within 5 seconds
  • Session approval completes within 2 seconds
  • No memory leaks after multiple connections
  • UI remains responsive during operations

Migration from WalletConnect to Reown

Overview

WalletConnect Inc has rebranded to Reown. All @walletconnect/* packages are being deprecated in favor of @reown/* packages.

Timeline:

  • Limited Support Phase: September 17, 2024 - February 17, 2025
    • Only critical bug fixes and security updates
  • End-of-Life: February 18, 2025 onwards
    • All support ceases for @walletconnect/* packages

Package Migration Map

Old PackageNew Package
@walletconnect/web3wallet@reown/walletkit
@walletconnect/coreIncluded in @reown/walletkit
@walletconnect/utilsIncluded in @reown/walletkit
@web3modal/*@reown/appkit-*

Step-by-Step Migration Guide

Step 1: Update Dependencies

# Remove old packages
npm uninstall @walletconnect/web3wallet @walletconnect/core @walletconnect/utils

# Install new package
npm install @reown/walletkit

Step 2: Update Imports

// Before
import { Web3Wallet } from '@walletconnect/web3wallet'
import { Core } from '@walletconnect/core'

// After
import { WalletKit } from '@reown/walletkit'

Step 3: Update Initialization

// Before
const core = new Core({ projectId })
const web3wallet = await Web3Wallet.init({
  core,
  metadata: { /* ... */ }
})

// After
const walletKit = await WalletKit.init({
  projectId,
  metadata: { /* ... */ }
})

Step 4: Update API Calls

Most API calls remain the same, but accessed through walletKit instead of web3wallet:

// Before
await web3wallet.approveSession({ /* ... */ })
await web3wallet.core.pairing.pair({ uri })

// After
await walletKit.approveSession({ /* ... */ })
await walletKit.core.pairing.pair({ uri })

Step 5: Test Thoroughly

Run your full test suite to ensure compatibility. Pay special attention to:

  • Session proposal handling
  • Request handling
  • Event listeners
  • Error handling

Migration Checklist

  • Update package.json dependencies
  • Update all imports
  • Update initialization code
  • Update API calls
  • Update type imports (if using TypeScript)
  • Run unit tests
  • Run integration tests
  • Test with real dApps
  • Update documentation
  • Deploy to staging
  • Monitor for errors
  • Deploy to production

Compatibility Notes

  • The Reown packages maintain backward compatibility with the WalletConnect v2 protocol
  • Existing sessions will continue to work after migration
  • No changes required on the dApp side
  • Project IDs from WalletConnect Cloud remain valid

Performance Optimization

Bundle Size Optimization

Tree Shaking:

// Import only what you need
import { WalletKit } from '@reown/walletkit'

// Avoid importing entire utils package
import { getSdkError } from '@walletconnect/utils' // ❌ Large bundle
import { getSdkError } from '@reown/walletkit/utils' // ✅ Tree-shakeable

Lazy Loading:

// Lazy load WalletConnect only when needed
const initWalletConnect = async () => {
  const { WalletKit } = await import('@reown/walletkit')
  return await WalletKit.init({ /* ... */ })
}

// Initialize on user action
button.addEventListener('click', async () => {
  const walletKit = await initWalletConnect()
})

Memory Management

Cleanup on Unmount:

useEffect(() => {
  const initWC = async () => {
    const walletKit = await WalletKit.init({ /* ... */ })
    setWalletKit(walletKit)
  }

  initWC()

  return () => {
    // Cleanup
    if (walletKit) {
      walletKit.removeAllListeners()
      // Disconnect all sessions if needed
      Object.keys(walletKit.getActiveSessions()).forEach(topic => {
        walletKit.disconnectSession({
          topic,
          reason: { code: 6000, message: 'App closed' }
        })
      })
    }
  }
}, [])

Performance Metrics

Expected Performance:

  • Initialization: < 2 seconds
  • Pairing: < 3 seconds
  • Session Approval: < 1 second
  • Request Handling: < 500ms (excluding user interaction)
  • Bundle Size: ~150KB (gzipped)
  • Memory Usage: ~10-20MB

Monitoring:

// Track initialization time
const startTime = performance.now()
const walletKit = await WalletKit.init({ /* ... */ })
const initTime = performance.now() - startTime
console.log(`WalletConnect initialized in ${initTime}ms`)

// Track memory usage
if (performance.memory) {
  console.log('Memory usage:', {
    used: (performance.memory.usedJSHeapSize / 1048576).toFixed(2) + ' MB',
    total: (performance.memory.totalJSHeapSize / 1048576).toFixed(2) + ' MB'
  })
}

Production File Structure

Recommended file organization for a production WalletConnect implementation:

src/
├── contexts/
│   └── WalletConnectContext.tsx          # Main context provider
├── hooks/
│   ├── useWalletConnect.ts               # Context hook
│   └── useWalletConnectRequest.ts        # Request handling hook
├── components/
│   ├── WalletConnect/
│   │   ├── SessionProposalModal.tsx      # Session approval UI
│   │   ├── SessionRequestModal.tsx       # Request approval UI
│   │   ├── ActiveSessionsList.tsx        # Connected dApps list
│   │   ├── PairingInput.tsx              # URI input component
│   │   └── TransactionPreview.tsx        # Transaction details display
├── utils/
│   ├── walletconnect.ts                  # Helper functions
│   ├── formatters.ts                     # Data formatting utilities
│   └── validators.ts                     # Validation functions
├── types/
│   └── walletconnect.ts                  # TypeScript type definitions
└── constants/
    └── walletconnect.ts                  # Constants and config

Example File Contents:

src/types/walletconnect.ts:

import type { SessionTypes, SignClientTypes } from '@walletconnect/types'

export type SessionProposal = SignClientTypes.EventArguments['session_proposal']
export type SessionRequest = SignClientTypes.EventArguments['session_request']
export type ActiveSession = SessionTypes.Struct

export interface WalletConnectContextValue {
  web3wallet: Web3Wallet | null
  isInitialized: boolean
  sessions: ActiveSession[]
  pendingProposal: SessionProposal | null
  pendingRequest: SessionRequest | null
  pair: (uri: string) => Promise<void>
  approveSession: (proposal: SessionProposal, address: string, chainId: number) => Promise<void>
  rejectSession: (proposal: SessionProposal) => Promise<void>
  disconnectSession: (topic: string) => Promise<void>
}

src/constants/walletconnect.ts:

export const WALLETCONNECT_PROJECT_ID = import.meta.env.VITE_WALLETCONNECT_PROJECT_ID

export const SUPPORTED_CHAINS = [1, 137, 11155111] // Mainnet, Polygon, Sepolia

export const SUPPORTED_METHODS = [
  'eth_sendTransaction',
  'eth_signTransaction',
  'personal_sign',
  'eth_signTypedData_v4',
]

export const SUPPORTED_EVENTS = ['chainChanged', 'accountsChanged']

export const ERROR_CODES = {
  USER_REJECTED: 5000,
  UNAUTHORIZED_METHOD: 5001,
  UNAUTHORIZED_CHAIN: 5002,
  USER_DISCONNECTED: 6000,
} as const

FAQ

General Questions

Q: What's the difference between WalletConnect v1 and v2?

A: WalletConnect v2 is a complete rewrite with:

  • Multi-chain support out of the box
  • Better performance and reliability
  • Improved security
  • Session persistence
  • Support for multiple simultaneous sessions
  • v1 is deprecated and no longer supported

Q: Do I need a project ID?

A: Yes, you must obtain a free project ID from WalletConnect Cloud (now Reown Cloud). This is required for the relay server connection.

Q: Can I use WalletConnect without a backend?

A: Yes, WalletConnect is entirely client-side. The relay server (provided by WalletConnect/Reown) handles message passing between wallet and dApp.

Q: Is WalletConnect free?

A: Yes, WalletConnect is free for most use cases. There are usage limits on the free tier, but they're generous for most applications.

Technical Questions

Q: Why use web3wallet.core.pairing.pair() instead of web3wallet.pair()?

A: Using core.pairing.pair() provides better event handling and is the recommended approach. The web3wallet.pair() method may not trigger events reliably in all cases.

Q: How long do sessions last?

A: Sessions typically last 7 days by default. They can be extended before expiration using web3wallet.extendSession().

Q: Can I have multiple active sessions?

A: Yes, WalletConnect v2 supports multiple simultaneous sessions with different dApps.

Q: How do I handle session expiration?

A: Monitor session expiry timestamps and either:

  1. Extend sessions before they expire using extendSession()
  2. Handle session_delete events and prompt users to reconnect

Q: What happens if the user closes the app during a request?

A: The request will timeout on the dApp side. Implement proper cleanup in your app's unmount/close handlers to send rejection responses for pending requests.

Q: Can I customize the supported methods per session?

A: Yes, you define supported methods when approving a session. Only include methods your wallet actually supports.

Q: How do I handle gas estimation for transactions?

A: Estimate gas before showing the transaction to the user:

const estimatedGas = await provider.estimateGas(transaction)
const gasPrice = await provider.getGasPrice()
const totalCost = estimatedGas.mul(gasPrice).add(transaction.value || 0)

Smart Contract Wallet Questions

Q: Does WalletConnect work with smart contract wallets (ERC-4337)?

A: Yes, but you need to:

  1. Build UserOperations from transaction requests
  2. Sign with the wallet owner key
  3. Submit to a bundler
  4. Return the transaction hash to the dApp

Q: How do I implement EIP-1271 signature validation?

A: For smart contract wallets, implement the isValidSignature function on-chain and return signatures that your contract can verify.

Q: Can I use passkeys with WalletConnect?

A: Yes, passkeys (WebAuthn) can be used as the signing mechanism for smart contract wallets. See the ERC-4337 section for implementation details.

Debugging Questions

Q: Why isn't my session proposal showing up?

A: Common causes:

  1. Event listeners not set up before pairing
  2. Using web3wallet.pair() instead of web3wallet.core.pairing.pair()
  3. Invalid or expired URI
  4. Network connectivity issues

Q: How do I debug WebSocket connection issues?

A: Enable debug logging and monitor relay events:

localStorage.setItem('debug', '@walletconnect/*')

web3wallet.core.relayer.on('relayer_connect', () => console.log('Connected'))
web3wallet.core.relayer.on('relayer_disconnect', () => console.log('Disconnected'))
web3wallet.core.relayer.on('relayer_error', (error) => console.error(error))

Q: Where is session data stored?

A: Sessions are stored in IndexedDB under wc@2:* databases. You can inspect this in browser DevTools > Application > IndexedDB.

Migration Questions

Q: Should I migrate to Reown packages now?

A: Yes, it's recommended to migrate before February 2025 when WalletConnect packages reach end-of-life.

Q: Will my existing sessions break after migration?

A: No, existing sessions will continue to work. The Reown packages maintain protocol compatibility.

Q: Do dApps need to update when I migrate?

A: No, the migration is wallet-side only. dApps don't need any changes.

Q: Can I use both old and new packages during migration?

A: Not recommended. Choose one and stick with it to avoid conflicts.

API Reference

Core Methods

Web3Wallet.init(options)

Initialize the Web3Wallet instance.

Parameters:

  • core: Core instance
  • metadata: Wallet metadata object
    • name: string
    • description: string
    • url: string
    • icons: string[]

Returns: Promise<Web3Wallet>

web3wallet.core.pairing.pair({ uri })

Pair with a dApp using WalletConnect URI.

Parameters:

  • uri: WalletConnect URI string (starts with wc:)

Returns: Promise<void>

web3wallet.approveSession({ id, namespaces })

Approve a session proposal.

Parameters:

  • id: Proposal ID (number)
  • namespaces: Namespace configuration object

Returns: Promise<SessionTypes.Struct>

web3wallet.rejectSession({ id, reason })

Reject a session proposal.

Parameters:

  • id: Proposal ID (number)
  • reason: Rejection reason object
    • code: number
    • message: string

Returns: Promise<void>

web3wallet.respondSessionRequest({ topic, response })

Respond to a session request.

Parameters:

  • topic: Session topic (string)
  • response: JSON-RPC response object
    • id: number
    • jsonrpc: '2.0'
    • result?: any
    • error?: { code: number, message: string }

Returns: Promise<void>

web3wallet.disconnectSession({ topic, reason })

Disconnect a session.

Parameters:

  • topic: Session topic (string)
  • reason: Disconnection reason object
    • code: number
    • message: string

Returns: Promise<void>

web3wallet.getActiveSessions()

Get all active sessions.

Returns: Record<string, SessionTypes.Struct>

web3wallet.updateSession({ topic, namespaces })

Update session namespaces (chains/accounts).

Parameters:

  • topic: Session topic (string)
  • namespaces: Updated namespace configuration

Returns: Promise<void>

web3wallet.emitSessionEvent({ topic, event, chainId })

Emit an event to the dApp.

Parameters:

  • topic: Session topic (string)
  • event: Event object
    • name: string (e.g., 'chainChanged')
    • data: any
  • chainId: Chain ID in CAIP-2 format (e.g., 'eip155:1')

Returns: Promise<void>

Events

  • session_proposal: Fired when dApp requests connection
  • session_request: Fired when dApp requests transaction/signature
  • session_delete: Fired when session is terminated
  • session_update: Fired when session is updated

Source

git clone https://github.com/hadv/claude-skill-set/blob/main/walletconnect/SKILL.mdView on GitHub

Overview

Expert knowledge for implementing WalletConnect v2 (Sign protocol) in web apps, enabling wallets to connect to dApps. Covers the complete lifecycle from initialization through session management and request handling, with migration guidance to Reown packages.

How This Skill Works

The integration uses Web3Wallet on the wallet side and Core as the protocol layer to manage pairing, sessions, and Sign protocol requests. Start by initializing Core with your projectId, then create and configure Web3Wallet with wallet metadata, listen for session_proposal events, approve sessions with correctly defined namespaces, and finally pair with the dApp URI.

When to Use It

  • You need a wallet to connect to dApps using WalletConnect v2 or the Sign protocol
  • You want to implement session proposal, user approval, and request handling flows
  • You are migrating from legacy WalletConnect v2 packages to Reown packages
  • You require multi chain support via namespaces across Ethereum mainnet and testnets
  • You want robust session lifecycle management including expiration and reconnection

Quick Start

  1. Step 1: Install packages (legacy or Reown) like npm install @walletconnect/web3wallet @walletconnect/core @walletconnect/utils or npm install @reown/walletkit
  2. Step 2: Initialize Core with your projectId and create Web3Wallet with metadata (name, description, url, icons)
  3. Step 3: Listen for session_proposal, approve with proper namespaces, and pair with the dApp URI

Best Practices

  • Define precise namespaces with correct chains, methods, and events (eg eip155 with eth methods and chain events)
  • Keep Core projectId and metadata consistent and secure, and support both legacy and Reown packages during transition
  • Implement clear session proposal UI and user flow to approve or reject via approved namespaces
  • Handle session lifecycle events, including expiration, reconnection, and disconnections gracefully
  • Test thoroughly across networks and package versions using the migration guidance to Reown

Example Use Cases

  • A wallet app initializes Core and Web3Wallet, handles session_proposal events, and approves sessions with Ethereum mainnet namespaces
  • A dApp pairs with a wallet using a wc URI and walletkit integration to establish a session for signing
  • A project migrates from WalletConnect v2 packages to Reown w/ @reown/walletkit following the migration guide
  • A multi-chain wallet config uses eip155 namespaces to support Ethereum mainnet and testnets with correct accounts
  • An app implements session expiration handling and reconnection to maintain a seamless user experience

Frequently Asked Questions

Add this skill to your agents
Sponsor this space

Reach thousands of developers