Documentation/Build on Signet/Bundles
Simulating Bundles
How to simulate Signet bundles before submission using the RPC method.
Once you’ve created a bundle, simulate it against a Signet RPC node before submitting. Simulation lets you verify execution, check profitability, and confirm that order fills are satisfied. The signet_callBundle RPC method is Signet’s equivalent to Flashbots’s eth_callBundle.
Setup
If you followed the setup in Bundles on Signet, add the provider features to alloy:
cargo add alloy --features provider-http,rpc-typesRequest Format
For simulation, use SignetCallBundle instead of SignetEthBundle. This type wraps Flashbots’s EthCallBundle and is designed for the RPC call rather than cache submission:
use signet_bundle::SignetCallBundle;
use alloy::rpc::types::mev::EthCallBundle;
let bundle = SignetCallBundle {
bundle: EthCallBundle {
// Normal Flashbots bundle fields
},
};Making the RPC Call
Use alloy’s Provider to call the signet_callBundle method:
use alloy::providers::{Provider, ProviderBuilder};
use signet_bundle::{SignetCallBundle, SignetCallBundleResponse};
let provider = ProviderBuilder::new()
.on_http("https://rpc.parmigiana.signet.sh".parse()?);
let response: SignetCallBundleResponse = provider
.client()
.request("signet_callBundle", (bundle,))
.await?;Response Format
The SignetCallBundleResponse extends the Flashbots response with Signet-specific order and fill data:
use signet_bundle::SignetCallBundleResponse;
// Standard Flashbots fields
println!("Bundle hash: {:?}", response.bundle_hash);
println!("Total gas used: {}", response.total_gas_used);
println!("Effective gas price: {} wei", response.bundle_gas_price);
// Signet-specific: aggregate orders and fills produced by the bundle
println!("Orders: {:?}", response.orders);
println!("Fills: {:?}", response.fills);The aggregate orders field (AggregateOrders) tracks the orders created by the bundle: What is being offered on the rollup, and what is expected to be received in the destination chain (either rollup, or host). The aggregate fills field (AggregateFills) tracks the assets transferred to recipients to satisfy existing orders.
Signet bundles have something unique about their validity compared to normal Flashbots bundles: The aggregate fills across all transactions must fully satisfy the aggregate order outputs. If any non-revertible transaction fails execution or produces insufficient fills to cover its orders, the entire bundle is invalid. Therefore, to check if a bundle is valid, check that the aggregate fills are greater than or equal aggregate orders for each asset and recipient.
Complete Example
Here’s a full simulation flow, from provider setup through response handling:
use alloy::{
eips::BlockNumberOrTag,
primitives::Bytes,
providers::{Provider, ProviderBuilder},
rpc::types::mev::EthCallBundle,
};
use signet_bundle::{SignetCallBundle, SignetCallBundleResponse};
let provider = ProviderBuilder::new()
.on_http("https://rpc.parmigiana.signet.sh".parse()?);
// Get the latest block number for simulation context
let block_number = provider.get_block_number().await?;
// Create the simulation bundle with your encoded transactions
let bundle = SignetCallBundle {
bundle: EthCallBundle {
txs: vec![/* your EIP-2718 encoded transactions */],
block_number: block_number + 1,
state_block_number: BlockNumberOrTag::Number(block_number),
..Default::default()
},
};
// Simulate
let response: SignetCallBundleResponse = provider
.client()
.request("signet_callBundle", (bundle,))
.await?;
// Check results
println!("Bundle hash: {:?}", response.bundle_hash);
println!("Total gas used: {}", response.total_gas_used);
println!("Gas price: {} wei", response.bundle_gas_price);
for (i, result) in response.results.iter().enumerate() {
match &result.revert {
Some(reason) => println!("tx {}: reverted - {}", i, reason),
None => println!("tx {}: success, gas used: {}", i, result.gas_used),
}
}Once simulation succeeds and fills satisfy orders, submit your bundle to the transaction cache.