---
title: "Multi-Chain dApp Architecture Guide"
sidebar_position: 10
tags: [multi-chain, architecture, cross-chain, TypeScript, blockchain]
keywords: [multi-chain dApp, cross-chain architecture, blockchain integration, TypeScript, state management]
description: "Complete guide to building multi-chain decentralized applications with unified state management, cross-chain operations, and seamless user experience."
last_updated: 2026-10-05
---

# Multi-Chain dApp Architecture Guide

This comprehensive guide covers building robust multi-chain decentralized applications that can interact with multiple blockchains simultaneously, providing users with a seamless cross-chain experience — the patterns below apply directly when your dApp talks to several chains through **bex router**'s unified API.

## Architecture Overview

Multi-chain dApps face unique challenges including:
- **State Management**: Coordinating data across different blockchains
- **Transaction Coordination**: Managing operations across multiple networks
- **Error Handling**: Dealing with varying failure modes
- **User Experience**: Providing unified interface for different chains
- **Performance**: Optimizing for multiple network latencies

## Core Architecture Patterns

### 1. Chain Abstraction Layer

```typescript
// types/blockchain.ts
export enum SupportedChain {
  SUI = 'sui',
  APTOS = 'aptos',
  ETHEREUM = 'ethereum',
  POLYGON = 'polygon',
}

export interface ChainConfig {
  chainId: string;
  name: string;
  rpcUrl: string;
  nativeCurrency: {
    name: string;
    symbol: string;
    decimals: number;
  };
  blockExplorer?: string;
}

export interface BlockchainClient {
  getBalance(address: string): Promise<string>;
  sendTransaction(tx: any): Promise<string>;
  getTransactionStatus(hash: string): Promise<TransactionStatus>;
  estimateGas?(tx: any): Promise<string>;
}

export interface TransactionStatus {
  hash: string;
  status: 'pending' | 'success' | 'failed';
  blockNumber?: number;
  gasUsed?: string;
}
```

### 2. Unified Client Manager

```typescript
// clients/ChainManager.ts
import { SuiClient, getFullnodeUrl } from '@mysten/sui/client';
import { Aptos, AptosConfig, Network } from '@aptos-labs/ts-sdk';
import { SupportedChain, ChainConfig, BlockchainClient } from '../types/blockchain';

export class ChainManager {
  private clients: Map<SupportedChain, BlockchainClient> = new Map();
  private configs: Map<SupportedChain, ChainConfig> = new Map();

  constructor() {
    this.initializeChains();
  }

  private initializeChains() {
    // Initialize Sui
    const suiConfig: ChainConfig = {
      chainId: 'sui-mainnet',
      name: 'Sui Mainnet',
      rpcUrl: process.env.NEXT_PUBLIC_SUI_RPC_URL || getFullnodeUrl('mainnet'),
      nativeCurrency: { name: 'SUI', symbol: 'SUI', decimals: 9 },
      blockExplorer: 'https://suiscan.xyz',
    };
    
    this.configs.set(SupportedChain.SUI, suiConfig);
    this.clients.set(SupportedChain.SUI, new SuiClientAdapter(suiConfig.rpcUrl));

    // Initialize Aptos
    const aptosConfig: ChainConfig = {
      chainId: 'aptos-mainnet',
      name: 'Aptos Mainnet',
      rpcUrl: process.env.NEXT_PUBLIC_APTOS_RPC_URL || 'https://fullnode.mainnet.aptoslabs.com/v1',
      nativeCurrency: { name: 'APT', symbol: 'APT', decimals: 8 },
      blockExplorer: 'https://aptoscan.com',
    };
    
    this.configs.set(SupportedChain.APTOS, aptosConfig);
    this.clients.set(SupportedChain.APTOS, new AptosClientAdapter(aptosConfig.rpcUrl));
  }

  getClient(chain: SupportedChain): BlockchainClient {
    const client = this.clients.get(chain);
    if (!client) {
      throw new Error(`Client not initialized for chain: ${chain}`);
    }
    return client;
  }

  getConfig(chain: SupportedChain): ChainConfig {
    const config = this.configs.get(chain);
    if (!config) {
      throw new Error(`Config not found for chain: ${chain}`);
    }
    return config;
  }

  getSupportedChains(): SupportedChain[] {
    return Array.from(this.clients.keys());
  }

  async getBalanceAcrossChains(address: string): Promise<Record<SupportedChain, string>> {
    const balances: Record<string, string> = {};
    
    const promises = Array.from(this.clients.entries()).map(async ([chain, client]) => {
      try {
        const balance = await client.getBalance(address);
        balances[chain] = balance;
      } catch (error) {
        console.error(`Error fetching balance for ${chain}:`, error);
        balances[chain] = '0';
      }
    });

    await Promise.allSettled(promises);
    return balances as Record<SupportedChain, string>;
  }
}
```

