В первой части мы построили «защищенный канал связи». В этой части мы переходим к написанию логики обмена. Наша задача — создать транзакцию для Uniswap V3 так, чтобы она не просто была приватной, но и гарантированно исполнилась.
1. Подготовка транзакции обмена
Для примера реализуем обмен ETH на USDC. Чтобы транзакция была валидной для бандла, нам нужно подготовить её объект заранее, не отправляя в сеть.
Нам понадобятся:
- Адрес Router Uniswap V3.
- ABI (хотя бы минимальный) для функции
exactInputSingle. - Расчет газа с учетом приоритета.
Важное правило: В Flashbots бандлах цена газа (
gasPrice) заменяется наmaxFeePerGasиmaxPriorityFeePerGas. Именно priorityFee (чаевые валидатору) определяет, попадет ли ваш бандл в блок.
2. Код: Сборка и симуляция
Добавим в наш проект файл swap.ts. Основной акцент здесь — на методе .simulate(). Это «киллер-фича» Flashbots, которая позволяет проверить выполнение транзакции на текущем состоянии блокчейна, не тратя ни цента на реальный газ.
import { ethers } from "ethers";
import { FlashbotsBundleRawTransaction } from "@flashbots/ethers-provider-bundle";
// Минимальный ABI для взаимодействия с Uniswap V3 Router
const ROUTER_ABI = [
"function exactInputSingle((address tokenIn, address tokenOut, uint24 fee, address recipient, uint256 deadline, uint256 amountIn, uint256 amountOutMinimum, uint160 sqrtPriceLimitX96)) external payable returns (uint256 amountOut)"
];
const ROUTER_ADDRESS = "0xE592427A0AEce92De3Edee1F18E0157C05861564";
export async function createAndSimulateBundle(wallet: ethers.Wallet, flashbotsProvider: any, provider: ethers.Provider) {
const block = await provider.getBlock("latest");
const nextBlockNumber = block!.number + 1;
// 1. Формируем интерфейс и данные транзакции
const iface = new ethers.Interface(ROUTER_ABI);
const deadline = Math.floor(Date.now() / 1000) + 60 * 20; // 20 минут
const amountIn = ethers.parseEther("0.1"); // Меняем 0.1 ETH
const params = {
tokenIn: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", // WETH
tokenOut: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC
fee: 3000, // 0.3%
recipient: wallet.address,
deadline: deadline,
amountIn: amountIn,
amountOutMinimum: 0, // В продакшене всегда считайте слиппейдж!
sqrtPriceLimitX96: 0
};
const data = iface.encodeFunctionData("exactInputSingle", [params]);
// 2. Структура транзакции
const transaction = {
to: ROUTER_ADDRESS,
value: amountIn,
data: data,
chainId: 1,
type: 2, // EIP-1559
gasLimit: 250000,
maxFeePerGas: ethers.parseUnits("50", "gwei"),
maxPriorityFeePerGas: ethers.parseUnits("2", "gwei"), // "Взятка" валидатору
nonce: await wallet.getNonce()
};
// 3. Создаем подписанный бандл
const signedBundle = await flashbotsProvider.signBundle([
{
signer: wallet,
transaction: transaction
}
]);
// 4. СИМУЛЯЦИЯ (Критически важный этап)
console.log("Запуск симуляции бандла...");
const simulation = await flashbotsProvider.simulate(signedBundle, nextBlockNumber);
if ("error" in simulation) {
console.error(`Ошибка симуляции: ${simulation.error.message}`);
return;
}
console.log("Симуляция успешна!", JSON.stringify(simulation, null, 2));
return signedBundle;
}
3. Технический нюанс: Почему симуляция обязательна?
В «публичном» эфире, если ваша транзакция зафейлится (например, не хватило лимита газа или изменилась цена), она все равно попадет в блок, и вы заплатите за газ.
В мире Flashbots:
- Если симуляция показывает ошибку — вы просто не отправляете бандл.
- Если бандл отправлен, но условия в блоке изменились и транзакция стала невыгодной — валидатор её просто проигнорирует.
Итог: Вы платите за газ только тогда, когда ваш Stealth Swap действительно исполнился.
4. Как рассчитать «взятку» (Priority Fee)
Валидаторы включают бандлы в порядке убывания их прибыльности для себя. Прибыльность бандла (Gas Price Score) считается как:

Если вы делаете обычный свап, maxPriorityFeePerGas в 1-2 gwei обычно достаточно. Но во время высокой волатильности конкуренция за место в блоке растет даже на приватных рельсах.
Что мы имеем теперь?
У нас есть подписанный и проверенный бандл, который готов к отправке. Мы точно знаем, что код Uniswap отработает корректно, а баланса кошелька хватит на покрытие всех расходов.
В следующей (заключительной) части: мы напишем цикл ожидания блока, реализуем реальную отправку бандла и сделаем удобный CLI-интерфейс для нашего инструмента.