Skip to Content
OverviewRecipesWalletConnect Integration

WalletConnect with Reown WalletKit

WaaP provides 1-click wallet onboarding via email, social login, or face ID. Reown WalletKit  makes that wallet universally connectable. Together, users get frictionless signup and access to 85,000+ dApps via the WalletConnect protocol — no browser extension, no seed phrase, no per-dApp integration needed.

What are we cooking?

A Next.js application based on the WaaP Wagmi Starter  that:

  • Logs users in via WaaP (email, social, face ID)
  • Initializes Reown WalletKit to act as a WalletConnect-compatible wallet
  • Lets users scan or paste a WalletConnect URI from any dApp
  • Handles session proposals — declaring supported chains (Ethereum, Base, Arbitrum)
  • Routes signing requests (personal_sign, eth_sendTransaction, eth_signTypedData) through WaaP’s 2PC signing
  • Manages active sessions (view, disconnect)

Key Components

  • WaaP - Secure, non-custodial signer with 1-click onboarding (email/social/face ID)
  • Reown WalletKit (@reown/walletkit) - WalletConnect v2 protocol implementation for wallets
  • WalletConnect Core (@walletconnect/core) - Relay transport, pairing, and encryption
  • wagmi / viem - Type-safe blockchain interactions and WaaP wallet client

Project Setup

Get started with the WaaP Wagmi Starter

npx gitpick holonym-foundation/waap-examples/tree/main/waap-wagmi-nextjs cd waap-wagmi-nextjs npm install

Install WalletKit Dependencies

npm install @reown/walletkit @walletconnect/core @walletconnect/utils

Configure Reown Project ID

You need a Reown Cloud project ID for the WalletConnect relay. Get one from cloud.reown.com .

Add it to .env:

NEXT_PUBLIC_REOWN_PROJECT_ID=your_reown_project_id

Core Functionality

Initialize WalletKit

Create a singleton module to initialize the WalletKit instance. This sets up the WalletConnect relay connection and declares your wallet’s metadata.

// src/lib/walletkit.ts import { Core } from '@walletconnect/core' import { WalletKit } from '@reown/walletkit' let walletKit: InstanceType<typeof WalletKit> | null = null export async function getWalletKit() { if (walletKit) return walletKit const core = new Core({ projectId: process.env.NEXT_PUBLIC_REOWN_PROJECT_ID, }) walletKit = await WalletKit.init({ core, metadata: { name: 'My WaaP Wallet', description: 'A WaaP-powered wallet with WalletConnect support', url: 'https://your-app.com', icons: ['https://your-app.com/icon.png'], }, }) return walletKit }

WalletKit Provider

Wrap your app with a provider that initializes WalletKit alongside WaaP and exposes it via React context.

// src/components/WalletKitProvider.tsx import { createContext, useContext, useEffect, useState, useCallback } from 'react' import { useAccount, useWalletClient } from 'wagmi' import { WalletKit } from '@reown/walletkit' import { getWalletKit } from '../lib/walletkit' type SessionRequest = { id: number topic: string params: { request: { method: string; params: any[] } chainId: string } } type WalletKitContextType = { walletKit: InstanceType<typeof WalletKit> | null sessions: any[] pendingRequest: SessionRequest | null setPendingRequest: (req: SessionRequest | null) => void } const WalletKitContext = createContext<WalletKitContextType>({ walletKit: null, sessions: [], pendingRequest: null, setPendingRequest: () => {}, }) export function useWalletKit() { return useContext(WalletKitContext) } export function WalletKitProvider({ children }: { children: React.ReactNode }) { const [kit, setKit] = useState<InstanceType<typeof WalletKit> | null>(null) const [sessions, setSessions] = useState<any[]>([]) const [pendingRequest, setPendingRequest] = useState<SessionRequest | null>(null) const { address } = useAccount() useEffect(() => { getWalletKit().then((wk) => { setKit(wk) setSessions(Object.values(wk.getActiveSessions())) }) }, []) // Listen for session requests useEffect(() => { if (!kit) return const onSessionRequest = (event: SessionRequest) => { setPendingRequest(event) } const onSessionDelete = () => { setSessions(Object.values(kit.getActiveSessions())) } kit.on('session_request', onSessionRequest) kit.on('session_delete', onSessionDelete) return () => { kit.off('session_request', onSessionRequest) kit.off('session_delete', onSessionDelete) } }, [kit]) return ( <WalletKitContext.Provider value={{ walletKit: kit, sessions, pendingRequest, setPendingRequest }}> {children} </WalletKitContext.Provider> ) }

Add the provider to your app layout alongside the existing WaaP/wagmi providers:

// In your app layout or _app.tsx import { WalletKitProvider } from '../components/WalletKitProvider' // Nest inside your existing WagmiProvider <WagmiProvider config={config}> <QueryClientProvider client={queryClient}> <WalletKitProvider> {children} </WalletKitProvider> </QueryClientProvider> </WagmiProvider>

Pair with a dApp

To connect to a dApp, the user pastes a WalletConnect URI (the string from a dApp’s QR code or “Copy to clipboard” button) and calls walletKit.pair().