### 3. Chain-Specific Adapters

```typescript
// clients/SuiClientAdapter.ts
import { SuiClient } from '@mysten/sui/client';
import { BlockchainClient, TransactionStatus } from '../types/blockchain';

export class SuiClientAdapter implements BlockchainClient {
  private client: SuiClient;

  constructor(rpcUrl: string) {
    this.client = new SuiClient({ url: rpcUrl });
  }

  async getBalance(address: string): Promise<string> {
    try {
      const balance = await this.client.getBalance({ owner: address });
      return balance.totalBalance;
    } catch (error) {
      console.error('Error fetching Sui balance:', error);
      return '0';
    }
  }

  async sendTransaction(tx: any): Promise<string> {
    try {
      const result = await this.client.signAndExecuteTransaction({
        transaction: tx.transaction,
        signer: tx.signer,
      });
      return result.digest;
    } catch (error) {
      console.error('Error sending Sui transaction:', error);
      throw error;
    }
  }

  async getTransactionStatus(hash: string): Promise<TransactionStatus> {
    try {
      const result = await this.client.getTransactionBlock({ digest: hash });
      
      return {
        hash,
        status: result.effects?.status?.status === 'success' ? 'success' : 'failed',
        blockNumber: Number(result.checkpoint),
        gasUsed: result.effects?.gasUsed?.computationCost,
      };
    } catch (error) {
      console.error('Error fetching Sui transaction status:', error);
      return { hash, status: 'pending' };
    }
  }
}
```

```typescript
// clients/AptosClientAdapter.ts
import { Aptos, AptosConfig } from '@aptos-labs/ts-sdk';
import { BlockchainClient, TransactionStatus } from '../types/blockchain';

export class AptosClientAdapter implements BlockchainClient {
  private client: Aptos;

  constructor(rpcUrl: string) {
    this.client = new Aptos(new AptosConfig({ fullnode: rpcUrl }));
  }

  async getBalance(address: string): Promise<string> {
    try {
      const resources = await this.client.getAccountResources({
        accountAddress: address,
      });
      
      const coinStore = resources.find(
        (resource) => resource.type === '0x1::coin::CoinStore&lt;0x1::aptos_coin::AptosCoin>'
      );
      
      if (coinStore?.data) {
        return (coinStore.data as any).coin.value;
      }
      return '0';
    } catch (error) {
      console.error('Error fetching Aptos balance:', error);
      return '0';
    }
  }

  async sendTransaction(tx: any): Promise<string> {
    try {
      const result = await this.client.signAndSubmitTransaction({
        signer: tx.signer,
        transaction: tx.transaction,
      });
      
      await this.client.waitForTransaction({ transactionHash: result.hash });
      return result.hash;
    } catch (error) {
      console.error('Error sending Aptos transaction:', error);
      throw error;
    }
  }

  async getTransactionStatus(hash: string): Promise<TransactionStatus> {
    try {
      const result = await this.client.getTransactionByHash({ transactionHash: hash });
      
      return {
        hash,
        status: result.success ? 'success' : 'failed',
        blockNumber: Number(result.version),
        gasUsed: result.gas_used,
      };
    } catch (error) {
      console.error('Error fetching Aptos transaction status:', error);
      return { hash, status: 'pending' };
    }
  }

  async estimateGas(tx: any): Promise<string> {
    try {
      const simulation = await this.client.transaction.simulate.simple({
        signerPublicKey: tx.signer.publicKey,
        transaction: tx.transaction,
      });
      
      return simulation[0].gas_used || '0';
    } catch (error) {
      console.error('Error estimating Aptos gas:', error);
      return '0';
    }
  }
}
```

