DEX聚合器开发教程

本教程展示了如何构建一个简单的多链DEX聚合器仪表板。该应用程序比较池子并根据给定的输入金额对其估计输出进行排序。

DEX聚合器开发教程
一键发币: Aptos | X Layer | SUI | SOL | BNB | ETH | BASE | ARB | OP | Polygon | Avalanche | 用AI学区块链开发

在去中心化金融(DeFi)中,流动性分散在数百个去中心化交易所(DEX)中,导致相同的代币对在不同价格上交易。这意味着交易者可能会错过最佳的交换率。

例如,当用ETH兑换USDC时,多个流动性池存在于不同的DEX如Uniswap或SushiSwap中。每个池可能有不同的流动性水平和费用,导致输出金额不同。DEX聚合器可以自动查找多个DEX的最佳利率,避免耗时的手动检查和次优交易。

本分步教程展示了如何使用CoinGecko API构建一个简单的多链DEX聚合器仪表板。该应用程序比较池子并根据给定的输入金额对其估计输出进行排序。

1、什么是DEX聚合器?

DEX聚合器是一种工具,它可以查询多个DEX来为给定的代币对和数量找到最佳的交换率。

现代聚合器如1inch、Paraswap和Matcha已成为DeFi基础设施的重要组成部分,每月处理数十亿美元的交易量。构建自己的聚合器提供了以下优势:

  • 最佳价格执行:同时扫描多个DEX以找到最佳的汇率。
  • 知情交易决策:提供关于流动性和费用的数据,帮助交易者了解哪些池子可以处理大额交易并最小化价格影响(滑点)。
  • 时间效率:节省手动检查多个DEX界面的价格和流动性的时间。

2、构建DEX聚合器的最佳API是什么?

CoinGecko API拥有超过20个链上DEX端点,是构建DEX聚合器的绝佳选择,因为它提供了广泛的DEX和网络支持的数据,并且所有数据都通过GeckoTerminal进行整合,只需一个API即可。其可靠的端点和一致的数据模式使得发现和比较池子变得简单。

以下是我们的DEX聚合器所使用的关键端点:

3、前提条件

您需要以下内容来构建DEX聚合器:

  • CoinGecko API密钥:本教程中使用的链上端点可在免费演示API计划中获得。如果您没有密钥,请按照此指南获取您的免费演示API密钥
  • 应用框架:本指南使用React 18作为前端,以及Express代理服务器作为后端。您还需要Node.js(版本18或更高)来运行应用程序。

4、设置项目环境

让我们从设置DEX聚合器的开发环境开始。我们将使用Node.js和Express作为后端,React作为前端。

4.1 初始化项目

首先,创建一个新目录并初始化Node.js项目:

mkdir dex-aggregator-dashboard
cd dex-aggregator-dashboard
npm init -y

4.2 安装依赖项

安装前后端所需的包:

npm install express axios cors dotenv
npm install --save-dev nodemon

4.3 创建项目结构

设置基本文件结构:

dex-aggregator-dashboard/
├── server.js          # Express backend
├── package.json       # Dependencies
├── .env               # API keys (create this)
└── public/            # Frontend files
    ├── index.html     # Main HTML
    ├── app.jsx        # React application
    └── styles.css     # Styling

4.4 配置环境变量

在项目根目录中创建一个.env文件并添加您的API密钥:

CG_DEMO_API_KEY=your_demo_key_here
CG_PRO_API_KEY=your_pro_key_here   # optional

5、如何找到特定代币对的所有可用交换池?

第一步是使用 /onchain/networks/{network}/tokens/{token_address}/pools 端点来获取选定网络下所有潜在池的列表。该端点返回特定网络上包含“基础”代币(您想要交换的代币)的所有流动性池。然后我们过滤这些结果以找到也包含“报价”代币(您想要接收的代币)的池。

这是一个脚本函数,它接受网络ID和代币地址作为输入并获取池数据:

// Function calls: GET /onchain/networks/{network}/tokens/{token_address}/pools
// Example: https://api.coingecko.com/api/v3/onchain/networks/eth/tokens/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/pools
async function discoverPoolsForToken(network, tokenAddress) {
  try {
    const response = await cgFetch(`/networks/${encodeURIComponent(network)}/tokens/${encodeURIComponent(tokenAddress)}/pools`);
    const pools = Array.isArray(response?.data) ? response.data : [];
    console.log(`Found ${pools.length} pools containing token ${tokenAddress} on ${network}`);
    return pools;
  } catch (error) {
    console.error('Error discovering pools:', error);
    throw error;
  }
}

