# Full Example

{% hint style="warning" %}
This documentation is geared for the new `PFL Auction` system and submission of `pfl_addSearcherBundle` before we officially upgraded our servers will likely fail
{% endhint %}

{% tabs %}
{% tab title="Nodejs" %}

```typescript
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
{% endtab %}

{% tab title="Python" %}

```python
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
{% endtab %}
{% endtabs %}

Also don't forget to check [Helpers](https://fastlane-labs.gitbook.io/polygon-fastlane/searcher-guides/helpers).
