Перейти к основному содержимому

FeeRouter и LimitOrderBook: гайд по интеграции

Этот документ — пошаговая инструкция для frontend (Next.js + Effector) и backend (NestJS + keeper) разработчиков по интеграции с контрактами FeeRouter и LimitOrderBook.

0. Что есть, что отсутствует

Уже сделано в контрактах:

  • Деплой контрактов: после деплоя у вас будут адреса FeeRouter и LimitOrderBook. Их и нужно положить в конфиг приложения.
  • ABI генерируется в apps/contracts-v2/typechain-types. Распространяется в остальные пакеты через pnpm copy-typechain.

Что должны сделать фронт/бэк:

  • Подцепить ABI и адреса.
  • Реализовать UI-сценарии (свап + лимит-ордера + my-orders).
  • Backend-keeper для исполнения ордеров.
  • Индексация событий (через The Graph, или собственный listener).

1. ABI и адреса

После pnpm build-packages + pnpm copy-typechain фабрики типов появятся в:

packages/typechain-types/factories/contracts/FeeRouter/FeeRouter__factory.ts
packages/typechain-types/factories/contracts/LimitOrderBook/LimitOrderBook__factory.ts

Адреса контрактов на сетях:

// apps/frontend/src/config/contracts.ts (создать)
export const CONTRACTS = {
polygon: {
feeRouter: '0x...', // подставить после деплоя
limitOrderBook: '0x...',
partnerDiamond: '0x...', // существующий MagnetDiamond
uniswapRouter: '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45',
uniswapQuoterV2: '0x61fFE014bA17989E743c5F6cB21bF9697530B21e',
wmatic: '0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270',
// экосистемные токены
unit: '0x...',
pulse: '0x...',
boost: '0x...',
xmgt: '0x...',
},
};

2. Frontend: обычный своп

2.1 Структура UI

Минимальная форма:

  • Селект tokenIn / tokenOut (из 4 экосистемных + опционально WMATIC/USDC).
  • Поле amountIn.
  • Расчёт ожидаемого amountOut через Uniswap V3 Quoter с учётом нашей fee.
  • Кнопка свапа.

2.2 Расчёт ожидаемого output

import { ethers } from 'ethers';
import { IQuoterV2__factory } from '@uniswap/v3-periphery/typechain';

async function quote({
feeRouter,
quoter,
tokenIn,
tokenOut,
poolFee,
amountIn,
}: {
feeRouter: ethers.Contract;
quoter: ethers.Contract;
tokenIn: string;
tokenOut: string;
poolFee: number;
amountIn: bigint;
}): Promise<{ amountOut: bigint; feeAmount: bigint; netInput: bigint }> {
const cfg = await feeRouter.feeConfig();
const totalFeeBps: bigint = cfg.totalFeeBps;
const feeAmount = (amountIn * totalFeeBps) / 10000n;
const netInput = amountIn - feeAmount;

const { amountOut } = await quoter.quoteExactInputSingle.staticCall({
tokenIn,
tokenOut,
amountIn: netInput,
fee: poolFee,
sqrtPriceLimitX96: 0,
});

return { amountOut, feeAmount, netInput };
}

В UI показывайте:

Получите ≈ X TKN_B · комиссия 3% (Y TKN_A)

2.3 Approve + swap

const slippageBps = 100n; // 1%
const minAmountOut = (expectedAmountOut * (10000n - slippageBps)) / 10000n;

// 1. Approve tokenIn → feeRouter
const tokenInContract = new ethers.Contract(tokenIn, ERC20_ABI, signer);
const allowance = await tokenInContract.allowance(userAddress, feeRouter.target);
if (allowance < amountIn) {
await (await tokenInContract.approve(feeRouter.target, ethers.MaxUint256)).wait();
}

// 2. Swap
const tx = await feeRouter.connect(signer).swap(
tokenIn,
tokenOut,
poolFee,
amountIn,
minAmountOut,
userAddress, // recipient
userAddress, // feePayer = сам юзер
);
await tx.wait();

2.4 Effector-стор (фронт)

Рекомендуемая структура (декларативно):

// features/swap/model.ts
import { createEffect, createEvent, createStore, sample } from 'effector';

