This is a complete example application demonstrating the new Unified Wallet Context Architecture in a zkWasm Mini Rollup project. The new architecture provides optimal developer experience through a single hook that contains all wallet functionality.
The new architecture replaces complex hook combinations with a single useWalletContext hook:
-
useWalletContext(): Provides complete wallet functionality- Connection states:
isConnected,isL2Connected,address,chainId - Account info:
l1Account,l2Account(with all methods) - PID management:
playerIdarray automatically calculated - Actions:
connectL1,connectL2,disconnect - Zero dependencies required
- Connection states:
-
Advanced hooks (optional): For users who need granular control
useConnection(): Connection state onlyuseWalletActions(address, chainId): Wallet operations- Direct Redux access via
useSelector
// ✅ New Approach - Unified Context (Recommended)
const {
isConnected, isL2Connected, l1Account, l2Account, playerId,
address, chainId, connectL1, connectL2, disconnect
} = useWalletContext();
// 🔧 Advanced Approach - Split Hooks (Optional)
const { isConnected, address, chainId } = useConnection();
const { connectAndLoginL1, loginL2 } = useWalletActions(address, chainId);Run from the project root directory:
npm installCopy the example environment variables file:
cp example/env.example example/.envConfigure the following variables in example/.env:
# WalletConnect Project ID (Required)
REACT_APP_WALLETCONNECT_PROJECT_ID=your_project_id_here
# Target Chain ID (Required)
REACT_APP_CHAIN_ID=11155111
# Contract Addresses (Required)
REACT_APP_DEPOSIT_CONTRACT=0x1234567890123456789012345678901234567890
REACT_APP_TOKEN_CONTRACT=0x0987654321098765432109876543210987654321
# Service Address
REACT_APP_URL=https://rpc.test-nugget.zkwasm.aiThe architecture uses unified environment variable naming across all React project types:
- All projects use
REACT_APP_prefix - Works with CRA, Next.js, Vite, and custom builds - Automatic fallback - Falls back to global variables if environment variables are not available
- Runtime configuration - Supports setting configuration at runtime
Common Chain IDs:
1- Ethereum Mainnet11155111- Sepolia Testnet137- Polygon42161- Arbitrum One8453- Base10- Optimism56- BSC (Binance Smart Chain)31337- Localhost (Development Environment)
- Visit WalletConnect Cloud
- Create a new project
- Copy the Project ID to your environment variables
npm run dev:exampleThe application will start at http://localhost:5173.
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import {
useWalletContext,
type AccountState,
} from '../../src/index';
// Define Redux store's root state type
interface RootState {
account: AccountState;
}
function WalletExample() {
const dispatch = useDispatch();
// Unified context for optimal performance
const {
isConnected, isL2Connected, l1Account, l2Account, playerId,
address, chainId, connectL1, connectL2, disconnect
} = useWalletContext();
// Redux state
const { l1Account: reduxL1Account, l2Account: reduxL2Account, status } = useSelector((state: RootState) => state.account);
// Derived states
const isL1Connected = !!l1Account;
const isL2Connected = !!l2Account;
const isL1Connecting = status === 'LoadingL1';
const isL2Connecting = status === 'LoadingL2';
const isDepositing = status === 'Deposit';
const handleConnect = async () => {
try {
await connectL1(dispatch);
} catch (error) {
console.error('Connection failed:', error);
}
};
const handleL2Login = async () => {
try {
await connectL2(dispatch, "MyApp");
} catch (error) {
console.error('L2 login failed:', error);
}
};
const handleDeposit = async () => {
if (!isL1Connected || !isL2Connected) {
alert('Please complete L1 and L2 login first');
return;
}
try {
await connectL2(dispatch, {
tokenIndex: 0,
amount: 0.01,
l1account: l1Account,
l2account: l2Account
});
} catch (error) {
console.error('Deposit failed:', error);
}
};
const handleReset = async () => {
await disconnect(dispatch);
};
return (
<div>
{/* Connection Status Display */}
<div className="status-section">
<h3>Connection Status</h3>
<p>Connected: {isConnected ? 'Yes' : 'No'}</p>
<p>Address: {address || 'Not connected'}</p>
<p>Chain ID: {chainId || 'Unknown'}</p>
<p>Status: {status}</p>
</div>
{/* Action Buttons */}
<div className="actions-section">
{!isConnected ? (
<button onClick={handleConnect} disabled={isL1Connecting}>
{isL1Connecting ? 'Connecting...' : 'Connect Wallet & Login L1'}
</button>
) : (
<div>
{isL1Connected && !isL2Connected && (
<button onClick={handleL2Login} disabled={isL2Connecting}>
{isL2Connecting ? 'Logging in L2...' : 'Login L2'}
</button>
)}
{isL1Connected && isL2Connected && (
<button onClick={handleDeposit} disabled={isDepositing}>
{isDepositing ? 'Depositing...' : 'Deposit 0.01 ETH'}
</button>
)}
<button onClick={handleReset}>Reset</button>
</div>
)}
</div>
</div>
);
}import React from 'react';
import {
// RainbowKit components from SDK
ConnectButton,
useConnectModal,
// Unified context
useWalletContext,
} from '../../src/index';
function RainbowKitIntegration() {
const { openConnectModal } = useConnectModal();
const { isConnected, address, chainId } = useConnection();
const { connectAndLoginL1 } = useWalletActions(address, chainId);
return (
<div>
{/* Official RainbowKit ConnectButton */}
<ConnectButton />
{/* Custom Connect Modal */}
<button onClick={openConnectModal}>
Custom Connect Button
</button>
{/* SDK Integration */}
{isConnected && (
<div>
<p>Connected with: {address}</p>
<p>Chain: {chainId}</p>
<button onClick={() => connectL1(dispatch)}>
Login L1 Account
</button>
</div>
)}
</div>
);
}The app automatically detects and validates environment variables on startup:
// Validate environment configuration
const validation = validateEnvConfig();
if (!validation.isValid) {
setConfigErrors(validation.errors);
} else {
setConfigErrors([]);
// Set Provider configuration
setProviderConfig({ type: 'rainbow' });
}Single hook provides all wallet functionality with optimized internal state management:
// This component gets all wallet functionality from one hook
function WalletDisplay() {
const {
isConnected, isL2Connected, l1Account, l2Account, playerId,
address, chainId, connectL1, connectL2, disconnect
} = useWalletContext();
return (
<div>
<p>Status: L1 {isConnected ? '✅' : '❌'} | L2 {isL2Connected ? '✅' : '❌'}</p>
<p>Player ID: {playerId ? `[${playerId[0]}, ${playerId[1]}]` : 'None'}</p>
<p>Address: {address}</p>
<p>Chain: {chainId}</p>
<button onClick={connectL1}>Connect L1</button>
<button onClick={connectL2}>Connect L2</button>
<button onClick={disconnect}>Disconnect</button>
</div>
);
}The app demonstrates proper state management for wallet connections:
- Unified State: All wallet state managed internally by
useWalletContext - Account Switching Detection: Automatically detects wallet account changes
- Auto-State Updates: Context automatically updates when underlying state changes
- L1 Account: Direct wallet connection information
- L2 Account: Generated from wallet signature with full L2AccountInfo instance
- Player ID (PID): Automatically calculated array from L2 account
- State Synchronization: Context keeps all state synchronized
- L2 Account Methods: Access all L2AccountInfo methods directly from context
- PID Management: Player ID automatically updated when L2 account changes
- Serialization: Full support for L2 account serialization/deserialization
// Unified Context provides all of this internally
interface WalletContextType {
isConnected: boolean;
isL2Connected: boolean;
l1Account: any;
l2Account: any;
playerId: [string, string] | null;
address: string | undefined;
chainId: number | undefined;
connectL1: () => Promise<void>;
connectL2: () => Promise<void>;
disconnect: () => void;
setPlayerId: (id: [string, string]) => void;
deposit: (params: { tokenIndex: number; amount: number }) => Promise<void>;
}
// Advanced users can still access Redux state directly
interface AccountState {
l1Account?: L1AccountInfo;
l2Account?: L2AccountInfo;
status: 'Initial' | 'LoadingL1' | 'LoadingL2' | 'L1AccountError' | 'L2AccountError' | 'Deposit' | 'Ready';
}
interface RootState {
account: AccountState;
}// useWalletContext - No dependencies required (Recommended)
const walletContext = useWalletContext();
// Advanced hooks - Require dependencies
const { isConnected, address, chainId } = useConnection();
const { connectAndLoginL1, loginL2 } = useWalletActions(address, chainId);interface ProviderConfig {
type: 'browser' | 'rainbow' | 'readonly' | 'wallet';
providerUrl?: string;
privateKey?: string;
chainId?: number;
}function AdvancedL2Operations() {
const { l2Account, playerId } = useWalletContext();
const handleL2Operations = React.useCallback(() => {
if (!l2Account) return;
// Access all L2AccountInfo methods directly
const serialized = l2Account.toSerializableData();
const [pid1, pid2] = l2Account.getPidArray();
const pubkeyHex = l2Account.toHexStr();
console.log('L2 Account Operations:', {
serialized,
pidArray: [pid1.toString(), pid2.toString()],
pubkeyHex,
contextPlayerId: playerId
});
}, [l2Account, playerId]);
return (
<div>
<button onClick={handleL2Operations}>
Perform L2 Operations
</button>
</div>
);
}// Use context conditionally based on component needs
function ConditionalWallet() {
const { isConnected, connectL1 } = useWalletContext();
if (!isConnected) {
return <button onClick={connectL1}>Connect Wallet</button>;
}
return <ConnectedWalletView />;
}
function ConnectedWalletView() {
// Only use full context when connected
const {
isL2Connected, l1Account, l2Account, playerId,
connectL2, disconnect
} = useWalletContext();
return (
<div>
<p>L1 Account: {l1Account?.address}</p>
<p>Player ID: {playerId ? `[${playerId[0]}, ${playerId[1]}]` : 'None'}</p>
{!isL2Connected ? (
<button onClick={connectL2}>Connect L2</button>
) : (
<p>L2 Connected: {l2Account?.toHexStr()}</p>
)}
<button onClick={disconnect}>Disconnect</button>
</div>
);
}function WalletErrorBoundary({ children }) {
const [hasError, setHasError] = React.useState(false);
React.useEffect(() => {
const handleError = (event) => {
console.error('Wallet context error:', event.error);
setHasError(true);
};
window.addEventListener('error', handleError);
return () => window.removeEventListener('error', handleError);
}, []);
if (hasError) {
return (
<div>
<h3>Wallet Connection Error</h3>
<p>Something went wrong with the wallet connection.</p>
<button onClick={() => setHasError(false)}>Retry</button>
</div>
);
}
return children;
}// Use unified context for most functionality, with simplified deposit
function HybridWalletComponent() {
const dispatch = useDispatch();
// ✨ NEW: Unified context now includes deposit method
const {
isConnected, isL2Connected, l1Account, l2Account, playerId,
address, chainId, connectL1, connectL2, disconnect, deposit
} = useWalletContext();
// Advanced hooks only needed for special operations like reset
const { reset } = useWalletActions(address, chainId);
// Direct Redux state for status monitoring
const { status } = useSelector((state: RootState) => state.account);
// ✨ NEW: Simplified deposit - no need for dispatch or account parameters
const handleDeposit = async () => {
if (!isConnected || !isL2Connected) {
alert('Please connect both L1 and L2 accounts first');
return;
}
try {
await deposit({
tokenIndex: 0,
amount: 0.01
});
alert('Deposit successful!');
} catch (error) {
console.error('Deposit failed:', error);
alert(`Deposit failed: ${error.message}`);
}
};
const handleReset = async () => {
try {
await reset(dispatch);
} catch (error) {
console.error('Reset failed:', error);
}
};
return (
<div>
<p>Status: {status}</p>
<p>L1: {isConnected ? '✅' : '❌'} | L2: {isL2Connected ? '✅' : '❌'}</p>
<p>Player ID: {playerId ? `[${playerId[0]}, ${playerId[1]}]` : 'None'}</p>
<button onClick={connectL1}>Connect L1</button>
<button onClick={connectL2}>Connect L2</button>
<button onClick={handleDeposit} disabled={!isConnected || !isL2Connected}>
Deposit 0.01 ETH
</button>
<button onClick={disconnect}>Disconnect</button>
<button onClick={handleReset}>Reset</button>
</div>
);
}-
Missing Context Provider
// ❌ Wrong - useWalletContext used outside provider function App() { const wallet = useWalletContext(); // Error! return <div>...</div>; } // ✅ Correct - Wrapped in provider function App() { return ( <DelphinusReactProvider appName="Your App Name"> <WalletComponent /> </DelphinusReactProvider> ); }
-
PID Consistency Issues
// Monitor PID consistency const { l2Account, playerId } = useWalletContext(); React.useEffect(() => { if (l2Account && playerId) { const [pid1, pid2] = l2Account.getPidArray(); const matches = playerId[0] === pid1.toString() && playerId[1] === pid2.toString(); if (!matches) { console.warn('PID mismatch detected'); } } }, [l2Account, playerId]);
-
Provider Configuration Errors
// Validate configuration before use const validation = validateEnvConfig(); if (!validation.isValid) { console.error('Configuration errors:', validation.errors); }
For performance metrics and debugging, use React DevTools Profiler to monitor component re-renders.