/ pages / api / rpc-proxy.ts
rpc-proxy.ts
  1  import { ChainId } from '@aave/contract-helpers';
  2  import { NextApiRequest, NextApiResponse } from 'next';
  3  
  4  // Documentation: ./server-side-rpc-proxy.md
  5  const NETWORK_CONFIG: Record<number, { network: string; apiKey: string }> = {
  6    // Mainnets
  7    [ChainId.mainnet]: { network: 'eth-mainnet', apiKey: process.env.MAINNET_RPC_API_KEY || '' },
  8    [ChainId.polygon]: { network: 'polygon-mainnet', apiKey: process.env.POLYGON_RPC_API_KEY || '' },
  9    [ChainId.avalanche]: { network: 'avax-mainnet', apiKey: process.env.AVALANCHE_RPC_API_KEY || '' },
 10    [ChainId.arbitrum_one]: {
 11      network: 'arb-mainnet',
 12      apiKey: process.env.ARBITRUM_RPC_API_KEY || '',
 13    },
 14    [ChainId.base]: { network: 'base-mainnet', apiKey: process.env.BASE_RPC_API_KEY || '' },
 15    [ChainId.optimism]: { network: 'opt-mainnet', apiKey: process.env.OPTIMISM_RPC_API_KEY || '' },
 16    [ChainId.metis_andromeda]: {
 17      network: 'metis-mainnet',
 18      apiKey: process.env.METIS_RPC_API_KEY || '',
 19    },
 20    [ChainId.xdai]: { network: 'gnosis-mainnet', apiKey: process.env.GNOSIS_RPC_API_KEY || '' },
 21    [ChainId.bnb]: { network: 'bnb-mainnet', apiKey: process.env.BNB_RPC_API_KEY || '' },
 22    [ChainId.scroll]: { network: 'scroll-mainnet', apiKey: process.env.SCROLL_RPC_API_KEY || '' },
 23    [ChainId.zksync]: { network: 'zksync-mainnet', apiKey: process.env.ZKSYNC_RPC_API_KEY || '' },
 24    [ChainId.linea]: { network: 'linea-mainnet', apiKey: process.env.LINEA_RPC_API_KEY || '' },
 25    [ChainId.sonic]: { network: 'sonic-mainnet', apiKey: process.env.SONIC_RPC_API_KEY || '' },
 26    [ChainId.celo]: { network: 'celo-mainnet', apiKey: process.env.CELO_RPC_API_KEY || '' },
 27  
 28    // Testnets
 29    [ChainId.sepolia]: { network: 'eth-sepolia', apiKey: process.env.MAINNET_RPC_API_KEY || '' },
 30    [ChainId.fuji]: { network: 'avax-fuji', apiKey: process.env.AVALANCHE_RPC_API_KEY || '' },
 31    [ChainId.arbitrum_sepolia]: {
 32      network: 'arb-sepolia',
 33      apiKey: process.env.ARBITRUM_RPC_API_KEY || '',
 34    },
 35    [ChainId.base_sepolia]: { network: 'base-sepolia', apiKey: process.env.BASE_RPC_API_KEY || '' },
 36    [ChainId.optimism_sepolia]: {
 37      network: 'opt-sepolia',
 38      apiKey: process.env.OPTIMISM_RPC_API_KEY || '',
 39    },
 40    [ChainId.scroll_sepolia]: {
 41      network: 'scroll-sepolia',
 42      apiKey: process.env.SCROLL_RPC_API_KEY || '',
 43    },
 44  };
 45  
 46  function getRpcUrl(chainId: number): string | null {
 47    const config = NETWORK_CONFIG[chainId];
 48    if (!config) return null;
 49    return `https://${config.network}.g.alchemy.com/v2/${config.apiKey}`;
 50  }
 51  
 52  export default async function handler(req: NextApiRequest, res: NextApiResponse) {
 53    const allowedOrigins = ['https://app.aave.com', 'https://aave.com'];
 54    const origin = req.headers.origin;
 55  
 56    const isOriginAllowed = (origin: string | undefined): boolean => {
 57      if (!origin) return false;
 58  
 59      if (allowedOrigins.includes(origin)) return true;
 60  
 61      // Match any subdomain ending with avaraxyz.vercel.app for deployment urls
 62      const allowedPatterns = [/^https:\/\/.*avaraxyz\.vercel\.app$/];
 63  
 64      return allowedPatterns.some((pattern) => pattern.test(origin));
 65    };
 66  
 67    if (process.env.CORS_DOMAINS_ALLOWED === 'true') {
 68      res.setHeader('Access-Control-Allow-Origin', '*');
 69    } else if (origin && isOriginAllowed(origin)) {
 70      res.setHeader('Access-Control-Allow-Origin', origin);
 71    }
 72  
 73    res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
 74    res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
 75  
 76    if (req.method === 'OPTIONS') {
 77      return res.status(200).end();
 78    }
 79  
 80    if (req.method !== 'POST') {
 81      return res.status(405).json({ error: 'Method not allowed' });
 82    }
 83  
 84    try {
 85      const { chainId, method, params } = req.body;
 86      const chainIdNumber = typeof chainId === 'string' ? parseInt(chainId) : chainId;
 87      const rpcUrl = getRpcUrl(chainIdNumber);
 88  
 89      if (!rpcUrl) {
 90        return res.status(400).json({ error: `Unsupported chain ID: ${chainIdNumber}` });
 91      }
 92  
 93      const rpcRequest = {
 94        jsonrpc: '2.0',
 95        id: Date.now(),
 96        method,
 97        params,
 98      };
 99  
100      const response = await fetch(rpcUrl, {
101        method: 'POST',
102        headers: {
103          'Content-Type': 'application/json',
104          Origin: origin || 'https://app.aave.com',
105          Referer: 'https://app.aave.com/',
106        },
107        body: JSON.stringify(rpcRequest),
108      });
109  
110      const data = await response.json();
111  
112      return res.status(200).json(data);
113    } catch (error) {
114      console.error('RPC proxy error:', error);
115      return res.status(500).json({ error: 'Internal server error', details: String(error) });
116    }
117  }