Integrations
React

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 ethers

Two 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 address
  • getVaultViaRelayer(keyHash, relayerUrl) — check if vault exists
  • createVaultViaRelayer(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-key

Complete 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 dev

Open http://localhost:3000 (opens in a new tab).

Next Steps