Web3 Wallets

MetaMask, WalletConnect, and browser wallet integration for LX

Web3 Wallet Integration

Specification: LP-9002 DEX API & RPC

Overview

LX supports all major Web3 wallets through standard interfaces:

  • EIP-1193: Provider API for browser wallets
  • EIP-6963: Multi-wallet discovery
  • WalletConnect v2: Mobile and desktop wallet connection

Supported Wallets

WalletTypeEIP-6963WalletConnectNotes
MetaMaskBrowser/MobileYesYesMost popular
Coinbase WalletBrowser/MobileYesYesExchange integration
RainbowMobileNoYesMobile-first
Trust WalletMobileNoYesMulti-chain
RabbyBrowserYesNoDeFi focused
FrameDesktopYesNoPrivacy focused
ZerionBrowser/MobileYesYesPortfolio tracking

MetaMask Integration

Installation

npm install @luxfi/trading

Basic Connection

import { Client, MetaMaskAdapter } from '@luxfi/trading';

const adapter = new MetaMaskAdapter();
const client = new Client({
  endpoint: 'https://dex.lux.network',
  wallet: adapter
});

// Connect wallet
async function connectWallet() {
  try {
    const accounts = await adapter.connect();
    console.log('Connected:', accounts[0]);

    // Check network
    const chainId = await adapter.getChainId();
    if (chainId !== 96369) {
      await adapter.switchNetwork(96369);
    }

    return accounts[0];
  } catch (error) {
    if (error.code === 4001) {
      console.log('User rejected connection');
    } else {
      console.error('Connection failed:', error);
    }
  }
}

Network Configuration

// Add Lux network to MetaMask
async function addLuxNetwork() {
  try {
    await window.ethereum.request({
      method: 'wallet_addEthereumChain',
      params: [{
        chainId: '0x17871', // 96369 in hex
        chainName: 'Lux Mainnet',
        nativeCurrency: {
          name: 'LUX',
          symbol: 'LUX',
          decimals: 18
        },
        rpcUrls: ['https://api.lux.network/ext/bc/C/rpc'],
        blockExplorerUrls: ['https://explorer.lux.network']
      }]
    });
  } catch (error) {
    if (error.code === 4902) {
      console.log('Network not added');
    }
  }
}

// Switch to Lux network
async function switchToLux() {
  try {
    await window.ethereum.request({
      method: 'wallet_switchEthereumChain',
      params: [{ chainId: '0x17871' }]
    });
  } catch (error) {
    if (error.code === 4902) {
      await addLuxNetwork();
    }
  }
}

Event Handling

// Listen for account changes
adapter.on('accountsChanged', (accounts: string[]) => {
  if (accounts.length === 0) {
    console.log('Wallet disconnected');
    handleDisconnect();
  } else {
    console.log('Account changed:', accounts[0]);
    handleAccountChange(accounts[0]);
  }
});

// Listen for network changes
adapter.on('chainChanged', (chainId: string) => {
  const id = parseInt(chainId, 16);
  if (id !== 96369) {
    console.log('Wrong network, please switch to Lux');
    showNetworkWarning();
  }
});

// Listen for disconnect
adapter.on('disconnect', () => {
  console.log('Wallet disconnected');
  handleDisconnect();
});

WalletConnect v2

Setup

import { WalletConnectAdapter } from '@luxfi/trading';

const adapter = new WalletConnectAdapter({
  projectId: 'YOUR_WALLETCONNECT_PROJECT_ID',
  chains: [96369], // Lux Mainnet
  optionalChains: [96370], // Lux Testnet
  metadata: {
    name: 'LX',
    description: 'High-performance decentralized exchange',
    url: 'https://dex.lux.network',
    icons: ['https://dex.lux.network/logo.png']
  }
});

QR Code Connection

import { WalletConnectModal } from '@walletconnect/modal';

const modal = new WalletConnectModal({
  projectId: 'YOUR_WALLETCONNECT_PROJECT_ID',
  chains: ['eip155:96369']
});

async function connectWithQR() {
  // Get connection URI
  const uri = await adapter.getConnectionUri();

  // Display QR code modal
  modal.openModal({ uri });

  // Wait for connection
  const accounts = await adapter.connect();
  modal.closeModal();

  return accounts[0];
}

