Skip to content

Unified Wallet Management System

Overview

The Prediction DAO frontend now features a harmonized wallet management system that provides a single, cohesive interface for all wallet-related operations throughout the application. This system consolidates wallet connection, transaction signing, balance management, and RVAC (Role-Based Access Control) into a unified context.

Wallet Connection Options

The application supports multiple wallet connection methods:

  1. MetaMask / Browser Wallet - Using injected provider (MetaMask, Brave, etc.)
  2. WalletConnect - Mobile wallets and other WalletConnect-compatible wallets

WalletConnect Setup

WalletConnect is always available in the wallet connection modal, enabling hardware wallet support out of the box. The application uses a fallback project ID for development and testing purposes.

For Production Use:

  1. Get a Project ID:
  2. Visit WalletConnect Cloud
  3. Create a new project
  4. Copy your Project ID

  5. Configure Environment: Add the Project ID to your .env file:

    VITE_WALLETCONNECT_PROJECT_ID=your_project_id_here
    

  6. Whitelist Your Domain:

  7. In WalletConnect Cloud dashboard
  8. Add your domain(s) to the allowed origins
  9. Include both production and development URLs

  10. Test Connection:

  11. Click the profile icon (👤) when not connected
  12. Select "WalletConnect" from the connector options
  13. Scan QR code with your mobile wallet
  14. Approve the connection request

Note: If VITE_WALLETCONNECT_PROJECT_ID is not set, a fallback demo project ID will be used. This is suitable for development and testing, but for production deployments, you should configure your own project ID for better reliability and to avoid rate limiting.

Architecture

WalletContext (src/contexts/WalletContext.jsx)

The WalletContext is the central hub for all wallet-related state and operations. It provides:

  • Wallet Connection: Connect/disconnect wallet with automatic provider setup
  • Multiple Connectors: Support for injected wallets and WalletConnect
  • Address Management: Current wallet address and connection state
  • Provider & Signer: Ethers.js provider and signer instances for transactions
  • Balance Tracking: ETC, WETC, and other token balances
  • Network Management: Network detection, validation, and switching
  • RVAC Integration: Role management tied directly to wallet address
  • Transaction Helpers: Utilities for sending transactions and signing messages

Key Features

  1. Single Source of Truth: All wallet state is managed in one place
  2. Multiple Wallet Support: Injected wallets (MetaMask) and WalletConnect
  3. RVAC Integration: User roles are automatically loaded and managed based on wallet address
  4. Balance Caching: Token balances are cached to reduce RPC calls
  5. Network Validation: Automatic detection of wrong network with switch capability
  6. Transaction Support: Helper methods for common transaction operations

Usage

Primary Hook: useWallet()

The main hook that provides access to all wallet functionality:

import { useWallet } from '../hooks'

function MyComponent() {
  const { 
    address,           // Current wallet address
    isConnected,       // Connection state
    balances,          // { etc, wetc, tokens }
    provider,          // Ethers provider
    signer,            // Ethers signer
    connectWallet,     // Connect function
    disconnectWallet,  // Disconnect function
    sendTransaction,   // Send transaction helper
    hasRole,           // Check if user has RVAC role
    grantRole,         // Grant RVAC role (for purchase flows)
  } = useWallet()

  // Use wallet functionality...
}

Specialized Hooks

For components that only need specific wallet functionality, use the specialized hooks:

useWalletAddress()

For components that only need address and connection state:

import { useWalletAddress } from '../hooks'

function AddressDisplay() {
  const { address, isConnected } = useWalletAddress()
  return <div>{isConnected ? address : 'Not connected'}</div>
}

useWalletBalances()

For components that display or check balances:

import { useWalletBalances } from '../hooks'

function BalanceDisplay() {
  const { balances, refreshBalances, getTokenBalance } = useWalletBalances()

  return (
    <div>
      <p>ETC Balance: {balances.etc}</p>
      <button onClick={refreshBalances}>Refresh</button>
    </div>
  )
}

useWalletTransactions()

For components that send transactions:

import { useWalletTransactions } from '../hooks'
import { ethers } from 'ethers'

function SendTransaction() {
  const { sendTransaction, signMessage } = useWalletTransactions()

  const handleSend = async () => {
    const tx = await sendTransaction({
      to: '0x...',
      value: ethers.parseEther('1.0')
    })
    await tx.wait()
  }

  return <button onClick={handleSend}>Send</button>
}

useWalletRoles()

For RVAC role-gated features:

import { useWalletRoles } from '../hooks'

function RoleGatedFeature() {
  const { hasRole, roles } = useWalletRoles()

  if (!hasRole('MARKET_MAKER')) {
    return <div>You need Market Maker access</div>
  }

  return <MarketMakerPanel />
}