要查找特定代币对的池,我们调用此函数并过滤结果:

// Complete implementation in server.js
app.get('/api/pools', async (req, res) => {
  const { network = 'eth', from, to } = req.query;
  if (!from || !to) return res.status(400).json({ error: 'Missing query params: from, to' });
 
  try {
    // 1) Discover all pools containing the 'from' token
    const pools = await discoverPoolsForToken(network, from);
   
    // 2) Filter to pools that also contain the 'to' token
    const toLc = String(to).toLowerCase();
    const matchedPools = pools.filter((pool) => {
      const baseTokenId = pool?.relationships?.base_token?.data?.id || '';
      const quoteTokenId = pool?.relationships?.quote_token?.data?.id || '';
      const baseAddr = baseTokenId.split('_')[1] || '';
      const quoteAddr = quoteTokenId.split('_')[1] || '';
      return baseAddr.toLowerCase() === toLc || quoteAddr.toLowerCase() === toLc;
    });
   
    console.log(`Matched ${matchedPools.length} pools for pair ${from}/${to}`);
    res.json({ data: matchedPools });
  } catch (error) {
    res.status(500).json({ error: 'Failed to fetch pools' });
  }
});

示例(简化的)发现响应:

6、在池之间比较汇率

有了上一步中得到的相关池地址列表,我们现在可以使用 /onchain/networks/{network}/pools/multi/{addresses} 端点来获取我们比较所需的具体数据。这个批量端点比为每个池单独请求更高效。

这是函数,它接受上一步中的地址数组,正确格式化并调用此端点:

// Get detailed pool information for comparison
app.get('/api/pools', async (req, res) => {
  const { network = 'eth', from, to } = req.query;
  if (!from || !to) return res.status(400).json({ error: 'Missing query params: from, to' });
 
  try {
    // 1) Discover pools involving the 'from' token
    const discovered = await cgFetch(`/networks/${encodeURIComponent(network)}/tokens/${encodeURIComponent(from)}/pools`);
    const pools = Array.isArray(discovered?.data) ? discovered.data : [];
   
    // 2) Filter to pools that pair with the 'to' token  
    const toLc = String(to).toLowerCase();
    const matched = pools.filter((p) => {
      const baseId = p?.relationships?.base_token?.data?.id || '';
      const quoteId = p?.relationships?.quote_token?.data?.id || '';
      const baseAddr = baseId.split('_')[1] || '';
      const quoteAddr = quoteId.split('_')[1] || '';
      return baseAddr.toLowerCase() === toLc || quoteAddr.toLowerCase() === toLc;
    });


    // 3) Extract pool addresses from discovery results
    const poolAddresses = matched
      .map(pool => pool?.attributes?.address)
      .filter(Boolean); // Remove any undefined addresses


    // 4) Fetch detailed info in batches via multi endpoint
    const chunkSize = 50;
    const results = [];
   
    for (let i = 0; i < poolAddresses.length; i += chunkSize) {
      const chunk = poolAddresses.slice(i, i + chunkSize);
      // Format addresses as comma-separated string for multi endpoint
      const addressesParam = chunk.join(',');
     
      // Call multi endpoint: GET /onchain/networks/{network}/pools/multi/{addresses}
      // Example: https://api.coingecko.com/api/v3/onchain/networks/eth/pools/multi/0xabc...,0xdef...,0x123...
      const detail = await cgFetch(`/networks/${encodeURIComponent(network)}/pools/multi/${addressesParam}`);
      if (Array.isArray(detail?.data)) results.push(...detail.data);
    }
   
    res.json({ data: results });
  } catch (e) {
    res.status(500).json({ error: 'Failed to fetch pools', details: e?.response?.data || e.message });
  }
});

多端点返回详细的池信息,包括价格、流动性及费用。这里是一个显示关键属性的示例响应:

