Documentation/Build on Signet
Simulating Signet bundles
Simulating Signet bundles with the `signet_callBundle` endpoint
Before submitting a bundle to the cache, you can simulate it against a Signet RPC node to verify it executes correctly, check profitability, and see what order fills you’ll need for the bundle to be valid. The signet_callBundle RPC method is Signet’s equivalent to Flashbots’s eth_callBundle.
Setup
Add the required Signet and Alloy crates to your project:
cargo add signet-bundle --git https://github.com/init4tech/signet-sdk
cargo add alloy --features provider-http,rpc-typesRequest Format
The SignetCallBundle type request wraps an EthCallBundle with the same structure as Flashbots. There are no differences:
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.
Putting it all together
With what we’ve learned, here’s a complete example of how the flow would look:
use alloy::{
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
let block_number = provider.get_block_number().await?;
// Build the bundle
let bundle = SignetCallBundle {
bundle: EthCallBundle {
txs: vec![/* your transactions */],
block_number: block_number + 1,
state_block_number: Some(block_number),
timestamp: None,
},
};
// Simulate
let response: SignetCallBundleResponse = provider
.client()
.request("signet_callBundle", (bundle,))
.await?;
println!("Bundle hash: {:?}", response.bundle_hash);
println!("Total gas used: {}", response.total_gas_used);
println!("Gas price: {} wei", response.bundle_gas_price);
// Check for reverts
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),
}
}