useWalletNetwork()

For network-dependent features:

import { useWalletNetwork } from '../hooks'

function NetworkCheck() {
  const { isCorrectNetwork, networkError, switchNetwork } = useWalletNetwork()

  if (networkError) {
    return (
      <div>
        <p>{networkError}</p>
        <button onClick={switchNetwork}>Switch Network</button>
      </div>
    )
  }

  return <div>Connected to correct network</div>
}

useWalletConnection()

For connect/disconnect buttons:

import { useWalletConnection } from '../hooks'

function ConnectButton() {
  const { isConnected, connectWallet, disconnectWallet } = useWalletConnection()

  return (
    <button onClick={isConnected ? disconnectWallet : connectWallet}>
      {isConnected ? 'Disconnect' : 'Connect Wallet'}
    </button>
  )
}

RVAC Integration

The wallet system integrates RVAC (Role-Based Access Control) management directly with the wallet address. This means:

  1. Automatic Loading: When a wallet connects, user roles are automatically loaded
  2. Address-Tied Roles: Roles are stored and retrieved based on wallet address
  3. Purchase Flow: Role purchases use the wallet system for both payment and role assignment
  4. Role Checking: Components can easily check roles without separate context

Example: Role Purchase Flow

import { useWallet, useWalletRoles } from '../hooks'

function RolePurchase() {
  const { address, isConnected, sendTransaction } = useWallet()
  const { hasRole, grantRole } = useWalletRoles()

  const handlePurchase = async (role, price) => {
    if (!isConnected) {
      alert('Please connect wallet')
      return
    }

    // Send payment transaction
    const tx = await sendTransaction({
      to: PAYMENT_ADDRESS,
      value: ethers.parseEther(price.toString())
    })
    await tx.wait()

    // Grant role to user
    grantRole(role)
  }

  return (
    <button onClick={() => handlePurchase('MARKET_MAKER', 100)}>
      Purchase Market Maker Role (100 ETC)
    </button>
  )
}

Transaction Flow

The wallet system provides helpers for common transaction operations:

1. Sending Transactions

const { sendTransaction } = useWallet()

const tx = await sendTransaction({
  to: recipientAddress,
  value: ethers.parseEther('1.0'),
  data: '0x...' // optional
})

const receipt = await tx.wait()

2. Signing Messages

const { signMessage } = useWallet()

const signature = await signMessage('Hello World')

3. Contract Interactions

const { signer } = useWallet()

const contract = new ethers.Contract(address, abi, signer)
const tx = await contract.someMethod(params)
await tx.wait()

Balance Management

The wallet system automatically tracks and caches balances:

Native Balance (ETC)

Automatically loaded when wallet connects and can be refreshed:

const { balances, refreshBalances } = useWallet()

console.log(`ETC Balance: ${balances.etc}`)
await refreshBalances() // Manually refresh

Token Balances

Get and cache ERC20 token balances:

const { getTokenBalance } = useWallet()

// Get WETC balance
const wetcBalance = await getTokenBalance(WETC_ADDRESS)

// Balance is now cached in balances.tokens[WETC_ADDRESS]

Network Management

The wallet system handles network validation and switching:

const { 
  isCorrectNetwork,  // Boolean: on expected network?
  networkError,      // String: error message if wrong network
  switchNetwork      // Function: switch to correct network
} = useWallet()

if (!isCorrectNetwork) {
  return (
    <div>
      <p>{networkError}</p>
      <button onClick={switchNetwork}>Switch Network</button>
    </div>
  )
}

Migration Guide

From Old Pattern

Before:

import { useWeb3 } from '../hooks/useWeb3'
import { useRoles } from '../hooks/useRoles'

function MyComponent() {
  const { account, provider, signer } = useWeb3()
  const { hasRole } = useRoles()

  // Component logic...
}

After:

import { useWallet } from '../hooks'

function MyComponent() {
  const { address, provider, signer, hasRole } = useWallet()

  // Component logic...
  // Note: 'account' is now 'address' (though 'account' still works as alias)
}

Backwards Compatibility

The old hooks (useWeb3, useRoles) are still available for backwards compatibility during migration. New code should use the unified useWallet hook.

Provider Hierarchy

The wallet system is integrated into the app's provider hierarchy:

