Skip to Content
RecipesWalletConnect

WalletConnect with Reown WalletKit

WaaP provides 1-click wallet onboarding via email, phone or social login. 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, phone, social)
  • 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 signer
  • Manages active sessions (view, disconnect)

Key Components

  • WaaP: Secure, non-custodial signer with 1-click onboarding (email, phone, social)
  • 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 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, phone, or social)
  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 signer handles the cryptography
  6. The result is sent back to the dApp via the WalletConnect relay

Security Considerations

  • Signing advantage: The signing key is held in a TEE and never reaches the browser, so a compromised device has no key to steal. And every request is still authorized by the Policy Engine before it is signed
  • 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.

Last updated on