Polygon FastLane
  • What is Polygon FastLane?
    • Overview
    • Design Principles
    • Components
    • Component Diagram
  • Getting Started as a Validator
    • Getting Started as a Validator
    • Connecting to a FastLane Sentry Node
      • Finding Your Enode Address & Peer ID
      • Adding FastLane as a Static Peer
    • Patching Your Sentry Nodes With The FastLane Patch
      • Installing from source
        • Patch Download
        • Patch Installation
      • Installing from packages
  • Withdrawing Validator Revenue
    • Validator Vault
      • Connect an Eligible Wallet
      • Revenue Redemption (withdrawal)
  • Searcher Guides
    • Getting Started as a Searcher
      • Solver Call Data
      • Submission Methods
      • Migration Guide for Searchers
    • Bundles (Backruns)
      • Bundle Format
      • Bid Submission
      • Bundle Requirements
      • Full Example
      • Subscribe Events
    • 4337 Bundles Integration Guide
      • Overview
      • How it works
      • RPC Reference
      • Examples
    • Searcher Contract Integration
      • Safety Considerations
      • atlasSolverCall
      • Direct Implementation
      • Proxy Implementation
      • Solver Concepts
      • Altas Bonding Concept
      • Bond atlETH
      • Estimating Solver Gas Charges
    • Addresses & Endpoints
    • Helpers
    • Common Mistakes
    • Atlas SDK's
  • Tools and Analytics
    • FastLane Bundle Explorer
      • Features Overview
      • Key Components
      • Usage Example
      • Error Codes & Troubleshooting
  • Key Concepts
    • Transaction Encoding
  • INFRASTRUCTURE
    • Health Status Endpoint
  • Reference
    • Relay JSON-RPC API
    • Relay REST API
    • Glossary of Terms
Powered by GitBook
On this page
  1. Searcher Guides
  2. Bundles (Backruns)

Full Example

Full working code to submit bundle

This documentation is geared for the new PFL Auction system and submission of pfl_addSearcherBundle before we officially upgraded our servers will likely fail

import { OperationBuilder, SolverOperation } from "@fastlane-labs/atlas-sdk";
import axios, { AxiosInstance } from "axios";
import { Contract, Interface, JsonRpcProvider, keccak256, parseEther, TypedDataDomain, Wallet } from "ethers";

const dappControlAddr = "0x3e23e4282FcE0cF42DCd0E9bdf39056434E65C1F"; // current dappControl address (review docs)
const dAppOpSignerAddr = "0x96D501A4C52669283980dc5648EEC6437e2E6346"; // current dAppOpSigner address (review docs)
const atlasVerificationAddr = "0xf31cf8740Dc4438Bb89a56Ee2234Ba9d5595c0E9"; // current atlasVerification address (review docs)
const atlasAddr = '0x4A394bD4Bc2f4309ac0b75c052b242ba3e0f32e0';

const PFLControlAbi = [
  {
    "inputs": [
      { "internalType": "bytes32", "name": "oppTxHash", "type": "bytes32" },
      { "internalType": "uint256", "name": "oppTxMaxFeePerGas", "type": "uint256" },
      { "internalType": "uint256", "name": "oppTxMaxPriorityFeePerGas", "type": "uint256" },
      { "internalType": "address", "name": "fastLaneSigner", "type": "address" }
    ],
    "name": "getBackrunUserOpHash",
    "outputs": [{ "internalType": "bytes32", "name": "userOpHash", "type": "bytes32" }],
    "stateMutability": "view",
    "type": "function"
  },
];

const eip712Domain: TypedDataDomain = {
    name: "AtlasVerification",
    version: "1.0",
    chainId: 137,
    verifyingContract: atlasVerificationAddr,
}

interface PflBundle {
  id: number;
  jsonrpc: string;
  method: string;
  params: string[];
}

const provider = new JsonRpcProvider("https://polygon.llamarpc.com");
// PK are foundry default PKs
const userSigner = new Wallet("ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", provider);
const solverSigner = new Wallet("59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d", provider);

