Full Example
// node LTS (v18.12.1)
// consider node-fetch to replace native fetch for lower
const https = require("https");
const { ethers } = require('ethers');
const { utils } = ethers;
// Find me at: https://github.com/FastLane-Labs/auction/blob/main/contracts/abis/FastLaneAuctionHandlerAbi.json
const abi = require('./FastLaneAuctionHandlerAbi.json');
const RELAY = "https://beta-rpc.fastlane-labs.xyz/";
const PRIVATE_KEY = "0xPrivateK3Y";
// Only used for Gas Price and Nonces
const ALCHEMY_KEY = "alchemy-apikey";
const provider = new ethers.providers.AlchemyProvider("matic", ALCHEMY_KEY);
const signer = new ethers.Wallet(PRIVATE_KEY);
console.log(`Address: ${signer.address}`);
const BID_AMOUNT = 100000;
// Bogus echo contract see https://github.com/FastLane-Labs/auction/blob/main/contracts/helpers/SearcherHelperRepayerEcho.sol
const searcherContract = "0x2FEf5C22a63CC8FE424bd83af9D1955A5c2d9e2E";
const auctionHandler = "0xf5DF545113DeE4DF10f8149090Aa737dDC05070a";
// Assuming as a searcher I intend to use _searcherCallDataBytes
// when received on my searcher contract `fastLaneCall(` callback
// to then trigger searcherContract.doMEV(uint256,string);
// See: Searcher Call Data section of the docs for more informations
// 0x2FEf5C22a63CC8FE424bd83af9D1955A5c2d9e2E contract will not use it, just an example
const searcherAbi = [
`function doMEV(uint256, string)`,
];
const iface = new ethers.utils.Interface(searcherAbi);
// Grab bytes for doMEV(uint256, string)
const searcherCallDataBytes = iface.encodeFunctionData("doMEV", [ethers.utils.parseEther("1.0"), "unused"]);
const AuctionHandlerContract = new ethers.Contract(auctionHandler, abi, signer);
async function createBundle() {
const nonce = await provider.getTransactionCount(signer.address);
const feeData = await provider.getFeeData();
const { gasPrice, lastBaseFeePerGas, maxFeePerGas, maxPriorityFeePerGas } = feeData;
console.log(`GAS: ${utils.formatUnits(gasPrice, "gwei")} gwei, NONCE: ${nonce}`);
console.log(`lastBaseFeePerGas: ${utils.formatUnits(maxFeePerGas, "gwei")} gwei`);
console.log(`maxFeePerGas: ${utils.formatUnits(maxFeePerGas, "gwei")} gwei`);
console.log(`maxPriorityFeePerGas: ${utils.formatUnits(maxPriorityFeePerGas, "gwei")} gwei`);
// Forge our own oppTx to backrun for demo purposes
// We'll sign this one from `signer` but normally it would originate from another EOA
const oppTx = {
type: 2,
from: signer.address,
to: signer.address,
value: utils.parseEther('0.000001'),
nonce: nonce,
gasLimit: "21000",
maxPriorityFeePerGas: gasPrice.add(lastBaseFeePerGas),
maxFeePerGas: gasPrice.add(gasPrice),
chainId: 137,
};
const checkOpp = await signer.checkTransaction(oppTx);
const signedOppTx = await signer.signTransaction(oppTx);
const oppTxHash = ethers.utils.keccak256(signedOppTx);
console.log("Precomputed oppTxHash:", oppTxHash);
const searcherTx = await AuctionHandlerContract.populateTransaction.submitFlashBid(
BID_AMOUNT,
oppTxHash,
searcherContract,
searcherCallDataBytes,
{
type: 2,
value: BID_AMOUNT,
nonce: nonce + 1,
gasLimit: utils.hexlify(500000),
// Must be same gas than opp
maxPriorityFeePerGas: gasPrice.add(lastBaseFeePerGas),
maxFeePerGas: gasPrice.add(gasPrice),
});
searcherTx.chainId = 137;
const checkSearcher = await signer.checkTransaction(searcherTx);
const signedSearcherTx = await signer.signTransaction(searcherTx);
console.log("Precomputed searcherTxHash:", ethers.utils.keccak256(signedSearcherTx));
const bundle = [
signedOppTx,
signedSearcherTx
];
// console.log(bundle);
return bundle;
}
const agent = new https.Agent({
keepAlive: true,
});
const ping = () => fetch(`${RELAY}/ping`, { agent }).then(r => console.log(`PONG : ${r.statusText}`));
const sendBundle = postData => fetch(RELAY, {
method: "POST", // or 'PUT',
agent,
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(postData),
});
const keepWarm = async() => {
setInterval(ping, 1000*60*10);
return ping();
}
async function main() {
// Warm it up.
await keepWarm();
const pfl_bundle = await createBundle(); // See submitFlashBid js doc
const postData = {
jsonrpc: "2.0",
method: "pfl_addSearcherBundle",
params: [pfl_bundle],
id: 1,
};
console.log(postData);
// Whenever needed. Fire a bundle.
const start = +Date.now();
await sendBundle(postData)
// See https://fastlane-labs.gitbook.io/polygon-fastlane/reference/relay-json-rpc-api
// as response can still have a .error field
.then(async(response) => {
const elapsed = +Date.now() - start;
console.log("Success:", response.statusText, elapsed);
const json = await response.json();
console.log(json);
if (json.error) throw json.error.message;
})
.catch((error) => {
console.error("Error:", error);
});
};
const getAuthor = async(blockNum) => {
const response = await fetch(`https://polygon-mainnet.g.alchemy.com/v2/${ALCHEMY_KEY}`, {
method: 'POST',
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
"id": 1,
"jsonrpc": "2.0",
"method": "bor_getAuthor",
"params": [
'0x'+blockNum.toString(16)
]
}),
});
const json = await response.json();
return json.result;
}
let waitBlock = 0;
provider.on('block', async(block) => {
try {
console.log(`Block: ${block}`);
if (block < waitBlock) return;
const author = await getAuthor(block);
console.log(author);
const validators = [
'0x127685D6dD6683085Da4B6a041eFcef1681E5C9C',
'0x02f70172f7f490653665c9bfac0666147c8af1f5',
'0xd2e4bE547B21E99A668f81EEcF0298471A19808f',
'0xb9EDE6f94D192073D8eaF85f8db677133d483249',
'0x9eaD03F7136Fc6b4bDb0780B00a1c14aE5A8B6d0',
].map(x => x.toLowerCase());
if (author && validators.includes(author.toLowerCase())) {
console.log('Validator span');
// Will shoot one demo bundle and close
await main();
console.log('Closing');
process.exit(0);
} else {
waitBlock = block + 10;
}
} catch (err) {
console.error(err);
}
});
```
Nodejs Version
In this example we backrun a transfer to ourselves
import json
import requests
from web3 import Web3
from hexbytes import HexBytes
import eth_abi
# setup requests session
s = requests.Session()
# setup keepalive connection
s.get('https://beta-rpc.fastlane-labs.xyz/ping')
# get the opportunity-creating transaction's parameters from the mempool
opportunityTx = web3.eth.get_transaction(hash_of_a_tx_in_mempool)
# Match the gas price parameters
searcherTxDict = {
'from': searcher_eoa_address,
'nonce': searcher_eoa_nonce,
'maxFeePerGas': opportunityTx.maxFeePerGas,
'maxPriorityFeePerGas': opportunityTx.maxPriorityFeePerGas,
'gas': 500_000,
'chainId': 137,
}
# Get the _searcherCallData
with open("searcher_abi.json") as _searcher_ABI:
searcher_ABI = json.load(_searcher_ABI)
searcher_contract = web3.eth.contract(
address=_searcherToAddress,
abi=searcher_ABI
)
regularSearcherTx = searcher_contract.functions.doExampleStuff(
Web3.toChecksumAddress(an_example_address),
int(an_amount),
).buildTransaction(searcherTxDict)
_searcherCallData = HexBytes(regularSearcherTx['data']
# make sure the bid amount is in wei
_bidAmount = 1 * 10**18 # To bid 1 matic
# format the opportunity-creating transaction hash to bytes32
_oppTxHash = eth_abi.encode_single('bytes32', opportunityTx.hash)
# create and sign the submitFlashBid transaction
with open("pfl_jit_auction_abi.json") as _fast_lane_ABI:
fast_lane_ABI = json.load(_fast_lane_ABI)
pfl_contract = web3.eth.contract(address=PFL_CONTRACT_ADDRESS, abi=fast_lane_ABI)
unsignedSearcherFlashBidTx = pfl_contract.functions.submitFlashBid(
_bidAmount,
_oppTxHash,
_searcherToAddress,
_searcherCallData
).buildTransaction(searcherTxDict)
signedSearcherFlashBidTx = web3.eth.account.sign_transaction(
unsignedSearcherFlashBidTx,
searcher_private_key
)
# get the full opportunity-creating transaction to send to the FastLane relay
fullOpportunityTx = web3.eth.get_raw_transaction(opportunityTx.hash)
# build the FastLane bundle
pfl_bundle = [
fullOpportunityTx.hex(),
signedSearcherFlashBidTx.rawTransaction.hex()
]
# submit the FastLane bundle to the relay
rsp = s.post('https://beta-rpc.fastlane-labs.xyz', json={
"jsonrpc": "2.0",
"method": "pfl_addSearcherBundle",
"params": [pfl_bundle],
"id": 1,
})
Python Version
Also don't forget to check Helpers.
Last updated