Переход на Universal Router в JavaScript-приложении редко осуществляется одномоментно. Существующий код обычно завязан на отдельных контрактах (например, SwapRouter, NFT-маркетплейсы, кастомные вызовы), и их резкая замена увеличивает риск ошибок. Практика показывает, что оптимальной является поэтапная миграция, при которой новая логика внедряется параллельно со старой.
Ключевые принципы:
Перед внедрением Universal Router необходимо разложить текущую систему на отдельные категории операций:
// Старый подход
await swapRouter.exactInputSingle(params);
// NFT покупка
await marketplace.buy(tokenId);
// Transfer
await token.transfer(to, amount);
Каждая категория может мигрировать независимо. Это позволяет избежать полного рефакторинга.
На раннем этапе 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);
}
}
Это позволяет:
Первым шагом переводятся наиболее простые сценарии:
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);
Преимущества:
После базовых операций переходят к составным транзакциям.
const commands = [
CommandType.V3_SWAP_EXACT_IN,
CommandType.TRANSFER
];
const inputs = [
encodeV3SwapExactIn({...}),
encodeTransfer({
token,
recipient,
amount
})
];
await adapter.execute(commands, inputs);
Здесь появляется ключевое преимущество Universal Router:
атомарное выполнение нескольких действий
Не обязательно сразу переводить все операции.
Постепенно доля увеличивается:
Чтобы избежать привязки к конкретной реализации, вводится слой сервисов.
class SwapService {
constructor({ useUniversal }) {
this.useUniversal = useUniversal;
}
async swap(params) {
if (this.useUniversal) {
return universalSwap(params);
}
return legacySwap(params);
}
}
Преимущества:
Важно учитывать различия в поведении:
На этапе миграции необходимо усиленное логирование:
try {
const result = await adapter.execute(commands, inputs);
logSuccess(result);
} catch (error) {
logError(error);
fallbackToLegacy();
}
Это позволяет:
Любая операция через Universal Router должна иметь резервный сценарий.
async function safeSwap(params) {
try {
return await universalSwap(params);
} catch (e) {
return await legacySwap(params);
}
}
Fallback особенно важен:
Тестирование проводится в несколько уровней:
Одновременное выполнение:
const legacyResult = await legacySwap(params);
const universalResult = await universalSwap(params);
Сравнение:
После стабилизации Universal Router:
async function swap(params) {
return universalSwap(params);
}
После полного перехода:
const commands = [
CommandType.WRAP_ETH,
CommandType.V3_SWAP_EXACT_IN,
CommandType.SWEEP
];
Это уменьшает:
После завершения миграции система приобретает: