├── .flake8 ├── .github └── workflows │ └── ruff.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .vscode └── settings.json ├── LICENSE ├── PKG-INFO ├── README.md ├── examples └── simple.py ├── flashbots ├── __init__.py ├── constants.py ├── flashbots.py ├── middleware.py ├── provider.py └── types.py ├── poetry.lock ├── pyproject.toml ├── release.sh └── setup.py /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = E203, E266, E501, W503, F403, F401 3 | max-line-length = 79 4 | max-complexity = 18 5 | select = B,C,E,F,W,T4,B9 -------------------------------------------------------------------------------- /.github/workflows/ruff.yml: -------------------------------------------------------------------------------- 1 | name: Ruff 2 | on: [ push, pull_request ] 3 | jobs: 4 | ruff: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v4.1.7 8 | - uses: chartboost/ruff-action@v1 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | *.pyc 3 | .idea 4 | build/ 5 | dist/ 6 | flashbots.egg-info/ 7 | .venv/ 8 | .mise.toml -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/astral-sh/ruff-pre-commit 3 | # Ruff version. 4 | rev: v0.5.4 5 | hooks: 6 | # Run the linter. 7 | - id: ruff 8 | args: [--fix] 9 | # Run the formatter. 10 | - id: ruff-format -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[python]": { 3 | "editor.formatOnSave": true, 4 | "editor.codeActionsOnSave": { 5 | "source.fixAll": "explicit", 6 | "source.organizeImports": "explicit" 7 | }, 8 | "editor.defaultFormatter": "charliermarsh.ruff" 9 | } 10 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Flashbots 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /PKG-INFO: -------------------------------------------------------------------------------- 1 | Metadata-Version: 2.1 2 | Name: flashbots 3 | Version: 2.0.0 4 | Summary: flashbots client 5 | Author: Georgios Konstantopoulos 6 | Author-email: 7 | Requires-Python: >=3.9,<4.0 8 | Classifier: Programming Language :: Python :: 3 9 | Classifier: Programming Language :: Python :: 3.9 10 | Requires-Dist: web3 (>=6,<7) 11 | Description-Content-Type: text/markdown 12 | 13 | This library works by injecting a new module in the Web3.py instance, which allows 14 | submitting "bundles" of transactions directly to miners. This is done by also creating 15 | a middleware which captures calls to `eth_sendBundle` and `eth_callBundle`, and sends 16 | them to an RPC endpoint which you have specified, which corresponds to `mev-geth`. 17 | To apply correct headers we use the `flashbot` function which injects the correct header on POST. 18 | 19 | ## Quickstart 20 | 21 | ```python 22 | from eth_account.signers.local import LocalAccount 23 | from web3 import Web3, HTTPProvider 24 | from flashbots import flashbot 25 | from eth_account.account import Account 26 | import os 27 | 28 | ETH_ACCOUNT_SIGNATURE: LocalAccount = Account.from_key(os.environ.get("ETH_SIGNATURE_KEY")) 29 | 30 | 31 | w3 = Web3(HTTPProvider("http://localhost:8545")) 32 | flashbot(w3, ETH_ACCOUNT_SIGNATURE) 33 | ``` 34 | 35 | Now the `w3.flashbots.sendBundle` method should be available to you. Look in `examples/simple.py` for usage examples. 36 | 37 | ### Goerli 38 | 39 | To use goerli, add the goerli relay RPC to the `flashbot` function arguments. 40 | 41 | ```python 42 | flashbot(w3, ETH_ACCOUNT_SIGNATURE, "https://relay-goerli.flashbots.net") 43 | ``` 44 | 45 | ## Development and testing 46 | 47 | Install [poetry](https://python-poetry.org/) 48 | 49 | Poetry will automatically fix your venv and all packages needed. 50 | 51 | ```sh 52 | poetry install 53 | ``` 54 | 55 | Tips: PyCharm has a poetry plugin 56 | 57 | ## Simple Goerli Example 58 | 59 | See `examples/simple.py` for environment variable definitions. 60 | 61 | ```sh 62 | poetry shell 63 | ETH_SENDER_KEY= \ 64 | PROVIDER_URL=https://eth-goerli.alchemyapi.io/v2/ \ 65 | ETH_SIGNER_KEY= \ 66 | python examples/simple.py 67 | ``` 68 | 69 | ## Linting 70 | 71 | It's advisable to run black with default rules for linting 72 | 73 | ```sh 74 | sudo pip install black # Black should be installed with a global entrypoint 75 | black . 76 | ``` 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # web3-flashbots 2 | 3 | This library works by injecting flashbots as a new module in the Web3.py instance, which allows submitting "bundles" of transactions directly to miners. This is done by also creating a middleware which captures calls to `eth_sendBundle` and `eth_callBundle`, and sends them to an RPC endpoint which you have specified, which corresponds to `mev-geth`. 4 | 5 | To apply correct headers we use the `flashbot` method which injects the correct header on POST. 6 | 7 | ## Quickstart 8 | 9 | ```python 10 | from eth_account.signers.local import LocalAccount 11 | from web3 import Web3, HTTPProvider 12 | from flashbots import flashbot 13 | from eth_account.account import Account 14 | import os 15 | 16 | ETH_ACCOUNT_SIGNATURE: LocalAccount = Account.from_key(os.environ.get("ETH_SIGNER_KEY")) 17 | 18 | 19 | w3 = Web3(HTTPProvider("http://localhost:8545")) 20 | flashbot(w3, ETH_ACCOUNT_SIGNATURE) 21 | ``` 22 | 23 | Now the `w3.flashbots.sendBundle` method should be available to you. Look in [examples/simple.py](./examples/simple.py) for usage examples. 24 | 25 | ### Goerli 26 | 27 | To use goerli, add the goerli relay RPC to the `flashbot` function arguments. 28 | 29 | ```python 30 | flashbot(w3, ETH_ACCOUNT_SIGNATURE, "https://relay-goerli.flashbots.net") 31 | ``` 32 | 33 | ## Development and testing 34 | 35 | Install [poetry](https://python-poetry.org/) 36 | 37 | Poetry will automatically fix your venv and all packages needed. 38 | 39 | ```sh 40 | poetry install 41 | ``` 42 | 43 | Tips: PyCharm has a poetry plugin 44 | 45 | ## Simple Testnet Example 46 | 47 | See [examples/simple.py](./examples/simple.py) for environment variable definitions. 48 | 49 | ```sh 50 | poetry shell 51 | ETH_SENDER_KEY= \ 52 | PROVIDER_URL=https://eth-holesky.g.alchemy.com/v2/ \ 53 | ETH_SIGNER_KEY= \ 54 | python examples/simple.py 55 | ``` 56 | 57 | ## Linting 58 | 59 | It's advisable to run black with default rules for linting 60 | 61 | ```sh 62 | sudo pip install black # Black should be installed with a global entrypoint 63 | black . 64 | ``` 65 | -------------------------------------------------------------------------------- /examples/simple.py: -------------------------------------------------------------------------------- 1 | """ 2 | Minimal viable example of flashbots usage with dynamic fee transactions. 3 | Sends a bundle of two transactions which transfer some ETH into a random account. 4 | 5 | Environment Variables: 6 | - ETH_SENDER_KEY: Private key of account which will send the ETH. 7 | - ETH_SIGNER_KEY: Private key of account which will sign the bundle. 8 | - This account is only used for reputation on flashbots and should be empty. 9 | - PROVIDER_URL: (Optional) HTTP JSON-RPC Ethereum provider URL. If not set, Flashbots Protect RPC will be used. 10 | - LOG_LEVEL: (Optional) Set the logging level. Default is 'INFO'. Options: DEBUG, INFO, WARNING, ERROR, CRITICAL. 11 | 12 | Usage: 13 | python examples/simple.py [--log-level LEVEL] 14 | 15 | Arguments: 16 | - network: The network to use (e.g., mainnet, goerli) 17 | - --log-level: (Optional) Set the logging level. Default is 'INFO'. 18 | 19 | Example: 20 | LOG_LEVEL=DEBUG python examples/simple.py mainnet --log-level DEBUG 21 | """ 22 | 23 | import argparse 24 | import logging 25 | import os 26 | import secrets 27 | from enum import Enum 28 | from uuid import uuid4 29 | 30 | from eth_account.account import Account 31 | from eth_account.signers.local import LocalAccount 32 | from web3 import HTTPProvider, Web3 33 | from web3.exceptions import TransactionNotFound 34 | from web3.types import TxParams 35 | 36 | from flashbots import FlashbotsWeb3, flashbot 37 | from flashbots.constants import FLASHBOTS_NETWORKS 38 | from flashbots.types import Network 39 | 40 | # Configure logging 41 | log_level = os.environ.get("LOG_LEVEL", "INFO").upper() 42 | logging.basicConfig( 43 | level=getattr(logging, log_level), 44 | format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", 45 | ) 46 | logger = logging.getLogger(__name__) 47 | 48 | 49 | class EnumAction(argparse.Action): 50 | def __init__(self, **kwargs): 51 | enum_type = kwargs.pop("type", None) 52 | if enum_type is None: 53 | raise ValueError("type must be assigned an Enum when using EnumAction") 54 | if not issubclass(enum_type, Enum): 55 | raise TypeError("type must be an Enum when using EnumAction") 56 | kwargs.setdefault("choices", tuple(e.value for e in enum_type)) 57 | super(EnumAction, self).__init__(**kwargs) 58 | self._enum = enum_type 59 | 60 | def __call__(self, parser, namespace, values, option_string=None): 61 | value = self._enum(values) 62 | setattr(namespace, self.dest, value) 63 | 64 | 65 | def parse_arguments() -> Network: 66 | parser = argparse.ArgumentParser(description="Flashbots simple example") 67 | parser.add_argument( 68 | "network", 69 | type=Network, 70 | action=EnumAction, 71 | help=f"The network to use ({', '.join(e.value for e in Network)})", 72 | ) 73 | parser.add_argument( 74 | "--log-level", 75 | type=str, 76 | choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], 77 | default="INFO", 78 | help="Set the logging level", 79 | ) 80 | args = parser.parse_args() 81 | return args.network 82 | 83 | 84 | def env(key: str) -> str: 85 | value = os.environ.get(key) 86 | if value is None: 87 | raise ValueError(f"Environment variable '{key}' is not set") 88 | return value 89 | 90 | 91 | def random_account() -> LocalAccount: 92 | key = "0x" + secrets.token_hex(32) 93 | return Account.from_key(key) 94 | 95 | 96 | def get_account_from_env(key: str) -> LocalAccount: 97 | return Account.from_key(env(key)) 98 | 99 | 100 | def setup_web3(network: Network) -> FlashbotsWeb3: 101 | provider_url = os.environ.get( 102 | "PROVIDER_URL", FLASHBOTS_NETWORKS[network]["provider_url"] 103 | ) 104 | logger.info(f"Using RPC: {provider_url}") 105 | relay_url = FLASHBOTS_NETWORKS[network]["relay_url"] 106 | w3 = flashbot( 107 | Web3(HTTPProvider(provider_url)), 108 | get_account_from_env("ETH_SIGNER_KEY"), 109 | relay_url, 110 | ) 111 | return w3 112 | 113 | 114 | def log_account_balances(w3: Web3, sender: str, receiver: str) -> None: 115 | logger.info( 116 | f"Sender account balance: {Web3.from_wei(w3.eth.get_balance(Web3.to_checksum_address(sender)), 'ether')} ETH" 117 | ) 118 | logger.info( 119 | f"Receiver account balance: {Web3.from_wei(w3.eth.get_balance(Web3.to_checksum_address(receiver)), 'ether')} ETH" 120 | ) 121 | 122 | 123 | def create_transaction( 124 | w3: Web3, sender: str, receiver: str, nonce: int, network: Network 125 | ) -> TxParams: 126 | # Get the latest gas price information 127 | latest = w3.eth.get_block("latest") 128 | base_fee = latest["baseFeePerGas"] 129 | 130 | # Set max priority fee (tip) to 2 Gwei 131 | max_priority_fee = Web3.to_wei(2, "gwei") 132 | 133 | # Set max fee to be base fee + priority fee 134 | max_fee = base_fee + max_priority_fee 135 | 136 | return { 137 | "from": sender, 138 | "to": receiver, 139 | "gas": 21000, 140 | "value": Web3.to_wei(0.001, "ether"), 141 | "nonce": nonce, 142 | "maxFeePerGas": max_fee, 143 | "maxPriorityFeePerGas": max_priority_fee, 144 | "chainId": FLASHBOTS_NETWORKS[network]["chain_id"], 145 | } 146 | 147 | 148 | def main() -> None: 149 | network = parse_arguments() 150 | sender = get_account_from_env("ETH_SENDER_KEY") 151 | receiver = Account.create().address 152 | w3 = setup_web3(network) 153 | 154 | logger.info(f"Sender address: {sender.address}") 155 | logger.info(f"Receiver address: {receiver}") 156 | log_account_balances(w3, sender.address, receiver) 157 | 158 | nonce = w3.eth.get_transaction_count(sender.address) 159 | tx1 = create_transaction(w3, sender.address, receiver, nonce, network) 160 | tx2 = create_transaction(w3, sender.address, receiver, nonce + 1, network) 161 | 162 | tx1_signed = w3.eth.account.sign_transaction(tx1, private_key=sender.key) 163 | bundle = [ 164 | {"signed_transaction": tx1_signed.rawTransaction}, 165 | {"transaction": tx2, "signer": sender}, 166 | ] 167 | 168 | # keep trying to send bundle until it gets mined 169 | while True: 170 | block = w3.eth.block_number 171 | 172 | # Simulation is only supported on mainnet 173 | if network == "mainnet": 174 | # Simulate bundle on current block. 175 | # If your RPC provider is not fast enough, you may get "block extrapolation negative" 176 | # error message triggered by "extrapolate_timestamp" function in "flashbots.py". 177 | try: 178 | w3.flashbots.simulate(bundle, block) 179 | except Exception as e: 180 | logger.error(f"Simulation error: {e}") 181 | return 182 | 183 | # send bundle targeting next block 184 | replacement_uuid = str(uuid4()) 185 | logger.info(f"replacementUuid {replacement_uuid}") 186 | send_result = w3.flashbots.send_bundle( 187 | bundle, 188 | target_block_number=block + 1, 189 | opts={"replacementUuid": replacement_uuid}, 190 | ) 191 | logger.info(f"bundleHash {w3.to_hex(send_result.bundle_hash())}") 192 | 193 | stats_v1 = w3.flashbots.get_bundle_stats( 194 | w3.to_hex(send_result.bundle_hash()), block 195 | ) 196 | logger.info(f"bundleStats v1 {stats_v1}") 197 | 198 | stats_v2 = w3.flashbots.get_bundle_stats_v2( 199 | w3.to_hex(send_result.bundle_hash()), block 200 | ) 201 | logger.info(f"bundleStats v2 {stats_v2}") 202 | 203 | send_result.wait() 204 | try: 205 | receipts = send_result.receipts() 206 | logger.info(f"Bundle was mined in block {receipts[0].blockNumber}") 207 | break 208 | except TransactionNotFound: 209 | logger.info(f"Bundle not found in block {block + 1}") 210 | cancel_res = w3.flashbots.cancel_bundles(replacement_uuid) 211 | logger.info(f"Canceled {cancel_res}") 212 | 213 | log_account_balances(w3, sender.address, receiver) 214 | 215 | 216 | if __name__ == "__main__": 217 | main() 218 | -------------------------------------------------------------------------------- /flashbots/__init__.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Union, cast 2 | 3 | from eth_account.signers.local import LocalAccount 4 | from eth_typing import URI 5 | from web3 import Web3 6 | from web3._utils.module import attach_modules 7 | 8 | from .flashbots import Flashbots 9 | from .middleware import construct_flashbots_middleware 10 | from .provider import FlashbotProvider 11 | 12 | DEFAULT_FLASHBOTS_RELAY = "https://relay.flashbots.net" 13 | 14 | 15 | class FlashbotsWeb3(Web3): 16 | flashbots: Flashbots 17 | 18 | 19 | def flashbot( 20 | w3: Web3, 21 | signature_account: LocalAccount, 22 | endpoint_uri: Optional[Union[URI, str]] = None, 23 | ) -> FlashbotsWeb3: 24 | """Inject the Flashbots module and middleware into a Web3 instance. 25 | 26 | This method enables sending bundles to various relays using "eth_sendBundle". 27 | 28 | Args: 29 | w3: The Web3 instance to modify. 30 | signature_account: The account used for signing transactions. 31 | endpoint_uri: The relay endpoint URI. Defaults to Flashbots relay. 32 | 33 | Returns: 34 | The modified Web3 instance with Flashbots functionality. 35 | 36 | Examples: 37 | Using default Flashbots relay: 38 | >>> flashbot(w3, signer) 39 | 40 | Using custom relay: 41 | >>> flashbot(w3, signer, CUSTOM_RELAY_URL) 42 | 43 | Available relay URLs: 44 | - Titan: 'https://rpc.titanbuilder.xyz/' 45 | - Beaver: 'https://rpc.beaverbuild.org/' 46 | - Rsync: 'https://rsync-builder.xyz/' 47 | - Flashbots: 'https://relay.flashbots.net' (default) 48 | """ 49 | 50 | flashbots_provider = FlashbotProvider(signature_account, endpoint_uri) 51 | flash_middleware = construct_flashbots_middleware(flashbots_provider) 52 | w3.middleware_onion.add(flash_middleware) 53 | 54 | # attach modules to add the new namespace commands 55 | attach_modules(w3, {"flashbots": (Flashbots,)}) 56 | 57 | return cast(FlashbotsWeb3, w3) 58 | -------------------------------------------------------------------------------- /flashbots/constants.py: -------------------------------------------------------------------------------- 1 | from typing import Dict 2 | 3 | from eth_typing import URI 4 | 5 | from .types import Network, NetworkConfig 6 | 7 | FLASHBOTS_NETWORKS: Dict[Network, NetworkConfig] = { 8 | Network.SEPOLIA: NetworkConfig( 9 | chain_id=11155111, 10 | provider_url=URI("https://rpc-sepolia.flashbots.net"), 11 | relay_url=URI("https://relay-sepolia.flashbots.net"), 12 | ), 13 | Network.HOLESKY: NetworkConfig( 14 | chain_id=17000, 15 | provider_url=URI("https://rpc-holesky.flashbots.net"), 16 | relay_url=URI("https://relay-holesky.flashbots.net"), 17 | ), 18 | Network.MAINNET: NetworkConfig( 19 | chain_id=1, 20 | provider_url=URI("https://rpc.flashbots.net"), 21 | relay_url=URI("https://relay.flashbots.net"), 22 | ), 23 | } 24 | -------------------------------------------------------------------------------- /flashbots/flashbots.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import time 3 | from functools import reduce 4 | from typing import Any, Callable, Dict, List, Optional, Union 5 | 6 | import rlp 7 | from eth_account import Account 8 | from eth_account._utils.legacy_transactions import ( 9 | Transaction, 10 | encode_transaction, 11 | serializable_unsigned_transaction_from_dict, 12 | ) 13 | from eth_account._utils.typed_transactions import ( 14 | AccessListTransaction, 15 | DynamicFeeTransaction, 16 | ) 17 | from eth_typing import HexStr 18 | from hexbytes import HexBytes 19 | from toolz import dissoc 20 | from web3 import Web3 21 | from web3.exceptions import TransactionNotFound 22 | from web3.method import Method 23 | from web3.module import Module 24 | from web3.types import Nonce, RPCEndpoint, TxParams 25 | 26 | from .types import ( 27 | FlashbotsBundleDictTx, 28 | FlashbotsBundleRawTx, 29 | FlashbotsBundleTx, 30 | FlashbotsOpts, 31 | SignedTxAndHash, 32 | TxReceipt, 33 | ) 34 | 35 | SECONDS_PER_BLOCK = 12 36 | 37 | 38 | class FlashbotsRPC: 39 | eth_sendBundle = RPCEndpoint("eth_sendBundle") 40 | eth_callBundle = RPCEndpoint("eth_callBundle") 41 | eth_cancelBundle = RPCEndpoint("eth_cancelBundle") 42 | eth_sendPrivateTransaction = RPCEndpoint("eth_sendPrivateTransaction") 43 | eth_cancelPrivateTransaction = RPCEndpoint("eth_cancelPrivateTransaction") 44 | flashbots_getBundleStats = RPCEndpoint("flashbots_getBundleStats") 45 | flashbots_getUserStats = RPCEndpoint("flashbots_getUserStats") 46 | flashbots_getBundleStatsV2 = RPCEndpoint("flashbots_getBundleStatsV2") 47 | flashbots_getUserStatsV2 = RPCEndpoint("flashbots_getUserStatsV2") 48 | 49 | 50 | class FlashbotsBundleResponse: 51 | w3: Web3 52 | bundle: List[SignedTxAndHash] 53 | target_block_number: int 54 | 55 | def __init__(self, w3: Web3, txs: List[HexBytes], target_block_number: int): 56 | self.w3 = w3 57 | 58 | def parse_tx(tx): 59 | return { 60 | "signed_transaction": tx, 61 | "hash": self.w3.keccak(tx), 62 | } 63 | 64 | self.bundle = list(map(parse_tx, txs)) 65 | self.target_block_number = target_block_number 66 | 67 | def wait(self) -> None: 68 | """Waits until the target block has been reached""" 69 | while self.w3.eth.block_number < self.target_block_number: 70 | time.sleep(1) 71 | 72 | def receipts(self) -> List[TxReceipt]: 73 | """Returns all the transaction receipts from the submitted bundle""" 74 | self.wait() 75 | return list( 76 | map(lambda tx: self.w3.eth.get_transaction_receipt(tx["hash"]), self.bundle) 77 | ) 78 | 79 | def bundle_hash(self) -> str: 80 | """Calculates bundle hash""" 81 | concat_hashes = reduce( 82 | lambda a, b: a + b, 83 | map(lambda tx: tx["hash"], self.bundle), 84 | ) 85 | return self.w3.keccak(concat_hashes) 86 | 87 | 88 | class FlashbotsPrivateTransactionResponse: 89 | w3: Web3 90 | tx: SignedTxAndHash 91 | max_block_number: int 92 | 93 | def __init__(self, w3: Web3, signed_tx: HexBytes, max_block_number: int): 94 | self.w3 = w3 95 | self.max_block_number = max_block_number 96 | self.tx = { 97 | "signed_transaction": signed_tx, 98 | "hash": self.w3.keccak(signed_tx), 99 | } 100 | 101 | def wait(self) -> bool: 102 | """Waits up to max block number, returns `True` if/when tx has been mined. 103 | 104 | If tx has not been mined by the time the current block > max_block_number, returns `False`. 105 | """ 106 | while True: 107 | try: 108 | self.w3.eth.get_transaction(self.tx["hash"]) 109 | return True 110 | except TransactionNotFound: 111 | if self.w3.eth.block_number > self.max_block_number: 112 | return False 113 | time.sleep(1) 114 | 115 | def receipt(self) -> Optional[TxReceipt]: 116 | """Gets private tx receipt if tx has been mined. If tx is not mined within `max_block_number` period, returns None.""" 117 | if self.wait(): 118 | return self.w3.eth.get_transaction_receipt(self.tx["hash"]) 119 | else: 120 | return None 121 | 122 | 123 | class Flashbots(Module): 124 | signed_txs: List[HexBytes] 125 | response: Union[FlashbotsBundleResponse, FlashbotsPrivateTransactionResponse] 126 | logger = logging.getLogger("flashbots") 127 | 128 | def sign_bundle( 129 | self, 130 | bundled_transactions: List[ 131 | Union[FlashbotsBundleTx, FlashbotsBundleRawTx, FlashbotsBundleDictTx] 132 | ], 133 | ) -> List[HexBytes]: 134 | """Given a bundle of signed and unsigned transactions, it signs them all""" 135 | nonces: Dict[HexStr, Nonce] = {} 136 | signed_transactions: List[HexBytes] = [] 137 | 138 | for tx in bundled_transactions: 139 | if "signed_transaction" in tx: # FlashbotsBundleRawTx 140 | tx_params = _parse_signed_tx(tx["signed_transaction"]) 141 | nonces[tx_params["from"]] = tx_params["nonce"] + 1 142 | signed_transactions.append(tx["signed_transaction"]) 143 | 144 | elif "signer" in tx: # FlashbotsBundleTx 145 | signer, tx = tx["signer"], tx["transaction"] 146 | tx["from"] = signer.address 147 | 148 | if tx.get("nonce") is None: 149 | tx["nonce"] = nonces.get( 150 | signer.address, 151 | self.w3.eth.get_transaction_count(signer.address), 152 | ) 153 | nonces[signer.address] = tx["nonce"] + 1 154 | 155 | if "gas" not in tx: 156 | tx["gas"] = self.w3.eth.estimate_gas(tx) 157 | 158 | signed_tx = signer.sign_transaction(tx) 159 | signed_transactions.append(signed_tx.rawTransaction) 160 | 161 | elif all(key in tx for key in ["v", "r", "s"]): # FlashbotsBundleDictTx 162 | v, r, s = ( 163 | tx["v"], 164 | int(tx["r"].hex(), base=16), 165 | int(tx["s"].hex(), base=16), 166 | ) 167 | 168 | tx_dict = { 169 | "nonce": tx["nonce"], 170 | "data": HexBytes(tx["input"]), 171 | "value": tx["value"], 172 | "gas": tx["gas"], 173 | } 174 | 175 | if "maxFeePerGas" in tx or "maxPriorityFeePerGas" in tx: 176 | assert "maxFeePerGas" in tx and "maxPriorityFeePerGas" in tx 177 | tx_dict["maxFeePerGas"], tx_dict["maxPriorityFeePerGas"] = ( 178 | tx["maxFeePerGas"], 179 | tx["maxPriorityFeePerGas"], 180 | ) 181 | else: 182 | assert "gasPrice" in tx 183 | tx_dict["gasPrice"] = tx["gasPrice"] 184 | 185 | if tx.get("accessList"): 186 | tx_dict["accessList"] = tx["accessList"] 187 | 188 | if tx.get("chainId"): 189 | tx_dict["chainId"] = tx["chainId"] 190 | 191 | if tx.get("to"): 192 | tx_dict["to"] = HexBytes(tx["to"]) 193 | 194 | unsigned_tx = serializable_unsigned_transaction_from_dict(tx_dict) 195 | raw = encode_transaction(unsigned_tx, vrs=(v, r, s)) 196 | assert self.w3.keccak(raw) == tx["hash"] 197 | signed_transactions.append(raw) 198 | 199 | return signed_transactions 200 | 201 | def to_hex(self, signed_transaction: bytes) -> str: 202 | tx_hex = signed_transaction.hex() 203 | if tx_hex[0:2] != "0x": 204 | tx_hex = f"0x{tx_hex}" 205 | return tx_hex 206 | 207 | def send_raw_bundle_munger( 208 | self, 209 | signed_bundled_transactions: List[HexBytes], 210 | target_block_number: int, 211 | opts: Optional[FlashbotsOpts] = None, 212 | ) -> List[Any]: 213 | """Given a raw signed bundle, it packages it up with the block number and the timestamps""" 214 | 215 | if opts is None: 216 | opts = {} 217 | 218 | # convert to hex 219 | return [ 220 | { 221 | "txs": list(map(lambda x: self.to_hex(x), signed_bundled_transactions)), 222 | "blockNumber": hex(target_block_number), 223 | "minTimestamp": opts["minTimestamp"] if "minTimestamp" in opts else 0, 224 | "maxTimestamp": opts["maxTimestamp"] if "maxTimestamp" in opts else 0, 225 | "revertingTxHashes": ( 226 | opts["revertingTxHashes"] if "revertingTxHashes" in opts else [] 227 | ), 228 | "replacementUuid": ( 229 | opts["replacementUuid"] if "replacementUuid" in opts else None 230 | ), 231 | } 232 | ] 233 | 234 | sendRawBundle: Method[Callable[[Any], Any]] = Method( 235 | FlashbotsRPC.eth_sendBundle, mungers=[send_raw_bundle_munger] 236 | ) 237 | send_raw_bundle = sendRawBundle 238 | 239 | def send_bundle_munger( 240 | self, 241 | bundled_transactions: List[Union[FlashbotsBundleTx, FlashbotsBundleRawTx]], 242 | target_block_number: int, 243 | opts: Optional[FlashbotsOpts] = None, 244 | ) -> List[Any]: 245 | signed_txs = self.sign_bundle(bundled_transactions) 246 | self.response = FlashbotsBundleResponse( 247 | self.w3, signed_txs, target_block_number 248 | ) 249 | self.logger.info(f"Sending bundle targeting block {target_block_number}") 250 | return self.send_raw_bundle_munger(signed_txs, target_block_number, opts) 251 | 252 | def raw_bundle_formatter(self, resp) -> Any: 253 | return lambda _: resp.response 254 | 255 | sendBundle: Method[Callable[[Any], Any]] = Method( 256 | FlashbotsRPC.eth_sendBundle, 257 | mungers=[send_bundle_munger], 258 | result_formatters=raw_bundle_formatter, 259 | ) 260 | 261 | send_bundle = sendBundle 262 | 263 | def cancel_bundles_munger( 264 | self, 265 | replacement_uuid: str, 266 | ) -> List[Any]: 267 | return [ 268 | { 269 | "replacementUuid": replacement_uuid, 270 | } 271 | ] 272 | 273 | def cancel_bundle_formatter(self, resp) -> Any: 274 | return lambda res: {"bundleHashes": res} 275 | 276 | cancelBundles: Method[Callable[[Any], Any]] = Method( 277 | FlashbotsRPC.eth_cancelBundle, 278 | mungers=[cancel_bundles_munger], 279 | result_formatters=cancel_bundle_formatter, 280 | ) 281 | cancel_bundles = cancelBundles 282 | 283 | def simulate( 284 | self, 285 | bundled_transactions: List[Union[FlashbotsBundleTx, FlashbotsBundleRawTx]], 286 | block_tag: Union[int, str] = None, 287 | state_block_tag: int = None, 288 | block_timestamp: int = None, 289 | ): 290 | # interpret block number from tag 291 | block_number = ( 292 | self.w3.eth.block_number 293 | if block_tag is None or block_tag == "latest" 294 | else block_tag 295 | ) 296 | 297 | # sets evm params 298 | evm_block_number = self.w3.to_hex(block_number) 299 | evm_block_state_number = ( 300 | self.w3.to_hex(state_block_tag) 301 | if state_block_tag is not None 302 | else self.w3.to_hex(block_number - 1) 303 | ) 304 | evm_timestamp = ( 305 | block_timestamp 306 | if block_timestamp is not None 307 | else self.extrapolate_timestamp(block_number, self.w3.eth.block_number) 308 | ) 309 | 310 | signed_bundled_transactions = self.sign_bundle(bundled_transactions) 311 | # calls evm simulator 312 | self.logger.info(f"Simulating bundle on block {block_number}") 313 | call_result = self.call_bundle( 314 | signed_bundled_transactions, 315 | evm_block_number, 316 | evm_block_state_number, 317 | evm_timestamp, 318 | ) 319 | 320 | return { 321 | "bundleHash": call_result["bundleHash"], 322 | "coinbaseDiff": call_result["coinbaseDiff"], 323 | "results": call_result["results"], 324 | "signedBundledTransactions": signed_bundled_transactions, 325 | "totalGasUsed": reduce( 326 | lambda a, b: a + b["gasUsed"], call_result["results"], 0 327 | ), 328 | } 329 | 330 | def extrapolate_timestamp(self, block_tag: int, latest_block_number: int): 331 | block_delta = block_tag - latest_block_number 332 | if block_delta < 0: 333 | raise Exception("block extrapolation negative") 334 | return self.w3.eth.get_block(latest_block_number)["timestamp"] + ( 335 | block_delta * SECONDS_PER_BLOCK 336 | ) 337 | 338 | def call_bundle_munger( 339 | self, 340 | signed_bundled_transactions: List[ 341 | Union[FlashbotsBundleTx, FlashbotsBundleRawTx] 342 | ], 343 | evm_block_number, 344 | evm_block_state_number, 345 | evm_timestamp, 346 | opts: Optional[FlashbotsOpts] = None, 347 | ) -> Any: 348 | """Given a raw signed bundle, it packages it up with the block number and the timestamps""" 349 | inpt = [ 350 | { 351 | "txs": list(map(lambda x: x.hex(), signed_bundled_transactions)), 352 | "blockNumber": evm_block_number, 353 | "stateBlockNumber": evm_block_state_number, 354 | "timestamp": evm_timestamp, 355 | } 356 | ] 357 | return inpt 358 | 359 | call_bundle: Method[Callable[[Any], Any]] = Method( 360 | json_rpc_method=FlashbotsRPC.eth_callBundle, mungers=[call_bundle_munger] 361 | ) 362 | 363 | def get_user_stats_munger(self) -> List: 364 | return [{"blockNumber": hex(self.w3.eth.block_number)}] 365 | 366 | getUserStats: Method[Callable[[Any], Any]] = Method( 367 | json_rpc_method=FlashbotsRPC.flashbots_getUserStats, 368 | mungers=[get_user_stats_munger], 369 | ) 370 | get_user_stats = getUserStats 371 | 372 | getUserStatsV2: Method[Callable[[Any], Any]] = Method( 373 | json_rpc_method=FlashbotsRPC.flashbots_getUserStatsV2, 374 | mungers=[get_user_stats_munger], 375 | ) 376 | get_user_stats_v2 = getUserStatsV2 377 | 378 | def get_bundle_stats_munger( 379 | self, bundle_hash: Union[str, int], block_number: Union[str, int] 380 | ) -> List: 381 | if isinstance(bundle_hash, int): 382 | bundle_hash = hex(bundle_hash) 383 | if isinstance(block_number, int): 384 | block_number = hex(block_number) 385 | return [{"bundleHash": bundle_hash, "blockNumber": block_number}] 386 | 387 | getBundleStats: Method[Callable[[Any], Any]] = Method( 388 | json_rpc_method=FlashbotsRPC.flashbots_getBundleStats, 389 | mungers=[get_bundle_stats_munger], 390 | ) 391 | get_bundle_stats = getBundleStats 392 | 393 | getBundleStatsV2: Method[Callable[[Any], Any]] = Method( 394 | json_rpc_method=FlashbotsRPC.flashbots_getBundleStatsV2, 395 | mungers=[get_bundle_stats_munger], 396 | ) 397 | get_bundle_stats_v2 = getBundleStatsV2 398 | 399 | # sends private transaction 400 | # returns tx hash 401 | def send_private_transaction_munger( 402 | self, 403 | transaction: Union[FlashbotsBundleTx, FlashbotsBundleRawTx], 404 | max_block_number: Optional[int] = None, 405 | ) -> Any: 406 | """Sends a single transaction to Flashbots. 407 | 408 | If `max_block_number` is set, Flashbots will try to submit the transaction in every block <= that block (max 25 blocks from present). 409 | """ 410 | signed_transaction: str 411 | if "signed_transaction" in transaction: 412 | signed_transaction = transaction["signed_transaction"] 413 | else: 414 | signed_transaction = ( 415 | transaction["signer"] 416 | .sign_transaction(transaction["transaction"]) 417 | .rawTransaction 418 | ) 419 | if max_block_number is None: 420 | # get current block num, add 25 421 | current_block = self.w3.eth.block_number 422 | max_block_number = current_block + 25 423 | params = { 424 | "tx": self.to_hex(signed_transaction), 425 | "maxBlockNumber": max_block_number, 426 | } 427 | self.response = FlashbotsPrivateTransactionResponse( 428 | self.w3, signed_transaction, max_block_number 429 | ) 430 | self.logger.info( 431 | f"Sending private transaction with max block number {max_block_number}" 432 | ) 433 | return [params] 434 | 435 | sendPrivateTransaction: Method[Callable[[Any], Any]] = Method( 436 | json_rpc_method=FlashbotsRPC.eth_sendPrivateTransaction, 437 | mungers=[send_private_transaction_munger], 438 | result_formatters=raw_bundle_formatter, 439 | ) 440 | send_private_transaction = sendPrivateTransaction 441 | 442 | # cancels private tx given pending private tx hash 443 | # returns True if successful, False otherwise 444 | def cancel_private_transaction_munger( 445 | self, 446 | tx_hash: str, 447 | ) -> bool: 448 | """Stops a private transaction from being sent to miners by Flashbots. 449 | 450 | Note: if a transaction has already been received by a miner, it may still be mined. This simply stops further submissions. 451 | """ 452 | params = { 453 | "txHash": tx_hash, 454 | } 455 | self.logger.info(f"Cancelling private transaction with hash {tx_hash}") 456 | return [params] 457 | 458 | cancelPrivateTransaction: Method[Callable[[Any], Any]] = Method( 459 | json_rpc_method=FlashbotsRPC.eth_cancelPrivateTransaction, 460 | mungers=[cancel_private_transaction_munger], 461 | ) 462 | cancel_private_transaction = cancelPrivateTransaction 463 | 464 | 465 | def _parse_signed_tx(signed_tx: HexBytes) -> TxParams: 466 | # decode tx params based on its type 467 | tx_type = signed_tx[0] 468 | if tx_type > int("0x7f", 16): 469 | # legacy and EIP-155 transactions 470 | decoded_tx = rlp.decode(signed_tx, Transaction).as_dict() 471 | else: 472 | # typed transactions (EIP-2718) 473 | if tx_type == 1: 474 | # EIP-2930 475 | sedes = AccessListTransaction._signed_transaction_serializer 476 | elif tx_type == 2: 477 | # EIP-1559 478 | sedes = DynamicFeeTransaction._signed_transaction_serializer 479 | else: 480 | raise ValueError(f"Unknown transaction type: {tx_type}.") 481 | decoded_tx = rlp.decode(signed_tx[1:], sedes).as_dict() 482 | 483 | # recover sender address and remove signature fields 484 | decoded_tx["from"] = Account.recover_transaction(signed_tx) 485 | decoded_tx = dissoc(decoded_tx, "v", "r", "s") 486 | return decoded_tx 487 | -------------------------------------------------------------------------------- /flashbots/middleware.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Callable 2 | 3 | from web3 import Web3 4 | from web3.middleware import Middleware 5 | from web3.types import RPCEndpoint, RPCResponse 6 | 7 | from .provider import FlashbotProvider 8 | 9 | FLASHBOTS_METHODS = [ 10 | "eth_sendBundle", 11 | "eth_callBundle", 12 | "eth_cancelBundle", 13 | "eth_sendPrivateTransaction", 14 | "eth_cancelPrivateTransaction", 15 | "flashbots_getBundleStats", 16 | "flashbots_getUserStats", 17 | "flashbots_getBundleStatsV2", 18 | "flashbots_getUserStatsV2", 19 | ] 20 | 21 | 22 | def construct_flashbots_middleware( 23 | flashbots_provider: FlashbotProvider, 24 | ) -> Middleware: 25 | """Captures Flashbots RPC requests and sends them to the Flashbots endpoint 26 | while also injecting the required authorization headers 27 | 28 | Keyword arguments: 29 | flashbots_provider -- An HTTP provider instantiated with any authorization headers 30 | required 31 | """ 32 | 33 | def flashbots_middleware( 34 | make_request: Callable[[RPCEndpoint, Any], Any], w3: Web3 35 | ) -> Callable[[RPCEndpoint, Any], RPCResponse]: 36 | def middleware(method: RPCEndpoint, params: Any) -> RPCResponse: 37 | if method not in FLASHBOTS_METHODS: 38 | return make_request(method, params) 39 | else: 40 | # otherwise intercept it and POST it 41 | return flashbots_provider.make_request(method, params) 42 | 43 | return middleware 44 | 45 | return flashbots_middleware 46 | -------------------------------------------------------------------------------- /flashbots/provider.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | from typing import Any, Dict, Optional, Union 4 | 5 | from eth_account import Account, messages 6 | from eth_account.signers.local import LocalAccount 7 | from eth_typing import URI 8 | from web3 import HTTPProvider, Web3 9 | from web3._utils.request import make_post_request 10 | from web3.types import RPCEndpoint, RPCResponse 11 | 12 | 13 | def get_default_endpoint() -> URI: 14 | return URI( 15 | os.environ.get("FLASHBOTS_HTTP_PROVIDER_URI", "https://relay.flashbots.net") 16 | ) 17 | 18 | 19 | class FlashbotProvider(HTTPProvider): 20 | """ 21 | A custom HTTP provider for submitting transactions to Flashbots. 22 | 23 | This provider extends the standard Web3 HTTPProvider specifically to add the 24 | required 'X-Flashbots-Signature' header for Flashbots request authentication. 25 | 26 | Key features: 27 | - Automatically signs and includes the Flashbots-specific header with each request. 28 | - Uses a designated account for signing Flashbots messages. 29 | - Maintains compatibility with standard Web3 provider interface. 30 | 31 | :param signature_account: LocalAccount used for signing Flashbots messages. 32 | :param endpoint_uri: URI of the Flashbots endpoint. Defaults to the standard Flashbots relay. 33 | :param request_kwargs: Additional keyword arguments for requests. 34 | :param session: Session object to use for requests. 35 | 36 | Usage: 37 | flashbots_provider = FlashbotProvider(signature_account) 38 | w3 = Web3(flashbots_provider) 39 | """ 40 | 41 | logger = logging.getLogger("web3.providers.FlashbotProvider") 42 | 43 | def __init__( 44 | self, 45 | signature_account: LocalAccount, 46 | endpoint_uri: Optional[Union[URI, str]] = None, 47 | request_kwargs: Optional[Dict[str, Any]] = None, 48 | session: Optional[Any] = None, 49 | ): 50 | """ 51 | Initialize the FlashbotProvider. 52 | 53 | :param signature_account: The account used for signing messages. 54 | :param endpoint_uri: The URI of the Flashbots endpoint. 55 | :param request_kwargs: Additional keyword arguments for requests. 56 | :param session: The session object to use for requests. 57 | """ 58 | _endpoint_uri = endpoint_uri or get_default_endpoint() 59 | super().__init__(_endpoint_uri, request_kwargs, session) 60 | self.signature_account = signature_account 61 | 62 | def _get_flashbots_headers(self, request_data: bytes) -> Dict[str, str]: 63 | message = messages.encode_defunct( 64 | text=Web3.keccak(text=request_data.decode("utf-8")).hex() 65 | ) 66 | signed_message = Account.sign_message( 67 | message, private_key=self.signature_account._private_key 68 | ) 69 | return { 70 | "X-Flashbots-Signature": f"{self.signature_account.address}:{signed_message.signature.hex()}" 71 | } 72 | 73 | def make_request(self, method: RPCEndpoint, params: Any) -> RPCResponse: 74 | """ 75 | Make a request to the Flashbots endpoint. 76 | 77 | :param method: The RPC method to call. 78 | :param params: The parameters for the RPC method. 79 | :return: The RPC response. 80 | """ 81 | self.logger.debug( 82 | f"Making request HTTP. URI: {self.endpoint_uri}, Method: {method}" 83 | ) 84 | request_data = self.encode_rpc_request(method, params) 85 | 86 | raw_response = make_post_request( 87 | self.endpoint_uri, 88 | request_data, 89 | headers=self.get_request_headers() 90 | | self._get_flashbots_headers(request_data), 91 | ) 92 | response = self.decode_rpc_response(raw_response) 93 | self.logger.debug( 94 | f"Getting response HTTP. URI: {self.endpoint_uri}, Method: {method}, Response: {response}" 95 | ) 96 | return response 97 | -------------------------------------------------------------------------------- /flashbots/types.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from typing import List, Optional, TypedDict, Union 3 | 4 | from eth_account.signers.local import LocalAccount 5 | from eth_typing import URI, HexStr 6 | from hexbytes import HexBytes 7 | from web3.types import TxParams, _Hash32 8 | 9 | # unsigned transaction 10 | FlashbotsBundleTx = TypedDict( 11 | "FlashbotsBundleTx", 12 | { 13 | "transaction": TxParams, 14 | "signer": LocalAccount, 15 | }, 16 | ) 17 | 18 | # signed transaction 19 | FlashbotsBundleRawTx = TypedDict( 20 | "FlashbotsBundleRawTx", 21 | { 22 | "signed_transaction": HexBytes, 23 | }, 24 | ) 25 | 26 | # transaction dict taken from w3.eth.get_block('pending', full_transactions=True) 27 | FlashbotsBundleDictTx = TypedDict( 28 | "FlashbotsBundleDictTx", 29 | { 30 | "accessList": list, 31 | "blockHash": HexBytes, 32 | "blockNumber": int, 33 | "chainId": str, 34 | "from": str, 35 | "gas": int, 36 | "gasPrice": int, 37 | "maxFeePerGas": int, 38 | "maxPriorityFeePerGas": int, 39 | "hash": HexBytes, 40 | "input": str, 41 | "nonce": int, 42 | "r": HexBytes, 43 | "s": HexBytes, 44 | "to": str, 45 | "transactionIndex": int, 46 | "type": str, 47 | "v": int, 48 | "value": int, 49 | }, 50 | total=False, 51 | ) 52 | 53 | FlashbotsOpts = TypedDict( 54 | "FlashbotsOpts", 55 | { 56 | "minTimestamp": Optional[int], 57 | "maxTimestamp": Optional[int], 58 | "revertingTxHashes": Optional[List[str]], 59 | "replacementUuid": Optional[str], 60 | }, 61 | ) 62 | 63 | 64 | # Type missing from eth_account, not really a part of flashbots web3 per sé 65 | SignTx = TypedDict( 66 | "SignTx", 67 | { 68 | "nonce": int, 69 | "chainId": int, 70 | "to": str, 71 | "data": str, 72 | "value": int, 73 | "gas": int, 74 | "gasPrice": int, 75 | }, 76 | total=False, 77 | ) 78 | 79 | # type alias 80 | TxReceipt = Union[_Hash32, HexBytes, HexStr] 81 | 82 | # response from bundle or private tx submission 83 | SignedTxAndHash = TypedDict( 84 | "SignedTxAndHash", 85 | { 86 | "signed_transaction": str, 87 | "hash": HexBytes, 88 | }, 89 | ) 90 | 91 | 92 | class Network(Enum): 93 | SEPOLIA = "sepolia" 94 | HOLESKY = "holesky" 95 | MAINNET = "mainnet" 96 | 97 | 98 | class NetworkConfig(TypedDict): 99 | chain_id: int 100 | provider_url: URI 101 | relay_url: URI 102 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | authors = [ 3 | "Georgios Konstantopoulos ", 4 | "Nikolas Papaioannou ", 5 | "Brock Smedley ", 6 | ] 7 | description = "" 8 | name = "flashbots" 9 | readme = "README.md" 10 | version = "2.0.0" 11 | 12 | [tool.poetry.dependencies] 13 | python = "^3.9" 14 | web3 = ">=6, <7" 15 | 16 | [tool.poetry.dev-dependencies] 17 | ruff = "^0.5.4" 18 | pytest-recording = "^0.11.0" 19 | vcrpy = "^4.1.1" 20 | 21 | [tool.ruff] 22 | target-version = "py312" 23 | 24 | [build-system] 25 | build-backend = "poetry.core.masonry.api" 26 | requires = ["poetry-core>=1.0.0"] 27 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # make sure this is running in a poetry shell 4 | if [ -z "$VIRTUAL_ENV" ]; then 5 | echo "This script should be run from a poetry shell. Run 'poetry shell' and try again." 6 | echo " 7 | Alternatively, if you don't want to use poetry and know that you have python and dependencies configured correctly, 8 | set VIRTUAL_ENV in your shell to override this check." 9 | exit 1 10 | fi 11 | 12 | # ensure twine and wheel are installed 13 | echo "Checking build requirements..." 14 | pip install twine wheel 1>/dev/null 15 | 16 | # build the package 17 | python setup.py sdist bdist_wheel 18 | if [ $? -ne 0 ]; then 19 | echo "Failed to build package" 20 | exit 1 21 | fi 22 | echo "*********************************************************************" 23 | echo "Build successful." 24 | 25 | # pick last two files in dist/ in case there are previous builds present 26 | files=(dist/*) 27 | files=("${files[@]: -2}") 28 | echo "Prepared files for upload:" 29 | for file in "${files[@]}"; do 30 | echo -e " - $file" 31 | done 32 | 33 | # parse the version number from the first element of files 34 | libname="flashbots" 35 | version=$(echo "$files" | grep -oP "$libname-\d+\.\d+\.\d+") 36 | version=${version#"$libname-"} 37 | 38 | # draw some lines to alert the user to their one last chance to exit 39 | for i in $(seq 1 69); do 40 | printf '*' 41 | sleep 0.013 42 | done; echo 43 | 44 | echo "This is the point of no return." 45 | echo -e " package:\t$libname" 46 | echo -e " version:\t$version" 47 | echo "Press Enter to upload the package to PyPI." 48 | read -rs dummy 49 | 50 | # upload the package 51 | twine upload "${files[@]}" 52 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from setuptools import setup 3 | 4 | packages = ["flashbots"] 5 | 6 | package_data = {"": ["*"]} 7 | 8 | install_requires = ["web3>=6,<7"] 9 | 10 | setup_kwargs = { 11 | "name": "flashbots", 12 | "version": "2.0.0", 13 | "description": "web3-flashbots.py", 14 | "long_description": 'This library works by injecting flashbots as a new module in the Web3.py instance, which allows submitting "bundles" of transactions directly to miners. This is done by also creating a middleware which captures calls to `eth_sendBundle` and `eth_callBundle`, and sends them to an RPC endpoint which you have specified, which corresponds to `mev-geth`.\n\nTo apply correct headers we use the `flashbot` method which injects the correct header on POST.\n\n## Quickstart\n\n```python\nfrom eth_account.signers.local import LocalAccount\nfrom web3 import Web3, HTTPProvider\nfrom flashbots import flashbot\nfrom eth_account.account import Account\nimport os\n\nETH_ACCOUNT_SIGNATURE: LocalAccount = Account.from_key(os.environ.get("ETH_SIGNATURE_KEY"))\n\n\nw3 = Web3(HTTPProvider("http://localhost:8545"))\nflashbot(w3, ETH_ACCOUNT_SIGNATURE)\n```\n\nNow the `w3.flashbots.sendBundle` method should be available to you. Look in [examples/simple.py](./examples/simple.py) for usage examples.\n\n### Testnet\n\nTo use an ethereum testnet, add the appropriate testnet relay RPC to the `flashbot` function arguments.\n\n```python\nflashbot(w3, ETH_ACCOUNT_SIGNATURE, "https://relay-holesky.flashbots.net")\n```\nCheck [flashbots docs](https://docs.flashbots.net/flashbots-auction/advanced/testnets#bundle-relay-urls) for up-to-date URLs.\n\n## Development and testing\n\nInstall [poetry](https://python-poetry.org/)\n\nPoetry will automatically fix your venv and all packages needed.\n\n```sh\npoetry install\n```\n\nTips: PyCharm has a poetry plugin\n\n## Simple Testnet Example\n\nSee [examples/simple.py](./examples/simple.py) for environment variable definitions.\n\n```sh\npoetry shell\nETH_SENDER_KEY= \\nPROVIDER_URL=https://eth-holesky.alchemyapi.io/v2/ \\nETH_SIGNER_KEY= \\npython examples/simple.py\n```\n\n## Linting\n\nIt\'s advisable to run black with default rules for linting\n\n```sh\nsudo pip install black # Black should be installed with a global entrypoint\nblack .\n```', 15 | "long_description_content_type": "text/markdown", 16 | "author": "Georgios Konstantopoulos", 17 | "author_email": "me@gakonst.com", 18 | "maintainer": "zeroXbrock", 19 | "maintainer_email": "brock@flashbots.net", 20 | "url": "https://github.com/flashbots/web3-flashbots", 21 | "packages": packages, 22 | "package_data": package_data, 23 | "install_requires": install_requires, 24 | "python_requires": ">=3.9,<4.0", 25 | } 26 | 27 | 28 | setup(**setup_kwargs) 29 | --------------------------------------------------------------------------------