├── .gitignore
├── README.md
├── frontrunner-gif.gif
├── generate_key_pair.py
├── play.py
├── requirements.txt
├── settings.toml.example
├── src
├── logger
│ ├── __pycache__
│ │ └── logger.cpython-312.pyc
│ └── logger.py
└── settings
│ ├── __pycache__
│ └── settings.cpython-312.pyc
│ └── settings.py
└── terminal_example.jpg
/.gitignore:
--------------------------------------------------------------------------------
1 | settings.toml
2 | myenv/
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # FastLane Frontrunner Bot PYTHON
2 |
3 | 💡We also have a Golang version of the bot [Here](https://github.com/FastLane-Labs/break-monad-frontrunner-bot)
4 |
5 |
6 |
7 |
8 |
9 | The objective is simple, you compete as a searcher to be first to land a transaction on-chain in each new block.
10 | Your ranking is determined by your win/loss ratio weighted by number of attempts - so you get some skin in the game.
11 |
12 |
13 |
14 |
15 |
16 |
17 | ## How to run
18 |
19 | ⚠️ if you don't have Python installed, jump at Prerequisites
20 |
21 | ### Settings
22 | 1. Add your configuration:
23 | Copy settings.toml.example to settings.toml and add your private key and rpc url
24 |
25 | settings.toml
26 | ```toml
27 | [api_settings]
28 | rpc_url = 'your_rpc_url_here'
29 |
30 | [eoa]
31 | private_key = 'your_private_key_here'
32 | ```
33 |
34 | ```
35 | London: https://rpc.monad-testnet-2.fastlane.xyz/b3qFoDfY9sR44yRyOeHAfyj9dpEXVoOC
36 | Bogota: https://rpc.monad-testnet-3.fastlane.xyz/j6EsEZHfw9Iqrp7DUX5e1aLUk85d1Yzw
37 | Singapore: https://rpc.monad-testnet-5.fastlane.xyz/FFHEYATiGl2Q83xOnf71ltrqZ57q9U1W
38 | ```
39 |
40 | Contract Address:
41 | ```
42 | 0xBce2C725304e09CEf4cD7639760B67f8A0Af5bc4
43 | ```
44 |
45 | ⚠️ IMPORTANT SECURITY NOTES:
46 | - Never share your private key or commit it to version control!
47 | - Store your private key securely and keep a backup
48 |
49 | ### Run the bot
50 |
51 | ```sh
52 | python play.py
53 | ```
54 | ### Run the bot advanced mode
55 |
56 | ```sh
57 | python play.py --gas_price_gwei 60 --attempts 1 --interval 5
58 | ```
59 | If you do not enter any arguments, the bot will use the default values. If you do not enter `attempts` the bot will run indefinitely.
60 |
61 | ## Prerequisites
62 |
63 | ### 1. Install Python
64 |
65 | First, you'll need to install Python on your computer:
66 |
67 | #### Windows:
68 | 1. Download the installer from [Python's official website](https://www.python.org/downloads/)
69 | 2. Run the installer and follow the prompts
70 | 3. Open Command Prompt and verify installation:
71 | ```sh
72 | python --version
73 | ```
74 |
75 | #### Mac:
76 | Using Homebrew:
77 | ```sh
78 | brew install python
79 | ```
80 |
81 | ### 2. Install Python Dependencies
82 |
83 | ```sh
84 | pip install -r requirements.txt
85 | ```
86 | If something is missing:
87 | ```sh
88 | pip install
89 | ```
90 |
91 | ### 3. Generate a private key
92 |
93 | You can generate a private key using the following command:
94 | ```sh
95 | python generate_key_pair.py
96 | ```
97 |
98 |
99 | ## Need Help?
100 |
101 | - Ask for help in the FastLane on Monad Discord (#frontunner channel)
102 | - Talk to ChatGPT
103 | - Create an issue in this repository
104 |
105 |
106 | ## License
107 |
108 | MIT
109 |
--------------------------------------------------------------------------------
/frontrunner-gif.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FastLane-Labs/break-monad-frontrunner-bot-py/37249cb8fe8000c1be9e5787766af25bd6ccfc5e/frontrunner-gif.gif
--------------------------------------------------------------------------------
/generate_key_pair.py:
--------------------------------------------------------------------------------
1 | from web3 import Account
2 |
3 | if __name__ == "__main__":
4 | # Generate a new random account
5 |
6 | account = Account.create()
7 |
8 | # Get the private key in hexadecimal format
9 | private_key = account.key.hex()
10 |
11 | # Get the address associated with the private key
12 | address = account.address
13 |
14 | # Output the private key and address
15 | print(f"Private Key: {private_key} !DONT SHARE THIS!")
16 | print(f"Public Address: {address} <-- send Testnet MON to this address to play")
17 |
18 |
--------------------------------------------------------------------------------
/play.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import argparse
3 | import toml
4 | import time
5 |
6 | from src.settings.settings import Settings, ApiSettings, GameSettings, EOA
7 | from src.logger.logger import Logs
8 | from web3 import Web3
9 |
10 | BALANCE_THRESHOLD: float = 0.001
11 | DEFAULT_ATTEMPTS: int = 10000000
12 | GAS_LIMIT: int = 200000
13 |
14 | def play() -> None:
15 |
16 | parser = argparse.ArgumentParser(description="Break Monad Frontrunner Bot.")
17 | parser.add_argument('--gas_price_gwei', type=int, default=0, help="Set the gas price in GWEI.")
18 | parser.add_argument('--attempts', type=int, default=False, help="Number of attempts to play.")
19 | parser.add_argument('--interval', type=float, default=1, help="Delay between attempts in seconds.")
20 | args = parser.parse_args()
21 |
22 |
23 | # Initialize logger
24 | logging.basicConfig(level=logging.INFO)
25 | logger = Logs(__name__).log(level=logging.INFO)
26 |
27 | # 1. Load config
28 | config_file = toml.load('settings.toml')
29 |
30 | # 2. Parse config
31 | settings = Settings(
32 | api_settings=ApiSettings(**config_file['api_settings']),
33 | game_settings=GameSettings(**config_file['game_settings']),
34 | eoa=EOA(**config_file['eoa'])
35 | )
36 |
37 | # 3. Initialize web3 client
38 | w3 = Web3(Web3.HTTPProvider(settings.api_settings.rpc_url))
39 |
40 | # w3
41 | if not w3.is_connected():
42 | raise Exception("Failed to connect to the Ethereum network.")
43 | else:
44 | logger.info("Connected to the Monad network.")
45 |
46 | # 4. Get frontrunner contract
47 | contract = w3.eth.contract(
48 | address=w3.to_checksum_address(settings.game_settings.frontrunner_contract_address),
49 | abi=settings.game_settings.abi
50 | )
51 |
52 | DEFAULT_GAS_PRICE: int = int(w3.eth.gas_price*10**-9) if args.gas_price_gwei == 0 else int(args.gas_price_gwei)
53 |
54 | logger.info(f"Using gas price: {DEFAULT_GAS_PRICE} GWEI")
55 |
56 | # 5. Get account
57 | try:
58 | account = w3.eth.account.from_key(settings.eoa.private_key)
59 | except Exception as e:
60 | logger.error(f"Failed to get account from private key: {e}")
61 | raise e
62 |
63 | logger.info(f"Account to be used: {account.address}")
64 |
65 | # Balance ceck
66 | balance = w3.from_wei(w3.eth.get_balance(account.address), 'ether')
67 | logger.info(f"Account balance: {balance} Testnet Monad")
68 |
69 | if balance < BALANCE_THRESHOLD:
70 | logger.error("Account balance is too low to play. Please add funds to the account.")
71 | logger.warning("Exiting...")
72 | time.sleep(1)
73 | return
74 |
75 | # Score check
76 | try:
77 | wins, losses = contract.functions.getScore(account.address).call()
78 |
79 | if wins > 0 or losses > 0:
80 | logger.info(f"It looks like it's not the first time: you won {wins} times and lost {losses} times.")
81 | else:
82 | logger.info("It looks like it's the first time you play. Good luck!")
83 |
84 | except Exception as e:
85 | logger.error(f"Failed to get score: {e} - Skipping...")
86 |
87 |
88 | nonce: int = w3.eth.get_transaction_count(account.address)
89 | logger.info(f"Nonce: {nonce}")
90 | chain_id: int = w3.eth.chain_id
91 |
92 | gas_price_wei: int = w3.to_wei(DEFAULT_GAS_PRICE, 'gwei')
93 |
94 | # if attempts is 0, play
95 | if args.attempts == False:
96 | attempts = DEFAULT_ATTEMPTS
97 | else:
98 | attempts = args.attempts
99 |
100 | while True:
101 | try:
102 | # Build the transaction with the given nonce and gas price.
103 | txn = contract.functions.frontrun().build_transaction({
104 | 'chainId': chain_id,
105 | 'gas': GAS_LIMIT,
106 | 'gasPrice': gas_price_wei,
107 | 'nonce': nonce,
108 | })
109 |
110 | # Sign the transaction with the private key.
111 | signed_txn = account.sign_transaction(txn)
112 |
113 | # Send the signed transaction.
114 | tx_hash = w3.eth.send_raw_transaction(signed_txn.raw_transaction)
115 | logger.info(f"Sent transaction with nonce {nonce}. Tx hash: {tx_hash.hex()}")
116 | except Exception as e:
117 | logger.error(f"Error sending transaction with nonce {nonce}: {e}")
118 |
119 | nonce += 1
120 | time.sleep(args.interval)
121 | attempts -= 1
122 | if attempts == 0:
123 | logger.info("Attempts limit reached. Exiting...")
124 | break
125 |
126 | logger.info("All attempts have been made. Exiting...")
127 | return
128 |
129 | if __name__ == "__main__":
130 | play()
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | toml==0.10.2
2 | web3==7.8.0
--------------------------------------------------------------------------------
/settings.toml.example:
--------------------------------------------------------------------------------
1 | [api_settings]
2 | rpc_url = ''
3 |
4 | [game_settings]
5 | frontrunner_contract_address = '0xBce2C725304e09CEf4cD7639760B67f8A0Af5bc4'
6 | abi_string = '[{"type":"function","name":"frontrun","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"getScore","inputs":[{"name":"participant","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"tuple","internalType":"struct Score","components":[{"name":"wins","type":"uint128","internalType":"uint128"},{"name":"losses","type":"uint128","internalType":"uint128"}]}],"stateMutability":"view"},{"type":"function","name":"getScores","inputs":[],"outputs":[{"name":"","type":"tuple[]","internalType":"struct Frontrunner.ParticipantData[]","components":[{"name":"Address","type":"address","internalType":"address"},{"name":"Wins","type":"uint256","internalType":"uint256"},{"name":"Losses","type":"uint256","internalType":"uint256"}]}],"stateMutability":"view"}]'
7 |
8 | [eoa]
9 | private_key = ''
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/logger/__pycache__/logger.cpython-312.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FastLane-Labs/break-monad-frontrunner-bot-py/37249cb8fe8000c1be9e5787766af25bd6ccfc5e/src/logger/__pycache__/logger.cpython-312.pyc
--------------------------------------------------------------------------------
/src/logger/logger.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import os
3 | import sys
4 |
5 | this_folder = os.path.dirname(__file__)
6 | logs_position = os.path.join(this_folder, '../../test.log')
7 |
8 |
9 | class LogFormatter(logging.Formatter):
10 |
11 | # Define colors
12 | skyblue = "\x1b[38;5;117m"
13 | yellow = "\x1b[33;20m"
14 | orange = "\x1b[38;5;214m"
15 | red = "\x1b[31;20m"
16 | bold_red = "\x1b[31;1m"
17 | green = "\x1b[32m"
18 | reset = "\x1b[0m"
19 |
20 | # Log format string without function name for normal logs
21 | _format = "[%(asctime)s] | %(levelname)s | %(message)s"
22 |
23 | # Log format string with function name highlighted for error logs
24 | error_format = "[%(asctime)s] | %(levelname)s | " + bold_red + "[%(funcName)s]" + reset + ": %(message)s"
25 |
26 | # Custom level name formats with padding for alignment
27 | LEVELNAME_FORMATS = {
28 | logging.DEBUG: yellow + "%(levelname)-7s" + reset,
29 | logging.INFO: skyblue + "%(levelname)-7s" + reset,
30 | logging.WARNING: orange + "%(levelname)-7s" + reset,
31 | logging.ERROR: bold_red + "%(levelname)-7s" + reset,
32 | logging.CRITICAL: bold_red + "%(levelname)-7s" + reset
33 | }
34 |
35 | # Custom message formats
36 | MESSAGE_FORMATS = {
37 | logging.DEBUG: "%(message)s",
38 | logging.INFO: green + "%(message)s" + reset, # Apply green to INFO messages
39 | logging.WARNING: "%(message)s",
40 | logging.ERROR: "%(message)s",
41 | logging.CRITICAL: "%(message)s"
42 | }
43 |
44 | def format(self, record: logging.LogRecord) -> str:
45 | # Determine if the log is an error or critical level, then use the special format
46 | if record.levelno in (logging.ERROR, logging.CRITICAL):
47 | log_fmt = self.error_format
48 | else:
49 | log_fmt = self._format
50 |
51 | # Replace %(levelname)s and %(message)s with the colored versions
52 | log_fmt = log_fmt.replace("%(levelname)s", self.LEVELNAME_FORMATS.get(record.levelno, "%(levelname)-7s"))
53 | log_fmt = log_fmt.replace("%(message)s", self.MESSAGE_FORMATS.get(record.levelno, "%(message)s"))
54 |
55 | formatter = logging.Formatter(log_fmt, self.datefmt)
56 | return formatter.format(record)
57 |
58 | class Logs:
59 |
60 | tag: str = ""
61 |
62 | def __init__(
63 | self,
64 | loggername: str
65 | ):
66 | self.loggername = loggername
67 | self.file = logs_position
68 |
69 | def log(self, level=logging.INFO):
70 |
71 | logger = logging.getLogger(self.loggername)
72 | # Prevent adding multiple handlers to the logger
73 | if not logger.handlers:
74 | logger.setLevel(logging.INFO)
75 | handler = logging.StreamHandler(sys.stdout)
76 | handler.setFormatter(LogFormatter())
77 | logger.addHandler(handler)
78 | logger.propagate = False
79 |
80 | return logger
81 |
--------------------------------------------------------------------------------
/src/settings/__pycache__/settings.cpython-312.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FastLane-Labs/break-monad-frontrunner-bot-py/37249cb8fe8000c1be9e5787766af25bd6ccfc5e/src/settings/__pycache__/settings.cpython-312.pyc
--------------------------------------------------------------------------------
/src/settings/settings.py:
--------------------------------------------------------------------------------
1 | from dataclasses import dataclass
2 | import json
3 |
4 | @dataclass
5 | class ApiSettings:
6 | rpc_url: str
7 |
8 | @dataclass
9 | class GameSettings:
10 | frontrunner_contract_address: str
11 | abi_string: str
12 |
13 | def __post_init__(self):
14 | self.abi = self._parse_abi_string(self.abi_string)
15 |
16 | def _parse_abi_string(self, abi_string):
17 | """
18 | Parses a JSON-formatted ABI string into a Python object.
19 |
20 | :param abi_string: str, a JSON string representing the contract's ABI.
21 | :return: The parsed ABI (typically a list of dictionaries).
22 | :raises ValueError: If the ABI string is not valid JSON.
23 | """
24 | try:
25 | abi = json.loads(abi_string)
26 | except json.JSONDecodeError as e:
27 | raise ValueError("Failed to decode ABI string. Please ensure it is valid JSON.") from e
28 | return abi
29 |
30 | @dataclass
31 | class EOA:
32 | private_key: str
33 |
34 |
35 | @dataclass
36 | class Settings:
37 | api_settings: ApiSettings
38 | game_settings: GameSettings
39 | eoa: EOA
40 |
41 |
42 |
--------------------------------------------------------------------------------
/terminal_example.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FastLane-Labs/break-monad-frontrunner-bot-py/37249cb8fe8000c1be9e5787766af25bd6ccfc5e/terminal_example.jpg
--------------------------------------------------------------------------------