Стратегии постепенной миграции

Переход на Universal Router в JavaScript-приложении редко осуществляется одномоментно. Существующий код обычно завязан на отдельных контрактах (например, SwapRouter, NFT-маркетплейсы, кастомные вызовы), и их резкая замена увеличивает риск ошибок. Практика показывает, что оптимальной является поэтапная миграция, при которой новая логика внедряется параллельно со старой.

Ключевые принципы:

  • Сосуществование старых и новых маршрутов
  • Минимизация изменений в бизнес-логике
  • Инкапсуляция Universal Router
  • Постепенное расширение покрытия

Декомпозиция текущей архитектуры

Перед внедрением Universal Router необходимо разложить текущую систему на отдельные категории операций:

Типовые группы вызовов

  • Swap-транзакции (Uniswap V2/V3)
  • Работа с NFT
  • Transfer/approve операций
  • Мульти-шаговые сценарии (например, swap + stake)

Пример анализа

// Старый подход
await swapRouter.exactInputSingle(params);

// NFT покупка
await marketplace.buy(tokenId);

// Transfer
await token.transfer(to, amount);

Каждая категория может мигрировать независимо. Это позволяет избежать полного рефакторинга.


Инкапсуляция Universal Router

На раннем этапе Universal Router внедряется как изолированный слой, не затрагивающий остальной код.

Создание адаптера

class UniversalRouterAdapter {
  constructor(routerContract) {
    this.router = routerContract;
  }

  async execute(commands, inputs, value = 0) {
    return this.router.execute(commands, inputs, {
      value
    });
  }
}

Этот слой выполняет две функции:

  • скрывает сложность команд
  • обеспечивает единый интерфейс

Параллельное выполнение логики

На этапе миграции старые и новые маршруты работают одновременно.

Стратегия выбора маршрута

Вводится переключатель:

const USE_UNIVERSAL_ROUTER = true;

async function swap(params) {
  if (USE_UNIVERSAL_ROUTER) {
    return universalSwap(params);
  } else {
    return legacySwap(params);
  }
}

Это позволяет:

  • быстро переключаться между реализациями
  • откатывать изменения без деплоя
  • проводить A/B тестирование

Миграция простых операций

Первым шагом переводятся наиболее простые сценарии:

Пример: swap через Universal Router

import { CommandType } from '@uniswap/universal-router-sdk';

const commands = [
  CommandType.V3_SWAP_EXACT_IN
];

const inputs = [
  encodeV3SwapExactIn({
    recipient: userAddress,
    amountIn,
    amountOutMin,
    path
  })
];

await adapter.execute(commands, inputs);

Преимущества:

  • минимальная логика
  • легко проверить корректность
  • быстро обнаруживаются ошибки

Миграция сложных сценариев

После базовых операций переходят к составным транзакциям.

Пример: swap + transfer

const commands = [
  CommandType.V3_SWAP_EXACT_IN,
  CommandType.TRANSFER
];

const inputs = [
  encodeV3SwapExactIn({...}),
  encodeTransfer({
    token,
    recipient,
    amount
  })
];

await adapter.execute(commands, inputs);

Здесь появляется ключевое преимущество Universal Router:

атомарное выполнение нескольких действий


Стратегия частичного покрытия

Не обязательно сразу переводить все операции.

Практический подход

  • 20% кода → Universal Router
  • 80% → legacy

Постепенно доля увеличивается:

  • этап 1: только swap
  • этап 2: swap + NFT
  • этап 3: полный переход

Введение уровня абстракции

Чтобы избежать привязки к конкретной реализации, вводится слой сервисов.

class SwapService {
  constructor({ useUniversal }) {
    this.useUniversal = useUniversal;
  }

  async swap(params) {
    if (this.useUniversal) {
      return universalSwap(params);
    }
    return legacySwap(params);
  }
}

Преимущества:

  • прозрачная замена реализации
  • единая точка управления
  • упрощение тестирования

Контроль состояния и обратная совместимость

Важно учитывать различия в поведении:

Отличия Universal Router

  • batching операций
  • необходимость encoding команд
  • работа с value (ETH)

Проверки

  • совпадение выходных значений
  • корректность gas usage
  • обработка ошибок

Логирование и мониторинг

На этапе миграции необходимо усиленное логирование:

try {
  const result = await adapter.execute(commands, inputs);
  logSuccess(result);
} catch (error) {
  logError(error);
  fallbackToLegacy();
}

Это позволяет:

  • быстро выявлять проблемы
  • анализировать поведение
  • принимать решения о расширении покрытия

Стратегия fallback

Любая операция через Universal Router должна иметь резервный сценарий.

async function safeSwap(params) {
  try {
    return await universalSwap(params);
  } catch (e) {
    return await legacySwap(params);
  }
}

Fallback особенно важен:

  • на ранних этапах
  • при работе с пользователями
  • при нестабильных сетях

Тестирование при миграции

Тестирование проводится в несколько уровней:

Unit-тесты

  • encoding команд
  • корректность параметров

Integration-тесты

  • выполнение транзакций
  • сравнение результатов

Shadow testing

Одновременное выполнение:

const legacyResult = await legacySwap(params);
const universalResult = await universalSwap(params);

Сравнение:

  • amountOut
  • gas
  • ошибки

Постепенное отключение legacy-кода

После стабилизации Universal Router:

  1. Удаляются fallback-ветки
  2. Убираются старые контракты
  3. Упрощается архитектура

Финальный шаг

async function swap(params) {
  return universalSwap(params);
}

Типичные ошибки при миграции

1. Слишком быстрый переход

  • приводит к критическим багам
  • сложно отследить источник проблем

2. Отсутствие fallback

  • риск полной неработоспособности

3. Игнорирование encoding логики

  • неверные команды
  • ошибки выполнения

4. Смешивание логики

  • усложнение кода
  • трудности поддержки

Оптимизация после миграции

После полного перехода:

  • объединение операций в batch
  • сокращение количества транзакций
  • оптимизация gas

Пример

const commands = [
  CommandType.WRAP_ETH,
  CommandType.V3_SWAP_EXACT_IN,
  CommandType.SWEEP
];

Это уменьшает:

  • стоимость
  • время выполнения
  • количество подписей

Архитектурный результат

После завершения миграции система приобретает:

  • единый интерфейс взаимодействия с DeFi
  • гибкость построения транзакций
  • упрощённую поддержку
  • расширяемость без переписывания логики