const dappControl = new Contract(dappControlAddr, PFLControlAbi, provider);

const httpFastlaneEndpoint = "https://polygon-rpc.fastlane.xyz";
const opportunityIsLegacyTx = false;

// helper function to get the http connection
const getHttpConnection = (endpoint: string): AxiosInstance => {
  return axios.create({
    baseURL: endpoint,
    timeout: 10000
  });
}

// helper function to submit the bundle to the fastlane endpoint
async function submitHttpBundle(bundle: PflBundle): Promise<void> {
  const conn = getHttpConnection(httpFastlaneEndpoint);
  try {
    const resp = await conn.post('/', bundle);
    if (resp.data.error) {
        console.error(`Error submitting bundle ${bundle.id}`, resp.data);
    } else {
        console.log(`Response received for bundle ${bundle.id}`, resp.data);
    }
  } catch (error) {
    console.error(`Error submitting bundle ${bundle.id}`, error);
  }
}

const createOpportunityRawTx = async (
    userSigner: Wallet,
    bidAmount: bigint,
    toAddress: string, 
    legacyTx: boolean, 
    maxFeePerGas: bigint | null, 
    maxPriorityFeePerGas: bigint | null
) => {
    if (!maxFeePerGas) {
        throw new Error("maxFeePerGas is required");
    }

    const txData = await userSigner.populateTransaction({
        to: toAddress,
        gasLimit: 100000,
        ...(legacyTx 
            ? { gasPrice: maxFeePerGas } 
            : { 
                maxFeePerGas, 
                maxPriorityFeePerGas: maxPriorityFeePerGas || maxFeePerGas
            }),
        value: bidAmount
    });
    return await userSigner.signTransaction(txData);
}

// Generate the solver call data for the solver operation
const generateSolverCallData = () => {
    const searcherAbi = [
        `function solve()`,
    ];

    const iface = new Interface(searcherAbi);

    // Grab bytes for solve()
    const searcherCallDataBytes = iface.encodeFunctionData("solve");
    return searcherCallDataBytes;
}

// helper function to generate the solver signature using eip712Domain
const generateSolverSignature = async (solverOp: SolverOperation) => {
    return await solverSigner.signTypedData(eip712Domain, solverOp.toTypedDataTypes(), solverOp.toTypedDataValues());
}

// helper function to generate the solver operation
const generateSolverOperation = async (userOpHash: string, bidAmount: bigint, maxFeePerGas: bigint, maxPriorityFeePerGas: bigint):Promise<SolverOperation> => {

    // Generate the solver call data
    const solverCallData = generateSolverCallData();

    // Generate the solver operation
    const solverOp = OperationBuilder.newSolverOperation({
        from: solverSigner.address, // solver address
        to: atlasAddr, // atlasAddr address
        value: BigInt(0), // 0 value
        gas: BigInt(500000), // 500,000 gasLimit
        maxFeePerGas: maxFeePerGas,
        deadline: BigInt(0), // 0 deadline
        solver: solverSigner.address, // solverSigner address
        control: dappControlAddr, // dappControl address
        userOpHash: userOpHash, 
        bidToken: "0x0000000000000000000000000000000000000000", // POL
        bidAmount: bidAmount,
        data: solverCallData,
        signature: "0x" // empty signature
    });
    
    // Generate the solver signature
    const solverSignature = await generateSolverSignature(solverOp);

    // Set the solver signature
    solverOp.setField("signature", solverSignature);
    return solverOp;
}


// helper function to generate the pfl bundle
const generatePflBundle = (solverOp: SolverOperation, opportunityRawTx: string, bundleId: number): PflBundle => {
    return {
        id: bundleId,
        jsonrpc: "2.0",
        method: "pfl_addSearcherBundle",
        params: [`${opportunityRawTx}`, `${JSON.stringify(solverOp.toStruct())}`]
    }
}