## State Management Architecture

### 1. Multi-Chain Store

```typescript
// store/multiChainStore.ts
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
import { SupportedChain } from '../types/blockchain';

interface ChainState {
  selectedChain: SupportedChain;
  connectedChains: Set<SupportedChain>;
  balances: Record<SupportedChain, string>;
  transactions: Record<SupportedChain, TransactionHistory[]>;
  isLoading: Record<SupportedChain, boolean>;
  errors: Record<SupportedChain, string | null>;
}

interface ChainActions {
  setSelectedChain: (chain: SupportedChain) => void;
  addConnectedChain: (chain: SupportedChain) => void;
  removeConnectedChain: (chain: SupportedChain) => void;
  updateBalance: (chain: SupportedChain, balance: string) => void;
  addTransaction: (chain: SupportedChain, tx: TransactionHistory) => void;
  setLoading: (chain: SupportedChain, loading: boolean) => void;
  setError: (chain: SupportedChain, error: string | null) => void;
  clearErrors: () => void;
}

interface TransactionHistory {
  hash: string;
  type: string;
  amount: string;
  timestamp: number;
  status: 'pending' | 'success' | 'failed';
}

export const useMultiChainStore = create<ChainState & ChainActions>()(
  devtools(
    persist(
      (set, get) => ({
        // Initial state
        selectedChain: SupportedChain.SUI,
        connectedChains: new Set(),
        balances: {} as Record<SupportedChain, string>,
        transactions: {} as Record<SupportedChain, TransactionHistory[]>,
        isLoading: {} as Record<SupportedChain, boolean>,
        errors: {} as Record<SupportedChain, string | null>,

        // Actions
        setSelectedChain: (chain) => set({ selectedChain: chain }),
        
        addConnectedChain: (chain) =>
          set((state) => ({
            connectedChains: new Set([...state.connectedChains, chain]),
          })),
        
        removeConnectedChain: (chain) =>
          set((state) => {
            const newConnected = new Set(state.connectedChains);
            newConnected.delete(chain);
            return { connectedChains: newConnected };
          }),
        
        updateBalance: (chain, balance) =>
          set((state) => ({
            balances: { ...state.balances, [chain]: balance },
          })),
        
        addTransaction: (chain, tx) =>
          set((state) => ({
            transactions: {
              ...state.transactions,
              [chain]: [tx, ...(state.transactions[chain] || [])].slice(0, 50), // Keep latest 50
            },
          })),
        
        setLoading: (chain, loading) =>
          set((state) => ({
            isLoading: { ...state.isLoading, [chain]: loading },
          })),
        
        setError: (chain, error) =>
          set((state) => ({
            errors: { ...state.errors, [chain]: error },
          })),
        
        clearErrors: () => set({ errors: {} as Record<SupportedChain, string | null> }),
      }),
      {
        name: 'multi-chain-store',
        partialize: (state) => ({
          selectedChain: state.selectedChain,
          transactions: state.transactions,
        }),
      }
    ),
    { name: 'multi-chain-store' }
  )
);
```

### 2. Multi-Chain React Hooks

