Smart Sessions
Smart Sessions enable users to interact with dApps without signing every transaction. By creating temporary, scoped permissions, you can provide a seamless user experience while maintaining security.
Overview
Smart Sessions use the Rhinestone Smart Sessions Module to provide:
- Temporary Permissions: Time-limited access without exposing main keys
- Scoped Access: Restrict sessions to specific contracts and functions
- Spending Limits: Control maximum value per transaction or period
- Automatic Revocation: Sessions expire automatically or can be revoked manually
Core Concepts
Session Key Lifecycle
- Creation: Generate temporary key pair for the session
- Permission Granting: Define scope, limits, and duration
- Active Usage: Session key signs transactions within constraints
- Expiration/Revocation: Session ends automatically or manually
Permission Structure
interface SessionPermission {
sessionKey: string // Public key for this session
target: string // Contract address to interact with
selector: string // Function selector (4-byte)
maxValue: bigint // Maximum ETH value per transaction
validUntil: number // Expiration timestamp
validAfter: number // Activation timestamp
maxUses?: number // Optional usage limit
}
Implementation
Smart Sessions Hook
hooks/useSmartSessions.ts
import { useState, useEffect } from 'react'
import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'
import { getModule } from '@rhinestone/module-sdk'
import { useStartale } from '../providers/StartaleAccountProvider'
export interface SmartSession {
id: string
sessionKey: string
permissions: SessionPermission[]
createdAt: number
expiresAt: number
isActive: boolean
usageCount: number
}
export function useSmartSessions() {
const { startaleAccount, startaleClient } = useStartale()
const [sessions, setSessions] = useState<SmartSession[]>([])
const [isModuleInstalled, setIsModuleInstalled] = useState(false)
useEffect(() => {
checkModuleInstallation()
loadExistingSessions()
}, [startaleAccount])
async function checkModuleInstallation() {
if (!startaleAccount) return
try {
// Check if smart sessions module is installed
// This would typically query the account's installed modules
console.log('Checking smart sessions module...')
setIsModuleInstalled(false) // For demo purposes
} catch (error) {
console.error('Error checking module:', error)
}
}
function loadExistingSessions() {
// In a real implementation, this would load from storage or chain
const savedSessions = localStorage.getItem('smartSessions')
if (savedSessions) {
setSessions(JSON.parse(savedSessions))
}
}
function createSessionKey(): { privateKey: string; address: string } {
const privateKey = generatePrivateKey()
const account = privateKeyToAccount(privateKey)
return {
privateKey,
address: account.address,
}
}
return {
sessions,
isModuleInstalled,
createSessionKey,
setSessions,
}
}
Smart Sessions Component
components/SmartSession.tsx
import { useState } from 'react'
import { encodeFunctionData, parseEther, parseUnits } from 'viem'
import { getInstallModule, getUninstallModule } from '@rhinestone/module-sdk'
import { useStartale } from '../providers/StartaleAccountProvider'
import { useSmartSessions, type SmartSession } from '../hooks/useSmartSessions'
type SessionStep = 'setup' | 'installed' | 'create' | 'manage'
export function SmartSessions() {
const { startaleClient } = useStartale()
const {
sessions,
isModuleInstalled,
createSessionKey,
setSessions
} = useSmartSessions()
const [currentStep, setCurrentStep] = useState<SessionStep>('setup')
const [isLoading, setIsLoading] = useState(false)
const step: SessionStep = isModuleInstalled
? sessions.length === 0 ? 'create' : 'manage'
: 'setup'
return (
<div className="smart-sessions">
<h3>Smart Sessions</h3>
<p>Create temporary permissions for seamless dApp interactions.</p>
{step === 'setup' && (
<SetupPhase
onInstall={installSmartSessions}
isLoading={isLoading}
/>
)}
{step === 'create' && (
<CreateSessionPhase
onCreateSession={createNewSession}
isLoading={isLoading}
/>
)}
{step === 'manage' && (
<ManageSessionsPhase
sessions={sessions}
onCreateNew={() => setCurrentStep('create')}
onRevokeSession={revokeSession}
onUninstall={uninstallSmartSessions}
isLoading={isLoading}
/>
)}
</div>
)
async function installSmartSessions() {
if (!startaleClient) return
setIsLoading(true)
try {
console.log('Installing smart sessions module...')
const installModule = getInstallModule({
module: 'smart-sessions',
initData: '0x', // No initialization data needed
type: 'executor',
})
const hash = await startaleClient.sendUserOperation({
calls: [installModule],
})
console.log('Smart sessions module installed:', hash)
alert('Smart sessions module installed successfully!')
} catch (error) {
console.error('Failed to install smart sessions module:', error)
alert(`Installation failed: ${(error as Error).message}`)
} finally {
setIsLoading(false)
}
}
async function uninstallSmartSessions() {
if (!startaleClient) return
const confirm = window.confirm('Remove smart sessions? All active sessions will be revoked.')
if (!confirm) return
setIsLoading(true)
try {
const uninstallModule = getUninstallModule({
module: 'smart-sessions',
type: 'executor',
})
const hash = await startaleClient.sendUserOperation({
calls: [uninstallModule],
})
console.log('Smart sessions module uninstalled:', hash)
setSessions([])
alert('Smart sessions module removed!')
} catch (error) {
console.error('Failed to uninstall module:', error)
alert(`Uninstallation failed: ${(error as Error).message}`)
} finally {
setIsLoading(false)
}
}
async function createNewSession(sessionConfig: SessionConfig) {
if (!startaleClient) return
setIsLoading(true)
try {
const { privateKey, address } = createSessionKey()
console.log('Creating smart session...', {
sessionKey: address,
config: sessionConfig,
})
// Create session permission data
const sessionData = encodeFunctionData({
abi: [
{
name: 'createSession',
type: 'function',
inputs: [
{ name: 'sessionKey', type: 'address' },
{ name: 'target', type: 'address' },
{ name: 'selector', type: 'bytes4' },
{ name: 'maxValue', type: 'uint256' },
{ name: 'validUntil', type: 'uint256' },
{ name: 'validAfter', type: 'uint256' },
],
outputs: [],
},
],
functionName: 'createSession',
args: [
address as `0x${string}`,
sessionConfig.target as `0x${string}`,
sessionConfig.selector as `0x${string}`,
parseEther(sessionConfig.maxValue),
BigInt(sessionConfig.validUntil),
BigInt(sessionConfig.validAfter),
],
})
// Submit session creation transaction
const hash = await startaleClient.sendUserOperation({
calls: [{
to: '0x...' as `0x${string}`, // Smart sessions module address
data: sessionData,
value: 0n,
}],
})
// Store session locally
const newSession: SmartSession = {
id: `session-${Date.now()}`,
sessionKey: address,
permissions: [{
sessionKey: address,
target: sessionConfig.target,
selector: sessionConfig.selector,
maxValue: parseEther(sessionConfig.maxValue),
validUntil: sessionConfig.validUntil,
validAfter: sessionConfig.validAfter,
}],
createdAt: Date.now(),
expiresAt: sessionConfig.validUntil * 1000,
isActive: true,
usageCount: 0,
}
const updatedSessions = [...sessions, newSession]
setSessions(updatedSessions)
localStorage.setItem('smartSessions', JSON.stringify(updatedSessions))
console.log('Session created:', hash)
alert('Smart session created successfully!')
} catch (error) {
console.error('Failed to create session:', error)
alert(`Session creation failed: ${(error as Error).message}`)
} finally {
setIsLoading(false)
}
}
async function revokeSession(sessionId: string) {
const session = sessions.find(s => s.id === sessionId)
if (!session || !startaleClient) return
const confirm = window.confirm('Revoke this session? It cannot be undone.')
if (!confirm) return
setIsLoading(true)
try {
console.log('Revoking session:', sessionId)
// In a real implementation, this would call the revoke function
// on the smart sessions module
// Update local state
const updatedSessions = sessions.map(s =>
s.id === sessionId ? { ...s, isActive: false } : s
)
setSessions(updatedSessions)
localStorage.setItem('smartSessions', JSON.stringify(updatedSessions))
alert('Session revoked successfully!')
} catch (error) {
console.error('Failed to revoke session:', error)
alert(`Revocation failed: ${(error as Error).message}`)
} finally {
setIsLoading(false)
}
}
}
// Setup Phase Component
function SetupPhase({
onInstall,
isLoading,
}: {
onInstall: () => void
isLoading: boolean
}) {
return (
<div className="setup-phase">
<h4>Install Smart Sessions Module</h4>
<p>Enable session-based interactions for seamless user experiences.</p>
<div className="features-list">
<h5>Features:</h5>
<ul>
<li>✅ Reduce signature friction</li>
<li>✅ Time-limited permissions</li>
<li>✅ Scoped contract access</li>
<li>✅ Spending limits</li>
<li>✅ Automatic expiration</li>
</ul>
</div>
<button
onClick={onInstall}
disabled={isLoading}
className="btn-primary"
>
{isLoading ? 'Installing...' : 'Install Smart Sessions'}
</button>
</div>
)
}
// Session Configuration Interface
interface SessionConfig {
target: string
functionName: string
selector: string
maxValue: string
duration: number
validAfter: number
validUntil: number
}
// Create Session Phase Component
function CreateSessionPhase({
onCreateSession,
isLoading,
}: {
onCreateSession: (config: SessionConfig) => void
isLoading: boolean
}) {
const [config, setConfig] = useState<Partial<SessionConfig>>({
target: '',
functionName: '',
maxValue: '0.1',
duration: 24, // hours
})
const presetConfigs = [
{
name: 'Counter Contract',
target: '0x6bcf154A6B80fDE9bd1556d39C9bCbB19B539Bd8',
functionName: 'increment',
selector: '0xd09de08a',
maxValue: '0',
},
{
name: 'ERC-20 Transfer',
target: '0x...',
functionName: 'transfer',
selector: '0xa9059cbb',
maxValue: '100',
},
{
name: 'NFT Mint',
target: '0x...',
functionName: 'mint',
selector: '0x40c10f19',
maxValue: '0.05',
},
]
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
if (!config.target || !config.functionName || !config.selector) {
alert('Please fill in all required fields')
return
}
const now = Math.floor(Date.now() / 1000)
const duration = (config.duration || 24) * 3600 // hours to seconds
const sessionConfig: SessionConfig = {
target: config.target!,
functionName: config.functionName!,
selector: config.selector!,
maxValue: config.maxValue || '0',
duration: config.duration || 24,
validAfter: now,
validUntil: now + duration,
}
onCreateSession(sessionConfig)
}
return (
<div className="create-session-phase">
<h4>Create New Session</h4>
<div className="preset-configs">
<h5>Quick Setup:</h5>
<div className="preset-buttons">
{presetConfigs.map((preset, index) => (
<button
key={index}
onClick={() => setConfig({
...config,
...preset,
})}
className="btn-secondary"
>
{preset.name}
</button>
))}
</div>
</div>
<form onSubmit={handleSubmit} className="session-form">
<div className="form-group">
<label>Target Contract Address *</label>
<input
type="text"
value={config.target || ''}
onChange={(e) => setConfig({ ...config, target: e.target.value })}
placeholder="0x..."
required
/>
</div>
<div className="form-group">
<label>Function Name *</label>
<input
type="text"
value={config.functionName || ''}
onChange={(e) => setConfig({ ...config, functionName: e.target.value })}
placeholder="increment, transfer, mint..."
required
/>
</div>
<div className="form-group">
<label>Function Selector *</label>
<input
type="text"
value={config.selector || ''}
onChange={(e) => setConfig({ ...config, selector: e.target.value })}
placeholder="0x..."
required
/>
<small>4-byte function signature hash</small>
</div>
<div className="form-group">
<label>Max Value Per Transaction (ETH)</label>
<input
type="number"
step="0.001"
min="0"
value={config.maxValue || ''}
onChange={(e) => setConfig({ ...config, maxValue: e.target.value })}
placeholder="0.1"
/>
</div>
<div className="form-group">
<label>Session Duration (hours)</label>
<select
value={config.duration || 24}
onChange={(e) => setConfig({ ...config, duration: Number(e.target.value) })}
>
<option value={1}>1 hour</option>
<option value={6}>6 hours</option>
<option value={24}>1 day</option>
<option value={168}>1 week</option>
<option value={720}>1 month</option>
</select>
</div>
<button
type="submit"
disabled={isLoading}
className="btn-primary"
>
{isLoading ? 'Creating...' : 'Create Session'}
</button>
</form>
</div>
)
}
// Manage Sessions Phase Component
function ManageSessionsPhase({
sessions,
onCreateNew,
onRevokeSession,
onUninstall,
isLoading,
}: {
sessions: SmartSession[]
onCreateNew: () => void
onRevokeSession: (sessionId: string) => void
onUninstall: () => void
isLoading: boolean
}) {
const activeSessions = sessions.filter(s => s.isActive && s.expiresAt > Date.now())
const expiredSessions = sessions.filter(s => !s.isActive || s.expiresAt <= Date.now())
return (
<div className="manage-sessions-phase">
<div className="sessions-header">
<h4>Active Sessions ({activeSessions.length})</h4>
<button onClick={onCreateNew} className="btn-primary">
Create New Session
</button>
</div>
{activeSessions.length === 0 && (
<div className="no-sessions">
<p>No active sessions. Create one to get started!</p>
</div>
)}
{activeSessions.map(session => (
<SessionCard
key={session.id}
session={session}
onRevoke={() => onRevokeSession(session.id)}
/>
))}
{expiredSessions.length > 0 && (
<div className="expired-sessions">
<h5>Expired/Revoked Sessions ({expiredSessions.length})</h5>
{expiredSessions.map(session => (
<SessionCard
key={session.id}
session={session}
onRevoke={() => {}}
disabled
/>
))}
</div>
)}
<div className="module-actions">
<button
onClick={onUninstall}
disabled={isLoading}
className="btn-danger"
>
{isLoading ? 'Removing...' : 'Uninstall Smart Sessions'}
</button>
</div>
</div>
)
}
// Session Card Component
function SessionCard({
session,
onRevoke,
disabled = false,
}: {
session: SmartSession
onRevoke: () => void
disabled?: boolean
}) {
const isExpired = session.expiresAt <= Date.now()
const timeRemaining = session.expiresAt - Date.now()
const formatTimeRemaining = (ms: number) => {
if (ms <= 0) return 'Expired'
const hours = Math.floor(ms / (1000 * 60 * 60))
const days = Math.floor(hours / 24)
if (days > 0) return `${days} days remaining`
return `${hours} hours remaining`
}
return (
<div className={`session-card ${disabled ? 'disabled' : ''}`}>
<div className="session-info">
<h5>Session {session.id.slice(-8)}</h5>
<p><strong>Target:</strong> {session.permissions[0]?.target}</p>
<p><strong>Max Value:</strong> {session.permissions[0]?.maxValue.toString()} ETH</p>
<p><strong>Usage:</strong> {session.usageCount} transactions</p>
<p><strong>Status:</strong>
<span className={`status ${isExpired ? 'expired' : 'active'}`}>
{formatTimeRemaining(timeRemaining)}
</span>
</p>
</div>
{!disabled && !isExpired && (
<div className="session-actions">
<button onClick={onRevoke} className="btn-danger">
Revoke
</button>
</div>
)}
</div>
)
}
Session Usage Example
Here's how to use a session key to execute transactions:
utils/sessionExecution.ts
import { createWalletClient, http } from 'viem'
import { privateKeyToAccount } from 'viem/accounts'
import { soneiumTestnet } from 'viem/chains'
export async function executeWithSession(
sessionPrivateKey: string,
targetContract: string,
callData: string
) {
try {
// Create session wallet client
const sessionAccount = privateKeyToAccount(sessionPrivateKey as `0x${string}`)
const sessionClient = createWalletClient({
account: sessionAccount,
chain: soneiumTestnet,
transport: http(),
})
console.log('Executing with session key:', sessionAccount.address)
// In a real implementation, this would go through the smart account
// with session validation, not direct execution
const hash = await sessionClient.sendTransaction({
to: targetContract as `0x${string}`,
data: callData as `0x${string}`,
value: 0n,
})
return hash
} catch (error) {
console.error('Session execution failed:', error)
throw error
}
}
Advanced Features
Batch Session Creation
Create multiple sessions at once:
utils/batchSessions.ts
export async function createBatchSessions(
client: StartaleAccountClient,
sessionConfigs: SessionConfig[]
) {
const calls = sessionConfigs.map(config => ({
to: SMART_SESSIONS_MODULE_ADDRESS,
data: encodeFunctionData({
abi: smartSessionsAbi,
functionName: 'createSession',
args: [
config.sessionKey,
config.target,
config.selector,
config.maxValue,
config.validUntil,
config.validAfter,
],
}),
value: 0n,
}))
return await client.sendUserOperation({ calls })
}
Session Analytics
Track session usage and performance:
utils/sessionAnalytics.ts
export interface SessionAnalytics {
totalSessions: number
activeSessions: number
totalTransactions: number
gassSaved: bigint
popularTargets: Array<{ address: string; count: number }>
}
export function analyzeSessionUsage(sessions: SmartSession[]): SessionAnalytics {
const activeSessions = sessions.filter(s => s.isActive && s.expiresAt > Date.now())
const totalTransactions = sessions.reduce((sum, s) => sum + s.usageCount, 0)
// Calculate popular targets
const targetCounts = new Map<string, number>()
sessions.forEach(session => {
session.permissions.forEach(permission => {
const current = targetCounts.get(permission.target) || 0
targetCounts.set(permission.target, current + 1)
})
})
const popularTargets = Array.from(targetCounts.entries())
.map(([address, count]) => ({ address, count }))
.sort((a, b) => b.count - a.count)
.slice(0, 5)
return {
totalSessions: sessions.length,
activeSessions: activeSessions.length,
totalTransactions,
gassSaved: BigInt(totalTransactions * 21000), // Rough estimate
popularTargets,
}
}
Security Considerations
Session Key Management
- Store session private keys securely (encrypted local storage)
- Never expose session keys in logs or network requests
- Implement proper key rotation for long-term sessions
Permission Scoping
- Use minimal permissions principle
- Set appropriate spending limits
- Choose reasonable expiration times
Monitoring & Alerts
- Track session usage for anomalies
- Implement spending alerts
- Monitor for unauthorized session creation
Integration Patterns
Gaming DApps
// Example: Game session for automated moves
const gameSession = {
target: GAME_CONTRACT,
selector: '0x...', // makeMove function
maxValue: 0n,
duration: 2 * 3600, // 2 hours
}
DeFi Applications
// Example: Trading session with limits
const tradingSession = {
target: DEX_CONTRACT,
selector: '0x...', // swap function
maxValue: parseEther('10'), // Max 10 ETH per tx
duration: 6 * 3600, // 6 hours
}
NFT Marketplaces
// Example: Bidding session
const biddingSession = {
target: MARKETPLACE_CONTRACT,
selector: '0x...', // placeBid function
maxValue: parseEther('5'), // Max 5 ETH per bid
duration: 24 * 3600, // 24 hours
}
Next Steps
- Experiment: Create sessions for different use cases
- Integrate: Add session support to your dApp
Troubleshooting
Session creation fails:
- Verify target contract address is valid
- Check function selector is correct (4 bytes)
- Ensure sufficient gas for transaction
Session execution rejected:
- Confirm session hasn't expired
- Verify transaction is within spending limits
- Check target contract and function match session permissions
High gas costs:
- Sessions require module installation (one-time cost)
- Batch session creation for efficiency
- Consider session duration vs creation frequency