Session Management

// Check existing session
async function checkSession() {
  const session = await adapter.getSession();
  if (session && !session.expired) {
    return session.accounts[0];
  }
  return null;
}

// Restore session
async function restoreSession() {
  const existing = await checkSession();
  if (existing) {
    console.log('Session restored:', existing);
    return existing;
  }
  // Need new connection
  return await connectWithQR();
}

// Disconnect session
async function disconnect() {
  await adapter.disconnect();
  console.log('Session disconnected');
}

EIP-6963 Multi-Wallet Discovery

import { discoverWallets, WalletInfo } from '@luxfi/trading';

// Discover all available wallets
const wallets = await discoverWallets();

// Display wallet selection UI
function WalletSelector({ wallets }: { wallets: WalletInfo[] }) {
  return (
    <div className="wallet-list">
      {wallets.map(wallet => (
        <button
          key={wallet.uuid}
          onClick={() => connectWallet(wallet)}
          className="wallet-option"
        >
          <img src={wallet.icon} alt={wallet.name} />
          <span>{wallet.name}</span>
        </button>
      ))}
    </div>
  );
}

// Connect selected wallet
async function connectWallet(wallet: WalletInfo) {
  const adapter = createAdapter(wallet.provider);
  const accounts = await adapter.connect();
  return accounts[0];
}

React Integration

Wallet Hook

import { useState, useEffect, useCallback } from 'react';
import { WalletAdapter, MetaMaskAdapter } from '@luxfi/trading';

export function useWallet() {
  const [adapter, setAdapter] = useState<WalletAdapter | null>(null);
  const [address, setAddress] = useState<string | null>(null);
  const [chainId, setChainId] = useState<number | null>(null);
  const [isConnecting, setIsConnecting] = useState(false);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    const metamask = new MetaMaskAdapter();
    setAdapter(metamask);

    // Check if already connected
    metamask.getAccounts().then(accounts => {
      if (accounts.length > 0) {
        setAddress(accounts[0]);
      }
    });

    // Subscribe to events
    metamask.on('accountsChanged', (accounts) => {
      setAddress(accounts[0] || null);
    });

    metamask.on('chainChanged', (id) => {
      setChainId(parseInt(id, 16));
    });

    return () => {
      metamask.removeAllListeners();
    };
  }, []);

  const connect = useCallback(async () => {
    if (!adapter) return;

    setIsConnecting(true);
    setError(null);

    try {
      const accounts = await adapter.connect();
      setAddress(accounts[0]);

      const chain = await adapter.getChainId();
      setChainId(chain);

      if (chain !== 96369) {
        await adapter.switchNetwork(96369);
      }
    } catch (err) {
      setError(err as Error);
    } finally {
      setIsConnecting(false);
    }
  }, [adapter]);

  const disconnect = useCallback(async () => {
    if (!adapter) return;
    await adapter.disconnect();
    setAddress(null);
  }, [adapter]);

  return {
    address,
    chainId,
    isConnecting,
    error,
    connect,
    disconnect,
    adapter
  };
}

Wallet Provider Context

import { createContext, useContext, ReactNode } from 'react';
import { useWallet } from './useWallet';

const WalletContext = createContext<ReturnType<typeof useWallet> | null>(null);

export function WalletProvider({ children }: { children: ReactNode }) {
  const wallet = useWallet();
  return (
    <WalletContext.Provider value={wallet}>
      {children}
    </WalletContext.Provider>
  );
}

export function useWalletContext() {
  const context = useContext(WalletContext);
  if (!context) {
    throw new Error('useWalletContext must be used within WalletProvider');
  }
  return context;
}

Connect Button Component

import { useWalletContext } from './WalletProvider';

export function ConnectButton() {
  const { address, isConnecting, connect, disconnect } = useWalletContext();

  if (isConnecting) {
    return <button disabled>Connecting...</button>;
  }

  if (address) {
    return (
      <div className="wallet-info">
        <span>{formatAddress(address)}</span>
        <button onClick={disconnect}>Disconnect</button>
      </div>
    );
  }

  return <button onClick={connect}>Connect Wallet</button>;
}