```typescript
// hooks/useMultiChain.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useMultiChainStore } from '../store/multiChainStore';
import { ChainManager } from '../clients/ChainManager';
import { SupportedChain } from '../types/blockchain';

const chainManager = new ChainManager();

export function useMultiChainBalances(address?: string) {
  const { connectedChains, updateBalance, setLoading, setError } = useMultiChainStore();

  return useQuery({
    queryKey: ['multichain-balances', address, Array.from(connectedChains)],
    queryFn: async () => {
      if (!address) return {};

      const results: Record<SupportedChain, string> = {} as Record<SupportedChain, string>;
      
      for (const chain of connectedChains) {
        try {
          setLoading(chain, true);
          const client = chainManager.getClient(chain);
          const balance = await client.getBalance(address);
          results[chain] = balance;
          updateBalance(chain, balance);
          setError(chain, null);
        } catch (error) {
          console.error(`Error fetching balance for ${chain}:`, error);
          setError(chain, error instanceof Error ? error.message : 'Unknown error');
          results[chain] = '0';
        } finally {
          setLoading(chain, false);
        }
      }

      return results;
    },
    enabled: !!address && connectedChains.size > 0,
    refetchInterval: 10000, // Refetch every 10 seconds
  });
}

export function useMultiChainTransaction() {
  const queryClient = useQueryClient();
  const { addTransaction, setError } = useMultiChainStore();

  return useMutation({
    mutationFn: async ({
      chain,
      transaction,
      type,
      amount,
    }: {
      chain: SupportedChain;
      transaction: any;
      type: string;
      amount: string;
    }) => {
      const client = chainManager.getClient(chain);
      const hash = await client.sendTransaction(transaction);
      
      // Add to transaction history
      addTransaction(chain, {
        hash,
        type,
        amount,
        timestamp: Date.now(),
        status: 'pending',
      });

      return { hash, chain };
    },
    onSuccess: (data) => {
      // Invalidate balance queries to refresh data
      queryClient.invalidateQueries({ queryKey: ['multichain-balances'] });
      setError(data.chain, null);
    },
    onError: (error, variables) => {
      setError(variables.chain, error instanceof Error ? error.message : 'Transaction failed');
    },
  });
}

export function useChainSwitcher() {
  const { selectedChain, setSelectedChain, connectedChains } = useMultiChainStore();

  const switchChain = (chain: SupportedChain) => {
    if (connectedChains.has(chain)) {
      setSelectedChain(chain);
    } else {
      throw new Error(`Chain ${chain} is not connected`);
    }
  };

  return {
    selectedChain,
    switchChain,
    availableChains: Array.from(connectedChains),
  };
}
```

## Cross-Chain Operations

### 1. Cross-Chain Bridge Integration