export const swapRequested = createEvent<SwapParams>();
export const swapFx = createEffect(async (params: SwapParams) => {
const { amountOut } = await quote(params);
const minOut = (amountOut * 9900n) / 10000n;
return feeRouter.swap(...args);
});

export const $isSwapping = createStore(false)
.on(swapFx, () => true)
.reset(swapFx.finally);

sample({ clock: swapRequested, target: swapFx });

3. Frontend: лимит-ордера

3.1 Форма размещения

Поля:

  • tokenIn, tokenOut, poolFee (выбирается автоматически по самому ликвидному пулу для пары).
  • amountIn.
  • priceTarget (или minAmountOut напрямую).
  • expiry (UI: дропдаун «1 час / 6 часов / 24 часа / 7 дней»).

UI должен сразу показывать:

  • К оплате: amountIn TKN_A + 0.3 POL (газ-депозит).
  • Ожидаемое получение по текущему конфигу: через quote (см. 2.2).
  • Предупреждение: «До 0.3 POL газ-депозита может быть удержано при исполнении».

3.2 placeOrder

const expiry = Math.floor(Date.now() / 1000) + 24 * 3600;
const gasDeposit = ethers.parseEther('0.3');

await tokenInContract.approve(limitOrderBook.target, ethers.MaxUint256);

const tx = await limitOrderBook.connect(signer).placeOrder(
tokenIn,
tokenOut,
poolFee,
amountIn,
minAmountOut,
expiry,
{ value: gasDeposit },
);
const receipt = await tx.wait();
const event = receipt.logs.find(l => l.fragment?.name === 'OrderPlaced');
const orderId = event.args.orderId;

3.3 My Orders view

Список ордеров пользователя индексируется из событий OrderPlaced (через The Graph или собственный листенер). Для каждого ордера в статусе Open показывайте:

  • Текущая рыночная цена vs. target (через quoter каждые N секунд).
  • "Дошло X% до исполнения".
  • Кнопки Cancel, Top up gas.
  • Газ-депозит и сколько примерно нужно для исполнения (через eth_estimateGas на executeOrder).

3.4 Cancel / TopUp

await limitOrderBook.connect(signer).cancelOrder(orderId);
await limitOrderBook.connect(signer).topUpGas(orderId, { value: ethers.parseEther('0.2') });

4. Backend: keeper

4.1 Архитектура

Сервис, который:

  1. Слушает событие OrderPlaced от LimitOrderBook и индексирует в свою БД.
  2. Слушает OrderCancelled, OrderExecuted, OrderExpired — обновляет статусы.
  3. Каждый блок (или каждые 3–5 секунд) проверяет все Open ордера: можно ли исполнить.
  4. Если можно — шлёт executeOrder через wallet keeper'а.

4.2 Индексация через ethers listeners

import { ethers } from 'ethers';
import { LimitOrderBook__factory } from '@magnet/typechain-types';

const provider = new ethers.JsonRpcProvider(POLYGON_RPC);
const limitOrderBook = LimitOrderBook__factory.connect(LOB_ADDRESS, provider);

limitOrderBook.on(limitOrderBook.filters.OrderPlaced(), async (orderId, maker, tokenIn, tokenOut, poolFee, amountIn, minAmountOut, expiry, gasDeposit, event) => {
await db.orders.upsert({
id: orderId.toString(),
maker,
tokenIn,
tokenOut,
poolFee,
amountIn: amountIn.toString(),
minAmountOut: minAmountOut.toString(),
expiry: Number(expiry),
gasDeposit: gasDeposit.toString(),
status: 'Open',
blockNumber: event.log.blockNumber,
});
});

// Аналогично для OrderCancelled / OrderExecuted / OrderExpired

Для исторических данных при старте — пройти через getLogs от deploymentBlock до latestBlock чанками по 5000 блоков (RPC-лимиты).

4.3 Проверка исполнимости

Каждый блок (или интервал):

const openOrders = await db.orders.findMany({ where: { status: 'Open' } });