用于费率比较的关键属性:

  • base_token_price_usd / quote_token_price_usd: 池中每种代币的当前美元价格,这对于在仪表板上显示美元价值很有用。
  • base_token_price_quote_token / quote_token_price_base_token: 池中两种代币之间的直接汇率。这就是我们用来计算交换输出的值。
  • reserve_in_usd: 池中的总流动性,以美元计。更高的流动性通常意味着大额交易的滑点更低。
  • pool_fee_percentage: 该池收取的费用(例如,“0.05”表示0.05%的费用)。较低的费用会为交易者带来更好的净输出。

7、如何计算最佳交换报价?

最佳报价是通过基于每个池的实时价格数据和用户的输入金额计算最终输出金额来找到的。然后我们根据估计的输出对池进行排序,最高的输出代表交易者的最佳交易。

这里是一个简化公式,用于计算估计的输出:

estimated_output = amount_in × price_factor × (1 − pool_fee_percentage/100) 💡 提示: 一些聚合器使用更先进的计算方法,考虑交易规模和池深度对价格影响的因素。要获得显示池流动性深度的交易和成交量分解,调用 /onchain/networks/{network}/pools/multi/{addresses} 端点时传递 include_volume_breakdown=true

这段代码遍历详细的池数据,对每个池进行计算,并返回按最佳报价排序的列表:

// Calculate and rank pools by best swap quote
function calculateBestQuotes(poolDetails, { amountIn, fromToken, toToken }) {
  const results = [];
  for (const pool of poolDetails) {
    const attr = pool.attributes || {};
    const rel = pool.relationships || {};
    // Extract token addresses from pool relationships
    const baseId = rel.base_token?.data?.id || '';
    const quoteId = rel.quote_token?.data?.id || '';
    const baseAddr = baseId.split('_')[1]?.toLowerCase() || '';
    const quoteAddr = quoteId.split('_')[1]?.toLowerCase() || '';
    const fromLc = fromToken.toLowerCase();
    const toLc = toToken.toLowerCase();
    // Determine the correct price factor based on swap direction
    let priceFactor = null;
    if (baseAddr === fromLc && quoteAddr === toLc) {
      // Swapping base token for quote token
      priceFactor = Number(attr.base_token_price_quote_token);
    } else if (baseAddr === toLc && quoteAddr === fromLc) {
      // Swapping quote token for base token  
      priceFactor = Number(attr.quote_token_price_base_token);
    }
    // Skip pools that don't have valid price data
    if (!Number.isFinite(priceFactor) || priceFactor <= 0) continue;
    // Calculate estimated output with fee adjustment
    const feePercent = Number(attr.pool_fee_percentage) || 0;
    const estimatedOutput = Number(amountIn) * priceFactor * (1 - feePercent / 100);
   
    results.push({
      poolId: pool.id,
      dexName: rel.dex?.data?.id || 'unknown',
      address: attr.address,
      estimatedOutput: estimatedOutput,
      pricePerToken: priceFactor,
      liquidityUsd: Number(attr.reserve_in_usd) || 0,
      feePercent: feePercent,
      poolName: attr.name || `${baseAddr.slice(0,6)}.../${quoteAddr.slice(0,6)}...`
    });
  }
  // Sort by estimated output (highest first = best quote)
  return results.sort((a, b) => b.estimatedOutput - a.estimatedOutput);
}

// Usage example
const sortedPools = calculateBestQuotes(poolDetails, {
  amountIn: 1000,
  fromToken: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', // USDC
  toToken: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'   // WETH
});

console.log(`Best quote: ${sortedPools[0].estimatedOutput} tokens from ${sortedPools[0].dexName}`);

此函数处理详细的池数据,处理交换方向逻辑,应用池费用,并返回按最高估计输出排序的最终池列表。

8、构建DEX聚合器仪表板

仪表板创建了一个用户友好的界面,用于比较不同DEX的交换率。我们将使用React组件来处理用户输入并在排序表格中显示结果。

8.1 实现输入表单

输入表单捕获必要的交换参数。用户首先选择网络,然后提供从代币和到代币的合约地址,最后输入金额。提交表单会触发池发现过程:

// Input form component for swap parameters
function SwapForm({ onSearch, loading }) {
  const [formData, setFormData] = useState({
    network: 'eth',
    fromToken: '',
    toToken: '',
    amount: '1000'
  });
  const handleSubmit = (e) => {
    e.preventDefault();
    if (formData.fromToken && formData.toToken && formData.amount) {
      onSearch(formData);
    }
  };
  return (
    <form onSubmit={handleSubmit} className="swap-form">
      <div className="form-group">
        <label>Network</label>
        <select
          value={formData.network}
          onChange={(e) => setFormData({...formData, network: e.target.value})}
        >
          <option value="eth">Ethereum</option>
          <option value="base">Base</option>
          <option value="arbitrum">Arbitrum</option>
          <option value="polygon">Polygon</option>
        </select>
      </div>
     
      <div className="form-group">
        <label>From Token (Contract Address)</label>
        <input
          type="text"
          placeholder="0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"
          value={formData.fromToken}
          onChange={(e) => setFormData({...formData, fromToken: e.target.value})}
        />
      </div>
     
      <div className="form-group">
        <label>To Token (Contract Address)</label>
        <input
          type="text"
          placeholder="0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"
          value={formData.toToken}
          onChange={(e) => setFormData({...formData, toToken: e.target.value})}
        />
      </div>
     
      <div className="form-group">
        <label>Swap Amount</label>
        <input
          type="number"
          placeholder="1000"
          value={formData.amount}
          onChange={(e) => setFormData({...formData, amount: e.target.value})}
        />
      </div>
     
      <button type="submit" disabled={loading}>
        {loading ? 'Finding Best Rates...' : 'Compare Rates'}
      </button>
    </form>
  );
}

8.2 结果表实现

结果表显示按估计输出排序的池数据,最佳费率突出显示在顶部:

// Results table component with copy functionality
function ResultsTable({ pools, loading }) {
  const copyToClipboard = (text) => {
    navigator.clipboard.writeText(text);
    // Show brief success feedback
  };
  if (loading) return <div className="loading">Fetching pool data...</div>;
  if (!pools.length) return <div className="no-results">No pools found for this pair</div>;
  return (
    <div className="results-container">
      <h3>Best Swap Rates (Sorted by Estimated Output)</h3>
      <table className="results-table">
        <thead>
          <tr>
            <th className="estimated-output-header">Estimated Output</th>
            <th>DEX Name</th>
            <th>Price per Token</th>
            <th>Pool Liquidity (USD)</th>
            <th>Pool Fee (%)</th>
            <th>Pool Address</th>
          </tr>
        </thead>
        <tbody>
          {pools.map((pool, index) => (
            <tr key={pool.poolId} className={index === 0 ? 'best-rate' : ''}>
              <td className="estimated-output">
                <strong>{pool.estimatedOutput.toFixed(6)}</strong>
                {index === 0 && <span className="best-badge">Best Rate</span>}
              </td>
              <td className="dex-name">{pool.dexName}</td>
              <td>{pool.pricePerToken.toFixed(8)}</td>
              <td>${pool.liquidityUsd.toLocaleString()}</td>
              <td>{pool.feePercent}%</td>
              <td className="address-cell">
                <span className="address">{pool.address.slice(0, 10)}...</span>
                <button
                  className="copy-btn"
                  onClick={() => copyToClipboard(pool.address)}
                  title="Copy full address">
                </button>
              </td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

8.3 完整仪表板集成

以下是组件如何在主应用程序中协同工作的示例:

// Main dashboard component
function DEXAggregatorDashboard() {
  const [pools, setPools] = useState([]);
  const [loading, setLoading] = useState(false);
  const searchPools = async ({ network, fromToken, toToken, amount }) => {
    setLoading(true);
    try {
      // Fetch pool data from our backend API
      const response = await fetch(`/api/pools?network=${network}&from=${fromToken}&to=${toToken}`);
      const data = await response.json();
     
      // Calculate best quotes and sort results
      const sortedPools = calculateBestQuotes(data.data, {
        amountIn: parseFloat(amount),
        fromToken,
        toToken
      });
     
      setPools(sortedPools);
    } catch (error) {
      console.error('Failed to fetch pools:', error);
      setPools([]);
    } finally {
      setLoading(false);
    }
  };
  return (
    <div className="dex-aggregator">
      <h1>DEX Aggregator Dashboard</h1>
      <SwapForm onSearch={searchPools} loading={loading} />
      <ResultsTable pools={pools} loading={loading} />
    </div>
  );
}

原文链接:How to Build a DEX Aggregator to Find the Best Swap Rates

DefiPlot翻译整理,转载请标明出处

免责声明:本站资源仅用于学习目的,也不应被视为投资建议,读者在采取任何行动之前应自行研究并对自己的决定承担全部责任。