```typescript
// services/CrossChainBridge.ts
import { SupportedChain } from '../types/blockchain';

export interface BridgeOperation {
  id: string;
  sourceChain: SupportedChain;
  targetChain: SupportedChain;
  asset: string;
  amount: string;
  status: 'initiated' | 'pending' | 'completed' | 'failed';
  sourceTxHash?: string;
  targetTxHash?: string;
  estimatedTime: number; // in minutes
}

export class CrossChainBridge {
  private operations: Map<string, BridgeOperation> = new Map();

  async initiateBridge({
    sourceChain,
    targetChain,
    asset,
    amount,
    sourceAddress,
    targetAddress,
  }: {
    sourceChain: SupportedChain;
    targetChain: SupportedChain;
    asset: string;
    amount: string;
    sourceAddress: string;
    targetAddress: string;
  }): Promise<BridgeOperation> {
    
    const operationId = `bridge_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
    
    const operation: BridgeOperation = {
      id: operationId,
      sourceChain,
      targetChain,
      asset,
      amount,
      status: 'initiated',
      estimatedTime: this.getEstimatedBridgeTime(sourceChain, targetChain),
    };

    this.operations.set(operationId, operation);

    try {
      // Step 1: Lock/burn assets on source chain
      const sourceTxHash = await this.lockAssetsOnSource({
        chain: sourceChain,
        asset,
        amount,
        address: sourceAddress,
      });

      operation.sourceTxHash = sourceTxHash;
      operation.status = 'pending';

      // Step 2: Monitor source transaction and mint on target
      this.monitorBridgeOperation(operationId, targetAddress);

      return operation;
    } catch (error) {
      operation.status = 'failed';
      throw error;
    }
  }

  private async lockAssetsOnSource({
    chain,
    asset,
    amount,
    address,
  }: {
    chain: SupportedChain;
    asset: string;
    amount: string;
    address: string;
  }): Promise<string> {
    // Implementation depends on the specific bridge protocol
    // This is a simplified example
    
    if (chain === SupportedChain.SUI) {
      return this.lockAssetsOnSui(asset, amount, address);
    } else if (chain === SupportedChain.APTOS) {
      return this.lockAssetsOnAptos(asset, amount, address);
    }
    
    throw new Error(`Bridge not supported for chain: ${chain}`);
  }

  private async lockAssetsOnSui(asset: string, amount: string, address: string): Promise<string> {
    // Implement Sui-specific bridge logic
    // This would involve calling the bridge contract on Sui
    throw new Error('Sui bridge not implemented');
  }

  private async lockAssetsOnAptos(asset: string, amount: string, address: string): Promise<string> {
    // Implement Aptos-specific bridge logic
    // This would involve calling the bridge contract on Aptos
    throw new Error('Aptos bridge not implemented');
  }

  private monitorBridgeOperation(operationId: string, targetAddress: string) {
    const operation = this.operations.get(operationId);
    if (!operation) return;

    // In a real implementation, this would:
    // 1. Monitor the source transaction for confirmation
    // 2. Trigger the mint/unlock on the target chain
    // 3. Update the operation status
    
    setTimeout(() => {
      operation.status = 'completed';
      operation.targetTxHash = 'mock_target_tx_hash';
    }, operation.estimatedTime * 60 * 1000);
  }

  private getEstimatedBridgeTime(source: SupportedChain, target: SupportedChain): number {
    // Return estimated time in minutes based on chain pair
    const bridgeTimes: Record<string, number> = {
      'sui_aptos': 15,
      'aptos_sui': 15,
      'sui_ethereum': 30,
      'ethereum_sui': 30,
      'aptos_ethereum': 30,
      'ethereum_aptos': 30,
    };
    
    return bridgeTimes[`${source}_${target}`] || 60;
  }

  getBridgeOperation(operationId: string): BridgeOperation | undefined {
    return this.operations.get(operationId);
  }

  getAllOperations(): BridgeOperation[] {
    return Array.from(this.operations.values());
  }
}
```

### 2. Multi-Chain Portfolio Management

```typescript
// services/PortfolioManager.ts
import { SupportedChain } from '../types/blockchain';
import { ChainManager } from '../clients/ChainManager';

export interface PortfolioAsset {
  chain: SupportedChain;
  address: string;
  symbol: string;
  name: string;
  balance: string;
  decimals: number;
  usdValue?: string;
  pricePerToken?: string;
}

export interface Portfolio {
  totalUsdValue: string;
  assets: PortfolioAsset[];
  chainDistribution: Record<SupportedChain, number>; // Percentage
}

export class PortfolioManager {
  private chainManager: ChainManager;
  private priceCache: Map<string, number> = new Map();

  constructor(chainManager: ChainManager) {
    this.chainManager = chainManager;
  }

  async getPortfolio(address: string): Promise<Portfolio> {
    const assets: PortfolioAsset[] = [];
    let totalUsdValue = 0;

    // Get native assets from each chain
    for (const chain of this.chainManager.getSupportedChains()) {
      try {
        const client = this.chainManager.getClient(chain);
        const config = this.chainManager.getConfig(chain);
        const balance = await client.getBalance(address);
        
        const asset: PortfolioAsset = {
          chain,
          address: 'native',
          symbol: config.nativeCurrency.symbol,
          name: config.nativeCurrency.name,
          balance,
          decimals: config.nativeCurrency.decimals,
        };

        // Get USD value
        const price = await this.getTokenPrice(asset.symbol);
        if (price) {
          const balanceInTokens = Number(balance) / Math.pow(10, asset.decimals);
          asset.usdValue = (balanceInTokens * price).toFixed(2);
          asset.pricePerToken = price.toString();
          totalUsdValue += Number(asset.usdValue);
        }

        assets.push(asset);
      } catch (error) {
        console.error(`Error fetching portfolio for ${chain}:`, error);
      }
    }

    // Calculate chain distribution
    const chainDistribution: Record<SupportedChain, number> = {} as Record<SupportedChain, number>;
    for (const asset of assets) {
      const usdValue = Number(asset.usdValue || 0);
      const percentage = totalUsdValue > 0 ? (usdValue / totalUsdValue) * 100 : 0;
      chainDistribution[asset.chain] = percentage;
    }

    return {
      totalUsdValue: totalUsdValue.toFixed(2),
      assets,
      chainDistribution,
    };
  }