// main function to submit the bundle to the fastlane endpoint
async function main() {
    const gasPrice = await provider.getFeeData();
    //match opportunity tx transaction type
    console.log("gasPrice", gasPrice);
    const maxFeePerGas = opportunityIsLegacyTx ? gasPrice.gasPrice : gasPrice.maxFeePerGas;
    const maxPriorityFeePerGas = opportunityIsLegacyTx ? gasPrice.gasPrice : gasPrice.maxPriorityFeePerGas;

    if (!maxFeePerGas || !maxPriorityFeePerGas) {
        throw new Error("Failed to get gas price data");
    }

    const solverContract = "0x0000000000000000000000000000000000000000";
    const bidAmount = BigInt(10000000000000000); // 0.01 POL

    const opportunityRawTx = await createOpportunityRawTx(userSigner, bidAmount, solverContract, opportunityIsLegacyTx, maxFeePerGas, maxPriorityFeePerGas);
    console.log("opportunityRawTx", opportunityRawTx);
    // Ensure the values passed to getBackrunUserOpHash are BigInt
    const userOpHash = await dappControl.getBackrunUserOpHash(
        keccak256(opportunityRawTx),
        maxFeePerGas,
        maxPriorityFeePerGas,
        dAppOpSignerAddr
    );
    console.log("userOpHash", userOpHash);
    // Generate the solver operation using the userOpHash and bidAmount
    const solverOp = await generateSolverOperation(userOpHash, bidAmount, maxFeePerGas, maxPriorityFeePerGas);

    const pflBundle = generatePflBundle(solverOp, opportunityRawTx, 1);

    // Submit the bundle to the fastlane endpoint
    await submitHttpBundle(pflBundle);

}

main().catch(console.error);

Nodejs Version

In this example we backrun a transfer to ourselves

from web3 import Web3, HTTPProvider
from eth_account import Account
import requests
from eth_account.messages import encode_structured_data
from eip712_structs import (
    EIP712Struct,
    Uint,
    Address as EIP712Address,
    Bytes,
    make_domain,
)
import json
from web3.middleware import geth_poa_middleware

# Constants
dappControlAddr = Web3.to_checksum_address("0x3e23e4282FcE0cF42DCd0E9bdf39056434E65C1F")
dAppOpSignerAddr = Web3.to_checksum_address(
    "0x96D501A4C52669283980dc5648EEC6437e2E6346"
)
atlasVerificationAddr = Web3.to_checksum_address(
    "0xf31cf8740Dc4438Bb89a56Ee2234Ba9d5595c0E9"
)
atlasAddr = Web3.to_checksum_address("0x4A394bD4Bc2f4309ac0b75c052b242ba3e0f32e0")

PFLControlAbi = [
    {
        "inputs": [
            {"internalType": "bytes32", "name": "oppTxHash", "type": "bytes32"},
            {"internalType": "uint256", "name": "oppTxMaxFeePerGas", "type": "uint256"},
            {
                "internalType": "uint256",
                "name": "oppTxMaxPriorityFeePerGas",
                "type": "uint256",
            },
            {"internalType": "address", "name": "fastLaneSigner", "type": "address"},
        ],
        "name": "getBackrunUserOpHash",
        "outputs": [
            {"internalType": "bytes32", "name": "userOpHash", "type": "bytes32"}
        ],
        "stateMutability": "view",
        "type": "function",
    },
]

# Replace the eip712Domain dict with a proper domain object
eip712Domain = make_domain(
    name="AtlasVerification",
    version="1.0",
    chainId=137,
    verifyingContract=atlasVerificationAddr,
)


class PflBundle:
    def __init__(self, id: int, jsonrpc: str, method: str, params: list):
        self.id = id
        self.jsonrpc = jsonrpc
        self.method = method
        self.params = params


# Set up the provider
provider_url = "https://polygon.llamarpc.com"
web3 = Web3(HTTPProvider(provider_url))

