Реализация DEX-интерфейса (обмен токенов) на сайте

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.

Разработка и обслуживание любых видов сайтов:

Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Реализация DEX-интерфейса (обмен токенов) на сайте
Сложная
~2-4 недели
Часто задаваемые вопросы

Наши компетенции:

Этапы разработки

Последние работы

  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    874
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    851

Реализация DEX-интерфейса (обмен токенов) на сайте

DEX swap-интерфейс — сложнее, чем кажется. Помимо самого обмена: получение котировок (on-chain или агрегатор), расчёт price impact, slippage tolerance, deadline для транзакции, approve + swap двухшаговый флоу, обработка native ETH vs WETH. Это полноценный продуктовый компонент, который либо собирается с нуля, либо интегрируется через агрегатор.

Два подхода: собственный DEX vs агрегатор

Собственный роутер (Uniswap v2/v3 fork): вы контролируете контракт, интерфейс работает напрямую с вашим пулом. Котировки считаются on-chain через getAmountsOut.

Агрегатор (1inch, 0x, Paraswap): маршрутизирует через лучший путь по всем DEX. Подходит для продуктов, где важна лучшая цена, а не эксклюзивный пул. API возвращает готовые transaction data.

Разбираем оба сценария.

Вариант 1: прямой Uniswap v2 роутер

// lib/swap.ts
import { createPublicClient, http, parseAbi, formatUnits, parseUnits } from 'viem';

const ROUTER_ABI = parseAbi([
  'function getAmountsOut(uint256 amountIn, address[] path) view returns (uint256[])',
  'function swapExactTokensForTokens(uint256,uint256,address[],address,uint256) returns (uint256[])',
  'function swapExactETHForTokens(uint256,address[],address,uint256) payable returns (uint256[])',
  'function swapExactTokensForETH(uint256,uint256,address[],address,uint256) returns (uint256[])',
]);

const WETH = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2' as const;

export async function getQuote(
  tokenIn: `0x${string}` | 'ETH',
  tokenOut: `0x${string}` | 'ETH',
  amountIn: bigint,
  decimalsIn: number,
  decimalsOut: number,
): Promise<{ amountOut: bigint; path: `0x${string}`[]; priceImpact: number }> {
  const client = createPublicClient({ chain: mainnet, transport: http() });

  const addressIn = tokenIn === 'ETH' ? WETH : tokenIn;
  const addressOut = tokenOut === 'ETH' ? WETH : tokenOut;

  // Прямой маршрут
  const directPath: `0x${string}`[] = [addressIn, addressOut];

  // Маршрут через WETH (если токены не имеют прямой пары)
  const wethPath: `0x${string}`[] = [addressIn, WETH, addressOut];

  const [directResult, wethResult] = await client.multicall({
    contracts: [
      { address: ROUTER, abi: ROUTER_ABI, functionName: 'getAmountsOut', args: [amountIn, directPath] },
      { address: ROUTER, abi: ROUTER_ABI, functionName: 'getAmountsOut', args: [amountIn, wethPath] },
    ],
    allowFailure: true,
  });

  const directOut = directResult.status === 'success'
    ? (directResult.result as bigint[])[directResult.result.length - 1]
    : 0n;

  const wethOut = wethResult.status === 'success'
    ? (wethResult.result as bigint[])[(wethResult.result as bigint[]).length - 1]
    : 0n;

  const bestOut = directOut >= wethOut ? directOut : wethOut;
  const bestPath = directOut >= wethOut ? directPath : wethPath;

  // Price impact — разница между spot price и реальной ценой
  // (упрощённо через резервы пула)
  const priceImpact = 0; // в реальном проекте считается по резервам

  return { amountOut: bestOut, path: bestPath, priceImpact };
}

Хук котировки с debounce

// hooks/useQuote.ts
import { useQuery } from '@tanstack/react-query';
import { useDebouncedValue } from '@/hooks/useDebouncedValue';

export function useQuote(
  tokenIn: string,
  tokenOut: string,
  amountIn: string,
  decimalsIn: number,
  decimalsOut: number,
) {
  // Debounce — не запрашиваем при каждом символе
  const debouncedAmount = useDebouncedValue(amountIn, 400);

  const amountWei = debouncedAmount ? parseUnits(debouncedAmount, decimalsIn) : 0n;

  return useQuery({
    queryKey: ['quote', tokenIn, tokenOut, amountWei.toString()],
    queryFn: () => getQuote(
      tokenIn as `0x${string}`,
      tokenOut as `0x${string}`,
      amountWei,
      decimalsIn,
      decimalsOut,
    ),
    enabled: amountWei > 0n,
    staleTime: 15_000, // котировка устаревает через 15 секунд
    refetchInterval: 15_000,
  });
}

Вариант 2: котировки через 0x API

// lib/0x.ts
export async function get0xQuote(
  sellToken: string,  // адрес или "ETH"
  buyToken: string,
  sellAmount: string, // в wei
  takerAddress?: string,
): Promise<{
  buyAmount: string;
  price: string;
  guaranteedPrice: string;
  to: string;
  data: string;
  value: string;
  gas: string;
  estimatedPriceImpact: string;
}> {
  const params = new URLSearchParams({
    sellToken,
    buyToken,
    sellAmount,
    ...(takerAddress && { takerAddress }),
    affiliateAddress: process.env.NEXT_PUBLIC_FEE_RECIPIENT ?? '',
    buyTokenPercentageFee: '0.005', // 0.5% протокольный сбор (опционально)
  });

  const res = await fetch(
    `https://api.0x.org/swap/v1/quote?${params}`,
    { headers: { '0x-api-key': process.env.ZRX_API_KEY! } },
  );

  if (!res.ok) {
    const error = await res.json();
    throw new Error(error.reason ?? 'Quote failed');
  }

  return res.json();
}

