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
from eth_typing import Address
from web3.types import Wei
from web3 import Web3, HTTPProvider
from eth_account import Account
from eth_utils import keccak
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("0xF1b1b8827163C980a8EC7F3968140AEF00f3A5F3")
dAppOpSignerAddr = Web3.to_checksum_address(
"0x96D501A4C52669283980dc5648EEC6437e2E6346"
)
atlasVerificationAddr = Web3.to_checksum_address(
"0x621c6970fD9F124230feE35117d318069056819a"
)
atlasAddr = Web3.to_checksum_address("0xB363f4D32DdB0b43622eA07Ae9145726941272B4")
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):
op = SolverOperationStruct(
from_addr=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=bytes.fromhex(solverOp.userOpHash[2:]),
bidToken=solverOp.bidToken,
bidAmount=solverOp.bidAmount,
data=bytes.fromhex(solverOp.data[2:])
if solverOp.data.startswith("0x")
else bytes.fromhex(solverOp.data),
)
# 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
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": str(eip712Domain.name),
"version": str(eip712Domain.version),
"chainId": 137,
"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 the structured data
encoded_data = encode_structured_data(primitive=typed_data)
# Sign the encoded 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": dAppOpSignerAddr, # dAppOpSigner 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
Also don't forget to check Helpers.
Last updated