W pierwszej części postawiliśmy nasz "bezpieczny tunel komunikacyjny”. W tej części przechodzimy do pisania logiki swapu. Naszym zadaniem jest przygotowanie transakcji na Uniswap V3 tak, aby nie tylko była prywatna, ale i miała gwarancję wykonania.
1. Przygotowanie transakcji swapu
Dla przykładu zrealizujemy wymianę ETH na USDC. Aby transakcja była poprawna dla bundle'a, musimy przygotować jej obiekt wcześniej, bez wysyłania go bezpośrednio do sieci.
Będziemy potrzebować:
- Adresu Routera Uniswap V3.
- ABI (chociażby minimalnego) dla funkcji
exactInputSingle. - Kalkulacji gazu z uwzględnieniem priorytetu.
Złota zasada: W paczkach Flashbots (bundles) standardowe
gasPricezastępuje się przezmaxFeePerGasorazmaxPriorityFeePerGas. To właśnie priorityFee (czyli "napiwek" dla walidatora) decyduje o tym, czy Twój bundle w ogóle wskoczy do bloku.
2. Kod: Składanie i symulacja
Dodajmy do naszego projektu plik swap.ts. Główny nacisk kładziemy tutaj na metodę .simulate(). To absolutny "killer-feature" Flashbots, który pozwala sprawdzić, czy transakcja przejdzie na aktualnym stanie blockchaina, nie wydając przy tym ani grosza na realny gaz.
import { ethers } from "ethers";
import { FlashbotsBundleRawTransaction } from "@flashbots/ethers-provider-bundle";
// Minimalne ABI do interakcji z 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. Tworzymy interfejs i przygotowujemy dane transakcji
const iface = new ethers.Interface(ROUTER_ABI);
const deadline = Math.floor(Date.now() / 1000) + 60 * 20; // 20 minut zapasu
const amountIn = ethers.parseEther("0.1"); // Wymieniamy 0.1 ETH
const params = {
tokenIn: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", // WETH
tokenOut: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC
fee: 3000, // 0.3%
recipient: wallet.address,
deadline: deadline,
amountIn: amountIn,
amountOutMinimum: 0, // Na produkcji zawsze liczcie slippage!
sqrtPriceLimitX96: 0
};
const data = iface.encodeFunctionData("exactInputSingle", [params]);
// 2. Struktura transakcji
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"), // "Łapówka" dla walidatora
nonce: await wallet.getNonce()
};
// 3. Tworzymy podpisany bundle
const signedBundle = await flashbotsProvider.signBundle([
{
signer: wallet,
transaction: transaction
}
]);
// 4. SYMULACJA (Kluczowy etap)
console.log("Odpalam symulację bundle'a...");
const simulation = await flashbotsProvider.simulate(signedBundle, nextBlockNumber);
if ("error" in simulation) {
console.error(`Błąd symulacji: ${simulation.error.message}`);
return;
}
console.log("Symulacja udana!", JSON.stringify(simulation, null, 2));
return signedBundle;
}
3. Kwestie techniczne: Dlaczego symulacja to mus?
W "publicznym" Ethereum, jeśli Twoja transakcja zfailuje (np. braknie limitu gazu albo cena się zmieni), i tak trafi do bloku, a Ty zapłacisz za gaz.
W świecie Flashbots wygląda to inaczej:
- Jeśli symulacja wypluje błąd – po prostu nie wysyłasz bundle'a.
- Jeśli bundle zostanie wysłany, ale warunki w bloku się zmieniły i transakcja stała się nieopłacalna – walidator ją po prostu zignoruje.
Podsumowując: Płacisz za gaz tylko wtedy, gdy Twój Stealth Swap faktycznie się wykona.
4. Jak wyliczyć "łapówkę" (Priority Fee)
Walidatorzy wrzucają paczki do bloku w kolejności ich rentowności. Opłacalność bundle'a (Gas Price Score) liczona jest w ten sposób:

Przy zwykłym swapie maxPriorityFeePerGas na poziomie 1-2 gwei zazwyczaj styka. Ale przy dużej zmienności konkurencja o miejsce w bloku rośnie nawet na tych prywatnych torach.
Co mamy na ten moment?
Mamy gotowy, podpisany i sprawdzony bundle, który tylko czeka na wysyłkę. Wiemy na stówę, że kod Uniswapa zadziała poprawnie, a na portfelu starczy środków na pokrycie kosztów.
W kolejnej (ostatniej) części: napiszemy pętlę czekającą na blok, ogarniemy realną wysyłkę bundle'a i zrobimy wygodny interfejs CLI dla naszego narzędzia.