En la primera parte, ya dejamos listo nuestro "túnel de comunicación seguro". En esta parte, nos metemos de lleno en la lógica del intercambio. Nuestra meta es armar una transacción para Uniswap V3 de modo que no solo sea privada, sino que tengamos la certeza de que se va a ejecutar sí o sí.
1. Preparando la transacción de swap
Para este ejemplo, vamos a configurar un cambio de ETH a USDC. Para que la transacción sea válida dentro de un bundle, necesitamos construir el objeto de la transacción de antemano, sin lanzarlo directamente a la red.
Esto es lo que necesitamos tener a mano:
- La dirección del Router de Uniswap V3.
- El ABI (al menos lo mínimo indispensable) para la función
exactInputSingle. - El cálculo del gas teniendo en cuenta la prioridad.
Regla de oro: En los bundles de Flashbots, el
gasPricede toda la vida se cambia pormaxFeePerGasymaxPriorityFeePerGas. Es justamente la priorityFee (la "propina" para el validador) lo que corta el bacalao y decide si tu bundle entra en el bloque o no.
2. Código: Montaje y simulación
Vamos a añadir el archivo swap.ts a nuestro proyecto. El plato fuerte aquí es el método .simulate(). Es la función estrella de Flashbots: te permite probar si la transacción chuta sobre el estado actual de la blockchain sin gastarte ni un céntimo en gas real.
import { ethers } from "ethers";
import { FlashbotsBundleRawTransaction } from "@flashbots/ethers-provider-bundle";
// ABI mínimo para interactuar con el Router de Uniswap V3
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. Preparamos la interfaz y los datos de la transacción
const iface = new ethers.Interface(ROUTER_ABI);
const deadline = Math.floor(Date.now() / 1000) + 60 * 20; // Margen de 20 minutos
const amountIn = ethers.parseEther("0.1"); // Vamos a cambiar 0.1 ETH
const params = {
tokenIn: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", // WETH
tokenOut: "0xA0b86991c6218b36c1d19D4a2e9Eb0ce3606eB48", // USDC
fee: 3000, // Pool del 0.3%
recipient: wallet.address,
deadline: deadline,
amountIn: amountIn,
amountOutMinimum: 0, // ¡En producción, calcula siempre el slippage!
sqrtPriceLimitX96: 0
};
const data = iface.encodeFunctionData("exactInputSingle", [params]);
// 2. Estructura de la transacción
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"), // El "soborno" al validador
nonce: await wallet.getNonce()
};
// 3. Creamos el bundle firmado
const signedBundle = await flashbotsProvider.signBundle([
{
signer: wallet,
transaction: transaction
}
]);
// 4. SIMULACIÓN (Paso crítico)
console.log("Lanzando simulación del bundle...");
const simulation = await flashbotsProvider.simulate(signedBundle, nextBlockNumber);
if ("error" in simulation) {
console.error(`Fallo en la simulación: ${simulation.error.message}`);
return;
}
console.log("¡Simulación niquelada!", JSON.stringify(simulation, null, 2));
return signedBundle;
}
3. El toque técnico: ¿Por qué es obligatoria la simulación?
En la mempool "pública", si tu transacción falla (por ejemplo, te quedas corto de gas o el precio se mueve), acaba entrando en el bloque igualmente y te toca pagar el gas por la cara.
En el mundo Flashbots:
- Si la simulación da error, simplemente no mandas el bundle. Dinero que te ahorras.
- Si mandas el bundle pero las condiciones del bloque cambian y el trade deja de salir a cuenta, el validador directamente pasa de él.
Conclusión: Solo pagas gas cuando tu Stealth Swap se ejecuta de verdad.
4. Cómo calcular el "soborno" (Priority Fee)
Los validadores meten los bundles por orden de rentabilidad. El "Gas Price Score" de un bundle se calcula así:

Para un swap normalito, una maxPriorityFeePerGas de 1-2 gwei suele bastar. Pero cuando el mercado está loco, la pelea por un hueco en el bloque se pone tensa, incluso en estos canales privados.
¿Qué tenemos por ahora?
Ya tenemos un bundle firmado y verificado, listo para entrar en acción. Sabemos a ciencia cierta que el código de Uniswap va a rodar fino y que la cartera tiene fondos para cubrir la jugada.
En la próxima (y última) parte: escribiremos el bucle de espera del bloque, haremos el envío real del bundle y montaremos una interfaz CLI apañada para nuestra herramienta.