  private async getTokenPrice(symbol: string): Promise<number | null> {
    // Check cache first
    if (this.priceCache.has(symbol)) {
      return this.priceCache.get(symbol)!;
    }

    try {
      // In a real implementation, you'd use a price API like CoinGecko
      const response = await fetch(
        `https://api.coingecko.com/api/v3/simple/price?ids=${symbol.toLowerCase()}&vs_currencies=usd`
      );
      const data = await response.json();
      const price = data[symbol.toLowerCase()]?.usd;
      
      if (price) {
        this.priceCache.set(symbol, price);
        // Cache for 5 minutes
        setTimeout(() => this.priceCache.delete(symbol), 5 * 60 * 1000);
      }
      
      return price || null;
    } catch (error) {
      console.error(`Error fetching price for ${symbol}:`, error);
      return null;
    }
  }

  async rebalancePortfolio(
    address: string,
    targetDistribution: Record<SupportedChain, number>
  ): Promise<void> {
    const currentPortfolio = await this.getPortfolio(address);
    
    // Calculate needed rebalancing operations
    const operations: Array<{
      sourceChain: SupportedChain;
      targetChain: SupportedChain;
      amount: string;
    }> = [];

    for (const [chain, targetPercent] of Object.entries(targetDistribution)) {
      const currentPercent = currentPortfolio.chainDistribution[chain as SupportedChain] || 0;
      const difference = targetPercent - currentPercent;
      
      if (Math.abs(difference) > 1) { // Only rebalance if difference > 1%
        // Calculate amount to move
        const targetUsdValue = (Number(currentPortfolio.totalUsdValue) * targetPercent) / 100;
        const currentUsdValue = (Number(currentPortfolio.totalUsdValue) * currentPercent) / 100;
        const amountToMove = Math.abs(targetUsdValue - currentUsdValue).toString();
        
        if (difference > 0) {
          // Need to move assets TO this chain
          const sourceChain = this.findBestSourceChain(currentPortfolio, amountToMove);
          if (sourceChain) {
            operations.push({
              sourceChain,
              targetChain: chain as SupportedChain,
              amount: amountToMove,
            });
          }
        }
      }
    }

    // Execute rebalancing operations
    console.log('Rebalancing operations:', operations);
    // Implementation would execute these operations using the bridge
  }

  private findBestSourceChain(
    portfolio: Portfolio,
    amountNeeded: string
  ): SupportedChain | null {
    // Find the chain with the highest balance that can cover the amount needed
    const sortedAssets = portfolio.assets
      .filter(asset => Number(asset.usdValue || 0) >= Number(amountNeeded))
      .sort((a, b) => Number(b.usdValue || 0) - Number(a.usdValue || 0));
    
    return sortedAssets[0]?.chain || null;
  }
}
```

## User Interface Components

### 1. Chain Selector Component

```typescript
// components/ChainSelector.tsx
import React from 'react';
import { useChainSwitcher } from '../hooks/useMultiChain';
import { useMultiChainStore } from '../store/multiChainStore';
import { SupportedChain } from '../types/blockchain';

