Skip to main content

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:

bash
cargo add signet-bundle --git https://github.com/init4tech/signet-sdk
cargo add alloy --features provider-http,rpc-types

Request Format

The SignetCallBundle type request wraps an EthCallBundle with the same structure as Flashbots. There are no differences:

rust
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:

rust
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:

rust
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:

rust
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),
    }
}
ESC

Start typing to search documentation...

Navigate Select ⌘K Open