// src/components/PairWithDapp.tsx import { useState } from 'react' import { useWalletKit } from './WalletKitProvider' export function PairWithDapp() { const { walletKit } = useWalletKit() const [uri, setUri] = useState('') const [isPairing, setIsPairing] = useState(false) const [error, setError] = useState('') const handlePair = async () => { if (!walletKit || !uri) return setIsPairing(true) setError('') try { await walletKit.pair({ uri }) setUri('') } catch (err) { setError(err instanceof Error ? err.message : 'Failed to pair') } finally { setIsPairing(false) } } return ( <div> <h3>Connect to a dApp</h3> <p>Paste a WalletConnect URI from any dApp:</p> <input type="text" value={uri} onChange={(e) => setUri(e.target.value)} placeholder="wc:a]281..." style={{ width: '100%', padding: '8px', marginBottom: '8px' }} /> <button onClick={handlePair} disabled={isPairing || !uri}> {isPairing ? 'Connecting...' : 'Connect'} </button> {error && <p style={{ color: 'red' }}>{error}</p>} </div> ) }

Note: For a production app, you can add QR code scanning with a library like html5-qrcode to let users scan dApp QR codes directly from their camera.

Handle Session Proposals

When a dApp initiates a connection, WalletKit emits a session_proposal event. You need to declare which chains, methods, and accounts your wallet supports, then approve or reject the proposal.

// src/components/SessionProposal.tsx import { useEffect, useState } from 'react' import { useAccount } from 'wagmi' import { buildApprovedNamespaces } from '@walletconnect/utils' import { useWalletKit } from './WalletKitProvider' const SUPPORTED_CHAINS = ['eip155:1', 'eip155:8453', 'eip155:42161'] // Ethereum, Base, Arbitrum const SUPPORTED_METHODS = [ 'eth_sendTransaction', 'personal_sign', 'eth_signTypedData', 'eth_signTypedData_v4', ] const SUPPORTED_EVENTS = ['accountsChanged', 'chainChanged'] export function SessionProposal() { const { walletKit } = useWalletKit() const { address } = useAccount() const [proposal, setProposal] = useState<any>(null) useEffect(() => { if (!walletKit) return const onProposal = (proposalEvent: any) => { setProposal(proposalEvent) } walletKit.on('session_proposal', onProposal) return () => { walletKit.off('session_proposal', onProposal) } }, [walletKit]) const handleApprove = async () => { if (!walletKit || !proposal || !address) return const { id, params } = proposal const approvedNamespaces = buildApprovedNamespaces({ proposal: params, supportedNamespaces: { eip155: { chains: SUPPORTED_CHAINS, methods: SUPPORTED_METHODS, events: SUPPORTED_EVENTS, accounts: SUPPORTED_CHAINS.map((chain) => `${chain}:${address}`), }, }, }) await walletKit.approveSession({ id, namespaces: approvedNamespaces }) setProposal(null) } const handleReject = async () => { if (!walletKit || !proposal) return await walletKit.rejectSession({ id: proposal.id, reason: { code: 5000, message: 'User rejected' }, }) setProposal(null) } if (!proposal) return null const { proposer } = proposal.params return ( <div style={{ padding: '16px', border: '1px solid #ccc', borderRadius: '8px' }}> <h3>Session Proposal</h3> <p><strong>{proposer.metadata.name}</strong> wants to connect</p> <p>{proposer.metadata.description}</p> <p>URL: {proposer.metadata.url}</p> <p>Chains: Ethereum, Base, Arbitrum</p> <div style={{ display: 'flex', gap: '8px', marginTop: '12px' }}> <button onClick={handleApprove}>Approve</button> <button onClick={handleReject}>Reject</button> </div> </div> ) }

Handle Session Requests

This is the key integration point — where dApp signing requests are routed through WaaP’s 2PC signer. When a connected dApp asks the user to sign a message or send a transaction, WalletKit emits a session_request event. You use the WaaP wallet client (via wagmi’s useWalletClient) to fulfill the request.

