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 installInstall WalletKit Dependencies
npm install @reown/walletkit @walletconnect/core @walletconnect/utilsConfigure 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_idCore 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-qrcodeto 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:
- User logs in with WaaP (email, social, or face ID)
- User pastes a WalletConnect URI from any dApp (e.g., Uniswap, Aave, OpenSea)
- A session proposal appears — user approves the connection
- When the dApp requests a signature or transaction, the request appears in the app
- User approves — WaaP’s 2PC signer handles the cryptography
- 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.
Related
- Methods — Full WaaP SDK method reference
- Send Transactions — Transaction signing with WaaP
- Supported Chains — Multi-chain configuration
- Smart Accounts — Combine with Account Abstraction for gasless transactions