Skip to main content

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.

This page assumes you’ve completed the getting started setup.

Build a simulation request

SignetCallBundleBuilder mirrors the submission builder but targets the signet_callBundle RPC method instead of the tx-cache HTTP endpoint. It requires transactions, a target block number, and a state block number to fork from:

typescript
import { SignetCallBundleBuilder } from '@signet-sh/sdk/signing';

const currentBlock = await signetPublicClient.getBlockNumber();

const bundle = SignetCallBundleBuilder.new()
  .withTxs([signedTx1, signedTx2])
  .withBlockNumber(currentBlock + 1n)
  .withStateBlockNumber('latest')
  .build();

Submit the simulation

Call signet_callBundle on a Signet RPC node. The SDK provides serialization helpers for JSON-RPC transmission:

typescript
import {
  serializeCallBundle,
  deserializeCallBundleResponse,
  type SignetCallBundleResponse,
} from '@signet-sh/sdk/signing';

const rawResponse = await signetPublicClient.request({
  method: 'signet_callBundle' as any,
  params: [serializeCallBundle(bundle)],
});

const response: SignetCallBundleResponse =
  deserializeCallBundleResponse(rawResponse as any);

Parse the response

The response contains per-transaction results and Signet-specific order/fill aggregation:

typescript
console.log('Bundle hash:', response.bundleHash);
console.log('Total gas used:', response.totalGasUsed);
console.log('Gas price:', response.bundleGasPrice);

for (const [i, result] of response.results.entries()) {
  if (result.revert) {
    console.log(`tx ${i}: reverted -`, result.revert);
  } else {
    console.log(`tx ${i}: success, gas used: ${result.gasUsed}`);
  }
}

// Signet-specific: aggregate orders and fills produced by the bundle
console.log('Orders:', response.orders);
console.log('Fills:', response.fills);

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.

Next steps

Once simulation succeeds and fills satisfy orders, submit your bundle to the transaction cache.

This page assumes you’ve completed the getting started setup.

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

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.

Example

Here’s a full simulation flow, from provider setup through response handling:

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

This page assumes you’ve completed the getting started setup.

Simulate a bundle

Use cast rpc to call signet_callBundle directly:

bash
cast rpc signet_callBundle \
  '{"txs":["0x02f8...signed_tx_1","0x02f8...signed_tx_2"],"blockNumber":"0x1","stateBlockNumber":"latest"}' \
  --rpc-url $SIGNET_RPC

The txs array contains raw EIP-2718 encoded signed transactions. blockNumber is the target block (hex-encoded), and stateBlockNumber is the state to fork from ("latest" or a hex block number).

Response

The RPC returns a JSON object with per-transaction results and Signet-specific order/fill aggregation:

json
{
  "bundleHash": "0x...",
  "bundleGasPrice": "0x...",
  "totalGasUsed": 150000,
  "results": [
    {
      "txHash": "0x...",
      "gasUsed": 75000,
      "value": "0x..."
    }
  ],
  "orders": { "outputs": {}, "inputs": {} },
  "fills": { "fills": {} }
}

If a transaction reverts, its result includes a revert field with the revert reason instead of value. The bundle is invalid if any non-revertible transaction fails or if fills don’t cover order outputs.

ESC

Start typing to search documentation...

Navigate Select ⌘K Open