export function ChainSelector() {
  const { selectedChain, switchChain, availableChains } = useChainSwitcher();
  const { isLoading, errors } = useMultiChainStore();

  const chainIcons: Record<SupportedChain, string> = {
    [SupportedChain.SUI]: '/icons/sui.svg',
    [SupportedChain.APTOS]: '/icons/aptos.svg',
    [SupportedChain.ETHEREUM]: '/icons/ethereum.svg',
    [SupportedChain.POLYGON]: '/icons/polygon.svg',
  };

  const chainNames: Record<SupportedChain, string> = {
    [SupportedChain.SUI]: 'Sui',
    [SupportedChain.APTOS]: 'Aptos',
    [SupportedChain.ETHEREUM]: 'Ethereum',
    [SupportedChain.POLYGON]: 'Polygon',
  };

  return (
    <div className="chain-selector">
      <h3>Select Chain</h3>
      <div className="chain-grid">
        {availableChains.map((chain) => (
          <button
            key={chain}
            className={`chain-button ${selectedChain === chain ? 'selected' : ''} ${
              isLoading[chain] ? 'loading' : ''
            } ${errors[chain] ? 'error' : ''}`}
            onClick={() => switchChain(chain)}
            disabled={isLoading[chain]}
          >
            <img src={chainIcons[chain]} alt={chainNames[chain]} />
            <span>{chainNames[chain]}</span>
            {isLoading[chain] && <div className="spinner" />}
            {errors[chain] && <div className="error-indicator">!</div>}
          </button>
        ))}
      </div>
      
      {errors[selectedChain] && (
        <div className="error-message">
          Error: {errors[selectedChain]}
        </div>
      )}
    </div>
  );
}
```

### 2. Multi-Chain Portfolio Dashboard

```typescript
// components/PortfolioDashboard.tsx
import React, { useEffect, useState } from 'react';
import { useMultiChainBalances } from '../hooks/useMultiChain';
import { PortfolioManager, Portfolio } from '../services/PortfolioManager';
import { ChainManager } from '../clients/ChainManager';

interface PortfolioDashboardProps {
  address: string;
}

