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 = "0xF1b1b8827163C980a8EC7F3968140AEF00f3A5F3"; // current dappControl address (review docs)
const dAppOpSignerAddr = "0x96D501A4C52669283980dc5648EEC6437e2E6346"; // current dAppOpSigner address (review docs)
const atlasVerificationAddr = "0x621c6970fD9F124230feE35117d318069056819a"; // current atlasVerification address (review docs)
const atlasAddr = '0xB363f4D32DdB0b43622eA07Ae9145726941272B4';

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, // dAppOpSigner 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

Also don't forget to check Helpers.

Last updated