Реалізація 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. Підходить для продуктів, де важлива найкраща ціна, а не excluzivний пул. 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 тижні.