|
2 | 2 | Minimal viable example of flashbots usage with dynamic fee transactions. |
3 | 3 | Sends a bundle of two transactions which transfer some ETH into a random account. |
4 | 4 |
|
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 | | -
|
16 | 5 | Environment Variables: |
17 | 6 | - 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. |
19 | 8 | - This account is only used for reputation on flashbots and should be empty. |
20 | 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 <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 |
21 | 21 | """ |
22 | 22 |
|
| 23 | +import argparse |
| 24 | +import logging |
23 | 25 | import os |
24 | 26 | import secrets |
| 27 | +from enum import Enum |
25 | 28 | from uuid import uuid4 |
26 | 29 |
|
27 | 30 | from eth_account.account import Account |
|
30 | 33 | from web3.exceptions import TransactionNotFound |
31 | 34 | from web3.types import TxParams |
32 | 35 |
|
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 |
56 | 82 |
|
57 | 83 |
|
58 | 84 | 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 |
60 | 89 |
|
61 | 90 |
|
62 | 91 | def random_account() -> LocalAccount: |
63 | 92 | key = "0x" + secrets.token_hex(32) |
64 | 93 | return Account.from_key(key) |
65 | 94 |
|
66 | 95 |
|
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"] |
97 | 103 | ) |
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, |
100 | 110 | ) |
| 111 | + return w3 |
101 | 112 |
|
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 |
105 | 113 |
|
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, |
110 | 139 | "gas": 21000, |
111 | | - "maxFeePerGas": Web3.to_wei(200, "gwei"), |
112 | | - "maxPriorityFeePerGas": Web3.to_wei(50, "gwei"), |
| 140 | + "value": Web3.to_wei(0.001, "ether"), |
113 | 141 | "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"], |
116 | 145 | } |
117 | | - tx1_signed = sender.sign_transaction(tx1) |
118 | 146 |
|
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 | | - } |
129 | 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) |
130 | 163 | bundle = [ |
131 | 164 | {"signed_transaction": tx1_signed.rawTransaction}, |
132 | | - {"signer": sender, "transaction": tx2}, |
| 165 | + {"transaction": tx2, "signer": sender}, |
133 | 166 | ] |
134 | 167 |
|
135 | 168 | # keep trying to send bundle until it gets mined |
136 | 169 | while True: |
137 | 170 | block = w3.eth.block_number |
138 | 171 |
|
139 | 172 | # Simulation is only supported on mainnet |
140 | | - if NETWORK == "mainnet": |
141 | | - print(f"Simulating on block {block}") |
| 173 | + if network == "mainnet": |
142 | 174 | # Simulate bundle on current block. |
143 | 175 | # If your RPC provider is not fast enough, you may get "block extrapolation negative" |
144 | 176 | # error message triggered by "extrapolate_timestamp" function in "flashbots.py". |
145 | 177 | try: |
146 | 178 | w3.flashbots.simulate(bundle, block) |
147 | | - print("Simulation successful.") |
148 | 179 | except Exception as e: |
149 | | - print("Simulation error", e) |
| 180 | + logger.error(f"Simulation error: {e}") |
150 | 181 | return |
151 | 182 |
|
152 | 183 | # send bundle targeting next block |
153 | | - print(f"Sending bundle targeting block {block+1}") |
154 | 184 | replacement_uuid = str(uuid4()) |
155 | | - print(f"replacementUuid {replacement_uuid}") |
| 185 | + logger.info(f"replacementUuid {replacement_uuid}") |
156 | 186 | send_result = w3.flashbots.send_bundle( |
157 | 187 | bundle, |
158 | 188 | target_block_number=block + 1, |
159 | 189 | opts={"replacementUuid": replacement_uuid}, |
160 | 190 | ) |
161 | | - print("bundleHash", w3.to_hex(send_result.bundle_hash())) |
| 191 | + logger.info(f"bundleHash {w3.to_hex(send_result.bundle_hash())}") |
162 | 192 |
|
163 | 193 | stats_v1 = w3.flashbots.get_bundle_stats( |
164 | 194 | w3.to_hex(send_result.bundle_hash()), block |
165 | 195 | ) |
166 | | - print("bundleStats v1", stats_v1) |
| 196 | + logger.info(f"bundleStats v1 {stats_v1}") |
167 | 197 |
|
168 | 198 | stats_v2 = w3.flashbots.get_bundle_stats_v2( |
169 | 199 | w3.to_hex(send_result.bundle_hash()), block |
170 | 200 | ) |
171 | | - print("bundleStats v2", stats_v2) |
| 201 | + logger.info(f"bundleStats v2 {stats_v2}") |
172 | 202 |
|
173 | 203 | send_result.wait() |
174 | 204 | try: |
175 | 205 | 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}") |
177 | 207 | break |
178 | 208 | 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}") |
181 | 210 | cancel_res = w3.flashbots.cancel_bundles(replacement_uuid) |
182 | | - print(f"canceled {cancel_res}") |
| 211 | + logger.info(f"Canceled {cancel_res}") |
183 | 212 |
|
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) |
190 | 214 |
|
191 | 215 |
|
192 | 216 | if __name__ == "__main__": |
|
0 commit comments