# Inject PoA middleware if needed
web3.middleware_onion.inject(geth_poa_middleware, layer=0)

# For the user signer
user_private_key = "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"  # Foundry default PK
user_signer = Account.from_key(user_private_key)
user_signer_address = user_signer.address

# For the solver signer
solver_private_key = "59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d"  # Foundry default PK
solver_signer = Account.from_key(solver_private_key)
solver_signer_address = solver_signer.address

# dappControl contract instance
dappControl = web3.eth.contract(address=dappControlAddr, abi=PFLControlAbi)

# Constants
httpFastlaneEndpoint = "https://polygon-rpc.fastlane.xyz"
opportunityIsLegacyTx = False

# Mock values for gas prices for opportunity tx
# These will be fetched dynamically in the main function


def submit_http_bundle(bundle: PflBundle):
    try:
        resp = requests.post(
            httpFastlaneEndpoint,
            json={
                "id": bundle.id,
                "jsonrpc": bundle.jsonrpc,
                "method": bundle.method,
                "params": bundle.params,
            },
            timeout=10,
        )
        data = resp.json()
        if "error" in data:
            print(f"Error submitting bundle {bundle.id}: {data}")
        else:
            print(f"Response received for bundle {bundle.id}: {data}")
    except Exception as e:
        print(f"Error submitting bundle {bundle.id}: {e}")


def generate_solver_call_data():
    searcherAbi = [
        {
            "inputs": [],
            "name": "solve",
            "outputs": [],
            "stateMutability": "nonpayable",
            "type": "function",
        }
    ]
    iface = web3.eth.contract(abi=searcherAbi)
    solver_call_data_bytes = iface.encodeABI(fn_name="solve", args=[])
    return solver_call_data_bytes


class SolverOperationStruct(EIP712Struct):
    from_addr = EIP712Address()
    to = EIP712Address()
    value = Uint(256)
    gas = Uint(256)
    maxFeePerGas = Uint(256)
    deadline = Uint(256)
    solver = EIP712Address()
    control = EIP712Address()
    userOpHash = Bytes(32)
    bidToken = EIP712Address()
    bidAmount = Uint(256)
    data = Bytes()


class SolverOperation:
    def __init__(self, **kwargs):
        self.from_addr = kwargs.get("from")
        self.to = kwargs.get("to")
        self.value = int(kwargs.get("value"))
        self.gas = int(kwargs.get("gas"))
        self.maxFeePerGas = int(kwargs.get("maxFeePerGas"))
        self.deadline = int(kwargs.get("deadline"))
        self.solver = kwargs.get("solver")
        self.control = kwargs.get("control")
        self.userOpHash = kwargs.get("userOpHash")
        self.bidToken = kwargs.get("bidToken")
        self.bidAmount = int(kwargs.get("bidAmount"))
        self.data = kwargs.get("data")
        self.signature = kwargs.get("signature")

    def set_field(self, field, value):
        setattr(self, field, value)

    def to_struct(self):
        # Return a dictionary representing the operation, with numeric values as hex strings
        return {
            "from": self.from_addr,
            "to": self.to,
            "value": hex(self.value),  # Convert to hex
            "gas": hex(self.gas),  # Convert to hex
            "maxFeePerGas": hex(self.maxFeePerGas),  # Convert to hex
            "deadline": hex(self.deadline),  # Convert to hex
            "solver": self.solver,
            "control": self.control,
            "userOpHash": self.userOpHash,  # Already hex string
            "bidToken": self.bidToken,
            "bidAmount": hex(self.bidAmount),  # Convert to hex
            "data": self.data,  # Already hex string
            "signature": self.signature,  # Already hex string
        }


