React Integration
Complete guide to integrating Veridex with React applications, based on real patterns from the test-app (opens in a new tab) — a production-grade multi-chain passkey wallet.
The Veridex SDK uses browser APIs (WebAuthn, localStorage) and must only run in client-side code. In Next.js, always add 'use client' to components that use the SDK.
Setup
Install Dependencies
npm install @veridex/sdk ethersTwo Ways to Initialize
The SDK supports two initialization patterns:
1. Quick Start (createSDK) — for simple apps that only need one chain:
import { createSDK } from '@veridex/sdk';
const sdk = createSDK('base', {
network: 'testnet',
relayerUrl: 'https://relayer.veridex.network',
});2. Full Control (VeridexSDK + EVMClient) — for multi-chain apps (recommended):
import { VeridexSDK } from '@veridex/sdk';
import { EVMClient } from '@veridex/sdk/chains/evm';
const evmClient = new EVMClient({
chainId: 84532,
wormholeChainId: 10004,
rpcUrl: 'https://sepolia.base.org',
hubContractAddress: '0x66D87dE68327f48A099c5B9bE97020Feab9a7c82',
wormholeCoreBridge: '0x79A1027a6A159502049F10906D333EC57E95F083',
name: 'Base Sepolia',
explorerUrl: 'https://sepolia.basescan.org',
vaultFactory: '0xCFaEb5652aa2Ee60b2229dC8895B4159749C7e53',
vaultImplementation: '0x0d13367C16c6f0B24eD275CC67C7D9f42878285c',
});
const sdk = new VeridexSDK({
chain: evmClient,
persistWallet: true,
testnet: true,
relayerUrl: '/api/relayer',
relayerApiKey: process.env.NEXT_PUBLIC_RELAYER_API_KEY,
queryApiKey: process.env.NEXT_PUBLIC_WORMHOLE_QUERY_API_KEY,
sponsorPrivateKey: process.env.NEXT_PUBLIC_VERIDEX_SPONSOR_KEY,
});The test-app uses the full control pattern with VeridexSDK + EVMClient because it manages vaults across 6+ chains (Base, Optimism, Arbitrum, Solana, Sui, Aptos, Starknet).
React Context (Production Pattern)
This is the real pattern used in the test-app. It manages SDK initialization, passkey authentication, multi-chain vault addresses, balances, and transfers.
Configuration File
// lib/config.ts
const RELAYER_DIRECT_URL = process.env.NEXT_PUBLIC_RELAYER_URL
|| 'https://relay.veridex.network';
const RELAYER_PROXY_URL = '/api/relayer';
const relayerUrl = process.env.NODE_ENV === 'production'
? RELAYER_PROXY_URL
: RELAYER_DIRECT_URL;
export const config = {
chainId: 84532,
wormholeChainId: 10004,
rpcUrl: process.env.NEXT_PUBLIC_BASE_SEPOLIA_RPC_URL || 'https://sepolia.base.org',
hubContract: '0x66D87dE68327f48A099c5B9bE97020Feab9a7c82',
wormholeCoreBridge: '0x79A1027a6A159502049F10906D333EC57E95F083',
chainName: 'Base Sepolia',
explorerUrl: 'https://sepolia.basescan.org',
vaultFactory: '0xCFaEb5652aa2Ee60b2229dC8895B4159749C7e53',
vaultImplementation: '0x0d13367C16c6f0B24eD275CC67C7D9f42878285c',
relayerUrl,
};
export const spokeConfigs = {
optimismSepolia: {
chainId: 11155420,
wormholeChainId: 10005,
rpcUrl: 'https://sepolia.optimism.io',
vaultFactory: '0xA5653d54079ABeCe780F8d9597B2bc4B09fe464A',
vaultImplementation: '0x8099b1406485d2255ff89Ce5Ea18520802AFC150',
},
arbitrumSepolia: {
chainId: 421614,
wormholeChainId: 10003,
rpcUrl: 'https://sepolia-rollup.arbitrum.io/rpc',
vaultFactory: '0xd36D3D5DB59d78f1E33813490F72DABC15C9B07c',
vaultImplementation: '0xB10ACf39eBF17fc33F722cBD955b7aeCB0611bc4',
},
};Context Provider
// lib/VeridexContext.tsx
'use client';
import React, { createContext, useContext, useState, useEffect, useCallback, ReactNode } from 'react';
import {
VeridexSDK,
PasskeyCredential,
UnifiedIdentity,
PortfolioBalance,
PreparedTransfer,
TransferResult,
TokenInfo,
TransferParams,
SpendingLimits,
FormattedSpendingLimits,
LimitCheckResult,
} from '@veridex/sdk';
import { EVMClient } from '@veridex/sdk/chains/evm';
import { SolanaClient } from '@veridex/sdk/chains/solana';
import { ethers } from 'ethers';
import { config, spokeConfigs } from '@/lib/config';
interface VeridexContextType {
sdk: VeridexSDK | null;
credential: PasskeyCredential | null;
identity: UnifiedIdentity | null;
isRegistered: boolean;
vaultAddress: string | null;
vaultDeployed: boolean;
isLoading: boolean;
// Authentication
register: (username: string, displayName: string) => Promise<void>;
login: () => Promise<void>;
logout: () => void;
hasStoredCredential: () => boolean;
// Balances
vaultBalances: PortfolioBalance | null;
isLoadingBalances: boolean;
refreshBalances: () => Promise<void>;
getTokenList: () => TokenInfo[];
// Transfers
prepareTransfer: (params: TransferParams) => Promise<PreparedTransfer>;
executeTransfer: (prepared: PreparedTransfer) => Promise<TransferResult>;
transferGasless: (params: TransferParams) => Promise<TransferResult>;
// Spending Limits
spendingLimits: SpendingLimits | null;
formattedSpendingLimits: FormattedSpendingLimits | null;
refreshSpendingLimits: () => Promise<void>;
checkTransactionLimit: (amount: bigint) => Promise<LimitCheckResult | null>;
}
const VeridexContext = createContext<VeridexContextType | undefined>(undefined);
export function VeridexProvider({ children }: { children: ReactNode }) {
const [sdk, setSdk] = useState<VeridexSDK | null>(null);
const [credential, setCredential] = useState<PasskeyCredential | null>(null);
const [identity, setIdentity] = useState<UnifiedIdentity | null>(null);
const [vaultAddress, setVaultAddress] = useState<string | null>(null);
const [vaultDeployed, setVaultDeployed] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [vaultBalances, setVaultBalances] = useState<PortfolioBalance | null>(null);
const [isLoadingBalances, setIsLoadingBalances] = useState(false);
const [spendingLimits, setSpendingLimits] = useState<SpendingLimits | null>(null);
const [formattedSpendingLimits, setFormattedSpendingLimits] =
useState<FormattedSpendingLimits | null>(null);
// Initialize SDK on mount
useEffect(() => {
const initSdk = async () => {
setIsLoading(true);
try {
// Create the EVM chain client (hub chain)
const evmClient = new EVMClient({
chainId: config.chainId,
wormholeChainId: config.wormholeChainId,
rpcUrl: config.rpcUrl,
hubContractAddress: config.hubContract,
wormholeCoreBridge: config.wormholeCoreBridge,
name: config.chainName,
explorerUrl: config.explorerUrl,
vaultFactory: config.vaultFactory,
vaultImplementation: config.vaultImplementation,
});
// Create the SDK instance
const veridexSdk = new VeridexSDK({
chain: evmClient,
persistWallet: true,
testnet: true,
relayerUrl: config.relayerUrl,
relayerApiKey: process.env.NEXT_PUBLIC_RELAYER_API_KEY,
queryApiKey: process.env.NEXT_PUBLIC_WORMHOLE_QUERY_API_KEY,
sponsorPrivateKey: process.env.NEXT_PUBLIC_VERIDEX_SPONSOR_KEY,
chainRpcUrls: {
10004: config.rpcUrl,
10005: spokeConfigs.optimismSepolia.rpcUrl,
10003: spokeConfigs.arbitrumSepolia.rpcUrl,
},
});
setSdk(veridexSdk);
// Try to restore saved credential (auto-login)
const savedCred = veridexSdk.passkey.loadFromLocalStorage();
if (savedCred) {
veridexSdk.setCredential(savedCred);
setCredential(savedCred);
await loadIdentity(veridexSdk);
// Auto-create vaults on all chains (sponsored, gasless)
if (veridexSdk.isSponsorshipAvailable()) {
await veridexSdk.ensureSponsoredVaultsOnAllChains();
}
}
} catch (error) {
console.error('SDK initialization error:', error);
} finally {
setIsLoading(false);
}
};
initSdk();
}, []);
const loadIdentity = async (sdkInstance: VeridexSDK) => {
try {
const unifiedIdentity = await sdkInstance.getUnifiedIdentity();
setIdentity(unifiedIdentity);
const chainAddr = unifiedIdentity.addresses.find(
a => a.wormholeChainId === config.wormholeChainId
);
if (chainAddr) {
setVaultAddress(chainAddr.address);
setVaultDeployed(chainAddr.deployed);
} else {
const addr = sdkInstance.getVaultAddress();
setVaultAddress(addr);
setVaultDeployed(await sdkInstance.vaultExists());
}
} catch (error) {
console.warn('Could not load identity:', error);
}
};
// --- Authentication ---
const register = async (username: string, displayName: string) => {
if (!sdk) throw new Error('SDK not initialized');
setIsLoading(true);
try {
const cred = await sdk.passkey.register(username, displayName);
sdk.setCredential(cred);
setCredential(cred);
sdk.passkey.saveToLocalStorage();
// Save to relayer for cross-device recovery
sdk.passkey.saveCredentialToRelayer().catch(console.warn);
await loadIdentity(sdk);
// Auto-create vaults (gasless)
if (sdk.isSponsorshipAvailable()) {
await sdk.ensureSponsoredVaultsOnAllChains();
await loadIdentity(sdk);
}
} finally {
setIsLoading(false);
}
};
const login = async () => {
if (!sdk) throw new Error('SDK not initialized');
setIsLoading(true);
try {
const { credential: cred } = await sdk.passkey.authenticate();
sdk.setCredential(cred);
setCredential(cred);
sdk.passkey.saveToLocalStorage();
await loadIdentity(sdk);
} finally {
setIsLoading(false);
}
};
const logout = () => {
if (sdk) {
sdk.clearCredential();
sdk.wallet.clearCache();
}
setCredential(null);
setIdentity(null);
setVaultAddress(null);
setVaultDeployed(false);
setVaultBalances(null);
};
const hasStoredCredential = (): boolean => {
if (!sdk) return false;
return sdk.passkey.hasStoredCredential();
};
// --- Balances ---
const refreshBalances = async () => {
if (!sdk || !vaultAddress) return;
setIsLoadingBalances(true);
try {
const balances = await sdk.getVaultBalances(true);
setVaultBalances(balances);
} catch (error) {
console.error('Error fetching balances:', error);
} finally {
setIsLoadingBalances(false);
}
};
const getTokenList = (): TokenInfo[] => {
if (!sdk) return [];
return sdk.getTokenList();
};
// --- Transfers ---
const prepareTransfer = async (params: TransferParams) => {
if (!sdk) throw new Error('SDK not initialized');
return await sdk.prepareTransfer(params);
};
const executeTransfer = async (prepared: PreparedTransfer) => {
if (!sdk) throw new Error('SDK not initialized');
const signer = new ethers.BrowserProvider(window.ethereum!).getSigner();
return await sdk.executeTransfer(prepared, await signer);
};
const transferGasless = async (params: TransferParams) => {
if (!sdk) throw new Error('SDK not initialized');
return await sdk.transferViaRelayer(params);
};
// --- Spending Limits ---
const refreshSpendingLimits = useCallback(async () => {
if (!sdk || !vaultAddress) return;
try {
const limits = await sdk.spendingLimits.getSpendingLimits(
vaultAddress, config.wormholeChainId
);
setSpendingLimits(limits);
const formatted = await sdk.spendingLimits.getFormattedSpendingLimits(
vaultAddress, config.wormholeChainId
);
setFormattedSpendingLimits(formatted);
} catch (error) {
console.error('Error fetching spending limits:', error);
}
}, [sdk, vaultAddress]);
const checkTransactionLimit = useCallback(async (amount: bigint) => {
if (!sdk || !vaultAddress) return null;
return await sdk.spendingLimits.checkTransactionLimit(
vaultAddress, config.wormholeChainId, amount
);
}, [sdk, vaultAddress]);
return (
<VeridexContext.Provider value={{
sdk, credential, identity,
isRegistered: !!credential,
vaultAddress, vaultDeployed, isLoading,
register, login, logout, hasStoredCredential,
vaultBalances, isLoadingBalances, refreshBalances, getTokenList,
prepareTransfer, executeTransfer, transferGasless,
spendingLimits, formattedSpendingLimits,
refreshSpendingLimits, checkTransactionLimit,
}}>
{children}
</VeridexContext.Provider>
);
}
export function useVeridex() {
const context = useContext(VeridexContext);
if (!context) throw new Error('useVeridex must be used within VeridexProvider');
return context;
}Use in App
// app/layout.tsx (Next.js App Router)
import { VeridexProvider } from '@/lib/VeridexContext';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<VeridexProvider>{children}</VeridexProvider>
</body>
</html>
);
}Components
Connect / Register Button
// components/ConnectButton.tsx
'use client';
import { useState } from 'react';
import { useVeridex } from '@/lib/VeridexContext';
export function ConnectButton() {
const {
isRegistered, vaultAddress, isLoading,
register, login, logout, hasStoredCredential,
} = useVeridex();
const [username, setUsername] = useState('');
const [error, setError] = useState('');
if (isLoading) {
return <button disabled>Loading...</button>;
}
if (isRegistered) {
return (
<div className="flex items-center gap-3">
<code>{vaultAddress?.slice(0, 6)}...{vaultAddress?.slice(-4)}</code>
<button onClick={logout} className="text-red-500 text-sm">Disconnect</button>
</div>
);
}
return (
<div className="space-y-4">
{/* Sign in with existing passkey */}
<button
onClick={async () => {
try { await login(); }
catch (err: any) { setError(err.message); }
}}
className="w-full bg-purple-600 text-white py-3 rounded-xl"
>
Sign In with Passkey
</button>
{/* Register new passkey */}
<form onSubmit={async (e) => {
e.preventDefault();
try { await register(username, username); }
catch (err: any) { setError(err.message); }
}}>
<input
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="Choose a username"
className="w-full p-3 border rounded-xl mb-2"
required
/>
<button type="submit" className="w-full bg-white/10 py-3 rounded-xl">
Create Wallet
</button>
</form>
{error && <p className="text-red-500 text-sm">{error}</p>}
</div>
);
}Balance Display
// components/BalanceDisplay.tsx
'use client';
import { useEffect } from 'react';
import { useVeridex } from '@/lib/VeridexContext';
export function BalanceDisplay() {
const {
isRegistered, vaultBalances, isLoadingBalances, refreshBalances,
} = useVeridex();
useEffect(() => {
if (isRegistered) refreshBalances();
}, [isRegistered]);
if (!isRegistered) return null;
if (isLoadingBalances) {
return <div className="animate-pulse">Loading balances...</div>;
}
return (
<div className="space-y-4">
<div className="flex justify-between items-center">
<h3 className="font-bold">Balances</h3>
<button onClick={refreshBalances} className="text-sm text-blue-600">Refresh</button>
</div>
{vaultBalances?.tokens.map((entry) => (
<div key={entry.token.address} className="flex justify-between p-3 bg-gray-50 rounded-lg">
<span>{entry.token.symbol}</span>
<span className="font-mono">{entry.formatted}</span>
</div>
))}
{vaultBalances?.totalUsdValue && (
<div className="text-right font-bold">
Total: ${vaultBalances.totalUsdValue.toFixed(2)}
</div>
)}
</div>
);
}Gasless Transfer Form
The test-app uses gasless transfers via the relayer — the user only needs their passkey, no MetaMask or gas tokens required.
// components/SendForm.tsx
'use client';
import { useState } from 'react';
import { ethers } from 'ethers';
import { useVeridex } from '@/lib/VeridexContext';
const USDC_ADDRESS = '0x036CbD53842c5426634e7929541eC2318f3dCF7e';
export function SendForm() {
const { isRegistered, transferGasless, vaultAddress } = useVeridex();
const [recipient, setRecipient] = useState('');
const [amount, setAmount] = useState('');
const [loading, setLoading] = useState(false);
const [result, setResult] = useState<string | null>(null);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setResult(null);
try {
const tx = await transferGasless({
targetChain: 10004, // Base Sepolia
token: USDC_ADDRESS,
recipient,
amount: ethers.parseUnits(amount, 6), // USDC has 6 decimals
});
setResult(`Sent! Tx: ${tx.transactionHash}`);
setRecipient('');
setAmount('');
} catch (error) {
setResult(error instanceof Error ? error.message : 'Transfer failed');
} finally {
setLoading(false);
}
};
if (!isRegistered) {
return <p className="text-center text-gray-500">Connect your wallet to send tokens</p>;
}
return (
<form onSubmit={handleSubmit} className="space-y-4">
<input
type="text"
value={recipient}
onChange={(e) => setRecipient(e.target.value)}
placeholder="Recipient address (0x...)"
className="w-full p-3 border rounded-lg"
required
/>
<input
type="number"
value={amount}
onChange={(e) => setAmount(e.target.value)}
placeholder="Amount (USDC)"
step="0.01"
className="w-full p-3 border rounded-lg"
required
/>
<button
type="submit"
disabled={loading}
className="w-full bg-blue-600 text-white py-3 rounded-lg disabled:bg-gray-400"
>
{loading ? 'Sending...' : 'Send USDC (Gasless)'}
</button>
{result && <p className="text-sm p-3 bg-gray-100 rounded-lg">{result}</p>}
</form>
);
}Multi-Chain Support
The test-app initializes chain clients for Solana, Sui, Aptos, and Starknet alongside the EVM hub:
import { SolanaClient } from '@veridex/sdk/chains/solana';
import { SuiClient } from '@veridex/sdk/chains/sui';
import { AptosClient } from '@veridex/sdk/chains/aptos';
import { StarknetClient } from '@veridex/sdk/chains/starknet';
// Inside your SDK initialization:
const solClient = new SolanaClient({
wormholeChainId: 1,
rpcUrl: 'https://api.devnet.solana.com',
programId: 'J7JehynQjN4XrucGQ5joMfhQWiViDmmLhQLriGUcWAM2',
wormholeCoreBridge: '3u8hJUVTA4jH1wYAyUur7FFZVQ8H635K3tSHHF4ssjQ5',
tokenBridge: 'DZnkkTmCiFWfYTfT41X3Rd1kDgozqzxWaHqsw6W4x2oe',
network: 'devnet',
commitment: 'confirmed',
});
const suiClientInstance = new SuiClient({
wormholeChainId: 21,
rpcUrl: 'https://fullnode.testnet.sui.io:443',
packageId: '0x6ae854c698d73e39f5dc07c4d2291fa81e8732aded14bbff3b98cfa8bfaebff5',
wormholeCoreBridge: '0x31358d198147da50db32eda2562951d53973a0c0ad5ed738e9b17d88b213d790',
network: 'testnet',
});Each chain client provides:
computeVaultAddress(keyHash)— deterministic vault addressgetVaultViaRelayer(keyHash, relayerUrl)— check if vault existscreateVaultViaRelayer(keyHash, relayerUrl)— create vault (gasless)
Sponsored Vault Creation
The SDK can auto-create vaults on all chains without gas:
// After registration or login
if (sdk.isSponsorshipAvailable()) {
const result = await sdk.ensureSponsoredVaultsOnAllChains();
console.log(`Created vaults on ${result.results.filter(r => r.success).length} chains`);
}Environment Variables
# .env.local
NEXT_PUBLIC_NETWORK=testnet
NEXT_PUBLIC_RELAYER_URL=https://relay.veridex.network
NEXT_PUBLIC_RELAYER_API_KEY=your-api-key
NEXT_PUBLIC_WORMHOLE_QUERY_API_KEY=your-query-key
NEXT_PUBLIC_VERIDEX_SPONSOR_KEY=your-sponsor-key
NEXT_PUBLIC_INTEGRATOR_SPONSOR_KEY=your-integrator-keyComplete Example
Clone the test-app for a complete working example with 6-chain support:
git clone https://github.com/Veridex-Protocol/demo
cd demo/test-app
cp .env.local.example .env.local
npm install
npm run devOpen http://localhost:3000 (opens in a new tab).