for (const order of openOrders) {
if (order.expiry < Math.floor(Date.now() / 1000)) {
// Опционально: вызвать expireOrder для подчистки
continue;
}

const cfg = await feeRouter.feeConfig();
const fee = (BigInt(order.amountIn) * cfg.totalFeeBps) / 10000n;
const netIn = BigInt(order.amountIn) - fee;

let expectedOut: bigint;
try {
expectedOut = (await quoter.quoteExactInputSingle.staticCall({
tokenIn: order.tokenIn,
tokenOut: order.tokenOut,
amountIn: netIn,
fee: order.poolFee,
sqrtPriceLimitX96: 0,
})).amountOut;
} catch {
continue; // нет ликвидности
}

if (expectedOut < BigInt(order.minAmountOut)) continue;

// Оценить газ
const gas = await limitOrderBook.executeOrder.estimateGas(order.id);
const gasPrice = (await provider.getFeeData()).gasPrice ?? 0n;
const estCost = gas * gasPrice + BigInt(EXECUTOR_TIP);

if (estCost + SAFETY_MARGIN >= BigInt(order.gasDeposit)) continue;

// Шлём
try {
const tx = await limitOrderBook.connect(keeperWallet).executeOrder(order.id);
await tx.wait();
} catch (e) {
logger.warn({ err: e, orderId: order.id }, 'execute failed');
}
}

4.4 Защита от MEV

Между моментом «бот видит, что ордер можно исполнить» и моментом «tx майнится» сторонний MEV-бот может перехватить. Для нас это нейтрально:

  • Maker получает свою цену (≥ minAmountOut).
  • Кто бы ни исполнил — keeper или MEV-бот — комиссия и партнёрка работают одинаково.
  • Хорошо: даже если наш keeper упал, ордер исполнят сторонние боты.

Для снижения wasted gas наш бот может слать tx через Polygon private mempool (bloXroute MEV-blocker, Merkle/Flashbots-style RPC). Это снижает количество tx, которые пришли «вторыми» и сожгли газ зря.

4.5 Конфиг keeper'а

// apps/backend/keeper.config.ts
export const KEEPER = {
pollInterval: 5_000, // мс
safetyMargin: parseEther('0.02'), // 0.02 POL запас
maxGasPrice: parseUnits('1000', 'gwei'),
privateRpcUrl: process.env.POLYGON_PRIVATE_RPC, // bloXroute
};

4.6 Мониторинг

Метрики, которые рекомендуется собирать:

  • keeper_orders_active — Open ордера в БД.
  • keeper_executions_total (counter по success/fail).
  • keeper_skip_reason (counter: price, gas, liquidity).
  • keeper_revenue_pol — сколько POL заработали на tips.

5. Сценарий «зарегистрировать новую пару»

Только owner (multisig):

feeRouter.setPairAllowed(tokenA, tokenB, true);
feeRouter.setPairAllowed(tokenB, tokenA, true); // если двунаправленно

После этого фронт должен подхватить новую пару (через индексатор события PairAllowedUpdated).

6. Сценарий «обновить fee config»

Только owner:

feeRouter.setFeeConfig({
totalFeeBps: 300,
staticRecipients: [
{ wallet: treasury, shareBps: 4000 },
{ wallet: dev, shareBps: 1000 },
{ wallet: marketing, shareBps: 1000 },
],
partnerLineBps: [800, 700, 500, 400, 300, 200, 200, 200, 100, 100, 100, 100, 100, 100, 100],
});

Сумма всех bps должна быть ровно 10000. Контракт revert'ит при невалидном конфиге.

7. Чек-лист интеграции

Фронт:

  • Подцепил ABI из typechain.
  • Положил адреса в config.
  • Реализовал quote (учёт fee).
  • Реализовал approve flow.
  • Реализовал swap + ошибки.
  • Реализовал placeOrder + газ-депозит.
  • Реализовал cancel / topUp.
  • My orders view с realtime-ценой.
  • Обработал события OrderExecuted (показать toast).

Бэк:

  • Listener на OrderPlaced / OrderCancelled / OrderExecuted / OrderExpired.
  • Полл-цикл проверки исполнимости (каждые 5с).
  • Quoter + газ-оценка.
  • Keeper wallet с POL для газа.
  • Private RPC для отправки tx.
  • Метрики + alerting (Sentry / Prometheus).
  • Опциональный cleanup expired ордеров (вызов expireOrder).