def generate_solver_signature(solverOp: SolverOperation):
    """
    Generates an EIP-712 signature for a solver operation.

    Args:
        solverOp: SolverOperation object containing the operation details

    Returns:
        str: Hex string of the signature
    """

    # Convert hex strings to bytes for bytes32 and bytes types
    user_op_hash_bytes = bytes.fromhex(solverOp.userOpHash[2:])  # Remove '0x' prefix
    data_bytes = bytes.fromhex(
        solverOp.data[2:] if solverOp.data.startswith("0x") else solverOp.data
    )

    # Prepare the typed data structure for EIP-712 signing
    typed_data = {
        "types": {
            "EIP712Domain": [
                {"name": "name", "type": "string"},
                {"name": "version", "type": "string"},
                {"name": "chainId", "type": "uint256"},
                {"name": "verifyingContract", "type": "address"},
            ],
            "SolverOperation": [
                {"name": "from", "type": "address"},
                {"name": "to", "type": "address"},
                {"name": "value", "type": "uint256"},
                {"name": "gas", "type": "uint256"},
                {"name": "maxFeePerGas", "type": "uint256"},
                {"name": "deadline", "type": "uint256"},
                {"name": "solver", "type": "address"},
                {"name": "control", "type": "address"},
                {"name": "userOpHash", "type": "bytes32"},
                {"name": "bidToken", "type": "address"},
                {"name": "bidAmount", "type": "uint256"},
                {"name": "data", "type": "bytes"},
            ],
        },
        "primaryType": "SolverOperation",
        "domain": {
            "name": "AtlasVerification",
            "version": "1.0",
            "chainId": 137,  # Polygon mainnet
            "verifyingContract": atlasVerificationAddr,
        },
        "message": {
            "from": solverOp.from_addr,
            "to": solverOp.to,
            "value": solverOp.value,
            "gas": solverOp.gas,
            "maxFeePerGas": solverOp.maxFeePerGas,
            "deadline": solverOp.deadline,
            "solver": solverOp.solver,
            "control": solverOp.control,
            "userOpHash": user_op_hash_bytes,
            "bidToken": solverOp.bidToken,
            "bidAmount": solverOp.bidAmount,
            "data": data_bytes,
        },
    }

    # Encode and sign the structured data
    encoded_data = encode_structured_data(primitive=typed_data)
    signed_message = solver_signer.sign_message(encoded_data)

    return signed_message.signature.hex()


def generate_solver_operation(
    userOpHash: str, bidAmount: int, maxFeePerGas: int, maxPriorityFeePerGas: int
) -> SolverOperation:
    # Generate the solver call data
    solver_call_data = generate_solver_call_data()

    # Generate the solver operation
    solverOp = SolverOperation(
        **{
            "from": solver_signer_address,  # solver address
            "to": atlasAddr,  # atlasAddr address
            "value": 0,  # 0 value
            "gas": 500000,  # 500,000 gasLimit
            "maxFeePerGas": maxFeePerGas,
            "deadline": 0,  # 0 deadline
            "solver": solver_signer_address,  # solver address
            "control": dappControlAddr,  # dappControl address
            "userOpHash": userOpHash,
            "bidToken": "0x0000000000000000000000000000000000000000",  # POL
            "bidAmount": bidAmount,
            "data": solver_call_data,
            "signature": "0x",  # empty signature initially
        }
    )

    # Generate the solver signature
    solver_signature = generate_solver_signature(solverOp)

    # Set the solver signature
    solverOp.set_field("signature", solver_signature)
    return solverOp


def generate_pfl_bundle(
    solverOp: SolverOperation, opportunityRawTx: str, bundle_id: int
) -> PflBundle:
    return PflBundle(
        id=bundle_id,
        jsonrpc="2.0",
        method="pfl_addSearcherBundle",
        params=[opportunityRawTx, json.dumps(solverOp.to_struct())],
    )