// src/components/SessionRequest.tsx import { useWalletClient } from 'wagmi' import { useWalletKit } from './WalletKitProvider' export function SessionRequest() { const { walletKit, pendingRequest, setPendingRequest } = useWalletKit() const { data: walletClient } = useWalletClient() if (!pendingRequest) return null const { id, topic, params } = pendingRequest const { request } = params const handleApprove = async () => { if (!walletKit || !walletClient) return try { let result: string switch (request.method) { case 'personal_sign': { const message = request.params[0] result = await walletClient.signMessage({ message: { raw: message }, }) break } case 'eth_sendTransaction': { const tx = request.params[0] result = await walletClient.sendTransaction({ to: tx.to, value: tx.value ? BigInt(tx.value) : undefined, data: tx.data, gas: tx.gas ? BigInt(tx.gas) : undefined, }) break } case 'eth_signTypedData': case 'eth_signTypedData_v4': { const typedData = JSON.parse(request.params[1]) result = await walletClient.signTypedData({ domain: typedData.domain, types: typedData.types, primaryType: typedData.primaryType, message: typedData.message, }) break } default: throw new Error(`Unsupported method: ${request.method}`) } await walletKit.respondSessionRequest({ topic, response: { id, jsonrpc: '2.0', result }, }) } catch (err) { await walletKit.respondSessionRequest({ topic, response: { id, jsonrpc: '2.0', error: { code: 5000, message: err instanceof Error ? err.message : 'Request failed' }, }, }) } setPendingRequest(null) } const handleReject = async () => { if (!walletKit) return await walletKit.respondSessionRequest({ topic, response: { id, jsonrpc: '2.0', error: { code: 5001, message: 'User rejected the request' }, }, }) setPendingRequest(null) } return ( <div style={{ padding: '16px', border: '1px solid #f0ad4e', borderRadius: '8px' }}> <h3>Signing Request</h3> <p><strong>Method:</strong> {request.method}</p> <pre style={{ maxHeight: '200px', overflow: 'auto', fontSize: '12px' }}> {JSON.stringify(request.params, null, 2)} </pre> <div style={{ display: 'flex', gap: '8px', marginTop: '12px' }}> <button onClick={handleApprove}>Approve</button> <button onClick={handleReject}>Reject</button> </div> </div> ) }

Session Management

Display active dApp sessions and let users disconnect them. This component also handles the session_delete event when a dApp disconnects from its side.

// src/components/ConnectedSessions.tsx import { useState, useEffect } from 'react' import { useWalletKit } from './WalletKitProvider' export function ConnectedSessions() { const { walletKit, sessions } = useWalletKit() const [activeSessions, setActiveSessions] = useState<any[]>(sessions) useEffect(() => { setActiveSessions(sessions) }, [sessions]) const refreshSessions = () => { if (!walletKit) return setActiveSessions(Object.values(walletKit.getActiveSessions())) } const handleDisconnect = async (topic: string) => { if (!walletKit) return await walletKit.disconnectSession({ topic, reason: { code: 6000, message: 'User disconnected' }, }) refreshSessions() } if (activeSessions.length === 0) { return <p>No active dApp connections.</p> } return ( <div> <h3>Connected dApps</h3> {activeSessions.map((session) => ( <div key={session.topic} style={{ padding: '12px', border: '1px solid #ddd', borderRadius: '8px', marginBottom: '8px', display: 'flex', justifyContent: 'space-between', alignItems: 'center', }} > <div> <strong>{session.peer.metadata.name}</strong> <p style={{ margin: '4px 0', fontSize: '14px', color: '#666' }}> {session.peer.metadata.url} </p> <p style={{ margin: 0, fontSize: '12px', color: '#999' }}> Chains: {session.namespaces?.eip155?.chains?.join(', ') || 'eip155'} </p> </div> <button onClick={() => handleDisconnect(session.topic)}>Disconnect</button> </div> ))} </div> ) }

Putting It All Together

Here is the full user flow wired up in a single page:

// src/app/page.tsx (or wherever your main page lives) import { useAccount } from 'wagmi' import { PairWithDapp } from '../components/PairWithDapp' import { SessionProposal } from '../components/SessionProposal' import { SessionRequest } from '../components/SessionRequest' import { ConnectedSessions } from '../components/ConnectedSessions' export default function Home() { const { address, isConnected } = useAccount() if (!isConnected) { return <p>Log in with WaaP to get started.</p> } return ( <div style={{ maxWidth: '600px', margin: '0 auto', padding: '24px' }}> <h1>WaaP + WalletConnect</h1> <p>Connected as: <code>{address}</code></p> {/* Pair with a new dApp */} <PairWithDapp /> {/* Session proposal modal */} <SessionProposal /> {/* Pending signing request */} <SessionRequest /> {/* Active sessions */} <ConnectedSessions /> </div> ) }

The flow:

  1. User logs in with WaaP (email, social, or face ID)
  2. User pastes a WalletConnect URI from any dApp (e.g., Uniswap, Aave, OpenSea)
  3. A session proposal appears — user approves the connection
  4. When the dApp requests a signature or transaction, the request appears in the app
  5. User approves — WaaP’s 2PC signer handles the cryptography
  6. The result is sent back to the dApp via the WalletConnect relay

Security Considerations

  • 2PC signing advantage: Unlike traditional wallets, the private key never exists in one place — even if the device is compromised, transactions require the TEE security share
  • Reown Verify API: WalletKit includes domain verification to protect users from phishing dApps — flag unverified or suspicious domains before approving sessions
  • Session scoping: Only approve the minimum required chains and methods; review session proposals carefully before accepting
  • Relay security: All WalletConnect relay messages are end-to-end encrypted; the relay server never sees plaintext

Conclusion

WaaP + WalletKit gives your embedded wallet universal dApp access with bank-grade security and zero-friction onboarding. One integration connects your WaaP wallet to the entire WalletConnect ecosystem — 85,000+ dApps, no browser extension required. Users sign up with their email and start interacting with Uniswap, Aave, or any WalletConnect-enabled dApp in seconds.