Dans la première partie, on a mis en place notre « tunnel de communication sécurisé ». Dans cette partie, on s'attaque au vif du sujet : la logique de swap. Notre mission est de construire une transaction Uniswap V3 qui soit non seulement privée, mais aussi garantie d'être exécutée sans accroc.
1. Préparation de la transaction de swap
Pour l'exemple, on va mettre en place un swap ETH vers USDC. Pour que la transaction soit valide au sein d'un bundle, on doit préparer l'objet de la transaction en amont, sans l'envoyer directement sur le réseau.
Ce qu'il nous faut sous la main :
- L'adresse du Router Uniswap V3.
- L'ABI (au moins le strict minimum) pour la fonction
exactInputSingle. - Un calcul des frais de gaz incluant la priorité.
La règle d'or : Dans les bundles Flashbots, le
gasPriceclassique est remplacé parmaxFeePerGasetmaxPriorityFeePerGas. C'est précisément le priorityFee (le « pourboire » pour le validateur) qui va déterminer si votre bundle finit dans un bloc ou non.
2. Code : Assemblage et Simulation
On ajoute le fichier swap.ts à notre projet. Le gros morceau ici, c'est la méthode .simulate(). C'est la fonctionnalité phare de Flashbots : elle permet de tester l'exécution de la transaction sur l'état actuel de la blockchain sans claquer un seul centime en gaz réel.
import { ethers } from "ethers";
import { FlashbotsBundleRawTransaction } from "@flashbots/ethers-provider-bundle";
// ABI minimal pour interagir avec le Router 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. On prépare l'interface et les données de la transaction
const iface = new ethers.Interface(ROUTER_ABI);
const deadline = Math.floor(Date.now() / 1000) + 60 * 20; // 20 minutes de marge
const amountIn = ethers.parseEther("0.1"); // On swap 0.1 ETH
const params = {
tokenIn: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", // WETH
tokenOut: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC
fee: 3000, // 0.3%
recipient: wallet.address,
deadline: deadline,
amountIn: amountIn,
amountOutMinimum: 0, // En prod, calculez toujours le slippage !
sqrtPriceLimitX96: 0
};
const data = iface.encodeFunctionData("exactInputSingle", [params]);
// 2. Structure de la transaction
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"), // Le fameux « pot-de-vin » au validateur
nonce: await wallet.getNonce()
};
// 3. Création du bundle signé
const signedBundle = await flashbotsProvider.signBundle([
{
signer: wallet,
transaction: transaction
}
]);
// 4. SIMULATION (Étape cruciale)
console.log("Lancement de la simulation du bundle...");
const simulation = await flashbotsProvider.simulate(signedBundle, nextBlockNumber);
if ("error" in simulation) {
console.error(`Échec de la simulation : ${simulation.error.message}`);
return;
}
console.log("Simulation réussie !", JSON.stringify(simulation, null, 2));
return signedBundle;
}
3. Le point technique : Pourquoi la simulation est indispensable ?
Sur le mempool « public », si votre transaction échoue (par exemple, limite de gaz trop courte ou prix qui bouge), elle est quand même incluse dans un bloc et vous payez les frais de gaz pour rien.
Dans l'univers Flashbots :
- Si la simulation renvoie une erreur, vous n'envoyez simplement pas le bundle.
- Si le bundle est envoyé mais que les conditions du bloc changent et rendent la transaction non rentable, le validateur l'ignorera purement et simplement.
En résumé : Vous ne payez le gaz que si votre Stealth Swap est effectivement exécuté.
4. Comment calculer le « pot-de-vin » (Priority Fee)
Les validateurs classent les bundles selon leur rentabilité. Le score de profitabilité d'un bundle (Gas Price Score) se calcule ainsi :

Pour un swap classique, un maxPriorityFeePerGas de 1 ou 2 gwei suffit généralement. Mais en période de forte volatilité, la compétition pour une place dans le bloc grimpe en flèche, même sur les circuits privés.
Où en sommes-nous ?
On a maintenant un bundle signé et vérifié, prêt à être balancé. On sait avec certitude que le code Uniswap va rouler correctement et que le solde du wallet couvre bien tous les frais.
Dans la prochaine (et dernière) partie : on va coder la boucle d'attente de bloc, envoyer réellement le bundle et pondre une interface CLI sympa pour notre outil.