def create_opportunity_raw_tx(
    user_signer: Account,
    bid_amount: int,
    to_address: str,
    legacy_tx: bool,
    max_fee_per_gas: int,
    max_priority_fee_per_gas: int,
) -> str:
    if not max_fee_per_gas:
        raise ValueError("maxFeePerGas is required")

    tx = {
        "to": to_address,
        "gas": 100000,
        "value": bid_amount,
    }

    if legacy_tx:
        tx["gasPrice"] = max_fee_per_gas
    else:
        tx["maxFeePerGas"] = max_fee_per_gas
        tx["maxPriorityFeePerGas"] = (
            max_priority_fee_per_gas if max_priority_fee_per_gas else max_fee_per_gas
        )

    # Get the latest nonce
    nonce = web3.eth.get_transaction_count(user_signer.address)
    tx["nonce"] = nonce

    # Get chain ID
    chain_id = web3.eth.chain_id
    tx["chainId"] = chain_id

    # Sign the transaction
    signed_tx = web3.eth.account.sign_transaction(tx, private_key=user_signer.key)
    raw_tx = signed_tx.rawTransaction.hex()
    return raw_tx


def main():
    # Fetch current fee data
    fee_data = web3.eth.fee_history(1, "latest", [10, 50, 90])
    base_fee = fee_data["baseFeePerGas"][-1]
    priority_fee = web3.eth.max_priority_fee

    # Calculate maxFeePerGas and maxPriorityFeePerGas
    max_priority_fee_per_gas = priority_fee
    max_fee_per_gas = base_fee + max_priority_fee_per_gas

    print(f"Base Fee: {web3.from_wei(base_fee, 'gwei')} gwei")
    print(
        f"Max Priority Fee Per Gas: {web3.from_wei(max_priority_fee_per_gas, 'gwei')} gwei"
    )
    print(f"Max Fee Per Gas: {web3.from_wei(max_fee_per_gas, 'gwei')} gwei")

    # Ensure fee values are in Wei (integer) format

    if not isinstance(max_fee_per_gas, int):
        max_fee_per_gas = int(max_fee_per_gas)
    if not isinstance(max_priority_fee_per_gas, int):
        max_priority_fee_per_gas = int(max_priority_fee_per_gas)

    solver_contract = "0x0000000000000000000000000000000000000000"
    bid_amount = Web3.to_wei(0.01, "ether")  # 0.01 ETH

    # Create the opportunity raw transaction
    opportunity_raw_tx = create_opportunity_raw_tx(
        user_signer=user_signer,
        bid_amount=bid_amount,
        to_address=solver_contract,
        legacy_tx=False,  # EIP-1559 transaction
        max_fee_per_gas=max_fee_per_gas,
        max_priority_fee_per_gas=max_priority_fee_per_gas,
    )
    print("opportunityRawTx:", opportunity_raw_tx)

    # Compute the opportunity transaction hash
    opp_tx_hash = Web3.keccak(
        bytes.fromhex(opportunity_raw_tx[2:])
    )  # Remove '0x' prefix
    print("oppTxHash:", opp_tx_hash.hex())

    # Call getBackrunUserOpHash on the dappControl contract
    try:
        user_op_hash = dappControl.functions.getBackrunUserOpHash(
            opp_tx_hash,
            max_fee_per_gas,
            max_priority_fee_per_gas,
            dAppOpSignerAddr,
        ).call()
        user_op_hash_hex = "0x" + user_op_hash.hex()
        print("userOpHash:", user_op_hash_hex)
    except Exception as e:
        print("Error calling getBackrunUserOpHash:", e)
        raise

    # Generate the solver operation using the userOpHash and bidAmount
    solver_op = generate_solver_operation(
        userOpHash=user_op_hash_hex,
        bidAmount=bid_amount,
        maxFeePerGas=max_fee_per_gas,
        maxPriorityFeePerGas=max_priority_fee_per_gas,
    )

    # Generate the pfl bundle
    pfl_bundle = generate_pfl_bundle(solver_op, opportunity_raw_tx, 1)

    # Submit the bundle to the fastlane endpoint
    submit_http_bundle(pfl_bundle)


if __name__ == "__main__":
    main()

Python Version

PreviousBundle RequirementsNextSubscribe Events

Last updated 5 months ago

Also don't forget to check .

Helpers