При использовании 0x — quote.to и quote.data передаются напрямую в транзакцию, без вызова конкретных функций роутера. Это упрощает интеграцию.

Компонент обмена

// components/SwapWidget.tsx
export function SwapWidget() {
  const { address } = useAccount();
  const [tokenIn, setTokenIn] = useState<Token>(ETH_TOKEN);
  const [tokenOut, setTokenOut] = useState<Token>(USDC_TOKEN);
  const [amountIn, setAmountIn] = useState('');
  const [slippage, setSlippage] = useState(0.5); // %

  const { data: quote, isLoading: quoteLoading } = useQuote(
    tokenIn.address, tokenOut.address, amountIn, tokenIn.decimals, tokenOut.decimals,
  );

  const amountOut = quote ? formatUnits(quote.amountOut, tokenOut.decimals) : '';

  // Минимум с учётом slippage
  const minOut = quote
    ? quote.amountOut - (quote.amountOut * BigInt(Math.floor(slippage * 100))) / 10000n
    : 0n;

  // Deadline: 20 минут от текущего момента
  const deadline = BigInt(Math.floor(Date.now() / 1000) + 1200);

  return (
    <div className="w-full max-w-md rounded-2xl border border-white/10 bg-neutral-900 p-4">
      <div className="space-y-2">
        <TokenInput
          label="Отдаёте"
          token={tokenIn}
          value={amountIn}
          onChange={setAmountIn}
          onTokenChange={setTokenIn}
          balance={walletBalanceIn}
          showMax
        />
        <SwapDirectionButton onClick={() => {
          setTokenIn(tokenOut);
          setTokenOut(tokenIn);
          setAmountIn(amountOut);
        }} />
        <TokenInput
          label="Получаете"
          token={tokenOut}
          value={quoteLoading ? '...' : amountOut}
          onTokenChange={setTokenOut}
          readOnly
        />
      </div>

      {quote && (
        <div className="mt-4 space-y-1.5 rounded-xl bg-neutral-800/50 p-3 text-sm">
          <Row label="Курс" value={`1 ${tokenIn.symbol} = ${(parseFloat(amountOut) / parseFloat(amountIn)).toFixed(6)} ${tokenOut.symbol}`} />
          <Row label="Price Impact" value={`${quote.priceImpact.toFixed(2)}%`} warn={quote.priceImpact > 2} />
          <Row label="Мин. получите" value={`${formatUnits(minOut, tokenOut.decimals)} ${tokenOut.symbol}`} />
          <Row label="Slippage" value={`${slippage}%`} />
        </div>
      )}

      <SwapButton
        quote={quote}
        tokenIn={tokenIn}
        amountIn={parseUnits(amountIn || '0', tokenIn.decimals)}
        minOut={minOut}
        deadline={deadline}
        path={quote?.path}
        className="mt-4 w-full"
      />

      <SlippageSettings value={slippage} onChange={setSlippage} className="mt-3" />
    </div>
  );
}

Обработка price impact

function PriceImpactWarning({ impact }: { impact: number }) {
  if (impact < 1) return null;
  if (impact < 3) return (
    <p className="text-sm text-yellow-400">⚠ Price impact {impact.toFixed(2)}% — выше обычного</p>
  );
  return (
    <div className="rounded-lg border border-red-500/30 bg-red-500/10 p-3 text-sm text-red-400">
      Price impact {impact.toFixed(2)}% — высокий риск потерь. Уменьшите сумму обмена или выберите другой маршрут.
    </div>
  );
}

Token selector с поиском

// Список токенов — из Uniswap token list или собственного
const TOKEN_LIST_URL = 'https://gateway.ipfs.io/ipns/tokens.uniswap.org';

export function TokenSelector({ onSelect }: { onSelect: (token: Token) => void }) {
  const [search, setSearch] = useState('');
  const { data: tokenList } = useQuery({
    queryKey: ['tokenList'],
    queryFn: () => fetch(TOKEN_LIST_URL).then(r => r.json()).then(d => d.tokens),
    staleTime: Infinity,
  });

  const filtered = tokenList?.filter(t =>
    t.chainId === 1 &&
    (t.symbol.toLowerCase().includes(search.toLowerCase()) ||
     t.name.toLowerCase().includes(search.toLowerCase()) ||
     t.address.toLowerCase() === search.toLowerCase()),
  ) ?? [];

  return (
    <div>
      <input
        value={search}
        onChange={e => setSearch(e.target.value)}
        placeholder="Название, символ или адрес"
        className="w-full rounded-lg border border-white/10 bg-neutral-800 px-3 py-2"
      />
      <VirtualList items={filtered} renderItem={token => (
        <TokenRow token={token} onClick={() => onSelect(token)} />
      )} />
    </div>
  );
}

Сроки: swap-виджет поверх Uniswap v2 роутера с котировками, slippage, approve и базовой обработкой ошибок — 5–7 дней. Полноценный DEX с агрегатором (0x/1inch), token selector по Uniswap token list, настройками slippage/deadline, историей транзакций и поддержкой нескольких сетей — 2–3 недели.