export function PortfolioDashboard({ address }: PortfolioDashboardProps) {
  const [portfolio, setPortfolio] = useState<Portfolio | null>(null);
  const [loading, setLoading] = useState(true);
  const { data: balances } = useMultiChainBalances(address);

  useEffect(() => {
    const loadPortfolio = async () => {
      if (!address) return;
      
      setLoading(true);
      try {
        const chainManager = new ChainManager();
        const portfolioManager = new PortfolioManager(chainManager);
        const portfolioData = await portfolioManager.getPortfolio(address);
        setPortfolio(portfolioData);
      } catch (error) {
        console.error('Error loading portfolio:', error);
      } finally {
        setLoading(false);
      }
    };

    loadPortfolio();
  }, [address]);

  if (loading) {
    return <div className="loading">Loading portfolio...</div>;
  }

  if (!portfolio) {
    return <div className="error">Failed to load portfolio</div>;
  }

  return (
    <div className="portfolio-dashboard">
      <div className="portfolio-header">
        <h2>Multi-Chain Portfolio</h2>
        <div className="total-value">
          Total Value: ${portfolio.totalUsdValue}
        </div>
      </div>

      <div className="chain-distribution">
        <h3>Chain Distribution</h3>
        <div className="distribution-chart">
          {Object.entries(portfolio.chainDistribution).map(([chain, percentage]) => (
            <div key={chain} className="distribution-bar">
              <span className="chain-name">{chain}</span>
              <div className="bar-container">
                <div 
                  className="bar-fill" 
                  style={{ width: `${percentage}%` }}
                />
              </div>
              <span className="percentage">{percentage.toFixed(1)}%</span>
            </div>
          ))}
        </div>
      </div>

      <div className="assets-list">
        <h3>Assets</h3>
        <div className="assets-grid">
          {portfolio.assets.map((asset, index) => (
            <div key={index} className="asset-card">
              <div className="asset-info">
                <span className="asset-symbol">{asset.symbol}</span>
                <span className="asset-chain">{asset.chain}</span>
              </div>
              <div className="asset-balance">
                <span className="balance">
                  {(Number(asset.balance) / Math.pow(10, asset.decimals)).toFixed(4)}
                </span>
                {asset.usdValue && (
                  <span className="usd-value">${asset.usdValue}</span>
                )}
              </div>
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}
```

## Testing Multi-Chain Applications

### 1. Integration Tests

```typescript
// __tests__/multiChain.test.ts
import { describe, it, expect, beforeEach } from '@jest/globals';
import { ChainManager } from '../clients/ChainManager';
import { SupportedChain } from '../types/blockchain';

describe('Multi-Chain Integration', () => {
  let chainManager: ChainManager;

  beforeEach(() => {
    chainManager = new ChainManager();
  });

  it('should initialize all supported chains', () => {
    const supportedChains = chainManager.getSupportedChains();
    expect(supportedChains).toContain(SupportedChain.SUI);
    expect(supportedChains).toContain(SupportedChain.APTOS);
  });

  it('should get client for each chain', () => {
    const suiClient = chainManager.getClient(SupportedChain.SUI);
    const aptosClient = chainManager.getClient(SupportedChain.APTOS);
    
    expect(suiClient).toBeDefined();
    expect(aptosClient).toBeDefined();
  });

  it('should fetch balances across chains', async () => {
    const mockAddress = '0x123';
    const balances = await chainManager.getBalanceAcrossChains(mockAddress);
    
    expect(balances).toHaveProperty(SupportedChain.SUI);
    expect(balances).toHaveProperty(SupportedChain.APTOS);
  });
});
```

### 2. Component Tests

```typescript
// __tests__/ChainSelector.test.tsx
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ChainSelector } from '../components/ChainSelector';
import { useChainSwitcher } from '../hooks/useMultiChain';

jest.mock('../hooks/useMultiChain');
const mockUseChainSwitcher = useChainSwitcher as jest.MockedFunction<typeof useChainSwitcher>;

describe('ChainSelector', () => {
  let queryClient: QueryClient;

  beforeEach(() => {
    queryClient = new QueryClient({
      defaultOptions: { queries: { retry: false } },
    });
  });

  it('should render available chains', () => {
    mockUseChainSwitcher.mockReturnValue({
      selectedChain: SupportedChain.SUI,
      switchChain: jest.fn(),
      availableChains: [SupportedChain.SUI, SupportedChain.APTOS],
    });

    render(
      <QueryClientProvider client={queryClient}>
        <ChainSelector />
      </QueryClientProvider>
    );

    expect(screen.getByText('Sui')).toBeInTheDocument();
    expect(screen.getByText('Aptos')).toBeInTheDocument();
  });

  it('should call switchChain when chain is clicked', () => {
    const mockSwitchChain = jest.fn();
    mockUseChainSwitcher.mockReturnValue({
      selectedChain: SupportedChain.SUI,
      switchChain: mockSwitchChain,
      availableChains: [SupportedChain.SUI, SupportedChain.APTOS],
    });

    render(
      <QueryClientProvider client={queryClient}>
        <ChainSelector />
      </QueryClientProvider>
    );

    fireEvent.click(screen.getByText('Aptos'));
    expect(mockSwitchChain).toHaveBeenCalledWith(SupportedChain.APTOS);
  });
});
```

## Best Practices Summary

1. **Abstract Chain Differences**: Use adapters to provide unified interfaces
2. **Centralized State Management**: Use a single store for multi-chain state
3. **Error Isolation**: Handle chain-specific errors independently
4. **Performance Optimization**: Cache data and batch operations when possible
5. **User Experience**: Provide clear feedback for cross-chain operations
6. **Security**: Validate operations on each chain independently
7. **Testing**: Test each chain integration thoroughly
8. **Monitoring**: Track operations across all chains
9. **Graceful Degradation**: Handle chain outages gracefully
10. **Documentation**: Document chain-specific behaviors and limitations

## Next Steps

- Implement API optimizations for better performance
- Follow security best practices for production deployment
- Check out specific chain guides: [Sui dApp Development](./sui/sui-dapp-development.md) and [Aptos dApp Development](./aptos/aptos-dapp-development.md)

## Resources

- [Sui TypeScript SDK](https://sdk.mystenlabs.com/)
- [Aptos TypeScript SDK](https://aptos-labs.github.io/aptos-ts-sdk/)
- Cross-Chain Bridge Protocols
- bex.co Multi-Chain APIs