function formatAddress(address: string): string {
  return `${address.slice(0, 6)}...${address.slice(-4)}`;
}

Order Signing

EIP-712 Typed Data

const ORDER_TYPES = {
  Order: [
    { name: 'symbol', type: 'string' },
    { name: 'side', type: 'uint8' },
    { name: 'type', type: 'uint8' },
    { name: 'price', type: 'uint256' },
    { name: 'quantity', type: 'uint256' },
    { name: 'nonce', type: 'uint256' },
    { name: 'expiry', type: 'uint256' }
  ]
};

const DOMAIN = {
  name: 'LX',
  version: '1',
  chainId: 96369,
  verifyingContract: '0x...' // DEX contract address
};

async function signOrder(order: Order): Promise<string> {
  const data = {
    domain: DOMAIN,
    types: ORDER_TYPES,
    primaryType: 'Order',
    message: {
      symbol: order.symbol,
      side: order.side === 'buy' ? 0 : 1,
      type: order.type === 'limit' ? 0 : 1,
      price: BigInt(Math.floor(parseFloat(order.price) * 1e8)),
      quantity: BigInt(Math.floor(parseFloat(order.quantity) * 1e8)),
      nonce: BigInt(order.nonce),
      expiry: BigInt(order.expiry)
    }
  };

  return await adapter.signTypedData(data);
}

Place Signed Order

async function placeOrder(order: OrderParams) {
  // Generate nonce and expiry
  const nonce = Date.now();
  const expiry = Date.now() + 5 * 60 * 1000; // 5 minutes

  const fullOrder = {
    ...order,
    nonce,
    expiry
  };

  // Sign order
  const signature = await signOrder(fullOrder);

  // Submit to DEX
  const result = await client.submitOrder({
    ...fullOrder,
    signature
  });

  return result;
}

Error Handling

import { WalletErrorCode } from '@luxfi/trading';

const ERROR_MESSAGES: Record<number, string> = {
  4001: 'Transaction rejected by user',
  4100: 'Requested account not authorized',
  4200: 'Requested method not supported',
  4900: 'Provider disconnected',
  4901: 'Chain not connected'
};

function handleWalletError(error: any): string {
  // Standard EIP-1193 errors
  if (error.code && ERROR_MESSAGES[error.code]) {
    return ERROR_MESSAGES[error.code];
  }

  // MetaMask specific errors
  if (error.code === -32002) {
    return 'Request already pending, check MetaMask';
  }

  if (error.code === -32603) {
    return 'Internal error, please try again';
  }

  // Generic fallback
  return error.message || 'Unknown wallet error';
}

Security Best Practices

1. Verify Chain ID

// Always verify chain before operations
async function ensureCorrectChain(): Promise<boolean> {
  const chainId = await adapter.getChainId();
  if (chainId !== 96369) {
    try {
      await adapter.switchNetwork(96369);
      return true;
    } catch {
      return false;
    }
  }
  return true;
}

2. Validate Addresses

import { isAddress, getAddress } from 'ethers';

function validateAddress(address: string): string | null {
  if (!isAddress(address)) {
    return null;
  }
  // Return checksummed address
  return getAddress(address);
}

3. Transaction Simulation

// Simulate before sending
async function simulateTransaction(tx: Transaction): Promise<boolean> {
  try {
    await adapter.estimateGas(tx);
    return true;
  } catch (error) {
    console.error('Transaction would fail:', error);
    return false;
  }
}

4. Spending Limits

// Check and set token allowance
async function ensureAllowance(
  token: string,
  spender: string,
  amount: bigint
): Promise<void> {
  const allowance = await getTokenAllowance(token, spender);

  if (allowance < amount) {
    // Request approval
    await approveToken(token, spender, amount);
  }
}

Testing

Mock Wallet for Tests

import { MockWalletAdapter } from '@luxfi/trading/testing';

const mockWallet = new MockWalletAdapter({
  accounts: ['0x1234...'],
  chainId: 96369,
  signatureResponse: '0xsig...'
});

// Use in tests
const client = new Client({
  endpoint: 'http://localhost:8080',
  wallet: mockWallet
});

// Verify interactions
expect(mockWallet.signTypedDataCalls).toHaveLength(1);

Next Steps