<WagmiProvider>
  <QueryClientProvider>
    <ThemeProvider>
      <WalletProvider>        {/* Primary wallet management */}
        <Web3Provider>         {/* Legacy - backwards compatibility */}
          <UserPreferencesProvider>
            <RoleProvider>     {/* Legacy - backwards compatibility */}
              <ETCswapProvider>  {/* Uses WalletProvider internally */}
                <UIProvider>
                  <App />
                </UIProvider>
              </ETCswapProvider>
            </RoleProvider>
          </UserPreferencesProvider>
        </Web3Provider>
      </WalletProvider>
    </ThemeProvider>
  </QueryClientProvider>
</WagmiProvider>

Best Practices

  1. Use Specialized Hooks: Use useWalletRoles(), useWalletBalances(), etc. when you only need specific functionality to avoid unnecessary re-renders

  2. Check Connection State: Always check isConnected before performing wallet operations

  3. Handle Errors: Wrap wallet operations in try-catch blocks and provide user feedback

  4. Refresh Balances: Call refreshBalances() after transactions that change balances

  5. Role Gating: Use hasRole() to gate features that require specific RVAC roles

Examples

Complete Wallet Integration Example

import { useWallet, useWalletRoles } from '../hooks'
import { useNotification } from '../hooks/useUI'
import { ethers } from 'ethers'

function CompleteExample() {
  const { 
    address, 
    isConnected, 
    balances,
    connectWallet,
    sendTransaction,
    isCorrectNetwork,
    switchNetwork
  } = useWallet()

  const { hasRole, grantRole } = useWalletRoles()
  const { showNotification } = useNotification()

  const handlePurchaseRole = async () => {
    try {
      // Check connection
      if (!isConnected) {
        await connectWallet()
      }

      // Check network
      if (!isCorrectNetwork) {
        await switchNetwork()
      }

      // Send payment
      const tx = await sendTransaction({
        to: PAYMENT_ADDRESS,
        value: ethers.parseEther('100')
      })

      showNotification('Transaction submitted...', 'info')
      await tx.wait()

      // Grant role
      grantRole('MARKET_MAKER')
      showNotification('Role purchased successfully!', 'success')

    } catch (error) {
      console.error(error)
      showNotification('Purchase failed: ' + error.message, 'error')
    }
  }

  return (
    <div>
      {isConnected ? (
        <div>
          <p>Address: {address}</p>
          <p>Balance: {balances.etc} ETC</p>
          {hasRole('MARKET_MAKER') ? (
            <p>You have Market Maker access!</p>
          ) : (
            <button onClick={handlePurchaseRole}>
              Purchase Market Maker Role (100 ETC)
            </button>
          )}
        </div>
      ) : (
        <button onClick={connectWallet}>Connect Wallet</button>
      )}
    </div>
  )
}

API Reference

WalletContext Value

{
  // Connection State
  address: string | null              // Current wallet address
  account: string | null              // Alias for address (backwards compat)
  isConnected: boolean                // Wallet connected?
  chainId: number | undefined         // Current chain ID

  // Provider & Signer
  provider: BrowserProvider | null    // Ethers provider instance
  signer: Signer | null              // Ethers signer instance

  // Network State
  networkError: string | null         // Network error message
  isCorrectNetwork: boolean           // On correct network?

  // Balances
  balances: {
    etc: string                       // Native ETC balance
    wetc: string                      // WETC balance
    tokens: Record<string, string>    // Token address -> balance
  }
  balancesLoading: boolean            // Loading balances?

  // RVAC Roles
  roles: string[]                     // Current user roles
  rolesLoading: boolean               // Loading roles?

  // Wallet Actions
  connectWallet: () => Promise<boolean>
  disconnectWallet: () => void
  switchNetwork: () => Promise<boolean>

  // Transaction Methods
  sendTransaction: (tx: TransactionRequest) => Promise<TransactionResponse>
  signMessage: (message: string) => Promise<string>

  // Balance Methods
  refreshBalances: () => Promise<void>
  getTokenBalance: (tokenAddress: string) => Promise<string>

  // RVAC Role Methods
  hasRole: (role: string) => boolean
  hasAnyRole: (roles: string[]) => boolean
  hasAllRoles: (roles: string[]) => boolean
  grantRole: (role: string) => boolean
  revokeRole: (role: string) => boolean
}

Troubleshooting

Issue: Wallet not connecting

Solution: Ensure MetaMask is installed and the user approves the connection request.

Issue: Wrong network error

Solution: Use the switchNetwork() function or manually switch in MetaMask.

Issue: Balances not updating

Solution: Call refreshBalances() after transactions that change balances.

Issue: Roles not loading

Solution: Roles are tied to wallet address. Ensure wallet is connected and roles are stored in localStorage.

Future Enhancements

  • Multi-wallet support
  • Transaction history tracking
  • Gas estimation helpers
  • Smart contract interaction helpers
  • ENS name resolution
  • Hardware wallet support