Skip to content

Commit 7892c87

Browse files
authored
Web3.py V6 upgrade (#96)
* Add logging to Flashbots module - Add logger to Flashbots class - Include log messages for key operations: - Sending bundle - Simulating bundle - Sending private transaction - Cancelling private transaction - Use proper logging instead of print in example.py * refactor simple.py * Refactor FlashbotProvider for improved clarity and efficiency - Streamline header combination in make_request method - Update docstring to accurately reflect provider's purpose - Improve overall code readability * Switch from Black to Ruff for code formatting and linting - Remove Black configuration and dependencies - Add Ruff configuration and dependencies - Update VSCode settings for Python to use Ruff - Add pre-commit config for Ruff - Replace GitHub Actions workflow for Black with Ruff - Update pyproject.toml to configure Ruff * Refactor: Format and lint code with ruff - Reorder and optimize imports across multiple files - Remove unused imports - Standardize import order and grouping - Minor code formatting adjustments - Remove trailing whitespace This commit improves code consistency and readability by applying ruff's linting and formatting rules to the project. * Refactor flashbot function and improve type safety - Introduce FlashbotsWeb3 class to enhance type checking - Remove Goerli-specific PoA middleware injection - Update flashbot function to return FlashbotsWeb3 instance - Improve error handling for environment variables in examples - Enhance address handling with Web3.to_checksum_address - Update transaction nonce type to use web3.types.Nonce - Minor code style improvements and type annotations * Refactor network configuration and setup - Add flashbots/constants.py with FLASHBOTS_NETWORKS - Update flashbots/types.py to include new network types - Modify setup_web3 function to use the new network configuration - Remove NETWORK_CONFIG and related functions from examples/simple.py * Improve transaction creation and gas price handling - Update create_transaction function to dynamically calculate gas prices - Modify transaction creation in main function * Enhance simple.py example and improve Flashbots module - Add command-line arguments for network selection and log level - Implement dynamic logging configuration - Update docstrings with usage instructions and examples - Refactor network type to 'Network' for consistency - Add get_networks() function to retrieve available networks - Improve flashbot() function documentation - Minor code cleanup and formatting improvements * refactor: improve network handling and type safety - Convert Network type to Enum for better type safety - Update FLASHBOTS_NETWORKS to use Network enum as keys - Remove get_networks() function, use Network enum values directly - Add EnumAction class for argparse to handle Network enum - Update parse_arguments() and related functions to use Network enum - Adjust type hints throughout the code to reflect these changes
1 parent f0062a5 commit 7892c87

13 files changed

Lines changed: 518 additions & 469 deletions

File tree

.github/workflows/main.yml

Lines changed: 0 additions & 28 deletions
This file was deleted.

.github/workflows/ruff.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
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

.pre-commit-config.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
{
2-
"editor.formatOnSave": true,
32
"[python]": {
4-
"editor.defaultFormatter": "ms-python.black-formatter"
3+
"editor.formatOnSave": true,
4+
"editor.codeActionsOnSave": {
5+
"source.fixAll": "explicit",
6+
"source.organizeImports": "explicit"
7+
},
8+
"editor.defaultFormatter": "charliermarsh.ruff"
59
}
610
}

examples/simple.py

Lines changed: 134 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,29 @@
22
Minimal viable example of flashbots usage with dynamic fee transactions.
33
Sends a bundle of two transactions which transfer some ETH into a random account.
44
5-
"eth_sendBundle" is a generic method that can be used to send a bundle to any relay.
6-
For instance, you can use the following relay URLs:
7-
titan: 'https://rpc.titanbuilder.xyz/'
8-
beaver: 'https://rpc.beaverbuild.org/'
9-
builder69: 'https://builder0x69.io/'
10-
rsync: 'https://rsync-builder.xyz/'
11-
flashbots: 'https://relay.flashbots.net'
12-
13-
You can simply replace the URL in the flashbot method to use a different relay like:
14-
flashbot(w3, signer, YOUR_CHOSEN_RELAY_URL)
15-
165
Environment Variables:
176
- ETH_SENDER_KEY: Private key of account which will send the ETH.
18-
- ETH_SIGNER_KEY: Private key of account which will sign the bundle.
7+
- ETH_SIGNER_KEY: Private key of account which will sign the bundle.
198
- This account is only used for reputation on flashbots and should be empty.
209
- 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 <network> [--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
2121
"""
2222

23+
import argparse
24+
import logging
2325
import os
2426
import secrets
27+
from enum import Enum
2528
from uuid import uuid4
2629

2730
from eth_account.account import Account
@@ -30,163 +33,184 @@
3033
from web3.exceptions import TransactionNotFound
3134
from web3.types import TxParams
3235

33-
from flashbots import flashbot
34-
35-
# Define the network to use
36-
NETWORK = "holesky" # Options: "sepolia", "holesky", "mainnet"
37-
38-
# Define chain IDs and Flashbots Protect RPC URLs
39-
NETWORK_CONFIG = {
40-
"sepolia": {
41-
"chain_id": 11155111,
42-
"provider_url": "https://rpc-sepolia.flashbots.net",
43-
"relay_url": "https://relay-sepolia.flashbots.net",
44-
},
45-
"holesky": {
46-
"chain_id": 17000,
47-
"provider_url": "https://rpc-holesky.flashbots.net",
48-
"relay_url": "https://relay-holesky.flashbots.net",
49-
},
50-
"mainnet": {
51-
"chain_id": 1,
52-
"provider_url": "https://rpc.flashbots.net",
53-
"relay_url": None, # Mainnet uses default Flashbots relay
54-
},
55-
}
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
5682

5783

5884
def env(key: str) -> str:
59-
return os.environ.get(key)
85+
value = os.environ.get(key)
86+
if value is None:
87+
raise ValueError(f"Environment variable '{key}' is not set")
88+
return value
6089

6190

6291
def random_account() -> LocalAccount:
6392
key = "0x" + secrets.token_hex(32)
6493
return Account.from_key(key)
6594

6695

67-
def main() -> None:
68-
# account to send the transfer and sign transactions
69-
sender: LocalAccount = Account.from_key(env("ETH_SENDER_KEY"))
70-
# account to receive the transfer
71-
receiverAddress: str = random_account().address
72-
# account to sign bundles & establish flashbots reputation
73-
# NOTE: this account should not store funds
74-
signer: LocalAccount = Account.from_key(env("ETH_SIGNER_KEY"))
75-
76-
# Use user-provided RPC URL if available, otherwise use Flashbots Protect RPC
77-
user_provider_url = env("PROVIDER_URL")
78-
if user_provider_url:
79-
provider_url = user_provider_url
80-
print(f"Using user-provided RPC: {provider_url}")
81-
else:
82-
provider_url = NETWORK_CONFIG[NETWORK]["provider_url"]
83-
print(f"Using Flashbots Protect RPC: {provider_url}")
84-
85-
w3 = Web3(HTTPProvider(provider_url))
86-
87-
relay_url = NETWORK_CONFIG[NETWORK]["relay_url"]
88-
if relay_url:
89-
flashbot(w3, signer, relay_url)
90-
else:
91-
flashbot(w3, signer)
92-
93-
print(f"Sender address: {sender.address}")
94-
print(f"Receiver address: {receiverAddress}")
95-
print(
96-
f"Sender account balance: {Web3.from_wei(w3.eth.get_balance(sender.address), 'ether')} ETH"
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"]
97103
)
98-
print(
99-
f"Receiver account balance: {Web3.from_wei(w3.eth.get_balance(receiverAddress), 'ether')} ETH"
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,
100110
)
111+
return w3
101112

102-
# bundle two EIP-1559 (type 2) transactions, pre-sign one of them
103-
# NOTE: chainId is necessary for all EIP-1559 txns
104-
# NOTE: nonce is required for signed txns
105113

106-
nonce = w3.eth.get_transaction_count(sender.address)
107-
tx1: TxParams = {
108-
"to": receiverAddress,
109-
"value": Web3.to_wei(0.001, "ether"),
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,
110139
"gas": 21000,
111-
"maxFeePerGas": Web3.to_wei(200, "gwei"),
112-
"maxPriorityFeePerGas": Web3.to_wei(50, "gwei"),
140+
"value": Web3.to_wei(0.001, "ether"),
113141
"nonce": nonce,
114-
"chainId": NETWORK_CONFIG[NETWORK]["chain_id"],
115-
"type": 2,
142+
"maxFeePerGas": max_fee,
143+
"maxPriorityFeePerGas": max_priority_fee,
144+
"chainId": FLASHBOTS_NETWORKS[network]["chain_id"],
116145
}
117-
tx1_signed = sender.sign_transaction(tx1)
118146

119-
tx2: TxParams = {
120-
"to": receiverAddress,
121-
"value": Web3.to_wei(0.001, "ether"),
122-
"gas": 21000,
123-
"maxFeePerGas": Web3.to_wei(200, "gwei"),
124-
"maxPriorityFeePerGas": Web3.to_wei(50, "gwei"),
125-
"nonce": nonce + 1,
126-
"chainId": NETWORK_CONFIG[NETWORK]["chain_id"],
127-
"type": 2,
128-
}
129147

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)
130163
bundle = [
131164
{"signed_transaction": tx1_signed.rawTransaction},
132-
{"signer": sender, "transaction": tx2},
165+
{"transaction": tx2, "signer": sender},
133166
]
134167

135168
# keep trying to send bundle until it gets mined
136169
while True:
137170
block = w3.eth.block_number
138171

139172
# Simulation is only supported on mainnet
140-
if NETWORK == "mainnet":
141-
print(f"Simulating on block {block}")
173+
if network == "mainnet":
142174
# Simulate bundle on current block.
143175
# If your RPC provider is not fast enough, you may get "block extrapolation negative"
144176
# error message triggered by "extrapolate_timestamp" function in "flashbots.py".
145177
try:
146178
w3.flashbots.simulate(bundle, block)
147-
print("Simulation successful.")
148179
except Exception as e:
149-
print("Simulation error", e)
180+
logger.error(f"Simulation error: {e}")
150181
return
151182

152183
# send bundle targeting next block
153-
print(f"Sending bundle targeting block {block+1}")
154184
replacement_uuid = str(uuid4())
155-
print(f"replacementUuid {replacement_uuid}")
185+
logger.info(f"replacementUuid {replacement_uuid}")
156186
send_result = w3.flashbots.send_bundle(
157187
bundle,
158188
target_block_number=block + 1,
159189
opts={"replacementUuid": replacement_uuid},
160190
)
161-
print("bundleHash", w3.to_hex(send_result.bundle_hash()))
191+
logger.info(f"bundleHash {w3.to_hex(send_result.bundle_hash())}")
162192

163193
stats_v1 = w3.flashbots.get_bundle_stats(
164194
w3.to_hex(send_result.bundle_hash()), block
165195
)
166-
print("bundleStats v1", stats_v1)
196+
logger.info(f"bundleStats v1 {stats_v1}")
167197

168198
stats_v2 = w3.flashbots.get_bundle_stats_v2(
169199
w3.to_hex(send_result.bundle_hash()), block
170200
)
171-
print("bundleStats v2", stats_v2)
201+
logger.info(f"bundleStats v2 {stats_v2}")
172202

173203
send_result.wait()
174204
try:
175205
receipts = send_result.receipts()
176-
print(f"\nBundle was mined in block {receipts[0].blockNumber}\a")
206+
logger.info(f"Bundle was mined in block {receipts[0].blockNumber}")
177207
break
178208
except TransactionNotFound:
179-
print(f"Bundle not found in block {block+1}")
180-
# essentially a no-op but it shows that the function works
209+
logger.info(f"Bundle not found in block {block + 1}")
181210
cancel_res = w3.flashbots.cancel_bundles(replacement_uuid)
182-
print(f"canceled {cancel_res}")
211+
logger.info(f"Canceled {cancel_res}")
183212

184-
print(
185-
f"Sender account balance: {Web3.from_wei(w3.eth.get_balance(sender.address), 'ether')} ETH"
186-
)
187-
print(
188-
f"Receiver account balance: {Web3.from_wei(w3.eth.get_balance(receiverAddress), 'ether')} ETH"
189-
)
213+
log_account_balances(w3, sender.address, receiver)
190214

191215

192216
if __name__ == "__main__":

0 commit comments

Comments
 (0)