├── .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 | Frontrunner Game Animation 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 | terminal example 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 --------------------------------------------------------------------------------